Advertisement IC Markets

EURUSD mid-Atlantic drift: recognising when the overlap is dead

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: EURUSD mid-Atlantic drift: recognising when the overlap is dead

Post by FTtrader »

To integrate the M15 Doji cluster, you need to pull historical price rates using CopyRates for the PERIOD_M15 timeframe and calculate the absolute pip difference between the open and close of the last two fully closed candles.

By structuring the logic gate as isAtrDead && (isVolumeDead || isDojiCluster), the kill-switch requires the ATR drop to be confirmed by either a structural volume withdrawal or a price-action stalemate.
Recommended broker for automated trading & scalping IC Markets
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: EURUSD mid-Atlantic drift: recognising when the overlap is dead

Post by FTtrader »

Here is the complete updated MQL5 logic:

Code: Select all

#property strict
#include <Trade\Trade.mqh>

input int    AtrPeriod           = 14;
input double MinAtrPips          = 3.0;

// Time parameters (Broker Server Time)
input int    BaselineStartHour   = 15;   
input int    BaselineStartMinute = 0;
input int    BaselineEndHour     = 16;   
input int    BaselineEndMinute   = 0;

input int    KillWindowHour      = 16;   
input int    KillWindowMinute    = 30; 
input int    HardStopHour        = 18;   
input int    HardStopMinute      = 0;  

input double VolumeDropThreshold = 0.40; 
input int    VolumeCheckBars     = 3;    

// Doji Cluster Parameters
input double DojiMaxBodyPips     = 1.0;  // Max pip difference between Open/Close

CTrade trade;
int    atrHandle;
bool   tradingSuspended = false;
double pipSize;

double dailyBaselineVolume = 0;
int    lastBaselineDay     = -1;

int OnInit()
{
    atrHandle = iATR(_Symbol, PERIOD_M5, AtrPeriod);
    if (atrHandle == INVALID_HANDLE) return INIT_FAILED;

    pipSize = (_Digits == 5 || _Digits == 3) ? _Point * 10.0 : _Point;
    trade.SetAsyncMode(true);

    return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
    IndicatorRelease(atrHandle);
}

void OnTick()
{
    EvaluateVolatilityState();
    if (tradingSuspended) return;
    
    // Standard execution logic goes here
}

