From 6f81e1940ad4ddd4eefbe12c056be28f2b64d127 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 5 Sep 2026 01:11:34 +0800 Subject: [PATCH] perf: compact daily stock-state cache keys --- .../fidc-core/src/platform_expr_strategy.rs | 129 ++++++++++++++++-- 1 file changed, 119 insertions(+), 10 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index a896d33..cab6424 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -1052,9 +1052,8 @@ pub struct PlatformExprStrategy { 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>, - >, + stock_state_cache: + RefCell, bool), Arc>>, } #[derive(Debug, Clone, PartialEq)] @@ -4022,13 +4021,7 @@ impl PlatformExprStrategy { } *self.stock_state_cache_calendar_index.borrow() }; - let cache_key = ( - date, - factor_date, - symbol_id, - execution_time, - use_intraday_quote, - ); + let cache_key = (factor_date, symbol_id, execution_time, use_intraday_quote); if let Some(state) = self.stock_state_cache.borrow().get(&cache_key) { return Ok(Arc::clone(state)); } @@ -12611,6 +12604,7 @@ fn code_number_value(value: &str) -> i64 { #[cfg(test)] mod tests { use std::collections::{BTreeMap, BTreeSet}; + use std::sync::Arc; use chrono::{NaiveDate, NaiveTime}; @@ -12637,6 +12631,121 @@ mod tests { NaiveDate::from_ymd_opt(year, month, day).expect("valid date") } + #[test] + fn stock_state_cache_resets_before_reusing_compact_keys_on_another_date() { + let dates = [d(2025, 1, 2), d(2025, 1, 3)]; + let symbol = "300001.SZ"; + let market = |date: NaiveDate, close: f64| DailyMarketSnapshot { + date, + symbol: symbol.to_string(), + timestamp: None, + day_open: close, + open: close, + high: close, + low: close, + close, + last_price: close, + bid1: close, + ask1: close, + prev_close: close, + volume: 1_000_000, + minute_volume: 10_000, + bid1_volume: 10_000, + ask1_volume: 10_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: close * 1.1, + lower_limit: close * 0.9, + price_tick: 0.01, + }; + let factor = |date: NaiveDate| DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn: 10.0, + free_float_cap_bn: 9.0, + pe_ttm: 12.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::from([("adjustment_factor_backward1".into(), 1.0)]), + }; + let candidate = |date: NaiveDate| CandidateEligibility { + date, + symbol: symbol.to_string(), + is_st: false, + is_star_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + risk_level_code: None, + }; + let data = DataSet::from_components( + vec![Instrument { + symbol: symbol.to_string(), + name: symbol.to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: Some(d(2020, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }], + vec![market(dates[0], 10.0), market(dates[1], 11.0)], + dates.into_iter().map(factor).collect(), + dates.into_iter().map(candidate).collect(), + dates + .into_iter() + .map(|date| BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1_000.0, + close: 1_000.0, + prev_close: 1_000.0, + volume: 1_000_000, + }) + .collect(), + ) + .expect("cache test dataset"); + let portfolio = PortfolioState::new(100_000.0); + let subscriptions = BTreeSet::new(); + let context = |date: NaiveDate, decision_index: usize| StrategyContext { + execution_date: date, + decision_date: date, + decision_index, + data: &data, + portfolio: &portfolio, + futures_account: None, + open_orders: &[], + dynamic_universe: None, + subscriptions: &subscriptions, + process_events: &[], + active_process_event: None, + active_datetime: None, + order_events: &[], + fills: &[], + }; + let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + let first_context = context(dates[0], 0); + let first = strategy + .stock_state(&first_context, dates[0], symbol) + .expect("first state"); + let first_again = strategy + .stock_state(&first_context, dates[0], symbol) + .expect("cached first state"); + assert!(Arc::ptr_eq(&first, &first_again)); + assert_eq!(first.close, 10.0); + + let second_context = context(dates[1], 1); + let second = strategy + .stock_state(&second_context, dates[1], symbol) + .expect("second state"); + assert!(!Arc::ptr_eq(&first, &second)); + assert_eq!(second.close, 11.0); + assert_eq!(*strategy.stock_state_cache_date.borrow(), Some(dates[1])); + assert_eq!(strategy.stock_state_cache.borrow().len(), 1); + } + #[test] fn dated_position_exposure_uses_the_latest_effective_point() { let schedule = BTreeMap::from([(d(2026, 8, 14), 0.6451), (d(2026, 8, 20), 0.3225)]);