wickra_core/indicators/
twiggs_money_flow.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
45pub struct TwiggsMoneyFlow {
46 period: usize,
47 prev_close: Option<f64>,
48 seed_ad: f64,
49 seed_vol: f64,
50 seed_count: usize,
51 ad_ema: Option<f64>,
52 vol_ema: Option<f64>,
53 last: Option<f64>,
54}
55
56impl TwiggsMoneyFlow {
57 pub fn new(period: usize) -> Result<Self> {
63 if period == 0 {
64 return Err(Error::PeriodZero);
65 }
66 if period > crate::error::MAX_PERIOD {
67 return Err(Error::InvalidPeriod {
68 message: crate::error::PERIOD_ABOVE_MAX,
69 });
70 }
71 Ok(Self {
72 period,
73 prev_close: None,
74 seed_ad: 0.0,
75 seed_vol: 0.0,
76 seed_count: 0,
77 ad_ema: None,
78 vol_ema: None,
79 last: None,
80 })
81 }
82
83 pub const fn period(&self) -> usize {
85 self.period
86 }
87
88 pub const fn value(&self) -> Option<f64> {
90 self.last
91 }
92
93 fn ratio(ad_ema: f64, vol_ema: f64) -> f64 {
94 if vol_ema == 0.0 {
95 0.0
96 } else {
97 ad_ema / vol_ema
98 }
99 }
100}
101
102impl Indicator for TwiggsMoneyFlow {
103 type Input = Candle;
104 type Output = f64;
105
106 fn update(&mut self, candle: Candle) -> Option<f64> {
107 let Some(prev_close) = self.prev_close else {
108 self.prev_close = Some(candle.close);
109 return None;
110 };
111 let trh = candle.high.max(prev_close);
112 let trl = candle.low.min(prev_close);
113 let range = trh - trl;
114 let ad = if range > 0.0 {
115 candle.volume * (2.0 * candle.close - trh - trl) / range
116 } else {
117 0.0
118 };
119 self.prev_close = Some(candle.close);
120
121 if let (Some(ad_ema), Some(vol_ema)) = (self.ad_ema, self.vol_ema) {
122 let n = self.period as f64;
123 let new_ad = ad_ema + (ad - ad_ema) / n;
124 let new_vol = vol_ema + (candle.volume - vol_ema) / n;
125 self.ad_ema = Some(new_ad);
126 self.vol_ema = Some(new_vol);
127 let v = Self::ratio(new_ad, new_vol);
128 self.last = Some(v);
129 return Some(v);
130 }
131
132 self.seed_ad += ad;
133 self.seed_vol += candle.volume;
134 self.seed_count += 1;
135 if self.seed_count == self.period {
136 let n = self.period as f64;
137 let ad_ema = self.seed_ad / n;
138 let vol_ema = self.seed_vol / n;
139 self.ad_ema = Some(ad_ema);
140 self.vol_ema = Some(vol_ema);
141 let v = Self::ratio(ad_ema, vol_ema);
142 self.last = Some(v);
143 return Some(v);
144 }
145 None
146 }
147
148 fn reset(&mut self) {
149 self.prev_close = None;
150 self.seed_ad = 0.0;
151 self.seed_vol = 0.0;
152 self.seed_count = 0;
153 self.ad_ema = None;
154 self.vol_ema = None;
155 self.last = None;
156 }
157
158 #[inline]
159 fn warmup_period(&self) -> usize {
160 self.period + 1
161 }
162
163 #[inline]
164 fn is_ready(&self) -> bool {
165 self.last.is_some()
166 }
167
168 #[inline]
169 fn name(&self) -> &'static str {
170 "TwiggsMoneyFlow"
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::traits::BatchExt;
178 use approx::assert_relative_eq;
179
180 fn candle(high: f64, low: f64, close: f64, volume: f64) -> Candle {
181 Candle::new_unchecked(low, high, low, close, volume, 0)
182 }
183
184 #[test]
185 fn rejects_zero_period() {
186 assert!(matches!(TwiggsMoneyFlow::new(0), Err(Error::PeriodZero)));
187 }
188
189 #[test]
190 fn flat_bars_drive_tmf_to_zero() {
191 let mut tmf = TwiggsMoneyFlow::new(2).unwrap();
195 let flat: Vec<Candle> = (0..6)
196 .map(|_| candle(100.0, 100.0, 100.0, 1_000.0))
197 .collect();
198 let last = tmf.batch(&flat).into_iter().flatten().last().unwrap();
199 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
200 }
201
202 #[test]
203 fn accessors_and_metadata() {
204 let tmf = TwiggsMoneyFlow::new(21).unwrap();
205 assert_eq!(tmf.period(), 21);
206 assert_eq!(tmf.warmup_period(), 22);
207 assert_eq!(tmf.name(), "TwiggsMoneyFlow");
208 assert!(!tmf.is_ready());
209 assert_eq!(tmf.value(), None);
210 }
211
212 #[test]
213 fn first_emission_at_warmup_period() {
214 let mut tmf = TwiggsMoneyFlow::new(3).unwrap();
215 let candles: Vec<Candle> = (0..8)
216 .map(|i| {
217 let base = 100.0 + f64::from(i);
218 candle(base + 1.0, base - 1.0, base, 1_000.0)
219 })
220 .collect();
221 let out = tmf.batch(&candles);
222 for o in out.iter().take(3) {
224 assert!(o.is_none());
225 }
226 assert!(out[3].is_some());
227 }
228
229 #[test]
230 fn closes_at_true_high_is_positive() {
231 let mut tmf = TwiggsMoneyFlow::new(3).unwrap();
233 let candles: Vec<Candle> = (0..12)
234 .map(|i| {
235 let base = 100.0 + f64::from(i);
236 Candle::new_unchecked(base - 1.0, base + 1.0, base - 1.0, base + 1.0, 1_000.0, 0)
238 })
239 .collect();
240 let last = tmf.batch(&candles).into_iter().flatten().last().unwrap();
241 assert!(
242 last > 0.9,
243 "closing at the high should drive TMF near +1, got {last}"
244 );
245 }
246
247 #[test]
248 fn closes_at_true_low_is_negative() {
249 let mut tmf = TwiggsMoneyFlow::new(3).unwrap();
250 let candles: Vec<Candle> = (0..12)
251 .map(|i| {
252 let base = 100.0 - f64::from(i);
253 Candle::new_unchecked(base + 1.0, base + 1.0, base - 1.0, base - 1.0, 1_000.0, 0)
255 })
256 .collect();
257 let last = tmf.batch(&candles).into_iter().flatten().last().unwrap();
258 assert!(
259 last < -0.5,
260 "closing at the low should drive TMF negative, got {last}"
261 );
262 }
263
264 #[test]
265 fn zero_volume_yields_zero() {
266 let mut tmf = TwiggsMoneyFlow::new(3).unwrap();
267 let candles: Vec<Candle> = (0..10)
268 .map(|i| {
269 let base = 100.0 + f64::from(i);
270 candle(base + 1.0, base - 1.0, base, 0.0)
271 })
272 .collect();
273 for v in tmf.batch(&candles).into_iter().flatten() {
274 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
275 }
276 }
277
278 #[test]
279 fn output_in_range() {
280 let mut tmf = TwiggsMoneyFlow::new(21).unwrap();
281 let candles: Vec<Candle> = (0..200)
282 .map(|i| {
283 let base = 100.0 + (f64::from(i) * 0.3).sin() * 12.0;
284 candle(base + 2.0, base - 2.0, base + 0.5, 1_000.0)
285 })
286 .collect();
287 for v in tmf.batch(&candles).into_iter().flatten() {
288 assert!((-1.0..=1.0).contains(&v), "TMF out of range: {v}");
289 }
290 }
291
292 #[test]
293 fn reset_clears_state() {
294 let mut tmf = TwiggsMoneyFlow::new(3).unwrap();
295 let candles: Vec<Candle> = (0..12)
296 .map(|i| {
297 let base = 100.0 + f64::from(i);
298 candle(base + 1.0, base - 1.0, base, 1_000.0)
299 })
300 .collect();
301 tmf.batch(&candles);
302 assert!(tmf.is_ready());
303 tmf.reset();
304 assert!(!tmf.is_ready());
305 assert_eq!(tmf.value(), None);
306 assert_eq!(tmf.update(candle(101.0, 99.0, 100.0, 1_000.0)), None);
307 }
308
309 #[test]
310 fn batch_equals_streaming() {
311 let candles: Vec<Candle> = (0..120)
312 .map(|i| {
313 let base = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
314 candle(base + 2.0, base - 1.5, base + 0.5, 1_000.0 + f64::from(i))
315 })
316 .collect();
317 let batch = TwiggsMoneyFlow::new(21).unwrap().batch(&candles);
318 let mut b = TwiggsMoneyFlow::new(21).unwrap();
319 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
320 assert_eq!(batch, streamed);
321 }
322}