kestrel_chartkit/engine/
balance_migration.rs1#[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#[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 pub migration: f64,
27 pub direction: MigrationDirection,
28 pub acceptance_strength: f64,
30}
31
32pub 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}