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