diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index a699e37..e9c433a 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -1327,6 +1327,7 @@ pub struct DataSet { market_series_end_positions_by_symbol_id: Arc>, benchmark_series_cache: Arc, symbol_id_by_code: Arc>, + symbol_by_id: Arc>>, eligible_universe_by_date: Arc>>>, benchmark_code: String, futures_params_by_symbol: Arc>>, @@ -1771,6 +1772,10 @@ impl DataSet { &factor_by_date, &candidate_by_date, ); + let mut symbol_by_id = vec![Arc::::from(""); symbol_id_by_code.len()]; + for (symbol, symbol_id) in &symbol_id_by_code { + symbol_by_id[*symbol_id as usize] = Arc::::from(symbol.as_str()); + } let mut instruments_by_symbol_id = vec![None; symbol_id_by_code.len()]; for (symbol, instrument) in &instruments { if let Some(symbol_id) = symbol_id_by_code.get(symbol).copied() { @@ -1859,6 +1864,7 @@ impl DataSet { ), benchmark_series_cache: Arc::new(benchmark_series_cache), symbol_id_by_code: Arc::new(symbol_id_by_code), + symbol_by_id: Arc::new(symbol_by_id), eligible_universe_by_date: Arc::new(OnceLock::new()), benchmark_code, futures_params_by_symbol: Arc::new(futures_params_by_symbol), @@ -1912,6 +1918,11 @@ impl DataSet { self.symbol_id_by_code.get(symbol).copied() } + pub(crate) fn shared_symbol_by_id(&self, symbol_id: u32) -> Option> { + let symbol = self.symbol_by_id.get(symbol_id as usize)?; + (!symbol.is_empty()).then(|| Arc::clone(symbol)) + } + pub(crate) fn symbol_count(&self) -> usize { self.symbol_id_by_code.len() } @@ -5047,6 +5058,10 @@ mod tests { .map(|row| row.symbol.as_str()), Some(symbol) ); + let first_shared = data.shared_symbol_by_id(symbol_id).expect("shared symbol"); + let second_shared = data.shared_symbol_by_id(symbol_id).expect("shared symbol"); + assert_eq!(first_shared.as_ref(), symbol); + assert!(Arc::ptr_eq(&first_shared, &second_shared)); assert_eq!( data.market_by_symbol_id(date, symbol_id) .map(|row| row.symbol.as_str()), diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index cab6424..0c2af72 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -668,7 +668,7 @@ struct DayExpressionState { #[derive(Debug, Clone)] struct StockExpressionState { - symbol: String, + symbol: Arc, symbol_id: u32, market_cap: f64, market_cap_bn: f64, @@ -4012,6 +4012,13 @@ impl PlatformExprStrategy { symbol: symbol.to_string(), }) })?; + let shared_symbol = ctx.data.shared_symbol_by_id(symbol_id).ok_or_else(|| { + BacktestError::Data(crate::data::DataSetError::MissingSnapshot { + kind: "symbol_reverse_index", + date, + symbol: symbol.to_string(), + }) + })?; let calendar_index = { let mut cache_date = self.stock_state_cache_date.borrow_mut(); if *cache_date != Some(date) { @@ -4216,7 +4223,7 @@ impl PlatformExprStrategy { }; let state = Arc::new(StockExpressionState { - symbol: symbol.to_string(), + symbol: shared_symbol, symbol_id, market_cap, market_cap_bn, @@ -4685,7 +4692,7 @@ impl PlatformExprStrategy { stock.lower_limit, stock.price_tick, ); - scope.push("symbol", stock.symbol.clone()); + scope.push("symbol", stock.symbol.to_string()); scope.push("market_cap", stock.market_cap); scope.push("market_cap_bn", stock.market_cap_bn); scope.push("free_float_cap", stock.free_float_cap); @@ -4784,7 +4791,7 @@ impl PlatformExprStrategy { scope.push("volume_ma100", stock.stock_volume_ma100); if include_factors_map { let mut factors = Map::new(); - factors.insert("symbol".into(), Dynamic::from(stock.symbol.clone())); + factors.insert("symbol".into(), Dynamic::from(stock.symbol.to_string())); factors.insert("market_cap".into(), Dynamic::from(stock.market_cap)); factors.insert("market_cap_bn".into(), Dynamic::from(stock.market_cap_bn)); factors.insert("free_float_cap".into(), Dynamic::from(stock.free_float_cap)); @@ -6841,7 +6848,7 @@ impl PlatformExprStrategy { let matched = ctx .get_margin_stocks(&margin_type) .iter() - .any(|symbol| symbol == &stock.symbol); + .any(|symbol| symbol.as_str() == stock.symbol.as_ref()); Ok(RuntimeHelperResolution::Boolean(matched)) } "dominant_future" | "get_dominant_future" => { @@ -6911,14 +6918,14 @@ impl PlatformExprStrategy { let stock = stock.ok_or_else(|| { BacktestError::Execution(format!("{helper} requires stock context")) })?; - return Ok((stock.symbol.clone(), default_lookback)); + return Ok((stock.symbol.to_string(), default_lookback)); } if args.len() == 1 { if let Ok(lookback) = Self::parse_positive_usize(&args[0]) { let stock = stock.ok_or_else(|| { BacktestError::Execution(format!("{helper} requires stock context")) })?; - return Ok((stock.symbol.clone(), lookback)); + return Ok((stock.symbol.to_string(), lookback)); } return Ok(( Self::parse_string_or_identifier(&args[0])?, @@ -12741,6 +12748,7 @@ mod tests { .stock_state(&second_context, dates[1], symbol) .expect("second state"); assert!(!Arc::ptr_eq(&first, &second)); + assert!(Arc::ptr_eq(&first.symbol, &second.symbol)); 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);