优化数值表达式helper执行路径
This commit is contained in:
@@ -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,
|
|
||||||
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
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
match expected_type {
|
(NumericVmValueType::Boolean, RuntimeHelperResolution::Boolean(value)) => {
|
||||||
NumericVmValueType::Number => resolved
|
Ok(NumericVmValue::Boolean(value))
|
||||||
|
}
|
||||||
|
(expected_type, RuntimeHelperResolution::Expression(expression)) => match expected_type
|
||||||
|
{
|
||||||
|
NumericVmValueType::Number => expression
|
||||||
.trim()
|
.trim()
|
||||||
.parse::<f64>()
|
.parse::<f64>()
|
||||||
.map(NumericVmValue::Number)
|
.map(NumericVmValue::Number)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
BacktestError::Execution(format!(
|
BacktestError::Execution(format!(
|
||||||
"runtime helper {} produced non-numeric expression {resolved:?}",
|
"runtime helper {} produced non-numeric expression {expression:?}",
|
||||||
binding.name
|
binding.name
|
||||||
))
|
))
|
||||||
}),
|
}),
|
||||||
NumericVmValueType::Boolean => match resolved.trim() {
|
NumericVmValueType::Boolean => match expression.trim() {
|
||||||
"true" => Ok(NumericVmValue::Boolean(true)),
|
"true" => Ok(NumericVmValue::Boolean(true)),
|
||||||
"false" => Ok(NumericVmValue::Boolean(false)),
|
"false" => Ok(NumericVmValue::Boolean(false)),
|
||||||
_ => Err(BacktestError::Execution(format!(
|
_ => Err(BacktestError::Execution(format!(
|
||||||
"runtime helper {} produced non-boolean expression {resolved:?}",
|
"runtime helper {} produced non-boolean expression {expression:?}",
|
||||||
binding.name
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user