Trading Days of the Month and other business day math

 

I wanted to try Larry Williams' Trading Day of Month strategies, but I had a hard time figuring out how to calculate the current trading day of the month accurately (basically similar in concept to business day calculations in Excel).  I could not find code on this site for it ... I am SURE someone has already done it, although I may not have used the right search terms.  So after searching around unsuccessfully, I made my own based on some C code on stackexchange.  While I was at it I made several other short-cut functions for my own work.  I am sharing it here with everyone in case others want to use it as a starting point.

My favorite functionalities here are:

  • Determine if a date is within a range of dates
  • Determine if a time is within a range of times, with support for times that bridge between two days (ie: 22:00 to 02:00)
  • Get first calendar date and last calendar date of a month
  • Get trading day of month for a given date
  • Get first trading day in a month, get last trading day in a month
  • Basic trading day addition/subtraction from a date...What is the resulting date when given an input date + trading days. For instance, 2022.12.01 + 7 trading days: 2022.12.12
Aside from the above, there are several other utility functions that get days, seconds, etc that allow for skipping the conversion to MqlDateTime for everything.  For instance, a call for hour:
// will return the hour from the current TimeLocal()

Hour();

// If you want the hour from another calculated datetime

Hour(your_calculated_datetime);

I use TimeLocal() in my code because my workstation is set to EST, and that's how I track my times, so my default value in most of the functions is to use TimeLocal() if no datetime is passed to the function. 

EDITED: Updated the functions somewhat to allow for:

Choice of times:  still defaults to TimeLocal(), but can easily select TimeCurrent, TimeTradeServer, TimeGMT

Concision (somewhat): reduced some redundancy, simplified some functions

Enjoy.  Happy New Year!

/*
   Various Time management extensions
*/
//
//
// datetime and int formats are good for the input
MqlDateTime ConvertTimeToStruct(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime datetime_struct;
   TimeToStruct(datetime_in, datetime_struct);
   return(datetime_struct);
  }
//
//
// the String Time for the datetime provided
string SecString(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   return(TimeToString(datetime_in,TIME_SECONDS));
  }
//
//
// the String Mins for the datetime provided
string MinString(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   return(TimeToString(datetime_in,TIME_MINUTES));
  }
//
//
// Takes a datetime and extracts just the HH:MM:SS portion to an integer (overload)
int SecInt(datetime datetime_in) {
   string sec_string = SecString(datetime_in);
   return(SecInt(sec_string));
  }
// Takes a VALIDATED "HH:MM:SS" string and converts it to an integer
int SecInt(string HHMMSS_in) {
   StringReplace(HHMMSS_in,":","");
   int sec_to_int = (int)StringToInteger(HHMMSS_in);
   return(sec_to_int);
  }
//
//
// Takes a datetime and extracts just the HH:MM portion to an integer (overload)
int MinInt(datetime datetime_in) {
   string min_string = MinString(datetime_in);
   return(MinInt(min_string));
  }
// Takes a VALIDATED "HH:MM" string and converts it to an integer
int MinInt(string HHMM_in) {
   StringReplace(HHMM_in,":","");
   int min_to_int = (int)StringToInteger(HHMM_in);
   return(min_to_int);
  }
//
//
// the hour for the datetime provided
int Hour(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   return(the_date.hour);
  }
//
//
// the minute for the date provided
int Min(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   return(the_date.min);
  }
//
//
// the second for the date provided
int Sec(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   return(the_date.sec);
  }
//
//
// the day of year 0 thru 364 for the date provided
int DayOfYear(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   return(the_date.day_of_year);
  }
//
//
// the day of week 0 thru 28,30,31 for the date provided
int DayOfMonth(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   return(the_date.day);
  }
//
//
// the day of week 0 thru 6 for the date provided
int DayOfWeek(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   return(the_date.day_of_week);
  }
