1use crate::calendar::{civil_from_timestamp, days_in_month};
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9fn in_turn_window(dom: u32, dim: u32, n_first: u32, n_last: u32) -> bool {
14 dom <= n_first || dom > dim.saturating_sub(n_last)
15}
16
17#[derive(Debug, Clone)]
44pub struct TurnOfMonth {
45 n_first: u32,
46 n_last: u32,
47 utc_offset_minutes: i32,
48 day: Option<(i64, u32, u32)>,
49 cur_close: f64,
50 prev_day_close: Option<f64>,
51 sum: f64,
52 count: u64,
53}
54
55impl TurnOfMonth {
56 pub fn new(n_first: u32, n_last: u32, utc_offset_minutes: i32) -> Result<Self> {
63 if n_first == 0 && n_last == 0 {
64 return Err(Error::PeriodZero);
65 }
66 Ok(Self {
67 n_first,
68 n_last,
69 utc_offset_minutes,
70 day: None,
71 cur_close: 0.0,
72 prev_day_close: None,
73 sum: 0.0,
74 count: 0,
75 })
76 }
77
78 pub fn classic() -> Self {
80 Self::new(3, 1, 0).expect("classic turn-of-month window is valid")
81 }
82
83 pub const fn params(&self) -> (u32, u32, i32) {
85 (self.n_first, self.n_last, self.utc_offset_minutes)
86 }
87
88 pub fn value(&self) -> Option<f64> {
90 if self.count == 0 {
91 None
92 } else {
93 Some(self.sum / self.count as f64)
94 }
95 }
96
97 fn roll_into(
100 &mut self,
101 year: i64,
102 month: u32,
103 dom: u32,
104 next_key: (i64, u32, u32),
105 close: f64,
106 ) {
107 if let Some(prev) = self.prev_day_close {
108 let ret = if prev == 0.0 {
109 0.0
110 } else {
111 self.cur_close / prev - 1.0
112 };
113 if in_turn_window(dom, days_in_month(year, month), self.n_first, self.n_last) {
114 self.sum += ret;
115 self.count += 1;
116 }
117 }
118 self.prev_day_close = Some(self.cur_close);
119 self.day = Some(next_key);
120 self.cur_close = close;
121 }
122}
123
124impl Indicator for TurnOfMonth {
125 type Input = Candle;
126 type Output = f64;
127
128 fn update(&mut self, candle: Candle) -> Option<f64> {
129 let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
130 let key = (civil.year, civil.month, civil.day);
131 match self.day {
132 Some(prev) if prev == key => {
133 self.cur_close = candle.close;
134 }
135 Some((year, month, dom)) => {
136 self.roll_into(year, month, dom, key, candle.close);
137 }
138 None => {
139 self.day = Some(key);
140 self.cur_close = candle.close;
141 }
142 }
143 self.value()
144 }
145
146 fn reset(&mut self) {
147 self.day = None;
148 self.cur_close = 0.0;
149 self.prev_day_close = None;
150 self.sum = 0.0;
151 self.count = 0;
152 }
153
154 fn warmup_period(&self) -> usize {
155 2
156 }
157
158 fn is_ready(&self) -> bool {
159 self.count > 0
160 }
161
162 fn name(&self) -> &'static str {
163 "TurnOfMonth"
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::traits::BatchExt;
171 use approx::assert_relative_eq;
172
173 const DAY: i64 = 24 * 3_600_000;
174 const JAN28_2021: i64 = 1_611_792_000_000;
176
177 fn c(close: f64, ts: i64) -> Candle {
178 Candle::new(close, close, close, close, 1.0, ts).unwrap()
179 }
180
181 #[test]
182 fn window_predicate_branches() {
183 assert!(in_turn_window(1, 31, 3, 1));
185 assert!(in_turn_window(3, 31, 3, 1));
186 assert!(!in_turn_window(4, 31, 3, 1));
187 assert!(in_turn_window(31, 31, 3, 1));
189 assert!(!in_turn_window(30, 31, 3, 1));
190 assert!(in_turn_window(1, 28, 0, 40));
192 }
193
194 #[test]
195 fn rejects_empty_window() {
196 assert!(matches!(TurnOfMonth::new(0, 0, 0), Err(Error::PeriodZero)));
197 }
198
199 #[test]
200 fn metadata_and_accessors() {
201 let tom = TurnOfMonth::classic();
202 assert_eq!(tom.params(), (3, 1, 0));
203 assert_eq!(tom.name(), "TurnOfMonth");
204 assert_eq!(tom.warmup_period(), 2);
205 assert!(!tom.is_ready());
206 assert!(tom.value().is_none());
207 }
208
209 #[test]
210 fn averages_in_window_returns_only() {
211 let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
212 assert!(tom.update(c(100.0, JAN28_2021)).is_none());
214 assert!(tom.update(c(110.0, JAN28_2021 + DAY)).is_none());
216 assert!(tom.update(c(120.0, JAN28_2021 + 2 * DAY)).is_none());
218 assert!(tom.update(c(121.0, JAN28_2021 + 3 * DAY)).is_none());
220 let v = tom.update(c(130.0, JAN28_2021 + 4 * DAY)).unwrap();
223 assert_relative_eq!(v, 121.0 / 120.0 - 1.0);
224 assert!(tom.is_ready());
225 }
226
227 #[test]
228 fn zero_prev_close_contributes_zero() {
229 let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
230 tom.update(c(0.0, JAN28_2021 + 2 * DAY));
232 tom.update(c(5.0, JAN28_2021 + 3 * DAY));
235 let v = tom.update(c(50.0, JAN28_2021 + 4 * DAY)).unwrap();
237 assert_relative_eq!(v, 0.0);
238 }
239
240 #[test]
241 fn same_day_bars_use_latest_close() {
242 let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
243 tom.update(c(100.0, JAN28_2021 + 2 * DAY));
245 tom.update(c(110.0, JAN28_2021 + 3 * DAY));
247 tom.update(c(120.0, JAN28_2021 + 3 * DAY + 3_600_000));
248 let v = tom.update(c(130.0, JAN28_2021 + 4 * DAY)).unwrap();
250 assert_relative_eq!(v, 0.20);
251 }
252
253 #[test]
254 fn reset_clears_state() {
255 let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
256 tom.update(c(121.0, JAN28_2021 + 3 * DAY));
257 tom.update(c(130.0, JAN28_2021 + 4 * DAY));
258 tom.reset();
259 assert!(!tom.is_ready());
260 assert!(tom.value().is_none());
261 }
262
263 #[test]
264 fn batch_equals_streaming() {
265 let candles: Vec<Candle> = (0..40)
266 .map(|i| c(100.0 + f64::from(i), JAN28_2021 + i64::from(i) * DAY))
267 .collect();
268 let mut a = TurnOfMonth::new(3, 2, 0).unwrap();
269 let mut b = TurnOfMonth::new(3, 2, 0).unwrap();
270 assert_eq!(
271 a.batch(&candles),
272 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
273 );
274 }
275}