perf: rank transient candidates by symbol id
This commit is contained in:
@@ -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)? {
|
||||||
|
|||||||
Reference in New Issue
Block a user