Discussing the article: "Decoding Opening Range Breakout Intraday Trading Strategies"

 

Check out the new article: Decoding Opening Range Breakout Intraday Trading Strategies.

Opening Range Breakout (ORB) strategies are built on the idea that the initial trading range established shortly after the market opens reflects significant price levels where buyers and sellers agree on value. By identifying breakouts above or below a certain range, traders can capitalize on the momentum that often follows as the market direction becomes clearer. In this article, we will explore three ORB strategies adapted from the Concretum Group.

Opening Range Breakout (ORB) strategies are built on the idea that the initial trading range established shortly after the market opens reflects significant price levels where buyers and sellers agree on value. By identifying breakouts above or below a certain range, traders can capitalize on the momentum that often follows as the market direction becomes clearer. 

In this article, we will explore three ORB strategies adapted from the Concretum Group papers. First, we will cover the research background, including key concepts and the methodology used. Then, for each strategy, we will explain how they work, list their signal rules, and analyze their performance statistically. Finally, we will examine them from a portfolio perspective, focusing on the topic of diversification.  

This article will not dive deep into programming but instead concentrates on the research process, including recreating, analyzing, and testing the strategies from these three papers. This will be suitable for readers looking for potential trading edges or who are curious about how these strategies were studied and replicated. Nevertheless, all the MQL5 code for the EAs will be disclosed. Readers are welcome to build upon the framework by themselves.

Author: Zhuo Kai Chen

 
Great article 

Well worth for any trader pursuing

Speaking from my own experience 
 
Timmy T #:
Great article 

Well worth for any trader pursuing

Speaking from my own experience 

Thank you Timmy.

 

Indeed great article, appreciate the time and effort you have invested in writing the code and share it with the community.

Got a simple question around "In the studies we are adapting, they focus on strategies that trade between market open and close (9:30 AM to 4:00 PM Eastern Time). Since our broker uses UTC+2/3, this translates to 18:30-24:00 server time—make sure to adjust for your own broker's time zone when testing"

Are you able to walk me through your thought process on the time conversion? am clearly missing something here..

Using a generic time converter - my conversion of 09:30 Eastern Time ends up being 16:30 for when mt5 is in GMT+2 and 17:30 for when mt5 is in GMT+3. 18:30 feels like it is 1-2 hours post market open.

Appreciate the assistance & thanks again..

Files:
NY_GMT.png  12 kb
 
Sunjoo #:

Indeed great article, appreciate the time and effort you have invested in writing the code and share it with the community.

Got a simple question around "In the studies we are adapting, they focus on strategies that trade between market open and close (9:30 AM to 4:00 PM Eastern Time). Since our broker uses UTC+2/3, this translates to 18:30-24:00 server time—make sure to adjust for your own broker's time zone when testing"

Are you able to walk me through your thought process on the time conversion? am clearly missing something here..

Using a generic time converter - my conversion of 09:30 Eastern Time ends up being 16:30 for when mt5 is in GMT+2 and 17:30 for when mt5 is in GMT+3. 18:30 feels like it is 1-2 hours post market open.

Appreciate the assistance & thanks again..

You are right, I wrongly converted the server time in the article for the New York stock market open time. It should be 17:30 instead of 18:30. With that being said, you can assume all my strategy rules in the article should be trading 1 hour after the market open. Thanks for pointing it out and sorry for the confusion.

 

Thanks for posting, very interesting!

A question about the code:

In your MarketOpened function you use:

"if (currentHour >= startHour && currentMinute>=startMinute)return true;"  <- This looks like it will trade only part of the hour if the market is open, since it will return false if you're at the beginning of every hour, even when the market is open.  It only works if minute is at 0, which is not the start of the market session.

 
Digitus #:

Thanks for posting, very interesting!

A question about the code:

In your MarketOpened function you use:

"if (currentHour >= startHour && currentMinute>=startMinute)return true;"  <- This looks like it will trade only part of the hour if the market is open, since it will return false if you're at the beginning of every hour, even when the market is open.  It only works if minute is at 0, which is not the start of the market session.

OMG, can't believe I missed this. It should be:

    if ((currentHour >= startHour &&currentMinute>=startMinute)||currentHour>startHour)return true;

