Skip to main content

kestrel_chartkit/engine/
structural_stop.rs

1/// Live CRV recompute for structural trailing-stop management (plan Anhang G.2):
2/// `RR_remaining = (NextTarget - CurrentPrice) / (CurrentPrice - ProtectiveStop)`. When this
3/// shrinks towards zero, holding further becomes mathematically unattractive even without a
4/// fixed profit target being hit — a trade can become structurally "fertig".
5///
6/// Sign convention: pass `next_target`/`current_price`/`protective_stop` so that
7/// `current_price - protective_stop` is positive for a long and negative for a short (i.e.
8/// keep numerator and denominator consistent with the trade direction).
9pub 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/// Tracks a structural (non-ATR, non-break-even) trailing stop: the stop only advances when
18/// the market has printed new confirmed structure, and only ever tightens risk — never
19/// loosens it (plan Anhang G.2: "Stop wird nur nachgezogen, wenn der Markt neue Struktur
20/// erzeugt hat").
21#[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    /// Advance the stop to a newly confirmed structural level (e.g. a new accepted balance
36    /// boundary), ignored if it would loosen risk.
37    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        // Short: target below price, stop above price → both numerator and denominator
55        // negative, ratio stays positive.
56        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); // would loosen risk, must be ignored
76        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); // would loosen risk, must be ignored
85        assert_eq!(stop.current_stop, 95.0);
86    }
87}