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