1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
45pub struct Thrusting {
46 prev: Option<Candle>,
47 has_emitted: bool,
48}
49
50impl Thrusting {
51 pub const fn new() -> Self {
53 Self {
54 prev: None,
55 has_emitted: false,
56 }
57 }
58}
59
60impl Indicator for Thrusting {
61 type Input = Candle;
62 type Output = f64;
63
64 #[inline]
65 fn update(&mut self, candle: Candle) -> Option<f64> {
66 let prev = self.prev;
67 self.prev = Some(candle);
68 let bar1 = prev?;
69 self.has_emitted = true;
70 let range1 = bar1.high - bar1.low;
71 if range1 <= 0.0 {
72 return Some(0.0);
73 }
74 let body1 = bar1.open - bar1.close;
75 let mid1 = f64::midpoint(bar1.open, bar1.close);
76 if bar1.close < bar1.open
77 && body1 >= 0.5 * range1
78 && candle.close > candle.open
79 && candle.open < bar1.low
80 && candle.close > bar1.close + 0.1 * body1
81 && candle.close < mid1
82 {
83 return Some(-1.0);
84 }
85 Some(0.0)
86 }
87
88 fn reset(&mut self) {
89 self.prev = None;
90 self.has_emitted = false;
91 }
92
93 #[inline]
94 fn warmup_period(&self) -> usize {
95 2
96 }
97
98 #[inline]
99 fn is_ready(&self) -> bool {
100 self.has_emitted
101 }
102
103 #[inline]
104 fn name(&self) -> &'static str {
105 "Thrusting"
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::traits::BatchExt;
113
114 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
115 Candle::new(open, high, low, close, 1.0, ts).unwrap()
116 }
117
118 #[test]
119 fn accessors_and_metadata() {
120 let t = Thrusting::new();
121 assert_eq!(t.name(), "Thrusting");
122 assert_eq!(t.warmup_period(), 2);
123 assert!(!t.is_ready());
124 }
125
126 #[test]
127 fn thrusting_is_minus_one() {
128 let mut t = Thrusting::new();
129 assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), None);
130 assert_eq!(t.update(c(7.0, 11.6, 6.9, 11.5, 1)), Some(-1.0));
131 }
132
133 #[test]
134 fn shallow_close_yields_zero() {
135 let mut t = Thrusting::new();
136 t.update(c(15.0, 15.1, 9.0, 10.0, 0));
137 assert_eq!(t.update(c(7.0, 10.3, 6.9, 10.2, 1)), Some(0.0));
139 }
140
141 #[test]
142 fn close_past_midpoint_yields_zero() {
143 let mut t = Thrusting::new();
144 t.update(c(15.0, 15.1, 9.0, 10.0, 0));
145 assert_eq!(t.update(c(7.0, 13.1, 6.9, 13.0, 1)), Some(0.0));
147 }
148
149 #[test]
150 fn second_bar_black_yields_zero() {
151 let mut t = Thrusting::new();
152 t.update(c(15.0, 15.1, 9.0, 10.0, 0));
153 assert_eq!(t.update(c(12.0, 12.1, 6.9, 11.5, 1)), Some(0.0));
154 }
155
156 #[test]
157 fn first_bar_returns_zero() {
158 let mut t = Thrusting::new();
159 assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), None);
160 }
161
162 #[test]
163 fn batch_equals_streaming() {
164 let candles: Vec<Candle> = (0..40)
165 .map(|i| {
166 let base = 100.0 + i as f64;
167 c(base + 5.0, base + 5.1, base - 1.0, base, i)
168 })
169 .collect();
170 let mut a = Thrusting::new();
171 let mut b = Thrusting::new();
172 assert_eq!(
173 a.batch(&candles),
174 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
175 );
176 }
177
178 #[test]
179 fn reset_clears_state() {
180 let mut t = Thrusting::new();
181 t.update(c(15.0, 15.1, 9.0, 10.0, 0));
182 t.update(c(7.0, 11.6, 6.9, 11.5, 1));
183 assert!(t.is_ready());
184 t.reset();
185 assert!(!t.is_ready());
186 assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), None);
187 }
188
189 #[test]
190 fn zero_range_first_bar_yields_zero() {
191 let mut t = Thrusting::new();
192 t.update(c(10.0, 10.0, 10.0, 10.0, 0));
194 assert_eq!(t.update(c(9.0, 10.0, 8.0, 9.5, 1)), Some(0.0));
195 }
196}