From 89f8bb32d0fa51114441b3f88399048b707801ae Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 8 Jul 2026 01:44:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90ALV=E6=97=A5=E5=86=85?= =?UTF-8?q?=E8=A1=A5=E4=BB=93=E6=89=A7=E8=A1=8C=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 | 386 +++++++++++------- 1 file changed, 244 insertions(+), 142 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index e3d53ab..67e2ce1 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -2240,6 +2240,172 @@ impl PlatformExprStrategy { per_slot_budget.min(single_slot_cap) } + #[allow(clippy::too_many_arguments)] + fn try_daily_top_up_once( + &mut self, + ctx: &StrategyContext<'_>, + day: &DayExpressionState, + stock_list: &[String], + decision_date: NaiveDate, + execution_date: NaiveDate, + projection_date: NaiveDate, + selection_factor_date: NaiveDate, + signal_date: NaiveDate, + target_budget: f64, + selection_limit: usize, + defer_execution_risk: bool, + current_position_symbol: Option<&str>, + projected: &mut PortfolioState, + projected_execution_state: &mut ProjectedExecutionState, + order_intents: &mut Vec, + aiquant_available_cash: &mut f64, + slot_working_symbols: &mut BTreeSet, + same_bar_buy_symbols: &mut BTreeSet, + pending_full_close_symbols: &BTreeSet, + same_day_sold_symbols: &BTreeSet, + exit_symbols: &BTreeSet, + delayed_sold_symbols: &BTreeSet, + intraday_attempted_buys: &mut BTreeSet, + pending_buy_value: &mut f64, + debug_daily_top_up: bool, + daily_top_up_debug_notes: &mut Vec, + ) -> Result { + if target_budget <= 0.0 || selection_limit == 0 || stock_list.is_empty() { + return Ok(false); + } + + let mut budget_working_symbols = slot_working_symbols.clone(); + budget_working_symbols.extend(same_bar_buy_symbols.iter().cloned()); + if Self::effective_slot_count(&budget_working_symbols, pending_full_close_symbols) + >= selection_limit + { + return Ok(false); + } + let value_symbols = budget_working_symbols.clone(); + let active_value = if debug_daily_top_up { + self.remaining_buy_cash_active_value( + ctx, + projected, + projection_date, + &budget_working_symbols, + &value_symbols, + ) + } else { + 0.0 + }; + let slot_buy_cash = self.remaining_buy_cash_per_slot( + ctx, + projected, + projection_date, + target_budget, + selection_limit, + &budget_working_symbols, + &value_symbols, + *pending_buy_value, + pending_full_close_symbols, + ); + let available_buy_cash = slot_buy_cash.min(*aiquant_available_cash); + if debug_daily_top_up { + daily_top_up_debug_notes.push(format!( + "daily_top_up_budget date={} working={} value_symbols={} same_bar_buys={} active_value={:.4} pending_buy_value={:.4} target_budget={:.4} slot_buy_cash={:.4} available_cash={:.4} portfolio_cash={:.4}", + execution_date, + budget_working_symbols.len(), + value_symbols.len(), + same_bar_buy_symbols.len(), + active_value, + *pending_buy_value, + target_budget, + slot_buy_cash, + available_buy_cash, + *aiquant_available_cash + )); + } + if slot_buy_cash <= 0.0 || available_buy_cash < slot_buy_cash * 0.5 { + return Ok(false); + } + + for symbol in stock_list { + if current_position_symbol + .map(|current| current == symbol) + .unwrap_or(false) + { + continue; + } + let released_exit_position = projected.positions().contains_key(symbol) + && !slot_working_symbols.contains(symbol) + && !same_day_sold_symbols.contains(symbol) + && !pending_full_close_symbols.contains(symbol); + if (projected.positions().contains_key(symbol) && !released_exit_position) + || same_day_sold_symbols.contains(symbol) + || (exit_symbols.contains(symbol) && !released_exit_position) + || pending_full_close_symbols.contains(symbol) + || delayed_sold_symbols.contains(symbol) + || intraday_attempted_buys.contains(symbol) + { + continue; + } + if !defer_execution_risk + && self + .buy_rejection_reason( + ctx, + execution_date, + symbol, + &self.stock_state(ctx, execution_date, symbol)?, + )? + .is_some() + { + continue; + } + let decision_stock = self.stock_state_with_factor_date( + ctx, + decision_date, + selection_factor_date, + symbol, + )?; + let buy_cash = available_buy_cash * self.buy_scale(ctx, day, &decision_stock)?; + if buy_cash <= 0.0 { + return Ok(false); + } + intraday_attempted_buys.insert(symbol.clone()); + let cash_before_buy = projected.cash(); + let order_result = self.project_order_value( + ctx, + projected, + projection_date, + symbol, + buy_cash, + projected_execution_state, + ); + if order_result.was_submitted() { + order_intents.push(OrderIntent::Value { + symbol: symbol.clone(), + value: buy_cash, + reason: "daily_top_up_buy".to_string(), + }); + self.remember_position_entry_date(symbol, signal_date); + let spent = (cash_before_buy - projected.cash()).max(0.0); + *aiquant_available_cash = (*aiquant_available_cash - spent).max(0.0); + slot_working_symbols.insert(symbol.clone()); + same_bar_buy_symbols.insert(symbol.clone()); + *pending_buy_value += available_buy_cash; + if debug_daily_top_up { + daily_top_up_debug_notes.push(format!( + "daily_top_up_fill date={} symbol={} requested_cash={:.4} filled_qty={} spent={:.4} remaining_cash={:.4}", + execution_date, + symbol, + buy_cash, + order_result.filled_quantity, + spent, + *aiquant_available_cash + )); + } + return Ok(true); + } + } + + Ok(false) + } + fn effective_slot_count( working_symbols: &BTreeSet, slot_blocking_symbols: &BTreeSet, @@ -7945,6 +8111,7 @@ impl Strategy for PlatformExprStrategy { let mut exit_symbols = BTreeSet::new(); let mut same_day_sold_symbols = BTreeSet::::new(); let mut intraday_attempted_buys = BTreeSet::::new(); + let mut same_bar_buy_symbols = BTreeSet::::new(); let mut delayed_sold_symbols = BTreeSet::::new(); let mut unresolved_stop_loss_symbols = BTreeSet::::new(); let mut pending_risk_level_forced_exit_symbols = BTreeSet::::new(); @@ -8453,12 +8620,23 @@ impl Strategy for PlatformExprStrategy { quantity: quantity_delta, reason: "daily_position_target_adjust".to_string(), }); + if quantity_delta > 0 { + same_bar_buy_symbols.insert(position.symbol.clone()); + } } } aiquant_available_cash = projected.cash(); } } + let daily_top_up_active = self.config.daily_top_up_enabled + && self.config.rotation_enabled + && !periodic_rebalance + && trading_ratio > 0.0 + && selection_limit > 0; + let daily_top_up_target_budget = aiquant_total_value * trading_ratio; + let mut daily_top_up_pending_buy_value = 0.0_f64; + for position in ctx.portfolio.positions().values() { if delayed_sold_symbols.contains(&position.symbol) || pending_full_close_symbols.contains(&position.symbol) @@ -8518,6 +8696,36 @@ impl Strategy for PlatformExprStrategy { } else { unresolved_stop_loss_symbols.insert(position.symbol.clone()); } + if daily_top_up_active { + self.try_daily_top_up_once( + ctx, + &day, + &stock_list, + decision_date, + execution_date, + projection_date, + selection_factor_date, + signal_date, + daily_top_up_target_budget, + selection_limit, + defer_execution_risk, + Some(position.symbol.as_str()), + &mut projected, + &mut projected_execution_state, + &mut order_intents, + &mut aiquant_available_cash, + &mut slot_working_symbols, + &mut same_bar_buy_symbols, + &pending_full_close_symbols, + &same_day_sold_symbols, + &exit_symbols, + &delayed_sold_symbols, + &mut intraday_attempted_buys, + &mut daily_top_up_pending_buy_value, + debug_daily_top_up, + &mut daily_top_up_debug_notes, + )?; + } continue; } @@ -8553,150 +8761,35 @@ impl Strategy for PlatformExprStrategy { } } } - } - if self.config.daily_top_up_enabled - && self.config.rotation_enabled - && !periodic_rebalance - && trading_ratio > 0.0 - && selection_limit > 0 - { - let target_budget = aiquant_total_value * trading_ratio; - let mut working_symbols = slot_working_symbols.clone(); - if self.config.release_slot_on_exit_signal { - for symbol in &same_day_sold_symbols { - working_symbols.remove(symbol); - slot_working_symbols.remove(symbol); - } - } - let value_symbols = working_symbols.clone(); - let mut pending_buy_value = 0.0_f64; - loop { - if Self::effective_slot_count(&working_symbols, &pending_full_close_symbols) - >= selection_limit - { - break; - } - let active_value = if debug_daily_top_up { - self.remaining_buy_cash_active_value( - ctx, - &projected, - projection_date, - &working_symbols, - &value_symbols, - ) - } else { - 0.0 - }; - let slot_buy_cash = self.remaining_buy_cash_per_slot( + if daily_top_up_active { + self.try_daily_top_up_once( ctx, - &projected, + &day, + &stock_list, + decision_date, + execution_date, projection_date, - target_budget, + selection_factor_date, + signal_date, + daily_top_up_target_budget, selection_limit, - &working_symbols, - &value_symbols, - pending_buy_value, + defer_execution_risk, + Some(position.symbol.as_str()), + &mut projected, + &mut projected_execution_state, + &mut order_intents, + &mut aiquant_available_cash, + &mut slot_working_symbols, + &mut same_bar_buy_symbols, &pending_full_close_symbols, - ); - let available_buy_cash = slot_buy_cash.min(aiquant_available_cash); - if debug_daily_top_up { - daily_top_up_debug_notes.push(format!( - "daily_top_up_budget date={} working={} value_symbols={} active_value={:.4} pending_buy_value={:.4} target_budget={:.4} slot_buy_cash={:.4} available_cash={:.4} portfolio_cash={:.4}", - execution_date, - working_symbols.len(), - value_symbols.len(), - active_value, - pending_buy_value, - target_budget, - slot_buy_cash, - available_buy_cash, - aiquant_available_cash - )); - } - if slot_buy_cash <= 0.0 || available_buy_cash < slot_buy_cash * 0.5 { - break; - } - - let mut attempted_any = false; - let mut filled_any = false; - for symbol in &stock_list { - let released_exit_position = projected.positions().contains_key(symbol) - && !working_symbols.contains(symbol) - && !same_day_sold_symbols.contains(symbol) - && !pending_full_close_symbols.contains(symbol); - if (projected.positions().contains_key(symbol) && !released_exit_position) - || same_day_sold_symbols.contains(symbol) - || (exit_symbols.contains(symbol) && !released_exit_position) - || pending_full_close_symbols.contains(symbol) - || delayed_sold_symbols.contains(symbol) - || intraday_attempted_buys.contains(symbol) - { - continue; - } - if !defer_execution_risk - && self - .buy_rejection_reason( - ctx, - execution_date, - symbol, - &self.stock_state(ctx, execution_date, symbol)?, - )? - .is_some() - { - continue; - } - let decision_stock = self.stock_state_with_factor_date( - ctx, - decision_date, - selection_factor_date, - symbol, - )?; - let buy_cash = - available_buy_cash * self.buy_scale(ctx, &day, &decision_stock)?; - if buy_cash <= 0.0 { - break; - } - intraday_attempted_buys.insert(symbol.clone()); - attempted_any = true; - let cash_before_buy = projected.cash(); - let order_result = self.project_order_value( - ctx, - &mut projected, - projection_date, - symbol, - buy_cash, - &mut projected_execution_state, - ); - if order_result.was_submitted() { - order_intents.push(OrderIntent::Value { - symbol: symbol.clone(), - value: buy_cash, - reason: "daily_top_up_buy".to_string(), - }); - self.remember_position_entry_date(symbol, signal_date); - let spent = (cash_before_buy - projected.cash()).max(0.0); - aiquant_available_cash = (aiquant_available_cash - spent).max(0.0); - working_symbols.insert(symbol.clone()); - slot_working_symbols.insert(symbol.clone()); - pending_buy_value += available_buy_cash; - if debug_daily_top_up { - daily_top_up_debug_notes.push(format!( - "daily_top_up_fill date={} symbol={} requested_cash={:.4} filled_qty={} spent={:.4} remaining_cash={:.4}", - execution_date, - symbol, - buy_cash, - order_result.filled_quantity, - spent, - aiquant_available_cash - )); - } - filled_any = true; - break; - } - } - if !attempted_any || !filled_any { - break; - } + &same_day_sold_symbols, + &exit_symbols, + &delayed_sold_symbols, + &mut intraday_attempted_buys, + &mut daily_top_up_pending_buy_value, + debug_daily_top_up, + &mut daily_top_up_debug_notes, + )?; } } @@ -21943,7 +22036,7 @@ mod tests { } #[test] - fn platform_daily_top_up_can_rebuy_released_unsellable_stop_loss_symbol() { + fn platform_daily_top_up_skips_current_stop_loss_symbol_like_aiquant() { let prev_date = d(2026, 3, 31); let date = d(2026, 4, 1); let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"]; @@ -22086,7 +22179,7 @@ mod tests { decision.order_intents ); assert!( - decision.order_intents.iter().any(|intent| matches!( + !decision.order_intents.iter().any(|intent| matches!( intent, OrderIntent::Value { symbol, reason, .. } if symbol == "000001.SZ" && reason == "daily_top_up_buy" @@ -22094,6 +22187,15 @@ mod tests { "{:?}", decision.order_intents ); + assert!( + decision.order_intents.iter().any(|intent| matches!( + intent, + OrderIntent::Value { symbol, reason, .. } + if symbol == "000003.SZ" && reason == "daily_top_up_buy" + )), + "{:?}", + decision.order_intents + ); } #[test]