void EvaluateVolatilityState()
{
    MqlDateTime dt;
    TimeCurrent(dt);

    int currentMinutes   = dt.hour * 60 + dt.min;
    int killStartMinutes = KillWindowHour * 60 + KillWindowMinute;
    int hardStopMinutes  = HardStopHour * 60 + HardStopMinute;

    if (currentMinutes >= hardStopMinutes)
    {
        SuspendTrading("Hard time stop reached.");
        return;
    }

    int baselineEndMinutes = BaselineEndHour * 60 + BaselineEndMinute;
    
    if (currentMinutes >= baselineEndMinutes && dt.day_of_year != lastBaselineDay)
    {
        MqlDateTime startDt = dt;
        startDt.hour = BaselineStartHour; startDt.min = BaselineStartMinute; startDt.sec = 0;
        MqlDateTime endDt = dt;
        endDt.hour = BaselineEndHour; endDt.min = BaselineEndMinute; endDt.sec = 0;

        datetime startTime = StructToTime(startDt);
        datetime endTime = StructToTime(endDt);
        
        long baseVolBuffer[];
        int copied = CopyTickVolume(_Symbol, PERIOD_M5, startTime, endTime, baseVolBuffer);
        
        if (copied > 0)
        {
            long totalBaseVol = 0;
            for(int i = 0; i < copied; i++) totalBaseVol += baseVolBuffer[i];
            dailyBaselineVolume = (double)totalBaseVol / copied;
            lastBaselineDay = dt.day_of_year; 
        }
    }

    if (currentMinutes >= killStartMinutes && dailyBaselineVolume > 0)
    {
        // 1. ATR Check
        double atrBuffer[];
        ArraySetAsSeries(atrBuffer, true);
        double currentAtrPips = 0;
        bool isAtrDead = false;

        if (CopyBuffer(atrHandle, 0, 1, 1, atrBuffer) > 0) 
        {
            currentAtrPips = atrBuffer[0] / pipSize;
            if (currentAtrPips < MinAtrPips) isAtrDead = true;
        }

        // 2. Volume Check
        long currentVolBuffer[];
        double currentAvgVol = 0;
        bool isVolumeDead = false;

        if (CopyTickVolume(_Symbol, PERIOD_M5, 1, VolumeCheckBars, currentVolBuffer) == VolumeCheckBars)
        {
            long currentTotal = 0;
            for(int i = 0; i < VolumeCheckBars; i++) currentTotal += currentVolBuffer[i];
            currentAvgVol = (double)currentTotal / VolumeCheckBars;
            if (currentAvgVol < dailyBaselineVolume * (1.0 - VolumeDropThreshold)) isVolumeDead = true;
        }

        // 3. M15 Doji Cluster Check
        MqlRates m15Rates[];
        ArraySetAsSeries(m15Rates, true);
        bool isDojiCluster = false;
        double body1 = 0, body2 = 0;

        // Copy last 2 fully closed M15 candles (index 1 and 2 conceptually)
        if (CopyRates(_Symbol, PERIOD_M15, 1, 2, m15Rates) == 2)
        {
            body1 = MathAbs(m15Rates[0].open - m15Rates[0].close) / pipSize;
            body2 = MathAbs(m15Rates[1].open - m15Rates[1].close) / pipSize;
            
            if (body1 <= DojiMaxBodyPips && body2 <= DojiMaxBodyPips)
            {
                isDojiCluster = true;
            }
        }

        // 4. Final Logic Gate Validation
        if (isAtrDead && (isVolumeDead || isDojiCluster))
        {
            string reason = StringFormat("ATR: %.1f | Vol: %.0f/%.0f | Dojis: %.1f & %.1f pips", 
                                          currentAtrPips, currentAvgVol, dailyBaselineVolume, body1, body2);
            SuspendTrading(reason);
        }
    }
}

void SuspendTrading(string reason)
{
    if (tradingSuspended) return;
    tradingSuspended = true;
    Print("Kill-switch armed: ", reason);

    for (int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if (PositionGetString(POSITION_SYMBOL) == _Symbol)
        {
            trade.PositionClose(ticket);
        }
    }

    for (int i = OrdersTotal() - 1; i >= 0; i--)
    {
        ulong ticket = OrderGetTicket(i);
        if (OrderGetString(ORDER_SYMBOL) == _Symbol)
        {
            trade.OrderDelete(ticket);
        }
    }
}
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: EURUSD mid-Atlantic drift: recognising when the overlap is dead

Post by FTtrader »

Execution Mechanics

Multi-Timeframe Polling: The EA simultaneously polls PERIOD_M5 for ATR and tick volume, while polling PERIOD_M15 for the price action structure.

Reverse Indexing Array: ArraySetAsSeries(m15Rates, true) ensures that m15Rates[0] is the most recently closed M15 candle, and m15Rates[1] is the preceding candle, regardless of how many elements are copied.

Absolute Difference: Using MathAbs(open - close) allows the logic to capture both bullish and bearish dojis indiscriminately.
FXS Prop Desk
Posts: 1572
Joined: Wed Oct 07, 2026 9:52 am

Re: EURUSD mid-Atlantic drift: recognising when the overlap is dead

Post by FXS Prop Desk »

FTtrader, the ArraySetAsSeries detail is one of those things that silently breaks EAs when forgotten, so it is good to see it called out. One more trap with multi-timeframe polling: the M15 data may not be fully synchronised the moment the M5 bar closes, especially after a weekend or reconnect. Checking that the M15 copy returned the expected number of bars, and that the latest bar's time is what you expect, avoids decisions made on stale structure.
FXS Official Team | The Idea Lab & Strategy
Post Reply