From 71b5acee3030f905bc0c57810aacc10dd873a4d3 Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 17 Jul 2026 22:41:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=8C=81=E4=B9=85=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E7=BB=84=E5=90=88=E8=B0=83=E4=BB=93=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 264 ++++++++++++++---- 1 file changed, 210 insertions(+), 54 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 0f4e010..18753a9 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -9659,6 +9659,9 @@ impl Strategy for PlatformExprStrategy { && trading_ratio > 0.0 && (self.config.target_portfolio_daily_enabled || trading_ratio < 1.0) && selection_limit > 0 + && !(persistent_model_lifecycle + && self.config.target_portfolio_daily_enabled + && daily_top_up_active) && (!ctx.portfolio.positions().is_empty() || (persistent_model_lifecycle && !self.position_entry_dates.is_empty())) { @@ -10188,7 +10191,89 @@ impl Strategy for PlatformExprStrategy { } } - if daily_top_up_active && self.config.target_portfolio_daily_enabled { + if daily_top_up_active + && self.config.target_portfolio_daily_enabled + && persistent_model_lifecycle + { + let mut target_symbols = self + .position_entry_dates + .keys() + .filter(|symbol| !exit_symbols.contains(*symbol)) + .cloned() + .collect::>(); + for symbol in &stock_list { + if target_symbols.len() >= selection_limit { + break; + } + if target_symbols.contains(symbol) || exit_symbols.contains(symbol) { + continue; + } + if ctx + .data + .market_latest_back_adjusted_close(signal_date, symbol) + .is_none() + { + continue; + } + self.remember_position_entry_date(symbol, signal_date); + target_symbols.insert(symbol.clone()); + } + + for symbol in target_symbols { + let decision_stock = self.stock_state_with_factor_date( + ctx, + decision_date, + selection_factor_date, + &symbol, + )?; + let stock_scale = self.buy_scale(ctx, &day, &decision_stock)?; + let target_value = + aiquant_total_value * trading_ratio / selection_limit as f64 * stock_scale; + if !target_value.is_finite() || target_value <= 0.0 { + continue; + } + + let before_qty = projected + .position(&symbol) + .map(|position| position.quantity) + .unwrap_or(0); + if before_qty > 0 { + self.project_target_value( + ctx, + &mut projected, + projection_date, + &symbol, + target_value, + &mut projected_execution_state, + ); + } else { + self.project_order_value( + ctx, + &mut projected, + projection_date, + &symbol, + target_value, + &mut projected_execution_state, + ); + intraday_attempted_buys.insert(symbol.clone()); + } + let after_qty = projected + .position(&symbol) + .map(|position| position.quantity) + .unwrap_or(0); + order_intents.push(OrderIntent::TargetValue { + symbol: symbol.clone(), + target_value, + reason: "model_target_portfolio_daily".to_string(), + }); + if defer_execution_risk { + deferred_daily_target_values.insert(symbol.clone(), target_value); + } + if after_qty > before_qty { + same_bar_buy_symbols.insert(symbol); + } + } + } else if daily_top_up_active && self.config.target_portfolio_daily_enabled { self.try_daily_top_up_at_position( ctx, &day, @@ -23892,51 +23977,80 @@ mod tests { status: "active".to_string(), }) .collect(), - symbols - .iter() - .map(|symbol| DailyMarketSnapshot { - date, - symbol: (*symbol).to_string(), - timestamp: Some("2025-04-30 10:18:00".to_string()), - day_open: 10.0, - open: 10.0, - high: 10.5, - low: 9.8, - close: 10.0, - last_price: 10.0, - bid1: 10.0, - ask1: 10.0, - prev_close: 9.9, - volume: 1_000_000, - minute_volume: 20_000, - bid1_volume: 20_000, - ask1_volume: 20_000, - trading_phase: Some("continuous".to_string()), - paused: false, - upper_limit: 11.0, - lower_limit: 9.0, - price_tick: 0.01, - }) - .collect(), - symbols - .iter() - .map(|symbol| DailyFactorSnapshot { - date, - symbol: (*symbol).to_string(), - market_cap_bn: match *symbol { - s if s == buy_first => 1.0, - s if s == buy_second => 2.0, - s if s == keep_first => 3.0, - s if s == keep_second => 4.0, - _ => 100.0, - }, - free_float_cap_bn: 10.0, - pe_ttm: 8.0, - turnover_ratio: Some(1.0), - effective_turnover_ratio: Some(1.0), - extra_factors: BTreeMap::new(), - }) - .collect(), + std::iter::once(DailyMarketSnapshot { + date: prev_date, + symbol: take_profit.to_string(), + timestamp: Some("2025-04-29 15:00:00".to_string()), + day_open: 8.0, + open: 8.0, + high: 8.0, + low: 8.0, + close: 8.0, + last_price: 8.0, + bid1: 8.0, + ask1: 8.0, + prev_close: 8.0, + volume: 1_000_000, + minute_volume: 20_000, + bid1_volume: 20_000, + ask1_volume: 20_000, + trading_phase: Some("closed".to_string()), + paused: false, + upper_limit: 8.8, + lower_limit: 7.2, + price_tick: 0.01, + }) + .chain(symbols.iter().map(|symbol| DailyMarketSnapshot { + date, + symbol: (*symbol).to_string(), + timestamp: Some("2025-04-30 10:18:00".to_string()), + day_open: 10.0, + open: 10.0, + high: 10.5, + low: 9.8, + close: 10.0, + last_price: 10.0, + bid1: 10.0, + ask1: 10.0, + prev_close: 9.9, + volume: 1_000_000, + minute_volume: 20_000, + bid1_volume: 20_000, + ask1_volume: 20_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 11.0, + lower_limit: 9.0, + price_tick: 0.01, + })) + .collect(), + std::iter::once(DailyFactorSnapshot { + date: prev_date, + symbol: take_profit.to_string(), + market_cap_bn: 100.0, + free_float_cap_bn: 10.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::from([("adjustment_factor_backward1".to_string(), 1.0)]), + }) + .chain(symbols.iter().map(|symbol| DailyFactorSnapshot { + date, + symbol: (*symbol).to_string(), + market_cap_bn: match *symbol { + s if s == buy_first => 1.0, + s if s == buy_second => 2.0, + s if s == keep_first => 3.0, + s if s == keep_second => 4.0, + _ => 100.0, + }, + free_float_cap_bn: 10.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::from([("adjustment_factor_backward1".to_string(), 1.0)]), + })) + .collect(), symbols .iter() .map(|symbol| CandidateEligibility { @@ -24023,22 +24137,31 @@ mod tests { cfg.daily_top_up_enabled = true; cfg.daily_position_target_adjust_enabled = true; cfg.target_portfolio_daily_enabled = true; + cfg.hold_until_exit_enabled = true; + cfg.stop_take_reference_price_mode = + PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose; cfg.release_slot_on_exit_signal = true; cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 2; + strategy.last_rebalance_date = Some(prev_date); + strategy + .position_entry_dates + .insert(take_profit.to_string(), prev_date); let decision = strategy.on_day(&ctx).expect("platform decision"); - let top_ups = decision + let positive_targets = decision .order_intents .iter() .filter_map(|intent| match intent { - OrderIntent::Value { + OrderIntent::TargetValue { symbol, - value, + target_value, reason, - } if reason == "daily_top_up_buy" => Some((symbol.as_str(), *value)), + } if reason == "model_target_portfolio_daily" && *target_value > 0.0 => { + Some((symbol.as_str(), *target_value)) + } _ => None, }) .collect::>(); @@ -24055,18 +24178,51 @@ mod tests { "{:?}", decision.order_intents ); + let clear_index = decision + .order_intents + .iter() + .position(|intent| matches!( + intent, + OrderIntent::TargetValue { + symbol, + target_value, + reason, + } if symbol == take_profit && *target_value == 0.0 && reason == "take_profit_exit" + )) + .expect("take-profit clear intent"); + let first_positive_index = decision + .order_intents + .iter() + .position(|intent| { + matches!( + intent, + OrderIntent::TargetValue { + target_value, + reason, + .. + } if *target_value > 0.0 && reason == "model_target_portfolio_daily" + ) + }) + .expect("positive target intent"); + assert!( + clear_index < first_positive_index, + "{:?}", + decision.order_intents + ); assert_eq!( - top_ups + positive_targets .iter() .map(|(symbol, _)| *symbol) .collect::>(), - vec![buy_first, buy_second], + vec![buy_first, buy_second, keep_first, keep_second], "{:?}", decision.order_intents ); assert!( - top_ups.iter().all(|(_, value)| *value >= 6_900.0), - "top ups must be sized after all same-batch sells: {:?}", + positive_targets + .iter() + .all(|(_, target_value)| (*target_value - 7_500.0).abs() < 1e-9), + "all targets must use the frozen target allocation: {:?}", decision.order_intents ); }