优化数值表达式helper执行路径
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user