kestrel_chartkit/engine/
market_context.rs1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4use crate::model::MarketRegime;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[cfg_attr(
9 feature = "serde",
10 derive(Serialize, Deserialize),
11 serde(rename_all = "snake_case")
12)]
13pub enum VolumeNodeKind {
14 Hvn,
15 Lvn,
16 Neutral,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(
21 feature = "serde",
22 derive(Serialize, Deserialize),
23 serde(rename_all = "snake_case")
24)]
25pub enum AcceptanceLevel {
26 High,
27 Low,
28 None,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[cfg_attr(
35 feature = "serde",
36 derive(Serialize, Deserialize),
37 serde(rename_all = "snake_case")
38)]
39pub enum AuctionPhase {
40 InsideBalance,
41 Breakout,
42 Acceptance { duration_bars: u32 },
43 Retest,
44 Expansion,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq)]
52#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
53pub struct MarketContextOutput {
54 pub regime: MarketRegime,
55 pub distance_to_vpoc_atr: f64,
56 pub current_volume_node: VolumeNodeKind,
57 pub previous_acceptance: AcceptanceLevel,
58 pub auction_phase: AuctionPhase,
59}
60
61pub fn classify_volume_node(
64 density: f64,
65 hvn_threshold: f64,
66 lvn_threshold: f64,
67) -> VolumeNodeKind {
68 if density >= hvn_threshold {
69 VolumeNodeKind::Hvn
70 } else if density <= lvn_threshold {
71 VolumeNodeKind::Lvn
72 } else {
73 VolumeNodeKind::Neutral
74 }
75}
76
77#[allow(clippy::too_many_arguments)]
78pub fn build_market_context(
79 regime: MarketRegime,
80 price: f64,
81 vpoc: f64,
82 atr: f64,
83 node: VolumeNodeKind,
84 previous_acceptance: AcceptanceLevel,
85 auction_phase: AuctionPhase,
86) -> MarketContextOutput {
87 let distance_to_vpoc_atr = if atr > 0.0 {
88 (price - vpoc).abs() / atr
89 } else {
90 0.0
91 };
92 MarketContextOutput {
93 regime,
94 distance_to_vpoc_atr,
95 current_volume_node: node,
96 previous_acceptance,
97 auction_phase,
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn volume_node_classification_thresholds() {
107 assert_eq!(classify_volume_node(0.9, 0.7, 0.2), VolumeNodeKind::Hvn);
108 assert_eq!(classify_volume_node(0.1, 0.7, 0.2), VolumeNodeKind::Lvn);
109 assert_eq!(classify_volume_node(0.5, 0.7, 0.2), VolumeNodeKind::Neutral);
110 }
111
112 #[test]
113 fn distance_to_vpoc_is_atr_normalized() {
114 let ctx = build_market_context(
115 MarketRegime::BullishExpansion,
116 110.0,
117 100.0,
118 5.0,
119 VolumeNodeKind::Neutral,
120 AcceptanceLevel::None,
121 AuctionPhase::Expansion,
122 );
123 assert!((ctx.distance_to_vpoc_atr - 2.0).abs() < 1e-9);
124 }
125}