wickra_core/indicators/
time_based_stop.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
42pub struct TimeBasedStop {
43 max_bars: usize,
44 bars_held: usize,
45 last: Option<f64>,
46}
47
48impl TimeBasedStop {
49 pub fn new(max_bars: usize) -> Result<Self> {
55 if max_bars == 0 {
56 return Err(Error::PeriodZero);
57 }
58 Ok(Self {
59 max_bars,
60 bars_held: 0,
61 last: None,
62 })
63 }
64
65 pub const fn max_bars(&self) -> usize {
67 self.max_bars
68 }
69
70 pub const fn bars_held(&self) -> usize {
72 self.bars_held
73 }
74
75 pub const fn triggered(&self) -> bool {
77 self.bars_held >= self.max_bars
78 }
79
80 pub const fn value(&self) -> Option<f64> {
82 self.last
83 }
84}
85
86impl Indicator for TimeBasedStop {
87 type Input = Candle;
88 type Output = f64;
89
90 fn update(&mut self, _candle: Candle) -> Option<f64> {
91 self.bars_held += 1;
92 let progress = (self.bars_held as f64 / self.max_bars as f64).min(1.0);
93 self.last = Some(progress);
94 Some(progress)
95 }
96
97 fn reset(&mut self) {
98 self.bars_held = 0;
99 self.last = None;
100 }
101
102 fn warmup_period(&self) -> usize {
103 1
104 }
105
106 fn is_ready(&self) -> bool {
107 self.last.is_some()
108 }
109
110 fn name(&self) -> &'static str {
111 "TimeBasedStop"
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::traits::BatchExt;
119 use approx::assert_relative_eq;
120
121 fn c() -> Candle {
122 Candle::new_unchecked(100.0, 101.0, 99.0, 100.0, 1.0, 0)
123 }
124
125 #[test]
126 fn rejects_zero_max_bars() {
127 assert!(matches!(TimeBasedStop::new(0), Err(Error::PeriodZero)));
128 }
129
130 #[test]
131 fn accessors_and_metadata() {
132 let t = TimeBasedStop::new(5).unwrap();
133 assert_eq!(t.max_bars(), 5);
134 assert_eq!(t.bars_held(), 0);
135 assert!(!t.triggered());
136 assert_eq!(t.warmup_period(), 1);
137 assert_eq!(t.name(), "TimeBasedStop");
138 assert!(!t.is_ready());
139 assert_eq!(t.value(), None);
140 }
141
142 #[test]
143 fn progress_climbs_to_one() {
144 let mut t = TimeBasedStop::new(4).unwrap();
145 let out = t.batch(&[c(), c(), c(), c()]);
146 assert_relative_eq!(out[0].unwrap(), 0.25, epsilon = 1e-12);
147 assert_relative_eq!(out[1].unwrap(), 0.50, epsilon = 1e-12);
148 assert_relative_eq!(out[2].unwrap(), 0.75, epsilon = 1e-12);
149 assert_relative_eq!(out[3].unwrap(), 1.00, epsilon = 1e-12);
150 }
151
152 #[test]
153 fn triggers_after_max_bars() {
154 let mut t = TimeBasedStop::new(3).unwrap();
155 t.update(c());
156 assert!(!t.triggered());
157 t.update(c());
158 assert!(!t.triggered());
159 t.update(c());
160 assert!(t.triggered());
161 }
162
163 #[test]
164 fn progress_saturates_at_one() {
165 let mut t = TimeBasedStop::new(2).unwrap();
167 let out = t.batch(&[c(), c(), c(), c()]);
168 assert_relative_eq!(out[2].unwrap(), 1.0, epsilon = 1e-12);
169 assert_relative_eq!(out[3].unwrap(), 1.0, epsilon = 1e-12);
170 }
171
172 #[test]
173 fn reset_restarts_timer() {
174 let mut t = TimeBasedStop::new(3).unwrap();
175 t.batch(&[c(), c(), c()]);
176 assert!(t.triggered());
177 t.reset();
178 assert!(!t.is_ready());
179 assert_eq!(t.bars_held(), 0);
180 assert!(!t.triggered());
181 assert_relative_eq!(t.update(c()).unwrap(), 1.0 / 3.0, epsilon = 1e-12);
182 }
183
184 #[test]
185 fn batch_equals_streaming() {
186 let candles = [c(); 10];
187 let batch = TimeBasedStop::new(4).unwrap().batch(&candles);
188 let mut b = TimeBasedStop::new(4).unwrap();
189 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
190 assert_eq!(batch, streamed);
191 }
192}