wickra_core/indicators/
step_trailing_stop.rs1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone)]
38pub struct StepTrailingStop {
39 step_size: f64,
40 prev_stop: Option<f64>,
41 long: bool,
42}
43
44impl StepTrailingStop {
45 pub fn new(step_size: f64) -> Result<Self> {
51 if !step_size.is_finite() || step_size <= 0.0 {
52 return Err(Error::NonPositiveMultiplier);
53 }
54 Ok(Self {
55 step_size,
56 prev_stop: None,
57 long: true,
58 })
59 }
60
61 pub fn classic() -> Self {
63 Self::new(1.0).expect("classic step is valid")
64 }
65
66 pub const fn step_size(&self) -> f64 {
68 self.step_size
69 }
70
71 fn snap_long(&self, close: f64) -> f64 {
73 ((close - self.step_size) / self.step_size).floor() * self.step_size
74 }
75
76 fn snap_short(&self, close: f64) -> f64 {
78 ((close + self.step_size) / self.step_size).ceil() * self.step_size
79 }
80}
81
82impl Indicator for StepTrailingStop {
83 type Input = f64;
84 type Output = f64;
85
86 #[inline]
87 fn update(&mut self, close: f64) -> Option<f64> {
88 if !close.is_finite() {
89 return None;
90 }
91 let stop = match self.prev_stop {
92 Some(prev) => {
93 if self.long {
94 if close < prev {
95 self.long = false;
96 self.snap_short(close)
97 } else {
98 prev.max(self.snap_long(close))
99 }
100 } else if close > prev {
101 self.long = true;
102 self.snap_long(close)
103 } else {
104 prev.min(self.snap_short(close))
105 }
106 }
107 None => self.snap_long(close),
108 };
109 self.prev_stop = Some(stop);
110 Some(stop)
111 }
112
113 fn reset(&mut self) {
114 self.prev_stop = None;
115 self.long = true;
116 }
117
118 #[inline]
119 fn warmup_period(&self) -> usize {
120 1
121 }
122
123 #[inline]
124 fn is_ready(&self) -> bool {
125 self.prev_stop.is_some()
126 }
127
128 #[inline]
129 fn name(&self) -> &'static str {
130 "StepTrailingStop"
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::traits::BatchExt;
138 use approx::assert_relative_eq;
139
140 #[test]
141 fn rejects_invalid_step() {
142 assert!(StepTrailingStop::new(0.0).is_err());
143 assert!(StepTrailingStop::new(-1.0).is_err());
144 assert!(StepTrailingStop::new(f64::NAN).is_err());
145 }
146
147 #[test]
148 fn accessors_and_metadata() {
149 let s = StepTrailingStop::classic();
150 assert_relative_eq!(s.step_size(), 1.0, epsilon = 1e-12);
151 assert_eq!(s.name(), "StepTrailingStop");
152 assert_eq!(s.warmup_period(), 1);
153 }
154
155 #[test]
156 fn first_value_snaps_below_price() {
157 let mut s = StepTrailingStop::new(1.0).unwrap();
158 assert_relative_eq!(s.update(100.4).unwrap(), 99.0, epsilon = 1e-12);
160 }
161
162 #[test]
163 fn long_stop_ratchets_in_discrete_steps() {
164 let mut s = StepTrailingStop::new(1.0).unwrap();
165 let out: Vec<f64> = [100.0, 100.5, 101.0, 102.0, 103.5]
166 .iter()
167 .map(|&p| s.update(p).unwrap())
168 .collect();
169 assert_relative_eq!(out[0], 99.0, epsilon = 1e-9);
171 assert_relative_eq!(out[1], 99.0, epsilon = 1e-9);
172 assert_relative_eq!(out[2], 100.0, epsilon = 1e-9);
173 assert_relative_eq!(out[3], 101.0, epsilon = 1e-9);
174 assert_relative_eq!(out[4], 102.0, epsilon = 1e-9);
175 }
176
177 #[test]
178 fn flips_to_short_on_close_through_and_back() {
179 let mut s = StepTrailingStop::new(1.0).unwrap();
180 s.update(100.0); s.update(105.0); let flipped = s.update(50.0).unwrap();
183 assert_relative_eq!(flipped, 51.0, epsilon = 1e-9);
185 let back = s.update(100.0).unwrap();
187 assert_relative_eq!(back, 99.0, epsilon = 1e-9);
188 }
189
190 #[test]
191 fn short_stop_ratchets_down() {
192 let mut s = StepTrailingStop::new(1.0).unwrap();
193 s.update(100.0);
194 s.update(50.0); let v = s.update(40.0).unwrap();
196 assert_relative_eq!(v, 41.0, epsilon = 1e-9);
198 }
199
200 #[test]
201 fn constant_series_holds_stop() {
202 let mut s = StepTrailingStop::new(1.0).unwrap();
203 let out = s.batch(&[100.0; 30]);
204 for v in out.into_iter().flatten() {
205 assert_relative_eq!(v, 99.0, epsilon = 1e-12);
206 }
207 }
208
209 #[test]
210 fn reset_clears_state() {
211 let mut s = StepTrailingStop::new(1.0).unwrap();
212 s.update(100.0);
213 s.update(50.0);
214 assert!(s.is_ready());
215 s.reset();
216 assert!(!s.is_ready());
217 assert_relative_eq!(s.update(200.0).unwrap(), 199.0, epsilon = 1e-12);
218 }
219
220 #[test]
221 fn batch_equals_streaming() {
222 let prices: Vec<f64> = (0..80)
223 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 8.0)
224 .collect();
225 let mut a = StepTrailingStop::classic();
226 let mut b = StepTrailingStop::classic();
227 assert_eq!(
228 a.batch(&prices),
229 prices.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
230 );
231 }
232}