From 4c0fde7621a44a0918f276c5459bde7590e32a0b Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 18 Jul 2026 08:35:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=8A=A8=E6=80=81=E6=8E=92?= =?UTF-8?q?=E5=90=8D=E6=9B=BF=E6=8D=A2=E7=9B=AE=E6=A0=87=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 86 ++++++++++++++++++- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 212006e..4100180 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -799,6 +799,7 @@ pub struct PlatformExprStrategy { engine: Engine, rebalance_day_counter: usize, last_rebalance_date: Option, + last_target_selection: Option>, last_trading_ratio: Option, portfolio_drawdown_controller: Option, pending_highlimit_holdings: BTreeSet, @@ -882,6 +883,18 @@ impl PlatformExprStrategy { defer_execution || projected_after_qty != before_qty } + fn replacement_reference_symbols( + &self, + actual_held_symbols: BTreeSet, + ) -> (&'static str, BTreeSet) { + if self.config.daily_replacement_limit > 0 { + if let Some(last_target_selection) = &self.last_target_selection { + return ("previous_target", last_target_selection.clone()); + } + } + ("actual_holdings", actual_held_symbols) + } + fn buffered_selection( ranked_symbols: &[String], held_symbols: &BTreeSet, @@ -1093,6 +1106,7 @@ impl PlatformExprStrategy { engine, rebalance_day_counter: 0, last_rebalance_date: None, + last_target_selection: None, last_trading_ratio: None, portfolio_drawdown_controller, pending_highlimit_holdings: BTreeSet::new(), @@ -9172,6 +9186,8 @@ impl Strategy for PlatformExprStrategy { 0 }; let mut risk_decisions = Vec::new(); + let mut replacement_reference_source = "disabled"; + let mut replacement_reference_count = 0usize; let stock_list = if self.config.rotation_enabled && !in_skip_window { let selection_buffer_rank = self.selection_candidate_limit(ctx, &day, selection_limit)?; @@ -9192,18 +9208,22 @@ impl Strategy for PlatformExprStrategy { )?; selection_notes = notes; risk_decisions = selection_risk_decisions; - let mut held_symbols = ctx + let mut actual_held_symbols = ctx .portfolio .positions() .keys() .cloned() .collect::>(); if persistent_model_lifecycle { - held_symbols.extend(self.position_entry_dates.keys().cloned()); + actual_held_symbols.extend(self.position_entry_dates.keys().cloned()); } + let (reference_source, replacement_reference_symbols) = + self.replacement_reference_symbols(actual_held_symbols); + replacement_reference_source = reference_source; + replacement_reference_count = replacement_reference_symbols.len(); let selected = Self::buffered_selection( &ranked_stock_list, - &held_symbols, + &replacement_reference_symbols, selection_limit, selection_buffer_rank, self.config.daily_replacement_limit, @@ -10738,6 +10758,10 @@ impl Strategy for PlatformExprStrategy { self.rebalance_day_counter = self.rebalance_day_counter.saturating_add(1); } } + if self.config.rotation_enabled && periodic_rebalance { + self.last_target_selection = + Some(stock_list.iter().take(selection_limit).cloned().collect()); + } if self.config.rotation_enabled && trading_ratio.is_finite() { self.last_trading_ratio = Some(trading_ratio); } @@ -10786,13 +10810,15 @@ impl Strategy for PlatformExprStrategy { ) }, format!( - "selected={} periodic_rebalance={} exits={} projected_positions={} intents={} limit={} decision_date={} signal_rebalance_date_count={} signal_rebalance_match={} selection_market_date={} selection_universe_factor_date={} selection_factor_date={} execution_date={} budget_total={:.2} marked_total={:.2} day_total={:.2}", + "selected={} periodic_rebalance={} exits={} projected_positions={} intents={} limit={} replacement_reference={} replacement_reference_count={} decision_date={} signal_rebalance_date_count={} signal_rebalance_match={} selection_market_date={} selection_universe_factor_date={} selection_factor_date={} execution_date={} budget_total={:.2} marked_total={:.2} day_total={:.2}", stock_list.len(), periodic_rebalance, exit_symbols.len(), projected.positions().len(), order_intents.len(), selection_limit, + replacement_reference_source, + replacement_reference_count, decision_date, self.config.signal_rebalance_dates.len(), self.config.signal_rebalance_dates.contains(&decision_date), @@ -31600,6 +31626,58 @@ mod tests { assert_eq!(held.difference(&selected_set).count(), 2); } + #[test] + fn daily_replacement_reference_uses_previous_target_before_lagged_orders_fill() { + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.daily_replacement_limit = 2; + let mut strategy = PlatformExprStrategy::new(cfg); + let previous_target = ["000001.SZ", "000002.SZ", "000003.SZ", "000004.SZ"] + .into_iter() + .map(str::to_string) + .collect::>(); + strategy.last_target_selection = Some(previous_target.clone()); + + let (source, reference) = strategy.replacement_reference_symbols(BTreeSet::new()); + let ranked = [ + "000005.SZ", + "000006.SZ", + "000007.SZ", + "000008.SZ", + "000001.SZ", + "000002.SZ", + "000003.SZ", + "000004.SZ", + ] + .into_iter() + .map(str::to_string) + .collect::>(); + let selected = PlatformExprStrategy::buffered_selection(&ranked, &reference, 4, 4, 2); + + assert_eq!(source, "previous_target"); + assert_eq!(reference, previous_target); + assert_eq!( + selected, + vec!["000005.SZ", "000006.SZ", "000001.SZ", "000002.SZ"] + ); + } + + #[test] + fn daily_replacement_reference_records_an_explicit_empty_previous_target() { + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.daily_replacement_limit = 2; + let mut strategy = PlatformExprStrategy::new(cfg); + strategy.last_target_selection = Some(BTreeSet::new()); + let actual = ["000001.SZ"] + .into_iter() + .map(str::to_string) + .collect::>(); + + let (source, reference) = strategy.replacement_reference_symbols(actual); + + assert_eq!(source, "previous_target"); + assert!(reference.is_empty()); + } + #[test] fn lagged_rebalance_emits_target_value_even_when_projection_is_unchanged() { assert!(PlatformExprStrategy::should_emit_rebalance_target_value(