//
//
// the calendar first day of the month for the date provided
datetime FirstDateOfMonth(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   datetime fom = (int)StringToTime(IntegerToString(the_date.year)+"."+IntegerToString(the_date.mon)+".01");
   return(fom);
  }
//
//
// the calendar last day of the month for the date provided
datetime LastDateOfMonth(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   datetime lom = (int)StringToTime(IntegerToString(the_date.year)+"."+IntegerToString(the_date.mon)+"."+IntegerToString(LengthOfMonth(datetime_in)));
   return(lom);
  }
//
//
// the calendar length of the month (in days) for the date provided
int LengthOfMonth(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   int the_month = the_date.mon;
   int the_year = the_date.year;

// per the post from: https://stackoverflow.com/questions/46704484/last-day-of-a-given-month

   if ( the_year < 1582 ) return(-1); /* Before this year the Gregorian Calendar was not defined */
   if ( the_month > 12 || the_month < 1 ) return(-1);
   if (the_month == 4 || the_month == 6 || the_month == 9 || the_month == 11) return(30);
   else if (the_month == 2) return (((the_year % 4 == 0 && the_year % 100 != 0) || (the_year % 400 == 0)) ? 29 : 28);

   return(31);
  }
//
//
// the date X trading days away
//    trading_days_diff can be positive or negative
datetime AddTradingDays(int trading_days_diff, datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   int first = (int)datetime_in;
   int day = 86400; // a day's worth of seconds
   int datetime_limit = (int)NormalizeDouble(first+(trading_days_diff*2*day),0);
   int i;

   if(trading_days_diff>0) {
      for(i = first; i <= datetime_limit; i = i+day) {
         if(TradingDays(datetime_in,i)==trading_days_diff) return((datetime)i);
        }
     }
   else if(trading_days_diff<0) {
      for(i = first; i >= datetime_limit; i = i-day) {
         if(TradingDays(i,datetime_in)== -trading_days_diff) return((datetime)i);
        }
     }
   return(-1);
  }
//
//
// the number of calendar days between the dates provided
int CalDays(datetime datetime_start, datetime datetime_end=0) {
   if(datetime_end <= 3) datetime_end = TimeSource((int)datetime_end);

   int first = (int)datetime_start;
   int c_days = -1;

   int day = 86400; // a day's worth of seconds
   for(int i = first; i <= datetime_end; i = i+day) {
      c_days++;
     }
   return(c_days);
  }
//
//
// the number of trading days between the dates provided
int TradingDays(datetime datetime_start, datetime datetime_end=0) {
   if(datetime_end <= 3) datetime_end = TimeSource((int)datetime_end);

   int first = (int)datetime_start;
   int t_days = -1;

   return(TDOMLoop(first, datetime_end, t_days));
  }
//
//
// the trading day of the month for the date provided
//    based on stackexchange post: https://codereview.stackexchange.com/questions/124762/getting-the-business-day-of-the-month
int TradingDayOfMonth(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   int first = (int)FirstDateOfMonth(datetime_in);
   int t_days = 0;
   datetime datetime_end = datetime_in;

   return(TDOMLoop(first, datetime_end, t_days));
  }
//
int TDOMLoop(int first_in, datetime datetime_end_in, int t_days_in) {
   int t_days = t_days_in;

   int day = 86400; // a day's worth of seconds
   for(int i = first_in; i <= datetime_end_in; i = i+day) {
      int dow = DayOfWeek(i);
      if(dow != 6 && dow != 0)
         t_days++;
     }
   return(t_days);
  }
//
//
// the date of the first trading day of the month for the date provided
datetime FirstTradingDateOfMonth(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);

   int first = (int)FirstDateOfMonth(datetime_in);
   int day = 86400; // a day's worth of seconds
   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   int limit_day = (int)StringToTime(IntegerToString(the_date.year) + "." + IntegerToString(the_date.mon) + ".08");

   for(int i = first; i<=limit_day; i = i+day) {
      if(TradingDayOfMonth(i)==1) return((datetime)i);
     }
   return(-1);
  }
