wickra_core/indicators/
ttm_trend.rs1use crate::error::Result;
4use crate::indicators::sma::Sma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
41pub struct TtmTrend {
42 period: usize,
43 sma: Sma,
44}
45
46impl TtmTrend {
47 pub fn new(period: usize) -> Result<Self> {
53 Ok(Self {
54 period,
55 sma: Sma::new(period)?,
56 })
57 }
58
59 pub const fn period(&self) -> usize {
61 self.period
62 }
63}
64
65impl Indicator for TtmTrend {
66 type Input = Candle;
67 type Output = f64;
68
69 fn update(&mut self, candle: Candle) -> Option<f64> {
70 let median = f64::midpoint(candle.high, candle.low);
71 let reference = self.sma.update(median)?;
72 Some(if candle.close > reference { 1.0 } else { -1.0 })
73 }
74
75 fn reset(&mut self) {
76 self.sma.reset();
77 }
78
79 fn warmup_period(&self) -> usize {
80 self.period
81 }
82
83 fn is_ready(&self) -> bool {
84 self.sma.is_ready()
85 }
86
87 fn name(&self) -> &'static str {
88 "TtmTrend"
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::error::Error;
96 use crate::traits::BatchExt;
97
98 fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
99 Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
100 }
101
102 #[test]
103 fn rejects_zero_period() {
104 assert!(matches!(TtmTrend::new(0), Err(Error::PeriodZero)));
105 }
106
107 #[test]
108 fn accessors_and_metadata() {
109 let t = TtmTrend::new(6).unwrap();
110 assert_eq!(t.period(), 6);
111 assert_eq!(t.warmup_period(), 6);
112 assert_eq!(t.name(), "TtmTrend");
113 assert!(!t.is_ready());
114 }
115
116 #[test]
117 fn warmup_then_emits() {
118 let mut t = TtmTrend::new(3).unwrap();
119 let candles: Vec<Candle> = (0..3).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
120 let out = t.batch(&candles);
121 assert!(out[0].is_none());
122 assert!(out[1].is_none());
123 assert!(out[2].is_some());
124 }
125
126 #[test]
127 fn close_above_reference_is_uptrend() {
128 let mut t = TtmTrend::new(3).unwrap();
130 let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
131 assert_eq!(t.batch(&candles).last().unwrap().unwrap(), 1.0);
132 }
133
134 #[test]
135 fn close_at_or_below_reference_is_downtrend() {
136 let mut t = TtmTrend::new(3).unwrap();
138 let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
139 assert_eq!(t.batch(&candles).last().unwrap().unwrap(), -1.0);
140 }
141
142 #[test]
143 fn reset_clears_state() {
144 let mut t = TtmTrend::new(3).unwrap();
145 let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
146 t.batch(&candles);
147 assert!(t.is_ready());
148 t.reset();
149 assert!(!t.is_ready());
150 }
151
152 #[test]
153 fn batch_equals_streaming() {
154 let candles: Vec<Candle> = (0..40_i64)
155 .map(|i| {
156 let base = 100.0 + (i as f64 * 0.25).sin() * 4.0;
157 candle(base + 1.0, base - 1.0, base + (i as f64 * 0.5).cos(), i)
158 })
159 .collect();
160 let mut a = TtmTrend::new(6).unwrap();
161 let mut b = TtmTrend::new(6).unwrap();
162 assert_eq!(
163 a.batch(&candles),
164 candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
165 );
166 }
167}