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 let (Some(vwap), Some(ema), true) = (ind.vwap, ind.ema, ind.st.is_some()) else {
242 return (0, empty_components(ind, liq, conf, ms, cvd, vol));
243 };
244
245 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);
253 let adj_min = cfg.conf_min_score * conf_adj;
254 let v6_bull = if conf.bull_score >= adj_min { 1_i8 } else { -1 };
255 let v6_bear = if conf.bear_score >= adj_min { 1_i8 } else { -1 };
256
257 let v7 = ms.bias; let v8: i8 = cvd.map_or(0, |c| {
260 if c.divergence != 0 {
261 c.divergence
262 } else if c.bullish {
263 1
264 } else {
265 -1
266 }
267 }); let v9: i8 = if ind.highs.len() >= 34 {
270 if ind.ao_rising { 1 } else { -1 }
271 } else {
272 0
273 }; let v10: i8 = if (ind.hurst - 0.5).abs() < 0.005 {
276 0
277 } else if ind.hurst >= cfg.hurst_threshold {
278 1
279 } else {
280 -1
281 }; let (v11_bull, v11_bear): (i8, i8) = if ind.price_accel.abs() < 0.005 {
284 (0, 0)
285 } else {
286 (
287 if ind.price_accel > 0.0 { 1 } else { -1 },
288 if ind.price_accel < 0.0 { 1 } else { -1 },
289 )
290 }; let fib_ok_long = !cfg.fib_zone_enabled || ms.in_discount || ms.fib500.is_none();
294 let fib_ok_short = !cfg.fib_zone_enabled || ms.in_premium || ms.fib500.is_none();
295
296 let (bull, bear) = match cfg.signal_mode.as_str() {
298 "strict" => {
299 let bull = v1 == 1
300 && v2 == 1
301 && v3 == -1
302 && v4 == 1
303 && v5 == 1
304 && v6_bull == 1
305 && v7 == 1
306 && fib_ok_long
307 && (v8 == 1 || v8 == 0);
308 let bear = v1 == -1
309 && v2 == -1
310 && v3 == 1
311 && v4 == -1
312 && v5 == -1
313 && v6_bear == 1
314 && v7 == -1
315 && fib_ok_short
316 && (v8 == -1 || v8 == 0);
317 (bull, bear)
318 }
319 "majority" => {
320 let core_bull = v1 == 1 && v2 == 1 && v3 == -1 && v4 == 1;
321 let core_bear = v1 == -1 && v2 == -1 && v3 == 1 && v4 == -1;
322
323 let ext_bull = [
324 v5 == 1,
325 v6_bull == 1,
326 v7 == 1,
327 fib_ok_long,
328 v8 == 1,
329 v9 == 1,
330 ind.wave_ok_long,
331 ind.mom_ok_long,
332 v10 == 1,
333 v11_bull == 1,
334 ]
335 .iter()
336 .filter(|&&b| b)
337 .count();
338
339 let ext_bear = [
340 v5 == -1,
341 v6_bear == 1,
342 v7 == -1,
343 fib_ok_short,
344 v8 == -1,
345 v9 == -1,
346 ind.wave_ok_short,
347 ind.mom_ok_short,
348 v10 == 1,
349 v11_bear == 1,
350 ]
351 .iter()
352 .filter(|&&b| b)
353 .count();
354
355 (core_bull && ext_bull >= 2, core_bear && ext_bear >= 2)
356 }
357 _ => {
358 let bull = v1 == 1 && v2 == 1 && v3 == -1 && v4 == 1;
360 let bear = v1 == -1 && v2 == -1 && v3 == 1 && v4 == -1;
361 (bull, bear)
362 }
363 };
364
365 let fib_zone = if ms.in_discount {
366 "discount"
367 } else if ms.in_premium {
368 "premium"
369 } else {
370 "mid"
371 };
372
373 let comps = SignalComponents {
374 v_vwap: v1,
375 v_ema: v2,
376 v_st: v3,
377 v_ts: v4,
378 v_liq: v5,
379 v_conf_bull: v6_bull,
380 v_conf_bear: v6_bear,
381 v_struct: v7,
382 v_cvd: v8,
383 v_ao: v9,
384 v_hurst: v10,
385 v_accel_bull: v11_bull,
386 v_accel_bear: v11_bear,
387 hurst: ind.hurst,
388 price_accel: ind.price_accel,
389 bull_score: conf.bull_score,
390 bear_score: conf.bear_score,
391 conf_min_adj: adj_min,
392 liq_imbalance: liq.imbalance,
393 liq_buy_pct: liq.buy_pct * 100.0,
394 poc: liq.poc_price,
395 struct_bias: ms.bias,
396 fib618: ms.fib618,
397 fib_zone,
398 fib_ok: if bull { fib_ok_long } else { fib_ok_short },
399 bos: ms.bos,
400 choch: ms.choch,
401 ts_norm: ind.ts_norm,
402 dominance: ind.dominance,
403 cvd_slope: cvd.map(|c| c.cvd_slope),
404 cvd_div: cvd.map_or(0, |c| c.divergence),
405 ao: ind.ao,
406 ao_rising: ind.ao_rising,
407 wr_pct: ind.wr_pct,
408 mom_pct: ind.mom_pct,
409 wave_ok_long: ind.wave_ok_long,
410 wave_ok_short: ind.wave_ok_short,
411 mom_ok_long: ind.mom_ok_long,
412 mom_ok_short: ind.mom_ok_short,
413 vol_pct: vol.map(|v| v.vol_pct),
414 vol_regime: vol.map(|v| v.vol_regime),
415 };
416
417 if bull {
418 return (1, comps);
419 }
420 if bear {
421 return (-1, comps);
422 }
423 (0, comps)
424}
425
426fn empty_components(
427 ind: &Indicators,
428 liq: &LiquidityProfile,
429 conf: &ConfluenceEngine,
430 ms: &MarketStructure,
431 cvd: Option<&CVDTracker>,
432 vol: Option<&VolatilityPercentile>,
433) -> SignalComponents {
434 SignalComponents {
435 v_vwap: 0,
436 v_ema: 0,
437 v_st: 0,
438 v_ts: 0,
439 v_liq: 0,
440 v_conf_bull: 0,
441 v_conf_bear: 0,
442 v_struct: 0,
443 v_cvd: 0,
444 v_ao: 0,
445 v_hurst: 0,
446 v_accel_bull: 0,
447 v_accel_bear: 0,
448 hurst: ind.hurst,
449 price_accel: ind.price_accel,
450 bull_score: conf.bull_score,
451 bear_score: conf.bear_score,
452 conf_min_adj: 0.0,
453 liq_imbalance: liq.imbalance,
454 liq_buy_pct: liq.buy_pct * 100.0,
455 poc: liq.poc_price,
456 struct_bias: ms.bias,
457 fib618: ms.fib618,
458 fib_zone: "mid",
459 fib_ok: false,
460 bos: false,
461 choch: false,
462 ts_norm: 0.5,
463 dominance: 0.0,
464 cvd_slope: cvd.map(|c| c.cvd_slope),
465 cvd_div: 0,
466 ao: ind.ao,
467 ao_rising: false,
468 wr_pct: 0.5,
469 mom_pct: 0.5,
470 wave_ok_long: false,
471 wave_ok_short: false,
472 mom_ok_long: false,
473 mom_ok_short: false,
474 vol_pct: vol.map(|v| v.vol_pct),
475 vol_regime: vol.map(|v| v.vol_regime),
476 }
477}
478
479pub struct SignalStreak {
486 required: usize,
487 direction: i32,
488 count: usize,
489}
490
491impl SignalStreak {
492 pub fn new(required: usize) -> Self {
493 Self {
494 required,
495 direction: 0,
496 count: 0,
497 }
498 }
499
500 pub fn update(&mut self, signal: i32) -> bool {
503 if signal != 0 && signal == self.direction {
504 self.count += 1;
505 } else {
506 self.direction = signal;
507 self.count = usize::from(signal != 0);
508 }
509 self.count >= self.required && signal != 0
510 }
511
512 pub fn reset(&mut self) {
513 self.direction = 0;
514 self.count = 0;
515 }
516 pub fn current_direction(&self) -> i32 {
517 self.direction
518 }
519 pub fn current_count(&self) -> usize {
520 self.count
521 }
522}
523
524#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
533 fn streak_fires_after_required_consecutive() {
534 let mut s = SignalStreak::new(3);
535 assert!(!s.update(1));
536 assert!(!s.update(1));
537 assert!(s.update(1)); assert!(s.update(1)); }
540
541 #[test]
542 fn streak_resets_on_direction_change() {
543 let mut s = SignalStreak::new(2);
544 assert!(!s.update(1));
545 assert!(s.update(1)); assert!(!s.update(-1)); assert!(s.update(-1)); }
549
550 #[test]
551 fn streak_zero_signal_breaks_streak() {
552 let mut s = SignalStreak::new(2);
553 s.update(1);
554 s.update(0); assert!(!s.update(1)); }
557
558 #[test]
559 fn streak_required_1_fires_immediately() {
560 let mut s = SignalStreak::new(1);
561 assert!(s.update(1));
562 assert!(s.update(-1));
563 assert!(!s.update(0));
564 }
565
566 #[test]
567 fn streak_tracks_direction_and_count() {
568 let mut s = SignalStreak::new(3);
569 s.update(1);
570 s.update(1);
571 assert_eq!(s.current_direction(), 1);
572 assert_eq!(s.current_count(), 2);
573 s.update(-1);
574 assert_eq!(s.current_direction(), -1);
575 assert_eq!(s.current_count(), 1);
576 }
577
578 #[test]
579 fn streak_reset_clears_state() {
580 let mut s = SignalStreak::new(2);
581 s.update(1);
582 s.update(1);
583 s.reset();
584 assert_eq!(s.current_count(), 0);
585 assert_eq!(s.current_direction(), 0);
586 assert!(!s.update(1)); }
588}