1use super::over_finite_bars;
43use crate::model::Bar;
44use crate::studies::palette;
45use crate::studies::stepper::{update_window, usable_behind};
46use crate::studies::{Indicator, IndicatorValues};
47use chrono::{DateTime, TimeZone, Utc};
48use egui::Color32;
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum MoonPhase {
53 New,
55 Full,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub struct LunarPhase {
62 pub phase: MoonPhase,
64 pub time: DateTime<Utc>,
66}
67
68const UNIX_EPOCH_JD: f64 = 2_440_587.5;
70const GREGORIAN_YEAR_DAYS: f64 = 365.2425;
72
73fn delta_t(year: f64) -> f64 {
76 let poly = |t: f64, c: &[f64]| c.iter().rev().fold(0.0, |acc, c| acc * t + c);
77 match year {
78 y if y < 1920.0 => poly(
79 y - 1900.0,
80 &[-2.79, 1.494119, -0.0598939, 0.0061966, -0.000197],
81 ),
82 y if y < 1941.0 => poly(y - 1920.0, &[21.20, 0.84493, -0.0761, 0.0020936]),
83 y if y < 1961.0 => poly(y - 1950.0, &[29.07, 0.407, -1.0 / 233.0, 1.0 / 2547.0]),
84 y if y < 1986.0 => poly(y - 1975.0, &[45.45, 1.067, -1.0 / 260.0, -1.0 / 718.0]),
85 y if y < 2005.0 => poly(
86 y - 2000.0,
87 &[
88 63.86,
89 0.3345,
90 -0.060374,
91 0.0017275,
92 0.000651814,
93 0.00002373599,
94 ],
95 ),
96 y if y < 2050.0 => poly(y - 2000.0, &[62.92, 0.32217, 0.005589]),
97 y => -20.0 + 32.0 * ((y - 1820.0) / 100.0).powi(2) - 0.5628 * (2150.0 - y),
98 }
99}
100
101fn phase_jd(k: f64) -> f64 {
104 let t = k / 1236.85;
105 let (t2, t3, t4) = (t * t, t * t * t, t * t * t * t);
106 let full = k.fract() != 0.0;
107 let jde = 2_451_550.097_66 + 29.530_588_861 * k + 0.000_154_37 * t2 - 0.000_000_150 * t3
108 + 0.000_000_000_73 * t4;
109 let e = 1.0 - 0.002_516 * t - 0.000_007_4 * t2;
110 let rad = f64::to_radians;
111 let m = rad(2.5534 + 29.105_356_70 * k - 0.000_001_4 * t2 - 0.000_000_11 * t3);
114 let mp = rad(
115 201.5643 + 385.816_935_28 * k + 0.010_758_2 * t2 + 0.000_012_38 * t3 - 0.000_000_058 * t4,
116 );
117 let f = rad(
118 160.7108 + 390.670_502_84 * k - 0.001_611_8 * t2 - 0.000_002_27 * t3 + 0.000_000_011 * t4,
119 );
120 let omega = rad(124.7746 - 1.563_755_88 * k + 0.002_067_2 * t2 + 0.000_002_15 * t3);
121 let (c0, c1, c2, c3) = if full {
124 (-0.40614, 0.17302, 0.01614, 0.01043)
125 } else {
126 (-0.40720, 0.17241, 0.01608, 0.01039)
127 };
128 let (c4, c5, c6) = if full {
129 (0.00734, -0.00515, 0.00209)
130 } else {
131 (0.00739, -0.00514, 0.00208)
132 };
133 #[rustfmt::skip]
134 let terms: [(f64, i32, f64, f64, f64); 24] = [
135 (c0, 0, 1.0, 0.0, 0.0),
136 (c1, 1, 0.0, 1.0, 0.0),
137 (c2, 0, 2.0, 0.0, 0.0),
138 (c3, 0, 0.0, 0.0, 2.0),
139 (c4, 1, 1.0, -1.0, 0.0),
140 (c5, 1, 1.0, 1.0, 0.0),
141 (c6, 2, 0.0, 2.0, 0.0),
142 (-0.00111, 0, 1.0, 0.0, -2.0),
143 (-0.00057, 0, 1.0, 0.0, 2.0),
144 (0.00056, 1, 2.0, 1.0, 0.0),
145 (-0.00042, 0, 3.0, 0.0, 0.0),
146 (0.00042, 1, 0.0, 1.0, 2.0),
147 (0.00038, 1, 0.0, 1.0, -2.0),
148 (-0.00024, 1, 2.0, -1.0, 0.0),
149 (-0.00007, 0, 1.0, 2.0, 0.0),
150 (0.00004, 0, 2.0, 0.0, -2.0),
151 (0.00004, 0, 0.0, 3.0, 0.0),
152 (0.00003, 0, 1.0, 1.0, -2.0),
153 (0.00003, 0, 2.0, 0.0, 2.0),
154 (-0.00003, 0, 1.0, 1.0, 2.0),
155 (0.00003, 0, 1.0, -1.0, 2.0),
156 (-0.00002, 0, 1.0, -1.0, -2.0),
157 (-0.00002, 0, 3.0, 1.0, 0.0),
158 (0.00002, 0, 4.0, 0.0, 0.0),
159 ];
160 let mut correction = -0.00017 * omega.sin();
161 for (c, e_power, a, b, d) in terms {
162 correction += c * e.powi(e_power) * (a * mp + b * m + d * f).sin();
163 }
164 #[rustfmt::skip]
166 let planetary: [(f64, f64, f64, f64); 14] = [
167 (299.77, 0.107408, -0.009173, 0.000325),
168 (251.88, 0.016321, 0.0, 0.000165),
169 (251.83, 26.651886, 0.0, 0.000164),
170 (349.42, 36.412478, 0.0, 0.000126),
171 (84.66, 18.206239, 0.0, 0.000110),
172 (141.74, 53.303771, 0.0, 0.000062),
173 (207.14, 2.453732, 0.0, 0.000060),
174 (154.84, 7.306860, 0.0, 0.000056),
175 (34.52, 27.261239, 0.0, 0.000047),
176 (207.19, 0.121824, 0.0, 0.000042),
177 (291.34, 1.844379, 0.0, 0.000040),
178 (161.72, 24.198154, 0.0, 0.000037),
179 (239.56, 25.513099, 0.0, 0.000035),
180 (331.55, 3.592518, 0.0, 0.000023),
181 ];
182 for (base, per_lunation, per_t2, amplitude) in planetary {
183 correction += amplitude * rad(base + per_lunation * k + per_t2 * t2).sin();
184 }
185 let jde = jde + correction;
186 let year = 2000.0 + (jde - 2_451_545.0) / GREGORIAN_YEAR_DAYS;
187 jde - delta_t(year) / 86_400.0
188}
189
190pub fn lunar_phases(from: DateTime<Utc>, to: DateTime<Utc>) -> Vec<LunarPhase> {
193 let from = from.max(Utc.with_ymd_and_hms(1900, 1, 1, 0, 0, 0).unwrap());
194 let to = to.min(Utc.with_ymd_and_hms(2100, 1, 1, 0, 0, 0).unwrap());
195 if from >= to {
197 return Vec::new();
198 }
199 let at = |ms: f64| Utc.timestamp_millis_opt(ms.round() as i64).unwrap();
200 let year = 1970.0 + from.timestamp() as f64 / 86_400.0 / GREGORIAN_YEAR_DAYS;
202 let mut half = ((year - 2000.0) * 12.3685 * 2.0).floor() as i64 - 2;
203 let mut phases = Vec::new();
204 loop {
205 let jd = phase_jd(half as f64 / 2.0);
206 let time = at((jd - UNIX_EPOCH_JD) * 86_400_000.0);
207 if time >= to {
208 return phases;
209 }
210 if time >= from {
211 let phase = if half % 2 == 0 {
212 MoonPhase::New
213 } else {
214 MoonPhase::Full
215 };
216 phases.push(LunarPhase { phase, time });
217 }
218 half += 1;
219 }
220}
221
222#[derive(Clone)]
225pub struct LunarPhases {
226 values: IndicatorValues,
227 new_color: Color32,
228 full_color: Color32,
229 visible: bool,
230}
231
232impl LunarPhases {
233 pub fn new() -> Self {
235 Self {
236 values: IndicatorValues::new(),
237 new_color: palette::BLUE.gamma_multiply_u8(128),
238 full_color: palette::WHITE.gamma_multiply_u8(128),
239 visible: true,
240 }
241 }
242
243 fn compute(data: &[Bar]) -> IndicatorValues {
245 let mut values = IndicatorValues::missing(data.len());
246 if !data.is_empty() {
247 values.set_value(1, 0, f64::NAN);
249 }
250 let [.., before, last] = data else {
251 return values;
252 };
253 let end = last.time + (last.time - before.time);
254 for LunarPhase { phase, time } in lunar_phases(data[0].time, end) {
255 let bar = data.partition_point(|b| b.time <= time) - 1;
256 let (line, price) = match phase {
257 MoonPhase::New => (0, data[bar].low),
258 MoonPhase::Full => (1, data[bar].high),
259 };
260 if values.value(line, bar).is_none() {
261 values.set_value(line, bar, price);
262 }
263 }
264 values
265 }
266}
267
268impl Default for LunarPhases {
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274impl Indicator for LunarPhases {
275 fn name(&self) -> &str {
276 "Lunar Phases"
277 }
278
279 fn desc(&self) -> &str {
280 "Lunar Phases - New and full moons marked on the bars that hold them"
281 }
282
283 fn calculate(&mut self, data: &[Bar]) {
284 self.update(data, 0);
285 }
286
287 fn update(&mut self, data: &[Bar], from: usize) {
288 let mut values = std::mem::take(&mut self.values);
289 let finite = |b: &Bar| b.high.is_finite() && b.low.is_finite();
290 let from = usable_behind(1, finite)(data, from.min(values.len()).min(data.len()));
293 update_window(&mut values, data, from, usable_behind(1, finite), |data| {
294 over_finite_bars(data, finite, Self::compute)
295 });
296 self.values = values;
297 }
298
299 fn is_incremental(&self) -> bool {
300 true
301 }
302
303 fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
304 crate::studies::PlotStyle {
305 color: if line == 0 {
306 self.new_color
307 } else {
308 self.full_color
309 },
310 width: 2.0,
311 pattern: crate::model::LineStyle::Solid,
312 plot_type: crate::studies::PlotType::Circles,
313 visible: true,
314 }
315 }
316
317 fn plot_text(&self, line: usize, bar: usize) -> Option<String> {
318 self.values
319 .value(line, bar)
320 .map(|_| if line == 0 { "New" } else { "Full" }.to_owned())
321 }
322
323 fn values(&self) -> &IndicatorValues {
324 &self.values
325 }
326
327 fn colors(&self) -> Vec<Color32> {
328 vec![self.new_color, self.full_color]
329 }
330
331 fn set_colors(&mut self, colors: Vec<Color32>) {
332 if let Some(&color) = colors.first() {
333 self.new_color = color;
334 }
335 if let Some(&color) = colors.get(1) {
336 self.full_color = color;
337 }
338 }
339
340 fn is_visible(&self) -> bool {
341 self.visible
342 }
343
344 fn set_visible(&mut self, visible: bool) {
345 self.visible = visible;
346 }
347
348 fn clone_box(&self) -> Box<dyn Indicator> {
349 Box::new(self.clone())
350 }
351
352 fn line_names(&self) -> Vec<String> {
353 vec!["New Moon".to_string(), "Full Moon".to_string()]
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use chrono::Datelike;
361
362 fn utc(y: i32, mo: u32, d: u32, h: u32, mi: u32) -> DateTime<Utc> {
363 Utc.with_ymd_and_hms(y, mo, d, h, mi, 0).unwrap()
364 }
365
366 #[test]
370 fn phases_match_published_times() {
371 use MoonPhase::{Full, New};
372 let published = [
373 (New, utc(1977, 2, 18, 3, 36)),
374 (New, utc(2000, 1, 6, 18, 14)),
375 (Full, utc(2000, 1, 21, 4, 40)),
376 (New, utc(2024, 1, 11, 11, 57)),
377 (Full, utc(2024, 1, 25, 17, 54)),
378 (Full, utc(2024, 3, 25, 7, 0)),
379 (New, utc(2024, 4, 8, 18, 21)),
380 (Full, utc(2024, 4, 23, 23, 49)),
381 ];
382 for (phase, expected) in published {
383 let found = lunar_phases(
384 expected - chrono::Duration::days(1),
385 expected + chrono::Duration::days(1),
386 );
387 assert_eq!(found.len(), 1, "{expected}");
388 assert_eq!(found[0].phase, phase, "{expected}");
389 assert!(
391 (found[0].time - expected).num_seconds().abs() <= 60,
392 "{expected}"
393 );
394 }
395 let cut = utc(2024, 2, 10, 0, 0);
397 let mut split = lunar_phases(utc(2024, 1, 1, 0, 0), cut);
398 split.extend(lunar_phases(cut, utc(2024, 4, 1, 0, 0)));
399 assert_eq!(
400 split,
401 lunar_phases(utc(2024, 1, 1, 0, 0), utc(2024, 4, 1, 0, 0))
402 );
403 assert!(lunar_phases(utc(1899, 1, 1, 0, 0), utc(1900, 1, 1, 0, 0)).is_empty());
404 assert!(lunar_phases(utc(2100, 1, 1, 0, 0), utc(2101, 1, 1, 0, 0)).is_empty());
405 assert!(lunar_phases(DateTime::<Utc>::MAX_UTC, DateTime::<Utc>::MAX_UTC).is_empty());
406 }
407
408 #[test]
412 fn study_marks_the_bar_holding_each_phase_once() {
413 let bars: Vec<Bar> = (0..24)
416 .map(|i| utc(2024, 1, 8, 0, 0) + chrono::Duration::days(i))
417 .filter(|t| t.weekday().number_from_monday() <= 5)
418 .map(|t| Bar::new(t, 10.0, 12.0, 8.0, 11.0, 1.0))
419 .collect();
420 let mut study = LunarPhases::new();
421 study.calculate(&bars);
422 let marked =
423 |study: &LunarPhases, bars: &[Bar], line: usize| -> Vec<(DateTime<Utc>, f64)> {
424 bars.iter()
425 .enumerate()
426 .filter_map(|(i, b)| study.values().value(line, i).map(|v| (b.time, v)))
427 .collect()
428 };
429 assert_eq!(marked(&study, &bars, 0), [(utc(2024, 1, 11, 0, 0), 8.0)]);
430 assert_eq!(marked(&study, &bars, 1), [(utc(2024, 1, 25, 0, 0), 12.0)]);
431 assert_eq!(study.plot_text(1, 13).as_deref(), Some("Full"));
432 let mut tail = LunarPhases::new();
433 tail.calculate(&bars[2..]);
434 for line in 0..2 {
435 assert_eq!(marked(&tail, &bars[2..], line), marked(&study, &bars, line));
436 }
437 let mut longer = bars.clone();
439 longer.push(Bar::new(utc(2024, 2, 1, 0, 0), 10.0, 12.0, 8.0, 11.0, 1.0));
440 study.update(&longer, bars.len());
441 let mut fresh = LunarPhases::new();
442 fresh.calculate(&longer);
443 for line in 0..2 {
444 assert_eq!(marked(&study, &longer, line), marked(&fresh, &longer, line));
445 }
446 }
447}