wickra_core/indicators/
td_propulsion.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
17use crate::traits::Indicator;
18
19#[derive(Debug, Clone, Default)]
21pub struct TdPropulsion {
22 prev: Option<Candle>,
23 last_value: Option<f64>,
24}
25
26impl TdPropulsion {
27 #[must_use]
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub const fn value(&self) -> Option<f64> {
35 self.last_value
36 }
37}
38
39impl Indicator for TdPropulsion {
40 type Input = Candle;
41 type Output = f64;
42
43 fn update(&mut self, candle: Candle) -> Option<f64> {
44 let Some(prev) = self.prev else {
45 self.prev = Some(candle);
46 self.last_value = Some(0.0);
47 return Some(0.0);
48 };
49 let v = if candle.open >= prev.close && candle.close > prev.high {
50 1.0
51 } else if candle.open <= prev.close && candle.close < prev.low {
52 -1.0
53 } else {
54 0.0
55 };
56 self.prev = Some(candle);
57 self.last_value = Some(v);
58 Some(v)
59 }
60
61 fn reset(&mut self) {
62 self.prev = None;
63 self.last_value = None;
64 }
65
66 fn warmup_period(&self) -> usize {
67 2
68 }
69
70 fn is_ready(&self) -> bool {
71 self.last_value.is_some()
72 }
73
74 fn name(&self) -> &'static str {
75 "TDPropulsion"
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::traits::BatchExt;
83
84 fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
85 Candle::new_unchecked(open, high, low, close, 0.0, 0)
86 }
87
88 #[test]
89 fn accessors_and_metadata() {
90 let td = TdPropulsion::new();
91 assert_eq!(td.warmup_period(), 2);
92 assert_eq!(td.name(), "TDPropulsion");
93 assert!(!td.is_ready());
94 assert_eq!(td.value(), None);
95 }
96
97 #[test]
98 fn first_bar_seeds_without_signal() {
99 let mut td = TdPropulsion::new();
100 assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0));
101 assert!(td.update(c(10.5, 12.0, 10.0, 11.5)).is_some());
102 }
103
104 #[test]
105 fn propulsion_up() {
106 let mut td = TdPropulsion::new();
108 td.update(c(9.5, 11.0, 9.0, 10.0));
109 assert_eq!(td.update(c(10.5, 12.0, 10.0, 11.5)), Some(1.0));
110 }
111
112 #[test]
113 fn propulsion_down() {
114 let mut td = TdPropulsion::new();
116 td.update(c(10.5, 11.0, 9.0, 10.0));
117 assert_eq!(td.update(c(9.5, 10.0, 8.0, 8.5)), Some(-1.0));
118 }
119
120 #[test]
121 fn no_thrust_is_zero() {
122 let mut td = TdPropulsion::new();
123 td.update(c(9.5, 11.0, 9.0, 10.0));
124 assert_eq!(td.update(c(10.5, 10.8, 10.0, 10.5)), Some(0.0));
126 }
127
128 #[test]
129 fn reset_clears_state() {
130 let mut td = TdPropulsion::new();
131 td.update(c(9.5, 11.0, 9.0, 10.0));
132 td.update(c(10.5, 12.0, 10.0, 11.5));
133 assert!(td.is_ready());
134 td.reset();
135 assert!(!td.is_ready());
136 assert_eq!(td.update(c(9.5, 11.0, 9.0, 10.0)), Some(0.0));
137 }
138
139 #[test]
140 fn batch_equals_streaming() {
141 let candles: Vec<Candle> = (0..40)
142 .map(|i| {
143 let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
144 c(b, b + 1.0, b - 1.0, b + 0.3)
145 })
146 .collect();
147 let batch = TdPropulsion::new().batch(&candles);
148 let mut b = TdPropulsion::new();
149 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
150 assert_eq!(batch, streamed);
151 }
152}