1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::atr::Atr;
7use crate::indicators::bollinger::BollingerBands;
8use crate::indicators::sma::Sma;
9use crate::ohlcv::Candle;
10use crate::traits::Indicator;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct TtmSqueezeOutput {
15 pub squeeze: f64,
19 pub momentum: f64,
25}
26
27#[derive(Debug, Clone)]
65pub struct TtmSqueeze {
66 period: usize,
67 kc_mult: f64,
68 bb: BollingerBands,
69 sma_close: Sma,
70 atr: Atr,
71 highs: VecDeque<f64>,
72 lows: VecDeque<f64>,
73 closes: VecDeque<f64>,
74 sum_x: f64,
76 denom: f64,
77}
78
79impl TtmSqueeze {
80 pub fn new(period: usize, bb_mult: f64, kc_mult: f64) -> Result<Self> {
86 if period < 2 {
87 return Err(Error::InvalidPeriod {
88 message: "TTM squeeze needs period >= 2 for the momentum regression",
89 });
90 }
91 if period > crate::error::MAX_PERIOD {
92 return Err(Error::InvalidPeriod {
93 message: crate::error::PERIOD_ABOVE_MAX,
94 });
95 }
96 if !bb_mult.is_finite() || bb_mult <= 0.0 || !kc_mult.is_finite() || kc_mult <= 0.0 {
97 return Err(Error::NonPositiveMultiplier);
98 }
99 let n = period as f64;
100 let sum_x = n * (n - 1.0) / 2.0;
101 let sum_xx = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
102 Ok(Self {
103 period,
104 kc_mult,
105 bb: BollingerBands::new(period, bb_mult)?,
106 sma_close: Sma::new(period)?,
107 atr: Atr::new(period)?,
108 highs: VecDeque::with_capacity(period),
109 lows: VecDeque::with_capacity(period),
110 closes: VecDeque::with_capacity(period),
111 sum_x,
112 denom: n * sum_xx - sum_x * sum_x,
113 })
114 }
115
116 pub fn classic() -> Self {
119 Self::new(20, 2.0, 1.5).expect("classic TTM Squeeze parameters are valid")
120 }
121
122 pub fn parameters(&self) -> (usize, f64, f64) {
124 (self.period, self.bb.multiplier(), self.kc_mult)
125 }
126}
127
128impl Indicator for TtmSqueeze {
129 type Input = Candle;
130 type Output = TtmSqueezeOutput;
131
132 fn update(&mut self, candle: Candle) -> Option<TtmSqueezeOutput> {
133 if self.highs.len() == self.period {
134 self.highs.pop_front();
135 self.lows.pop_front();
136 self.closes.pop_front();
137 }
138 self.highs.push_back(candle.high);
139 self.lows.push_back(candle.low);
140 self.closes.push_back(candle.close);
141
142 let bb = self.bb.update(candle.close);
146 let mid = self.sma_close.update(candle.close);
147 let atr = self.atr.update(candle);
148 let (bb, mid, atr) = (bb?, mid?, atr?);
149
150 let kc_upper = mid + self.kc_mult * atr;
151 let kc_lower = mid - self.kc_mult * atr;
152 let squeeze = f64::from(bb.upper <= kc_upper && bb.lower >= kc_lower);
153
154 let hi = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
158 let lo = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
159 let hl_mid = f64::midpoint(hi, lo);
160 let baseline = f64::midpoint(hl_mid, mid);
164 let mut sum_y = 0.0;
165 let mut sum_xy = 0.0;
166 for (i, &c) in self.closes.iter().enumerate() {
167 let y = c - baseline;
168 let x = i as f64;
169 sum_y += y;
170 sum_xy += x * y;
171 }
172 let n = self.period as f64;
173 let slope = (n * sum_xy - self.sum_x * sum_y) / self.denom;
174 let intercept = (sum_y - slope * self.sum_x) / n;
175 let momentum = intercept + slope * (n - 1.0);
176
177 Some(TtmSqueezeOutput { squeeze, momentum })
178 }
179
180 fn reset(&mut self) {
181 self.bb.reset();
182 self.sma_close.reset();
183 self.atr.reset();
184 self.highs.clear();
185 self.lows.clear();
186 self.closes.clear();
187 }
188
189 #[inline]
190 fn warmup_period(&self) -> usize {
191 self.period
192 }
193
194 #[inline]
195 fn is_ready(&self) -> bool {
196 self.bb.is_ready() && self.sma_close.is_ready() && self.atr.is_ready()
197 }
198
199 #[inline]
200 fn name(&self) -> &'static str {
201 "TtmSqueeze"
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::traits::BatchExt;
209 use approx::assert_relative_eq;
210
211 fn c(h: f64, l: f64, cl: f64) -> Candle {
212 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
213 }
214
215 #[test]
216 fn rejects_invalid_period() {
217 assert!(TtmSqueeze::new(0, 2.0, 1.5).is_err());
218 assert!(TtmSqueeze::new(1, 2.0, 1.5).is_err());
219 }
220
221 #[test]
222 fn rejects_non_positive_multipliers() {
223 assert!(matches!(
224 TtmSqueeze::new(20, 0.0, 1.5),
225 Err(Error::NonPositiveMultiplier)
226 ));
227 assert!(matches!(
228 TtmSqueeze::new(20, 2.0, -1.0),
229 Err(Error::NonPositiveMultiplier)
230 ));
231 assert!(matches!(
232 TtmSqueeze::new(20, f64::NAN, 1.5),
233 Err(Error::NonPositiveMultiplier)
234 ));
235 }
236
237 #[test]
238 fn accessors_and_metadata() {
239 let s = TtmSqueeze::classic();
240 let (p, b, k) = s.parameters();
241 assert_eq!(p, 20);
242 assert_relative_eq!(b, 2.0, epsilon = 1e-12);
243 assert_relative_eq!(k, 1.5, epsilon = 1e-12);
244 assert_eq!(s.warmup_period(), 20);
245 assert_eq!(s.name(), "TtmSqueeze");
246 }
247
248 #[test]
249 fn flat_market_has_zero_momentum() {
250 let candles: Vec<Candle> = (0..30).map(|_| c(10.0, 10.0, 10.0)).collect();
251 let mut s = TtmSqueeze::new(20, 2.0, 1.5).unwrap();
252 let last = s.batch(&candles).into_iter().flatten().last().unwrap();
253 assert_relative_eq!(last.momentum, 0.0, epsilon = 1e-9);
254 assert_relative_eq!(last.squeeze, 1.0, epsilon = 1e-12);
257 }
258
259 #[test]
260 fn batch_equals_streaming() {
261 let candles: Vec<Candle> = (0..40)
262 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
263 .collect();
264 let mut a = TtmSqueeze::new(20, 2.0, 1.5).unwrap();
265 let mut b = TtmSqueeze::new(20, 2.0, 1.5).unwrap();
266 assert_eq!(
267 a.batch(&candles),
268 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
269 );
270 }
271
272 #[test]
273 fn reset_clears_state() {
274 let candles: Vec<Candle> = (0..30)
275 .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
276 .collect();
277 let mut s = TtmSqueeze::classic();
278 s.batch(&candles);
279 assert!(s.is_ready());
280 s.reset();
281 assert!(!s.is_ready());
282 assert_eq!(s.update(candles[0]), None);
283 }
284
285 #[test]
287 fn warmup_returns_none() {
288 let mut s = TtmSqueeze::new(20, 2.0, 1.5).unwrap();
289 for i in 0..19 {
290 let base = 100.0 + f64::from(i);
291 assert!(s.update(c(base + 1.0, base - 1.0, base)).is_none());
292 }
293 assert!(s.update(c(121.0, 119.0, 120.0)).is_some());
294 }
295
296 #[test]
298 fn squeeze_is_binary() {
299 let candles: Vec<Candle> = (0..60)
300 .map(|i| {
301 let m = 100.0 + (f64::from(i) * 0.4).sin() * 2.0;
302 c(m + 1.0, m - 1.0, m)
303 })
304 .collect();
305 let mut s = TtmSqueeze::new(20, 2.0, 1.5).unwrap();
306 for o in s.batch(&candles).into_iter().flatten() {
307 assert!(o.squeeze == 0.0 || o.squeeze == 1.0);
308 }
309 }
310}