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