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)]
12pub struct MultiFactorMarketScore {
13 trend: TrendQualityScoreEngine,
14 rsi: Rsi,
15 pressure: BuySellPressureEstimator,
16 volatility: VolatilityRegimeDetector,
17}
18
19impl MultiFactorMarketScore {
20 pub fn new(period: usize) -> Self {
21 Self {
22 trend: TrendQualityScoreEngine::new(period),
23 rsi: Rsi::with_period(period),
24 pressure: BuySellPressureEstimator::new(period),
25 volatility: VolatilityRegimeDetector::new(period, 2.0, 1.5),
26 }
27 }
28
29 pub fn with_defaults() -> Self {
30 Self::new(14)
31 }
32}
33
34impl Indicator for MultiFactorMarketScore {
35 fn name(&self) -> &str {
36 "multi_factor"
37 }
38
39 fn warmup_period(&self) -> usize {
40 self.trend
41 .warmup_period()
42 .max(self.rsi.warmup_period())
43 .max(self.pressure.warmup_period())
44 .max(self.volatility.warmup_period())
45 }
46
47 fn reset(&mut self) {
48 self.trend.reset();
49 self.rsi.reset();
50 self.pressure.reset();
51 self.volatility.reset();
52 }
53
54 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
55 let trend_out = self.trend.on_bar(bar);
56 let rsi_out = self.rsi.on_bar(bar);
57 let pressure_out = self.pressure.on_bar(bar);
58 let vol_out = self.volatility.on_bar(bar);
59 let (Some(trend_out), Some(rsi_out), Some(pressure_out), Some(vol_out)) =
60 (trend_out, rsi_out, pressure_out, vol_out)
61 else {
62 return None;
63 };
64 let trend_score = trend_out.value / 100.0;
65 let rsi_norm = (rsi_out.value - 50.0) / 50.0;
66 let pressure_score = pressure_out.value / 100.0;
67
68 let vol_state = vol_out.value; let raw_composite = trend_score * 0.35 + rsi_norm * 0.25 + pressure_score * 0.40;
72
73 let final_score = if vol_state < 0.0 {
75 raw_composite * 0.5
76 } else {
77 raw_composite
78 }
79 .clamp(-1.0, 1.0);
80
81 let mut extra = HashMap::new();
82 extra.insert("trend_factor".to_string(), trend_score);
83 extra.insert("rsi_factor".to_string(), rsi_norm);
84 extra.insert("pressure_factor".to_string(), pressure_score);
85 extra.insert("volatility_factor".to_string(), vol_state);
86
87 Some(IndicatorOutput::with_extra(final_score, extra))
88 }
89
90 fn alerts(&self) -> Vec<IndicatorAlert> {
91 Vec::new()
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_multi_factor_market_score() {
101 let mut mf = MultiFactorMarketScore::with_defaults();
102 let mut out = None;
103 for i in 0..150 {
104 let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
105 out = mf.on_bar(&b);
106 }
107 assert!(out.is_some());
108 let val = out.unwrap().value;
109 assert!((-1.0..=1.0).contains(&val));
110 }
111}