wickra_core/indicators/
long_line.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6use std::collections::VecDeque;
7
8#[derive(Debug, Clone)]
49pub struct LongLine {
50 period: usize,
51 ranges: VecDeque<f64>,
52 has_emitted: bool,
56}
57
58impl Default for LongLine {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl LongLine {
65 pub const fn new() -> Self {
67 Self {
68 period: 5,
69 ranges: VecDeque::new(),
70 has_emitted: false,
71 }
72 }
73
74 pub fn with_period(period: usize) -> Result<Self> {
78 if period == 0 {
79 return Err(Error::PeriodZero);
80 }
81 if period > crate::error::MAX_PERIOD {
82 return Err(Error::InvalidPeriod {
83 message: crate::error::PERIOD_ABOVE_MAX,
84 });
85 }
86 Ok(Self {
87 period,
88 ranges: VecDeque::new(),
89 has_emitted: false,
90 })
91 }
92
93 pub fn period(&self) -> usize {
95 self.period
96 }
97}
98
99impl Indicator for LongLine {
100 type Input = Candle;
101 type Output = f64;
102
103 #[inline]
104 fn update(&mut self, candle: Candle) -> Option<f64> {
105 let range = candle.high - candle.low;
106 let body = candle.close - candle.open;
107 if self.ranges.len() < self.period {
108 self.ranges.push_back(range);
109 return None;
110 }
111 self.has_emitted = true;
113 let avg = self.ranges.iter().sum::<f64>() / self.period as f64;
114 self.ranges.push_back(range);
115 self.ranges.pop_front();
116 if range > avg && body.abs() >= 0.5 * range {
117 return Some(if body > 0.0 { 1.0 } else { -1.0 });
118 }
119 Some(0.0)
120 }
121
122 fn reset(&mut self) {
123 self.has_emitted = false;
124 self.ranges.clear();
125 }
126
127 #[inline]
128 fn warmup_period(&self) -> usize {
129 self.period
130 }
131
132 #[inline]
133 fn is_ready(&self) -> bool {
134 self.has_emitted
135 }
136
137 #[inline]
138 fn name(&self) -> &'static str {
139 "LongLine"
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::traits::BatchExt;
147
148 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
149 Candle::new(open, high, low, close, 1.0, ts).unwrap()
150 }
151
152 fn warm(t: &mut LongLine) {
153 for ts in 0..5 {
154 assert_eq!(t.update(c(10.0, 10.5, 9.5, 10.2, ts)), None);
155 }
156 }
157
158 #[test]
159 fn rejects_zero_period() {
160 assert!(LongLine::with_period(0).is_err());
161 }
162
163 #[test]
164 fn accepts_valid_period() {
165 let t = LongLine::with_period(10).unwrap();
166 assert_eq!(t.period(), 10);
167 }
168
169 #[test]
170 fn accessors_and_metadata() {
171 let t = LongLine::new();
172 assert_eq!(t.name(), "LongLine");
173 assert_eq!(t.warmup_period(), 5);
174 assert!(!t.is_ready());
175 assert_eq!(t.period(), 5);
176 }
177
178 #[test]
179 fn long_white_line_is_plus_one() {
180 let mut t = LongLine::new();
181 warm(&mut t);
182 assert!(!t.is_ready());
185 assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, 5)), Some(1.0));
186 }
187
188 #[test]
189 fn long_black_line_is_minus_one() {
190 let mut t = LongLine::new();
191 warm(&mut t);
192 assert_eq!(t.update(c(13.0, 13.1, 9.9, 10.0, 5)), Some(-1.0));
193 }
194
195 #[test]
196 fn short_range_yields_zero() {
197 let mut t = LongLine::new();
198 warm(&mut t);
199 assert_eq!(t.update(c(10.0, 10.5, 9.5, 10.2, 5)), Some(0.0));
201 }
202
203 #[test]
204 fn wide_range_small_body_yields_zero() {
205 let mut t = LongLine::new();
206 warm(&mut t);
207 assert_eq!(t.update(c(10.5, 13.0, 9.9, 10.6, 5)), Some(0.0));
209 }
210
211 #[test]
212 fn warmup_withholds() {
213 let mut t = LongLine::new();
214 for ts in 0..5 {
215 assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, ts)), None);
216 }
217 }
218
219 #[test]
220 fn batch_equals_streaming() {
221 let candles: Vec<Candle> = (0..40)
222 .map(|i| {
223 let base = 100.0 + i as f64;
224 if i % 7 == 0 {
225 c(base, base + 4.0, base - 0.1, base + 3.9, i)
226 } else {
227 c(base, base + 0.5, base - 0.5, base + 0.2, i)
228 }
229 })
230 .collect();
231 let mut a = LongLine::new();
232 let mut b = LongLine::new();
233 assert_eq!(
234 a.batch(&candles),
235 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
236 );
237 }
238
239 #[test]
240 fn reset_clears_state() {
241 let mut t = LongLine::new();
242 warm(&mut t);
243 t.update(c(10.0, 13.0, 9.9, 12.9, 5));
244 assert!(t.is_ready());
245 t.reset();
246 assert!(!t.is_ready());
247 assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, 0)), None);
248 }
249
250 #[test]
251 fn default_matches_new() {
252 assert_eq!(LongLine::default().period(), LongLine::new().period());
253 }
254}