kestrel_chartkit/indicator/
trend_quality.rs1use super::adx::Adx;
2use super::efficiency::LegEfficiencyEngine;
3use super::moving_averages::EmaEngine;
4use super::volume_indicators::RvolEngine;
5use super::{Indicator, IndicatorAlert, IndicatorOutput};
6use crate::model::Bar;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
26pub struct TrendQualityScoreEngine {
27 ema: EmaEngine,
28 efficiency: LegEfficiencyEngine,
29 adx: Adx,
30 rvol: RvolEngine,
31 prev_ema: Option<f64>,
32}
33
34impl TrendQualityScoreEngine {
35 pub fn new(period: usize) -> Self {
36 Self {
37 ema: EmaEngine::new(period),
38 efficiency: LegEfficiencyEngine::new(period),
39 adx: Adx::with_period(period),
40 rvol: RvolEngine::new(period),
41 prev_ema: None,
42 }
43 }
44
45 pub fn with_defaults() -> Self {
46 Self::new(14)
47 }
48}
49
50impl Indicator for TrendQualityScoreEngine {
51 fn name(&self) -> &str {
52 "trend_quality"
53 }
54
55 fn warmup_period(&self) -> usize {
56 self.ema
57 .warmup_period()
58 .max(self.efficiency.warmup_period())
59 .max(self.adx.warmup_period())
60 .max(self.rvol.warmup_period())
61 }
62
63 fn reset(&mut self) {
64 self.ema.reset();
65 self.efficiency.reset();
66 self.adx.reset();
67 self.rvol.reset();
68 self.prev_ema = None;
69 }
70
71 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
72 let ema_out = self.ema.on_bar(bar);
73 let efficiency_out = self.efficiency.on_bar(bar);
74 let adx_out = self.adx.on_bar(bar);
75 let rvol_out = self.rvol.on_bar(bar);
76 let (Some(ema_out), Some(efficiency_out), Some(adx_out), Some(rvol_out)) =
77 (ema_out, efficiency_out, adx_out, rvol_out)
78 else {
79 return None;
80 };
81 let ema_val = ema_out.value;
82 let eff_val = efficiency_out.value;
83 let rvol_val = rvol_out.value;
84
85 let slope = match self.prev_ema {
86 Some(prev) => (ema_val - prev) / prev.max(1e-8),
87 None => 0.0,
88 };
89 self.prev_ema = Some(ema_val);
90
91 let direction = if slope > 0.0 { 1.0 } else { -1.0 };
92 let strength = (adx_out.value / 50.0).clamp(0.0, 1.0); let participation = (rvol_val / 2.0).clamp(0.2, 1.0); let raw_score = direction * eff_val * strength * participation * 100.0;
96 let quality_score = raw_score.clamp(-100.0, 100.0);
97
98 let mut extra = HashMap::new();
99 extra.insert("direction".to_string(), direction);
100 extra.insert("efficiency".to_string(), eff_val);
101 extra.insert("strength".to_string(), strength);
102 extra.insert("participation".to_string(), participation);
103
104 Some(IndicatorOutput::with_extra(quality_score, extra))
105 }
106
107 fn alerts(&self) -> Vec<IndicatorAlert> {
108 Vec::new()
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_trend_quality_score() {
118 let mut tq = TrendQualityScoreEngine::with_defaults();
119 let mut out = None;
120 for i in 0..100 {
121 let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
122 out = tq.on_bar(&b);
123 }
124 assert!(out.is_some());
125 }
126}