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