1use std::collections::BTreeMap;
8use std::sync::Arc;
9
10use crate::data::Candle;
11use crate::spec::{Condition, IntPredicate, Operand, OperandExpr, PriceField};
12
13#[derive(Debug, Clone)]
15pub struct BarRow {
16 pub candle: Candle,
18 pub values: BTreeMap<Arc<str>, f64>,
25}
26
27#[derive(Debug, Clone, Copy)]
29pub struct RuleState {
30 pub in_position: bool,
32 pub bars_since_entry: Option<u32>,
34}
35
36fn price(candle: &Candle, field: PriceField) -> f64 {
37 match field {
38 PriceField::Open => candle.open,
39 PriceField::High => candle.high,
40 PriceField::Low => candle.low,
41 PriceField::Close => candle.close,
42 PriceField::Volume => candle.volume,
43 PriceField::Hlc3 => candle.hlc3(),
44 PriceField::Ohlc4 => candle.ohlc4(),
45 }
46}
47
48pub fn eval_operand(op: &Operand, history: &[BarRow], idx: usize) -> Option<f64> {
50 let row = history.get(idx)?;
51 match op {
52 Operand::Const(v) => Some(*v),
53 Operand::Ref(name) => row.values.get(name.as_str()).copied(),
56 Operand::Expr(expr) => match expr.as_ref() {
57 OperandExpr::Price(field) => Some(price(&row.candle, *field)),
58 OperandExpr::Prev((inner, n)) => {
59 let i = idx.checked_sub(*n as usize)?;
60 eval_operand(inner, history, i)
61 }
62 OperandExpr::Add((a, b)) => binary(a, b, history, idx, |x, y| x + y),
63 OperandExpr::Sub((a, b)) => binary(a, b, history, idx, |x, y| x - y),
64 OperandExpr::Mul((a, b)) => binary(a, b, history, idx, |x, y| x * y),
65 OperandExpr::Div((a, b)) => binary(a, b, history, idx, |x, y| {
66 if y.abs() < f64::EPSILON {
67 f64::NAN
68 } else {
69 x / y
70 }
71 }),
72 },
73 }
74}
75
76fn binary(
77 lhs: &Operand,
78 rhs: &Operand,
79 history: &[BarRow],
80 idx: usize,
81 combine: impl Fn(f64, f64) -> f64,
82) -> Option<f64> {
83 Some(combine(
84 eval_operand(lhs, history, idx)?,
85 eval_operand(rhs, history, idx)?,
86 ))
87}
88
89fn compare(
90 lhs: &Operand,
91 rhs: &Operand,
92 history: &[BarRow],
93 idx: usize,
94 predicate: impl Fn(f64, f64) -> bool,
95) -> bool {
96 match (
97 eval_operand(lhs, history, idx),
98 eval_operand(rhs, history, idx),
99 ) {
100 (Some(left), Some(right)) => predicate(left, right),
101 _ => false,
102 }
103}
104
105fn cross(a: &Operand, b: &Operand, history: &[BarRow], idx: usize, above: bool) -> bool {
106 if idx == 0 {
107 return false;
108 }
109 let (Some(an), Some(bn)) = (eval_operand(a, history, idx), eval_operand(b, history, idx))
110 else {
111 return false;
112 };
113 let (Some(ap), Some(bp)) = (
114 eval_operand(a, history, idx - 1),
115 eval_operand(b, history, idx - 1),
116 ) else {
117 return false;
118 };
119 if above {
120 ap <= bp && an > bn
121 } else {
122 ap >= bp && an < bn
123 }
124}
125
126fn int_pred(pred: IntPredicate, n: u32) -> bool {
127 match pred {
128 IntPredicate::Gt(k) => n > k,
129 IntPredicate::Lt(k) => n < k,
130 IntPredicate::Ge(k) => n >= k,
131 IntPredicate::Le(k) => n <= k,
132 IntPredicate::Eq(k) => n == k,
133 }
134}
135
136pub fn eval_condition(cond: &Condition, history: &[BarRow], idx: usize, state: RuleState) -> bool {
138 match cond {
139 Condition::Gt((a, b)) => compare(a, b, history, idx, |x, y| x > y),
140 Condition::Lt((a, b)) => compare(a, b, history, idx, |x, y| x < y),
141 Condition::Ge((a, b)) => compare(a, b, history, idx, |x, y| x >= y),
142 Condition::Le((a, b)) => compare(a, b, history, idx, |x, y| x <= y),
143 Condition::Eq((a, b)) => compare(a, b, history, idx, |x, y| (x - y).abs() < f64::EPSILON),
144 Condition::Ne((a, b)) => compare(a, b, history, idx, |x, y| (x - y).abs() >= f64::EPSILON),
145 Condition::CrossAbove((a, b)) => cross(a, b, history, idx, true),
146 Condition::CrossBelow((a, b)) => cross(a, b, history, idx, false),
147 Condition::Between((a, lo, hi)) => {
148 match (
149 eval_operand(a, history, idx),
150 eval_operand(lo, history, idx),
151 eval_operand(hi, history, idx),
152 ) {
153 (Some(x), Some(l), Some(h)) => l <= x && x <= h,
154 _ => false,
155 }
156 }
157 Condition::Rising((a, n)) => compare_prev(a, *n, history, idx, |now, then| now > then),
158 Condition::Falling((a, n)) => compare_prev(a, *n, history, idx, |now, then| now < then),
159 Condition::All(cs) => cs.iter().all(|c| eval_condition(c, history, idx, state)),
160 Condition::Any(cs) => cs.iter().any(|c| eval_condition(c, history, idx, state)),
161 Condition::Not(c) => !eval_condition(c, history, idx, state),
162 Condition::InPosition(want) => state.in_position == *want,
163 Condition::BarsSinceEntry(pred) => {
164 state.bars_since_entry.is_some_and(|n| int_pred(*pred, n))
165 }
166 }
167}
168
169fn compare_prev(
170 a: &Operand,
171 n: u32,
172 history: &[BarRow],
173 idx: usize,
174 f: impl Fn(f64, f64) -> bool,
175) -> bool {
176 let Some(prev_idx) = idx.checked_sub(n as usize) else {
177 return false;
178 };
179 match (
180 eval_operand(a, history, idx),
181 eval_operand(a, history, prev_idx),
182 ) {
183 (Some(now), Some(then)) => f(now, then),
184 _ => false,
185 }
186}
187
188#[must_use]
195pub fn operand_lookback(op: &Operand) -> usize {
196 match op {
197 Operand::Ref(_) | Operand::Const(_) => 0,
198 Operand::Expr(expr) => match expr.as_ref() {
199 OperandExpr::Price(_) => 0,
200 OperandExpr::Prev((inner, n)) => *n as usize + operand_lookback(inner),
202 OperandExpr::Add((a, b))
203 | OperandExpr::Sub((a, b))
204 | OperandExpr::Mul((a, b))
205 | OperandExpr::Div((a, b)) => operand_lookback(a).max(operand_lookback(b)),
206 },
207 }
208}
209
210#[must_use]
212pub fn condition_lookback(cond: &Condition) -> usize {
213 match cond {
214 Condition::Gt((a, b))
215 | Condition::Lt((a, b))
216 | Condition::Ge((a, b))
217 | Condition::Le((a, b))
218 | Condition::Eq((a, b))
219 | Condition::Ne((a, b)) => operand_lookback(a).max(operand_lookback(b)),
220 Condition::CrossAbove((a, b)) | Condition::CrossBelow((a, b)) => {
222 1 + operand_lookback(a).max(operand_lookback(b))
223 }
224 Condition::Between((a, lo, hi)) => operand_lookback(a)
225 .max(operand_lookback(lo))
226 .max(operand_lookback(hi)),
227 Condition::Rising((a, n)) | Condition::Falling((a, n)) => *n as usize + operand_lookback(a),
228 Condition::All(cs) | Condition::Any(cs) => {
229 cs.iter().map(condition_lookback).max().unwrap_or(0)
230 }
231 Condition::Not(c) => condition_lookback(c),
232 Condition::InPosition(_) | Condition::BarsSinceEntry(_) => 0,
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::spec::StrategySpec;
241
242 fn row(close: f64, vals: &[(&str, f64)]) -> BarRow {
243 BarRow {
244 candle: Candle {
245 time: 0,
246 open: close,
247 high: close,
248 low: close,
249 close,
250 volume: 0.0,
251 },
252 values: vals.iter().map(|(k, v)| (Arc::from(*k), *v)).collect(),
253 }
254 }
255
256 const STATE: RuleState = RuleState {
257 in_position: false,
258 bars_since_entry: None,
259 };
260
261 #[test]
262 fn const_and_ref() {
263 let h = vec![row(10.0, &[("ema", 5.0)])];
264 assert_eq!(eval_operand(&Operand::Const(3.0), &h, 0), Some(3.0));
265 assert_eq!(eval_operand(&Operand::Ref("ema".into()), &h, 0), Some(5.0));
266 assert_eq!(eval_operand(&Operand::Ref("missing".into()), &h, 0), None);
267 }
268
269 #[test]
270 fn price_and_prev() {
271 let h = vec![row(10.0, &[]), row(20.0, &[])];
272 let close = StrategySpec::parse(
273 r#"{"symbol":"x","timeframe":"1h","indicators":{},
274 "entry":{"gt":[{"price":"close"},{"prev":[{"price":"close"},1]}]},
275 "exit":{"in_position":true},"sizing":{"type":"fixed_qty","qty":1}}"#,
276 )
277 .unwrap();
278 assert!(eval_condition(&close.entry, &h, 1, STATE));
279 assert!(!eval_condition(&close.entry, &h, 0, STATE)); }
281
282 #[test]
283 fn cross_above_detects_crossing() {
284 let h = vec![
286 row(0.0, &[("f", 1.0), ("s", 2.0)]),
287 row(0.0, &[("f", 3.0), ("s", 2.0)]),
288 ];
289 let cond = Condition::CrossAbove((Operand::Ref("f".into()), Operand::Ref("s".into())));
290 assert!(!eval_condition(&cond, &h, 0, STATE));
291 assert!(eval_condition(&cond, &h, 1, STATE));
292 }
293
294 #[test]
295 fn all_any_not() {
296 let h = vec![row(0.0, &[("a", 5.0)])];
297 let gt = Condition::Gt((Operand::Ref("a".into()), Operand::Const(1.0)));
298 let lt = Condition::Lt((Operand::Ref("a".into()), Operand::Const(1.0)));
299 assert!(eval_condition(
300 &Condition::All(vec![gt.clone()]),
301 &h,
302 0,
303 STATE
304 ));
305 assert!(eval_condition(
306 &Condition::Any(vec![lt.clone(), gt.clone()]),
307 &h,
308 0,
309 STATE
310 ));
311 assert!(eval_condition(&Condition::Not(Box::new(lt)), &h, 0, STATE));
312 }
313
314 fn ohlcv_row(open: f64, high: f64, low: f64, close: f64, volume: f64) -> BarRow {
315 BarRow {
316 candle: Candle {
317 time: 0,
318 open,
319 high,
320 low,
321 close,
322 volume,
323 },
324 values: BTreeMap::new(),
325 }
326 }
327
328 fn px(field: PriceField) -> Operand {
329 Operand::Expr(Box::new(OperandExpr::Price(field)))
330 }
331
332 fn expr(e: OperandExpr) -> Operand {
333 Operand::Expr(Box::new(e))
334 }
335
336 #[test]
337 fn all_price_fields_resolve() {
338 let h = vec![ohlcv_row(10.0, 20.0, 5.0, 15.0, 100.0)];
339 let at = |f| eval_operand(&px(f), &h, 0).unwrap();
340 assert!((at(PriceField::Open) - 10.0).abs() < 1e-9);
341 assert!((at(PriceField::High) - 20.0).abs() < 1e-9);
342 assert!((at(PriceField::Low) - 5.0).abs() < 1e-9);
343 assert!((at(PriceField::Close) - 15.0).abs() < 1e-9);
344 assert!((at(PriceField::Volume) - 100.0).abs() < 1e-9);
345 assert!((at(PriceField::Hlc3) - (20.0 + 5.0 + 15.0) / 3.0).abs() < 1e-9);
346 assert!((at(PriceField::Ohlc4) - (10.0 + 20.0 + 5.0 + 15.0) / 4.0).abs() < 1e-9);
347 }
348
349 #[test]
350 fn arithmetic_operands() {
351 let h = vec![ohlcv_row(0.0, 0.0, 0.0, 0.0, 0.0)];
352 let c = |v| Operand::Const(v);
353 let ev = |e| eval_operand(&expr(e), &h, 0);
354 assert_eq!(
355 ev(OperandExpr::Add((Box::new(c(2.0)), Box::new(c(3.0))))),
356 Some(5.0)
357 );
358 assert_eq!(
359 ev(OperandExpr::Sub((Box::new(c(2.0)), Box::new(c(3.0))))),
360 Some(-1.0)
361 );
362 assert_eq!(
363 ev(OperandExpr::Mul((Box::new(c(2.0)), Box::new(c(3.0))))),
364 Some(6.0)
365 );
366 assert_eq!(
367 ev(OperandExpr::Div((Box::new(c(6.0)), Box::new(c(3.0))))),
368 Some(2.0)
369 );
370 let nan = ev(OperandExpr::Div((Box::new(c(1.0)), Box::new(c(0.0))))).unwrap();
372 assert!(nan.is_nan());
373 }
374
375 #[test]
376 fn arithmetic_with_missing_operand_is_none() {
377 let h = vec![ohlcv_row(0.0, 0.0, 0.0, 0.0, 0.0)];
378 let miss = Box::new(Operand::Ref("nope".into()));
379 let got = eval_operand(
380 &expr(OperandExpr::Add((miss, Box::new(Operand::Const(1.0))))),
381 &h,
382 0,
383 );
384 assert_eq!(got, None);
385 }
386
387 #[test]
388 fn prev_underflow_is_none() {
389 let h = vec![ohlcv_row(0.0, 0.0, 0.0, 0.0, 0.0)];
390 let got = eval_operand(
391 &expr(OperandExpr::Prev((Box::new(px(PriceField::Close)), 3))),
392 &h,
393 0,
394 );
395 assert_eq!(got, None);
396 }
397
398 #[test]
399 fn comparisons_ge_le_eq_ne() {
400 let h = vec![row(0.0, &[("a", 2.0)])];
401 let a = Operand::Ref("a".into());
402 let two = Operand::Const(2.0);
403 let three = Operand::Const(3.0);
404 assert!(eval_condition(
405 &Condition::Ge((a.clone(), two.clone())),
406 &h,
407 0,
408 STATE
409 ));
410 assert!(eval_condition(
411 &Condition::Le((a.clone(), two.clone())),
412 &h,
413 0,
414 STATE
415 ));
416 assert!(eval_condition(
417 &Condition::Eq((a.clone(), two.clone())),
418 &h,
419 0,
420 STATE
421 ));
422 assert!(eval_condition(
423 &Condition::Ne((a.clone(), three)),
424 &h,
425 0,
426 STATE
427 ));
428 let miss = Operand::Ref("x".into());
430 assert!(!eval_condition(&Condition::Gt((miss, two)), &h, 0, STATE));
431 }
432
433 #[test]
434 fn cross_below_detects_crossing() {
435 let h = vec![
436 row(0.0, &[("f", 3.0), ("s", 2.0)]),
437 row(0.0, &[("f", 1.0), ("s", 2.0)]),
438 ];
439 let cond = Condition::CrossBelow((Operand::Ref("f".into()), Operand::Ref("s".into())));
440 assert!(!eval_condition(&cond, &h, 0, STATE));
441 assert!(eval_condition(&cond, &h, 1, STATE));
442 }
443
444 #[test]
445 fn between_inside_and_outside() {
446 let h = vec![row(0.0, &[("a", 5.0)])];
447 let a = Operand::Ref("a".into());
448 let inside = Condition::Between((a.clone(), Operand::Const(1.0), Operand::Const(10.0)));
449 let outside = Condition::Between((a.clone(), Operand::Const(6.0), Operand::Const(10.0)));
450 assert!(eval_condition(&inside, &h, 0, STATE));
451 assert!(!eval_condition(&outside, &h, 0, STATE));
452 let miss = Condition::Between((
454 Operand::Ref("x".into()),
455 Operand::Const(1.0),
456 Operand::Const(10.0),
457 ));
458 assert!(!eval_condition(&miss, &h, 0, STATE));
459 }
460
461 #[test]
462 fn rising_and_falling() {
463 let up = vec![row(0.0, &[("a", 1.0)]), row(0.0, &[("a", 2.0)])];
464 let down = vec![row(0.0, &[("a", 2.0)]), row(0.0, &[("a", 1.0)])];
465 let a = Operand::Ref("a".into());
466 assert!(eval_condition(
467 &Condition::Rising((a.clone(), 1)),
468 &up,
469 1,
470 STATE
471 ));
472 assert!(eval_condition(
473 &Condition::Falling((a.clone(), 1)),
474 &down,
475 1,
476 STATE
477 ));
478 assert!(!eval_condition(&Condition::Rising((a, 5)), &up, 1, STATE));
480 }
481
482 #[test]
483 fn in_position_and_bars_since_entry() {
484 let h = vec![row(0.0, &[])];
485 let open_state = RuleState {
486 in_position: true,
487 bars_since_entry: Some(5),
488 };
489 assert!(eval_condition(
490 &Condition::InPosition(true),
491 &h,
492 0,
493 open_state
494 ));
495 assert!(!eval_condition(&Condition::InPosition(true), &h, 0, STATE));
496
497 let cases = [
499 (IntPredicate::Gt(4), true),
500 (IntPredicate::Lt(6), true),
501 (IntPredicate::Ge(5), true),
502 (IntPredicate::Le(5), true),
503 (IntPredicate::Eq(5), true),
504 (IntPredicate::Eq(4), false),
505 ];
506 for (pred, want) in cases {
507 assert_eq!(
508 eval_condition(&Condition::BarsSinceEntry(pred), &h, 0, open_state),
509 want
510 );
511 }
512 assert!(!eval_condition(
514 &Condition::BarsSinceEntry(IntPredicate::Ge(0)),
515 &h,
516 0,
517 STATE
518 ));
519 }
520}