From 12da5a470401b751ef3de662f4f7ddc77b21040d Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 8 Jul 2026 00:59:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90ALV=E8=A1=A5=E4=BB=93?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E8=BF=94=E5=9B=9E=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 | 97 ++++++++++++------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 9f6064e..df391f2 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -357,6 +357,36 @@ struct ProjectedExecutionFill { next_cursor: NaiveDateTime, } +#[derive(Debug, Clone, Copy, Default)] +struct ProjectedOrderValueResult { + submitted_quantity: u32, + filled_quantity: u32, +} + +impl ProjectedOrderValueResult { + fn not_submitted() -> Self { + Self::default() + } + + fn submitted_without_fill(submitted_quantity: u32) -> Self { + Self { + submitted_quantity, + filled_quantity: 0, + } + } + + fn submitted_with_fill(submitted_quantity: u32, filled_quantity: u32) -> Self { + Self { + submitted_quantity, + filled_quantity, + } + } + + fn was_submitted(&self) -> bool { + self.submitted_quantity > 0 + } +} + #[derive(Debug, Clone)] struct DayExpressionState { date: NaiveDate, @@ -2037,9 +2067,9 @@ impl PlatformExprStrategy { return None; } if cash_delta > 0.0 { - let filled = + let result = self.project_order_value(ctx, projected, date, symbol, cash_delta, execution_state); - return (filled > 0).then_some(filled); + return (result.filled_quantity > 0).then_some(result.filled_quantity); } if !Self::defer_projection_execution_risk(ctx, date) @@ -2235,23 +2265,23 @@ impl PlatformExprStrategy { symbol: &str, order_value: f64, execution_state: &mut ProjectedExecutionState, - ) -> u32 { + ) -> ProjectedOrderValueResult { if order_value <= 0.0 { - return 0; + return ProjectedOrderValueResult::not_submitted(); } let round_lot = self.projected_round_lot(ctx, symbol); let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol); let order_step_size = self.projected_order_step_size(ctx, symbol); let market = match ctx.data.market(date, symbol) { Some(market) => market, - None => return 0, + None => return ProjectedOrderValueResult::not_submitted(), }; let stock = match self.stock_state(ctx, date, symbol) { Ok(stock) => stock, Err(BacktestError::Data(crate::data::DataSetError::MissingSnapshot { .. })) => { - return 0; + return ProjectedOrderValueResult::not_submitted(); } - Err(_) => return 0, + Err(_) => return ProjectedOrderValueResult::not_submitted(), }; if !Self::defer_projection_execution_risk(ctx, date) && self @@ -2260,7 +2290,7 @@ impl PlatformExprStrategy { .flatten() .is_some() { - return 0; + return ProjectedOrderValueResult::not_submitted(); } let raw_sizing_price = if self.config.aiquant_transaction_cost { self.aiquant_scheduled_last_price(ctx, date, symbol) @@ -2274,7 +2304,7 @@ impl PlatformExprStrategy { raw_sizing_price }; if !sizing_price.is_finite() || sizing_price <= 0.0 { - return 0; + return ProjectedOrderValueResult::not_submitted(); } let snapshot_requested_qty = self.value_buy_quantity( projected.cash().min(order_value), @@ -2304,8 +2334,9 @@ impl PlatformExprStrategy { self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size); } if quantity == 0 { - return 0; + return ProjectedOrderValueResult::not_submitted(); } + let submitted_quantity = quantity; let defer_projection_execution_risk = Self::defer_projection_execution_risk(ctx, date); let fill = self .projected_select_execution_fill( @@ -2363,12 +2394,12 @@ impl PlatformExprStrategy { } }); let Some(fill) = fill else { - return 0; + return ProjectedOrderValueResult::submitted_without_fill(submitted_quantity); }; let gross_amount = fill.price * fill.quantity as f64; let cash_out = gross_amount + self.buy_commission(gross_amount); if cash_out > cash_limit + 1e-6 { - return 0; + return ProjectedOrderValueResult::submitted_without_fill(submitted_quantity); } projected.apply_cash_delta(-cash_out); projected @@ -2381,7 +2412,7 @@ impl PlatformExprStrategy { execution_state .execution_cursors .insert(symbol.to_string(), fill.next_cursor); - fill.quantity + ProjectedOrderValueResult::submitted_with_fill(submitted_quantity, fill.quantity) } fn defer_projection_execution_risk(ctx: &StrategyContext<'_>, date: NaiveDate) -> bool { @@ -8600,7 +8631,7 @@ impl Strategy for PlatformExprStrategy { intraday_attempted_buys.insert(symbol.clone()); attempted_any = true; let cash_before_buy = projected.cash(); - let filled_qty = self.project_order_value( + let order_result = self.project_order_value( ctx, &mut projected, projection_date, @@ -8608,15 +8639,13 @@ impl Strategy for PlatformExprStrategy { buy_cash, &mut projected_execution_state, ); - if filled_qty > 0 { + 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); - } - if filled_qty > 0 { 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()); @@ -8628,7 +8657,7 @@ impl Strategy for PlatformExprStrategy { execution_date, symbol, buy_cash, - filled_qty, + order_result.filled_quantity, spent, aiquant_available_cash )); @@ -8759,7 +8788,7 @@ impl Strategy for PlatformExprStrategy { continue; } let cash_before_buy = projected.cash(); - let filled_qty = self.project_order_value( + let order_result = self.project_order_value( ctx, &mut projected, projection_date, @@ -8767,7 +8796,7 @@ impl Strategy for PlatformExprStrategy { buy_cash, &mut projected_execution_state, ); - if filled_qty > 0 { + if order_result.was_submitted() { order_intents.push(OrderIntent::Value { symbol: symbol.clone(), value: buy_cash, @@ -8855,7 +8884,7 @@ impl Strategy for PlatformExprStrategy { continue; } let cash_before_buy = projected.cash(); - let filled_qty = self.project_order_value( + let order_result = self.project_order_value( ctx, &mut projected, projection_date, @@ -8863,7 +8892,7 @@ impl Strategy for PlatformExprStrategy { buy_cash, &mut projected_execution_state, ); - if filled_qty > 0 { + if order_result.was_submitted() { order_intents.push(OrderIntent::Value { symbol: symbol.clone(), value: buy_cash, @@ -10790,7 +10819,7 @@ mod tests { let mut projected = portfolio.clone(); let mut execution_state = super::ProjectedExecutionState::default(); - let filled = strategy.project_order_value( + let result = strategy.project_order_value( &ctx, &mut projected, date, @@ -10799,7 +10828,7 @@ mod tests { &mut execution_state, ); - assert_eq!(filled, 24_400); + assert_eq!(result.filled_quantity, 24_400); let position = projected.position(symbol).expect("position"); assert_eq!(position.quantity, 24_400); assert!((position.average_cost - 5.12022).abs() < 1e-9); @@ -10924,7 +10953,7 @@ mod tests { let mut projected = portfolio.clone(); let mut execution_state = super::ProjectedExecutionState::default(); - let filled = strategy.project_order_value( + let result = strategy.project_order_value( &ctx, &mut projected, date, @@ -10933,7 +10962,7 @@ mod tests { &mut execution_state, ); - assert_eq!(filled, 24_400); + assert_eq!(result.filled_quantity, 24_400); } #[test] @@ -19445,7 +19474,7 @@ mod tests { } #[test] - fn platform_daily_top_up_continues_after_unfilled_candidate() { + fn platform_daily_top_up_counts_submitted_zero_fill_candidate_as_strategy_slot() { let prev_date = d(2025, 2, 2); let date = d(2025, 2, 3); let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"]; @@ -19590,7 +19619,7 @@ mod tests { symbol, reason, .. - } if symbol == "000002.SZ" && reason == "daily_top_up_buy" + } if symbol == "000003.SZ" && reason == "daily_top_up_buy" )), "{:?}", decision.order_intents @@ -19598,7 +19627,7 @@ mod tests { assert!( !decision.order_intents.iter().any(|intent| matches!( intent, - OrderIntent::Value { symbol, .. } if symbol == "000003.SZ" + OrderIntent::Value { symbol, .. } if symbol == "000002.SZ" )), "{:?}", decision.order_intents @@ -23409,7 +23438,7 @@ mod tests { assert!(ctx.data.execution_quotes_on(date, symbol).is_empty()); assert!(!ctx.data.execution_quotes_on(date, other_symbol).is_empty()); - let filled = strategy.project_order_value( + let result = strategy.project_order_value( &ctx, &mut projected, date, @@ -23418,7 +23447,7 @@ mod tests { &mut execution_state, ); - assert_eq!(filled, 0); + assert_eq!(result.filled_quantity, 0); assert_eq!( projected.position(symbol).map(|position| position.quantity), None @@ -23560,7 +23589,7 @@ mod tests { .execution_quotes_on(decision_date, other_symbol) .is_empty() ); - let filled = strategy.project_order_value( + let result = strategy.project_order_value( &ctx, &mut projected, decision_date, @@ -23569,10 +23598,10 @@ mod tests { &mut execution_state, ); - assert!(filled > 0); + assert!(result.filled_quantity > 0); assert_eq!( projected.position(symbol).map(|position| position.quantity), - Some(filled) + Some(result.filled_quantity) ); assert!(projected.cash() < portfolio.cash()); }