//
//
// the date of the last trading day of the month for the date provided
datetime LastTradingDateOfMonth(datetime datetime_in=0) {
   if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);
   
   int last = (int)LastDateOfMonth(datetime_in);
   int day = 86400; // a day's worth of seconds
   MqlDateTime the_date = ConvertTimeToStruct(datetime_in);
   int limit_day = (int)StringToTime(IntegerToString(the_date.year) + "." + IntegerToString(the_date.mon) + ".21");

   for(int i = last; i>=limit_day; i = i-day) { // counting down from the end
      int dow = DayOfWeek(i);
      if(dow != 6 && dow != 0) return((datetime)i);
     }
   return(-1);
  }
//
//
// is the datetime within the rage specified by range_begin and range_end
bool DateInRange(datetime range_begin, datetime range_end, datetime test_time=0) {
   if(test_time <= 3) test_time = TimeSource((int)test_time);

   if(range_begin <= test_time && range_end >= test_time) return(true);

   return(false);
  }
//
//
// is the time within the range specified by datetimes range_begin and range_end (overload)
bool TimeInRange(datetime range_begin, datetime range_end, datetime test_time=0) {
   string rb_string = TimeToString(range_begin, TIME_MINUTES);
   string re_string = TimeToString(range_end, TIME_MINUTES);

   return(TimeInRange(rb_string, re_string, test_time));
  }
// is the time within the range specified by strings range_begin and range_end, compared to datetime test time
bool TimeInRange(string range_begin, string range_end, datetime test_time=0) {
   if(test_time <= 3) test_time = TimeSource((int)test_time);
     
   if(!ValidateHHMMFormat(range_begin) && !ValidateHHMMFormat(range_end)) return(false);

   int rb = MinInt(range_begin);
   int re = MinInt(range_end);
   int tt = MinInt(test_time);

   if(rb < re) {
      if(rb <= tt && re >= tt) return(true);
     }
   else if(rb > re) {
      int day_end = MinInt("23:59");
      int day_start = MinInt("00:00");

      if(rb <= tt && day_end >= tt) return(true);
      else if(day_start <= tt && re >= tt) return(true);
     }
   return(false);
  }
//
//
// This will validate times for HH:MM format
bool ValidateHHMMFormat(string test_time_in) {
   string time_arr[];
   string sep = ":";
   ushort u_sep = StringGetCharacter(sep,0);
   StringSplit(test_time_in,u_sep,time_arr);

   if(
      ArraySize(time_arr)==2 &&
      StringToInteger(time_arr[0]) >= 0 && StringToInteger(time_arr[0]) <=23 &&
      StringToInteger(time_arr[1]) >= 0 && StringToInteger(time_arr[1]) <=59
   ) return(true);

   Print(__FILE__, " ", __FUNCTION__,":TimeInRange Error: Time format not valid for function.  Must be in HH:MM format, input received: ", test_time_in);
   return(false);
  }
//
//
//
datetime TimeSource(int choice_in) {
   datetime time_source;
   switch(choice_in) {
      case 0:
         time_source = TimeLocal();
         break;
      case 1:
         time_source = TimeCurrent();
         break;
      case 2:
         time_source = TimeTradeServer();
         break;
      case 3:
         time_source = TimeGMT();
         break;
      default:
         time_source = TimeLocal();
         break;
     }
   return(time_source);
  }
