Japanese bank holidays and gotobi days in MQL5 without a calendar file (code)

If an EA needs Japanese bank holidays, for example to trade the Tokyo fixing on gotobi days or to stay out of thin Tokyo mornings, there is no need for a calendar file or WebRequest: the holidays follow fixed rules and can be computed. This is the code I use. It covers the equinox days for 1980–2099, substitute holidays, the "citizens' holiday" between two holidays, the 2019 enthronement days, the 2020 and 2021 Olympic moves and the bank year-end (31 December to 3 January). I checked it against the Cabinet Office lists for 2026 and 2027, and it matches day for day.

// Japanese bank holidays and gotobi days. All dates are Japanese dates (00:00 JST).
datetime JpDate(const int y, const int m, const int d)
{
   MqlDateTime s;
   ZeroMemory(s);
   s.year = y; s.mon = m; s.day = d;
   return StructToTime(s);
}

int JpDow(const datetime t) { MqlDateTime s; TimeToStruct(t, s); return s.day_of_week; }

// equinox days, valid for 1980-2099
int JpVernalDay(const int y)   { return (int)MathFloor(20.8431 + 0.242194 * (y - 1980) - MathFloor((y - 1980) / 4.0)); }
int JpAutumnalDay(const int y) { return (int)MathFloor(23.2488 + 0.242194 * (y - 1980) - MathFloor((y - 1980) / 4.0)); }

// a national holiday by its own rule (substitute and citizens' holidays are added in JpHoliday)
bool JpBaseHoliday(const int y, const int m, const int d)
{
   int dow = JpDow(JpDate(y, m, d));
   int nth = (d - 1) / 7 + 1;                             // 1st, 2nd, 3rd ... weekday of the month
   if(y == 2019 && ((m == 4 && d == 30) || (m == 5 && (d == 1 || d == 2)) || (m == 10 && d == 22))) return true;
   if(y == 2020 && ((m == 7 && (d == 23 || d == 24)) || (m == 8 && d == 10))) return true;   // Olympic moves
   if(y == 2021 && ((m == 7 && (d == 22 || d == 23)) || (m == 8 && d == 8))) return true;
   switch(m)
   {
      case 1:  return d == 1 || (dow == 1 && nth == 2);                       // New Year, Coming of Age Day
      case 2:  return d == 11 || (y >= 2020 && d == 23);                      // Foundation Day, Emperor's Birthday
      case 3:  return d == JpVernalDay(y);
      case 4:  return d == 29;                                                // Showa Day
      case 5:  return d == 3 || d == 4 || d == 5;
      case 7:  return y != 2020 && y != 2021 && dow == 1 && nth == 3;         // Marine Day
      case 8:  return y >= 2016 && y != 2020 && y != 2021 && d == 11;         // Mountain Day
      case 9:  return (dow == 1 && nth == 3) || d == JpAutumnalDay(y);        // Respect for the Aged, equinox
      case 10: return y != 2020 && y != 2021 && dow == 1 && nth == 2;         // Sports Day
      case 11: return d == 3 || d == 23;
      case 12: return y <= 2018 && d == 23;                                   // former Emperor's Birthday
   }
   return false;
}

bool JpHoliday(const datetime day)
{
   MqlDateTime s;
   TimeToStruct(day, s);
   if(JpBaseHoliday(s.year, s.mon, s.day)) return true;
   if(s.day_of_week == 0) return false;
   // substitute holiday: the first non-holiday after a holiday that fell on a Sunday
   for(datetime t = day - 86400; ; t -= 86400)
   {
      MqlDateTime p;
      TimeToStruct(t, p);
      if(!JpBaseHoliday(p.year, p.mon, p.day)) break;
      if(p.day_of_week == 0) return true;
   }
   // citizens' holiday: a weekday between two holidays
   MqlDateTime a, b;
   TimeToStruct(day - 86400, a);
   TimeToStruct(day + 86400, b);
   return JpBaseHoliday(a.year, a.mon, a.day) && JpBaseHoliday(b.year, b.mon, b.day);
}

bool JpBankHoliday(const datetime day)
{
   MqlDateTime s;
   TimeToStruct(day, s);
   if(s.day_of_week == 0 || s.day_of_week == 6) return true;
   if((s.mon == 12 && s.day == 31) || (s.mon == 1 && s.day <= 3)) return true;   // bank year-end
   return JpHoliday(day);
}

// gotobi: the 5th, 10th, 15th, 20th, 25th and the last day of the month,
// moved back to the previous business day when they fall on a bank holiday
bool JpIsGotobi(const datetime jst_time)
{
   datetime day = jst_time - jst_time % 86400;
   if(JpBankHoliday(day)) return false;
   MqlDateTime s;
   TimeToStruct(day, s);
   int ny = (s.mon == 12) ? s.year + 1 : s.year, nm = (s.mon == 12) ? 1 : s.mon + 1;
   int last = (int)((JpDate(ny, nm, 1) - JpDate(s.year, s.mon, 1)) / 86400);
   int days[6] = {5, 10, 15, 20, 25, 0};
   days[5] = last;
   for(int i = 0; i < 6; i++)
   {
      datetime g = JpDate(s.year, s.mon, days[i]);
      for(int k = 0; k < 10 && JpBankHoliday(g); k++) g -= 86400;
      if(g == day) return true;
   }
   return false;
}

datetime JpNextGotobi(const datetime jst_time)
{
   datetime d = jst_time - jst_time % 86400;
   for(int i = 0; i < 40; i++, d += 86400)
      if(JpIsGotobi(d)) return d;
   return 0;
}

Notes:

  • All dates are Japanese dates (00:00 JST). Live, the Japanese time is TimeGMT() + 9 hours. In the Strategy Tester TimeGMT() follows the server time, so there you need the broker's GMT offset to convert server time to JST.
  • Gotobi days are the 5th, 10th, 15th, 20th, 25th and the last day of the month. When one falls on a bank holiday, payments are settled on the previous business day, so the function moves it back.
  • The equinox days use the usual approximation. The exact dates are announced in February of the year before, so it is worth checking each new year against the official list.
  • A new or moved holiday, like the 2019 and 2020–2021 changes, still needs a code change.

If you find a date where this disagrees with the official list, please post it here.