减少滚动窗口重复索引开销
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
|
|
||||||
@@ -959,7 +960,7 @@ impl SymbolPriceSeries {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn numeric_value_at(&self, index: usize, field: &str) -> Option<f64> {
|
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]),
|
"day_open" | "dayopen" => Some(self.day_opens[index]),
|
||||||
"open" => Some(self.opens[index]),
|
"open" => Some(self.opens[index]),
|
||||||
"high" => Some(self.highs[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()))
|
.and_then(|rows| find_arc_by_symbol(rows, symbol, |row| row.symbol.as_str()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn market_series(&self, symbol: &str) -> Option<Arc<SymbolPriceSeries>> {
|
fn market_series(&self, symbol: &str) -> Option<&SymbolPriceSeries> {
|
||||||
self.market_series_by_symbol.get(symbol).cloned()
|
self.market_series_by_symbol.get(symbol).map(Arc::as_ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn adjusted_close_series(&self, symbol: &str) -> Option<Arc<AdjustedCloseSeries>> {
|
fn adjusted_close_series(&self, symbol: &str) -> Option<&AdjustedCloseSeries> {
|
||||||
self.adjusted_close_series_by_symbol.get(symbol).cloned()
|
self.adjusted_close_series_by_symbol
|
||||||
|
.get(symbol)
|
||||||
|
.map(Arc::as_ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn factor(&self, date: NaiveDate, symbol: &str) -> Option<&DailyFactorSnapshot> {
|
pub fn factor(&self, date: NaiveDate, symbol: &str) -> Option<&DailyFactorSnapshot> {
|
||||||
@@ -2452,19 +2455,14 @@ impl DataSet {
|
|||||||
field: &str,
|
field: &str,
|
||||||
lookback: usize,
|
lookback: usize,
|
||||||
) -> Option<f64> {
|
) -> Option<f64> {
|
||||||
let field = normalize_field(field);
|
let field = normalized_field(field);
|
||||||
match field.as_str() {
|
match field.as_ref() {
|
||||||
"close" | "prev_close" | "stock_close" | "price" => self
|
"close" | "prev_close" | "stock_close" | "price" => self
|
||||||
.adjusted_close_series(symbol)
|
.adjusted_close_series(symbol)
|
||||||
.and_then(|series| series.decision_moving_average(date, lookback)),
|
.and_then(|series| series.decision_moving_average(date, lookback)),
|
||||||
"volume" | "stock_volume" => {
|
"volume" | "stock_volume" => self
|
||||||
if !self.source_daily_volume_window_available(date, symbol, lookback, false) {
|
.market_series(symbol)
|
||||||
None
|
.and_then(|series| series.decision_volume_moving_average(date, lookback)),
|
||||||
} else {
|
|
||||||
self.market_series(symbol)
|
|
||||||
.and_then(|series| series.decision_volume_moving_average(date, lookback))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"day_open" | "dayopen" => {
|
"day_open" | "dayopen" => {
|
||||||
self.market_moving_average(date, symbol, lookback, PriceField::DayOpen)
|
self.market_moving_average(date, symbol, lookback, PriceField::DayOpen)
|
||||||
}
|
}
|
||||||
@@ -2483,22 +2481,14 @@ impl DataSet {
|
|||||||
field: &str,
|
field: &str,
|
||||||
lookback: usize,
|
lookback: usize,
|
||||||
) -> Option<f64> {
|
) -> Option<f64> {
|
||||||
let field = normalize_field(field);
|
let field = normalized_field(field);
|
||||||
match field.as_str() {
|
match field.as_ref() {
|
||||||
"close" | "prev_close" | "stock_close" | "price" => self
|
"close" | "prev_close" | "stock_close" | "price" => self
|
||||||
.adjusted_close_series(symbol)
|
.adjusted_close_series(symbol)
|
||||||
.and_then(|series| series.current_moving_average(date, lookback)),
|
.and_then(|series| series.current_moving_average(date, lookback)),
|
||||||
"volume" | "stock_volume" => {
|
"volume" | "stock_volume" => self
|
||||||
if !self.source_daily_volume_window_available(date, symbol, lookback, true) {
|
.market_series(symbol)
|
||||||
None
|
.and_then(|series| series.current_volume_moving_average(date, lookback)),
|
||||||
} 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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"day_open" | "dayopen" => {
|
"day_open" | "dayopen" => {
|
||||||
self.market_moving_average(date, symbol, lookback, PriceField::DayOpen)
|
self.market_moving_average(date, symbol, lookback, PriceField::DayOpen)
|
||||||
}
|
}
|
||||||
@@ -2525,21 +2515,16 @@ impl DataSet {
|
|||||||
if lookback == 0 {
|
if lookback == 0 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let field = normalize_field(field);
|
let field = normalized_field(field);
|
||||||
match field.as_str() {
|
match field.as_ref() {
|
||||||
"close" | "prev_close" | "stock_close" | "price" => self
|
"close" | "prev_close" | "stock_close" | "price" => self
|
||||||
.adjusted_close_series(symbol)
|
.adjusted_close_series(symbol)
|
||||||
.map(|series| series.values(date, lookback, false))
|
.map(|series| series.values(date, lookback, false))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
"volume" | "stock_volume" => {
|
"volume" | "stock_volume" => self
|
||||||
if !self.source_daily_volume_window_available(date, symbol, lookback, false) {
|
.market_series(symbol)
|
||||||
Vec::new()
|
|
||||||
} else {
|
|
||||||
self.market_series(symbol)
|
|
||||||
.and_then(|series| series.decision_volume_values(date, lookback))
|
.and_then(|series| series.decision_volume_values(date, lookback))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default(),
|
||||||
}
|
|
||||||
}
|
|
||||||
"day_open" | "dayopen" => self
|
"day_open" | "dayopen" => self
|
||||||
.market_series(symbol)
|
.market_series(symbol)
|
||||||
.map(|series| series.trailing_values(date, lookback, PriceField::DayOpen))
|
.map(|series| series.trailing_values(date, lookback, PriceField::DayOpen))
|
||||||
@@ -2563,14 +2548,9 @@ impl DataSet {
|
|||||||
field: &str,
|
field: &str,
|
||||||
lookback: usize,
|
lookback: usize,
|
||||||
) -> Vec<f64> {
|
) -> Vec<f64> {
|
||||||
let field = normalize_field(field);
|
let field = normalized_field(field);
|
||||||
if matches!(field.as_str(), "volume" | "stock_volume")
|
|
||||||
&& !self.source_daily_volume_window_available(date, symbol, lookback, true)
|
|
||||||
{
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
if matches!(
|
if matches!(
|
||||||
field.as_str(),
|
field.as_ref(),
|
||||||
"close" | "prev_close" | "stock_close" | "price"
|
"close" | "prev_close" | "stock_close" | "price"
|
||||||
) {
|
) {
|
||||||
return self
|
return self
|
||||||
@@ -2578,40 +2558,17 @@ impl DataSet {
|
|||||||
.map(|series| series.values(date, lookback, true))
|
.map(|series| series.values(date, lookback, true))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
}
|
}
|
||||||
if matches!(field.as_str(), "volume" | "stock_volume") {
|
if matches!(field.as_ref(), "volume" | "stock_volume") {
|
||||||
return self
|
return self
|
||||||
.market_series(symbol)
|
.market_series(symbol)
|
||||||
.and_then(|series| series.current_volume_values(date, lookback))
|
.and_then(|series| series.current_volume_values(date, lookback))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
}
|
}
|
||||||
self.market_series(symbol)
|
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()
|
.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(
|
pub fn factor_numeric_values(
|
||||||
&self,
|
&self,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
@@ -3034,11 +2991,16 @@ fn intraday_quote_price_bar(snapshot: &IntradayExecutionQuote) -> PriceBar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_field(field: &str) -> String {
|
fn normalize_field(field: &str) -> String {
|
||||||
field
|
normalized_field(field).into_owned()
|
||||||
.trim()
|
}
|
||||||
.trim_matches('"')
|
|
||||||
.trim_matches('\'')
|
fn normalized_field(field: &str) -> Cow<'_, str> {
|
||||||
.to_ascii_lowercase()
|
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> {
|
fn normalize_factor_snapshots(factors: Vec<DailyFactorSnapshot>) -> Vec<DailyFactorSnapshot> {
|
||||||
|
|||||||
@@ -8432,7 +8432,6 @@ impl PlatformExprStrategy {
|
|||||||
&& Self::expr_requires_stock_extra_factors(
|
&& Self::expr_requires_stock_extra_factors(
|
||||||
&config.stock_filter_expr,
|
&config.stock_filter_expr,
|
||||||
prelude_declared_identifiers,
|
prelude_declared_identifiers,
|
||||||
true,
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@@ -8444,9 +8443,7 @@ impl PlatformExprStrategy {
|
|||||||
config.rank_expr.as_str(),
|
config.rank_expr.as_str(),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.any(|expr| {
|
.any(|expr| Self::expr_requires_stock_extra_factors(expr, prelude_declared_identifiers))
|
||||||
Self::expr_requires_stock_extra_factors(expr, prelude_declared_identifiers, true)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stock_extra_factor_identifiers_for_config(
|
fn stock_extra_factor_identifiers_for_config(
|
||||||
@@ -8468,7 +8465,6 @@ impl PlatformExprStrategy {
|
|||||||
&mut identifiers,
|
&mut identifiers,
|
||||||
&config.stock_filter_expr,
|
&config.stock_filter_expr,
|
||||||
prelude_declared_identifiers,
|
prelude_declared_identifiers,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for expr in [
|
for expr in [
|
||||||
@@ -8481,7 +8477,6 @@ impl PlatformExprStrategy {
|
|||||||
&mut identifiers,
|
&mut identifiers,
|
||||||
expr,
|
expr,
|
||||||
prelude_declared_identifiers,
|
prelude_declared_identifiers,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
identifiers
|
identifiers
|
||||||
@@ -8491,7 +8486,6 @@ impl PlatformExprStrategy {
|
|||||||
out: &mut BTreeSet<String>,
|
out: &mut BTreeSet<String>,
|
||||||
expr: &str,
|
expr: &str,
|
||||||
prelude_declared_identifiers: &BTreeSet<String>,
|
prelude_declared_identifiers: &BTreeSet<String>,
|
||||||
stock_rolling_helpers_require_extra: bool,
|
|
||||||
) {
|
) {
|
||||||
let normalized = Self::normalize_expr(expr);
|
let normalized = Self::normalize_expr(expr);
|
||||||
for name in Self::extract_identifier_candidates(&normalized) {
|
for name in Self::extract_identifier_candidates(&normalized) {
|
||||||
@@ -8503,11 +8497,6 @@ impl PlatformExprStrategy {
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if stock_rolling_helpers_require_extra
|
|
||||||
&& matches!(name.as_str(), "rolling_mean" | "sma" | "ma" | "vma")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.insert(name);
|
out.insert(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8558,23 +8547,12 @@ impl PlatformExprStrategy {
|
|||||||
fn expr_requires_stock_extra_factors(
|
fn expr_requires_stock_extra_factors(
|
||||||
expr: &str,
|
expr: &str,
|
||||||
prelude_declared_identifiers: &BTreeSet<String>,
|
prelude_declared_identifiers: &BTreeSet<String>,
|
||||||
stock_rolling_helpers_require_extra: bool,
|
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let normalized = Self::normalize_expr(expr);
|
let normalized = Self::normalize_expr(expr);
|
||||||
let identifiers = Self::extract_identifier_candidates(&normalized);
|
let identifiers = Self::extract_identifier_candidates(&normalized);
|
||||||
if identifiers.contains("factors") || identifiers.contains("factor") {
|
if identifiers.contains("factors") || identifiers.contains("factor") {
|
||||||
return true;
|
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| {
|
identifiers.into_iter().any(|name| {
|
||||||
!Self::is_expression_keyword(&name)
|
!Self::is_expression_keyword(&name)
|
||||||
&& !Self::is_runtime_helper(&name)
|
&& !Self::is_runtime_helper(&name)
|
||||||
@@ -8717,11 +8695,6 @@ impl PlatformExprStrategy {
|
|||||||
) {
|
) {
|
||||||
let compact = Self::compact_expr(expr);
|
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");
|
||||||
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, "sma");
|
||||||
Self::require_stock_rollings_for_named_helper(requirements, &compact, "ma");
|
Self::require_stock_rollings_for_named_helper(requirements, &compact, "ma");
|
||||||
Self::require_stock_rollings_for_vma_helper(requirements, &compact);
|
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]
|
#[test]
|
||||||
fn platform_expr_missing_requested_factor_does_not_default_to_zero() {
|
fn platform_expr_missing_requested_factor_does_not_default_to_zero() {
|
||||||
let date = d(2025, 5, 19);
|
let date = d(2025, 5, 19);
|
||||||
|
|||||||
Reference in New Issue
Block a user