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>(params: &HashMap<String, String, S>) -> Result<Box<dyn Indicator>, IndicatorError> {
160 let signal_confirm_bars = param_usize(params, "confirm_bars", 3)?;
161 Ok(Box::new(SignalIndicator::new(
162 IndicatorConfig::default(),
163 ConfluenceParams::default(),
164 LiquidityParams::default(),
165 StructureParams::default(),
166 CvdParams::default(),
167 signal_confirm_bars,
168 )))
169}
170
171#[derive(Debug, Clone)]
175pub struct SignalComponents {
176 pub v_vwap: i8,
178 pub v_ema: i8,
179 pub v_st: i8,
180 pub v_ts: i8,
181 pub v_liq: i8,
182 pub v_conf_bull: i8,
183 pub v_conf_bear: i8,
184 pub v_struct: i8,
185 pub v_cvd: i8,
186 pub v_ao: i8,
187 pub v_hurst: i8,
188 pub v_accel_bull: i8,
189 pub v_accel_bear: i8,
190 pub hurst: f64,
192 pub price_accel: f64,
193 pub bull_score: f64,
194 pub bear_score: f64,
195 pub conf_min_adj: f64,
196 pub liq_imbalance: f64,
197 pub liq_buy_pct: f64,
198 pub poc: Option<f64>,
199 pub struct_bias: i8,
200 pub fib618: Option<f64>,
201 pub fib_zone: &'static str,
202 pub fib_ok: bool,
203 pub bos: bool,
204 pub choch: bool,
205 pub ts_norm: f64,
206 pub dominance: f64,
207 pub cvd_slope: Option<f64>,
208 pub cvd_div: i8,
209 pub ao: f64,
210 pub ao_rising: bool,
211 pub wr_pct: f64,
212 pub mom_pct: f64,
213 pub wave_ok_long: bool,
214 pub wave_ok_short: bool,
215 pub mom_ok_long: bool,
216 pub mom_ok_short: bool,
217 pub vol_pct: Option<f64>,
218 pub vol_regime: Option<&'static str>,
219}
220
221pub fn compute_signal(
230 close: f64,
231 ind: &Indicators,
232 liq: &LiquidityProfile,
233 conf: &ConfluenceEngine,
234 ms: &MarketStructure,
235 cfg: &IndicatorConfig,
236 cvd: Option<&CVDTracker>,
237 vol: Option<&VolatilityPercentile>,
238) -> (i32, SignalComponents) {
239 if ind.vwap.is_none() || ind.ema.is_none() || ind.st.is_none() {
240 return (0, empty_components(ind, liq, conf, ms, cvd, vol));
241 }
242
243 let vwap = ind.vwap.unwrap();
244 let ema = ind.ema.unwrap();
245
246 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);
254 let adj_min = cfg.conf_min_score * conf_adj;
255 let v6_bull = if conf.bull_score >= adj_min { 1_i8 } else { -1 };
256 let v6_bear = if conf.bear_score >= adj_min { 1_i8 } else { -1 };
257
258 let v7 = ms.bias; let v8: i8 = cvd.map_or(0, |c| {
261 if c.divergence != 0 {
262 c.divergence
263 } else if c.bullish {
264 1
265 } else {
266 -1
267 }
268 }); let v9: i8 = if ind.highs.len() >= 34 {
271 if ind.ao_rising { 1 } else { -1 }
272 } else {
273 0
274 }; let v10: i8 = if (ind.hurst - 0.5).abs() < 0.005 {
277 0
278 } else if ind.hurst >= cfg.hurst_threshold {
279 1
280 } else {
281 -1
282 }; let (v11_bull, v11_bear): (i8, i8) = if ind.price_accel.abs() < 0.005 {
285 (0, 0)
286 } else {
287 (
288 if ind.price_accel > 0.0 { 1 } else { -1 },
289 if ind.price_accel < 0.0 { 1 } else { -1 },
290 )
291 }; let fib_ok_long = !cfg.fib_zone_enabled || ms.in_discount || ms.fib500.is_none();
295 let fib_ok_short = !cfg.fib_zone_enabled || ms.in_premium || ms.fib500.is_none();
296
297 let (bull, bear) = match cfg.signal_mode.as_str() {
299 "strict" => {
300 let bull = v1 == 1
301 && v2 == 1
302 && v3 == -1
303 && v4 == 1
304 && v5 == 1
305 && v6_bull == 1
306 && v7 == 1
307 && fib_ok_long
308 && (v8 == 1 || v8 == 0);
309 let bear = v1 == -1
310 && v2 == -1
311 && v3 == 1
312 && v4 == -1
313 && v5 == -1
314 && v6_bear == 1
315 && v7 == -1
316 && fib_ok_short
317 && (v8 == -1 || v8 == 0);
318 (bull, bear)
319 }
320 "majority" => {
321 let core_bull = v1 == 1 && v2 == 1 && v3 == -1 && v4 == 1;
322 let core_bear = v1 == -1 && v2 == -1 && v3 == 1 && v4 == -1;
323
324 let ext_bull = [
325 v5 == 1,
326 v6_bull == 1,
327 v7 == 1,
328 fib_ok_long,
329 v8 == 1,
330 v9 == 1,
331 ind.wave_ok_long,
332 ind.mom_ok_long,
333 v10 == 1,
334 v11_bull == 1,
335 ]
336 .iter()
337 .filter(|&&b| b)
338 .count();
339
340 let ext_bear = [
341 v5 == -1,
342 v6_bear == 1,
343 v7 == -1,
344 fib_ok_short,
345 v8 == -1,
346 v9 == -1,
347 ind.wave_ok_short,
348 ind.mom_ok_short,
349 v10 == 1,
350 v11_bear == 1,
351 ]
352 .iter()
353 .filter(|&&b| b)
354 .count();
355
356 (core_bull && ext_bull >= 2, core_bear && ext_bear >= 2)
357 }
358 _ => {
359 let bull = v1 == 1 && v2 == 1 && v3 == -1 && v4 == 1;
361 let bear = v1 == -1 && v2 == -1 && v3 == 1 && v4 == -1;
362 (bull, bear)
363 }
364 };
365
366 let fib_zone = if ms.in_discount {
367 "discount"
368 } else if ms.in_premium {
369 "premium"
370 } else {
371 "mid"
372 };
373
374 let comps = SignalComponents {
375 v_vwap: v1,
376 v_ema: v2,
377 v_st: v3,
378 v_ts: v4,
379 v_liq: v5,
380 v_conf_bull: v6_bull,
381 v_conf_bear: v6_bear,
382 v_struct: v7,
383 v_cvd: v8,
384 v_ao: v9,
385 v_hurst: v10,
386 v_accel_bull: v11_bull,
387 v_accel_bear: v11_bear,
388 hurst: ind.hurst,
389 price_accel: ind.price_accel,
390 bull_score: conf.bull_score,
391 bear_score: conf.bear_score,
392 conf_min_adj: adj_min,
393 liq_imbalance: liq.imbalance,
394 liq_buy_pct: liq.buy_pct * 100.0,
395 poc: liq.poc_price,
396 struct_bias: ms.bias,
397 fib618: ms.fib618,
398 fib_zone,
399 fib_ok: if bull { fib_ok_long } else { fib_ok_short },
400 bos: ms.bos,
401 choch: ms.choch,
402 ts_norm: ind.ts_norm,
403 dominance: ind.dominance,
404 cvd_slope: cvd.map(|c| c.cvd_slope),
405 cvd_div: cvd.map_or(0, |c| c.divergence),
406 ao: ind.ao,
407 ao_rising: ind.ao_rising,
408 wr_pct: ind.wr_pct,
409 mom_pct: ind.mom_pct,
410 wave_ok_long: ind.wave_ok_long,
411 wave_ok_short: ind.wave_ok_short,
412 mom_ok_long: ind.mom_ok_long,
413 mom_ok_short: ind.mom_ok_short,
414 vol_pct: vol.map(|v| v.vol_pct),
415 vol_regime: vol.map(|v| v.vol_regime),
416 };
417
418 if bull {
419 return (1, comps);
420 }
421 if bear {
422 return (-1, comps);
423 }
424 (0, comps)
425}
426
427fn empty_components(
428 ind: &Indicators,
429 liq: &LiquidityProfile,
430 conf: &ConfluenceEngine,
431 ms: &MarketStructure,
432 cvd: Option<&CVDTracker>,
433 vol: Option<&VolatilityPercentile>,
434) -> SignalComponents {
435 SignalComponents {
436 v_vwap: 0,
437 v_ema: 0,
438 v_st: 0,
439 v_ts: 0,
440 v_liq: 0,
441 v_conf_bull: 0,
442 v_conf_bear: 0,
443 v_struct: 0,
444 v_cvd: 0,
445 v_ao: 0,
446 v_hurst: 0,
447 v_accel_bull: 0,
448 v_accel_bear: 0,
449 hurst: ind.hurst,
450 price_accel: ind.price_accel,
451 bull_score: conf.bull_score,
452 bear_score: conf.bear_score,
453 conf_min_adj: 0.0,
454 liq_imbalance: liq.imbalance,
455 liq_buy_pct: liq.buy_pct * 100.0,
456 poc: liq.poc_price,
457 struct_bias: ms.bias,
458 fib618: ms.fib618,
459 fib_zone: "mid",
460 fib_ok: false,
461 bos: false,
462 choch: false,
463 ts_norm: 0.5,
464 dominance: 0.0,
465 cvd_slope: cvd.map(|c| c.cvd_slope),
466 cvd_div: 0,
467 ao: ind.ao,
468 ao_rising: false,
469 wr_pct: 0.5,
470 mom_pct: 0.5,
471 wave_ok_long: false,
472 wave_ok_short: false,
473 mom_ok_long: false,
474 mom_ok_short: false,
475 vol_pct: vol.map(|v| v.vol_pct),
476 vol_regime: vol.map(|v| v.vol_regime),
477 }
478}
479
480pub struct SignalStreak {
487 required: usize,
488 direction: i32,
489 count: usize,
490}
491
492impl SignalStreak {
493 pub fn new(required: usize) -> Self {
494 Self {
495 required,
496 direction: 0,
497 count: 0,
498 }
499 }
500
501 pub fn update(&mut self, signal: i32) -> bool {
504 if signal != 0 && signal == self.direction {
505 self.count += 1;
506 } else {
507 self.direction = signal;
508 self.count = usize::from(signal != 0);
509 }
510 self.count >= self.required && signal != 0
511 }
512
513 pub fn reset(&mut self) {
514 self.direction = 0;
515 self.count = 0;
516 }
517 pub fn current_direction(&self) -> i32 {
518 self.direction
519 }
520 pub fn current_count(&self) -> usize {
521 self.count
522 }
523}
524
525#[cfg(test)]
528mod tests {
529 use super::*;
530
531 #[test]
534 fn streak_fires_after_required_consecutive() {
535 let mut s = SignalStreak::new(3);
536 assert!(!s.update(1));
537 assert!(!s.update(1));
538 assert!(s.update(1)); assert!(s.update(1)); }
541
542 #[test]
543 fn streak_resets_on_direction_change() {
544 let mut s = SignalStreak::new(2);
545 assert!(!s.update(1));
546 assert!(s.update(1)); assert!(!s.update(-1)); assert!(s.update(-1)); }
550
551 #[test]
552 fn streak_zero_signal_breaks_streak() {
553 let mut s = SignalStreak::new(2);
554 s.update(1);
555 s.update(0); assert!(!s.update(1)); }
558
559 #[test]
560 fn streak_required_1_fires_immediately() {
561 let mut s = SignalStreak::new(1);
562 assert!(s.update(1));
563 assert!(s.update(-1));
564 assert!(!s.update(0));
565 }
566
567 #[test]
568 fn streak_tracks_direction_and_count() {
569 let mut s = SignalStreak::new(3);
570 s.update(1);
571 s.update(1);
572 assert_eq!(s.current_direction(), 1);
573 assert_eq!(s.current_count(), 2);
574 s.update(-1);
575 assert_eq!(s.current_direction(), -1);
576 assert_eq!(s.current_count(), 1);
577 }
578
579 #[test]
580 fn streak_reset_clears_state() {
581 let mut s = SignalStreak::new(2);
582 s.update(1);
583 s.update(1);
584 s.reset();
585 assert_eq!(s.current_count(), 0);
586 assert_eq!(s.current_direction(), 0);
587 assert!(!s.update(1)); }
589}