SPB Git

spb/wp40_uqo

Public MIT
7.9 KB · 163 lines cpp
1#include "raid/data/calendar.hpp"2#include "raid/core/error.hpp"3#include <algorithm>45namespace raid {67namespace {8// nth (1-based) weekday (0=Sun..6=Sat) of a month; n = -1 → last.9Date nth_weekday(int y, unsigned m, int wd, int n) {10  if (n > 0) {11    Date first = Date::from_ymd(y, m, 1);12    int delta = (wd - first.weekday() + 7) % 7;13    return first + (delta + 7 * (n - 1));14  }15  // last16  const unsigned nm = m == 12 ? 1 : m + 1; const int ny = m == 12 ? y + 1 : y;17  Date last = Date::from_ymd(ny, nm, 1) - 1;18  int delta = (last.weekday() - wd + 7) % 7;19  return last - delta;20}21// Western Easter (Anonymous Gregorian algorithm)22Date easter(int y) {23  const int a = y % 19, b = y / 100, c = y % 100, d = b / 4, e = b % 4, f = (b + 8) / 25, g = (b - f + 1) / 3;24  const int h = (19 * a + b - d - g + 15) % 30, i = c / 4, k = c % 4, l = (32 + 2 * e + 2 * i - h - k) % 7;25  const int m = (a + 11 * h + 22 * l) / 451;26  const int month = (h + l - 7 * m + 114) / 31, day = ((h + l - 7 * m + 114) % 31) + 1;27  return Date::from_ymd(y, static_cast<unsigned>(month), static_cast<unsigned>(day));28}29// Observed date of a fixed holiday: Saturday → Friday, Sunday → Monday. NYSE rule: if Saturday, the preceding Friday is a30// holiday unless that Friday is the last trading day of the month/year (then no day off) — for New Year's Day on a31// Saturday the exchange does NOT close on Friday Dec 31.32std::optional<Date> observed(int y, unsigned m, unsigned d, bool is_new_year) {33  Date dt = Date::from_ymd(y, m, d);34  const int wd = dt.weekday();35  if (wd == 6) { if (is_new_year) return std::nullopt; return dt - 1; }36  if (wd == 0) return dt + 1;37  return dt;38}39struct Holiday { Date date; const char* name; };40std::vector<Holiday> holidays_for_year(int y) {41  std::vector<Holiday> h;42  if (auto d = observed(y, 1, 1, true)) h.push_back({*d, "New Year's Day"});43  h.push_back({nth_weekday(y, 1, 1, 3), "Martin Luther King Jr. Day"});   // since 199844  h.push_back({nth_weekday(y, 2, 1, 3), "Presidents' Day"});45  h.push_back({easter(y) - 2, "Good Friday"});46  h.push_back({nth_weekday(y, 5, 1, -1), "Memorial Day"});47  if (y >= 2022) if (auto d = observed(y, 6, 19, false)) h.push_back({*d, "Juneteenth"});48  if (auto d = observed(y, 7, 4, false)) h.push_back({*d, "Independence Day"});49  h.push_back({nth_weekday(y, 9, 1, 1), "Labor Day"});50  h.push_back({nth_weekday(y, 11, 4, 4), "Thanksgiving Day"});51  if (auto d = observed(y, 12, 25, false)) h.push_back({*d, "Christmas Day"});52  // Special closures (NYSE), 2000–202653  static const struct { int y; unsigned m, d; const char* n; } specials[] = {54    {2001, 9, 11, "September 11 attacks"}, {2001, 9, 12, "September 11 attacks"}, {2001, 9, 13, "September 11 attacks"}, {2001, 9, 14, "September 11 attacks"},55    {2004, 6, 11, "Reagan funeral"}, {2007, 1, 2, "Ford funeral"}, {2012, 10, 29, "Hurricane Sandy"}, {2012, 10, 30, "Hurricane Sandy"},56    {2018, 12, 5, "G.H.W. Bush funeral"}, {2025, 1, 9, "Carter funeral"},57  };58  for (const auto& s : specials) if (s.y == y) h.push_back({Date::from_ymd(s.y, s.m, s.d), s.n});59  return h;60}61}  // namespace6263std::optional<std::string> MarketCalendar::holiday_name(Date d) const {64  const int wd = d.weekday();65  if (wd == 0) return "Sunday";66  if (wd == 6) return "Saturday";67  for (const auto& h : holidays_for_year(d.ymd().year)) if (h.date == d) return h.name;68  return std::nullopt;69}7071bool MarketCalendar::is_trading_day(Date d) const { return !holiday_name(d).has_value(); }7273bool MarketCalendar::is_early_close(Date d) const {74  if (!is_trading_day(d)) return false;75  const auto [y, m, dd] = d.ymd();76  // Historical one-offs (NYSE), verified against exchange_calendars 4.13 XNYS 2000–2026:77  if (d == Date::from_ymd(2002, 7, 5) || d == Date::from_ymd(2003, 12, 26)) return true;   // Friday after a Thursday holiday78  if (d == Date::from_ymd(2002, 7, 3)) return false;                                         // full day in 200279  // Day after Thanksgiving80  if (d == nth_weekday(y, 11, 4, 4) + 1) return true;81  // Christmas Eve when it is a weekday (Mon–Fri) and a trading day; since 1993 rule, 13:00 close82  if (m == 12 && dd == 24 && d.weekday() >= 1 && d.weekday() <= 5) return true;83  // July 3 when July 4 is a weekday (Tue–Fri) — 13:00 close (rule in force since 1995 roughly; NYSE: when the 4th is a weekday)84  if (m == 7 && dd == 3 && d.weekday() >= 1 && d.weekday() <= 5) {85    const int wd4 = Date::from_ymd(y, 7, 4).weekday();86    if (wd4 >= 2 && wd4 <= 5) return true;87  }88  return false;89}9091i64 MarketCalendar::new_york_offset(Timestamp utc) {92  // Determine year from UTC date (safe: transitions are in March/November, far from year boundaries)93  const i64 days = utc.us >= 0 ? utc.us / 86'400'000'000LL : -((-utc.us + 86'400'000'000LL - 1) / 86'400'000'000LL);94  const int y = Date(static_cast<i32>(days)).ymd().year;95  Date start, end;96  if (y >= 2007) { start = nth_weekday(y, 3, 0, 2); end = nth_weekday(y, 11, 0, 1); }97  else { start = nth_weekday(y, 4, 0, 1); end = nth_weekday(y, 10, 0, -1); }98  // DST starts at 02:00 EST = 07:00 UTC; ends at 02:00 EDT = 06:00 UTC99  const Timestamp dst_start(static_cast<i64>(start.days) * 86'400'000'000LL + 7LL * 3'600'000'000LL);100  const Timestamp dst_end(static_cast<i64>(end.days) * 86'400'000'000LL + 6LL * 3'600'000'000LL);101  return (utc >= dst_start && utc < dst_end) ? -14400 : -18000;102}103104MarketCalendar::Local MarketCalendar::to_new_york(Timestamp utc) {105  const i64 off = new_york_offset(utc);106  const i64 local_us = utc.us + off * 1'000'000LL;107  i64 days = local_us / 86'400'000'000LL, rem = local_us % 86'400'000'000LL;108  if (rem < 0) { rem += 86'400'000'000LL; days -= 1; }109  return {Date(static_cast<i32>(days)), static_cast<int>(rem / 60'000'000LL), off};110}111112Timestamp MarketCalendar::from_new_york(Date d, int minute_of_day) {113  // First guess with the standard offset, then correct with the offset valid at that instant.114  const i64 naive = static_cast<i64>(d.days) * 86'400'000'000LL + static_cast<i64>(minute_of_day) * 60'000'000LL;115  Timestamp guess(naive + 18000LL * 1'000'000LL);116  const i64 off = new_york_offset(guess);117  Timestamp t(naive - off * 1'000'000LL);118  // re-check (handles instants near the transition)119  const i64 off2 = new_york_offset(t);120  if (off2 != off) t = Timestamp(naive - off2 * 1'000'000LL);121  return t;122}123124SessionBounds MarketCalendar::session(Date d) const {125  RAID_REQUIRE(is_trading_day(d), invalid_argument, d.iso() + " is not a trading day");126  SessionBounds s; s.session = d; s.early_close = is_early_close(d);127  s.open_utc = from_new_york(d, 9 * 60 + 30);128  s.close_utc = from_new_york(d, s.early_close ? 13 * 60 : 16 * 60);129  s.ext_open_utc = from_new_york(d, 4 * 60);130  s.ext_close_utc = from_new_york(d, s.early_close ? 17 * 60 : 20 * 60);131  s.utc_offset_seconds = new_york_offset(s.open_utc);132  return s;133}134135usize MarketCalendar::expected_regular_bars(Date d, Interval iv) const {136  const auto s = session(d);137  const i64 minutes = (s.close_utc - s.open_utc).us / 60'000'000LL;138  switch (iv) {139    case Interval::m1: return static_cast<usize>(minutes) + 1;      // 391 / 211, includes the closing print bar140    case Interval::m5: return static_cast<usize>(minutes / 5) + 1;  // 79 / 43141    case Interval::m30: return static_cast<usize>(minutes / 30) + 1;142    case Interval::h1: return static_cast<usize>(minutes / 60) + 1;143    case Interval::d1: return 1;144  }145  return 0;146}147148std::vector<Date> MarketCalendar::sessions(Date start, Date end) const {149  std::vector<Date> out;150  for (Date d = start; d <= end; d = d + 1) if (is_trading_day(d)) out.push_back(d);151  return out;152}153Date MarketCalendar::next_session(Date d) const { Date x = d + 1; while (!is_trading_day(x)) x = x + 1; return x; }154Date MarketCalendar::previous_session(Date d) const { Date x = d - 1; while (!is_trading_day(x)) x = x - 1; return x; }155156usize trading_days_between(const MarketCalendar& cal, Date a, Date b) {157  if (b < a) std::swap(a, b);158  usize n = 0; for (Date d = a + 1; d <= b; d = d + 1) if (cal.is_trading_day(d)) ++n;159  return n;160}161162}  // namespace raid163
Files · edf90b664f59d98f1489651599148df98a2fb1d1
Menu

Theme