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)]
36pub struct TdPropulsion {
37 prev: Option<Candle>,
38 last_value: Option<f64>,
39}
40
41impl TdPropulsion {
42 #[must_use]
44 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub const fn value(&self) -> Option<f64> {
50 self.last_value
51 }
52}
53
54impl Indicator for TdPropulsion {
55 type Input = Candle;
56 type Output = f64;
57
58 fn update(&mut self, candle: Candle) -> Option<f64> {
59 let Some(prev) = self.prev else {
60 self.prev = Some(candle);
61 self.last_value = Some(0.0);
62 return Some(0.0);
63 };
64 let v = if candle.open >= prev.close && candle.close > prev.high {
65 1.0
66 } else if candle.open <= prev.close && candle.close < prev.low {
67 -1.0
68 } else {
69 0.0
70 };
71 self.prev = Some(candle);
72 self.last_value = Some(v);
73 Some(v)
74 }
75
76 fn reset(&mut self) {
77 self.prev = None;
78 self.last_value = None;
79 }
80
81 fn warmup_period(&self) -> usize {
82 2
83 }
84
85 fn is_ready(&self) -> bool {
86 self.last_value.is_some()
87 }
88
89 fn name(&self) -> &'static str {
90 "TDPropulsion"
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::traits::BatchExt;
98
99 fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
100 Candle::new_unchecked(open, high, low, close, 0.0, 0)
101 }
102
103 #[test]
104 fn accessors_and_metadata() {
105 let td = TdPropulsion::new();
106 assert_eq!(td.warmup_period(), 2);
107 assert_eq!(td.name(), "TDPropulsion");
108 assert!(!td.is_ready());
109 assert_eq!(td.value(), None);
110 }
111
112 #[test]
113 fn first_bar_seeds_without_signal() {
114 let mut td = TdPropulsion::new();
115 assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0));
116 assert!(td.update(c(10.5, 12.0, 10.0, 11.5)).is_some());
117 }
118
119 #[test]
120 fn propulsion_up() {
121 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, 12.0, 10.0, 11.5)), Some(1.0));
125 }
126
127 #[test]
128 fn propulsion_down() {
129 let mut td = TdPropulsion::new();
131 td.update(c(10.5, 11.0, 9.0, 10.0));
132 assert_eq!(td.update(c(9.5, 10.0, 8.0, 8.5)), Some(-1.0));
133 }
134
135 #[test]
136 fn no_thrust_is_zero() {
137 let mut td = TdPropulsion::new();
138 td.update(c(9.5, 11.0, 9.0, 10.0));
139 assert_eq!(td.update(c(10.5, 10.8, 10.0, 10.5)), Some(0.0));
141 }
142
143 #[test]
144 fn reset_clears_state() {
145 let mut td = TdPropulsion::new();
146 td.update(c(9.5, 11.0, 9.0, 10.0));
147 td.update(c(10.5, 12.0, 10.0, 11.5));
148 assert!(td.is_ready());
149 td.reset();
150 assert!(!td.is_ready());
151 assert_eq!(td.update(c(9.5, 11.0, 9.0, 10.0)), Some(0.0));
152 }
153
154 #[test]
155 fn batch_equals_streaming() {
156 let candles: Vec<Candle> = (0..40)
157 .map(|i| {
158 let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
159 c(b, b + 1.0, b - 1.0, b + 0.3)
160 })
161 .collect();
162 let batch = TdPropulsion::new().batch(&candles);
163 let mut b = TdPropulsion::new();
164 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
165 assert_eq!(batch, streamed);
166 }
167}