1use crate::error::FinError;
54use crate::signals::{BarInput, Signal, SignalValue};
55use rust_decimal::Decimal;
56use std::collections::VecDeque;
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum NormMethod {
63 MinMax,
67
68 ZScore,
72
73 Percentile,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum Direction {
84 Above,
86 Below,
88 Cross,
90}
91
92#[derive(Debug, Clone)]
99pub struct SignalKind {
100 pub name: String,
102}
103
104impl SignalKind {
105 pub fn new(name: impl Into<String>) -> Self {
107 Self { name: name.into() }
108 }
109}
110
111#[derive(Debug, Clone)]
130pub enum SignalExpr {
131 Raw(SignalKind),
133
134 Add(Box<SignalExpr>, Box<SignalExpr>),
136
137 Sub(Box<SignalExpr>, Box<SignalExpr>),
139
140 Mul(Box<SignalExpr>, Decimal),
142
143 Lag(Box<SignalExpr>, usize),
147
148 Normalize(Box<SignalExpr>, NormMethod, usize),
152
153 Threshold(Box<SignalExpr>, Decimal, Direction),
157}
158
159impl SignalExpr {
160 pub fn raw(name: impl Into<String>) -> Self {
162 Self::Raw(SignalKind::new(name))
163 }
164
165 pub fn add(self, rhs: SignalExpr) -> Self {
167 Self::Add(Box::new(self), Box::new(rhs))
168 }
169
170 pub fn sub(self, rhs: SignalExpr) -> Self {
172 Self::Sub(Box::new(self), Box::new(rhs))
173 }
174
175 pub fn mul(self, factor: Decimal) -> Self {
177 Self::Mul(Box::new(self), factor)
178 }
179
180 pub fn lag(self, n: usize) -> Self {
182 Self::Lag(Box::new(self), n)
183 }
184
185 pub fn normalize(self, method: NormMethod, window: usize) -> Self {
187 Self::Normalize(Box::new(self), method, window)
188 }
189
190 pub fn threshold(self, level: Decimal, direction: Direction) -> Self {
192 Self::Threshold(Box::new(self), level, direction)
193 }
194
195 pub fn leaf_names(&self) -> Vec<&str> {
197 let mut names = Vec::new();
198 self.collect_leaf_names(&mut names);
199 names
200 }
201
202 fn collect_leaf_names<'a>(&'a self, out: &mut Vec<&'a str>) {
203 match self {
204 Self::Raw(kind) => out.push(&kind.name),
205 Self::Add(l, r) | Self::Sub(l, r) => {
206 l.collect_leaf_names(out);
207 r.collect_leaf_names(out);
208 }
209 Self::Mul(inner, _)
210 | Self::Lag(inner, _)
211 | Self::Normalize(inner, _, _)
212 | Self::Threshold(inner, _, _) => inner.collect_leaf_names(out),
213 }
214 }
215}
216
217enum ExprState {
224 Raw,
225 Add(Box<ExprState>, Box<ExprState>),
226 Sub(Box<ExprState>, Box<ExprState>),
227 Mul(Box<ExprState>),
228 Lag {
229 inner: Box<ExprState>,
230 buffer: VecDeque<SignalValue>,
231 n: usize,
232 },
233 Normalize {
234 inner: Box<ExprState>,
235 window: VecDeque<Decimal>,
236 window_size: usize,
237 method: NormMethod,
238 prev: SignalValue,
239 },
240 Threshold {
241 inner: Box<ExprState>,
242 level: Decimal,
243 direction: Direction,
244 prev: SignalValue,
245 },
246}
247
248impl ExprState {
249 fn from_expr(expr: &SignalExpr) -> Self {
251 match expr {
252 SignalExpr::Raw(_) => Self::Raw,
253 SignalExpr::Add(l, r) => {
254 Self::Add(Box::new(Self::from_expr(l)), Box::new(Self::from_expr(r)))
255 }
256 SignalExpr::Sub(l, r) => {
257 Self::Sub(Box::new(Self::from_expr(l)), Box::new(Self::from_expr(r)))
258 }
259 SignalExpr::Mul(inner, _) => Self::Mul(Box::new(Self::from_expr(inner))),
260 SignalExpr::Lag(inner, n) => Self::Lag {
261 inner: Box::new(Self::from_expr(inner)),
262 buffer: VecDeque::new(),
263 n: *n,
264 },
265 SignalExpr::Normalize(inner, method, window_size) => Self::Normalize {
266 inner: Box::new(Self::from_expr(inner)),
267 window: VecDeque::new(),
268 window_size: *window_size,
269 method: *method,
270 prev: SignalValue::Unavailable,
271 },
272 SignalExpr::Threshold(inner, _, direction) => Self::Threshold {
273 inner: Box::new(Self::from_expr(inner)),
274 level: Decimal::ZERO, direction: *direction,
276 prev: SignalValue::Unavailable,
277 },
278 }
279 }
280
281 fn eval(
283 &mut self,
284 expr: &SignalExpr,
285 leaf_values: &std::collections::HashMap<String, SignalValue>,
286 ) -> SignalValue {
287 match (self, expr) {
288 (Self::Raw, SignalExpr::Raw(kind)) => leaf_values
289 .get(&kind.name)
290 .cloned()
291 .unwrap_or(SignalValue::Unavailable),
292
293 (Self::Add(ls, rs), SignalExpr::Add(le, re)) => {
294 let l = ls.eval(le, leaf_values);
295 let r = rs.eval(re, leaf_values);
296 l.add(r)
297 }
298
299 (Self::Sub(ls, rs), SignalExpr::Sub(le, re)) => {
300 let l = ls.eval(le, leaf_values);
301 let r = rs.eval(re, leaf_values);
302 l.sub(r)
303 }
304
305 (Self::Mul(inner_state), SignalExpr::Mul(inner_expr, factor)) => {
306 let v = inner_state.eval(inner_expr, leaf_values);
307 v.mul(*factor)
308 }
309
310 (
311 Self::Lag { inner, buffer, n },
312 SignalExpr::Lag(inner_expr, _),
313 ) => {
314 let v = inner.eval(inner_expr, leaf_values);
315 if *n == 0 {
316 return v;
317 }
318 buffer.push_back(v);
320 if buffer.len() > *n {
322 buffer.pop_front().unwrap_or(SignalValue::Unavailable)
323 } else {
324 SignalValue::Unavailable
325 }
326 }
327
328 (
329 Self::Normalize { inner, window, window_size, method, .. },
330 SignalExpr::Normalize(inner_expr, _, _),
331 ) => {
332 let v = inner.eval(inner_expr, leaf_values);
333 match v {
334 SignalValue::Unavailable => SignalValue::Unavailable,
335 SignalValue::Scalar(d) => {
336 window.push_back(d);
337 if window.len() > *window_size {
338 window.pop_front();
339 }
340 if window.len() < *window_size {
341 return SignalValue::Unavailable;
342 }
343 compute_norm(window, *method, d)
344 }
345 }
346 }
347
348 (
349 Self::Threshold { inner, prev, direction, level },
350 SignalExpr::Threshold(inner_expr, threshold_level, _),
351 ) => {
352 *level = *threshold_level;
353 let v = inner.eval(inner_expr, leaf_values);
354 let result = match direction {
355 Direction::Above => match &v {
356 SignalValue::Scalar(curr) if *curr > *level => {
357 SignalValue::Scalar(Decimal::ONE)
358 }
359 SignalValue::Scalar(_) => SignalValue::Scalar(Decimal::ZERO),
360 SignalValue::Unavailable => SignalValue::Unavailable,
361 },
362 Direction::Below => match &v {
363 SignalValue::Scalar(curr) if *curr < *level => {
364 SignalValue::Scalar(-Decimal::ONE)
365 }
366 SignalValue::Scalar(_) => SignalValue::Scalar(Decimal::ZERO),
367 SignalValue::Unavailable => SignalValue::Unavailable,
368 },
369 Direction::Cross => {
370
371 match (&v, &*prev) {
372 (SignalValue::Scalar(curr), SignalValue::Scalar(p)) => {
373 if *curr > *level && *p <= *level {
374 SignalValue::Scalar(Decimal::ONE)
375 } else if *curr < *level && *p >= *level {
376 SignalValue::Scalar(-Decimal::ONE)
377 } else {
378 SignalValue::Scalar(Decimal::ZERO)
379 }
380 }
381 _ => SignalValue::Unavailable,
382 }
383 }
384 };
385 *prev = v;
386 result
387 }
388
389 _ => SignalValue::Unavailable,
391 }
392 }
393
394 fn reset(&mut self) {
396 match self {
397 Self::Raw | Self::Mul(_) => {}
398 Self::Add(l, r) | Self::Sub(l, r) => {
399 l.reset();
400 r.reset();
401 }
402 Self::Lag { inner, buffer, .. } => {
403 inner.reset();
404 buffer.clear();
405 }
406 Self::Normalize { inner, window, prev, .. } => {
407 inner.reset();
408 window.clear();
409 *prev = SignalValue::Unavailable;
410 }
411 Self::Threshold { inner, prev, .. } => {
412 inner.reset();
413 *prev = SignalValue::Unavailable;
414 }
415 }
416 }
417}
418
419fn compute_norm(
421 window: &VecDeque<Decimal>,
422 method: NormMethod,
423 current: Decimal,
424) -> SignalValue {
425 if window.is_empty() {
426 return SignalValue::Unavailable;
427 }
428 match method {
429 NormMethod::MinMax => {
430 let min = window.iter().copied().fold(current, Decimal::min);
431 let max = window.iter().copied().fold(current, Decimal::max);
432 let range = max - min;
433 if range.is_zero() {
434 SignalValue::Scalar(Decimal::ZERO)
435 } else {
436 match (current - min).checked_div(range) {
437 Some(v) => SignalValue::Scalar(v),
438 None => SignalValue::Unavailable,
439 }
440 }
441 }
442 NormMethod::ZScore => {
443 let n = window.len() as f64;
444 if n < 2.0 {
445 return SignalValue::Unavailable;
446 }
447 use rust_decimal::prelude::ToPrimitive;
448 let mean: f64 = window.iter().filter_map(|v| v.to_f64()).sum::<f64>() / n;
449 let variance: f64 = window
450 .iter()
451 .filter_map(|v| v.to_f64())
452 .map(|v| (v - mean).powi(2))
453 .sum::<f64>()
454 / (n - 1.0);
455 let std_dev = variance.sqrt();
456 if std_dev == 0.0 {
457 return SignalValue::Scalar(Decimal::ZERO);
458 }
459 let curr_f = current.to_f64().unwrap_or(mean);
460 match Decimal::try_from((curr_f - mean) / std_dev) {
461 Ok(z) => SignalValue::Scalar(z),
462 Err(_) => SignalValue::Unavailable,
463 }
464 }
465 NormMethod::Percentile => {
466 let n = window.len();
467 let count_below = window.iter().filter(|&&v| v < current).count();
468 let count_equal = window.iter().filter(|&&v| v == current).count();
469 let rank_f = (count_below as f64 + 0.5 * count_equal as f64) / n as f64;
471 match Decimal::try_from(rank_f) {
472 Ok(rank) => SignalValue::Scalar(rank),
473 Err(_) => SignalValue::Unavailable,
474 }
475 }
476 }
477}
478
479pub struct ComposedSignal {
496 name: String,
497 expr: SignalExpr,
498 state: ExprState,
499 leaves: Vec<Box<dyn Signal>>,
500 bars_seen: usize,
501}
502
503impl ComposedSignal {
504 pub fn new(
513 name: impl Into<String>,
514 expr: SignalExpr,
515 leaves: Vec<Box<dyn Signal>>,
516 ) -> Result<Self, FinError> {
517 if leaves.is_empty() {
518 return Err(FinError::InvalidInput(
519 "ComposedSignal requires at least one leaf signal".into(),
520 ));
521 }
522 let state = ExprState::from_expr(&expr);
523 Ok(Self {
524 name: name.into(),
525 expr,
526 state,
527 leaves,
528 bars_seen: 0,
529 })
530 }
531
532 pub fn leaf_warmup_period(&self) -> usize {
534 self.leaves.iter().map(|s| s.period()).max().unwrap_or(0)
535 }
536}
537
538impl Signal for ComposedSignal {
539 fn name(&self) -> &str {
540 &self.name
541 }
542
543 fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
544 self.bars_seen += 1;
545
546 let mut leaf_values = std::collections::HashMap::with_capacity(self.leaves.len());
548 for leaf in &mut self.leaves {
549 let val = leaf.update(bar)?;
550 leaf_values.insert(leaf.name().to_owned(), val);
551 }
552
553 let result = self.state.eval(&self.expr, &leaf_values);
555 Ok(result)
556 }
557
558 fn is_ready(&self) -> bool {
559 self.leaves.iter().all(|s| s.is_ready())
560 }
561
562 fn period(&self) -> usize {
563 self.leaf_warmup_period()
564 }
565
566 fn reset(&mut self) {
567 for leaf in &mut self.leaves {
568 leaf.reset();
569 }
570 self.state.reset();
571 self.bars_seen = 0;
572 }
573}
574
575pub struct SignalBuilder<S: Signal + 'static> {
601 signal: S,
602 expr: SignalExpr,
604 norm_window: usize,
606}
607
608impl<S: Signal + 'static> SignalBuilder<S> {
609 pub fn new(signal: S) -> Self {
613 let name = signal.name().to_owned();
614 Self {
615 signal,
616 expr: SignalExpr::raw(name),
617 norm_window: 20,
618 }
619 }
620
621 pub fn with_norm_window(mut self, window: usize) -> Self {
623 self.norm_window = window;
624 self
625 }
626
627 pub fn lag(mut self, n: usize) -> Self {
629 self.expr = self.expr.lag(n);
630 self
631 }
632
633 pub fn normalize(mut self, method: NormMethod) -> Self {
635 let window = self.norm_window;
636 self.expr = self.expr.normalize(method, window);
637 self
638 }
639
640 pub fn normalize_window(mut self, method: NormMethod, window: usize) -> Self {
642 self.expr = self.expr.normalize(method, window);
643 self
644 }
645
646 pub fn threshold(mut self, level: Decimal, direction: Direction) -> Self {
648 self.expr = self.expr.threshold(level, direction);
649 self
650 }
651
652 pub fn scale(mut self, factor: Decimal) -> Self {
654 self.expr = self.expr.mul(factor);
655 self
656 }
657
658 pub fn build(self) -> ComposedSignal {
662 let composed_name = format!("composed({})", self.signal.name());
663 self.build_named(composed_name)
664 }
665
666 pub fn build_named(self, name: impl Into<String>) -> ComposedSignal {
668 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(self.signal)];
671 let state = ExprState::from_expr(&self.expr);
672 ComposedSignal { name: name.into(), expr: self.expr, state, leaves, bars_seen: 0 }
673 }
674}
675
676#[cfg(test)]
679mod tests {
680 use super::*;
681 use crate::signals::indicators::{Ema, Rsi, Sma};
682 use rust_decimal_macros::dec;
683
684 fn bar(close: &str) -> BarInput {
685 BarInput::from_close(close.parse().unwrap())
686 }
687
688 fn feed_n(signal: &mut impl Signal, close: &str, n: usize) {
689 for _ in 0..n {
690 signal.update(&bar(close)).unwrap();
691 }
692 }
693
694 #[test]
697 fn test_expr_raw_leaf_name() {
698 let expr = SignalExpr::raw("sma5");
699 assert_eq!(expr.leaf_names(), vec!["sma5"]);
700 }
701
702 #[test]
703 fn test_expr_add_leaf_names() {
704 let expr = SignalExpr::raw("a").add(SignalExpr::raw("b"));
705 let names = expr.leaf_names();
706 assert!(names.contains(&"a"));
707 assert!(names.contains(&"b"));
708 }
709
710 #[test]
711 fn test_expr_nested_leaf_names() {
712 let expr = SignalExpr::raw("sma5")
713 .lag(1)
714 .normalize(NormMethod::ZScore, 20)
715 .threshold(dec!(0), Direction::Above);
716 assert_eq!(expr.leaf_names(), vec!["sma5"]);
717 }
718
719 #[test]
722 fn test_composed_raw_passthrough() {
723 let sma = Sma::new("sma3", 3).unwrap();
724 let expr = SignalExpr::raw("sma3");
725 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
726 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
727
728 feed_n(&mut composed, "10", 2);
729 let v = composed.update(&bar("10")).unwrap();
730 assert!(matches!(v, SignalValue::Scalar(_)));
731 }
732
733 #[test]
734 fn test_composed_raw_unavailable_during_warmup() {
735 let sma = Sma::new("sma5", 5).unwrap();
736 let expr = SignalExpr::raw("sma5");
737 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
738 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
739
740 let v = composed.update(&bar("10")).unwrap();
741 assert_eq!(v, SignalValue::Unavailable);
742 }
743
744 #[test]
747 fn test_composed_mul_scales_value() {
748 let sma = Sma::new("sma1", 1).unwrap();
749 let expr = SignalExpr::raw("sma1").mul(dec!(2));
750 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
751 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
752
753 let v = composed.update(&bar("10")).unwrap();
754 assert_eq!(v, SignalValue::Scalar(dec!(20)));
755 }
756
757 #[test]
760 fn test_composed_add_two_signals() {
761 let sma1 = Sma::new("sma_a", 1).unwrap();
762 let sma2 = Sma::new("sma_b", 1).unwrap();
763 let expr = SignalExpr::raw("sma_a").add(SignalExpr::raw("sma_b"));
764 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma1), Box::new(sma2)];
765 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
766
767 let v = composed.update(&bar("15")).unwrap();
768 assert_eq!(v, SignalValue::Scalar(dec!(30)));
770 }
771
772 #[test]
775 fn test_composed_lag_delays_values() {
776 let sma = Sma::new("sma1", 1).unwrap();
777 let expr = SignalExpr::raw("sma1").lag(2);
778 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
779 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
780
781 composed.update(&bar("10")).unwrap();
783 composed.update(&bar("20")).unwrap();
784 let v = composed.update(&bar("30")).unwrap();
786 assert_eq!(v, SignalValue::Scalar(dec!(10)));
787 }
788
789 #[test]
790 fn test_composed_lag_zero_is_passthrough() {
791 let sma = Sma::new("sma1", 1).unwrap();
792 let expr = SignalExpr::raw("sma1").lag(0);
793 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
794 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
795
796 let v = composed.update(&bar("42")).unwrap();
797 assert_eq!(v, SignalValue::Scalar(dec!(42)));
798 }
799
800 #[test]
803 fn test_normalize_minmax_range_of_constant_returns_zero() {
804 let sma = Sma::new("sma1", 1).unwrap();
805 let expr = SignalExpr::raw("sma1").normalize(NormMethod::MinMax, 3);
806 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
807 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
808
809 composed.update(&bar("10")).unwrap();
811 composed.update(&bar("10")).unwrap();
812 let v = composed.update(&bar("10")).unwrap();
813 assert_eq!(v, SignalValue::Scalar(dec!(0)));
814 }
815
816 #[test]
817 fn test_normalize_minmax_high_value_approaches_one() {
818 let sma = Sma::new("sma1", 1).unwrap();
819 let expr = SignalExpr::raw("sma1").normalize(NormMethod::MinMax, 3);
820 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
821 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
822
823 composed.update(&bar("0")).unwrap();
825 composed.update(&bar("50")).unwrap();
826 let v = composed.update(&bar("100")).unwrap();
827 assert_eq!(v, SignalValue::Scalar(dec!(1)));
828 }
829
830 #[test]
833 fn test_normalize_zscore_mean_value_near_zero() {
834 let sma = Sma::new("sma1", 1).unwrap();
835 let expr = SignalExpr::raw("sma1").normalize(NormMethod::ZScore, 5);
836 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
837 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
838
839 for _ in 0..4 {
841 composed.update(&bar("10")).unwrap();
842 }
843 let v = composed.update(&bar("10")).unwrap();
844 if let SignalValue::Scalar(z) = v {
845 assert!(z.abs() < dec!(0.001), "z-score of mean should be near 0, got {z}");
846 } else {
847 panic!("expected Scalar");
848 }
849 }
850
851 #[test]
854 fn test_normalize_percentile_highest_value() {
855 let sma = Sma::new("sma1", 1).unwrap();
856 let expr = SignalExpr::raw("sma1").normalize(NormMethod::Percentile, 4);
857 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
858 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
859
860 composed.update(&bar("10")).unwrap();
861 composed.update(&bar("20")).unwrap();
862 composed.update(&bar("30")).unwrap();
863 let v = composed.update(&bar("100")).unwrap(); if let SignalValue::Scalar(pct) = v {
865 assert!(pct > dec!(0.5), "max value should have pct > 0.5, got {pct}");
866 } else {
867 panic!("expected Scalar");
868 }
869 }
870
871 #[test]
874 fn test_threshold_above_emits_one_when_above() {
875 let sma = Sma::new("sma1", 1).unwrap();
876 let expr = SignalExpr::raw("sma1").threshold(dec!(50), Direction::Above);
877 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
878 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
879
880 let v = composed.update(&bar("75")).unwrap();
881 assert_eq!(v, SignalValue::Scalar(dec!(1)));
882 }
883
884 #[test]
885 fn test_threshold_above_emits_zero_when_below() {
886 let sma = Sma::new("sma1", 1).unwrap();
887 let expr = SignalExpr::raw("sma1").threshold(dec!(50), Direction::Above);
888 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
889 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
890
891 let v = composed.update(&bar("25")).unwrap();
892 assert_eq!(v, SignalValue::Scalar(dec!(0)));
893 }
894
895 #[test]
896 fn test_threshold_below_emits_neg_one_when_below() {
897 let sma = Sma::new("sma1", 1).unwrap();
898 let expr = SignalExpr::raw("sma1").threshold(dec!(50), Direction::Below);
899 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
900 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
901
902 let v = composed.update(&bar("20")).unwrap();
903 assert_eq!(v, SignalValue::Scalar(dec!(-1)));
904 }
905
906 #[test]
907 fn test_threshold_cross_emits_one_on_upward_cross() {
908 let sma = Sma::new("sma1", 1).unwrap();
909 let expr = SignalExpr::raw("sma1").threshold(dec!(50), Direction::Cross);
910 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
911 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
912
913 composed.update(&bar("40")).unwrap(); composed.update(&bar("40")).unwrap(); let v = composed.update(&bar("60")).unwrap(); assert_eq!(v, SignalValue::Scalar(dec!(1)));
917 }
918
919 #[test]
920 fn test_threshold_cross_emits_neg_one_on_downward_cross() {
921 let sma = Sma::new("sma1", 1).unwrap();
922 let expr = SignalExpr::raw("sma1").threshold(dec!(50), Direction::Cross);
923 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
924 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
925
926 composed.update(&bar("60")).unwrap(); composed.update(&bar("60")).unwrap(); let v = composed.update(&bar("40")).unwrap(); assert_eq!(v, SignalValue::Scalar(dec!(-1)));
930 }
931
932 #[test]
935 fn test_builder_builds_composed_signal() {
936 let sma = Sma::new("sma5", 5).unwrap();
937 let mut composed = SignalBuilder::new(sma).lag(1).build();
938 assert_eq!(composed.name(), "composed(sma5)");
939 let v = composed.update(&bar("100")).unwrap();
940 assert_eq!(v, SignalValue::Unavailable); }
942
943 #[test]
944 fn test_builder_build_named() {
945 let sma = Sma::new("sma5", 5).unwrap();
946 let composed = SignalBuilder::new(sma).build_named("my_signal");
947 assert_eq!(composed.name(), "my_signal");
948 }
949
950 #[test]
951 fn test_builder_scale() {
952 let sma = Sma::new("sma1", 1).unwrap();
953 let mut composed = SignalBuilder::new(sma).scale(dec!(3)).build();
954 let v = composed.update(&bar("10")).unwrap();
955 assert_eq!(v, SignalValue::Scalar(dec!(30)));
956 }
957
958 #[test]
959 fn test_builder_normalize_minmax() {
960 let sma = Sma::new("sma1", 1).unwrap();
961 let mut composed = SignalBuilder::new(sma)
962 .normalize_window(NormMethod::MinMax, 3)
963 .build();
964 composed.update(&bar("0")).unwrap();
965 composed.update(&bar("50")).unwrap();
966 let v = composed.update(&bar("100")).unwrap();
967 assert_eq!(v, SignalValue::Scalar(dec!(1)));
968 }
969
970 #[test]
971 fn test_builder_threshold_above() {
972 let sma = Sma::new("sma1", 1).unwrap();
973 let mut composed = SignalBuilder::new(sma)
974 .threshold(dec!(50), Direction::Above)
975 .build();
976 let v = composed.update(&bar("80")).unwrap();
977 assert_eq!(v, SignalValue::Scalar(dec!(1)));
978 }
979
980 #[test]
981 fn test_builder_chain_lag_normalize_threshold() {
982 let rsi = Rsi::new("rsi5", 5).unwrap();
983 let mut composed = SignalBuilder::new(rsi)
984 .lag(1)
985 .normalize_window(NormMethod::ZScore, 10)
986 .threshold(dec!(1), Direction::Above)
987 .build();
988
989 for _ in 0..30 {
991 composed.update(&bar("50")).unwrap();
992 }
993 let v = composed.update(&bar("50")).unwrap();
995 assert!(matches!(v, SignalValue::Scalar(_)));
996 }
997
998 #[test]
1001 fn test_composed_reset_restarts_warmup() {
1002 let sma = Sma::new("sma3", 3).unwrap();
1003 let expr = SignalExpr::raw("sma3");
1004 let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma)];
1005 let mut composed = ComposedSignal::new("test", expr, leaves).unwrap();
1006
1007 feed_n(&mut composed, "10", 3);
1008 assert!(composed.is_ready());
1009 composed.reset();
1010 assert!(!composed.is_ready());
1011 let v = composed.update(&bar("10")).unwrap();
1012 assert_eq!(v, SignalValue::Unavailable);
1013 }
1014
1015 #[test]
1018 fn test_composed_period_reflects_max_leaf_period() {
1019 let ema = Ema::new("ema10", 10).unwrap();
1020 let composed = SignalBuilder::new(ema).build();
1021 assert_eq!(composed.period(), 10);
1022 }
1023
1024 #[test]
1027 fn test_composed_new_fails_with_empty_leaves() {
1028 let expr = SignalExpr::raw("nonexistent");
1029 let leaves: Vec<Box<dyn Signal>> = vec![];
1030 let result = ComposedSignal::new("test", expr, leaves);
1031 assert!(result.is_err());
1032 }
1033}