perf: rank transient candidates by symbol id

This commit is contained in:
boris
2026-09-05 06:07:23 +08:00
parent 866fe32a8c
commit d7e11be01f
+69 -70
View File
@@ -8,9 +8,11 @@ use rhai::{AST, Dynamic, Engine, Map, Scope};
use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel}; use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel};
use crate::cost::ChinaAShareCostModel; use crate::cost::ChinaAShareCostModel;
#[cfg(test)]
use crate::data::EligibleUniverseSnapshot;
use crate::data::{ use crate::data::{
CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DailySnapshotView, DataSet, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DailySnapshotView, DataSet,
EligibleUniverseSnapshot, PriceField, decision_free_float_cap_bn, decision_market_cap_bn, PriceField, decision_free_float_cap_bn, decision_market_cap_bn,
}; };
use crate::engine::BacktestError; use crate::engine::BacktestError;
use crate::events::OrderSide; use crate::events::OrderSide;
@@ -9441,10 +9443,6 @@ 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 { fn field_value_from_caps(&self, market_cap_bn: f64, free_float_cap_bn: f64) -> f64 {
match self.config.market_cap_field.as_str() { match self.config.market_cap_field.as_str() {
"market_cap_bn" => market_cap_bn, "market_cap_bn" => market_cap_bn,
@@ -9754,6 +9752,7 @@ impl PlatformExprStrategy {
.is_some_and(|suffix| suffix.eq_ignore_ascii_case(".BSE")) .is_some_and(|suffix| suffix.eq_ignore_ascii_case(".BSE"))
} }
#[cfg(test)]
fn stock_numeric_field_value( fn stock_numeric_field_value(
&self, &self,
candidate: &EligibleUniverseSnapshot, candidate: &EligibleUniverseSnapshot,
@@ -9850,18 +9849,6 @@ impl PlatformExprStrategy {
} }
} }
fn selection_field_value(
&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( fn selection_field_value_from_caps(
&self, &self,
market_cap_bn: f64, market_cap_bn: f64,
@@ -9888,11 +9875,12 @@ impl PlatformExprStrategy {
.unwrap_or_else(|| self.field_value_from_caps(market_cap_bn, free_float_cap_bn)) .unwrap_or_else(|| self.field_value_from_caps(market_cap_bn, free_float_cap_bn))
} }
fn rank_value( fn rank_value_from_caps(
&self, &self,
ctx: &StrategyContext<'_>, ctx: &StrategyContext<'_>,
day: &DayExpressionState, day: &DayExpressionState,
candidate: &EligibleUniverseSnapshot, market_cap_bn: f64,
free_float_cap_bn: f64,
stock: &StockExpressionState, stock: &StockExpressionState,
) -> Result<f64, BacktestError> { ) -> Result<f64, BacktestError> {
if !self.config.rank_expr.trim().is_empty() { if !self.config.rank_expr.trim().is_empty() {
@@ -9906,22 +9894,18 @@ impl PlatformExprStrategy {
let rank_by = self.config.rank_by.as_str(); let rank_by = self.config.rank_by.as_str();
match rank_by { match rank_by {
"market_cap" => { "market_cap" => {
return Ok(Self::market_cap_storage_to_strategy_unit( return Ok(Self::market_cap_storage_to_strategy_unit(market_cap_bn));
candidate.market_cap_bn,
));
} }
"market_cap_bn" => return Ok(candidate.market_cap_bn), "market_cap_bn" => return Ok(market_cap_bn),
"free_float_cap" | "free_float_market_cap" => { "free_float_cap" | "free_float_market_cap" => {
return Ok(Self::market_cap_storage_to_strategy_unit( return Ok(Self::market_cap_storage_to_strategy_unit(free_float_cap_bn));
candidate.free_float_cap_bn,
));
} }
"free_float_cap_bn" => return Ok(candidate.free_float_cap_bn), "free_float_cap_bn" => return Ok(free_float_cap_bn),
_ => {} _ => {}
} }
Ok(self Ok(self
.stock_numeric_field_value(candidate, stock, rank_by) .stock_numeric_field_value_from_caps(market_cap_bn, free_float_cap_bn, stock, rank_by)
.unwrap_or_else(|| self.field_value(candidate))) .unwrap_or_else(|| self.field_value_from_caps(market_cap_bn, free_float_cap_bn)))
} }
fn rank_reuses_market_cap_order(&self) -> bool { fn rank_reuses_market_cap_order(&self) -> bool {
@@ -10236,34 +10220,31 @@ impl PlatformExprStrategy {
let factor = universe_factor_day let factor = universe_factor_day
.factor(symbol_id) .factor(symbol_id)
.expect("market-cap order references missing factor row"); .expect("market-cap order references missing factor row");
let candidate = EligibleUniverseSnapshot { let symbol = factor.symbol.as_str();
symbol: factor.symbol.clone(), let market_cap_bn = decision_market_cap_bn(factor);
market_cap_bn: decision_market_cap_bn(factor), let free_float_cap_bn = decision_free_float_cap_bn(factor);
free_float_cap_bn: decision_free_float_cap_bn(factor),
};
let stock = self.uncached_selection_stock_state_from_views_by_symbol_id( let stock = self.uncached_selection_stock_state_from_views_by_symbol_id(
ctx, ctx,
date, date,
stock_factor_date, stock_factor_date,
symbol_id, symbol_id,
&candidate.symbol, 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(market_cap_bn, free_float_cap_bn, &stock);
if !field_value.is_finite() { if !field_value.is_finite() {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
diagnostics.push(format!( diagnostics.push(format!("{symbol} rejected by missing selection field"));
"{} rejected by missing selection field",
candidate.symbol
));
} }
continue; continue;
} }
if field_value < band_low || field_value > band_high { if field_value < band_low || field_value > band_high {
continue; continue;
} }
let rank_value = self.rank_value(ctx, day, &candidate, &stock)?; let rank_value =
self.rank_value_from_caps(ctx, day, market_cap_bn, free_float_cap_bn, &stock)?;
if !rank_value.is_finite() { if !rank_value.is_finite() {
// Model-score artifacts intentionally contain only the PIT-eligible // Model-score artifacts intentionally contain only the PIT-eligible
// ranked universe. Do not report a missing score for a symbol that // ranked universe. Do not report a missing score for a symbol that
@@ -10272,13 +10253,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!("{symbol} rejected by {reason}"));
} }
continue; continue;
} }
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!("{symbol} rejected by stock_expr"));
} }
continue; continue;
} }
@@ -10286,26 +10267,25 @@ 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", "{symbol} rejected by missing signal-day post-adjusted close"
candidate.symbol
)); ));
} }
continue; continue;
} }
missing_rank_count += 1; missing_rank_count += 1;
if missing_rank_examples.len() < 5 { if missing_rank_examples.len() < 5 {
missing_rank_examples.push(candidate.symbol.clone()); missing_rank_examples.push(factor.symbol.clone());
} }
continue; continue;
} }
let state_index = candidate_states.len(); let state_index = candidate_states.len();
candidate_states.push(stock); candidate_states.push(stock);
candidates.push((candidate, rank_value, state_index)); candidates.push((symbol_id, rank_value, state_index));
} }
if !self.rank_reuses_market_cap_order() { if !self.rank_reuses_market_cap_order() {
candidates.sort_by(|lhs, rhs| { candidates.sort_by(|lhs, rhs| {
@@ -10321,7 +10301,15 @@ impl PlatformExprStrategy {
.unwrap_or(std::cmp::Ordering::Equal) .unwrap_or(std::cmp::Ordering::Equal)
}; };
if ordering == std::cmp::Ordering::Equal { if ordering == std::cmp::Ordering::Equal {
lhs.0.symbol.cmp(&rhs.0.symbol) let lhs_symbol = &universe_factor_day
.factor(lhs.0)
.expect("ranked candidate references missing factor row")
.symbol;
let rhs_symbol = &universe_factor_day
.factor(rhs.0)
.expect("ranked candidate references missing factor row")
.symbol;
lhs_symbol.cmp(rhs_symbol)
} else { } else {
ordering ordering
} }
@@ -10329,19 +10317,22 @@ impl PlatformExprStrategy {
} }
let mut selected = Vec::new(); let mut selected = Vec::new();
for (candidate, _, state_index) in candidates { for (symbol_id, _, state_index) in candidates {
let factor = universe_factor_day
.factor(symbol_id)
.expect("ranked candidate references missing factor row");
let stock = &candidate_states[state_index]; let stock = &candidate_states[state_index];
if !self.selection_candidate_passes_filters( if !self.selection_candidate_passes_filters(
ctx, ctx,
date, date,
day, day,
&candidate.symbol, &factor.symbol,
stock, stock,
diagnostics, diagnostics,
)? { )? {
continue; continue;
} }
selected.push(candidate.symbol.clone()); selected.push(factor.symbol.clone());
if selected.len() >= limit { if selected.len() >= limit {
break; break;
} }
@@ -10979,26 +10970,24 @@ impl PlatformExprStrategy {
let factor = universe_factor_day let factor = universe_factor_day
.factor(symbol_id) .factor(symbol_id)
.expect("market-cap order references missing factor row"); .expect("market-cap order references missing factor row");
let candidate = EligibleUniverseSnapshot { let symbol = factor.symbol.as_str();
symbol: factor.symbol.clone(), let market_cap_bn = decision_market_cap_bn(factor);
market_cap_bn: decision_market_cap_bn(factor), let free_float_cap_bn = decision_free_float_cap_bn(factor);
free_float_cap_bn: decision_free_float_cap_bn(factor),
};
let stock = self.uncached_selection_stock_state_from_views_by_symbol_id( let stock = self.uncached_selection_stock_state_from_views_by_symbol_id(
ctx, ctx,
date, date,
stock_factor_date, stock_factor_date,
symbol_id, symbol_id,
&candidate.symbol, 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(market_cap_bn, free_float_cap_bn, &stock);
if !field_value.is_finite() { if !field_value.is_finite() {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
diagnostics.push(format!( diagnostics.push(format!(
"{} quote_plan rejected by missing selection field", "{symbol} quote_plan rejected by missing selection field"
candidate.symbol
)); ));
} }
continue; continue;
@@ -11006,29 +10995,27 @@ impl PlatformExprStrategy {
if field_value < band_low || field_value > band_high { if field_value < band_low || field_value > band_high {
continue; continue;
} }
let rank_value = self.rank_value(ctx, day, &candidate, &stock)?; let rank_value =
self.rank_value_from_caps(ctx, day, market_cap_bn, free_float_cap_bn, &stock)?;
if !rank_value.is_finite() { if !rank_value.is_finite() {
// The score universe is already filtered by the signal-day // The score universe is already filtered by the signal-day
// candidate contract. Missing rank is actionable only when the // candidate contract. Missing rank is actionable only when the
// daily portion of that same contract still admits the symbol. // daily portion of that same contract still admits the symbol.
if !self.stock_passes_quote_plan_filter(ctx, day, &stock, quote_usage)? { if !self.stock_passes_quote_plan_filter(ctx, day, &stock, quote_usage)? {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
diagnostics.push(format!( diagnostics.push(format!("{symbol} quote_plan rejected by stock_expr"));
"{} quote_plan rejected by stock_expr",
candidate.symbol
));
} }
continue; continue;
} }
missing_rank_count += 1; missing_rank_count += 1;
if missing_rank_examples.len() < 5 { if missing_rank_examples.len() < 5 {
missing_rank_examples.push(candidate.symbol.clone()); missing_rank_examples.push(factor.symbol.clone());
} }
continue; continue;
} }
let state_index = candidate_states.len(); let state_index = candidate_states.len();
candidate_states.push(stock); candidate_states.push(stock);
candidates.push((candidate.symbol.clone(), rank_value, state_index)); candidates.push((symbol_id, rank_value, state_index));
} }
candidates.sort_by(|lhs, rhs| { candidates.sort_by(|lhs, rhs| {
let ordering = if self.config.rank_desc { let ordering = if self.config.rank_desc {
@@ -11041,7 +11028,15 @@ impl PlatformExprStrategy {
.unwrap_or(std::cmp::Ordering::Equal) .unwrap_or(std::cmp::Ordering::Equal)
}; };
if ordering == std::cmp::Ordering::Equal { if ordering == std::cmp::Ordering::Equal {
lhs.0.cmp(&rhs.0) let lhs_symbol = &universe_factor_day
.factor(lhs.0)
.expect("quote-plan candidate references missing factor row")
.symbol;
let rhs_symbol = &universe_factor_day
.factor(rhs.0)
.expect("quote-plan candidate references missing factor row")
.symbol;
lhs_symbol.cmp(rhs_symbol)
} else { } else {
ordering ordering
} }
@@ -11050,7 +11045,11 @@ impl PlatformExprStrategy {
let mut quote_candidate_symbols = Vec::new(); let mut quote_candidate_symbols = Vec::new();
let mut selected_symbols = Vec::new(); let mut selected_symbols = Vec::new();
let mut processed_scope = 0usize; let mut processed_scope = 0usize;
for (symbol, _, state_index) in &candidates { for (symbol_id, _, state_index) in &candidates {
let symbol = &universe_factor_day
.factor(*symbol_id)
.expect("quote-plan candidate references missing factor row")
.symbol;
let stock = &candidate_states[*state_index]; let stock = &candidate_states[*state_index];
processed_scope += 1; processed_scope += 1;
if !self.stock_passes_quote_plan_filter(ctx, day, stock, quote_usage)? { if !self.stock_passes_quote_plan_filter(ctx, day, stock, quote_usage)? {