diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index ce73dbe..a914b23 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -10606,7 +10606,9 @@ impl Strategy for PlatformExprStrategy { continue; } if rebalance_working_symbols.len() >= selection_limit { - break; + // New symbols cannot consume another slot, but later ranked symbols may + // already be held and still require a target-value adjustment. + continue; } if ((pre_rebalance_symbols.contains(symbol) || projected.positions().contains_key(symbol)) @@ -11072,7 +11074,7 @@ mod tests { let previous_date = d(2025, 1, 2); let execution_date = d(2025, 1, 3); let unresolved_symbol = "000999.SZ"; - let active_symbols = ["000001.SZ", "000002.SZ"]; + let active_symbols = ["000001.SZ", "000002.SZ", "000003.SZ"]; let data = DataSet::from_components( std::iter::once(Instrument { symbol: unresolved_symbol.to_string(), @@ -11164,6 +11166,9 @@ mod tests { portfolio .position_mut(unresolved_symbol) .buy(previous_date, 1_000, 10.0); + portfolio + .position_mut("000003.SZ") + .buy(previous_date, 100, 10.0); let subscriptions = BTreeSet::new(); let before_delisting_ctx = StrategyContext { execution_date: previous_date, @@ -11205,21 +11210,28 @@ mod tests { let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = "000001.SZ".to_string(); cfg.refresh_rate = 1; - cfg.max_positions = 2; + cfg.max_positions = 3; cfg.benchmark_short_ma_days = 1; cfg.benchmark_long_ma_days = 1; cfg.market_cap_lower_expr = "0".to_string(); cfg.market_cap_upper_expr = "100".to_string(); - cfg.selection_limit_expr = "2".to_string(); + cfg.selection_limit_expr = "3".to_string(); cfg.stock_filter_expr = "close > 0".to_string(); cfg.stop_loss_expr.clear(); cfg.take_profit_expr.clear(); cfg.aiquant_transaction_cost = true; - cfg.daily_replacement_limit = 2; + cfg.daily_replacement_limit = 3; cfg.selection_buffer_multiple = 2.0; cfg.rebalance_existing_positions = false; let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 1; + strategy.last_trading_ratio = Some(0.1); + strategy.last_target_selection = Some( + active_symbols + .iter() + .map(|symbol| (*symbol).to_string()) + .collect(), + ); let decision = strategy.on_day(&ctx).expect("platform decision"); @@ -11233,7 +11245,9 @@ mod tests { decision .diagnostics .iter() - .any(|note| note == "selected_symbols=000001.SZ,000002.SZ") + .any(|note| note == "selected_symbols=000001.SZ,000002.SZ,000003.SZ"), + "{:?}", + decision.diagnostics ); assert!(!decision.order_intents.iter().any(|intent| { matches!( @@ -11265,6 +11279,18 @@ mod tests { && reason == "periodic_rebalance_buy" ) })); + assert!(decision.order_intents.iter().any(|intent| { + matches!( + intent, + OrderIntent::TargetValue { + symbol, + target_value, + reason, + } if symbol == "000003.SZ" + && *target_value > 0.0 + && reason == "periodic_rebalance_target_adjust" + ) + })); } #[test]