From 5ff8ddca92871fcde3bf720420a1d4b8315b12c0 Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 25 Aug 2026 08:53:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B1=E4=BA=AB=E9=80=89=E8=82=A1=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=B9=B6=E5=90=88=E5=B9=B6=E9=A3=8E=E6=8E=A7=E6=89=AB?= =?UTF-8?q?=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 316 +++++++++++------- 1 file changed, 201 insertions(+), 115 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index b5ab1e7..07ad337 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -952,7 +952,7 @@ pub struct PlatformExprStrategy { stock_text_factors_required: bool, stock_state_cache_date: RefCell>, stock_state_cache: RefCell< - AHashMap<(NaiveDate, NaiveDate, u32, Option, bool), StockExpressionState>, + AHashMap<(NaiveDate, NaiveDate, u32, Option, bool), Arc>, >, } @@ -3155,7 +3155,7 @@ impl PlatformExprStrategy { ctx, execution_date, symbol, - &self.stock_state(ctx, execution_date, symbol)?, + self.stock_state(ctx, execution_date, symbol)?.as_ref(), )? .is_some() { @@ -3614,7 +3614,7 @@ impl PlatformExprStrategy { ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str, - ) -> Result { + ) -> Result, BacktestError> { self.stock_state_with_factor_date(ctx, date, date, symbol) } @@ -3624,7 +3624,7 @@ impl PlatformExprStrategy { date: NaiveDate, factor_date: NaiveDate, symbol: &str, - ) -> Result { + ) -> Result, BacktestError> { self.stock_state_with_factor_date_and_time(ctx, date, factor_date, symbol, None, true) } @@ -3634,7 +3634,7 @@ impl PlatformExprStrategy { date: NaiveDate, factor_date: NaiveDate, symbol: &str, - ) -> Result { + ) -> Result, BacktestError> { let use_intraday_quote = self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly; self.stock_state_with_factor_date_and_time( ctx, @@ -3681,7 +3681,7 @@ impl PlatformExprStrategy { date: NaiveDate, symbol: &str, execution_time: Option, - ) -> Result { + ) -> Result, BacktestError> { self.stock_state_with_factor_date_and_time(ctx, date, date, symbol, execution_time, true) } @@ -3745,7 +3745,7 @@ impl PlatformExprStrategy { symbol: &str, execution_time: Option, use_intraday_quote: bool, - ) -> Result { + ) -> Result, BacktestError> { let symbol_id = ctx.data.symbol_id(symbol).ok_or_else(|| { BacktestError::Data(crate::data::DataSetError::MissingSnapshot { kind: "symbol_index", @@ -3768,7 +3768,7 @@ impl PlatformExprStrategy { use_intraday_quote, ); if let Some(state) = self.stock_state_cache.borrow().get(&cache_key) { - return Ok(state.clone()); + return Ok(Arc::clone(state)); } let market = ctx @@ -3921,7 +3921,7 @@ impl PlatformExprStrategy { BTreeMap::new() }; - let state = StockExpressionState { + let state = Arc::new(StockExpressionState { symbol: symbol.to_string(), symbol_id, market_cap, @@ -3997,10 +3997,10 @@ impl PlatformExprStrategy { } else { BTreeMap::new() }, - }; + }); self.stock_state_cache .borrow_mut() - .insert(cache_key, state.clone()); + .insert(cache_key, Arc::clone(&state)); Ok(state) } @@ -7752,7 +7752,7 @@ impl PlatformExprStrategy { ctx: &StrategyContext<'_>, date: NaiveDate, symbol: Option<&str>, - ) -> Result, BacktestError> { + ) -> Result>, BacktestError> { let Some(symbol) = symbol else { return Ok(None); }; @@ -7802,7 +7802,7 @@ impl PlatformExprStrategy { if !self.action_when_matches( ctx, day, - stock_state.as_ref(), + stock_state.as_deref(), when_expr.as_deref(), )? { continue; @@ -7817,7 +7817,7 @@ impl PlatformExprStrategy { match kind { PlatformExplicitOrderKind::Shares => { let quantity = - self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?; + self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?; if quantity == 0 { continue; } @@ -7829,7 +7829,7 @@ impl PlatformExprStrategy { } PlatformExplicitOrderKind::LimitShares => { let quantity = - self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?; + self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?; if quantity == 0 { continue; } @@ -7837,7 +7837,7 @@ impl PlatformExprStrategy { ctx, limit_price_expr.as_deref().unwrap_or_default(), day, - stock_state.as_ref(), + stock_state.as_deref(), None, )?; intents.push(OrderIntent::LimitShares { @@ -7849,7 +7849,7 @@ impl PlatformExprStrategy { } PlatformExplicitOrderKind::Lots => { let lots = - self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?; + self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?; if lots == 0 { continue; } @@ -7861,7 +7861,7 @@ impl PlatformExprStrategy { } PlatformExplicitOrderKind::LimitLots => { let lots = - self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?; + self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?; if lots == 0 { continue; } @@ -7869,7 +7869,7 @@ impl PlatformExprStrategy { ctx, limit_price_expr.as_deref().unwrap_or_default(), day, - stock_state.as_ref(), + stock_state.as_deref(), None, )?; intents.push(OrderIntent::LimitLots { @@ -7881,7 +7881,7 @@ impl PlatformExprStrategy { } PlatformExplicitOrderKind::TargetShares => { let target_quantity = - self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?; + self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?; intents.push(OrderIntent::TargetShares { symbol: symbol.clone(), target_quantity, @@ -7890,12 +7890,12 @@ impl PlatformExprStrategy { } PlatformExplicitOrderKind::LimitTargetShares => { let target_quantity = - self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?; + self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?; let limit_price = self.eval_float( ctx, limit_price_expr.as_deref().unwrap_or_default(), day, - stock_state.as_ref(), + stock_state.as_deref(), None, )?; intents.push(OrderIntent::LimitTargetShares { @@ -7906,8 +7906,13 @@ impl PlatformExprStrategy { }); } PlatformExplicitOrderKind::Value => { - let value = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let value = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; if value.abs() <= f64::EPSILON { continue; } @@ -7919,21 +7924,38 @@ impl PlatformExprStrategy { } PlatformExplicitOrderKind::VwapValue | PlatformExplicitOrderKind::TwapValue => { - let value = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let value = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; if value.abs() <= f64::EPSILON { continue; } let start_time = start_time_expr .as_deref() .map(|expr| { - self.eval_time_expr(ctx, expr, day, stock_state.as_ref(), None) + self.eval_time_expr( + ctx, + expr, + day, + stock_state.as_deref(), + None, + ) }) .transpose()?; let end_time = end_time_expr .as_deref() .map(|expr| { - self.eval_time_expr(ctx, expr, day, stock_state.as_ref(), None) + self.eval_time_expr( + ctx, + expr, + day, + stock_state.as_deref(), + None, + ) }) .transpose()?; intents.push(OrderIntent::AlgoValue { @@ -7950,8 +7972,13 @@ impl PlatformExprStrategy { }); } PlatformExplicitOrderKind::LimitValue => { - let value = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let value = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; if value.abs() <= f64::EPSILON { continue; } @@ -7959,7 +7986,7 @@ impl PlatformExprStrategy { ctx, limit_price_expr.as_deref().unwrap_or_default(), day, - stock_state.as_ref(), + stock_state.as_deref(), None, )?; intents.push(OrderIntent::LimitValue { @@ -7970,8 +7997,13 @@ impl PlatformExprStrategy { }); } PlatformExplicitOrderKind::Percent => { - let percent = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let percent = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; if percent.abs() <= f64::EPSILON { continue; } @@ -7983,21 +8015,38 @@ impl PlatformExprStrategy { } PlatformExplicitOrderKind::VwapPercent | PlatformExplicitOrderKind::TwapPercent => { - let percent = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let percent = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; if percent.abs() <= f64::EPSILON { continue; } let start_time = start_time_expr .as_deref() .map(|expr| { - self.eval_time_expr(ctx, expr, day, stock_state.as_ref(), None) + self.eval_time_expr( + ctx, + expr, + day, + stock_state.as_deref(), + None, + ) }) .transpose()?; let end_time = end_time_expr .as_deref() .map(|expr| { - self.eval_time_expr(ctx, expr, day, stock_state.as_ref(), None) + self.eval_time_expr( + ctx, + expr, + day, + stock_state.as_deref(), + None, + ) }) .transpose()?; intents.push(OrderIntent::AlgoPercent { @@ -8014,8 +8063,13 @@ impl PlatformExprStrategy { }); } PlatformExplicitOrderKind::LimitPercent => { - let percent = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let percent = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; if percent.abs() <= f64::EPSILON { continue; } @@ -8023,7 +8077,7 @@ impl PlatformExprStrategy { ctx, limit_price_expr.as_deref().unwrap_or_default(), day, - stock_state.as_ref(), + stock_state.as_deref(), None, )?; intents.push(OrderIntent::LimitPercent { @@ -8034,8 +8088,13 @@ impl PlatformExprStrategy { }); } PlatformExplicitOrderKind::TargetValue => { - let target_value = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let target_value = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; intents.push(OrderIntent::TargetValue { symbol: symbol.clone(), target_value, @@ -8043,13 +8102,18 @@ impl PlatformExprStrategy { }); } PlatformExplicitOrderKind::LimitTargetValue => { - let target_value = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let target_value = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; let limit_price = self.eval_float( ctx, limit_price_expr.as_deref().unwrap_or_default(), day, - stock_state.as_ref(), + stock_state.as_deref(), None, )?; intents.push(OrderIntent::LimitTargetValue { @@ -8060,8 +8124,13 @@ impl PlatformExprStrategy { }); } PlatformExplicitOrderKind::TargetPercent => { - let target_percent = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let target_percent = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; intents.push(OrderIntent::TargetPercent { symbol: symbol.clone(), target_percent, @@ -8069,13 +8138,18 @@ impl PlatformExprStrategy { }); } PlatformExplicitOrderKind::LimitTargetPercent => { - let target_percent = - self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?; + let target_percent = self.eval_float( + ctx, + amount_expr, + day, + stock_state.as_deref(), + None, + )?; let limit_price = self.eval_float( ctx, limit_price_expr.as_deref().unwrap_or_default(), day, - stock_state.as_ref(), + stock_state.as_deref(), None, )?; intents.push(OrderIntent::LimitTargetPercent { @@ -8098,7 +8172,7 @@ impl PlatformExprStrategy { if !self.action_when_matches( ctx, day, - stock_state.as_ref(), + stock_state.as_deref(), when_expr.as_deref(), )? { continue; @@ -8109,7 +8183,7 @@ impl PlatformExprStrategy { ctx, order_id_expr.as_deref().unwrap_or_default(), day, - stock_state.as_ref(), + stock_state.as_deref(), None, )?; if order_id == 0 { @@ -8503,6 +8577,7 @@ impl PlatformExprStrategy { } } + #[cfg(test)] fn selectable_universe_on( &self, ctx: &StrategyContext<'_>, @@ -8524,14 +8599,30 @@ impl PlatformExprStrategy { factor_date: NaiveDate, selection_risk_deferral: SelectionRiskDeferral, ) -> Vec { + self.selection_universe_and_risk_decisions_with_options( + ctx, + date, + factor_date, + selection_risk_deferral, + false, + ) + .0 + } + + fn selection_universe_and_risk_decisions_with_options( + &self, + ctx: &StrategyContext<'_>, + date: NaiveDate, + factor_date: NaiveDate, + selection_risk_deferral: SelectionRiskDeferral, + collect_risk_decisions: bool, + ) -> (Vec, Vec) { let mut rows = Vec::new(); + let mut decisions = Vec::new(); let factor_rows = ctx.data.factor_snapshot_rows_on(factor_date); let factor_symbol_ids = ctx.data.factor_symbol_ids_on(factor_date); debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len()); for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) { - if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() { - continue; - } if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) { continue; } @@ -8547,12 +8638,43 @@ impl PlatformExprStrategy { let Some(market) = ctx.data.market_by_symbol_id(date, symbol_id) else { continue; }; - if let Some(_reason) = - self.selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market) + let (reject_from_universe, selection_decision) = if collect_risk_decisions { + let decision = ChinaAShareRiskControl::selection_rejection_decision_with_config( + date, + candidate, + market, + ctx.data.instrument(&factor.symbol), + &self.config.risk_config, + ); + let rejected = decision.as_ref().is_some_and(|decision| { + !selection_risk_deferral.should_defer_rejection(&decision.rule_code) + }); + (rejected, decision) + } else { + let reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, + candidate, + market, + ctx.data.instrument(&factor.symbol), + &self.config.risk_config, + ); + ( + reason.is_some_and(|reason| { + !selection_risk_deferral.should_defer_rejection(reason) + }), + None, + ) + }; + if let Some(decision) = selection_decision + && !selection_risk_deferral.should_suppress_diagnostic(&decision.rule_code) { - if !selection_risk_deferral.should_defer_rejection(_reason) { - continue; - } + decisions.push(decision); + } + if reject_from_universe { + continue; + } + if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() { + continue; } if !self.stock_passes_universe_exclude(candidate, market) { continue; @@ -8574,9 +8696,10 @@ impl PlatformExprStrategy { .unwrap_or(std::cmp::Ordering::Equal) .then_with(|| left.symbol.cmp(&right.symbol)) }); - rows + (rows, decisions) } + #[cfg(test)] fn selection_risk_decisions( &self, ctx: &StrategyContext<'_>, @@ -8591,6 +8714,7 @@ impl PlatformExprStrategy { ) } + #[cfg(test)] fn selection_risk_decisions_with_options( &self, ctx: &StrategyContext<'_>, @@ -8598,40 +8722,14 @@ impl PlatformExprStrategy { factor_date: NaiveDate, selection_risk_deferral: SelectionRiskDeferral, ) -> Vec { - let mut decisions = Vec::new(); - let factor_rows = ctx.data.factor_snapshot_rows_on(factor_date); - let factor_symbol_ids = ctx.data.factor_symbol_ids_on(factor_date); - debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len()); - for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) { - if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) { - continue; - } - let synthetic_candidate; - let candidate = - if let Some(candidate) = ctx.data.candidate_by_symbol_id(date, symbol_id) { - candidate - } else { - synthetic_candidate = - crate::data::missing_candidate_risk_state(date, &factor.symbol); - &synthetic_candidate - }; - let Some(market) = ctx.data.market_by_symbol_id(date, symbol_id) else { - continue; - }; - if let Some(decision) = ChinaAShareRiskControl::selection_rejection_decision_with_config( - date, - candidate, - market, - ctx.data.instrument(&factor.symbol), - &self.config.risk_config, - ) { - if selection_risk_deferral.should_suppress_diagnostic(&decision.rule_code) { - continue; - } - decisions.push(decision); - } - } - decisions + self.selection_universe_and_risk_decisions_with_options( + ctx, + date, + factor_date, + selection_risk_deferral, + true, + ) + .1 } fn selection_risk_decision_diagnostics( @@ -8667,23 +8765,6 @@ impl PlatformExprStrategy { diagnostics } - fn selection_risk_rejection_reason( - &self, - ctx: &StrategyContext<'_>, - date: NaiveDate, - symbol: &str, - candidate: &crate::data::CandidateEligibility, - market: &DailyMarketSnapshot, - ) -> Option<&'static str> { - ChinaAShareRiskControl::selection_rejection_reason_with_config( - date, - candidate, - market, - ctx.data.instrument(symbol), - &self.config.risk_config, - ) - } - fn stock_selection_limit_rejection_reason( &self, stock: &StockExpressionState, @@ -9013,8 +9094,13 @@ impl PlatformExprStrategy { // Selection policy is evaluated on the signal day. Lagged execution only // defers buy/sell risk to the actual execution bar; it must not disable an // explicitly configured signal-day universe filter. - let universe = self.selectable_universe_on(ctx, date, universe_factor_date); - let risk_decisions = self.selection_risk_decisions(ctx, date, universe_factor_date); + let (universe, risk_decisions) = self.selection_universe_and_risk_decisions_with_options( + ctx, + date, + universe_factor_date, + SelectionRiskDeferral::None, + true, + ); let mut diagnostics = Self::selection_risk_decision_diagnostics( &risk_decisions, date, @@ -11534,7 +11620,7 @@ impl Strategy for PlatformExprStrategy { ctx, execution_date, symbol, - &self.stock_state(ctx, execution_date, symbol)?, + self.stock_state(ctx, execution_date, symbol)?.as_ref(), )? .is_some() {