quantwave_core/indicators/
pa_confluence.rs1use crate::indicators::market_structure::{Bias, MarketStructureState, PAEvent, PAEventKind};
7use crate::regimes::MarketRegime;
8
9#[derive(Debug, Clone, Default)]
11pub struct ConfluenceContext {
12 pub regime_label: Option<u32>,
14 pub hurst_persistence: Option<f64>,
16 pub min_hurst: f64,
18 pub required_regime: Option<u32>,
20 pub required_bias: Option<u32>,
22}
23
24impl ConfluenceContext {
25 pub fn bull_flag_filter(regime_label: u32, hurst: f64) -> Self {
26 Self {
27 regime_label: Some(regime_label),
28 hurst_persistence: Some(hurst),
29 min_hurst: 0.5,
30 required_regime: Some(1),
31 required_bias: Some(1),
32 }
33 }
34}
35
36pub fn score_pa_event(event: &PAEvent, ctx: &ConfluenceContext) -> f64 {
38 let mut score = event.strength * event.confidence;
39
40 if let Some(h) = ctx.hurst_persistence {
41 if h >= ctx.min_hurst {
42 score *= 1.0 + (h - ctx.min_hurst).min(0.5);
43 } else {
44 score *= 0.5;
45 }
46 }
47
48 if let (Some(req), Some(cur)) = (ctx.required_regime, ctx.regime_label) {
49 if cur == req {
50 score *= 1.2;
51 } else {
52 score *= 0.3;
53 }
54 }
55
56 match &event.kind {
57 PAEventKind::GeometricFlag(f) if f.is_bull => score *= 1.1,
58 PAEventKind::GeometricFlag(f) if !f.is_bull => score *= 1.05,
59 PAEventKind::GeometricHs(h) if h.breakout_confirmed => score *= 1.15,
60 PAEventKind::SrInteraction(sr) => {
61 score *= 1.0 + (sr.strength / 10.0).min(0.2);
62 }
63 _ => {}
64 }
65
66 score
67}
68
69pub fn passes_confluence_filter(
71 event: &PAEvent,
72 ctx: &ConfluenceContext,
73 structure: Option<&MarketStructureState>,
74) -> bool {
75 if let Some(req_bias) = ctx.required_bias {
76 if let Some(st) = structure {
77 let bias = match st.bias {
78 Bias::Bullish => 1u32,
79 Bias::Bearish => 2,
80 Bias::Neutral => 0,
81 };
82 if bias != req_bias && req_bias != 0 {
83 return false;
84 }
85 }
86 }
87
88 if let Some(req) = ctx.required_regime {
89 if ctx.regime_label != Some(req) {
90 return false;
91 }
92 }
93
94 if let Some(h) = ctx.hurst_persistence {
95 if h < ctx.min_hurst {
96 return false;
97 }
98 }
99
100 match &event.kind {
101 PAEventKind::GeometricFlag(f) => f.breakout_confirmed,
102 PAEventKind::GeometricHs(h) => h.breakout_confirmed && h.score >= 60.0,
103 PAEventKind::MarketStructureFlip(_) => true,
104 PAEventKind::SrInteraction(_) => true,
105 }
106}
107
108pub fn enrich_pa_event(event: &mut PAEvent, ctx: &ConfluenceContext) {
110 if let Some(label) = ctx.regime_label {
111 event.regime_at_event = Some(match label {
112 1 => "Bull".into(),
113 2 => "Bear".into(),
114 3 => "Crisis".into(),
115 _ => "Steady".into(),
116 });
117 }
118 if let Some(h) = ctx.hurst_persistence {
119 event.feature_values.push(("hurst_persistence".into(), h));
120 }
121 event
122 .feature_values
123 .push(("confluence_score".into(), score_pa_event(event, ctx)));
124}
125
126pub fn filter_confluent_events(
128 events: Vec<PAEvent>,
129 ctx: &ConfluenceContext,
130 structure: Option<&MarketStructureState>,
131) -> Vec<PAEvent> {
132 events
133 .into_iter()
134 .filter(|e| passes_confluence_filter(e, ctx, structure))
135 .map(|mut e| {
136 enrich_pa_event(&mut e, ctx);
137 e
138 })
139 .collect()
140}
141
142pub fn regime_to_label(regime: MarketRegime) -> u32 {
144 match regime {
145 MarketRegime::Bull => 1,
146 MarketRegime::Bear => 2,
147 MarketRegime::Crisis => 3,
148 MarketRegime::Steady => 0,
149 MarketRegime::Cluster(c) => 4 + (c as u32),
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::indicators::geometric_patterns::FlagPattern;
157 use crate::indicators::market_structure::FlipEvent;
158
159 fn sample_flag() -> FlagPattern {
160 FlagPattern {
161 id: 1,
162 is_bull: true,
163 pole_start_bar: 10,
164 pole_end_bar: 12,
165 flag_start_bar: 13,
166 flag_end_bar: 20,
167 pole_length: 3.0,
168 pole_length_atr: 2.5,
169 max_retrace_pct: 40.0,
170 pullbacks: 2,
171 pushes: 1,
172 breakout_confirmed: true,
173 breakout_price: 105.0,
174 consolidation_bars: 7,
175 pole_strength: 2.5,
176 }
177 }
178
179 #[test]
180 fn test_bull_flag_passes_confluence() {
181 let event = PAEvent::from_flag(sample_flag(), 20);
182 let ctx = ConfluenceContext::bull_flag_filter(1, 0.6);
183 let st = MarketStructureState {
184 bias: Bias::Bullish,
185 last_swing_high: None,
186 last_swing_low: None,
187 current_flip: None,
188 swing_depth_used: 3,
189 bar_index: 20,
190 };
191 assert!(passes_confluence_filter(&event, &ctx, Some(&st)));
192 assert!(score_pa_event(&event, &ctx) > 0.5);
193 }
194
195 #[test]
196 fn test_wrong_regime_fails_filter() {
197 let event = PAEvent::from_flag(sample_flag(), 20);
198 let ctx = ConfluenceContext::bull_flag_filter(2, 0.6);
199 let st = MarketStructureState {
200 bias: Bias::Bullish,
201 last_swing_high: None,
202 last_swing_low: None,
203 current_flip: None,
204 swing_depth_used: 3,
205 bar_index: 20,
206 };
207 assert!(!passes_confluence_filter(&event, &ctx, Some(&st)));
208 }
209
210 #[test]
211 fn test_enrich_adds_metadata() {
212 let mut event = PAEvent::from_market_structure_flip(
213 FlipEvent {
214 is_bearish: false,
215 price: 100.0,
216 bar: 5,
217 structure_strength: 3,
218 },
219 5,
220 );
221 let ctx = ConfluenceContext {
222 regime_label: Some(1),
223 hurst_persistence: Some(0.65),
224 ..Default::default()
225 };
226 enrich_pa_event(&mut event, &ctx);
227 assert_eq!(event.regime_at_event, Some("Bull".into()));
228 assert!(event.feature_values.iter().any(|(k, _)| k == "confluence_score"));
229 }
230}