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)]
12pub struct TrendQualityScoreEngine {
13 ema: EmaEngine,
14 efficiency: LegEfficiencyEngine,
15 adx: Adx,
16 rvol: RvolEngine,
17 prev_ema: Option<f64>,
18}
19
20impl TrendQualityScoreEngine {
21 pub fn new(period: usize) -> Self {
22 Self {
23 ema: EmaEngine::new(period),
24 efficiency: LegEfficiencyEngine::new(period),
25 adx: Adx::with_period(period),
26 rvol: RvolEngine::new(period),
27 prev_ema: None,
28 }
29 }
30
31 pub fn with_defaults() -> Self {
32 Self::new(14)
33 }
34}
35
36impl Indicator for TrendQualityScoreEngine {
37 fn name(&self) -> &str {
38 "trend_quality"
39 }
40
41 fn warmup_period(&self) -> usize {
42 self.ema
43 .warmup_period()
44 .max(self.efficiency.warmup_period())
45 .max(self.adx.warmup_period())
46 .max(self.rvol.warmup_period())
47 }
48
49 fn reset(&mut self) {
50 self.ema.reset();
51 self.efficiency.reset();
52 self.adx.reset();
53 self.rvol.reset();
54 self.prev_ema = None;
55 }
56
57 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
58 let ema_out = self.ema.on_bar(bar);
59 let efficiency_out = self.efficiency.on_bar(bar);
60 let adx_out = self.adx.on_bar(bar);
61 let rvol_out = self.rvol.on_bar(bar);
62 let (Some(ema_out), Some(efficiency_out), Some(adx_out), Some(rvol_out)) =
63 (ema_out, efficiency_out, adx_out, rvol_out)
64 else {
65 return None;
66 };
67 let ema_val = ema_out.value;
68 let eff_val = efficiency_out.value;
69 let rvol_val = rvol_out.value;
70
71 let slope = match self.prev_ema {
72 Some(prev) => (ema_val - prev) / prev.max(1e-8),
73 None => 0.0,
74 };
75 self.prev_ema = Some(ema_val);
76
77 let direction = if slope > 0.0 { 1.0 } else { -1.0 };
78 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;
82 let quality_score = raw_score.clamp(-100.0, 100.0);
83
84 let mut extra = HashMap::new();
85 extra.insert("direction".to_string(), direction);
86 extra.insert("efficiency".to_string(), eff_val);
87 extra.insert("strength".to_string(), strength);
88 extra.insert("participation".to_string(), participation);
89
90 Some(IndicatorOutput::with_extra(quality_score, extra))
91 }
92
93 fn alerts(&self) -> Vec<IndicatorAlert> {
94 Vec::new()
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_trend_quality_score() {
104 let mut tq = TrendQualityScoreEngine::with_defaults();
105 let mut out = None;
106 for i in 0..100 {
107 let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
108 out = tq.on_bar(&b);
109 }
110 assert!(out.is_some());
111 }
112}