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.
EURUSD mid-Atlantic drift: recognising when the overlap is dead
Re: EURUSD mid-Atlantic drift: recognising when the overlap is dead
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);
}
}
}Re: EURUSD mid-Atlantic drift: recognising when the overlap is dead
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.
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
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