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 #[inline]
70 fn update(&mut self, candle: Candle) -> Option<f64> {
71 let median = f64::midpoint(candle.high, candle.low);
72 let reference = self.sma.update(median)?;
73 Some(if candle.close > reference { 1.0 } else { -1.0 })
74 }
75
76 fn reset(&mut self) {
77 self.sma.reset();
78 }
79
80 #[inline]
81 fn warmup_period(&self) -> usize {
82 self.period
83 }
84
85 #[inline]
86 fn is_ready(&self) -> bool {
87 self.sma.is_ready()
88 }
89
90 #[inline]
91 fn name(&self) -> &'static str {
92 "TtmTrend"
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::error::Error;
100 use crate::traits::BatchExt;
101
102 fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
103 Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
104 }
105
106 #[test]
107 fn rejects_zero_period() {
108 assert!(matches!(TtmTrend::new(0), Err(Error::PeriodZero)));
109 }
110
111 #[test]
112 fn accessors_and_metadata() {
113 let t = TtmTrend::new(6).unwrap();
114 assert_eq!(t.period(), 6);
115 assert_eq!(t.warmup_period(), 6);
116 assert_eq!(t.name(), "TtmTrend");
117 assert!(!t.is_ready());
118 }
119
120 #[test]
121 fn warmup_then_emits() {
122 let mut t = TtmTrend::new(3).unwrap();
123 let candles: Vec<Candle> = (0..3).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
124 let out = t.batch(&candles);
125 assert!(out[0].is_none());
126 assert!(out[1].is_none());
127 assert!(out[2].is_some());
128 }
129
130 #[test]
131 fn close_above_reference_is_uptrend() {
132 let mut t = TtmTrend::new(3).unwrap();
134 let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
135 assert_eq!(t.batch(&candles).last().unwrap().unwrap(), 1.0);
136 }
137
138 #[test]
139 fn close_at_or_below_reference_is_downtrend() {
140 let mut t = TtmTrend::new(3).unwrap();
142 let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
143 assert_eq!(t.batch(&candles).last().unwrap().unwrap(), -1.0);
144 }
145
146 #[test]
147 fn reset_clears_state() {
148 let mut t = TtmTrend::new(3).unwrap();
149 let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
150 t.batch(&candles);
151 assert!(t.is_ready());
152 t.reset();
153 assert!(!t.is_ready());
154 }
155
156 #[test]
157 fn batch_equals_streaming() {
158 let candles: Vec<Candle> = (0..40_i64)
159 .map(|i| {
160 let base = 100.0 + (i as f64 * 0.25).sin() * 4.0;
161 candle(base + 1.0, base - 1.0, base + (i as f64 * 0.5).cos(), i)
162 })
163 .collect();
164 let mut a = TtmTrend::new(6).unwrap();
165 let mut b = TtmTrend::new(6).unwrap();
166 assert_eq!(
167 a.batch(&candles),
168 candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
169 );
170 }
171}