Skip to main content

kestrel_chartkit/engine/
acceptance_detector.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4use crate::engine::market_context::AcceptanceLevel;
5use crate::model::Bar;
6
7/// Rejection kind at key structural level.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[cfg_attr(
10    feature = "serde",
11    derive(Serialize, Deserialize),
12    serde(rename_all = "snake_case")
13)]
14pub enum RejectionKind {
15    RejectionHigh,
16    RejectionLow,
17    None,
18}
19
20/// Result of acceptance vs rejection evaluation over a lookback window.
21#[derive(Debug, Clone, Copy, PartialEq)]
22#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23pub struct AcceptanceDetectorOutput {
24    pub acceptance_level: AcceptanceLevel,
25    pub rejection_kind: RejectionKind,
26    pub consecutive_acceptance_bars: u32,
27    pub level: f64,
28}
29
30/// Evaluates whether closing price stays over N bars above/below a key level or gets rejected.
31pub fn detect_acceptance_rejection(
32    bars: &[Bar],
33    level: f64,
34    min_acceptance_bars: usize,
35) -> AcceptanceDetectorOutput {
36    if bars.is_empty() || level <= 0.0 {
37        return AcceptanceDetectorOutput {
38            acceptance_level: AcceptanceLevel::None,
39            rejection_kind: RejectionKind::None,
40            consecutive_acceptance_bars: 0,
41            level,
42        };
43    }
44
45    let mut consecutive_above = 0u32;
46    let mut consecutive_below = 0u32;
47
48    for b in bars.iter().rev() {
49        if b.close > level {
50            if consecutive_below > 0 {
51                break;
52            }
53            consecutive_above += 1;
54        } else if b.close < level {
55            if consecutive_above > 0 {
56                break;
57            }
58            consecutive_below += 1;
59        } else {
60            break;
61        }
62    }
63
64    let min_b = min_acceptance_bars as u32;
65    let (acc_level, rejection, count) = if consecutive_above >= min_b {
66        (
67            AcceptanceLevel::High,
68            RejectionKind::None,
69            consecutive_above,
70        )
71    } else if consecutive_below >= min_b {
72        (AcceptanceLevel::Low, RejectionKind::None, consecutive_below)
73    } else {
74        let last_bar = bars.last().unwrap();
75        let rejection = if last_bar.high > level && last_bar.close < level {
76            RejectionKind::RejectionHigh
77        } else if last_bar.low < level && last_bar.close > level {
78            RejectionKind::RejectionLow
79        } else {
80            RejectionKind::None
81        };
82        (AcceptanceLevel::None, rejection, 0)
83    };
84
85    AcceptanceDetectorOutput {
86        acceptance_level: acc_level,
87        rejection_kind: rejection,
88        consecutive_acceptance_bars: count,
89        level,
90    }
91}