减少滚动窗口重复索引开销

This commit is contained in:
boris
2026-08-24 03:36:07 +08:00
parent 2013314e4f
commit ea79fdae46
2 changed files with 57 additions and 104 deletions
+37 -75
View File
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::{Arc, OnceLock};
@@ -959,7 +960,7 @@ impl SymbolPriceSeries {
}
fn numeric_value_at(&self, index: usize, field: &str) -> Option<f64> {
match normalize_field(field).as_str() {
match normalized_field(field).as_ref() {
"day_open" | "dayopen" => Some(self.day_opens[index]),
"open" => Some(self.opens[index]),
"high" => Some(self.highs[index]),
@@ -1405,12 +1406,14 @@ impl DataSet {
.and_then(|rows| find_arc_by_symbol(rows, symbol, |row| row.symbol.as_str()))
}
fn market_series(&self, symbol: &str) -> Option<Arc<SymbolPriceSeries>> {
self.market_series_by_symbol.get(symbol).cloned()
fn market_series(&self, symbol: &str) -> Option<&SymbolPriceSeries> {
self.market_series_by_symbol.get(symbol).map(Arc::as_ref)
}
fn adjusted_close_series(&self, symbol: &str) -> Option<Arc<AdjustedCloseSeries>> {
self.adjusted_close_series_by_symbol.get(symbol).cloned()
fn adjusted_close_series(&self, symbol: &str) -> Option<&AdjustedCloseSeries> {
self.adjusted_close_series_by_symbol
.get(symbol)
.map(Arc::as_ref)
}
pub fn factor(&self, date: NaiveDate, symbol: &str) -> Option<&DailyFactorSnapshot> {
@@ -2452,19 +2455,14 @@ impl DataSet {
field: &str,
lookback: usize,
) -> Option<f64> {
let field = normalize_field(field);
match field.as_str() {
let field = normalized_field(field);
match field.as_ref() {
"close" | "prev_close" | "stock_close" | "price" => self
.adjusted_close_series(symbol)
.and_then(|series| series.decision_moving_average(date, lookback)),
"volume" | "stock_volume" => {
if !self.source_daily_volume_window_available(date, symbol, lookback, false) {
None
} else {
self.market_series(symbol)
.and_then(|series| series.decision_volume_moving_average(date, lookback))
}
}
"volume" | "stock_volume" => self
.market_series(symbol)
.and_then(|series| series.decision_volume_moving_average(date, lookback)),
"day_open" | "dayopen" => {
self.market_moving_average(date, symbol, lookback, PriceField::DayOpen)
}
@@ -2483,22 +2481,14 @@ impl DataSet {
field: &str,
lookback: usize,
) -> Option<f64> {
let field = normalize_field(field);
match field.as_str() {
let field = normalized_field(field);
match field.as_ref() {
"close" | "prev_close" | "stock_close" | "price" => self
.adjusted_close_series(symbol)
.and_then(|series| series.current_moving_average(date, lookback)),
"volume" | "stock_volume" => {
if !self.source_daily_volume_window_available(date, symbol, lookback, true) {
None
} else {
self.market_series(symbol)
.and_then(|series| series.current_volume_moving_average(date, lookback))
.or_else(|| {
self.factor_moving_average(date, symbol, "daily_volume", lookback)
})
}
}
"volume" | "stock_volume" => self
.market_series(symbol)
.and_then(|series| series.current_volume_moving_average(date, lookback)),
"day_open" | "dayopen" => {
self.market_moving_average(date, symbol, lookback, PriceField::DayOpen)
}
@@ -2525,21 +2515,16 @@ impl DataSet {
if lookback == 0 {
return Vec::new();
}
let field = normalize_field(field);
match field.as_str() {
let field = normalized_field(field);
match field.as_ref() {
"close" | "prev_close" | "stock_close" | "price" => self
.adjusted_close_series(symbol)
.map(|series| series.values(date, lookback, false))
.unwrap_or_default(),
"volume" | "stock_volume" => {
if !self.source_daily_volume_window_available(date, symbol, lookback, false) {
Vec::new()
} else {
self.market_series(symbol)
"volume" | "stock_volume" => self
.market_series(symbol)
.and_then(|series| series.decision_volume_values(date, lookback))
.unwrap_or_default()
}
}
.unwrap_or_default(),
"day_open" | "dayopen" => self
.market_series(symbol)
.map(|series| series.trailing_values(date, lookback, PriceField::DayOpen))
@@ -2563,14 +2548,9 @@ impl DataSet {
field: &str,
lookback: usize,
) -> Vec<f64> {
let field = normalize_field(field);
if matches!(field.as_str(), "volume" | "stock_volume")
&& !self.source_daily_volume_window_available(date, symbol, lookback, true)
{
return Vec::new();
}
let field = normalized_field(field);
if matches!(
field.as_str(),
field.as_ref(),
"close" | "prev_close" | "stock_close" | "price"
) {
return self
@@ -2578,40 +2558,17 @@ impl DataSet {
.map(|series| series.values(date, lookback, true))
.unwrap_or_default();
}
if matches!(field.as_str(), "volume" | "stock_volume") {
if matches!(field.as_ref(), "volume" | "stock_volume") {
return self
.market_series(symbol)
.and_then(|series| series.current_volume_values(date, lookback))
.unwrap_or_default();
}
self.market_series(symbol)
.map(|series| series.trailing_numeric_values(date, lookback, &field, true))
.map(|series| series.trailing_numeric_values(date, lookback, field.as_ref(), true))
.unwrap_or_default()
}
fn source_daily_volume_window_available(
&self,
date: NaiveDate,
symbol: &str,
lookback: usize,
include_now: bool,
) -> bool {
if lookback == 0 {
return false;
}
let Some(series) = self.market_series(symbol) else {
return false;
};
let end_index = if include_now {
series.end_index(date)
} else {
series.previous_completed_end_index(date)
};
end_index
.and_then(|end| series.valid_volume_window(end, lookback))
.is_some()
}
pub fn factor_numeric_values(
&self,
date: NaiveDate,
@@ -3034,11 +2991,16 @@ fn intraday_quote_price_bar(snapshot: &IntradayExecutionQuote) -> PriceBar {
}
fn normalize_field(field: &str) -> String {
field
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_ascii_lowercase()
normalized_field(field).into_owned()
}
fn normalized_field(field: &str) -> Cow<'_, str> {
let trimmed = field.trim().trim_matches('"').trim_matches('\'');
if trimmed.bytes().all(|byte| !byte.is_ascii_uppercase()) {
Cow::Borrowed(trimmed)
} else {
Cow::Owned(trimmed.to_ascii_lowercase())
}
}
fn normalize_factor_snapshots(factors: Vec<DailyFactorSnapshot>) -> Vec<DailyFactorSnapshot> {
+19 -28
View File
@@ -8432,7 +8432,6 @@ impl PlatformExprStrategy {
&& Self::expr_requires_stock_extra_factors(
&config.stock_filter_expr,
prelude_declared_identifiers,
true,
)
{
return true;
@@ -8444,9 +8443,7 @@ impl PlatformExprStrategy {
config.rank_expr.as_str(),
]
.into_iter()
.any(|expr| {
Self::expr_requires_stock_extra_factors(expr, prelude_declared_identifiers, true)
})
.any(|expr| Self::expr_requires_stock_extra_factors(expr, prelude_declared_identifiers))
}
fn stock_extra_factor_identifiers_for_config(
@@ -8468,7 +8465,6 @@ impl PlatformExprStrategy {
&mut identifiers,
&config.stock_filter_expr,
prelude_declared_identifiers,
true,
);
}
for expr in [
@@ -8481,7 +8477,6 @@ impl PlatformExprStrategy {
&mut identifiers,
expr,
prelude_declared_identifiers,
true,
);
}
identifiers
@@ -8491,7 +8486,6 @@ impl PlatformExprStrategy {
out: &mut BTreeSet<String>,
expr: &str,
prelude_declared_identifiers: &BTreeSet<String>,
stock_rolling_helpers_require_extra: bool,
) {
let normalized = Self::normalize_expr(expr);
for name in Self::extract_identifier_candidates(&normalized) {
@@ -8503,11 +8497,6 @@ impl PlatformExprStrategy {
{
continue;
}
if stock_rolling_helpers_require_extra
&& matches!(name.as_str(), "rolling_mean" | "sma" | "ma" | "vma")
{
continue;
}
out.insert(name);
}
}
@@ -8558,23 +8547,12 @@ impl PlatformExprStrategy {
fn expr_requires_stock_extra_factors(
expr: &str,
prelude_declared_identifiers: &BTreeSet<String>,
stock_rolling_helpers_require_extra: bool,
) -> bool {
let normalized = Self::normalize_expr(expr);
let identifiers = Self::extract_identifier_candidates(&normalized);
if identifiers.contains("factors") || identifiers.contains("factor") {
return true;
}
if stock_rolling_helpers_require_extra
&& identifiers.iter().any(|name| {
matches!(
name.as_str(),
"rolling_mean" | "rolling_mean_current" | "sma" | "ma" | "vma"
)
})
{
return true;
}
identifiers.into_iter().any(|name| {
!Self::is_expression_keyword(&name)
&& !Self::is_runtime_helper(&name)
@@ -8717,11 +8695,6 @@ impl PlatformExprStrategy {
) {
let compact = Self::compact_expr(expr);
Self::require_stock_rollings_for_named_helper(requirements, &compact, "rolling_mean");
Self::require_stock_rollings_for_named_helper(
requirements,
&compact,
"rolling_mean_current",
);
Self::require_stock_rollings_for_named_helper(requirements, &compact, "sma");
Self::require_stock_rollings_for_named_helper(requirements, &compact, "ma");
Self::require_stock_rollings_for_vma_helper(requirements, &compact);
@@ -12614,6 +12587,24 @@ mod tests {
);
}
#[test]
fn current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings() {
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.stock_filter_expr = concat!(
"rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10)",
" && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)"
)
.to_string();
let strategy = PlatformExprStrategy::new(cfg);
assert!(!strategy.stock_extra_factors_required);
assert!(!strategy.stock_rolling_requirements.requires("close", 5));
assert!(!strategy.stock_rolling_requirements.requires("close", 10));
assert!(!strategy.stock_rolling_requirements.requires("volume", 5));
assert!(!strategy.stock_rolling_requirements.requires("volume", 100));
}
#[test]
fn platform_expr_missing_requested_factor_does_not_default_to_zero() {
let date = d(2025, 5, 19);