I'm genuinely sorry for the careless mistake. Thanks for reading so carefully and pointing it out.

 

Ps. For ORB3, I hardcoded the time for the market open time to be 9:30. You can change it to these functions so that you can match the server time of New York market open time.

//+------------------------------------------------------------------+
//| Get the upper Concretum band value                               |
//+------------------------------------------------------------------+
double getUpperBand(int target_hour = 17, int target_min = 30) {
    // Get the time of the current bar
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // Find today's opening price at target time (e.g., 17:30 server time)
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_target_today = getBarShiftForTime(today_start, target_hour, target_min);
    if (bar_at_target_today < 0) return 0; // Return 0 if no target bar exists
    double open_target_today = iOpen(_Symbol, PERIOD_M1, bar_at_target_today);
    if (open_target_today == 0) return 0; // No valid price
    
    // Calculate sigma based on the past 14 days
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_target = getBarShiftForTime(day_start, target_hour, target_min);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_target < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist
        double open_target = iOpen(_Symbol, PERIOD_M1, bar_at_target);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_target == 0) continue; // Skip if no valid opening price
        double move = MathAbs(close_HHMM / open_target - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // Return 0 if no valid data
    double sigma = sum_moves / valid_days;
    
    // Calculate the upper band
    double upper_band = open_target_today * (1 + sigma);
    
    // Plot a blue dot at the upper band level
    string obj_name = "UpperBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, upper_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return upper_band;
}

//+------------------------------------------------------------------+
//| Get the lower Concretum band value                               |
//+------------------------------------------------------------------+
double getLowerBand(int target_hour = 17, int target_min = 30) {
    // Get the time of the current bar
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // Find today's opening price at target time (e.g., 17:30 server time)
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_target_today = getBarShiftForTime(today_start, target_hour, target_min);
    if (bar_at_target_today < 0) return 0; // Return 0 if no target bar exists
    double open_target_today = iOpen(_Symbol, PERIOD_M1, bar_at_target_today);
    if (open_target_today == 0) return 0; // No valid price
    
    // Calculate sigma based on the past 14 days
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_target = getBarShiftForTime(day_start, target_hour, target_min);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_target < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist
        double open_target = iOpen(_Symbol, PERIOD_M1, bar_at_target);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_target == 0) continue; // Skip if no valid opening price
        double move = MathAbs(close_HHMM / open_target - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // Return 0 if no valid data
    double sigma = sum_moves / valid_days;
    
    // Calculate the lower band
    double lower_band = open_target_today * (1 - sigma);
    
    // Plot a red dot at the lower band level
    string obj_name = "LowerBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, lower_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrRed);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return lower_band;
}

Changing the time of the calculation could be a way to optimize the strategy further. :)

 
Zhuo Kai Chen #:

OMG, can't believe I missed this. It should be:

I'm genuinely sorry for the careless mistake. Thanks for reading so carefully and pointing it out.

No worries, I already merged open and closed market into one function.

bool MarketState()
{
   MqlDateTime structTime;
   TimeCurrent(structTime);
   structTime.sec = 0;
   structTime.hour = startHour;
   structTime.min = startMinute; 
   datetime timeStart = StructToTime(structTime);
   structTime.hour = endHour;
   structTime.min = endMinute;   
   datetime timeEnd = StructToTime(structTime);
   if(TimeCurrent() >= timeStart && TimeCurrent() < timeEnd)return true;
   else return false;
}


One more thing:  You use broker OHLC data for backtesting, no delay. Those backtests seem a bit optimistic compared to backtests done on real tick data with random delay for slippage and requotes.

Thanks again for your efforts!

 
Digitus #:

No worries, I already merged open and closed market into one function.


One more thing:  You use broker OHLC data for backtesting, no delay. Those backtests seem a bit optimistic compared to backtests done on real tick data with random delay for slippage and requotes.

Thanks again for your efforts!

Well done on your modification! I have updated all the code in my Github.

For your concern, the trading logic occurs every new bar and does not involves tick movement. Besides, the average holding time is like a few hours, which I think won't make slippage significant problem. I would say very few brokers provide real tick data for more than 5 years, and 1 min OHLC is enough.