While I tested compilation in MT4 and MT5 on my instances, I cannot vouch for it being bullet proof... only that I did some QA on it and I cannot find obvious errors.  But - if you find errors in it, please shout out and I will fix as best as I can. No promise that this is the most efficient code possible, I admit to some code duplication and clunky looking string management.  Otherwise, the usual disclaimers: you are responsible for your own trading, no promise of fitness for use or functionality in trading, blah blah, yadda yadda.  My apologies in advance if my coding is crap or if my naming conventions suck: I'm using MQL as my excuse to learn to code. I'm a level 2 n00b
 
  1.       if(dow != 6 && dow != 0) return((datetime)i);
    This fails as it depends on the timezone of the broker.

    Brokers use a variety of time zones. Their local time, with or without Daylight Savings Time (DST), London, UTC, London+2, UTC+2, NY+7.

    Only with NY+7 does the broker's 00:00 equals 5 PM ET and the start of a daily bar (and H4) is the start of a new FX day.

    GMT/BST brokers, means there is a 1 or 2 hour D1/H4 bar on Sunday (depending on NY DST), and a short Friday bar. (Problems with indicators based off bars.)

  2. Then there are market holiday (country and broker specific), requires knowledge of when your broker stops and starts (not necessary the same as the market.)
              "Free-of-Holes" Charts - MQL4 Articles (2006)
              No candle if open = close ? - MQL4 programming forum (2010)

 
William Roeder #:
  1. This fails as it depends on the timezone of the broker.
  2. Then there are market holiday (country and broker specific), requires knowledge of when your broker stops and starts (not necessary the same as the market.)
              "Free-of-Holes" Charts - MQL4 Articles (2006)
              No candle if open = close ? - MQL4 programming forum (2010)

Hi William, yes you are correct of course. Thank you for the important feedback.

  1. For this point, while researching possible approaches, I happened on Carl Schreiber/gooly's articles on similar matters:  
    https://www.mql5.com/en/articles/9929

    He provides some functionality and including his work may allow smoother methods to manage this (server times and daylight times). There might be a "dumb" but acceptable method if I pass a string with "exclusion days" to the function to allow for Sunday trading (right now, my trading preference is to avoid Sundays) 
    // exclusion days passed to functions...  eh, I think there must be a more elegant way.  pass a pipe sep string like "6" or "0|1|5|6" in the exclude variable to create the trading exclusion days
    //
    int TradingDayOfMonth(datetime datetime_in=0, string exclude="0|6") {
       if(datetime_in <= 3) datetime_in = TimeSource((int)datetime_in);
    
       int first = (int)FirstDateOfMonth(datetime_in);
       int t_days = 0;
       datetime datetime_end = datetime_in;
    
       return(TDOMLoop(first, datetime_end, t_days, exclude));
      }
    //
    //
    //
    int TDOMLoop(int first_in, datetime datetime_end_in, int t_days_in, string exclude="0|6") {
       int t_days = t_days_in;
    
       int day = 86400; // a day's worth of seconds
       for(int i = first_in; i <= datetime_end_in; i = i+day) {
          int dow = DayOfWeek(i);
          if(MemberOfSet(dow,exclude)!=1 && MemberOfSet(dow,exclude)!=-1)
             t_days++;
         }
       return(t_days);
      }
    //
    //
    //
    int MemberOfSet(int this_value_in, string ints_as_string_in) {
       string set_arr[];
       string sep = "|";
       ushort u_sep = StringGetCharacter(sep,0);
       StringSplit(ints_as_string_in,u_sep,set_arr);
    
       if(ArraySize(set_arr)<1) {
          Print("Error: weekend exclusion days are blank");
          return(-1);
         }
    // 0-6 are chars 48-54
       for(int i = 0; i < ArraySize(set_arr); i++) {
          int test = (StringGetCharacter(set_arr[i],0)>=48 && StringGetCharacter(set_arr[i],0) <=54) ? (int)StringToInteger(set_arr[i]) : -1;
          if(test < 0 || test > 6) {
             Print("Error: Numbers outside of weekdays range (0 through 6), or wrong character");
             return(-1);
            }
          if(this_value_in==test) return(1); // a member of the set
         }
       return(0); // not a member of the set
      }
  2. Yes, holidays.  I have some coarse management for holidays elsewhere in my code (somewhat outside the scope of this exercise above, but compatible.)  It works, but I am not completely happy with what I have.  There is this article from Dennis K, which looks interesting and may provide some better guidance,
    https://www.mql5.com/en/articles/9874

    But I am guessing that there will always be some gaps. 

