kestrel_chartkit/engine/
structural_stop.rs1pub fn remaining_rr(next_target: f64, current_price: f64, protective_stop: f64) -> f64 {
10 let denom = current_price - protective_stop;
11 if denom.abs() < f64::EPSILON {
12 return 0.0;
13 }
14 (next_target - current_price) / denom
15}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct StructuralTrailingStop {
23 pub current_stop: f64,
24 pub is_long: bool,
25}
26
27impl StructuralTrailingStop {
28 pub fn new(initial_stop: f64, is_long: bool) -> Self {
29 Self {
30 current_stop: initial_stop,
31 is_long,
32 }
33 }
34
35 pub fn advance(&mut self, new_structural_level: f64) {
38 if self.is_long {
39 if new_structural_level > self.current_stop {
40 self.current_stop = new_structural_level;
41 }
42 } else if new_structural_level < self.current_stop {
43 self.current_stop = new_structural_level;
44 }
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 #[test]
53 fn remaining_rr_for_a_short_setup() {
54 let rr = remaining_rr(29_180.0, 29_690.0, 29_746.0);
57 assert!((rr - 9.107142857142858).abs() < 1e-6);
58 }
59
60 #[test]
61 fn remaining_rr_shrinks_as_price_approaches_target() {
62 let far = remaining_rr(110.0, 100.0, 95.0);
63 let near = remaining_rr(110.0, 108.0, 95.0);
64 assert!(
65 near < far,
66 "RR_remaining should shrink as price nears the target"
67 );
68 }
69
70 #[test]
71 fn long_stop_only_advances_upward() {
72 let mut stop = StructuralTrailingStop::new(100.0, true);
73 stop.advance(105.0);
74 assert_eq!(stop.current_stop, 105.0);
75 stop.advance(102.0); assert_eq!(stop.current_stop, 105.0);
77 }
78
79 #[test]
80 fn short_stop_only_advances_downward() {
81 let mut stop = StructuralTrailingStop::new(100.0, false);
82 stop.advance(95.0);
83 assert_eq!(stop.current_stop, 95.0);
84 stop.advance(98.0); assert_eq!(stop.current_stop, 95.0);
86 }
87}