fix: align risk-free dates with engine schedule
This commit is contained in:
@@ -458,6 +458,60 @@ pub struct BacktestEngine<S, C, R> {
|
|||||||
risk_free_rate_contract: Option<RiskFreeRateContract>,
|
risk_free_rate_contract: Option<RiskFreeRateContract>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn backtest_execution_schedule(
|
||||||
|
data: &DataSet,
|
||||||
|
start_date: Option<NaiveDate>,
|
||||||
|
end_date: Option<NaiveDate>,
|
||||||
|
decision_lag_trading_days: usize,
|
||||||
|
) -> Vec<(NaiveDate, Option<(usize, NaiveDate)>)> {
|
||||||
|
let calendar_dates = data
|
||||||
|
.calendar()
|
||||||
|
.iter()
|
||||||
|
.filter(|date| start_date.map(|start| *date >= start).unwrap_or(true))
|
||||||
|
.filter(|date| end_date.map(|end| *date <= end).unwrap_or(true))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let has_decision_inputs = |date: NaiveDate| {
|
||||||
|
!data.factor_snapshot_rows_on(date).is_empty()
|
||||||
|
&& !data.candidate_snapshot_rows_on(date).is_empty()
|
||||||
|
};
|
||||||
|
let has_execution_market = |date: NaiveDate| !data.market_snapshot_rows_on(date).is_empty();
|
||||||
|
let mut schedule = Vec::new();
|
||||||
|
for (calendar_idx, execution_date) in calendar_dates.iter().copied().enumerate() {
|
||||||
|
if decision_lag_trading_days == 0 {
|
||||||
|
if has_decision_inputs(execution_date) {
|
||||||
|
schedule.push((execution_date, Some((calendar_idx, execution_date))));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !has_execution_market(execution_date) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let decision_slot = calendar_idx
|
||||||
|
.checked_sub(decision_lag_trading_days)
|
||||||
|
.map(|decision_idx| (decision_idx, calendar_dates[decision_idx]));
|
||||||
|
match decision_slot {
|
||||||
|
Some((_, decision_date)) if has_decision_inputs(decision_date) => {
|
||||||
|
schedule.push((execution_date, decision_slot));
|
||||||
|
}
|
||||||
|
None => schedule.push((execution_date, None)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backtest_execution_dates(
|
||||||
|
data: &DataSet,
|
||||||
|
start_date: Option<NaiveDate>,
|
||||||
|
end_date: Option<NaiveDate>,
|
||||||
|
decision_lag_trading_days: usize,
|
||||||
|
) -> Vec<NaiveDate> {
|
||||||
|
backtest_execution_schedule(data, start_date, end_date, decision_lag_trading_days)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(execution_date, _)| execution_date)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl<S, C, R> BacktestEngine<S, C, R> {
|
impl<S, C, R> BacktestEngine<S, C, R> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
data: DataSet,
|
data: DataSet,
|
||||||
@@ -1977,52 +2031,16 @@ where
|
|||||||
self.subscriptions = self.strategy.initial_subscriptions();
|
self.subscriptions = self.strategy.initial_subscriptions();
|
||||||
let scheduler_calendar = self.data.calendar().clone();
|
let scheduler_calendar = self.data.calendar().clone();
|
||||||
let scheduler = Scheduler::new(&scheduler_calendar);
|
let scheduler = Scheduler::new(&scheduler_calendar);
|
||||||
let calendar_dates = self
|
let execution_schedule = backtest_execution_schedule(
|
||||||
.data
|
&self.data,
|
||||||
.calendar()
|
self.config.start_date,
|
||||||
|
self.config.end_date,
|
||||||
|
self.config.decision_lag_trading_days,
|
||||||
|
);
|
||||||
|
let execution_dates = execution_schedule
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|date| {
|
.map(|(execution_date, _)| *execution_date)
|
||||||
self.config
|
|
||||||
.start_date
|
|
||||||
.map(|start| *date >= start)
|
|
||||||
.unwrap_or(true)
|
|
||||||
})
|
|
||||||
.filter(|date| self.config.end_date.map(|end| *date <= end).unwrap_or(true))
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let has_decision_inputs = |date: NaiveDate| {
|
|
||||||
!self.data.factor_snapshot_rows_on(date).is_empty()
|
|
||||||
&& !self.data.candidate_snapshot_rows_on(date).is_empty()
|
|
||||||
};
|
|
||||||
let has_execution_market =
|
|
||||||
|date: NaiveDate| !self.data.market_snapshot_rows_on(date).is_empty();
|
|
||||||
let mut execution_dates = Vec::new();
|
|
||||||
let mut decision_slots = Vec::new();
|
|
||||||
for (calendar_idx, execution_date) in calendar_dates.iter().copied().enumerate() {
|
|
||||||
if self.config.decision_lag_trading_days == 0 {
|
|
||||||
if has_decision_inputs(execution_date) {
|
|
||||||
execution_dates.push(execution_date);
|
|
||||||
decision_slots.push(Some((calendar_idx, execution_date)));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !has_execution_market(execution_date) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let decision_slot = calendar_idx
|
|
||||||
.checked_sub(self.config.decision_lag_trading_days)
|
|
||||||
.map(|decision_idx| (decision_idx, calendar_dates[decision_idx]));
|
|
||||||
match decision_slot {
|
|
||||||
Some((_, decision_date)) if has_decision_inputs(decision_date) => {
|
|
||||||
execution_dates.push(execution_date);
|
|
||||||
decision_slots.push(decision_slot);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
execution_dates.push(execution_date);
|
|
||||||
decision_slots.push(None);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut result = BacktestResult {
|
let mut result = BacktestResult {
|
||||||
strategy_name: self.strategy.name().to_string(),
|
strategy_name: self.strategy.name().to_string(),
|
||||||
benchmark_series: self
|
benchmark_series: self
|
||||||
@@ -2117,7 +2135,9 @@ where
|
|||||||
let day_order_start = result.order_events.len();
|
let day_order_start = result.order_events.len();
|
||||||
let day_fill_start = result.fills.len();
|
let day_fill_start = result.fills.len();
|
||||||
|
|
||||||
let decision_slot = decision_slots.get(execution_idx).copied().flatten();
|
let decision_slot = execution_schedule
|
||||||
|
.get(execution_idx)
|
||||||
|
.and_then(|(_, decision_slot)| *decision_slot);
|
||||||
let Some((decision_index, decision_date)) = decision_slot else {
|
let Some((decision_index, decision_date)) = decision_slot else {
|
||||||
let mut process_events = Vec::new();
|
let mut process_events = Vec::new();
|
||||||
let mut report = BrokerExecutionReport::default();
|
let mut report = BrokerExecutionReport::default();
|
||||||
@@ -5452,6 +5472,28 @@ mod tests {
|
|||||||
.expect("dataset")
|
.expect("dataset")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backtest_execution_dates_match_sparse_lagged_equity_schedule() {
|
||||||
|
let dates = [d(2025, 1, 2), d(2025, 1, 3), d(2025, 1, 6), d(2025, 1, 7)];
|
||||||
|
let data = DataSet::from_components(
|
||||||
|
vec![default_instrument()],
|
||||||
|
dates.iter().map(|date| market(*date, 10.0, 10.0)).collect(),
|
||||||
|
vec![factor(dates[0]), factor(dates[2])],
|
||||||
|
vec![candidate(dates[0]), candidate(dates[2])],
|
||||||
|
dates.iter().map(|date| benchmark(*date)).collect(),
|
||||||
|
)
|
||||||
|
.expect("sparse lagged dataset");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
super::backtest_execution_dates(&data, Some(dates[0]), Some(dates[3]), 1,),
|
||||||
|
vec![dates[0], dates[1], dates[3]]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::backtest_execution_dates(&data, Some(dates[0]), Some(dates[3]), 0,),
|
||||||
|
vec![dates[0], dates[2]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn engine_with_matching(
|
fn engine_with_matching(
|
||||||
matching_type: MatchingType,
|
matching_type: MatchingType,
|
||||||
execution_price_field: PriceField,
|
execution_price_field: PriceField,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub use engine::{
|
|||||||
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
|
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
|
||||||
BacktestResult, BacktestTerminalAssetClass, BacktestTerminalAudit, BacktestTerminalOpenOrder,
|
BacktestResult, BacktestTerminalAssetClass, BacktestTerminalAudit, BacktestTerminalOpenOrder,
|
||||||
BacktestTerminalStatus, DailyEquityPoint, ExecutionQuoteRequest, FuturesValidationConfig,
|
BacktestTerminalStatus, DailyEquityPoint, ExecutionQuoteRequest, FuturesValidationConfig,
|
||||||
ProcessEventRetention,
|
ProcessEventRetention, backtest_execution_dates,
|
||||||
};
|
};
|
||||||
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
||||||
pub use events::{
|
pub use events::{
|
||||||
|
|||||||
Reference in New Issue
Block a user