初始化回测核心引擎骨架
This commit is contained in:
167
crates/fidc-core/src/engine.rs
Normal file
167
crates/fidc-core/src/engine.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use chrono::NaiveDate;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::broker::{BrokerExecutionReport, BrokerSimulator};
|
||||
use crate::cost::CostModel;
|
||||
use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField};
|
||||
use crate::events::{AccountEvent, FillEvent, OrderEvent, PositionEvent};
|
||||
use crate::portfolio::{HoldingSummary, PortfolioState};
|
||||
use crate::rules::EquityRuleHooks;
|
||||
use crate::strategy::{Strategy, StrategyContext, StrategyDecision};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BacktestError {
|
||||
#[error(transparent)]
|
||||
Data(#[from] DataSetError),
|
||||
#[error("missing {field} price for {symbol} on {date}")]
|
||||
MissingPrice {
|
||||
date: NaiveDate,
|
||||
symbol: String,
|
||||
field: &'static str,
|
||||
},
|
||||
#[error("benchmark snapshot missing for {date}")]
|
||||
MissingBenchmark { date: NaiveDate },
|
||||
#[error("{0}")]
|
||||
Execution(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BacktestConfig {
|
||||
pub initial_cash: f64,
|
||||
pub benchmark_code: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DailyEquityPoint {
|
||||
#[serde(with = "date_format")]
|
||||
pub date: NaiveDate,
|
||||
pub cash: f64,
|
||||
pub market_value: f64,
|
||||
pub total_equity: f64,
|
||||
pub benchmark_close: f64,
|
||||
pub notes: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BacktestResult {
|
||||
pub strategy_name: String,
|
||||
pub equity_curve: Vec<DailyEquityPoint>,
|
||||
pub benchmark_series: Vec<BenchmarkSnapshot>,
|
||||
pub order_events: Vec<OrderEvent>,
|
||||
pub fills: Vec<FillEvent>,
|
||||
pub position_events: Vec<PositionEvent>,
|
||||
pub account_events: Vec<AccountEvent>,
|
||||
pub holdings_summary: Vec<HoldingSummary>,
|
||||
}
|
||||
|
||||
pub struct BacktestEngine<S, C, R> {
|
||||
data: DataSet,
|
||||
strategy: S,
|
||||
broker: BrokerSimulator<C, R>,
|
||||
config: BacktestConfig,
|
||||
}
|
||||
|
||||
impl<S, C, R> BacktestEngine<S, C, R> {
|
||||
pub fn new(
|
||||
data: DataSet,
|
||||
strategy: S,
|
||||
broker: BrokerSimulator<C, R>,
|
||||
config: BacktestConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
data,
|
||||
strategy,
|
||||
broker,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, C, R> BacktestEngine<S, C, R>
|
||||
where
|
||||
S: Strategy,
|
||||
C: CostModel,
|
||||
R: EquityRuleHooks,
|
||||
{
|
||||
pub fn run(&mut self) -> Result<BacktestResult, BacktestError> {
|
||||
let mut portfolio = PortfolioState::new(self.config.initial_cash);
|
||||
let mut result = BacktestResult {
|
||||
strategy_name: self.strategy.name().to_string(),
|
||||
benchmark_series: self.data.benchmark_series(),
|
||||
order_events: Vec::new(),
|
||||
fills: Vec::new(),
|
||||
position_events: Vec::new(),
|
||||
account_events: Vec::new(),
|
||||
equity_curve: Vec::new(),
|
||||
holdings_summary: Vec::new(),
|
||||
};
|
||||
|
||||
for execution_date in self.data.calendar().iter() {
|
||||
let decision = match self.data.calendar().previous_day(execution_date) {
|
||||
Some(decision_date) => {
|
||||
let decision_index = self.data.calendar().index_of(decision_date).unwrap_or(0);
|
||||
self.strategy.on_day(&StrategyContext {
|
||||
execution_date,
|
||||
decision_date,
|
||||
decision_index,
|
||||
data: &self.data,
|
||||
portfolio: &portfolio,
|
||||
})?
|
||||
}
|
||||
None => StrategyDecision::default(),
|
||||
};
|
||||
|
||||
let report = self
|
||||
.broker
|
||||
.execute(execution_date, &mut portfolio, &self.data, &decision)?;
|
||||
self.extend_result(&mut result, report);
|
||||
|
||||
portfolio.update_prices(execution_date, &self.data, PriceField::Close)?;
|
||||
|
||||
let benchmark = self
|
||||
.data
|
||||
.benchmark(execution_date)
|
||||
.ok_or(BacktestError::MissingBenchmark {
|
||||
date: execution_date,
|
||||
})?;
|
||||
let notes = decision.notes.join(" | ");
|
||||
|
||||
result.equity_curve.push(DailyEquityPoint {
|
||||
date: execution_date,
|
||||
cash: portfolio.cash(),
|
||||
market_value: portfolio.market_value(),
|
||||
total_equity: portfolio.total_equity(),
|
||||
benchmark_close: benchmark.close,
|
||||
notes,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(last_date) = self.data.calendar().days().last().copied() {
|
||||
result.holdings_summary = portfolio.holdings_summary(last_date);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn extend_result(&self, result: &mut BacktestResult, report: BrokerExecutionReport) {
|
||||
result.order_events.extend(report.order_events);
|
||||
result.fills.extend(report.fill_events);
|
||||
result.position_events.extend(report.position_events);
|
||||
result.account_events.extend(report.account_events);
|
||||
}
|
||||
}
|
||||
|
||||
mod date_format {
|
||||
use chrono::NaiveDate;
|
||||
use serde::Serializer;
|
||||
|
||||
const FORMAT: &str = "%Y-%m-%d";
|
||||
|
||||
pub fn serialize<S>(date: &NaiveDate, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&date.format(FORMAT).to_string())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user