Another main challenge for me is that I have a regular day-job too, and I am always pinched for time. There is also my "bigger" project, my own EAs I am developing for myself, I don't want to get too far afield of what's needed to complete them.  I must pick and choose very carefully what I work on next.

But, for sure I will have to look into solutions to both of your points in more detail.  I don't want my personal preferences to keep me naive on these issues.

Dealing with Time (Part 2): The Functions
Dealing with Time (Part 2): The Functions
  • www.mql5.com
Determing the broker offset and GMT automatically. Instead of asking the support of your broker, from whom you will probably receive an insufficient answer (who would be willing to explain a missing hour), we simply look ourselves how they time their prices in the weeks of the time changes — but not cumbersome by hand, we let a program do it — why do we have a PC after all.
 

By the way, William R - how do you get the color coding in the code section?

it would definitely make my code blocks easier to read, but I don't see a control for it.  I just used the general code styling

Files:
codestyle.PNG  11 kb
codecolor.PNG  2 kb
 
Talky_Zebra #: By the way, William R - how do you get the color coding in the code section? it would definitely make my code blocks easier to read, but I don't see a control for it.  I just used the general code styling

By using the built-in code pasting functionality instead of selecting the "Code" Style — [Alt-S] instead of [Ctrl-S].

 
Fernando Carreiro #:

By using the built-in code pasting functionality instead of selecting the "Code" Style — [Alt-S] instead of [Ctrl-S].

Fernando!  Simple but brilliant, thank you for saving me from eternal embarrassment!  :D

I have updated the code blocks to make it easier on the eyes.  Happy New Year!
 

Hi there, thank you for sharing this very handy set of time and date functions! As a fellow coder just getting into MQL, I really appreciate you posting your code and detailed explanations.

The trading day of month calculation is super useful. I've also struggled with trying to properly account for weekends and holidays when doing date math in MQL. Your loop approach to iterate day by day makes total sense.

Some other features I like are the easy TimeLocal vs TimeGMT switching, date range checking, and first/last day of month. Lots of practical usage here!

I tested out a few scenarios and didn't catch any obvious bugs. The logic looks solid. Some minor suggestions:

  • Add comments above each function explaining its purpose
  • Use more descriptive parameter names like start_date and end_date instead of datetime_start etc.
  • Consider saving into an include file to easily calendar across EAs
 
helin avsar #:

Hi there, thank you for sharing this very handy set of time and date functions! As a fellow coder just getting into MQL, I really appreciate you posting your code and detailed explanations.

The trading day of month calculation is super useful. I've also struggled with trying to properly account for weekends and holidays when doing date math in MQL. Your loop approach to iterate day by day makes total sense.

Some other features I like are the easy TimeLocal vs TimeGMT switching, date range checking, and first/last day of month. Lots of practical usage here!

I tested out a few scenarios and didn't catch any obvious bugs. The logic looks solid. Some minor suggestions:

  • Add comments above each function explaining its purpose
  • Use more descriptive parameter names like start_date and end_date instead of datetime_start etc.
  • Consider saving into an include file to easily calendar across EAs

Hi helin, I am very glad you found them useful.  I don't often get the chance to help others on the forum.

I have made some mods to these and improved readability.  But yes - for my own use, they live in a separate include file, and I often update my own comments (to remind myself what I did the last time... months ago!) and I have changed some of the param/variable names several times to help myself.  It's a learning process for me.  :)

Feel free to modify them for your uses as you will.  I am not a pro coder, and I will not be updating these as a library for others to use.  I just shared them for folks like you to pick them up if helpful.
Reason: