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

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