1use std::collections::HashMap;
18
19use crate::error::IndicatorError;
20use crate::indicator::{Indicator, IndicatorOutput};
21use crate::indicator_config::IndicatorConfig;
22use crate::registry::param_usize;
23use crate::signal::confluence::{ConfluenceEngine, ConfluenceParams};
24use crate::signal::cvd::{CVDTracker, CvdParams};
25use crate::signal::engine::Indicators;
26use crate::signal::liquidity::{LiquidityParams, LiquidityProfile};
27use crate::signal::structure::{MarketStructure, StructureParams};
28use crate::signal::vol_regime::VolatilityPercentile;
29use crate::types::Candle;
30
31#[derive(Debug, Clone)]
44pub struct SignalIndicator {
45 pub engine_cfg: IndicatorConfig,
46 pub conf_params: ConfluenceParams,
47 pub liq_params: LiquidityParams,
48 pub struct_params: StructureParams,
49 pub cvd_params: CvdParams,
50 pub signal_confirm_bars: usize,
51}
52
53impl SignalIndicator {
54 pub fn new(
55 engine_cfg: IndicatorConfig,
56 conf_params: ConfluenceParams,
57 liq_params: LiquidityParams,
58 struct_params: StructureParams,
59 cvd_params: CvdParams,
60 signal_confirm_bars: usize,
61 ) -> Self {
62 Self {
63 engine_cfg,
64 conf_params,
65 liq_params,
66 struct_params,
67 cvd_params,
68 signal_confirm_bars,
69 }
70 }
71 pub fn with_defaults() -> Self {
72 Self::new(
73 IndicatorConfig::default(),
74 ConfluenceParams::default(),
75 LiquidityParams::default(),
76 StructureParams::default(),
77 CvdParams::default(),
78 3,
79 )
80 }
81}
82
83impl Indicator for SignalIndicator {
84 fn name(&self) -> &'static str {
85 "Signal"
86 }
87 fn required_len(&self) -> usize {
88 self.engine_cfg
90 .training_period
91 .max(self.conf_params.trend_len + 1)
92 .max(self.liq_params.period)
93 .max(self.cvd_params.div_lookback + 1)
94 .max(self.struct_params.swing_len * 4 + 10)
95 }
96 fn required_columns(&self) -> &[&'static str] {
97 &["open", "high", "low", "close", "volume"]
98 }
99
100 fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
101 self.check_len(candles)?;
102 let cp = &self.conf_params;
103 let lp = &self.liq_params;
104 let sp = &self.struct_params;
105
106 let mut ind = Indicators::new(self.engine_cfg.clone());
107 let mut liq = LiquidityProfile::new(lp.period, lp.n_bins);
108 let mut conf = ConfluenceEngine::new(
109 cp.fast_len,
110 cp.slow_len,
111 cp.trend_len,
112 cp.rsi_len,
113 cp.adx_len,
114 );
115 let mut ms = MarketStructure::new(sp.swing_len, sp.atr_mult);
116 let mut cvd = CVDTracker::new(self.cvd_params.slope_bars, self.cvd_params.div_lookback);
117 let mut vol = VolatilityPercentile::new(100);
118 let cfg = IndicatorConfig::default();
119 let mut streak = SignalStreak::new(self.signal_confirm_bars);
120
121 let n = candles.len();
122 let mut signal_out = vec![f64::NAN; n];
123 let mut bull_out = vec![f64::NAN; n];
124 let mut bear_out = vec![f64::NAN; n];
125
126 for (i, c) in candles.iter().enumerate() {
127 ind.update(c);
128 liq.update(c);
129 conf.update(c);
130 ms.update(c);
131 cvd.update(c);
132 vol.update(ind.atr);
133
134 let (raw, comps) = compute_signal(
135 c.close,
136 &ind,
137 &liq,
138 &conf,
139 &ms,
140 &cfg,
141 Some(&cvd),
142 Some(&vol),
143 );
144 let confirmed = if streak.update(raw) { raw } else { 0 };
145 signal_out[i] = confirmed as f64;
146 bull_out[i] = comps.bull_score;
147 bear_out[i] = comps.bear_score;
148 }
149 Ok(IndicatorOutput::from_pairs([
150 ("signal", signal_out),
151 ("signal_bull_score", bull_out),
152 ("signal_bear_score", bear_out),
153 ]))
154 }
155}
156
157pub fn factory<S: ::std::hash::BuildHasher>(
160 params: &HashMap<String, String, S>,
161) -> Result<Box<dyn Indicator>, IndicatorError> {
162 let signal_confirm_bars = param_usize(params, "confirm_bars", 3)?;
163 Ok(Box::new(SignalIndicator::new(
164 IndicatorConfig::default(),
165 ConfluenceParams::default(),
166 LiquidityParams::default(),
167 StructureParams::default(),
168 CvdParams::default(),
169 signal_confirm_bars,
170 )))
171}
172
173#[derive(Debug, Clone)]
177pub struct SignalComponents {
178 pub v_vwap: i8,
180 pub v_ema: i8,
181 pub v_st: i8,
182 pub v_ts: i8,
183 pub v_liq: i8,
184 pub v_conf_bull: i8,
185 pub v_conf_bear: i8,
186 pub v_struct: i8,
187 pub v_cvd: i8,
188 pub v_ao: i8,
189 pub v_hurst: i8,
190 pub v_accel_bull: i8,
191 pub v_accel_bear: i8,
192 pub hurst: f64,
194 pub price_accel: f64,
195 pub bull_score: f64,
196 pub bear_score: f64,
197 pub conf_min_adj: f64,
198 pub liq_imbalance: f64,
199 pub liq_buy_pct: f64,
200 pub poc: Option<f64>,
201 pub struct_bias: i8,
202 pub fib618: Option<f64>,
203 pub fib_zone: &'static str,
204 pub fib_ok: bool,
205 pub bos: bool,
206 pub choch: bool,
207 pub ts_norm: f64,
208 pub dominance: f64,
209 pub cvd_slope: Option<f64>,
210 pub cvd_div: i8,
211 pub ao: f64,
212 pub ao_rising: bool,
213 pub wr_pct: f64,
214 pub mom_pct: f64,
215 pub wave_ok_long: bool,
216 pub wave_ok_short: bool,
217 pub mom_ok_long: bool,
218 pub mom_ok_short: bool,
219 pub vol_pct: Option<f64>,
220 pub vol_regime: Option<&'static str>,
221}
222
223pub fn compute_signal(
232 close: f64,
233 ind: &Indicators,
234 liq: &LiquidityProfile,
235 conf: &ConfluenceEngine,
236 ms: &MarketStructure,
237 cfg: &IndicatorConfig,
238 cvd: Option<&CVDTracker>,
239 vol: Option<&VolatilityPercentile>,
240) -> (i32, SignalComponents) {
241 if ind.vwap.is_none() || ind.ema.is_none() || ind.st.is_none() {
242 return (0, empty_components(ind, liq, conf, ms, cvd, vol));
243 }
244
245 let vwap = ind.vwap.unwrap();
246 let ema = ind.ema.unwrap();
247
248 let v1 = if close > vwap { 1_i8 } else { -1 }; let v2 = if close > ema { 1 } else { -1 }; let v3 = if ind.st_dir_pub == -1 { -1 } else { 1 }; let v4 = if ind.ts_bullish { 1 } else { -1 }; let v5 = if liq.bullish() { 1 } else { -1 }; let conf_adj = vol.map_or(1.0, |v| v.conf_adj);
256 let adj_min = cfg.conf_min_score * conf_adj;
257 let v6_bull = if conf.bull_score >= adj_min { 1_i8 } else { -1 };
258 let v6_bear = if conf.bear_score >= adj_min { 1_i8 } else { -1 };
259
260 let v7 = ms.bias; let v8: i8 = cvd.map_or(0, |c| {
263 if c.divergence != 0 {
264 c.divergence
265 } else if c.bullish {
266 1
267 } else {
268 -1
269 }
270 }); let v9: i8 = if ind.highs.len() >= 34 {
273 if ind.ao_rising { 1 } else { -1 }
274 } else {
275 0
276 }; let v10: i8 = if (ind.hurst - 0.5).abs() < 0.005 {
279 0
280 } else if ind.hurst >= cfg.hurst_threshold {
281 1
282 } else {
283 -1
284 }; let (v11_bull, v11_bear): (i8, i8) = if ind.price_accel.abs() < 0.005 {
287 (0, 0)
288 } else {
289 (
290 if ind.price_accel > 0.0 { 1 } else { -1 },
291 if ind.price_accel < 0.0 { 1 } else { -1 },
292 )
293 }; let fib_ok_long = !cfg.fib_zone_enabled || ms.in_discount || ms.fib500.is_none();
297 let fib_ok_short = !cfg.fib_zone_enabled || ms.in_premium || ms.fib500.is_none();
298
299 let (bull, bear) = match cfg.signal_mode.as_str() {
301 "strict" => {
302 let bull = v1 == 1
303 && v2 == 1
304 && v3 == -1
305 && v4 == 1
306 && v5 == 1
307 && v6_bull == 1
308 && v7 == 1
309 && fib_ok_long
310 && (v8 == 1 || v8 == 0);
311 let bear = v1 == -1
312 && v2 == -1
313 && v3 == 1
314 && v4 == -1
315 && v5 == -1
316 && v6_bear == 1
317 && v7 == -1
318 && fib_ok_short
319 && (v8 == -1 || v8 == 0);
320 (bull, bear)
321 }
322 "majority" => {
323 let core_bull = v1 == 1 && v2 == 1 && v3 == -1 && v4 == 1;
324 let core_bear = v1 == -1 && v2 == -1 && v3 == 1 && v4 == -1;
325
326 let ext_bull = [
327 v5 == 1,
328 v6_bull == 1,
329 v7 == 1,
330 fib_ok_long,
331 v8 == 1,
332 v9 == 1,
333 ind.wave_ok_long,
334 ind.mom_ok_long,
335 v10 == 1,
336 v11_bull == 1,
337 ]
338 .iter()
339 .filter(|&&b| b)
340 .count();
341
342 let ext_bear = [
343 v5 == -1,
344 v6_bear == 1,
345 v7 == -1,
346 fib_ok_short,
347 v8 == -1,
348 v9 == -1,
349 ind.wave_ok_short,
350 ind.mom_ok_short,
351 v10 == 1,
352 v11_bear == 1,
353 ]
354 .iter()
355 .filter(|&&b| b)
356 .count();
357
358 (core_bull && ext_bull >= 2, core_bear && ext_bear >= 2)
359 }
360 _ => {
361 let bull = v1 == 1 && v2 == 1 && v3 == -1 && v4 == 1;
363 let bear = v1 == -1 && v2 == -1 && v3 == 1 && v4 == -1;
364 (bull, bear)
365 }
366 };
367
368 let fib_zone = if ms.in_discount {
369 "discount"
370 } else if ms.in_premium {
371 "premium"
372 } else {
373 "mid"
374 };
375
376 let comps = SignalComponents {
377 v_vwap: v1,
378 v_ema: v2,
379 v_st: v3,
380 v_ts: v4,
381 v_liq: v5,
382 v_conf_bull: v6_bull,
383 v_conf_bear: v6_bear,
384 v_struct: v7,
385 v_cvd: v8,
386 v_ao: v9,
387 v_hurst: v10,
388 v_accel_bull: v11_bull,
389 v_accel_bear: v11_bear,
390 hurst: ind.hurst,
391 price_accel: ind.price_accel,
392 bull_score: conf.bull_score,
393 bear_score: conf.bear_score,
394 conf_min_adj: adj_min,
395 liq_imbalance: liq.imbalance,
396 liq_buy_pct: liq.buy_pct * 100.0,
397 poc: liq.poc_price,
398 struct_bias: ms.bias,
399 fib618: ms.fib618,
400 fib_zone,
401 fib_ok: if bull { fib_ok_long } else { fib_ok_short },
402 bos: ms.bos,
403 choch: ms.choch,
404 ts_norm: ind.ts_norm,
405 dominance: ind.dominance,
406 cvd_slope: cvd.map(|c| c.cvd_slope),
407 cvd_div: cvd.map_or(0, |c| c.divergence),
408 ao: ind.ao,
409 ao_rising: ind.ao_rising,
410 wr_pct: ind.wr_pct,
411 mom_pct: ind.mom_pct,
412 wave_ok_long: ind.wave_ok_long,
413 wave_ok_short: ind.wave_ok_short,
414 mom_ok_long: ind.mom_ok_long,
415 mom_ok_short: ind.mom_ok_short,
416 vol_pct: vol.map(|v| v.vol_pct),
417 vol_regime: vol.map(|v| v.vol_regime),
418 };
419
420 if bull {
421 return (1, comps);
422 }
423 if bear {
424 return (-1, comps);
425 }
426 (0, comps)
427}
428
429fn empty_components(
430 ind: &Indicators,
431 liq: &LiquidityProfile,
432 conf: &ConfluenceEngine,
433 ms: &MarketStructure,
434 cvd: Option<&CVDTracker>,
435 vol: Option<&VolatilityPercentile>,
436) -> SignalComponents {
437 SignalComponents {
438 v_vwap: 0,
439 v_ema: 0,
440 v_st: 0,
441 v_ts: 0,
442 v_liq: 0,
443 v_conf_bull: 0,
444 v_conf_bear: 0,
445 v_struct: 0,
446 v_cvd: 0,
447 v_ao: 0,
448 v_hurst: 0,
449 v_accel_bull: 0,
450 v_accel_bear: 0,
451 hurst: ind.hurst,
452 price_accel: ind.price_accel,
453 bull_score: conf.bull_score,
454 bear_score: conf.bear_score,
455 conf_min_adj: 0.0,
456 liq_imbalance: liq.imbalance,
457 liq_buy_pct: liq.buy_pct * 100.0,
458 poc: liq.poc_price,
459 struct_bias: ms.bias,
460 fib618: ms.fib618,
461 fib_zone: "mid",
462 fib_ok: false,
463 bos: false,
464 choch: false,
465 ts_norm: 0.5,
466 dominance: 0.0,
467 cvd_slope: cvd.map(|c| c.cvd_slope),
468 cvd_div: 0,
469 ao: ind.ao,
470 ao_rising: false,
471 wr_pct: 0.5,
472 mom_pct: 0.5,
473 wave_ok_long: false,
474 wave_ok_short: false,
475 mom_ok_long: false,
476 mom_ok_short: false,
477 vol_pct: vol.map(|v| v.vol_pct),
478 vol_regime: vol.map(|v| v.vol_regime),
479 }
480}
481
482pub struct SignalStreak {
489 required: usize,
490 direction: i32,
491 count: usize,
492}
493
494impl SignalStreak {
495 pub fn new(required: usize) -> Self {
496 Self {
497 required,
498 direction: 0,
499 count: 0,
500 }
501 }
502
503 pub fn update(&mut self, signal: i32) -> bool {
506 if signal != 0 && signal == self.direction {
507 self.count += 1;
508 } else {
509 self.direction = signal;
510 self.count = usize::from(signal != 0);
511 }
512 self.count >= self.required && signal != 0
513 }
514
515 pub fn reset(&mut self) {
516 self.direction = 0;
517 self.count = 0;
518 }
519 pub fn current_direction(&self) -> i32 {
520 self.direction
521 }
522 pub fn current_count(&self) -> usize {
523 self.count
524 }
525}
526
527#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
536 fn streak_fires_after_required_consecutive() {
537 let mut s = SignalStreak::new(3);
538 assert!(!s.update(1));
539 assert!(!s.update(1));
540 assert!(s.update(1)); assert!(s.update(1)); }
543
544 #[test]
545 fn streak_resets_on_direction_change() {
546 let mut s = SignalStreak::new(2);
547 assert!(!s.update(1));
548 assert!(s.update(1)); assert!(!s.update(-1)); assert!(s.update(-1)); }
552
553 #[test]
554 fn streak_zero_signal_breaks_streak() {
555 let mut s = SignalStreak::new(2);
556 s.update(1);
557 s.update(0); assert!(!s.update(1)); }
560
561 #[test]
562 fn streak_required_1_fires_immediately() {
563 let mut s = SignalStreak::new(1);
564 assert!(s.update(1));
565 assert!(s.update(-1));
566 assert!(!s.update(0));
567 }
568
569 #[test]
570 fn streak_tracks_direction_and_count() {
571 let mut s = SignalStreak::new(3);
572 s.update(1);
573 s.update(1);
574 assert_eq!(s.current_direction(), 1);
575 assert_eq!(s.current_count(), 2);
576 s.update(-1);
577 assert_eq!(s.current_direction(), -1);
578 assert_eq!(s.current_count(), 1);
579 }
580
581 #[test]
582 fn streak_reset_clears_state() {
583 let mut s = SignalStreak::new(2);
584 s.update(1);
585 s.update(1);
586 s.reset();
587 assert_eq!(s.current_count(), 0);
588 assert_eq!(s.current_direction(), 0);
589 assert!(!s.update(1)); }
591}