diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 0901494..b977138 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -1997,7 +1997,24 @@ impl DataSet { symbol_id: u32, include_now: bool, ) -> Option { - let calendar_index = self.calendar.index_of(date)?; + let calendar_index = self.calendar_index(date)?; + self.market_series_end_index_by_symbol_id_at_calendar_index( + calendar_index, + symbol_id, + include_now, + ) + } + + pub(crate) fn calendar_index(&self, date: NaiveDate) -> Option { + self.calendar.index_of(date) + } + + fn market_series_end_index_by_symbol_id_at_calendar_index( + &self, + calendar_index: usize, + symbol_id: u32, + include_now: bool, + ) -> Option { let positions = self .market_series_end_positions_by_symbol_id .as_ref() @@ -2020,6 +2037,14 @@ impl DataSet { self.market_series_end_index_by_symbol_id(date, symbol_id, true) } + pub(crate) fn market_current_series_end_index_by_symbol_id_at_calendar_index( + &self, + calendar_index: usize, + symbol_id: u32, + ) -> Option { + self.market_series_end_index_by_symbol_id_at_calendar_index(calendar_index, symbol_id, true) + } + pub fn factor(&self, date: NaiveDate, symbol: &str) -> Option<&DailyFactorSnapshot> { let symbol_id = self.symbol_id(symbol)?; self.factor_by_symbol_id(date, symbol_id) @@ -2061,9 +2086,10 @@ impl DataSet { ) } - pub(crate) fn market_standard_rolling_means_by_symbol_id( + pub(crate) fn market_standard_rolling_means_by_symbol_id_with_calendar_index( &self, date: NaiveDate, + calendar_index: Option, symbol_id: u32, close_lookbacks: &[usize; 7], volume_lookbacks: &[usize; 5], @@ -2079,7 +2105,13 @@ impl DataSet { } // Both series are built from the same market-date sequence. Reuse the // indexed boundary lookup instead of repeating it for close and volume. - let series_end = self.market_series_end_index_by_symbol_id(date, symbol_id, include_now); + let series_end = calendar_index.and_then(|calendar_index| { + self.market_series_end_index_by_symbol_id_at_calendar_index( + calendar_index, + symbol_id, + include_now, + ) + }); let close = if close_required { self.adjusted_close_series_by_symbol_id(symbol_id) .map(|series| { @@ -5227,6 +5259,85 @@ mod tests { ); } + #[test] + #[ignore = "manual release-mode calendar index reuse benchmark"] + fn benchmark_series_boundary_reuses_calendar_index() { + use std::hint::black_box; + use std::time::Instant; + + let data = volume_contract_data(Some([1.0, 1.0, 1.0])); + let symbol_id = data.symbol_id("000001.SZ").expect("symbol id"); + let date = *data.calendar().days().last().expect("calendar date"); + let calendar_index = data.calendar_index(date).expect("calendar index"); + let iterations = 5_000_000usize; + let mut date_lookup_nanos = 0u128; + let mut reused_nanos = 0u128; + let mut date_lookup_checksum = 0usize; + let mut reused_checksum = 0usize; + + for sample in 0..6 { + let measure_date_lookup = || { + let started = Instant::now(); + let mut checksum = 0usize; + for _ in 0..iterations { + checksum += black_box( + data.market_series_end_index_by_symbol_id( + black_box(date), + black_box(symbol_id), + true, + ) + .unwrap(), + ); + } + (started.elapsed().as_nanos(), checksum) + }; + let measure_reused = || { + let started = Instant::now(); + let mut checksum = 0usize; + for _ in 0..iterations { + checksum += black_box( + data.market_series_end_index_by_symbol_id_at_calendar_index( + black_box(calendar_index), + black_box(symbol_id), + true, + ) + .unwrap(), + ); + } + (started.elapsed().as_nanos(), checksum) + }; + let (first_nanos, first_checksum, second_nanos, second_checksum) = if sample % 2 == 0 { + let (date_nanos, date_checksum) = measure_date_lookup(); + let (reused_nanos, reused_checksum) = measure_reused(); + (date_nanos, date_checksum, reused_nanos, reused_checksum) + } else { + let (reused_nanos, reused_checksum) = measure_reused(); + let (date_nanos, date_checksum) = measure_date_lookup(); + (date_nanos, date_checksum, reused_nanos, reused_checksum) + }; + date_lookup_nanos += first_nanos; + date_lookup_checksum += first_checksum; + reused_nanos += second_nanos; + reused_checksum += second_checksum; + } + + assert_eq!(date_lookup_checksum, reused_checksum); + let date_lookup_seconds = date_lookup_nanos as f64 / 1_000_000_000.0; + let reused_seconds = reused_nanos as f64 / 1_000_000_000.0; + eprintln!( + "{}", + serde_json::json!({ + "schemaVersion": "fidc-series-boundary-calendar-index-benchmark/v1", + "samples": 6, + "iterationsPerSample": iterations, + "dateLookupSeconds": date_lookup_seconds, + "reusedCalendarIndexSeconds": reused_seconds, + "speedup": date_lookup_seconds / reused_seconds, + "checksum": date_lookup_checksum, + }) + ); + } + #[test] #[ignore = "manual component benchmark"] fn benchmark_daily_snapshot_view_lookup() { @@ -5743,8 +5854,10 @@ mod tests { let symbol_id = data.symbol_id("000001.SZ").unwrap(); let close_lookbacks = [1, 2, 3, 1, 2, 3, 0]; let volume_lookbacks = [1, 2, 3, 0, 2]; - let batched = data.market_standard_rolling_means_by_symbol_id( + let calendar_index = data.calendar_index(date); + let batched = data.market_standard_rolling_means_by_symbol_id_with_calendar_index( date, + calendar_index, symbol_id, &close_lookbacks, &volume_lookbacks, @@ -5774,8 +5887,9 @@ mod tests { ) ); } - let current = data.market_standard_rolling_means_by_symbol_id( + let current = data.market_standard_rolling_means_by_symbol_id_with_calendar_index( date, + calendar_index, symbol_id, &close_lookbacks, &volume_lookbacks, @@ -5830,6 +5944,22 @@ mod tests { data.market_series_end_index_by_symbol_id(dates[2], symbol_id, true), Some(3) ); + let calendar_index = data.calendar_index(dates[2]).expect("calendar index"); + assert_eq!( + data.market_series_end_index_by_symbol_id_at_calendar_index( + calendar_index, + symbol_id, + false, + ), + Some(2) + ); + assert_eq!( + data.market_current_series_end_index_by_symbol_id_at_calendar_index( + calendar_index, + symbol_id, + ), + Some(3) + ); let extended = data .clone() diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 20be0ad..5d96d81 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -1051,6 +1051,7 @@ pub struct PlatformExprStrategy { stock_extra_factor_map_required: bool, stock_text_factors_required: bool, stock_state_cache_date: RefCell>, + stock_state_cache_calendar_index: RefCell>, stock_state_cache: RefCell< AHashMap<(NaiveDate, NaiveDate, u32, Option, bool), Arc>, >, @@ -1373,6 +1374,7 @@ impl PlatformExprStrategy { stock_extra_factor_map_required, stock_text_factors_required, stock_state_cache_date: RefCell::new(None), + stock_state_cache_calendar_index: RefCell::new(None), stock_state_cache: RefCell::new(AHashMap::new()), } } @@ -4011,13 +4013,15 @@ impl PlatformExprStrategy { symbol: symbol.to_string(), }) })?; - { + let calendar_index = { let mut cache_date = self.stock_state_cache_date.borrow_mut(); if *cache_date != Some(date) { self.stock_state_cache.borrow_mut().clear(); *cache_date = Some(date); + *self.stock_state_cache_calendar_index.borrow_mut() = ctx.data.calendar_index(date); } - } + *self.stock_state_cache_calendar_index.borrow() + }; let cache_key = ( date, factor_date, @@ -4078,8 +4082,13 @@ impl PlatformExprStrategy { let current_series_end = self .stock_current_rolling_mean_required .then(|| { - ctx.data - .market_current_series_end_index_by_symbol_id(date, symbol_id) + calendar_index.and_then(|calendar_index| { + ctx.data + .market_current_series_end_index_by_symbol_id_at_calendar_index( + calendar_index, + symbol_id, + ) + }) }) .flatten(); let required_rolling = |field: &'static str, lookback: usize| { @@ -4104,13 +4113,16 @@ impl PlatformExprStrategy { required_rolling("volume", 60), required_rolling("volume", 100), ]; - let rolling_means = ctx.data.market_standard_rolling_means_by_symbol_id( - date, - symbol_id, - &close_lookbacks, - &volume_lookbacks, - false, - ); + let rolling_means = ctx + .data + .market_standard_rolling_means_by_symbol_id_with_calendar_index( + date, + calendar_index, + symbol_id, + &close_lookbacks, + &volume_lookbacks, + false, + ); let close_rolling = |index: usize| rolling_means.close[index].unwrap_or(f64::NAN); let volume_rolling = |index: usize| rolling_means.volume[index].unwrap_or(f64::NAN); let stock_ma_short = close_rolling(0);