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