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