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
+211 -44
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(
&self,
ctx: &StrategyContext<'_>,
@@ -4149,6 +4178,33 @@ impl PlatformExprStrategy {
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(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "symbol_reverse_index",
@@ -9371,13 +9427,17 @@ impl PlatformExprStrategy {
}
fn field_value(&self, row: &EligibleUniverseSnapshot) -> f64 {
self.field_value_from_caps(row.market_cap_bn, row.free_float_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" => row.market_cap_bn,
"market_cap_bn" => market_cap_bn,
"free_float_cap" | "free_float_market_cap" => {
Self::market_cap_storage_to_strategy_unit(row.free_float_cap_bn)
Self::market_cap_storage_to_strategy_unit(free_float_cap_bn)
}
"free_float_cap_bn" => row.free_float_cap_bn,
_ => Self::market_cap_storage_to_strategy_unit(row.market_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,
collect_risk_decisions: bool,
) -> (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 eligible_symbols = vec![false; ctx.data.symbol_count()];
let execution_day = ctx.data.daily_snapshot_view(date);
@@ -9504,16 +9596,9 @@ impl PlatformExprStrategy {
{
continue;
}
let factor = factor_day
.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),
});
symbol_ids.push(symbol_id);
}
(rows, decisions)
(symbol_ids, decisions)
}
#[cfg(test)]
@@ -9657,6 +9742,21 @@ impl PlatformExprStrategy {
candidate: &EligibleUniverseSnapshot,
stock: &StockExpressionState,
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> {
match field {
"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_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_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_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(),
}
}
@@ -9737,20 +9837,38 @@ impl PlatformExprStrategy {
&self,
candidate: &EligibleUniverseSnapshot,
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 {
match self.config.market_cap_field.as_str() {
"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" => {
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())
.unwrap_or_else(|| self.field_value(candidate))
self.stock_numeric_field_value_from_caps(
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(
@@ -9800,7 +9918,7 @@ impl PlatformExprStrategy {
ctx: &StrategyContext<'_>,
date: NaiveDate,
day: &DayExpressionState,
candidate: &EligibleUniverseSnapshot,
symbol: &str,
stock: &StockExpressionState,
diagnostics: &mut Vec<String>,
) -> Result<bool, BacktestError> {
@@ -9808,13 +9926,13 @@ impl PlatformExprStrategy {
&& let Some(reason) = self.stock_selection_limit_rejection_reason(stock)
{
if diagnostics.len() < 12 {
diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason));
diagnostics.push(format!("{} rejected by {}", symbol, reason));
}
return Ok(false);
}
if !self.stock_passes_expr(ctx, day, stock)? {
if diagnostics.len() < 12 {
diagnostics.push(format!("{} rejected by stock_expr", candidate.symbol));
diagnostics.push(format!("{} rejected by stock_expr", symbol));
}
return Ok(false);
}
@@ -9822,13 +9940,13 @@ impl PlatformExprStrategy {
== PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose
&& ctx
.data
.market_latest_back_adjusted_close(date, &candidate.symbol)
.market_latest_back_adjusted_close(date, symbol)
.is_none()
{
if diagnostics.len() < 12 {
diagnostics.push(format!(
"{} rejected by missing signal-day post-adjusted close",
candidate.symbol
symbol
));
}
return Ok(false);
@@ -9968,13 +10086,14 @@ 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, risk_decisions) = self.selection_universe_and_risk_decisions_with_options(
ctx,
date,
universe_factor_date,
SelectionRiskDeferral::None,
true,
);
let (universe_symbol_ids, risk_decisions) = self
.selection_symbol_ids_and_risk_decisions_with_options(
ctx,
date,
universe_factor_date,
SelectionRiskDeferral::None,
true,
);
let mut diagnostics = Self::selection_risk_decision_diagnostics(
&risk_decisions,
date,
@@ -9982,6 +10101,7 @@ impl PlatformExprStrategy {
5,
);
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);
// 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
// ordered stream instead of materializing a second candidate vector.
if self.rank_reuses_market_cap_order() && self.config.daily_replacement_limit == 0 {
let mut selected = Vec::with_capacity(limit.min(universe.len()));
for candidate in universe {
let stock = self.selection_stock_state_with_factor_date_from_views(
let mut selected = Vec::with_capacity(limit.min(universe_symbol_ids.len()));
for symbol_id in universe_symbol_ids {
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,
date,
stock_factor_date,
&candidate.symbol,
symbol_id,
symbol,
&execution_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 {
continue;
}
@@ -10007,11 +10136,11 @@ impl PlatformExprStrategy {
ctx,
date,
day,
&candidate,
symbol,
&stock,
&mut diagnostics,
)? {
selected.push(candidate.symbol);
selected.push(factor.symbol.clone());
if selected.len() >= limit {
break;
}
@@ -10020,6 +10149,20 @@ impl PlatformExprStrategy {
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 missing_rank_count = 0usize;
let mut missing_rank_examples = Vec::new();
@@ -10114,7 +10257,7 @@ impl PlatformExprStrategy {
ctx,
date,
day,
&candidate,
&candidate.symbol,
&stock,
&mut diagnostics,
)? {
@@ -13920,6 +14063,16 @@ mod tests {
.selection_risk_decisions(&ctx, date, date)
.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();
selection_cfg.risk_config.static_rules.reject_kcb_selection = true;
let selection_strategy = PlatformExprStrategy::new(selection_cfg);
@@ -13937,6 +14090,20 @@ mod tests {
);
assert!(risk_diagnostics.is_empty(), "{risk_diagnostics:?}");
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(
&risk_decisions,
date,