优化数值表达式helper执行路径

This commit is contained in:
boris
2026-08-26 03:25:41 +08:00
committed by Boris
parent afef38e45e
commit bf2e3af4eb
+119 -125
View File
@@ -926,7 +926,18 @@ enum RuntimeExpressionSegment {
struct RuntimeHelperBinding {
name: String,
args: Vec<String>,
scope_name: String,
}
/// Typed result of resolving a runtime helper.
///
/// The Rhai path still binds numeric values into its scope, but the numeric
/// VM consumes these values directly. Keeping the typed value here avoids a
/// per-evaluation `Dynamic -> String -> f64/bool` round trip.
#[derive(Debug)]
enum RuntimeHelperResolution {
Expression(String),
Number(f64),
Boolean(bool),
}
pub struct PlatformExprStrategy {
@@ -4792,21 +4803,13 @@ impl PlatformExprStrategy {
*self.numeric_vm_fallbacks.borrow_mut() += 1;
return Ok(None);
};
let mut helper_scope = Scope::new();
let mut scratch = self.numeric_vm_scratch.borrow_mut();
let value = vm_plan
.program
.evaluate(&mut scratch, |index, identifier, expected_type| {
if let Some(binding) = vm_plan.helper_bindings[index].as_ref() {
return self
.numeric_vm_runtime_helper_value(
ctx,
day,
stock,
binding,
expected_type,
&mut helper_scope,
)
.numeric_vm_runtime_helper_value(ctx, day, stock, binding, expected_type)
.map_err(|error| NumericVmEvalError::new(error.to_string()));
}
self.numeric_vm_identifier_value(ctx, day, stock, position, identifier)
@@ -4832,63 +4835,41 @@ impl PlatformExprStrategy {
stock: Option<&StockExpressionState>,
binding: &RuntimeHelperBinding,
expected_type: NumericVmValueType,
scope: &mut Scope<'_>,
) -> Result<NumericVmValue, BacktestError> {
let resolved = self.resolve_runtime_helper(
ctx,
day,
stock,
&binding.name,
&binding.args,
&binding.scope_name,
scope,
)?;
if resolved == binding.scope_name {
return match expected_type {
NumericVmValueType::Number => scope
.get_value::<f64>(&binding.scope_name)
.map(NumericVmValue::Number)
.or_else(|| {
scope
.get_value::<i64>(&binding.scope_name)
.map(|value| NumericVmValue::Number(value as f64))
})
.ok_or_else(|| {
BacktestError::Execution(format!(
"runtime helper {} did not bind a numeric value",
binding.name
))
}),
NumericVmValueType::Boolean => scope
.get_value::<bool>(&binding.scope_name)
.map(NumericVmValue::Boolean)
.ok_or_else(|| {
BacktestError::Execution(format!(
"runtime helper {} did not bind a boolean value",
binding.name
))
}),
};
let resolved =
self.resolve_runtime_helper(ctx, day, stock, &binding.name, &binding.args)?;
match (expected_type, resolved) {
(NumericVmValueType::Number, RuntimeHelperResolution::Number(value)) => {
Ok(NumericVmValue::Number(value))
}
match expected_type {
NumericVmValueType::Number => resolved
(NumericVmValueType::Boolean, RuntimeHelperResolution::Boolean(value)) => {
Ok(NumericVmValue::Boolean(value))
}
(expected_type, RuntimeHelperResolution::Expression(expression)) => match expected_type
{
NumericVmValueType::Number => expression
.trim()
.parse::<f64>()
.map(NumericVmValue::Number)
.map_err(|_| {
BacktestError::Execution(format!(
"runtime helper {} produced non-numeric expression {resolved:?}",
"runtime helper {} produced non-numeric expression {expression:?}",
binding.name
))
}),
NumericVmValueType::Boolean => match resolved.trim() {
NumericVmValueType::Boolean => match expression.trim() {
"true" => Ok(NumericVmValue::Boolean(true)),
"false" => Ok(NumericVmValue::Boolean(false)),
_ => Err(BacktestError::Execution(format!(
"runtime helper {} produced non-boolean expression {resolved:?}",
"runtime helper {} produced non-boolean expression {expression:?}",
binding.name
))),
},
},
(expected_type, value) => Err(BacktestError::Execution(format!(
"runtime helper {} returned {:?}, expected {:?}",
binding.name, value, expected_type
))),
}
}
@@ -5849,7 +5830,6 @@ impl PlatformExprStrategy {
let binding = RuntimeHelperBinding {
name: name.clone(),
args: args.clone(),
scope_name: scope_name.clone(),
};
if let Some(existing) = bindings.get(scope_name)
&& (existing.name != binding.name || existing.args != binding.args)
@@ -5981,9 +5961,17 @@ impl PlatformExprStrategy {
name,
args,
scope_name,
} => output.push_str(
&self.resolve_runtime_helper(ctx, day, stock, name, args, scope_name, scope)?,
),
} => match self.resolve_runtime_helper(ctx, day, stock, name, args)? {
RuntimeHelperResolution::Expression(expression) => output.push_str(&expression),
RuntimeHelperResolution::Number(value) => {
scope.push(scope_name.clone(), value);
output.push_str(scope_name);
}
RuntimeHelperResolution::Boolean(value) => {
scope.push(scope_name.clone(), value);
output.push_str(scope_name);
}
},
}
}
Ok(output)
@@ -5996,9 +5984,7 @@ impl PlatformExprStrategy {
stock: Option<&StockExpressionState>,
helper: &str,
args: &[String],
scope_name: &str,
scope: &mut Scope<'_>,
) -> Result<String, BacktestError> {
) -> Result<RuntimeHelperResolution, BacktestError> {
match helper {
"factor" => {
let key = Self::normalize_runtime_factor_key(&Self::parse_string_or_identifier(
@@ -6025,19 +6011,21 @@ impl PlatformExprStrategy {
stock.symbol, day.date
))
})?;
return Ok(Self::push_runtime_helper_value(
scope,
scope_name,
Dynamic::from(value),
));
return Ok(RuntimeHelperResolution::Number(value));
}
Ok(format!("factors[{}]", Self::quote_rhai_string(&key)))
Ok(RuntimeHelperResolution::Expression(format!(
"factors[{}]",
Self::quote_rhai_string(&key)
)))
}
"day_factor" => {
let key = Self::normalize_runtime_factor_key(&Self::parse_string_or_identifier(
args.first().map(String::as_str).unwrap_or_default(),
)?);
Ok(format!("day_factors[{}]", Self::quote_rhai_string(&key)))
Ok(RuntimeHelperResolution::Expression(format!(
"day_factors[{}]",
Self::quote_rhai_string(&key)
)))
}
"rolling_mean" | "sma" | "ma" => {
if args.len() != 2 {
@@ -6048,11 +6036,7 @@ impl PlatformExprStrategy {
let field = Self::parse_string_or_identifier(&args[0])?;
let lookback = Self::parse_positive_usize(&args[1])?;
let value = self.resolve_rolling_mean(ctx, day, stock, &field, lookback)?;
Ok(Self::push_runtime_helper_value(
scope,
scope_name,
Dynamic::from(value),
))
Ok(RuntimeHelperResolution::Number(value))
}
"rolling_mean_current" => {
if args.len() != 2 {
@@ -6063,11 +6047,7 @@ impl PlatformExprStrategy {
let field = Self::parse_string_or_identifier(&args[0])?;
let lookback = Self::parse_positive_usize(&args[1])?;
let value = self.resolve_current_rolling_mean(ctx, day, stock, &field, lookback)?;
Ok(Self::push_runtime_helper_value(
scope,
scope_name,
Dynamic::from(value),
))
Ok(RuntimeHelperResolution::Number(value))
}
"rolling_max_current" => {
if args.len() != 2 {
@@ -6080,7 +6060,7 @@ impl PlatformExprStrategy {
let values =
self.resolve_current_rolling_values(ctx, day, stock, &field, lookback)?;
let value = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"rolling_return_stddev_current" => {
if args.len() != 2 {
@@ -6106,7 +6086,9 @@ impl PlatformExprStrategy {
"invalid current rolling return for field {field} with count {return_count}"
)));
}
Ok(Self::format_rhai_float(rolling_sample_stddev(&returns)))
Ok(Self::normalized_runtime_number(rolling_sample_stddev(
&returns,
)))
}
"vma" => {
if args.len() != 1 {
@@ -6116,11 +6098,7 @@ impl PlatformExprStrategy {
}
let lookback = Self::parse_positive_usize(&args[0])?;
let value = self.resolve_rolling_mean(ctx, day, stock, "volume", lookback)?;
Ok(Self::push_runtime_helper_value(
scope,
scope_name,
Dynamic::from(value),
))
Ok(RuntimeHelperResolution::Number(value))
}
"rolling_sum" | "rolling_min" | "rolling_max" | "rolling_stddev" | "stddev"
| "rolling_zscore" => {
@@ -6140,7 +6118,7 @@ impl PlatformExprStrategy {
"rolling_zscore" => rolling_zscore(&values),
_ => 0.0,
};
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"pct_change" => {
if args.len() != 2 {
@@ -6164,7 +6142,7 @@ impl PlatformExprStrategy {
} else {
last / first - 1.0
};
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"factor_value" | "get_factor_value" => {
if args.is_empty() || args.len() > 2 {
@@ -6183,7 +6161,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"factor_text" | "get_factor_text" => {
if args.is_empty() || args.len() > 2 {
@@ -6202,7 +6180,9 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value.clone())
.unwrap_or_default();
Ok(Self::quote_rhai_string(&value))
Ok(RuntimeHelperResolution::Expression(
Self::quote_rhai_string(&value),
))
}
"dividend_cash" | "has_dividend" => {
let (symbol, lookback) =
@@ -6215,9 +6195,9 @@ impl PlatformExprStrategy {
.map(|row| row.dividend_cash_before_tax)
.sum::<f64>();
if helper == "has_dividend" {
Ok((total.abs() > f64::EPSILON).to_string())
Ok(RuntimeHelperResolution::Boolean(total.abs() > f64::EPSILON))
} else {
Ok(Self::format_rhai_float(total))
Ok(Self::normalized_runtime_number(total))
}
}
"split_ratio" | "has_split" => {
@@ -6227,9 +6207,9 @@ impl PlatformExprStrategy {
let splits = ctx.data.get_split(&symbol, start, day.date);
let ratio = splits.iter().map(|row| row.split_ratio).product::<f64>();
if helper == "has_split" {
Ok((!splits.is_empty()).to_string())
Ok(RuntimeHelperResolution::Boolean(!splits.is_empty()))
} else {
Ok(Self::format_rhai_float(if splits.is_empty() {
Ok(Self::normalized_runtime_number(if splits.is_empty() {
1.0
} else {
ratio
@@ -6253,7 +6233,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"shares" | "get_shares_value" => {
let stock = stock.ok_or_else(|| {
@@ -6267,7 +6247,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"turnover_rate" | "get_turnover_rate_value" => {
let stock = stock.ok_or_else(|| {
@@ -6281,7 +6261,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"price_change_rate" | "get_price_change_rate_value" => {
if args.len() > 1 {
@@ -6299,7 +6279,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"stock_connect" | "get_stock_connect_value" => {
let stock = stock.ok_or_else(|| {
@@ -6313,7 +6293,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"current_performance" => {
let stock = stock.ok_or_else(|| {
@@ -6327,7 +6307,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"fundamental" | "get_fundamentals_value" => {
let stock = stock.ok_or_else(|| {
@@ -6341,7 +6321,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"financial" | "get_financials_value" => {
let stock = stock.ok_or_else(|| {
@@ -6355,7 +6335,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"pit_financial" | "get_pit_financials_value" => {
let stock = stock.ok_or_else(|| {
@@ -6369,7 +6349,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"industry_code" | "get_industry_code" => {
if args.len() > 2 {
@@ -6390,7 +6370,7 @@ impl PlatformExprStrategy {
.get_industry(&stock.symbol, &source, level)
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"industry_name" | "get_industry_name" => {
if args.len() > 2 {
@@ -6411,7 +6391,9 @@ impl PlatformExprStrategy {
.get_industry_name(&stock.symbol, &source, level)
.map(|row| row.value)
.unwrap_or_default();
Ok(Self::quote_rhai_string(&value))
Ok(RuntimeHelperResolution::Expression(
Self::quote_rhai_string(&value),
))
}
"yield_curve" | "get_yield_curve_value" => {
if args.is_empty() || args.len() > 2 {
@@ -6427,7 +6409,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
"is_margin_stock" => {
if args.len() > 1 {
@@ -6447,7 +6429,7 @@ impl PlatformExprStrategy {
.get_margin_stocks(&margin_type)
.iter()
.any(|symbol| symbol == &stock.symbol);
Ok(matched.to_string())
Ok(RuntimeHelperResolution::Boolean(matched))
}
"dominant_future" | "get_dominant_future" => {
if args.len() != 1 {
@@ -6457,7 +6439,9 @@ impl PlatformExprStrategy {
}
let underlying = Self::parse_string_or_identifier(&args[0])?;
let symbol = ctx.get_dominant_future(&underlying).unwrap_or_default();
Ok(Self::quote_rhai_string(&symbol))
Ok(RuntimeHelperResolution::Expression(
Self::quote_rhai_string(&symbol),
))
}
"dominant_future_price" | "get_dominant_future_price_value" => {
if args.is_empty() || args.len() > 3 {
@@ -6478,7 +6462,7 @@ impl PlatformExprStrategy {
.last()
.map(|row| Self::price_bar_field(row, &field))
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
Ok(Self::normalized_runtime_number(value))
}
other => Err(BacktestError::Execution(format!(
"unsupported platform helper: {other}"
@@ -6574,6 +6558,17 @@ impl PlatformExprStrategy {
))
}
fn normalized_runtime_number(value: f64) -> RuntimeHelperResolution {
let value = if value.is_finite() {
format!("{value:.12}")
.parse::<f64>()
.expect("formatted finite runtime helper number must parse")
} else {
0.0
};
RuntimeHelperResolution::Number(value)
}
fn parse_optional_positive_usize(
raw: Option<&String>,
fallback: usize,
@@ -6583,24 +6578,6 @@ impl PlatformExprStrategy {
.map(|value| value.unwrap_or(fallback))
}
fn format_rhai_float(value: f64) -> String {
if value.is_finite() {
format!("{value:.12}")
} else {
"0.0".to_string()
}
}
fn push_runtime_helper_value(
scope: &mut Scope<'_>,
scope_name: &str,
value: Dynamic,
) -> String {
let name = scope_name.to_string();
scope.push_dynamic(name.clone(), value);
name
}
fn runtime_helper_scope_name(helper: &str, args: &[String]) -> String {
let signature = format!("{helper}({})", args.join(","));
let mut hash = 14_695_981_039_346_656_037u64;
@@ -11989,8 +11966,8 @@ mod tests {
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController,
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode,
PlatformTradeAction, PlatformUniverseActionKind, SelectionRiskDeferral,
StockFilterQuoteUsage, framework_stock_rolling_factor_requirement,
PlatformTradeAction, PlatformUniverseActionKind, RuntimeHelperResolution,
SelectionRiskDeferral, StockFilterQuoteUsage, framework_stock_rolling_factor_requirement,
};
use crate::{
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
@@ -12058,6 +12035,23 @@ mod tests {
assert_eq!(framework_stock_rolling_factor_requirement("alpha001"), None);
}
#[test]
fn typed_runtime_numbers_preserve_legacy_rhai_formatting() {
let RuntimeHelperResolution::Number(value) =
PlatformExprStrategy::normalized_runtime_number(1.2345678901235)
else {
panic!("expected numeric helper result");
};
assert_eq!(value, "1.234567890123".parse::<f64>().unwrap());
let RuntimeHelperResolution::Number(value) =
PlatformExprStrategy::normalized_runtime_number(f64::NAN)
else {
panic!("expected numeric helper result");
};
assert_eq!(value, 0.0);
}
#[test]
fn platform_rebalance_keeps_unresolved_delisted_position_without_orders_or_replacement() {
let previous_date = d(2025, 1, 2);