修复next open决策执行日映射
This commit is contained in:
@@ -1652,7 +1652,7 @@ where
|
|||||||
let mut portfolio = PortfolioState::new(self.config.initial_cash);
|
let mut portfolio = PortfolioState::new(self.config.initial_cash);
|
||||||
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 execution_dates = self
|
let calendar_dates = self
|
||||||
.data
|
.data
|
||||||
.calendar()
|
.calendar()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1663,11 +1663,41 @@ where
|
|||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
})
|
})
|
||||||
.filter(|date| self.config.end_date.map(|end| *date <= end).unwrap_or(true))
|
.filter(|date| self.config.end_date.map(|end| *date <= end).unwrap_or(true))
|
||||||
.filter(|date| {
|
|
||||||
!self.data.factor_snapshots_on(*date).is_empty()
|
|
||||||
&& !self.data.candidate_snapshots_on(*date).is_empty()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let has_decision_inputs = |date: NaiveDate| {
|
||||||
|
!self.data.factor_snapshots_on(date).is_empty()
|
||||||
|
&& !self.data.candidate_snapshots_on(date).is_empty()
|
||||||
|
};
|
||||||
|
let has_execution_market =
|
||||||
|
|date: NaiveDate| !self.data.market_snapshots_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
|
||||||
@@ -1757,9 +1787,7 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
);
|
);
|
||||||
|
|
||||||
let decision_slot = execution_idx
|
let decision_slot = decision_slots.get(execution_idx).copied().flatten();
|
||||||
.checked_sub(self.config.decision_lag_trading_days)
|
|
||||||
.map(|decision_idx| (decision_idx, execution_dates[decision_idx]));
|
|
||||||
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();
|
||||||
@@ -4858,6 +4886,52 @@ mod tests {
|
|||||||
assert_eq!(result.fills[0].price, 12.0);
|
assert_eq!(result.fills[0].price, 12.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn next_bar_open_executes_last_decision_on_market_only_execution_day() {
|
||||||
|
let first = d(2025, 1, 2);
|
||||||
|
let second = d(2025, 1, 3);
|
||||||
|
let third = d(2025, 1, 6);
|
||||||
|
let dataset = DataSet::from_components(
|
||||||
|
vec![default_instrument()],
|
||||||
|
vec![
|
||||||
|
market(first, 10.0, 11.5),
|
||||||
|
market(second, 12.0, 13.0),
|
||||||
|
market(third, 14.0, 15.0),
|
||||||
|
],
|
||||||
|
vec![factor(first), factor(second)],
|
||||||
|
vec![candidate(first), candidate(second)],
|
||||||
|
vec![benchmark(first), benchmark(second), benchmark(third)],
|
||||||
|
)
|
||||||
|
.expect("dataset");
|
||||||
|
let broker = scheduled_next_open_broker(FidcRiskControlConfig::default());
|
||||||
|
let config = BacktestConfig {
|
||||||
|
initial_cash: 100_000.0,
|
||||||
|
benchmark_code: "000852.SH".to_string(),
|
||||||
|
start_date: Some(first),
|
||||||
|
end_date: Some(third),
|
||||||
|
decision_lag_trading_days: 1,
|
||||||
|
execution_price_field: PriceField::Open,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = BacktestEngine::new(
|
||||||
|
dataset,
|
||||||
|
ScheduledBuyStrategy {
|
||||||
|
rule: ScheduleRule::weekly_by_weekday("weekly_signal", 5, ScheduleStage::OnDay),
|
||||||
|
expected_decision_date: second,
|
||||||
|
},
|
||||||
|
broker,
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("backtest run");
|
||||||
|
|
||||||
|
assert_eq!(result.fills.len(), 1);
|
||||||
|
assert_eq!(result.fills[0].date, third);
|
||||||
|
assert_eq!(result.fills[0].decision_date, Some(second));
|
||||||
|
assert_eq!(result.fills[0].execution_date, Some(third));
|
||||||
|
assert_eq!(result.fills[0].price, 14.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_bar_open_strategy_context_data_helpers_use_decision_date() {
|
fn next_bar_open_strategy_context_data_helpers_use_decision_date() {
|
||||||
let first = d(2025, 1, 2);
|
let first = d(2025, 1, 2);
|
||||||
|
|||||||
Reference in New Issue
Block a user