fix: preserve unknown numeric conditions through boolean expressions

This commit is contained in:
boris
2026-09-09 03:56:44 +08:00
parent 3cea91467d
commit fda2e70456
2 changed files with 275 additions and 45 deletions
+204 -39
View File
@@ -11,6 +11,7 @@ pub(crate) enum ValueType {
pub(crate) enum Value { pub(crate) enum Value {
Number(f64), Number(f64),
Boolean(bool), Boolean(bool),
Missing(ValueType),
} }
impl Value { impl Value {
@@ -18,20 +19,28 @@ impl Value {
match self { match self {
Self::Number(_) => ValueType::Number, Self::Number(_) => ValueType::Number,
Self::Boolean(_) => ValueType::Boolean, Self::Boolean(_) => ValueType::Boolean,
Self::Missing(value_type) => value_type,
} }
} }
pub(crate) fn as_number(self) -> Option<f64> { pub(crate) fn as_number(self) -> Option<f64> {
match self { match self {
Self::Number(value) => Some(value), Self::Number(value) => Some(value),
Self::Boolean(_) => None, Self::Boolean(_) | Self::Missing(_) => None,
} }
} }
pub(crate) fn as_bool(self) -> Option<bool> { pub(crate) fn as_bool(self) -> Option<bool> {
match self { match self {
Self::Boolean(value) => Some(value), Self::Boolean(value) => Some(value),
Self::Number(_) => None, Self::Number(_) | Self::Missing(_) => None,
}
}
fn normalized(self) -> Self {
match self {
Self::Number(value) if !value.is_finite() => Self::Missing(ValueType::Number),
value => value,
} }
} }
} }
@@ -103,6 +112,8 @@ enum BinaryOp {
LessEqual, LessEqual,
Greater, Greater,
GreaterEqual, GreaterEqual,
And,
Or,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -134,7 +145,7 @@ enum Instruction {
Binary(BinaryOp), Binary(BinaryOp),
Call { builtin: Builtin, argc: u8 }, Call { builtin: Builtin, argc: u8 },
JumpIfFalse(usize), JumpIfFalse(usize),
JumpIfTrue(usize), ShortCircuit { on: bool, target: usize },
Jump(usize), Jump(usize),
Return, Return,
} }
@@ -165,7 +176,7 @@ impl Program {
let mut pc = 0usize; let mut pc = 0usize;
while let Some(instruction) = self.instructions.get(pc) { while let Some(instruction) = self.instructions.get(pc) {
match *instruction { match *instruction {
Instruction::Push(value) => scratch.stack.push(value), Instruction::Push(value) => scratch.stack.push(value.normalized()),
Instruction::LoadVariable(index) => { Instruction::LoadVariable(index) => {
let index = usize::from(index); let index = usize::from(index);
let cached = scratch.variables[index]; let cached = scratch.variables[index];
@@ -173,7 +184,8 @@ impl Program {
Some(value) => value, Some(value) => value,
None => { None => {
let expected_type = self.variable_types[index]; let expected_type = self.variable_types[index];
let value = resolve(index, &self.variables[index], expected_type)?; let value =
resolve(index, &self.variables[index], expected_type)?.normalized();
if value.value_type() != expected_type { if value.value_type() != expected_type {
return Err(EvalError::new(format!( return Err(EvalError::new(format!(
"variable {} expected {:?}, got {:?}", "variable {} expected {:?}, got {:?}",
@@ -219,15 +231,23 @@ impl Program {
scratch.stack.push(value); scratch.stack.push(value);
} }
Instruction::JumpIfFalse(target) => { Instruction::JumpIfFalse(target) => {
let condition = pop_bool(&mut scratch.stack)?; // Like CASE WHEN, only a known true condition takes this branch.
let condition = match pop(&mut scratch.stack)? {
Value::Boolean(value) => value,
Value::Missing(ValueType::Boolean) => false,
_ => return Err(EvalError::new("boolean operand required")),
};
if !condition { if !condition {
pc = target; pc = target;
continue; continue;
} }
} }
Instruction::JumpIfTrue(target) => { Instruction::ShortCircuit { on, target } => {
let condition = pop_bool(&mut scratch.stack)?; let condition =
if condition { scratch.stack.last().copied().ok_or_else(|| {
EvalError::new("stack underflow during short circuit")
})?;
if condition.as_bool() == Some(on) {
pc = target; pc = target;
continue; continue;
} }
@@ -284,31 +304,56 @@ fn pop(stack: &mut Vec<Value>) -> Result<Value, EvalError> {
stack.pop().ok_or_else(|| EvalError::new("stack underflow")) stack.pop().ok_or_else(|| EvalError::new("stack underflow"))
} }
fn pop_bool(stack: &mut Vec<Value>) -> Result<bool, EvalError> {
pop(stack)?
.as_bool()
.ok_or_else(|| EvalError::new("boolean operand required"))
}
fn number(value: Value) -> Result<f64, EvalError> { fn number(value: Value) -> Result<f64, EvalError> {
if value == Value::Missing(ValueType::Number) {
return Ok(f64::NAN);
}
value value
.as_number() .as_number()
.ok_or_else(|| EvalError::new("numeric operand required")) .ok_or_else(|| EvalError::new("numeric operand required"))
} }
fn eval_unary(operator: UnaryOp, value: Value) -> Result<Value, EvalError> { fn eval_unary(operator: UnaryOp, value: Value) -> Result<Value, EvalError> {
match operator { if matches!(value, Value::Missing(_)) {
return Ok(value);
}
let result: Result<Value, EvalError> = match operator {
UnaryOp::Negate => Ok(Value::Number(-number(value)?)), UnaryOp::Negate => Ok(Value::Number(-number(value)?)),
UnaryOp::Not => { UnaryOp::Not => {
Ok(Value::Boolean(!value.as_bool().ok_or_else(|| { Ok(Value::Boolean(!value.as_bool().ok_or_else(|| {
EvalError::new("boolean operand required for !") EvalError::new("boolean operand required for !")
})?)) })?))
} }
} };
Ok(result?.normalized())
} }
fn eval_binary(operator: BinaryOp, lhs: Value, rhs: Value) -> Result<Value, EvalError> { fn eval_binary(operator: BinaryOp, lhs: Value, rhs: Value) -> Result<Value, EvalError> {
match operator { if matches!(operator, BinaryOp::And | BinaryOp::Or) {
let (lhs, rhs) = (lhs.as_bool(), rhs.as_bool());
let result = match operator {
BinaryOp::And if lhs == Some(false) || rhs == Some(false) => Some(false),
BinaryOp::And if lhs == Some(true) && rhs == Some(true) => Some(true),
BinaryOp::Or if lhs == Some(true) || rhs == Some(true) => Some(true),
BinaryOp::Or if lhs == Some(false) && rhs == Some(false) => Some(false),
_ => None,
};
return Ok(result
.map(Value::Boolean)
.unwrap_or(Value::Missing(ValueType::Boolean)));
}
if matches!(lhs, Value::Missing(_)) || matches!(rhs, Value::Missing(_)) {
let value_type = match operator {
BinaryOp::Add
| BinaryOp::Subtract
| BinaryOp::Multiply
| BinaryOp::Divide
| BinaryOp::Remainder => ValueType::Number,
_ => ValueType::Boolean,
};
return Ok(Value::Missing(value_type));
}
let result: Result<Value, EvalError> = match operator {
BinaryOp::Add => Ok(Value::Number(number(lhs)? + number(rhs)?)), BinaryOp::Add => Ok(Value::Number(number(lhs)? + number(rhs)?)),
BinaryOp::Subtract => Ok(Value::Number(number(lhs)? - number(rhs)?)), BinaryOp::Subtract => Ok(Value::Number(number(lhs)? - number(rhs)?)),
BinaryOp::Multiply => Ok(Value::Number(number(lhs)? * number(rhs)?)), BinaryOp::Multiply => Ok(Value::Number(number(lhs)? * number(rhs)?)),
@@ -356,7 +401,27 @@ fn eval_binary(operator: BinaryOp, lhs: Value, rhs: Value) -> Result<Value, Eval
(lhs - rhs) / float_comparison_scale(lhs, rhs) > -f64::EPSILON, (lhs - rhs) / float_comparison_scale(lhs, rhs) > -f64::EPSILON,
)) ))
} }
BinaryOp::And | BinaryOp::Or => unreachable!(),
};
Ok(result?.normalized())
}
pub(crate) fn finite_comparison(operator: &str, lhs: f64, rhs: f64) -> Option<bool> {
if !lhs.is_finite() || !rhs.is_finite() {
return None;
} }
let operator = match operator {
"==" => BinaryOp::Equal,
"!=" => BinaryOp::NotEqual,
"<" => BinaryOp::Less,
"<=" => BinaryOp::LessEqual,
">" => BinaryOp::Greater,
">=" => BinaryOp::GreaterEqual,
_ => return None,
};
eval_binary(operator, Value::Number(lhs), Value::Number(rhs))
.ok()?
.as_bool()
} }
fn float_comparison_scale(lhs: f64, rhs: f64) -> f64 { fn float_comparison_scale(lhs: f64, rhs: f64) -> f64 {
@@ -382,7 +447,16 @@ fn eval_builtin(builtin: Builtin, args: &[Value]) -> Result<Value, EvalError> {
.ok_or_else(|| EvalError::new("missing builtin argument")) .ok_or_else(|| EvalError::new("missing builtin argument"))
.and_then(number) .and_then(number)
}; };
Ok(match builtin { if !matches!(builtin, Builtin::Nz | Builtin::SafeDiv | Builtin::Iff)
&& args.iter().any(|value| matches!(value, Value::Missing(_)))
{
return Ok(Value::Missing(if builtin == Builtin::Between {
ValueType::Boolean
} else {
ValueType::Number
}));
}
let result = match builtin {
Builtin::Round => Value::Number(numeric(0)?.round()), Builtin::Round => Value::Number(numeric(0)?.round()),
Builtin::Floor => Value::Number(numeric(0)?.floor()), Builtin::Floor => Value::Number(numeric(0)?.floor()),
Builtin::Ceil => Value::Number(numeric(0)?.ceil()), Builtin::Ceil => Value::Number(numeric(0)?.ceil()),
@@ -393,7 +467,13 @@ fn eval_builtin(builtin: Builtin, args: &[Value]) -> Result<Value, EvalError> {
Builtin::Pow => Value::Number(numeric(0)?.powf(numeric(1)?)), Builtin::Pow => Value::Number(numeric(0)?.powf(numeric(1)?)),
Builtin::Log => Value::Number(numeric(0)?.ln()), Builtin::Log => Value::Number(numeric(0)?.ln()),
Builtin::Exp => Value::Number(numeric(0)?.exp()), Builtin::Exp => Value::Number(numeric(0)?.exp()),
Builtin::Clamp => Value::Number(numeric(0)?.clamp(numeric(1)?, numeric(2)?)), Builtin::Clamp => {
let (value, low, high) = (numeric(0)?, numeric(1)?, numeric(2)?);
if low > high {
return Err(EvalError::new("clamp lower bound exceeds upper bound"));
}
Value::Number(value.clamp(low, high))
}
Builtin::Between => { Builtin::Between => {
let value = numeric(0)?; let value = numeric(0)?;
Value::Boolean(value >= numeric(1)? && value <= numeric(2)?) Value::Boolean(value >= numeric(1)? && value <= numeric(2)?)
@@ -417,13 +497,15 @@ fn eval_builtin(builtin: Builtin, args: &[Value]) -> Result<Value, EvalError> {
}) })
} }
Builtin::Iff => { Builtin::Iff => {
let condition = args let condition = match args.first().copied() {
.first() Some(Value::Boolean(value)) => value,
.and_then(|value| value.as_bool()) Some(Value::Missing(ValueType::Boolean)) => false,
.ok_or_else(|| EvalError::new("iff condition must be boolean"))?; _ => return Err(EvalError::new("iff condition must be boolean")),
};
if condition { args[1] } else { args[2] } if condition { args[1] } else { args[2] }
} }
}) };
Ok(result.normalized())
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@@ -1119,24 +1201,20 @@ where
let lhs_type = self.expression(lhs)?; let lhs_type = self.expression(lhs)?;
require_type(lhs_type, ValueType::Boolean, position)?; require_type(lhs_type, ValueType::Boolean, position)?;
let branch = self.instructions.len(); let branch = self.instructions.len();
self.instructions.push(match operator { self.instructions.push(Instruction::ShortCircuit {
ParsedBinaryOp::And => Instruction::JumpIfFalse(usize::MAX), on: operator == ParsedBinaryOp::Or,
ParsedBinaryOp::Or => Instruction::JumpIfTrue(usize::MAX), target: usize::MAX,
_ => unreachable!(),
}); });
let rhs_type = self.expression(rhs)?; let rhs_type = self.expression(rhs)?;
require_type(rhs_type, ValueType::Boolean, rhs.position())?; require_type(rhs_type, ValueType::Boolean, rhs.position())?;
let end_jump = self.instructions.len();
self.instructions.push(Instruction::Jump(usize::MAX));
let short_target = self.instructions.len();
self.instructions self.instructions
.push(Instruction::Push(Value::Boolean(matches!( .push(Instruction::Binary(if operator == ParsedBinaryOp::And {
operator, BinaryOp::And
ParsedBinaryOp::Or } else {
)))); BinaryOp::Or
}));
let end_target = self.instructions.len(); let end_target = self.instructions.len();
patch_jump(&mut self.instructions, branch, short_target)?; patch_jump(&mut self.instructions, branch, end_target)?;
patch_jump(&mut self.instructions, end_jump, end_target)?;
return Ok(ValueType::Boolean); return Ok(ValueType::Boolean);
} }
@@ -1285,7 +1363,7 @@ fn patch_jump(
}; };
match instruction { match instruction {
Instruction::JumpIfFalse(value) Instruction::JumpIfFalse(value)
| Instruction::JumpIfTrue(value) | Instruction::ShortCircuit { target: value, .. }
| Instruction::Jump(value) => { | Instruction::Jump(value) => {
*value = target; *value = target;
Ok(()) Ok(())
@@ -1394,6 +1472,93 @@ mod tests {
); );
} }
#[test]
fn nullable_boolean_truth_table_preserves_unknown_under_negation() {
let unknown = Value::Missing(ValueType::Boolean);
let states = [Value::Boolean(false), Value::Boolean(true), unknown];
let and = [
[states[0], states[0], states[0]],
[states[0], states[1], unknown],
[states[0], unknown, unknown],
];
let or = [
[states[0], states[1], unknown],
[states[1], states[1], states[1]],
[unknown, states[1], unknown],
];
for (i, lhs) in states.iter().enumerate() {
for (j, rhs) in states.iter().enumerate() {
let values = [("lhs", *lhs), ("rhs", *rhs)];
assert_eq!(evaluate("lhs && rhs", &values), and[i][j]);
assert_eq!(evaluate("lhs || rhs", &values), or[i][j]);
assert_eq!(evaluate("!!(lhs && rhs)", &values), and[i][j]);
assert_eq!(evaluate("!!(lhs || rhs)", &values), or[i][j]);
}
}
assert_eq!(evaluate("!value", &[("value", unknown)]), unknown);
}
#[test]
fn missing_numeric_operands_do_not_become_boolean_false_or_zero() {
let unknown = Value::Missing(ValueType::Boolean);
for missing in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
for operator in ["==", "!=", "<", "<=", ">", ">="] {
let values = [("value", Value::Number(missing))];
assert_eq!(evaluate(&format!("value {operator} 0.0"), &values), unknown);
assert_eq!(
evaluate(&format!("!(0.0 {operator} value)"), &values),
unknown
);
assert_eq!(
evaluate(&format!("!((value + 1.0) {operator} 0.0)"), &values),
unknown
);
}
}
let missing = [("value", Value::Number(f64::NAN))];
assert_eq!(evaluate("!(min(value, 1.0) > 0.0)", &missing), unknown);
assert_eq!(evaluate("!between(value, 0.0, 1.0)", &missing), unknown);
assert_eq!(evaluate("!(1.0 / 0.0 > 0.0)", &[]), unknown);
assert_eq!(evaluate("!(sqrt(-1.0) > 0.0)", &[]), unknown);
assert_eq!(evaluate("nz(value, 7.0)", &missing), Value::Number(7.0));
assert_eq!(
evaluate("nz(value, 0.0) == 0.0", &missing),
Value::Boolean(true)
);
}
#[test]
fn nullable_short_circuit_and_explicit_conditional_branches_are_lazy() {
for source in [
"false && missing",
"true || missing",
"if true { true } else { missing }",
] {
let program = compile(source, |_| Some(ValueType::Boolean)).unwrap();
program
.evaluate(&mut Scratch::default(), |_, _, _| {
Err(EvalError::new("unused input must not be resolved"))
})
.unwrap();
}
let unknown = Value::Missing(ValueType::Boolean);
assert_eq!(
evaluate("if value { 1.0 } else { 2.0 }", &[("value", unknown)]),
Value::Number(2.0)
);
assert_eq!(
evaluate("iff(value, 1.0, 2.0)", &[("value", unknown)]),
Value::Number(2.0)
);
}
#[test]
fn invalid_clamp_returns_error_without_panicking() {
let program = compile("clamp(1.0, 2.0, 0.0)", |_| None).unwrap();
let result = program.evaluate(&mut Scratch::default(), |_, _, _| unreachable!());
assert!(result.unwrap_err().to_string().contains("lower bound"));
}
#[test] #[test]
fn short_circuit_does_not_resolve_unused_variable() { fn short_circuit_does_not_resolve_unused_variable() {
let program = compile("false && missing", |name| { let program = compile("false && missing", |name| {
+71 -6
View File
@@ -1388,6 +1388,15 @@ pub struct PlatformSelectionQuotePlan {
pub diagnostics: Vec<String>, pub diagnostics: Vec<String>,
} }
fn checked_rhai_comparison(
operator: &str,
lhs: f64,
rhs: f64,
) -> Result<bool, Box<rhai::EvalAltResult>> {
numeric_expr_vm::finite_comparison(operator, lhs, rhs)
.ok_or_else(|| format!("missing_numeric_operand: operator={operator}").into())
}
fn platform_safe_div(lhs: f64, rhs: f64, fallback: f64) -> f64 { fn platform_safe_div(lhs: f64, rhs: f64, fallback: f64) -> f64 {
if rhs.abs() <= f64::EPSILON { if rhs.abs() <= f64::EPSILON {
fallback fallback
@@ -1565,21 +1574,48 @@ impl PlatformExprStrategy {
pub fn new(config: PlatformExprStrategyConfig) -> Self { pub fn new(config: PlatformExprStrategyConfig) -> Self {
let mut engine = Engine::new(); let mut engine = Engine::new();
// Dynamic scripts cannot carry a nullable boolean through Rhai's
// logical operators. Reject an unknown comparison instead of letting
// native NaN comparisons turn missing data into a buy/sell signal.
for operator in ["==", "!=", "<", "<=", ">", ">="] {
engine.register_fn(operator, move |lhs: f64, rhs: f64| {
checked_rhai_comparison(operator, lhs, rhs)
});
engine.register_fn(operator, move |lhs: f64, rhs: i64| {
checked_rhai_comparison(operator, lhs, rhs as f64)
});
engine.register_fn(operator, move |lhs: i64, rhs: f64| {
checked_rhai_comparison(operator, lhs as f64, rhs)
});
}
engine.register_fn("round", |value: f64| value.round()); engine.register_fn("round", |value: f64| value.round());
engine.register_fn("floor", |value: f64| value.floor()); engine.register_fn("floor", |value: f64| value.floor());
engine.register_fn("ceil", |value: f64| value.ceil()); engine.register_fn("ceil", |value: f64| value.ceil());
engine.register_fn("abs", |value: f64| value.abs()); engine.register_fn("abs", |value: f64| value.abs());
engine.register_fn("min", |lhs: f64, rhs: f64| lhs.min(rhs)); engine.register_fn("min", |lhs: f64, rhs: f64| {
engine.register_fn("max", |lhs: f64, rhs: f64| lhs.max(rhs)); if lhs.is_finite() && rhs.is_finite() { lhs.min(rhs) } else { f64::NAN }
});
engine.register_fn("max", |lhs: f64, rhs: f64| {
if lhs.is_finite() && rhs.is_finite() { lhs.max(rhs) } else { f64::NAN }
});
engine.register_fn("sqrt", |value: f64| value.sqrt()); engine.register_fn("sqrt", |value: f64| value.sqrt());
engine.register_fn("pow", |lhs: f64, rhs: f64| lhs.powf(rhs)); engine.register_fn("pow", |lhs: f64, rhs: f64| lhs.powf(rhs));
engine.register_fn("log", |value: f64| value.ln()); engine.register_fn("log", |value: f64| value.ln());
engine.register_fn("exp", |value: f64| value.exp()); engine.register_fn("exp", |value: f64| value.exp());
engine.register_fn("clamp", |value: f64, low: f64, high: f64| { engine.register_fn("clamp", |value: f64, low: f64, high: f64| -> Result<f64, Box<rhai::EvalAltResult>> {
value.clamp(low, high) if !value.is_finite() || !low.is_finite() || !high.is_finite() {
return Ok(f64::NAN);
}
if low > high {
return Err("clamp lower bound exceeds upper bound".into());
}
Ok(value.clamp(low, high))
}); });
engine.register_fn("between", |value: f64, low: f64, high: f64| { engine.register_fn("between", |value: f64, low: f64, high: f64| -> Result<bool, Box<rhai::EvalAltResult>> {
value >= low && value <= high if !value.is_finite() || !low.is_finite() || !high.is_finite() {
return Err("missing_numeric_operand: function=between".into());
}
Ok(value >= low && value <= high)
}); });
engine.register_fn( engine.register_fn(
"nz", "nz",
@@ -8330,6 +8366,7 @@ impl PlatformExprStrategy {
return match value { return match value {
NumericVmValue::Number(number) => Ok(number), NumericVmValue::Number(number) => Ok(number),
NumericVmValue::Boolean(boolean) => Ok(if boolean { 1.0 } else { 0.0 }), NumericVmValue::Boolean(boolean) => Ok(if boolean { 1.0 } else { 0.0 }),
NumericVmValue::Missing(_) => Ok(f64::NAN),
}; };
} }
let value = self.eval_dynamic(ctx, expr, day, stock, position)?; let value = self.eval_dynamic(ctx, expr, day, stock, position)?;
@@ -8360,6 +8397,7 @@ impl PlatformExprStrategy {
return match value { return match value {
NumericVmValue::Boolean(boolean) => Ok(boolean), NumericVmValue::Boolean(boolean) => Ok(boolean),
NumericVmValue::Number(number) => Ok(number.is_finite() && number != 0.0), NumericVmValue::Number(number) => Ok(number.is_finite() && number != 0.0),
NumericVmValue::Missing(_) => Ok(false),
}; };
} }
let value = self.eval_dynamic(ctx, expr, day, stock, position)?; let value = self.eval_dynamic(ctx, expr, day, stock, position)?;
@@ -16604,6 +16642,33 @@ mod tests {
assert!(missing_stock.turnover_ratio.is_nan()); assert!(missing_stock.turnover_ratio.is_nan());
assert!(missing_stock.effective_turnover_ratio.is_nan()); assert!(missing_stock.effective_turnover_ratio.is_nan());
assert_eq!(present_stock.turnover_ratio, 0.0); assert_eq!(present_stock.turnover_ratio, 0.0);
for predicate in [
"!(model_score > 0.0)",
"!(model_score != 0.0)",
"!between(model_score, 0.0, 1.0)",
"!(min(model_score, 1.0) > 0.0)",
"!(model_score > 0.0) || false",
] {
assert!(!strategy.eval_bool(&ctx, predicate, &day, Some(&missing_stock), None).unwrap(), "{predicate}");
}
for predicate in [
"!(model_score > 0.0) || true",
"!(model_score > 0.0 && false)",
] {
assert!(strategy.eval_bool(&ctx, predicate, &day, Some(&missing_stock), None).unwrap(), "{predicate}");
}
for predicate in [
"symbol == \"000001.SZ\" && !(model_score > 0.0)",
"symbol == \"000001.SZ\" && !(model_score > 0)",
"symbol == \"000001.SZ\" && !(0 < model_score)",
] {
let error = strategy.eval_bool(&ctx, predicate, &day, Some(&missing_stock), None).unwrap_err();
assert!(error.to_string().contains("missing_numeric_operand"), "{error}");
}
assert!(!strategy.eval_bool(
&ctx, "symbol == \"OTHER\" && !(model_score > 0.0)",
&day, Some(&missing_stock), None,
).unwrap());
assert!(!strategy.eval_bool(&ctx, "model_score", &day, Some(&missing_stock), None).unwrap()); assert!(!strategy.eval_bool(&ctx, "model_score", &day, Some(&missing_stock), None).unwrap());
assert!(strategy.eval_bool(&ctx, "model_score", &day, Some(&present_stock), None).unwrap()); assert!(strategy.eval_bool(&ctx, "model_score", &day, Some(&present_stock), None).unwrap());
for field in ["turnover_ratio", "effective_turnover_ratio"] { for field in ["turnover_ratio", "effective_turnover_ratio"] {