diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index e7a2d87..f18eaa0 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -6158,7 +6158,7 @@ impl PlatformExprStrategy { ) -> Result { match helper { "factor" => { - let key = Self::normalize_runtime_factor_key(&Self::parse_string_or_identifier( + let key = Self::normalize_runtime_factor_key(Self::parse_string_or_identifier_ref( args.first().map(String::as_str).unwrap_or_default(), )?); if let Some((field, lookback)) = framework_stock_rolling_factor_requirement(&key) { @@ -6190,7 +6190,7 @@ impl PlatformExprStrategy { ))) } "day_factor" => { - let key = Self::normalize_runtime_factor_key(&Self::parse_string_or_identifier( + let key = Self::normalize_runtime_factor_key(Self::parse_string_or_identifier_ref( args.first().map(String::as_str).unwrap_or_default(), )?); Ok(RuntimeHelperResolution::Expression(format!( @@ -6204,7 +6204,7 @@ impl PlatformExprStrategy { "{helper} expects 2 arguments" ))); } - let field = Self::parse_string_or_identifier(&args[0])?; + let field = Self::parse_string_or_identifier_ref(&args[0])?; let lookback = Self::parse_positive_usize(&args[1])?; let value = self.resolve_rolling_mean(ctx, day, stock, &field, lookback)?; Ok(RuntimeHelperResolution::Number(value)) @@ -6215,7 +6215,7 @@ impl PlatformExprStrategy { "rolling_mean_current expects 2 arguments".to_string(), )); } - let field = Self::parse_string_or_identifier(&args[0])?; + let field = Self::parse_string_or_identifier_ref(&args[0])?; let lookback = Self::parse_positive_usize(&args[1])?; let value = self.resolve_current_rolling_mean(ctx, day, stock, &field, lookback)?; Ok(RuntimeHelperResolution::Number(value)) @@ -6226,7 +6226,7 @@ impl PlatformExprStrategy { "rolling_max_current expects field and lookback".to_string(), )); } - let field = Self::parse_string_or_identifier(&args[0])?; + let field = Self::parse_string_or_identifier_ref(&args[0])?; let lookback = Self::parse_positive_usize(&args[1])?; let values = self.resolve_current_rolling_values(ctx, day, stock, &field, lookback)?; @@ -6239,7 +6239,7 @@ impl PlatformExprStrategy { "rolling_return_stddev_current expects field and return count".to_string(), )); } - let field = Self::parse_string_or_identifier(&args[0])?; + let field = Self::parse_string_or_identifier_ref(&args[0])?; let return_count = Self::parse_positive_usize(&args[1])?; let values = self.resolve_current_rolling_values( ctx, @@ -6278,7 +6278,7 @@ impl PlatformExprStrategy { "{helper} expects field and lookback" ))); } - let field = Self::parse_string_or_identifier(&args[0])?; + let field = Self::parse_string_or_identifier_ref(&args[0])?; let lookback = Self::parse_positive_usize(&args[1])?; let values = self.resolve_rolling_values(ctx, day, stock, &field, lookback)?; let value = match helper { @@ -6297,7 +6297,7 @@ impl PlatformExprStrategy { "pct_change expects field and lookback".to_string(), )); } - let field = Self::parse_string_or_identifier(&args[0])?; + let field = Self::parse_string_or_identifier_ref(&args[0])?; let lookback = Self::parse_positive_usize(&args[1])?; let values = self.resolve_rolling_values( ctx, @@ -7083,6 +7083,35 @@ impl PlatformExprStrategy { ))) } + /// Parse the literal/identifier forms used by runtime helpers without + /// allocating a new String on every stock evaluation. The owned parser + /// above remains the boundary for helpers that must retain the value. + fn parse_string_or_identifier_ref(raw: &str) -> Result<&str, BacktestError> { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Err(BacktestError::Execution( + "platform helper argument cannot be empty".to_string(), + )); + } + if let Some(value) = trimmed + .strip_prefix('"') + .and_then(|value| value.strip_suffix('"')) + .or_else(|| { + trimmed + .strip_prefix('\'') + .and_then(|value| value.strip_suffix('\'')) + }) + { + return Ok(value); + } + if Self::is_expression_identifier(trimmed) { + return Ok(trimmed); + } + Err(BacktestError::Execution(format!( + "platform helper expects a factor name or string literal, got {trimmed}" + ))) + } + fn normalize_runtime_factor_key(key: &str) -> String { key.trim() .trim_matches('"')