perf(core): reuse rolling endpoints per stock state

This commit is contained in:
boris
2026-08-28 08:16:04 +08:00
parent 3d2ab17d72
commit 732c3cfbf5
2 changed files with 141 additions and 6 deletions
+136 -5
View File
@@ -646,14 +646,22 @@ impl AdjustedCloseSeries {
} }
fn decision_moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> { fn decision_moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> {
let end = self.decision_end_index(date)?;
self.decision_moving_average_at_end(end, lookback)
}
fn decision_end_index(&self, date: NaiveDate) -> Option<usize> {
match self.dates.binary_search(&date) {
Ok(index) => Some(index),
Err(0) => None,
Err(index) => Some(index),
}
}
fn decision_moving_average_at_end(&self, end: usize, lookback: usize) -> Option<f64> {
if lookback == 0 { if lookback == 0 {
return None; return None;
} }
let end = match self.dates.binary_search(&date) {
Ok(index) => index,
Err(0) => return None,
Err(index) => index,
};
if end < lookback { if end < lookback {
return None; return None;
} }
@@ -945,6 +953,14 @@ impl SymbolPriceSeries {
fn decision_volume_moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> { fn decision_volume_moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> {
let end = self.previous_completed_end_index(date)?; let end = self.previous_completed_end_index(date)?;
self.decision_volume_moving_average_at_end(end, lookback)
}
fn decision_volume_moving_average_at_end(
&self,
end: usize,
lookback: usize,
) -> Option<f64> {
self.valid_volume_window(end, lookback).map(|(start, end)| { self.valid_volume_window(end, lookback).map(|(start, end)| {
normalize_rolling_factor( normalize_rolling_factor(
(self.valid_volume_sum_prefix[end] - self.valid_volume_sum_prefix[start]) (self.valid_volume_sum_prefix[end] - self.valid_volume_sum_prefix[start])
@@ -1246,6 +1262,28 @@ pub(crate) struct SymbolSnapshotRefs<'a> {
pub candidate: Option<&'a CandidateEligibility>, pub candidate: Option<&'a CandidateEligibility>,
} }
#[derive(Debug, Clone, Copy)]
pub(crate) struct DecisionRollingCursor<'a> {
adjusted_close: Option<(&'a AdjustedCloseSeries, usize)>,
volume: Option<(&'a SymbolPriceSeries, usize)>,
}
impl DecisionRollingCursor<'_> {
pub(crate) fn moving_average(&self, field: &str, lookback: usize) -> Option<f64> {
match field {
"close" => self
.adjusted_close
.and_then(|(series, end)| series.decision_moving_average_at_end(end, lookback)),
"volume" => self
.volume
.and_then(|(series, end)| {
series.decision_volume_moving_average_at_end(end, lookback)
}),
_ => None,
}
}
}
impl DataSet { impl DataSet {
pub fn with_additional_trading_dates( pub fn with_additional_trading_dates(
mut self, mut self,
@@ -2881,6 +2919,27 @@ impl DataSet {
} }
} }
pub(crate) fn market_decision_rolling_cursor_by_symbol_id(
&self,
date: NaiveDate,
symbol_id: u32,
) -> DecisionRollingCursor<'_> {
let adjusted_close = self
.adjusted_close_series_by_symbol_id(symbol_id)
.and_then(|series| series.decision_end_index(date).map(|end| (series, end)));
let volume = self
.market_series_by_symbol_id(symbol_id)
.and_then(|series| {
series
.previous_completed_end_index(date)
.map(|end| (series, end))
});
DecisionRollingCursor {
adjusted_close,
volume,
}
}
pub fn market_current_numeric_moving_average( pub fn market_current_numeric_moving_average(
&self, &self,
date: NaiveDate, date: NaiveDate,
@@ -4132,6 +4191,78 @@ mod tests {
assert!(signal.candidate.is_none()); assert!(signal.candidate.is_none());
} }
#[test]
fn decision_rolling_cursor_matches_existing_close_and_volume_contract() {
let dates = ["2025-01-02", "2025-01-03", "2025-01-06", "2025-01-07"];
let market = dates
.iter()
.enumerate()
.map(|(index, date)| market_row(date, 10.0 + index as f64, 1_000 + index as u64 * 100))
.collect::<Vec<_>>();
let factors = dates
.iter()
.map(|date| DailyFactorSnapshot {
date: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
symbol: "000001.SZ".to_string(),
market_cap_bn: 100.0,
free_float_cap_bn: 80.0,
pe_ttm: 0.0,
turnover_ratio: None,
effective_turnover_ratio: None,
extra_factors: NumericFactorMap::from([(
Cow::Borrowed(BACKWARD_ADJUSTMENT_FACTOR_FIELD),
1.0,
)]),
})
.collect::<Vec<_>>();
let benchmarks = dates
.iter()
.enumerate()
.map(|(index, date)| benchmark_row(date, 20.0 + index as f64))
.collect::<Vec<_>>();
let data = DataSet::from_components(
vec![Instrument {
symbol: "000001.SZ".to_string(),
name: "000001.SZ".to_string(),
board: "SZ".to_string(),
round_lot: 100,
listed_at: None,
delisted_at: None,
status: "active".to_string(),
}],
market,
factors,
Vec::new(),
benchmarks,
)
.unwrap();
let symbol_id = data.symbol_id("000001.SZ").unwrap();
let date = NaiveDate::parse_from_str("2025-01-07", "%Y-%m-%d").unwrap();
let cursor = data.market_decision_rolling_cursor_by_symbol_id(date, symbol_id);
for lookback in [1, 2, 3] {
assert_eq!(
cursor.moving_average("close", lookback),
data.market_decision_numeric_moving_average_by_symbol_id(
date,
symbol_id,
"000001.SZ",
"close",
lookback,
)
);
assert_eq!(
cursor.moving_average("volume", lookback),
data.market_decision_numeric_moving_average_by_symbol_id(
date,
symbol_id,
"000001.SZ",
"volume",
lookback,
)
);
}
}
#[test] #[test]
fn additional_terminal_calendar_dates_are_isolated_from_shared_market_data() { fn additional_terminal_calendar_dates_are_isolated_from_shared_market_data() {
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap(); let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
@@ -3935,11 +3935,15 @@ impl PlatformExprStrategy {
None None
}; };
let instrument = ctx.data.instrument(symbol); let instrument = ctx.data.instrument(symbol);
let rolling_cursor = ctx
.data
.market_decision_rolling_cursor_by_symbol_id(date, symbol_id);
let rolling = |field: &'static str, lookback: usize| -> f64 { let rolling = |field: &'static str, lookback: usize| -> f64 {
if !self.stock_rolling_requirements.requires(field, lookback) { if !self.stock_rolling_requirements.requires(field, lookback) {
return f64::NAN; return f64::NAN;
} }
self.stock_decision_rolling_mean(ctx, date, symbol_id, symbol, field, lookback) rolling_cursor
.moving_average(field, lookback)
.unwrap_or(f64::NAN) .unwrap_or(f64::NAN)
}; };
let stock_ma_short = rolling("close", self.config.stock_short_ma_days); let stock_ma_short = rolling("close", self.config.stock_short_ma_days);