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 let pp = self.prev_prev;
63 let p = self.prev;
64 self.prev_prev = self.prev;
65 self.prev = Some(candle);
66 let (Some(b1), Some(b2)) = (pp, p) else {
67 return None;
68 };
69 self.has_emitted = true;
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 #[inline]
111 fn warmup_period(&self) -> usize {
112 3
113 }
114
115 #[inline]
116 fn is_ready(&self) -> bool {
117 self.has_emitted
118 }
119
120 #[inline]
121 fn name(&self) -> &'static str {
122 "ThreeOutside"
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::traits::BatchExt;
130
131 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
132 Candle::new(open, high, low, close, 1.0, ts).unwrap()
133 }
134
135 #[test]
136 fn accessors_and_metadata() {
137 let t = ThreeOutside::new();
138 assert_eq!(t.name(), "ThreeOutside");
139 assert_eq!(t.warmup_period(), 3);
140 assert!(!t.is_ready());
141 }
142
143 #[test]
144 fn three_outside_up_is_plus_one() {
145 let mut t = ThreeOutside::new();
146 assert_eq!(t.update(c(11.0, 11.2, 9.8, 10.0, 0)), None);
147 assert_eq!(t.update(c(9.5, 12.0, 9.5, 11.5, 1)), None);
148 assert_eq!(t.update(c(11.5, 13.0, 11.4, 12.5, 2)), Some(1.0));
150 }
151
152 #[test]
153 fn three_outside_down_is_minus_one() {
154 let mut t = ThreeOutside::new();
155 assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), None);
156 assert_eq!(t.update(c(12.0, 12.0, 9.0, 9.0, 1)), None);
157 assert_eq!(t.update(c(9.0, 9.1, 7.9, 8.0, 2)), Some(-1.0));
159 }
160
161 #[test]
162 fn unconfirmed_third_bar_yields_zero() {
163 let mut t = ThreeOutside::new();
164 t.update(c(11.0, 11.2, 9.8, 10.0, 0));
165 t.update(c(9.5, 12.0, 9.5, 11.5, 1));
166 assert_eq!(t.update(c(11.0, 11.4, 10.9, 11.3, 2)), Some(0.0));
168 }
169
170 #[test]
171 fn first_two_bars_return_zero() {
172 let mut t = ThreeOutside::new();
173 assert_eq!(t.update(c(11.0, 11.2, 9.8, 10.0, 0)), None);
174 assert_eq!(t.update(c(9.5, 12.0, 9.5, 11.5, 1)), None);
175 }
176
177 #[test]
178 fn batch_equals_streaming() {
179 let candles: Vec<Candle> = (0..40)
180 .map(|i| {
181 let base = 100.0 + i as f64;
182 c(base, base + 1.5, base - 0.5, base + 1.0, i)
183 })
184 .collect();
185 let mut a = ThreeOutside::new();
186 let mut b = ThreeOutside::new();
187 assert_eq!(
188 a.batch(&candles),
189 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
190 );
191 }
192
193 #[test]
194 fn reset_clears_state() {
195 let mut t = ThreeOutside::new();
196 t.update(c(11.0, 11.2, 9.8, 10.0, 0));
197 t.update(c(9.5, 12.0, 9.5, 11.5, 1));
198 t.update(c(11.5, 13.0, 11.4, 12.5, 2));
199 assert!(t.is_ready());
200 t.reset();
201 assert!(!t.is_ready());
202 assert_eq!(t.update(c(11.0, 11.2, 9.8, 10.0, 0)), None);
203 }
204}