1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
46pub struct SeparatingLines {
47 prev: Option<Candle>,
48 has_emitted: bool,
49}
50
51impl SeparatingLines {
52 pub const fn new() -> Self {
54 Self {
55 prev: None,
56 has_emitted: false,
57 }
58 }
59}
60
61impl Indicator for SeparatingLines {
62 type Input = Candle;
63 type Output = f64;
64
65 #[inline]
66 fn update(&mut self, candle: Candle) -> Option<f64> {
67 let prev = self.prev;
68 self.prev = Some(candle);
69 let bar1 = prev?;
70 self.has_emitted = true;
71 let range1 = bar1.high - bar1.low;
72 let range2 = candle.high - candle.low;
73 if range1 <= 0.0 || range2 <= 0.0 {
74 return Some(0.0);
75 }
76 if (candle.open - bar1.open).abs() > 0.05 * range1 {
78 return Some(0.0);
79 }
80 let body2 = candle.close - candle.open;
81 if body2.abs() < 0.5 * range2 {
82 return Some(0.0); }
84 let tol = 0.05 * range2;
85 if bar1.close < bar1.open && body2 > 0.0 && candle.open - candle.low <= tol {
87 return Some(1.0);
88 }
89 if bar1.close > bar1.open && body2 < 0.0 && candle.high - candle.open <= tol {
91 return Some(-1.0);
92 }
93 Some(0.0)
94 }
95
96 fn reset(&mut self) {
97 self.prev = None;
98 self.has_emitted = false;
99 }
100
101 #[inline]
102 fn warmup_period(&self) -> usize {
103 2
104 }
105
106 #[inline]
107 fn is_ready(&self) -> bool {
108 self.has_emitted
109 }
110
111 #[inline]
112 fn name(&self) -> &'static str {
113 "SeparatingLines"
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::traits::BatchExt;
121
122 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
123 Candle::new(open, high, low, close, 1.0, ts).unwrap()
124 }
125
126 #[test]
127 fn accessors_and_metadata() {
128 let t = SeparatingLines::new();
129 assert_eq!(t.name(), "SeparatingLines");
130 assert_eq!(t.warmup_period(), 2);
131 assert!(!t.is_ready());
132 }
133
134 #[test]
135 fn bullish_separating_lines_is_plus_one() {
136 let mut t = SeparatingLines::new();
137 assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), None);
138 assert_eq!(t.update(c(12.0, 14.1, 12.0, 14.0, 1)), Some(1.0));
139 }
140
141 #[test]
142 fn bearish_separating_lines_is_minus_one() {
143 let mut t = SeparatingLines::new();
144 assert_eq!(t.update(c(10.0, 12.1, 9.9, 12.0, 0)), None);
145 assert_eq!(t.update(c(10.0, 10.0, 7.9, 8.0, 1)), Some(-1.0));
146 }
147
148 #[test]
149 fn same_color_yields_zero() {
150 let mut t = SeparatingLines::new();
151 t.update(c(12.0, 14.1, 11.9, 14.0, 0));
153 assert_eq!(t.update(c(12.0, 14.1, 12.0, 14.0, 1)), Some(0.0));
154 }
155
156 #[test]
157 fn different_open_yields_zero() {
158 let mut t = SeparatingLines::new();
159 t.update(c(12.0, 12.1, 9.9, 10.0, 0));
160 assert_eq!(t.update(c(13.0, 15.1, 13.0, 15.0, 1)), Some(0.0));
162 }
163
164 #[test]
165 fn opening_shadow_yields_zero() {
166 let mut t = SeparatingLines::new();
167 t.update(c(12.0, 12.1, 9.9, 10.0, 0));
168 assert_eq!(t.update(c(12.0, 14.1, 11.0, 14.0, 1)), Some(0.0));
170 }
171
172 #[test]
173 fn first_bar_returns_zero() {
174 let mut t = SeparatingLines::new();
175 assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), None);
176 }
177
178 #[test]
179 fn batch_equals_streaming() {
180 let candles: Vec<Candle> = (0..40)
181 .map(|i| {
182 let base = 100.0 + i as f64;
183 c(base, base + 2.0, base, base + 1.9, i)
184 })
185 .collect();
186 let mut a = SeparatingLines::new();
187 let mut b = SeparatingLines::new();
188 assert_eq!(
189 a.batch(&candles),
190 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
191 );
192 }
193
194 #[test]
195 fn reset_clears_state() {
196 let mut t = SeparatingLines::new();
197 t.update(c(12.0, 12.1, 9.9, 10.0, 0));
198 t.update(c(12.0, 14.1, 12.0, 14.0, 1));
199 assert!(t.is_ready());
200 t.reset();
201 assert!(!t.is_ready());
202 assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), None);
203 }
204
205 #[test]
206 fn zero_range_yields_zero() {
207 let mut t = SeparatingLines::new();
208 t.update(c(10.0, 10.0, 10.0, 10.0, 0));
210 assert_eq!(t.update(c(10.0, 12.0, 9.0, 11.0, 1)), Some(0.0));
211 }
212
213 #[test]
214 fn short_second_body_yields_zero() {
215 let mut t = SeparatingLines::new();
216 t.update(c(10.0, 12.0, 8.0, 9.0, 0));
217 assert_eq!(t.update(c(10.0, 11.0, 9.0, 10.1, 1)), Some(0.0));
219 }
220}