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> {
70 if n_first == 0 && n_last == 0 {
71 return Err(Error::PeriodZero);
72 }
73 Ok(Self {
74 n_first,
75 n_last,
76 utc_offset_minutes,
77 day: None,
78 cur_close: 0.0,
79 prev_day_close: None,
80 sum: 0.0,
81 count: 0,
82 })
83 }
84
85 pub fn classic() -> Self {
87 Self::new(3, 1, 0).expect("classic turn-of-month window is valid")
88 }
89
90 pub const fn params(&self) -> (u32, u32, i32) {
92 (self.n_first, self.n_last, self.utc_offset_minutes)
93 }
94
95 pub fn value(&self) -> Option<f64> {
97 if self.count == 0 {
98 None
99 } else {
100 Some(self.sum / self.count as f64)
101 }
102 }
103
104 fn roll_into(
107 &mut self,
108 year: i64,
109 month: u32,
110 dom: u32,
111 next_key: (i64, u32, u32),
112 close: f64,
113 ) {
114 if let Some(prev) = self.prev_day_close {
115 let ret = if prev == 0.0 {
116 0.0
117 } else {
118 self.cur_close / prev - 1.0
119 };
120 if in_turn_window(dom, days_in_month(year, month), self.n_first, self.n_last) {
121 self.sum += ret;
122 self.count += 1;
123 }
124 }
125 self.prev_day_close = Some(self.cur_close);
126 self.day = Some(next_key);
127 self.cur_close = close;
128 }
129}
130
131impl Indicator for TurnOfMonth {
132 type Input = Candle;
133 type Output = f64;
134
135 #[inline]
136 fn update(&mut self, candle: Candle) -> Option<f64> {
137 let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
138 let key = (civil.year, civil.month, civil.day);
139 match self.day {
140 Some(prev) if prev == key => {
141 self.cur_close = candle.close;
142 }
143 Some((year, month, dom)) => {
144 self.roll_into(year, month, dom, key, candle.close);
145 }
146 None => {
147 self.day = Some(key);
148 self.cur_close = candle.close;
149 }
150 }
151 self.value()
152 }
153
154 fn reset(&mut self) {
155 self.day = None;
156 self.cur_close = 0.0;
157 self.prev_day_close = None;
158 self.sum = 0.0;
159 self.count = 0;
160 }
161
162 #[inline]
163 fn warmup_period(&self) -> usize {
164 2
165 }
166
167 #[inline]
168 fn is_ready(&self) -> bool {
169 self.count > 0
170 }
171
172 #[inline]
173 fn name(&self) -> &'static str {
174 "TurnOfMonth"
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::traits::BatchExt;
182 use approx::assert_relative_eq;
183
184 const DAY: i64 = 24 * 3_600_000;
185 const JAN28_2021: i64 = 1_611_792_000_000;
187
188 fn c(close: f64, ts: i64) -> Candle {
189 Candle::new(close, close, close, close, 1.0, ts).unwrap()
190 }
191
192 #[test]
193 fn window_predicate_branches() {
194 assert!(in_turn_window(1, 31, 3, 1));
196 assert!(in_turn_window(3, 31, 3, 1));
197 assert!(!in_turn_window(4, 31, 3, 1));
198 assert!(in_turn_window(31, 31, 3, 1));
200 assert!(!in_turn_window(30, 31, 3, 1));
201 assert!(in_turn_window(1, 28, 0, 40));
203 }
204
205 #[test]
206 fn rejects_empty_window() {
207 assert!(matches!(TurnOfMonth::new(0, 0, 0), Err(Error::PeriodZero)));
208 }
209
210 #[test]
211 fn metadata_and_accessors() {
212 let tom = TurnOfMonth::classic();
213 assert_eq!(tom.params(), (3, 1, 0));
214 assert_eq!(tom.name(), "TurnOfMonth");
215 assert_eq!(tom.warmup_period(), 2);
216 assert!(!tom.is_ready());
217 assert!(tom.value().is_none());
218 }
219
220 #[test]
221 fn averages_in_window_returns_only() {
222 let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
223 assert!(tom.update(c(100.0, JAN28_2021)).is_none());
225 assert!(tom.update(c(110.0, JAN28_2021 + DAY)).is_none());
227 assert!(tom.update(c(120.0, JAN28_2021 + 2 * DAY)).is_none());
229 assert!(tom.update(c(121.0, JAN28_2021 + 3 * DAY)).is_none());
231 let v = tom.update(c(130.0, JAN28_2021 + 4 * DAY)).unwrap();
234 assert_relative_eq!(v, 121.0 / 120.0 - 1.0);
235 assert!(tom.is_ready());
236 }
237
238 #[test]
239 fn zero_prev_close_contributes_zero() {
240 let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
241 tom.update(c(0.0, JAN28_2021 + 2 * DAY));
243 tom.update(c(5.0, JAN28_2021 + 3 * DAY));
246 let v = tom.update(c(50.0, JAN28_2021 + 4 * DAY)).unwrap();
248 assert_relative_eq!(v, 0.0);
249 }
250
251 #[test]
252 fn same_day_bars_use_latest_close() {
253 let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
254 tom.update(c(100.0, JAN28_2021 + 2 * DAY));
256 tom.update(c(110.0, JAN28_2021 + 3 * DAY));
258 tom.update(c(120.0, JAN28_2021 + 3 * DAY + 3_600_000));
259 let v = tom.update(c(130.0, JAN28_2021 + 4 * DAY)).unwrap();
261 assert_relative_eq!(v, 0.20);
262 }
263
264 #[test]
265 fn reset_clears_state() {
266 let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
267 tom.update(c(121.0, JAN28_2021 + 3 * DAY));
268 tom.update(c(130.0, JAN28_2021 + 4 * DAY));
269 tom.reset();
270 assert!(!tom.is_ready());
271 assert!(tom.value().is_none());
272 }
273
274 #[test]
275 fn batch_equals_streaming() {
276 let candles: Vec<Candle> = (0..40)
277 .map(|i| c(100.0 + f64::from(i), JAN28_2021 + i64::from(i) * DAY))
278 .collect();
279 let mut a = TurnOfMonth::new(3, 2, 0).unwrap();
280 let mut b = TurnOfMonth::new(3, 2, 0).unwrap();
281 assert_eq!(
282 a.batch(&candles),
283 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
284 );
285 }
286}