Skip to main content

kestrel_chartkit/engine/
balance_classifier.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4use crate::engine::market_context::AuctionPhase;
5use crate::model::Bar;
6
7/// Balance vs Imbalance classification (plan Anhang C).
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 MarketBalanceState {
15    Balance,
16    Imbalance,
17    Transition,
18}
19
20/// Balance / Imbalance output combining state classification with auction phase.
21#[derive(Debug, Clone, Copy, PartialEq)]
22#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23pub struct BalanceClassifierOutput {
24    pub state: MarketBalanceState,
25    pub auction_phase: AuctionPhase,
26    pub compression_ratio: f64,
27    pub balance_confidence: f64,
28}
29
30/// Classifies market state into Balance, Imbalance, or Transition based on price compression and pivot structure.
31pub fn classify_balance_imbalance(
32    bars: &[Bar],
33    atr_raw: f64,
34    pivot_score: f64,
35) -> BalanceClassifierOutput {
36    if bars.len() < 5 || atr_raw <= 0.0 {
37        return BalanceClassifierOutput {
38            state: MarketBalanceState::Transition,
39            auction_phase: AuctionPhase::InsideBalance,
40            compression_ratio: 1.0,
41            balance_confidence: 0.5,
42        };
43    }
44
45    let len = bars.len();
46    let recent_slice = &bars[len - 5..];
47    let range = recent_slice.iter().map(|b| b.high).fold(f64::MIN, f64::max)
48        - recent_slice.iter().map(|b| b.low).fold(f64::MAX, f64::min);
49
50    let compression_ratio = range / (atr_raw * 5.0).max(1e-8);
51
52    let (state, phase, confidence) = if compression_ratio < 0.60 && pivot_score.abs() < 0.40 {
53        (
54            MarketBalanceState::Balance,
55            AuctionPhase::InsideBalance,
56            (1.0 - compression_ratio).clamp(0.5, 1.0),
57        )
58    } else if compression_ratio > 1.20 || pivot_score.abs() >= 0.70 {
59        (
60            MarketBalanceState::Imbalance,
61            AuctionPhase::Expansion,
62            (compression_ratio / 2.0).clamp(0.6, 1.0),
63        )
64    } else {
65        (MarketBalanceState::Transition, AuctionPhase::Retest, 0.50)
66    };
67
68    BalanceClassifierOutput {
69        state,
70        auction_phase: phase,
71        compression_ratio,
72        balance_confidence: confidence,
73    }
74}