1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
40pub struct ThreeOutside {
41 prev: Option<Candle>,
42 prev_prev: Option<Candle>,
43 has_emitted: bool,
44}
45
46impl ThreeOutside {
47 pub const fn new() -> Self {
49 Self {
50 prev: None,
51 prev_prev: None,
52 has_emitted: false,
53 }
54 }
55}
56
57impl Indicator for ThreeOutside {
58 type Input = Candle;
59 type Output = f64;
60
61 fn update(&mut self, candle: Candle) -> Option<f64> {
62 self.has_emitted = true;
63 let pp = self.prev_prev;
64 let p = self.prev;
65 self.prev_prev = self.prev;
66 self.prev = Some(candle);
67 let (Some(b1), Some(b2)) = (pp, p) else {
68 return Some(0.0);
69 };
70 let body1 = (b1.close - b1.open).abs();
71 let body2 = (b2.close - b2.open).abs();
72 if body1 <= 0.0 || body2 <= body1 {
73 return Some(0.0);
74 }
75 let b1_red = b1.close < b1.open;
76 let b1_green = b1.close > b1.open;
77 let b2_green = b2.close > b2.open;
78 let b2_red = b2.close < b2.open;
79 let b3_green = candle.close > candle.open;
80 let b3_red = candle.close < candle.open;
81 if b1_red
83 && b2_green
84 && b2.open <= b1.close
85 && b2.close >= b1.open
86 && b3_green
87 && candle.close > b2.close
88 {
89 return Some(1.0);
90 }
91 if b1_green
93 && b2_red
94 && b2.open >= b1.close
95 && b2.close <= b1.open
96 && b3_red
97 && candle.close < b2.close
98 {
99 return Some(-1.0);
100 }
101 Some(0.0)
102 }
103
104 fn reset(&mut self) {
105 self.prev = None;
106 self.prev_prev = None;
107 self.has_emitted = false;
108 }
109
110 fn warmup_period(&self) -> usize {
111 3
112 }
113
114 fn is_ready(&self) -> bool {
115 self.has_emitted
116 }
117
118 fn name(&self) -> &'static str {
119 "ThreeOutside"
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::traits::BatchExt;
127
128 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
129 Candle::new(open, high, low, close, 1.0, ts).unwrap()
130 }
131
132 #[test]
133 fn accessors_and_metadata() {
134 let t = ThreeOutside::new();
135 assert_eq!(t.name(), "ThreeOutside");
136 assert_eq!(t.warmup_period(), 3);
137 assert!(!t.is_ready());
138 }
139
140 #[test]
141 fn three_outside_up_is_plus_one() {
142 let mut t = ThreeOutside::new();
143 assert_eq!(t.update(c(11.0, 11.2, 9.8, 10.0, 0)), Some(0.0));
144 assert_eq!(t.update(c(9.5, 12.0, 9.5, 11.5, 1)), Some(0.0));
145 assert_eq!(t.update(c(11.5, 13.0, 11.4, 12.5, 2)), Some(1.0));
147 }
148
149 #[test]
150 fn three_outside_down_is_minus_one() {
151 let mut t = ThreeOutside::new();
152 assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
153 assert_eq!(t.update(c(12.0, 12.0, 9.0, 9.0, 1)), Some(0.0));
154 assert_eq!(t.update(c(9.0, 9.1, 7.9, 8.0, 2)), Some(-1.0));
156 }
157
158 #[test]
159 fn unconfirmed_third_bar_yields_zero() {
160 let mut t = ThreeOutside::new();
161 t.update(c(11.0, 11.2, 9.8, 10.0, 0));
162 t.update(c(9.5, 12.0, 9.5, 11.5, 1));
163 assert_eq!(t.update(c(11.0, 11.4, 10.9, 11.3, 2)), Some(0.0));
165 }
166
167 #[test]
168 fn first_two_bars_return_zero() {
169 let mut t = ThreeOutside::new();
170 assert_eq!(t.update(c(11.0, 11.2, 9.8, 10.0, 0)), Some(0.0));
171 assert_eq!(t.update(c(9.5, 12.0, 9.5, 11.5, 1)), Some(0.0));
172 }
173
174 #[test]
175 fn batch_equals_streaming() {
176 let candles: Vec<Candle> = (0..40)
177 .map(|i| {
178 let base = 100.0 + i as f64;
179 c(base, base + 1.5, base - 0.5, base + 1.0, i)
180 })
181 .collect();
182 let mut a = ThreeOutside::new();
183 let mut b = ThreeOutside::new();
184 assert_eq!(
185 a.batch(&candles),
186 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
187 );
188 }
189
190 #[test]
191 fn reset_clears_state() {
192 let mut t = ThreeOutside::new();
193 t.update(c(11.0, 11.2, 9.8, 10.0, 0));
194 t.update(c(9.5, 12.0, 9.5, 11.5, 1));
195 t.update(c(11.5, 13.0, 11.4, 12.5, 2));
196 assert!(t.is_ready());
197 t.reset();
198 assert!(!t.is_ready());
199 assert_eq!(t.update(c(11.0, 11.2, 9.8, 10.0, 0)), Some(0.0));
200 }
201}