Skip to main content

kestrel_chartkit/engine/
balance_migration.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[cfg_attr(
6    feature = "serde",
7    derive(Serialize, Deserialize),
8    serde(rename_all = "snake_case")
9)]
10pub enum MigrationDirection {
11    Bullish,
12    Bearish,
13    Neutral,
14}
15
16/// Balance Migration Engine output (plan Anhang G.1): tracks
17/// `Balance_1 → Expansion → Balance_2` and whether the new balance is accepted higher or
18/// lower than the previous one. Generalizes VPOC-migration (`VPOC_t − VPOC_{t-1}`) to any
19/// balance-midpoint source.
20#[derive(Debug, Clone, Copy, PartialEq)]
21#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
22pub struct BalanceMigrationOutput {
23    pub previous_balance_mid: f64,
24    pub new_balance_mid: f64,
25    /// `new_balance_mid - previous_balance_mid`, signed.
26    pub migration: f64,
27    pub direction: MigrationDirection,
28    /// 0.0..1.0
29    pub acceptance_strength: f64,
30}
31
32/// `acceptance_bars` / `min_acceptance_bars` → 0.0..1.0 confirmation strength of the new
33/// balance (plan Anhang G.1: "Acceptance" as a duration, not a single touch).
34pub fn build_balance_migration(
35    previous_balance_mid: f64,
36    new_balance_mid: f64,
37    acceptance_bars: u32,
38    min_acceptance_bars: u32,
39) -> BalanceMigrationOutput {
40    let migration = new_balance_mid - previous_balance_mid;
41    let direction = if migration > 0.0 {
42        MigrationDirection::Bullish
43    } else if migration < 0.0 {
44        MigrationDirection::Bearish
45    } else {
46        MigrationDirection::Neutral
47    };
48    let acceptance_strength = if min_acceptance_bars > 0 {
49        (acceptance_bars as f64 / min_acceptance_bars as f64).clamp(0.0, 1.0)
50    } else {
51        1.0
52    };
53    BalanceMigrationOutput {
54        previous_balance_mid,
55        new_balance_mid,
56        migration,
57        direction,
58        acceptance_strength,
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn higher_new_balance_is_bullish_migration() {
68        let out = build_balance_migration(25_480.0, 25_417.0, 3, 4);
69        assert_eq!(out.direction, MigrationDirection::Bearish);
70        assert!((out.migration - (-63.0)).abs() < 1e-6);
71        assert!((out.acceptance_strength - 0.75).abs() < 1e-9);
72    }
73
74    #[test]
75    fn acceptance_strength_is_clamped_to_one() {
76        let out = build_balance_migration(100.0, 105.0, 10, 4);
77        assert!((out.acceptance_strength - 1.0).abs() < 1e-9);
78    }
79}