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