kestrel_chartkit/indicator/
multi_factor.rs1use super::buy_sell_pressure::BuySellPressureEstimator;
2use super::rsi::Rsi;
3use super::trend_quality::TrendQualityScoreEngine;
4use super::volatility_regime::VolatilityRegimeDetector;
5use super::{Indicator, IndicatorAlert, IndicatorOutput};
6use crate::model::Bar;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
24pub struct MultiFactorMarketScore {
25 trend: TrendQualityScoreEngine,
26 rsi: Rsi,
27 pressure: BuySellPressureEstimator,
28 volatility: VolatilityRegimeDetector,
29}
30
31impl MultiFactorMarketScore {
32 pub fn new(period: usize) -> Self {
33 Self {
34 trend: TrendQualityScoreEngine::new(period),
35 rsi: Rsi::with_period(period),
36 pressure: BuySellPressureEstimator::new(period),
37 volatility: VolatilityRegimeDetector::new(period, 2.0, 1.5),
38 }
39 }
40
41 pub fn with_defaults() -> Self {
42 Self::new(14)
43 }
44}
45
46impl Indicator for MultiFactorMarketScore {
47 fn name(&self) -> &str {
48 "multi_factor"
49 }
50
51 fn warmup_period(&self) -> usize {
52 self.trend
53 .warmup_period()
54 .max(self.rsi.warmup_period())
55 .max(self.pressure.warmup_period())
56 .max(self.volatility.warmup_period())
57 }
58
59 fn reset(&mut self) {
60 self.trend.reset();
61 self.rsi.reset();
62 self.pressure.reset();
63 self.volatility.reset();
64 }
65
66 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
67 let trend_out = self.trend.on_bar(bar);
68 let rsi_out = self.rsi.on_bar(bar);
69 let pressure_out = self.pressure.on_bar(bar);
70 let vol_out = self.volatility.on_bar(bar);
71 let (Some(trend_out), Some(rsi_out), Some(pressure_out), Some(vol_out)) =
72 (trend_out, rsi_out, pressure_out, vol_out)
73 else {
74 return None;
75 };
76 let trend_score = trend_out.value / 100.0;
77 let rsi_norm = (rsi_out.value - 50.0) / 50.0;
78 let pressure_score = pressure_out.value / 100.0;
79
80 let vol_state = vol_out.value; let raw_composite = trend_score * 0.35 + rsi_norm * 0.25 + pressure_score * 0.40;
84
85 let final_score = if vol_state < 0.0 {
87 raw_composite * 0.5
88 } else {
89 raw_composite
90 }
91 .clamp(-1.0, 1.0);
92
93 let mut extra = HashMap::new();
94 extra.insert("trend_factor".to_string(), trend_score);
95 extra.insert("rsi_factor".to_string(), rsi_norm);
96 extra.insert("pressure_factor".to_string(), pressure_score);
97 extra.insert("volatility_factor".to_string(), vol_state);
98
99 Some(IndicatorOutput::with_extra(final_score, extra))
100 }
101
102 fn alerts(&self) -> Vec<IndicatorAlert> {
103 Vec::new()
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_multi_factor_market_score() {
113 let mut mf = MultiFactorMarketScore::with_defaults();
114 let mut out = None;
115 for i in 0..150 {
116 let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
117 out = mf.on_bar(&b);
118 }
119 assert!(out.is_some());
120 let val = out.unwrap().value;
121 assert!((-1.0..=1.0).contains(&val));
122 }
123}