perf: stream selection candidates by symbol id

This commit is contained in:
boris
2026-09-05 04:44:53 +08:00
parent c2e9c11a9a
commit 0e3c2028d0
+207 -40
View File
@@ -4007,6 +4007,35 @@ impl PlatformExprStrategy {
) )
} }
#[allow(clippy::too_many_arguments)]
fn selection_stock_state_with_factor_date_from_views_by_symbol_id<'a>(
&self,
ctx: &StrategyContext<'a>,
date: NaiveDate,
factor_date: NaiveDate,
symbol_id: u32,
symbol: &str,
execution_day: &DailySnapshotView<'a>,
factor_day: &DailySnapshotView<'a>,
) -> Result<Arc<StockExpressionState>, BacktestError> {
let source = ViewStockStateSnapshotSource {
execution: execution_day,
factor: factor_day,
same_date: factor_date == date,
};
let use_intraday_quote = self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly;
self.stock_state_with_factor_date_and_time_from_source_by_symbol_id(
ctx,
date,
factor_date,
symbol_id,
symbol,
None,
use_intraday_quote,
&source,
)
}
fn stock_decision_rolling_mean( fn stock_decision_rolling_mean(
&self, &self,
ctx: &StrategyContext<'_>, ctx: &StrategyContext<'_>,
@@ -4149,6 +4178,33 @@ impl PlatformExprStrategy {
symbol: symbol.to_string(), symbol: symbol.to_string(),
}) })
})?; })?;
self.stock_state_with_factor_date_and_time_from_source_by_symbol_id(
ctx,
date,
factor_date,
symbol_id,
symbol,
execution_time,
use_intraday_quote,
source,
)
}
#[allow(clippy::too_many_arguments)]
fn stock_state_with_factor_date_and_time_from_source_by_symbol_id<'a, S>(
&self,
ctx: &StrategyContext<'a>,
date: NaiveDate,
factor_date: NaiveDate,
symbol_id: u32,
symbol: &str,
execution_time: Option<NaiveTime>,
use_intraday_quote: bool,
source: &S,
) -> Result<Arc<StockExpressionState>, BacktestError>
where
S: StockStateSnapshotSource<'a>,
{
let shared_symbol = ctx.data.shared_symbol_by_id(symbol_id).ok_or_else(|| { let shared_symbol = ctx.data.shared_symbol_by_id(symbol_id).ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "symbol_reverse_index", kind: "symbol_reverse_index",
@@ -9371,13 +9427,17 @@ impl PlatformExprStrategy {
} }
fn field_value(&self, row: &EligibleUniverseSnapshot) -> f64 { fn field_value(&self, row: &EligibleUniverseSnapshot) -> f64 {
match self.config.market_cap_field.as_str() { self.field_value_from_caps(row.market_cap_bn, row.free_float_cap_bn)
"market_cap_bn" => row.market_cap_bn,
"free_float_cap" | "free_float_market_cap" => {
Self::market_cap_storage_to_strategy_unit(row.free_float_cap_bn)
} }
"free_float_cap_bn" => row.free_float_cap_bn,
_ => Self::market_cap_storage_to_strategy_unit(row.market_cap_bn), fn field_value_from_caps(&self, market_cap_bn: f64, free_float_cap_bn: f64) -> f64 {
match self.config.market_cap_field.as_str() {
"market_cap_bn" => market_cap_bn,
"free_float_cap" | "free_float_market_cap" => {
Self::market_cap_storage_to_strategy_unit(free_float_cap_bn)
}
"free_float_cap_bn" => free_float_cap_bn,
_ => Self::market_cap_storage_to_strategy_unit(market_cap_bn),
} }
} }
@@ -9421,7 +9481,39 @@ impl PlatformExprStrategy {
selection_risk_deferral: SelectionRiskDeferral, selection_risk_deferral: SelectionRiskDeferral,
collect_risk_decisions: bool, collect_risk_decisions: bool,
) -> (Vec<EligibleUniverseSnapshot>, Vec<FidcRiskDecisionAudit>) { ) -> (Vec<EligibleUniverseSnapshot>, Vec<FidcRiskDecisionAudit>) {
let mut rows = Vec::new(); let (symbol_ids, decisions) = self.selection_symbol_ids_and_risk_decisions_with_options(
ctx,
date,
factor_date,
selection_risk_deferral,
collect_risk_decisions,
);
let factor_day = ctx.data.daily_snapshot_view(factor_date);
let rows = symbol_ids
.into_iter()
.map(|symbol_id| {
let factor = factor_day
.factor(symbol_id)
.expect("market-cap order references missing factor row");
EligibleUniverseSnapshot {
symbol: factor.symbol.clone(),
market_cap_bn: decision_market_cap_bn(factor),
free_float_cap_bn: decision_free_float_cap_bn(factor),
}
})
.collect();
(rows, decisions)
}
fn selection_symbol_ids_and_risk_decisions_with_options(
&self,
ctx: &StrategyContext<'_>,
date: NaiveDate,
factor_date: NaiveDate,
selection_risk_deferral: SelectionRiskDeferral,
collect_risk_decisions: bool,
) -> (Vec<u32>, Vec<FidcRiskDecisionAudit>) {
let mut symbol_ids = Vec::new();
let mut decisions = Vec::new(); let mut decisions = Vec::new();
let mut eligible_symbols = vec![false; ctx.data.symbol_count()]; let mut eligible_symbols = vec![false; ctx.data.symbol_count()];
let execution_day = ctx.data.daily_snapshot_view(date); let execution_day = ctx.data.daily_snapshot_view(date);
@@ -9504,16 +9596,9 @@ impl PlatformExprStrategy {
{ {
continue; continue;
} }
let factor = factor_day symbol_ids.push(symbol_id);
.factor(symbol_id)
.expect("market-cap order references missing factor row");
rows.push(EligibleUniverseSnapshot {
symbol: factor.symbol.clone(),
market_cap_bn: decision_market_cap_bn(factor),
free_float_cap_bn: decision_free_float_cap_bn(factor),
});
} }
(rows, decisions) (symbol_ids, decisions)
} }
#[cfg(test)] #[cfg(test)]
@@ -9657,6 +9742,21 @@ impl PlatformExprStrategy {
candidate: &EligibleUniverseSnapshot, candidate: &EligibleUniverseSnapshot,
stock: &StockExpressionState, stock: &StockExpressionState,
field: &str, field: &str,
) -> Option<f64> {
self.stock_numeric_field_value_from_caps(
candidate.market_cap_bn,
candidate.free_float_cap_bn,
stock,
field,
)
}
fn stock_numeric_field_value_from_caps(
&self,
candidate_market_cap_bn: f64,
candidate_free_float_cap_bn: f64,
stock: &StockExpressionState,
field: &str,
) -> Option<f64> { ) -> Option<f64> {
match field { match field {
"market_cap" => Some(stock.market_cap), "market_cap" => Some(stock.market_cap),
@@ -9722,13 +9822,13 @@ impl PlatformExprStrategy {
"is_one_yuan" => Some(if stock.is_one_yuan { 1.0 } else { 0.0 }), "is_one_yuan" => Some(if stock.is_one_yuan { 1.0 } else { 0.0 }),
"is_new_listing" => Some(if stock.is_new_listing { 1.0 } else { 0.0 }), "is_new_listing" => Some(if stock.is_new_listing { 1.0 } else { 0.0 }),
"candidate_market_cap" => Some(Self::market_cap_storage_to_strategy_unit( "candidate_market_cap" => Some(Self::market_cap_storage_to_strategy_unit(
candidate.market_cap_bn, candidate_market_cap_bn,
)), )),
"candidate_market_cap_bn" => Some(candidate.market_cap_bn), "candidate_market_cap_bn" => Some(candidate_market_cap_bn),
"candidate_free_float_cap" => Some(Self::market_cap_storage_to_strategy_unit( "candidate_free_float_cap" => Some(Self::market_cap_storage_to_strategy_unit(
candidate.free_float_cap_bn, candidate_free_float_cap_bn,
)), )),
"candidate_free_float_cap_bn" => Some(candidate.free_float_cap_bn), "candidate_free_float_cap_bn" => Some(candidate_free_float_cap_bn),
other => stock.extra_factors.get(other).copied(), other => stock.extra_factors.get(other).copied(),
} }
} }
@@ -9737,20 +9837,38 @@ impl PlatformExprStrategy {
&self, &self,
candidate: &EligibleUniverseSnapshot, candidate: &EligibleUniverseSnapshot,
stock: &StockExpressionState, stock: &StockExpressionState,
) -> f64 {
self.selection_field_value_from_caps(
candidate.market_cap_bn,
candidate.free_float_cap_bn,
stock,
)
}
fn selection_field_value_from_caps(
&self,
market_cap_bn: f64,
free_float_cap_bn: f64,
stock: &StockExpressionState,
) -> f64 { ) -> f64 {
match self.config.market_cap_field.as_str() { match self.config.market_cap_field.as_str() {
"market_cap" => { "market_cap" => {
return Self::market_cap_storage_to_strategy_unit(candidate.market_cap_bn); return Self::market_cap_storage_to_strategy_unit(market_cap_bn);
} }
"market_cap_bn" => return candidate.market_cap_bn, "market_cap_bn" => return market_cap_bn,
"free_float_cap" | "free_float_market_cap" => { "free_float_cap" | "free_float_market_cap" => {
return Self::market_cap_storage_to_strategy_unit(candidate.free_float_cap_bn); return Self::market_cap_storage_to_strategy_unit(free_float_cap_bn);
} }
"free_float_cap_bn" => return candidate.free_float_cap_bn, "free_float_cap_bn" => return free_float_cap_bn,
_ => {} _ => {}
} }
self.stock_numeric_field_value(candidate, stock, self.config.market_cap_field.as_str()) self.stock_numeric_field_value_from_caps(
.unwrap_or_else(|| self.field_value(candidate)) market_cap_bn,
free_float_cap_bn,
stock,
self.config.market_cap_field.as_str(),
)
.unwrap_or_else(|| self.field_value_from_caps(market_cap_bn, free_float_cap_bn))
} }
fn rank_value( fn rank_value(
@@ -9800,7 +9918,7 @@ impl PlatformExprStrategy {
ctx: &StrategyContext<'_>, ctx: &StrategyContext<'_>,
date: NaiveDate, date: NaiveDate,
day: &DayExpressionState, day: &DayExpressionState,
candidate: &EligibleUniverseSnapshot, symbol: &str,
stock: &StockExpressionState, stock: &StockExpressionState,
diagnostics: &mut Vec<String>, diagnostics: &mut Vec<String>,
) -> Result<bool, BacktestError> { ) -> Result<bool, BacktestError> {
@@ -9808,13 +9926,13 @@ impl PlatformExprStrategy {
&& let Some(reason) = self.stock_selection_limit_rejection_reason(stock) && let Some(reason) = self.stock_selection_limit_rejection_reason(stock)
{ {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason)); diagnostics.push(format!("{} rejected by {}", symbol, reason));
} }
return Ok(false); return Ok(false);
} }
if !self.stock_passes_expr(ctx, day, stock)? { if !self.stock_passes_expr(ctx, day, stock)? {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
diagnostics.push(format!("{} rejected by stock_expr", candidate.symbol)); diagnostics.push(format!("{} rejected by stock_expr", symbol));
} }
return Ok(false); return Ok(false);
} }
@@ -9822,13 +9940,13 @@ impl PlatformExprStrategy {
== PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose == PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose
&& ctx && ctx
.data .data
.market_latest_back_adjusted_close(date, &candidate.symbol) .market_latest_back_adjusted_close(date, symbol)
.is_none() .is_none()
{ {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
diagnostics.push(format!( diagnostics.push(format!(
"{} rejected by missing signal-day post-adjusted close", "{} rejected by missing signal-day post-adjusted close",
candidate.symbol symbol
)); ));
} }
return Ok(false); return Ok(false);
@@ -9968,7 +10086,8 @@ impl PlatformExprStrategy {
// Selection policy is evaluated on the signal day. Lagged execution only // 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 // defers buy/sell risk to the actual execution bar; it must not disable an
// explicitly configured signal-day universe filter. // explicitly configured signal-day universe filter.
let (universe, risk_decisions) = self.selection_universe_and_risk_decisions_with_options( let (universe_symbol_ids, risk_decisions) = self
.selection_symbol_ids_and_risk_decisions_with_options(
ctx, ctx,
date, date,
universe_factor_date, universe_factor_date,
@@ -9982,6 +10101,7 @@ impl PlatformExprStrategy {
5, 5,
); );
let execution_day = ctx.data.daily_snapshot_view(date); let execution_day = ctx.data.daily_snapshot_view(date);
let universe_factor_day = ctx.data.daily_snapshot_view(universe_factor_date);
let factor_day = ctx.data.daily_snapshot_view(stock_factor_date); let factor_day = ctx.data.daily_snapshot_view(stock_factor_date);
// The universe is already stably ordered by market cap. When the // The universe is already stably ordered by market cap. When the
@@ -9989,17 +10109,26 @@ impl PlatformExprStrategy {
// complete ranking for replacement limiting, select directly from the // complete ranking for replacement limiting, select directly from the
// ordered stream instead of materializing a second candidate vector. // ordered stream instead of materializing a second candidate vector.
if self.rank_reuses_market_cap_order() && self.config.daily_replacement_limit == 0 { if self.rank_reuses_market_cap_order() && self.config.daily_replacement_limit == 0 {
let mut selected = Vec::with_capacity(limit.min(universe.len())); let mut selected = Vec::with_capacity(limit.min(universe_symbol_ids.len()));
for candidate in universe { for symbol_id in universe_symbol_ids {
let stock = self.selection_stock_state_with_factor_date_from_views( let factor = universe_factor_day
.factor(symbol_id)
.expect("market-cap order references missing factor row");
let symbol = factor.symbol.as_str();
let stock = self.selection_stock_state_with_factor_date_from_views_by_symbol_id(
ctx, ctx,
date, date,
stock_factor_date, stock_factor_date,
&candidate.symbol, symbol_id,
symbol,
&execution_day, &execution_day,
&factor_day, &factor_day,
)?; )?;
let field_value = self.selection_field_value(&candidate, &stock); let field_value = self.selection_field_value_from_caps(
decision_market_cap_bn(factor),
decision_free_float_cap_bn(factor),
&stock,
);
if !field_value.is_finite() || field_value < band_low || field_value > band_high { if !field_value.is_finite() || field_value < band_low || field_value > band_high {
continue; continue;
} }
@@ -10007,11 +10136,11 @@ impl PlatformExprStrategy {
ctx, ctx,
date, date,
day, day,
&candidate, symbol,
&stock, &stock,
&mut diagnostics, &mut diagnostics,
)? { )? {
selected.push(candidate.symbol); selected.push(factor.symbol.clone());
if selected.len() >= limit { if selected.len() >= limit {
break; break;
} }
@@ -10020,6 +10149,20 @@ impl PlatformExprStrategy {
return Ok((selected, diagnostics, risk_decisions)); return Ok((selected, diagnostics, risk_decisions));
} }
let universe = universe_symbol_ids
.into_iter()
.map(|symbol_id| {
let factor = universe_factor_day
.factor(symbol_id)
.expect("market-cap order references missing factor row");
EligibleUniverseSnapshot {
symbol: factor.symbol.clone(),
market_cap_bn: decision_market_cap_bn(factor),
free_float_cap_bn: decision_free_float_cap_bn(factor),
}
})
.collect::<Vec<_>>();
let mut candidates = Vec::new(); let mut candidates = Vec::new();
let mut missing_rank_count = 0usize; let mut missing_rank_count = 0usize;
let mut missing_rank_examples = Vec::new(); let mut missing_rank_examples = Vec::new();
@@ -10114,7 +10257,7 @@ impl PlatformExprStrategy {
ctx, ctx,
date, date,
day, day,
&candidate, &candidate.symbol,
&stock, &stock,
&mut diagnostics, &mut diagnostics,
)? { )? {
@@ -13920,6 +14063,16 @@ mod tests {
.selection_risk_decisions(&ctx, date, date) .selection_risk_decisions(&ctx, date, date)
.is_empty() .is_empty()
); );
let (default_symbol_ids, default_direct_decisions) = default_strategy
.selection_symbol_ids_and_risk_decisions_with_options(
&ctx,
date,
date,
SelectionRiskDeferral::None,
true,
);
assert_eq!(default_symbol_ids, vec![data.symbol_id(symbol).unwrap()]);
assert!(default_direct_decisions.is_empty());
let mut selection_cfg = PlatformExprStrategyConfig::microcap_rotation(); let mut selection_cfg = PlatformExprStrategyConfig::microcap_rotation();
selection_cfg.risk_config.static_rules.reject_kcb_selection = true; selection_cfg.risk_config.static_rules.reject_kcb_selection = true;
let selection_strategy = PlatformExprStrategy::new(selection_cfg); let selection_strategy = PlatformExprStrategy::new(selection_cfg);
@@ -13937,6 +14090,20 @@ mod tests {
); );
assert!(risk_diagnostics.is_empty(), "{risk_diagnostics:?}"); assert!(risk_diagnostics.is_empty(), "{risk_diagnostics:?}");
let risk_decisions = selection_strategy.selection_risk_decisions(&ctx, date, date); let risk_decisions = selection_strategy.selection_risk_decisions(&ctx, date, date);
let (rejected_symbol_ids, direct_risk_decisions) = selection_strategy
.selection_symbol_ids_and_risk_decisions_with_options(
&ctx,
date,
date,
SelectionRiskDeferral::None,
true,
);
assert!(rejected_symbol_ids.is_empty());
assert_eq!(direct_risk_decisions.len(), risk_decisions.len());
assert_eq!(
direct_risk_decisions[0].rule_code,
risk_decisions[0].rule_code
);
let risk_diagnostics = PlatformExprStrategy::selection_risk_decision_diagnostics( let risk_diagnostics = PlatformExprStrategy::selection_risk_decision_diagnostics(
&risk_decisions, &risk_decisions,
date, date,