From 5481db63df10f30d3938fb1e614780af262d3e73 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 4 Jul 2026 03:05:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A1=A8=E8=BE=BE=E5=BC=8F?= =?UTF-8?q?=E7=AD=96=E7=95=A5next-open=E9=A3=8E=E6=8E=A7=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 191 +++++++++++------- 1 file changed, 115 insertions(+), 76 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 82c76b4..d1a1029 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -4915,6 +4915,7 @@ impl PlatformExprStrategy { }; let selection_date = if self.config.aiquant_transaction_cost && self.config.intraday_execution_time.is_some() + && !ctx.is_lagged_execution() { ctx.execution_date } else { @@ -5756,9 +5757,10 @@ impl PlatformExprStrategy { &self, ctx: &StrategyContext<'_>, ) -> Result { - let day = self.day_state(ctx, ctx.execution_date)?; + let decision_date = ctx.decision_date; + let day = self.day_state(ctx, decision_date)?; let (order_intents, action_diagnostics) = - self.explicit_action_intents(ctx, ctx.execution_date, &day)?; + self.explicit_action_intents(ctx, decision_date, &day)?; let mut diagnostics = vec![format!( "platform_expr signal={} last={:.2} explicit_actions={} stage={}", self.config.signal_symbol, @@ -5921,9 +5923,10 @@ impl PlatformExprStrategy { let Some(market) = ctx.data.market(date, &factor.symbol) else { continue; }; - if self - .selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market) - .is_some() + if !ctx.is_lagged_execution() + && self + .selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market) + .is_some() { continue; } @@ -5956,6 +5959,9 @@ impl PlatformExprStrategy { date: NaiveDate, factor_date: NaiveDate, ) -> Vec { + if ctx.is_lagged_execution() { + return Vec::new(); + } let mut decisions = Vec::new(); for factor in ctx.data.factor_snapshots_on(factor_date) { if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) { @@ -6401,13 +6407,18 @@ impl PlatformExprStrategy { let mut selected = Vec::new(); for (candidate, stock, _) in candidates { - if let Some(reason) = self.stock_selection_limit_rejection_reason(&stock) { + if !ctx.is_lagged_execution() + && let Some(reason) = self.stock_selection_limit_rejection_reason(&stock) + { if diagnostics.len() < 12 { diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason)); } continue; } - if let Some(reason) = self.buy_rejection_reason(ctx, date, &candidate.symbol, &stock)? { + if !ctx.is_lagged_execution() + && let Some(reason) = + self.buy_rejection_reason(ctx, date, &candidate.symbol, &stock)? + { if diagnostics.len() < 12 { diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason)); } @@ -7083,13 +7094,17 @@ impl PlatformExprStrategy { if quote_candidate_symbols.len() < quote_candidate_limit { quote_candidate_symbols.push(symbol.clone()); } - if let Some(reason) = self.stock_selection_limit_rejection_reason(stock) { + if !ctx.is_lagged_execution() + && let Some(reason) = self.stock_selection_limit_rejection_reason(stock) + { if diagnostics.len() < 12 { diagnostics.push(format!("{symbol} quote_plan rejected by {reason}")); } continue; } - if let Some(reason) = self.buy_rejection_reason(ctx, date, symbol, &stock)? { + if !ctx.is_lagged_execution() + && let Some(reason) = self.buy_rejection_reason(ctx, date, symbol, &stock)? + { if diagnostics.len() < 12 { diagnostics.push(format!("{symbol} quote_plan rejected by {reason}")); } @@ -7123,11 +7138,11 @@ impl PlatformExprStrategy { ctx: &StrategyContext<'_>, _scope_limit: usize, ) -> Result { - if !self.config.rotation_enabled || self.config.in_skip_window(ctx.execution_date) { + if !self.config.rotation_enabled || self.config.in_skip_window(ctx.decision_date) { return Ok(PlatformSelectionQuotePlan { execution_date: ctx.execution_date, decision_date: ctx.decision_date, - selection_date: ctx.execution_date, + selection_date: ctx.decision_date, factor_date: ctx.decision_date, requires_intraday_selection_quotes: false, band_low: 0.0, @@ -7178,21 +7193,21 @@ impl PlatformExprStrategy { fn stop_take_action( &self, ctx: &StrategyContext<'_>, - _signal_date: NaiveDate, - execution_date: NaiveDate, + signal_date: NaiveDate, + _execution_date: NaiveDate, day: &DayExpressionState, symbol: &str, ) -> Result<(bool, bool), BacktestError> { let Some(position) = ctx.portfolio.position(symbol) else { return Ok((false, false)); }; - self.stop_take_action_for_position(ctx, execution_date, day, position) + self.stop_take_action_for_position(ctx, signal_date, day, position) } fn stop_take_action_for_position( &self, ctx: &StrategyContext<'_>, - execution_date: NaiveDate, + signal_date: NaiveDate, day: &DayExpressionState, position: &crate::portfolio::Position, ) -> Result<(bool, bool), BacktestError> { @@ -7219,7 +7234,7 @@ impl PlatformExprStrategy { } let stock = match self.stock_state_at_time( ctx, - execution_date, + signal_date, symbol, Some(self.intraday_execution_start_time()), ) { @@ -7249,9 +7264,9 @@ impl PlatformExprStrategy { prev_close: stock.prev_close, holding_return, quantity: position.quantity as i64, - sellable_qty: position.sellable_qty(execution_date) as i64, - sellable: position.sellable_qty(execution_date) as i64, - closable: position.sellable_qty(execution_date) as i64, + sellable_qty: position.sellable_qty(signal_date) as i64, + sellable: position.sellable_qty(signal_date) as i64, + closable: position.sellable_qty(signal_date) as i64, old_quantity: position.day_start_quantity() as i64, bought_quantity: position.bought_quantity() as i64, sold_quantity: position.sold_quantity() as i64, @@ -7352,7 +7367,7 @@ impl Strategy for PlatformExprStrategy { .filter(|symbol| !symbol.trim().is_empty()) .cloned() .collect::>(); - if self.config.rotation_enabled && !self.config.in_skip_window(ctx.execution_date) { + if self.config.rotation_enabled && !self.config.in_skip_window(ctx.decision_date) { let plan = self.selection_quote_plan(ctx, 0)?; symbols.extend(plan.order_symbols); symbols.extend(plan.candidate_symbols); @@ -7366,7 +7381,7 @@ impl Strategy for PlatformExprStrategy { ) -> Result { if self.config.explicit_action_stage == PlatformExplicitActionStage::OpenAuction && !self.config.explicit_actions.is_empty() - && self.explicit_actions_active(ctx.data.calendar(), ctx.execution_date) + && self.explicit_actions_active(ctx.data.calendar(), ctx.decision_date) { return self.explicit_action_decision(ctx); } @@ -7376,14 +7391,21 @@ impl Strategy for PlatformExprStrategy { fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result { let execution_date = ctx.execution_date; let decision_date = ctx.decision_date; - let in_skip_window = self.config.in_skip_window(execution_date); + let defer_execution_risk = ctx.is_lagged_execution(); + let signal_date = decision_date; + let projection_date = if defer_execution_risk { + signal_date + } else { + execution_date + }; + let in_skip_window = self.config.in_skip_window(signal_date); let day = self.day_state(ctx, decision_date)?; let (selection_market_date, selection_universe_factor_date, selection_factor_date) = self.selection_dates(ctx); let (explicit_action_intents, explicit_action_diagnostics) = if !in_skip_window && self.config.explicit_action_stage == PlatformExplicitActionStage::OnDay - && self.explicit_actions_active(ctx.data.calendar(), execution_date) + && self.explicit_actions_active(ctx.data.calendar(), signal_date) { self.explicit_action_intents(ctx, decision_date, &day)? } else { @@ -7398,7 +7420,7 @@ impl Strategy for PlatformExprStrategy { let previous_trading_ratio = self.last_trading_ratio.unwrap_or(1.0); let weak_market_shrink_due = trading_ratio.is_finite() && trading_ratio < previous_trading_ratio - 1e-9; - let marked_total_value = self.marked_total_value(ctx, execution_date); + let marked_total_value = self.marked_total_value(ctx, projection_date); let mut aiquant_total_value = if marked_total_value.is_finite() && marked_total_value > 0.0 { marked_total_value @@ -7445,14 +7467,14 @@ impl Strategy for PlatformExprStrategy { } else if let Some(schedule) = &self.config.rebalance_schedule { schedule.matches( ctx.data.calendar(), - execution_date, + signal_date, ScheduleStage::OnDay, default_stage_time(ScheduleStage::OnDay), ) || empty_rebalance_retry } else if self.config.calendar_rebalance_interval { self.last_rebalance_date .map(|last| { - execution_date.signed_duration_since(last).num_days() + signal_date.signed_duration_since(last).num_days() >= effective_refresh_rate as i64 }) .unwrap_or(true) @@ -7489,7 +7511,7 @@ impl Strategy for PlatformExprStrategy { } match self.stock_is_at_upper_limit_at_time( ctx, - execution_date, + projection_date, &position.symbol, delayed_limit_exit_time, )? { @@ -7517,7 +7539,7 @@ impl Strategy for PlatformExprStrategy { if self .aiquant_scheduled_quote_at_time( ctx, - execution_date, + projection_date, &symbol, Some(delayed_limit_exit_time), ) @@ -7527,7 +7549,7 @@ impl Strategy for PlatformExprStrategy { } let stock = match self.stock_state_at_time( ctx, - execution_date, + projection_date, &symbol, Some(delayed_limit_exit_time), ) { @@ -7559,7 +7581,7 @@ impl Strategy for PlatformExprStrategy { }); let projected_sold = if ctx .data - .execution_quotes_on(execution_date, &symbol) + .execution_quotes_on(projection_date, &symbol) .is_empty() { false @@ -7567,7 +7589,7 @@ impl Strategy for PlatformExprStrategy { self.project_target_zero_at_time( ctx, &mut projected, - execution_date, + projection_date, &symbol, &mut projected_execution_state, Some(delayed_limit_exit_time), @@ -7583,7 +7605,7 @@ impl Strategy for PlatformExprStrategy { } if !delayed_sold_symbols.is_empty() { let projected_total = - self.marked_total_value_for_portfolio(ctx, &projected, execution_date); + self.marked_total_value_for_portfolio(ctx, &projected, projection_date); if projected_total.is_finite() && projected_total > 0.0 { aiquant_total_value = projected_total; } @@ -7608,7 +7630,7 @@ impl Strategy for PlatformExprStrategy { reason: "seasonal_stop_window".to_string(), }); } - let mut notes = vec![format!("seasonal stop window on {}", execution_date)]; + let mut notes = vec![format!("seasonal stop window on {}", signal_date)]; if !delayed_sold_symbols.is_empty() { notes.push(format!( "delayed limit open sells before seasonal stop: {}", @@ -7659,7 +7681,7 @@ impl Strategy for PlatformExprStrategy { } if self.regular_sell_should_wait_due_to_highlimit( ctx, - execution_date, + projection_date, &position.symbol, self.intraday_execution_start_time(), )? { @@ -7667,7 +7689,7 @@ impl Strategy for PlatformExprStrategy { } if self.candidate_requires_risk_level_forced_exit( ctx, - execution_date, + projection_date, &position.symbol, risk_level_forced_exit_time, )? { @@ -7676,7 +7698,7 @@ impl Strategy for PlatformExprStrategy { continue; } let (stop_hit, profit_hit) = - self.stop_take_action_for_position(ctx, execution_date, &day, position)?; + self.stop_take_action_for_position(ctx, signal_date, &day, position)?; if stop_hit { pending_full_close_symbols.insert(position.symbol.clone()); continue; @@ -7685,14 +7707,15 @@ impl Strategy for PlatformExprStrategy { pending_full_close_symbols.insert(position.symbol.clone()); continue; } - let can_sell = self.can_sell_position(ctx, execution_date, &position.symbol); + let can_sell = defer_execution_risk + || self.can_sell_position(ctx, execution_date, &position.symbol); if !can_sell { continue; } if self.config.delayed_limit_open_exit_enabled { let stock = match self.stock_state_at_time( ctx, - execution_date, + projection_date, &position.symbol, Some(self.intraday_execution_start_time()), ) { @@ -7726,7 +7749,7 @@ impl Strategy for PlatformExprStrategy { .project_target_zero_at_time( ctx, &mut projected, - execution_date, + projection_date, &symbol, &mut projected_execution_state, Some(risk_level_forced_exit_time), @@ -7755,7 +7778,7 @@ impl Strategy for PlatformExprStrategy { } if self.regular_sell_should_wait_due_to_highlimit( ctx, - execution_date, + projection_date, &position.symbol, self.intraday_execution_start_time(), )? { @@ -7767,7 +7790,7 @@ impl Strategy for PlatformExprStrategy { let current_value = self.projected_position_value_at_execution_price( ctx, &projected, - execution_date, + projection_date, &position.symbol, ); if let Some(threshold) = self @@ -7788,7 +7811,7 @@ impl Strategy for PlatformExprStrategy { self.project_target_value( ctx, &mut trial_projected, - execution_date, + projection_date, &position.symbol, target_value, &mut trial_execution_state, @@ -7834,15 +7857,16 @@ impl Strategy for PlatformExprStrategy { } if self.regular_sell_should_wait_due_to_highlimit( ctx, - execution_date, + projection_date, &position.symbol, self.intraday_execution_start_time(), )? { continue; } let (stop_hit, profit_hit) = - self.stop_take_action_for_position(ctx, execution_date, &day, projected_position)?; - let can_sell = self.can_sell_position(ctx, execution_date, &position.symbol); + self.stop_take_action_for_position(ctx, signal_date, &day, projected_position)?; + let can_sell = defer_execution_risk + || self.can_sell_position(ctx, execution_date, &position.symbol); if stop_hit { exit_symbols.insert(position.symbol.clone()); order_intents.push(OrderIntent::TargetValue { @@ -7855,7 +7879,7 @@ impl Strategy for PlatformExprStrategy { .project_target_zero( ctx, &mut projected, - execution_date, + projection_date, &position.symbol, &mut projected_execution_state, ) @@ -7883,7 +7907,7 @@ impl Strategy for PlatformExprStrategy { .project_target_zero( ctx, &mut projected, - execution_date, + projection_date, &position.symbol, &mut projected_execution_state, ) @@ -7919,7 +7943,7 @@ impl Strategy for PlatformExprStrategy { self.remaining_buy_cash_active_value( ctx, &projected, - execution_date, + projection_date, &working_symbols, &value_symbols, ) @@ -7929,7 +7953,7 @@ impl Strategy for PlatformExprStrategy { let slot_buy_cash = self.remaining_buy_cash_per_slot( ctx, &projected, - execution_date, + projection_date, target_budget, selection_limit, &working_symbols, @@ -7966,10 +7990,15 @@ impl Strategy for PlatformExprStrategy { { continue; } - let execution_stock = self.stock_state(ctx, execution_date, symbol)?; - if self - .buy_rejection_reason(ctx, execution_date, symbol, &execution_stock)? - .is_some() + if !defer_execution_risk + && self + .buy_rejection_reason( + ctx, + execution_date, + symbol, + &self.stock_state(ctx, execution_date, symbol)?, + )? + .is_some() { continue; } @@ -7990,7 +8019,7 @@ impl Strategy for PlatformExprStrategy { let filled_qty = self.project_order_value( ctx, &mut projected, - execution_date, + projection_date, symbol, buy_cash, &mut projected_execution_state, @@ -8041,13 +8070,13 @@ impl Strategy for PlatformExprStrategy { } if self.regular_sell_should_wait_due_to_highlimit( ctx, - execution_date, + projection_date, symbol, self.intraday_execution_start_time(), )? { continue; } - if !self.can_sell_position(ctx, execution_date, symbol) { + if !defer_execution_risk && !self.can_sell_position(ctx, execution_date, symbol) { continue; } order_intents.push(OrderIntent::TargetValue { @@ -8059,7 +8088,7 @@ impl Strategy for PlatformExprStrategy { .project_target_zero( ctx, &mut projected, - execution_date, + projection_date, symbol, &mut projected_execution_state, ) @@ -8109,17 +8138,22 @@ impl Strategy for PlatformExprStrategy { let current_value = self.projected_position_value_at_execution_price( ctx, &projected, - execution_date, + projection_date, symbol, ); let buy_cash = (target_cash - current_value).min(aiquant_available_cash); if buy_cash <= 0.0 { continue; } - let execution_stock = self.stock_state(ctx, execution_date, symbol)?; - if self - .buy_rejection_reason(ctx, execution_date, symbol, &execution_stock)? - .is_some() + if !defer_execution_risk + && self + .buy_rejection_reason( + ctx, + execution_date, + symbol, + &self.stock_state(ctx, execution_date, symbol)?, + )? + .is_some() { continue; } @@ -8130,7 +8164,7 @@ impl Strategy for PlatformExprStrategy { let filled_qty = self.project_order_value( ctx, &mut projected, - execution_date, + projection_date, symbol, buy_cash, &mut projected_execution_state, @@ -8164,7 +8198,7 @@ impl Strategy for PlatformExprStrategy { let slot_buy_cash = self.remaining_buy_cash_per_slot( ctx, &projected, - execution_date, + projection_date, target_budget, selection_limit, &rebalance_working_symbols, @@ -8180,10 +8214,15 @@ impl Strategy for PlatformExprStrategy { if self.config.aiquant_transaction_cost && buy_cash < target_cash * 0.5 { break; } - let execution_stock = self.stock_state(ctx, execution_date, symbol)?; - if self - .buy_rejection_reason(ctx, execution_date, symbol, &execution_stock)? - .is_some() + if !defer_execution_risk + && self + .buy_rejection_reason( + ctx, + execution_date, + symbol, + &self.stock_state(ctx, execution_date, symbol)?, + )? + .is_some() { continue; } @@ -8194,7 +8233,7 @@ impl Strategy for PlatformExprStrategy { let filled_qty = self.project_order_value( ctx, &mut projected, - execution_date, + projection_date, symbol, buy_cash, &mut projected_execution_state, @@ -8216,7 +8255,7 @@ impl Strategy for PlatformExprStrategy { if self.config.rotation_enabled && self.config.rebalance_schedule.is_none() { if self.config.calendar_rebalance_interval { if periodic_rebalance { - self.last_rebalance_date = Some(execution_date); + self.last_rebalance_date = Some(signal_date); } } else if periodic_rebalance { self.rebalance_day_counter = 1; @@ -13945,7 +13984,7 @@ mod tests { } #[test] - fn platform_aiquant_intraday_selection_filters_limits_on_execution_date() { + fn platform_next_open_selection_ignores_execution_day_limit_state() { let factor_date = d(2023, 11, 10); let decision_date = d(2023, 11, 13); let execution_date = d(2023, 11, 14); @@ -14186,7 +14225,7 @@ mod tests { decision .diagnostics .iter() - .any(|item| item.contains("selection_market_date=2023-11-14")), + .any(|item| item.contains("selection_market_date=2023-11-13")), "{:?}", decision.diagnostics ); @@ -14202,7 +14241,7 @@ mod tests { decision .diagnostics .iter() - .any(|item| item == "selected_symbols=000002.SZ"), + .any(|item| item == "selected_symbols=600462.SH"), "{:?}", decision.diagnostics ); @@ -14210,7 +14249,7 @@ mod tests { decision .diagnostics .iter() - .any(|item| item == "600462.SH rejected by upper_limit"), + .all(|item| item != "600462.SH rejected by upper_limit"), "{:?}", decision.diagnostics ); @@ -14218,9 +14257,9 @@ mod tests { decision.order_intents.iter().any(|intent| match intent { OrderIntent::Shares { symbol, quantity, .. - } => symbol == fallback_symbol && *quantity > 0, + } => symbol == limit_symbol && *quantity > 0, OrderIntent::Value { symbol, value, .. } => { - symbol == fallback_symbol && *value > 0.0 + symbol == limit_symbol && *value > 0.0 } _ => false, }),