按选股表达式跳过无关盘中quote

This commit is contained in:
boris
2026-06-21 01:37:04 +08:00
parent 192ac3f843
commit 5b34f3b55b
+118 -45
View File
@@ -553,14 +553,12 @@ pub struct PlatformExprStrategy {
prelude_numeric_constants: HashMap<String, f64>, prelude_numeric_constants: HashMap<String, f64>,
compact_stock_filter_expr: String, compact_stock_filter_expr: String,
stock_filter_quote_usage: StockFilterQuoteUsage, stock_filter_quote_usage: StockFilterQuoteUsage,
selection_quote_usage: StockFilterQuoteUsage,
stock_rolling_requirements: StockRollingRequirements, stock_rolling_requirements: StockRollingRequirements,
stock_extra_factors_required: bool, stock_extra_factors_required: bool,
stock_state_cache_date: RefCell<Option<NaiveDate>>, stock_state_cache_date: RefCell<Option<NaiveDate>>,
stock_state_cache: stock_state_cache: RefCell<
RefCell<HashMap<(NaiveDate, NaiveDate, String, Option<NaiveTime>), StockExpressionState>>, HashMap<(NaiveDate, NaiveDate, String, Option<NaiveTime>, bool), StockExpressionState>,
scheduled_quote_cache_date: RefCell<Option<NaiveDate>>,
scheduled_quote_cache: RefCell<
HashMap<(NaiveDate, String, NaiveDateTime), Option<crate::data::IntradayExecutionQuote>>,
>, >,
} }
@@ -649,6 +647,8 @@ impl PlatformExprStrategy {
let compact_stock_filter_expr = Self::compact_expr(&normalized_stock_filter_expr); let compact_stock_filter_expr = Self::compact_expr(&normalized_stock_filter_expr);
let stock_filter_quote_usage = let stock_filter_quote_usage =
Self::stock_filter_quote_usage_for_expr(&normalized_stock_filter_expr); Self::stock_filter_quote_usage_for_expr(&normalized_stock_filter_expr);
let selection_quote_usage =
Self::selection_quote_usage_for_config(&config, &normalized_stock_filter_expr);
let stock_rolling_requirements = let stock_rolling_requirements =
Self::stock_rolling_requirements_for_config(&config, &normalized_stock_filter_expr); Self::stock_rolling_requirements_for_config(&config, &normalized_stock_filter_expr);
let stock_extra_factors_required = Self::stock_extra_factors_required_for_config( let stock_extra_factors_required = Self::stock_extra_factors_required_for_config(
@@ -672,12 +672,11 @@ impl PlatformExprStrategy {
prelude_numeric_constants, prelude_numeric_constants,
compact_stock_filter_expr, compact_stock_filter_expr,
stock_filter_quote_usage, stock_filter_quote_usage,
selection_quote_usage,
stock_rolling_requirements, stock_rolling_requirements,
stock_extra_factors_required, stock_extra_factors_required,
stock_state_cache_date: RefCell::new(None), stock_state_cache_date: RefCell::new(None),
stock_state_cache: RefCell::new(HashMap::new()), stock_state_cache: RefCell::new(HashMap::new()),
scheduled_quote_cache_date: RefCell::new(None),
scheduled_quote_cache: RefCell::new(HashMap::new()),
} }
} }
@@ -1198,22 +1197,22 @@ impl PlatformExprStrategy {
) )
} }
fn aiquant_scheduled_quote( fn aiquant_scheduled_quote<'a>(
&self, &self,
ctx: &StrategyContext<'_>, ctx: &'a StrategyContext<'_>,
date: NaiveDate, date: NaiveDate,
symbol: &str, symbol: &str,
) -> Option<crate::data::IntradayExecutionQuote> { ) -> Option<&'a crate::data::IntradayExecutionQuote> {
self.aiquant_scheduled_quote_at_time(ctx, date, symbol, None) self.aiquant_scheduled_quote_at_time(ctx, date, symbol, None)
} }
fn aiquant_scheduled_quote_at_time( fn aiquant_scheduled_quote_at_time<'a>(
&self, &self,
ctx: &StrategyContext<'_>, ctx: &'a StrategyContext<'_>,
date: NaiveDate, date: NaiveDate,
symbol: &str, symbol: &str,
execution_time: Option<NaiveTime>, execution_time: Option<NaiveTime>,
) -> Option<crate::data::IntradayExecutionQuote> { ) -> Option<&'a crate::data::IntradayExecutionQuote> {
if !self.config.aiquant_transaction_cost { if !self.config.aiquant_transaction_cost {
return None; return None;
} }
@@ -1224,28 +1223,11 @@ impl PlatformExprStrategy {
&ProjectedExecutionState::default(), &ProjectedExecutionState::default(),
execution_time, execution_time,
); );
{ ctx.data
let mut cache_date = self.scheduled_quote_cache_date.borrow_mut();
if *cache_date != Some(date) {
self.scheduled_quote_cache.borrow_mut().clear();
*cache_date = Some(date);
}
}
let cache_key = (date, symbol.to_string(), start_cursor);
if let Some(cached) = self.scheduled_quote_cache.borrow().get(&cache_key) {
return cached.clone();
}
let quote = ctx
.data
.execution_quotes_on(date, symbol) .execution_quotes_on(date, symbol)
.iter() .iter()
.filter(|quote| quote.timestamp <= start_cursor) .filter(|quote| quote.timestamp <= start_cursor)
.max_by_key(|quote| quote.timestamp) .max_by_key(|quote| quote.timestamp)
.cloned();
self.scheduled_quote_cache
.borrow_mut()
.insert(cache_key, quote.clone());
quote
} }
fn aiquant_scheduled_buy_price( fn aiquant_scheduled_buy_price(
@@ -1255,7 +1237,7 @@ impl PlatformExprStrategy {
symbol: &str, symbol: &str,
) -> Option<f64> { ) -> Option<f64> {
self.aiquant_scheduled_quote(ctx, date, symbol) self.aiquant_scheduled_quote(ctx, date, symbol)
.and_then(|quote| self.projected_quote_raw_price(&quote, OrderSide::Buy)) .and_then(|quote| self.projected_quote_raw_price(quote, OrderSide::Buy))
} }
fn aiquant_scheduled_sell_price_at_time( fn aiquant_scheduled_sell_price_at_time(
@@ -1266,7 +1248,7 @@ impl PlatformExprStrategy {
execution_time: Option<NaiveTime>, execution_time: Option<NaiveTime>,
) -> Option<f64> { ) -> Option<f64> {
self.aiquant_scheduled_quote_at_time(ctx, date, symbol, execution_time) self.aiquant_scheduled_quote_at_time(ctx, date, symbol, execution_time)
.and_then(|quote| self.projected_quote_raw_price(&quote, OrderSide::Sell)) .and_then(|quote| self.projected_quote_raw_price(quote, OrderSide::Sell))
} }
fn aiquant_scheduled_last_price( fn aiquant_scheduled_last_price(
@@ -2211,7 +2193,25 @@ impl PlatformExprStrategy {
factor_date: NaiveDate, factor_date: NaiveDate,
symbol: &str, symbol: &str,
) -> Result<StockExpressionState, BacktestError> { ) -> Result<StockExpressionState, BacktestError> {
self.stock_state_with_factor_date_and_time(ctx, date, factor_date, symbol, None) self.stock_state_with_factor_date_and_time(ctx, date, factor_date, symbol, None, true)
}
fn selection_stock_state_with_factor_date(
&self,
ctx: &StrategyContext<'_>,
date: NaiveDate,
factor_date: NaiveDate,
symbol: &str,
) -> Result<StockExpressionState, BacktestError> {
let use_intraday_quote = self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly;
self.stock_state_with_factor_date_and_time(
ctx,
date,
factor_date,
symbol,
None,
use_intraday_quote,
)
} }
fn stock_decision_rolling_mean( fn stock_decision_rolling_mean(
@@ -2244,7 +2244,7 @@ impl PlatformExprStrategy {
symbol: &str, symbol: &str,
execution_time: Option<NaiveTime>, execution_time: Option<NaiveTime>,
) -> Result<StockExpressionState, BacktestError> { ) -> Result<StockExpressionState, BacktestError> {
self.stock_state_with_factor_date_and_time(ctx, date, date, symbol, execution_time) self.stock_state_with_factor_date_and_time(ctx, date, date, symbol, execution_time, true)
} }
fn stock_is_at_upper_limit_at_time( fn stock_is_at_upper_limit_at_time(
@@ -2313,6 +2313,7 @@ impl PlatformExprStrategy {
factor_date: NaiveDate, factor_date: NaiveDate,
symbol: &str, symbol: &str,
execution_time: Option<NaiveTime>, execution_time: Option<NaiveTime>,
use_intraday_quote: bool,
) -> Result<StockExpressionState, BacktestError> { ) -> Result<StockExpressionState, BacktestError> {
{ {
let mut cache_date = self.stock_state_cache_date.borrow_mut(); let mut cache_date = self.stock_state_cache_date.borrow_mut();
@@ -2321,7 +2322,13 @@ impl PlatformExprStrategy {
*cache_date = Some(date); *cache_date = Some(date);
} }
} }
let cache_key = (date, factor_date, symbol.to_string(), execution_time); let cache_key = (
date,
factor_date,
symbol.to_string(),
execution_time,
use_intraday_quote,
);
if let Some(state) = self.stock_state_cache.borrow().get(&cache_key) { if let Some(state) = self.stock_state_cache.borrow().get(&cache_key) {
return Ok(state.clone()); return Ok(state.clone());
} }
@@ -2329,8 +2336,11 @@ impl PlatformExprStrategy {
let market = ctx.data.require_market(date, symbol)?; let market = ctx.data.require_market(date, symbol)?;
let feature_market = ctx.data.market(factor_date, symbol).unwrap_or(market); let feature_market = ctx.data.market(factor_date, symbol).unwrap_or(market);
let intraday_same_day_factor = self.config.aiquant_transaction_cost && factor_date == date; let intraday_same_day_factor = self.config.aiquant_transaction_cost && factor_date == date;
let decision_quote = let decision_quote = if use_intraday_quote {
self.aiquant_scheduled_quote_at_time(ctx, date, symbol, execution_time); self.aiquant_scheduled_quote_at_time(ctx, date, symbol, execution_time)
} else {
None
};
let factor = ctx.data.require_factor(factor_date, symbol)?; let factor = ctx.data.require_factor(factor_date, symbol)?;
let candidate = ctx.data.require_candidate(date, symbol)?; let candidate = ctx.data.require_candidate(date, symbol)?;
let instrument = ctx.data.instrument(symbol); let instrument = ctx.data.instrument(symbol);
@@ -2451,12 +2461,11 @@ impl PlatformExprStrategy {
low: expression_low, low: expression_low,
close: expression_close, close: expression_close,
last: decision_quote last: decision_quote
.as_ref()
.and_then(|quote| { .and_then(|quote| {
(quote.last_price.is_finite() && quote.last_price > 0.0) (quote.last_price.is_finite() && quote.last_price > 0.0)
.then_some(quote.last_price) .then_some(quote.last_price)
}) })
.or_else(|| decision_quote.as_ref().and_then(|quote| quote.buy_price())) .or_else(|| decision_quote.and_then(|quote| quote.buy_price()))
.unwrap_or(market.last_price), .unwrap_or(market.last_price),
prev_close: feature_market.prev_close, prev_close: feature_market.prev_close,
amount, amount,
@@ -6208,8 +6217,12 @@ impl PlatformExprStrategy {
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
let mut candidates = Vec::new(); let mut candidates = Vec::new();
for candidate in universe { for candidate in universe {
let stock = let stock = self.selection_stock_state_with_factor_date(
self.stock_state_with_factor_date(ctx, date, stock_factor_date, &candidate.symbol)?; ctx,
date,
stock_factor_date,
&candidate.symbol,
)?;
let field_value = self.selection_field_value(&candidate, &stock); let field_value = self.selection_field_value(&candidate, &stock);
if !field_value.is_finite() { if !field_value.is_finite() {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
@@ -6296,6 +6309,34 @@ impl PlatformExprStrategy {
usage usage
} }
fn merge_quote_usage(
lhs: StockFilterQuoteUsage,
rhs: StockFilterQuoteUsage,
) -> StockFilterQuoteUsage {
match (lhs, rhs) {
(StockFilterQuoteUsage::IntradayQuote, _)
| (_, StockFilterQuoteUsage::IntradayQuote) => StockFilterQuoteUsage::IntradayQuote,
(StockFilterQuoteUsage::LimitStateOnly, _)
| (_, StockFilterQuoteUsage::LimitStateOnly) => StockFilterQuoteUsage::LimitStateOnly,
_ => StockFilterQuoteUsage::DailyOnly,
}
}
fn selection_quote_usage_for_config(
config: &PlatformExprStrategyConfig,
normalized_stock_filter_expr: &str,
) -> StockFilterQuoteUsage {
[
normalized_stock_filter_expr,
config.market_cap_field.as_str(),
config.rank_by.as_str(),
config.rank_expr.as_str(),
]
.into_iter()
.map(Self::stock_filter_quote_usage_for_expr)
.fold(StockFilterQuoteUsage::DailyOnly, Self::merge_quote_usage)
}
fn stock_rolling_requirements_for_config( fn stock_rolling_requirements_for_config(
config: &PlatformExprStrategyConfig, config: &PlatformExprStrategyConfig,
normalized_stock_filter_expr: &str, normalized_stock_filter_expr: &str,
@@ -6571,10 +6612,15 @@ impl PlatformExprStrategy {
}) })
} }
#[cfg(test)]
fn stock_filter_uses_intraday_quote_fields(&self) -> bool { fn stock_filter_uses_intraday_quote_fields(&self) -> bool {
self.stock_filter_quote_usage() != StockFilterQuoteUsage::DailyOnly self.stock_filter_quote_usage() != StockFilterQuoteUsage::DailyOnly
} }
fn selection_uses_intraday_quote_fields(&self) -> bool {
self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly
}
fn quote_plan_candidate_limit(&self, selection_limit: usize) -> usize { fn quote_plan_candidate_limit(&self, selection_limit: usize) -> usize {
selection_limit selection_limit
.saturating_mul(3) .saturating_mul(3)
@@ -6637,8 +6683,12 @@ impl PlatformExprStrategy {
let quote_usage = self.stock_filter_quote_usage(); let quote_usage = self.stock_filter_quote_usage();
let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit); let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit);
for candidate in universe { for candidate in universe {
let stock = let stock = self.selection_stock_state_with_factor_date(
self.stock_state_with_factor_date(ctx, date, stock_factor_date, &candidate.symbol)?; ctx,
date,
stock_factor_date,
&candidate.symbol,
)?;
let field_value = self.selection_field_value(&candidate, &stock); let field_value = self.selection_field_value(&candidate, &stock);
if !field_value.is_finite() { if !field_value.is_finite() {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
@@ -6748,7 +6798,7 @@ impl PlatformExprStrategy {
let day = self.day_state(ctx, ctx.decision_date)?; let day = self.day_state(ctx, ctx.decision_date)?;
let (selection_date, universe_factor_date, stock_factor_date) = self.selection_dates(ctx); let (selection_date, universe_factor_date, stock_factor_date) = self.selection_dates(ctx);
let requires_intraday_selection_quotes = self.stock_filter_uses_intraday_quote_fields(); let requires_intraday_selection_quotes = self.selection_uses_intraday_quote_fields();
let (band_low, band_high) = self.market_cap_band(ctx, &day)?; let (band_low, band_high) = self.market_cap_band(ctx, &day)?;
let selection_limit = self let selection_limit = self
.selection_limit(ctx, &day)? .selection_limit(ctx, &day)?
@@ -20898,6 +20948,29 @@ mod tests {
strategy.stock_filter_uses_intraday_quote_fields(), strategy.stock_filter_uses_intraday_quote_fields(),
"actual tick/quote fields still require intraday selection quotes" "actual tick/quote fields still require intraday selection quotes"
); );
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.stock_filter_expr = "close > 1.0".to_string();
cfg.rank_by = "last_price".to_string();
let strategy = PlatformExprStrategy::new(cfg);
assert_eq!(
strategy.stock_filter_quote_usage(),
StockFilterQuoteUsage::DailyOnly
);
assert!(
strategy.selection_uses_intraday_quote_fields(),
"selection ranking by tick fields must still request intraday selection quotes"
);
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.stock_filter_expr =
"rolling_mean(\"close\", 5) > rolling_mean(\"close\", 10)".to_string();
cfg.rank_by = "market_cap".to_string();
let strategy = PlatformExprStrategy::new(cfg);
assert!(
!strategy.selection_uses_intraday_quote_fields(),
"pure daily rolling selection must not prefetch intraday quotes"
);
} }
#[test] #[test]