初始化回测核心引擎骨架

This commit is contained in:
zsb
2026-04-06 23:56:37 -07:00
commit 334864cbc5
25 changed files with 2878 additions and 0 deletions

View 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())
}
}