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