preview
Implementing a Daily Loss Limit and Drawdown Circuit Breaker in MQL5

Implementing a Daily Loss Limit and Drawdown Circuit Breaker in MQL5

MetaTrader 5Trading |
275 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Introduction

Prop‑firm and other risk‑managed accounts operate under a simple but strict rule: exceed a fixed daily loss and the account is closed or flagged. In practice, many Expert Advisors fail to enforce that rule reliably because they:

  • check the limit only on new bars (not every tick),
  • sum only realized P&L and ignore floating P&L (including swap),
  • have no real gate that prevents new orders once the limit is breached.

The result is an EA that appears compliant in logs yet lets the account slip past the allowable loss in real time. One boundary is fixed from the outset: the module you are about to build operates within a single EA on a single chart. It cannot see or block another EA's orders, and it cannot intercept a manual order placed through the terminal.

This article defines a clear engineering requirement and delivers a reproducible solution: a drop‑in MQL5 module that, on every OnTick(), computes the account's combined daily P&L (realized + floating, including swap) using the broker's server midnight as the reset boundary; compares it to a configured daily loss limit; and, if the limit is breached, immediately closes all open positions, cancels all pending orders, and places the system into a HALTED state until the next server midnight. The module exposes a small, testable public contract (Init(), OnTick(), IsHalted(), GetStatus(), ForceReset()) so you know exactly where and how to integrate it: call IsHalted() before every OrderSend() in your EA. The implementation includes a live chart dashboard and a verification script so the math and trigger logic are verifiable before you deploy.

Architecture of the circuit breaker

Architecture of the circuit breaker. The Expert Advisor drives CRiskCircuitBreaker through OnTick() and CircuitBreakerDashboard through OnTimer(). The breaker owns three components that read history and positions, close all positions, and cancel all orders. The dashboard renders straight to ChartSetString().


Section 1: Computing Combined Daily P&L

CDailyPnlCalculator answers one question every tick: how much has this account made or lost today, counting both what has already closed and what is still open. Getting this right depends on three decisions.

The first is the reset boundary. The calendar day resets at midnight server time, not local time. Server time is what the trade server uses to timestamp every deal, so anchoring the daily window to it is the only choice that stays consistent with the broker's own records. HistorySelect() takes a from and to timestamp and scopes the deal history cache to that window. The calculator computes midnight of the current server day and passes that as from, with ::TimeCurrent() as to.

The second decision is which deal types count toward realized P&L. Deal history includes an entry for each trade leg, including the entry deal that opens the position. Only DEAL_ENTRY_OUT, a deal that closes a position, and DEAL_ENTRY_INOUT, a deal that partially closes and partially reverses a position in netting mode, actually realize profit or loss. An entry deal has no profit of its own to report, so including it would introduce meaningless zero-value noise into the sum.

The third decision is that floating P&L includes swap. POSITION_PROFIT reflects only the price-driven unrealized gain or loss, but POSITION_SWAP is a real cost or credit already accruing against the account for every open position. Leaving it out would understate the account's true daily exposure at the exact moment that exposure matters most.

//+------------------------------------------------------------------+
//|                                          DailyPnlCalculator.mqh  |
//+------------------------------------------------------------------+
#ifndef DAILYPNLCALCULATOR_MQH
#define DAILYPNLCALCULATOR_MQH

//+------------------------------------------------------------------+
//| CDailyPnlCalculator                                              |
//| Computes today's realized P&L from closed deal history and       |
//| today's floating P&L from currently open positions, plus the     |
//| combined total of both. The calendar day boundary is midnight    |
//| server time, matching the timestamps the trade server itself     |
//| uses on every deal.                                              |
//+------------------------------------------------------------------+
class CDailyPnlCalculator
  {
private:
   datetime          MidnightToday(void) const;

public:
                     CDailyPnlCalculator(void);
                    ~CDailyPnlCalculator(void);

   double            GetRealizedPnl(void);
   double            GetFloatingPnl(void) const;
   double            GetCombinedPnl(void);
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CDailyPnlCalculator::CDailyPnlCalculator(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CDailyPnlCalculator::~CDailyPnlCalculator(void)
  {
  }

//+------------------------------------------------------------------+
//| MidnightToday                                                    |
//| Returns midnight of the current server day. This is the reset    |
//| boundary the whole circuit breaker anchors to, since it matches  |
//| the server timestamps recorded on every deal.                    |
//+------------------------------------------------------------------+
datetime CDailyPnlCalculator::MidnightToday(void) const
  {
   MqlDateTime dt;
   ::TimeToStruct(::TimeCurrent(), dt);

//--- zeroing hour/min/sec turns "now" into "midnight of the same day"
   dt.hour = 0;
   dt.min  = 0;
   dt.sec  = 0;
   return(::StructToTime(dt));
  }

//+------------------------------------------------------------------+
//| GetRealizedPnl                                                   |
//| Computes today's realized P&L by selecting deal history from     |
//| midnight of the current server day to now, and summing profit,   |
//| swap, and commission for every exit deal. Entry deals are        |
//| skipped since they carry no realized result of their own.        |
//+------------------------------------------------------------------+
double CDailyPnlCalculator::GetRealizedPnl(void)
  {
   datetime from = MidnightToday();
   datetime to   = ::TimeCurrent();

//--- scope the history cache to today's window before reading deals
   if(!::HistorySelect(from, to))
     {
      ::PrintFormat("CDailyPnlCalculator: HistorySelect failed, error=%d", ::GetLastError());
      return(0.0);
     }

   double realized = 0.0;
   int total = ::HistoryDealsTotal();

   for(int i = 0; i < total; i++)
     {
      ulong ticket = ::HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;

      //--- only closing deals realize P&L; an entry deal has none of its own
      long entry = ::HistoryDealGetInteger(ticket, DEAL_ENTRY);
      if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT)
         continue;

      double profit     = ::HistoryDealGetDouble(ticket, DEAL_PROFIT);
      double swap       = ::HistoryDealGetDouble(ticket, DEAL_SWAP);
      double commission = ::HistoryDealGetDouble(ticket, DEAL_COMMISSION);

      realized += (profit + swap + commission);
     }

   return(realized);
  }

//+------------------------------------------------------------------+
//| GetFloatingPnl                                                   |
//| Computes current floating P&L by summing POSITION_PROFIT and     |
//| POSITION_SWAP across every open position. Swap is included       |
//| because it is a real, already-accruing cost or credit, and       |
//| leaving it out would understate today's true exposure.           |
//+------------------------------------------------------------------+
double CDailyPnlCalculator::GetFloatingPnl(void) const
  {
   double floating = 0.0;
   int total = ::PositionsTotal();

   for(int i = 0; i < total; i++)
     {
      //--- PositionGetTicket() also selects the position for the Get* calls below
      ulong ticket = ::PositionGetTicket(i);
      if(ticket == 0)
         continue;

      double profit = ::PositionGetDouble(POSITION_PROFIT);
      double swap   = ::PositionGetDouble(POSITION_SWAP);

      floating += (profit + swap);
     }

   return(floating);
  }

//+------------------------------------------------------------------+
//| GetCombinedPnl                                                   |
//| Returns combined daily P&L: realized P&L plus floating P&L.      |
//+------------------------------------------------------------------+
double CDailyPnlCalculator::GetCombinedPnl(void)
  {
   return(GetRealizedPnl() + GetFloatingPnl());
  }

#endif // DAILYPNLCALCULATOR_MQH
//+------------------------------------------------------------------+

GetRealizedPnl() selects the deal history window, then loops through every deal and filters by entry type before adding it to the running total. GetFloatingPnl() loops through every open position and sums profit plus swap. GetCombinedPnl() is a one-line wrapper that adds the two together. Keeping these as three separate public methods, rather than one method that returns a single number, lets the dashboard display each figure on its own line without recomputing anything.


Section 2: CCircuitBreakerState — the State Snapshot

A circuit breaker needs to answer several related questions at once. Is it halted right now? When did it halt? What was the combined daily P&L at that moment? When will it reset? How many positions and orders did it act on? Six separate variables scattered across the EA would make every one of those questions harder to answer consistently, especially once the dashboard also needs to read the same information. A single struct gives every part of the system, the engine, the dashboard, and the test script, one shared source of truth.

Each field has a specific job. is_halted is the gate flag that IsHalted() reads directly. halt_time and halt_pnl record the exact moment and the exact combined daily P&L value that triggered the halt. This matters for reviewing why the account stopped trading after the fact. reset_time is the next midnight, computed once at halt time and again at initialization, so the engine never has to recompute it on every tick. positions_closed and orders_canceled record how much cleanup actually happened, which is the fastest way to confirm the halt sequence did what it was supposed to.

//+------------------------------------------------------------------+
//|                                        CircuitBreakerState.mqh   |
//+------------------------------------------------------------------+
#ifndef CIRCUITBREAKERSTATE_MQH
#define CIRCUITBREAKERSTATE_MQH

//+------------------------------------------------------------------+
//| CCircuitBreakerState                                             |
//| Holds a full snapshot of the circuit breaker's current condition.|
//| is_halted is the gate flag read before every order submission.   |
//| halt_time and halt_pnl record when and why the halt fired.       |
//| reset_time is the next midnight, at which the halt clears.       |
//| positions_closed and orders_canceled record the halt sequence's  |
//| cleanup results.                                                 |
//+------------------------------------------------------------------+
struct CCircuitBreakerState
  {
   bool              is_halted;
   datetime          halt_time;
   double            halt_pnl;
   datetime          reset_time;
   int               positions_closed;
   int               orders_canceled;

                     CCircuitBreakerState(void);
  };

//+------------------------------------------------------------------+
//| Constructor: Starts in a clean, non-halted state.                |
//+------------------------------------------------------------------+
CCircuitBreakerState::CCircuitBreakerState(void)
  {
//--- every field starts zeroed/false so a freshly declared state is
//--- never mistaken for one that has already recorded a real halt
   is_halted        = false;
   halt_time        = 0;
   halt_pnl         = 0.0;
   reset_time       = 0;
   positions_closed = 0;
   orders_canceled  = 0;
  }

#endif // CIRCUITBREAKERSTATE_MQH
//+------------------------------------------------------------------+

The struct itself holds no logic. Its only method is a constructor that zeroes every field, so a freshly declared CCircuitBreakerState can never be mistaken for one that already recorded a real halt.


Section 3: CPositionCloser — Closing All Open Positions

When the circuit breaker fires, every open position needs to close, regardless of symbol, direction, or size. CloseAll() iterates the position list and closes each one with a reduced-to-zero market order. This is the same mechanism used for a normal full close: a TRADE_ACTION_DEAL request against the ticket with the position's full volume and the opposite order type.

The iteration direction matters. PositionsTotal() returns the current count of open positions, and closing a position removes it from that list immediately. This shifts every position after it down by one index. Iterating forward from index 0 means that after closing the position at index 2, the position that used to be at index 3 is now at index 2. The loop's next iteration at index 3 then skips it entirely. Iterating backward, from the last index down to zero, avoids this. Closing the position at the current index only affects indices after it, which have already been processed and are never revisited.

//+------------------------------------------------------------------+
//|                                              PositionCloser.mqh  |
//+------------------------------------------------------------------+
#ifndef POSITIONCLOSER_MQH
#define POSITIONCLOSER_MQH

//+------------------------------------------------------------------+
//| CPositionCloser                                                  |
//| Closes every open position with a reduced-to-zero market order.  |
//| Iteration runs backward through the position list so that        |
//| closing one position never shifts the index of a position that   |
//| still needs to be visited.                                       |
//+------------------------------------------------------------------+
class CPositionCloser
  {
private:
   ENUM_ORDER_TYPE_FILLING SelectFillingMode(const string symbol) const;
   bool              CloseSinglePosition(const ulong ticket);

public:
                     CPositionCloser(void);
                    ~CPositionCloser(void);

   void              CloseAll(int &closed_count, int &failed_count);
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CPositionCloser::CPositionCloser(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CPositionCloser::~CPositionCloser(void)
  {
  }

//+------------------------------------------------------------------+
//| SelectFillingMode                                                |
//| Returns a filling mode the symbol actually advertises support    |
//| for, since a request with an unsupported filling mode is         |
//| rejected by the client terminal before it ever reaches the       |
//| trade server.                                                    |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE_FILLING CPositionCloser::SelectFillingMode(const string symbol) const
  {
   long filling_flags = ::SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);

//--- prefer FOK, then IOC, and only fall back to Return if neither is advertised
   if((filling_flags & SYMBOL_FILLING_FOK) != 0)
      return(ORDER_FILLING_FOK);
   if((filling_flags & SYMBOL_FILLING_IOC) != 0)
      return(ORDER_FILLING_IOC);

   return(ORDER_FILLING_RETURN);
  }

//+------------------------------------------------------------------+
//| CloseSinglePosition                                              |
//| Closes a single position by ticket, logging the ticket, symbol,  |
//| volume, and close price on success, or the failure reason on     |
//| rejection.                                                       |
//+------------------------------------------------------------------+
bool CPositionCloser::CloseSinglePosition(const ulong ticket)
  {
   if(!::PositionSelectByTicket(ticket))
      return(false);

   string symbol         = ::PositionGetString(POSITION_SYMBOL);
   long   position_type  = ::PositionGetInteger(POSITION_TYPE);
   double volume         = ::PositionGetDouble(POSITION_VOLUME);

//--- a long closes at bid, a short closes at ask
   double close_price = (position_type == POSITION_TYPE_BUY)
                        ? ::SymbolInfoDouble(symbol, SYMBOL_BID)
                        : ::SymbolInfoDouble(symbol, SYMBOL_ASK);

   MqlTradeRequest request;
   MqlTradeResult  result;
   ::ZeroMemory(request);
   ::ZeroMemory(result);

//--- Full volume, opposite order type: reduces the position to zero
//--- rather than opening a new, separate position
   request.action         = TRADE_ACTION_DEAL;
   request.position       = ticket;
   request.symbol         = symbol;
   request.volume         = volume;
   request.price          = close_price;
   request.deviation      = 10;
   request.type           = (position_type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
   request.type_filling   = SelectFillingMode(symbol);

   bool send_ok = ::OrderSend(request, result);

   if(send_ok && (result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED))
     {
      ::PrintFormat("CPositionCloser: closed ticket=%I64u symbol=%s volume=%.2f price=%.5f",
                    ticket, symbol, volume, result.price);
      return(true);
     }

   ::PrintFormat("CPositionCloser: failed to close ticket=%I64u symbol=%s volume=%.2f "
                 "retcode=%d comment=%s",
                 ticket, symbol, volume, result.retcode, result.comment);
   return(false);
  }

//+------------------------------------------------------------------+
//| CloseAll                                                         |
//| Closes every open position, iterating backward through the       |
//| position list so a closed position never disturbs the index of   |
//| a position still waiting to be processed.                        |
//+------------------------------------------------------------------+
void CPositionCloser::CloseAll(int &closed_count, int &failed_count)
  {
   closed_count = 0;
   failed_count = 0;

//--- Backward loop: PositionsTotal() shrinks by one after every close,
//--- so a forward loop would skip whichever position slides into the
//--- just-vacated index
   for(int i = ::PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = ::PositionGetTicket(i);
      if(ticket == 0)
         continue;

      if(CloseSinglePosition(ticket))
         closed_count++;
      else
         failed_count++;
     }
  }

#endif // POSITIONCLOSER_MQH
//+------------------------------------------------------------------+

SelectFillingMode() checks SYMBOL_FILLING_MODE and picks a fill type the symbol actually supports, since a request with an unsupported filling mode gets rejected by the client terminal itself, before it ever reaches the trade server. CloseSinglePosition() builds the closing request for one ticket, sends it, and logs the outcome either way. CloseAll() is the public method the circuit breaker calls, and it is the one that iterates backward for the reason explained above. Note that CloseAll() closes every position on the entire account, across every symbol and every chart, not only positions on the chart the EA is attached to.


Section 4: COrderCanceler — Canceling All Pending Orders

Pending orders are the other half of the halt sequence. A limit or stop order sitting untouched can still fill after the circuit breaker closes every open position, silently reopening exposure the halt was meant to prevent. CancelAll() walks OrdersTotal(), the count of currently pending orders, and cancels each one.

In MQL5, canceling a pending order is not a single function call. There is no global OrderDelete() function in MQL5. That name exists only in MQL4. Instead, canceling a pending order uses the same request-and-response pattern as every other trade action in this article: build an MqlTradeRequest, set action to TRADE_ACTION_REMOVE, set order to the ticket, and send it through OrderSend(). This keeps the whole codebase consistent, since opening, closing, modifying, and canceling all go through the same one function.

The same backward iteration rule from CPositionCloser applies here for the same reason. OrdersTotal() shrinks by one every time an order is canceled, so a forward loop would skip the order that slides into the just-vacated index. Iterating from the last index down to zero means every cancellation only affects positions in the list that have already been visited.

//+------------------------------------------------------------------+
//|                                                OrderCanceler.mqh |
//+------------------------------------------------------------------+
#ifndef ORDERCANCELER_MQH
#define ORDERCANCELER_MQH

//+------------------------------------------------------------------+
//| COrderCanceler                                                   |
//| Cancels every pending order using OrderSend() with               |
//| TRADE_ACTION_REMOVE. Iteration runs backward through the order   |
//| list, mirroring CPositionCloser, so that deleting one order never| 
//| disturbs the index of an order still waiting to be processed.    |
//+------------------------------------------------------------------+
class COrderCanceler
  {
private:
   bool              CancelSingleOrder(const ulong ticket);

public:
                     COrderCanceler(void);
                    ~COrderCanceler(void);

   void              CancelAll(int &canceled_count, int &failed_count);
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
COrderCanceler::COrderCanceler(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
COrderCanceler::~COrderCanceler(void)
  {
  }

//+------------------------------------------------------------------+
//| CancelSingleOrder                                                |
//| Cancels a single pending order by ticket, logging the ticket and |
//| order type on success, or the failure reason on rejection.       |
//| MQL5 has no standalone OrderDelete() function (that is an MQL4   |
//| API); a pending order is canceled the same way every other       |
//| trade action here is performed, through OrderSend() with a       |
//| TRADE_ACTION_REMOVE request.                                     |
//+------------------------------------------------------------------+
bool COrderCanceler::CancelSingleOrder(const ulong ticket)
  {
//--- OrderSelect() must succeed before OrderGetInteger() can read order properties.
   if(!::OrderSelect(ticket))
      return(false);

   long order_type = ::OrderGetInteger(ORDER_TYPE);

   MqlTradeRequest request;
   MqlTradeResult  result;
   ::ZeroMemory(request);
   ::ZeroMemory(result);

//--- TRADE_ACTION_REMOVE cancels a pending order by ticket; no price,
//--- volume, or symbol fields are needed for this action type
   request.action = TRADE_ACTION_REMOVE;
   request.order  = ticket;

   bool send_ok = ::OrderSend(request, result);

   if(send_ok && (result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED))
     {
      ::PrintFormat("COrderCanceler: canceled ticket=%I64u type=%d", ticket, order_type);
      return(true);
     }

   ::PrintFormat("COrderCanceler: failed to cancel ticket=%I64u type=%d retcode=%d comment=%s",
                 ticket, order_type, result.retcode, result.comment);
   return(false);
  }

//+------------------------------------------------------------------+
//| CancelAll                                                        |
//| Cancels every pending order, iterating backward through the      |
//| order list for the same reason CPositionCloser iterates          |
//| backward through the position list.                              |
//+------------------------------------------------------------------+
void COrderCanceler::CancelAll(int &canceled_count, int &failed_count)
  {
   canceled_count = 0;
   failed_count   = 0;

//--- Backward loop: OrdersTotal() shrinks by one after every cancel
   for(int i = ::OrdersTotal() - 1; i >= 0; i--)
     {
      ulong ticket = ::OrderGetTicket(i);
      if(ticket == 0)
         continue;

      if(CancelSingleOrder(ticket))
         canceled_count++;
      else
         failed_count++;
     }
  }

#endif // ORDERCANCELER_MQH
//+------------------------------------------------------------------+

CancelSingleOrder() selects the order, reads its type for logging, builds the removal request, and sends it. CancelAll() walks the order list backward and tallies successes and failures, exactly mirroring the pattern in CPositionCloser.


Section 5: CRiskCircuitBreaker — the Public Interface

CRiskCircuitBreaker owns one instance each of CDailyPnlCalculator, CPositionCloser, and COrderCanceler, plus a CCircuitBreakerState that tracks the current condition. It is the only class the EA talks to directly.

Init() stores the configured daily loss limit, expected as a negative number such as -500.0, and computes the first reset_time so the state is fully valid from the very first tick.

//+------------------------------------------------------------------+
//|                                          RiskCircuitBreaker.mqh  |
//+------------------------------------------------------------------+
#ifndef RISKCIRCUITBREAKER_MQH
#define RISKCIRCUITBREAKER_MQH

#include "DailyPnlCalculator.mqh"
#include "PositionCloser.mqh"
#include "OrderCanceler.mqh"
#include "CircuitBreakerState.mqh"

//+------------------------------------------------------------------+
//| CRiskCircuitBreaker                                              |
//| Public interface for the whole circuit breaker system. Evaluates |
//| combined daily P&L on every OnTick() call, triggers the halt     |
//| sequence when the daily loss limit is breached, and resets       |
//| automatically at the next midnight server time.                  |
//+------------------------------------------------------------------+
class CRiskCircuitBreaker
  {
private:
   CDailyPnlCalculator  m_pnl_calc;
   CPositionCloser      m_closer;
   COrderCanceler       m_canceler;
   CCircuitBreakerState m_state;
   double               m_daily_loss_limit;

   datetime             NextMidnight(const datetime from) const;
   void                 TriggerHalt(const double combined_pnl);

public:
                        CRiskCircuitBreaker(void);
                       ~CRiskCircuitBreaker(void);

   void                 Init(const double daily_loss_limit);
   void                 OnTick(void);
   bool                 IsHalted(void) const;
   void                 GetStatus(CCircuitBreakerState &state) const;
   void                 ForceReset(void);
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CRiskCircuitBreaker::CRiskCircuitBreaker(void)
  {
   m_daily_loss_limit = 0.0;
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CRiskCircuitBreaker::~CRiskCircuitBreaker(void)
  {
  }

//+------------------------------------------------------------------+
//| NextMidnight                                                     |
//| Computes the next midnight following the given server timestamp. |
//| Midnight server time is the reset boundary for the calendar day. |
//+------------------------------------------------------------------+
datetime CRiskCircuitBreaker::NextMidnight(const datetime from) const
  {
   MqlDateTime dt;
   ::TimeToStruct(from, dt);
   dt.hour = 0;
   dt.min  = 0;
   dt.sec  = 0;

//--- StructToTime() gives today's midnight; add 24 hours for tomorrow's
   datetime today_midnight = ::StructToTime(dt);
   return(today_midnight + 24 * 60 * 60);
  }

//+------------------------------------------------------------------+
//| Init                                                             |
//| Stores the configured daily loss limit and computes the first    |
//| reset_time so the state is valid from the first tick onward.     |
//+------------------------------------------------------------------+
void CRiskCircuitBreaker::Init(const double daily_loss_limit)
  {
//--- expected as a negative number, e.g. -500.0 for a $500 daily loss limit
   m_daily_loss_limit  = daily_loss_limit;
   m_state.is_halted   = false;
   m_state.reset_time  = NextMidnight(::TimeCurrent());

   ::PrintFormat("CRiskCircuitBreaker: initialized with daily loss limit=%.2f, reset_time=%s",
                 m_daily_loss_limit,
                 ::TimeToString(m_state.reset_time, TIME_DATE | TIME_MINUTES | TIME_SECONDS));
  }

OnTick() runs one of two paths. If the circuit breaker is already in the HALTED state, it only checks whether the calendar day has rolled past reset_time. If it is not halted, it computes combined daily P&L exactly once and compares it against the daily loss limit. Evaluating this on every tick, rather than once per bar, is what closes the overshoot gap described in the introduction. The check runs as often as the terminal delivers price updates, not once every few minutes.

//+------------------------------------------------------------------+
//| OnTick                                                           |
//| Evaluated on every tick. If already halted, checks only for the  |
//| midnight reset. Otherwise computes combined daily P&L and        |
//| triggers the halt sequence if the daily loss limit is breached.  |
//+------------------------------------------------------------------+
void CRiskCircuitBreaker::OnTick(void)
  {
   if(m_state.is_halted)
     {
      //--- while halted, the only thing OnTick() does is watch for the reset
      if(::TimeCurrent() >= m_state.reset_time)
        {
         ::Print("CRiskCircuitBreaker: calendar day rolled over, clearing HALTED state");
         m_state.is_halted  = false;
         m_state.reset_time = NextMidnight(::TimeCurrent());
        }
      return;
     }

   double combined_pnl = m_pnl_calc.GetCombinedPnl();

//--- both values are negative for a loss, so "<=" means "at or past the limit"
   if(combined_pnl <= m_daily_loss_limit)
      TriggerHalt(combined_pnl);
  }

The system makes one guarantee whenever is_halted is true: no open positions and no new orders, both at once, until the next reset. Every step in TriggerHalt() exists to make that guarantee true before the flag is ever set, not after.

TriggerHalt() sequences the actual halt in a fixed order. It closes every open position first, then cancels every pending order, and only after both of those complete does it set is_halted to true. This order matters. If the HALTED flag were set first and the closing logic failed partway through, the account would show as halted while still holding open risk, which is the opposite of what the flag is supposed to mean.

//+------------------------------------------------------------------+
//| TriggerHalt                                                      |
//| Sequences the halt: closes every open position, cancels every    |
//| pending order, then sets the HALTED state. Positions and orders  |
//| are cleared before the flag is set so IsHalted() never reports   |
//| true while real exposure still remains open.                     |
//+------------------------------------------------------------------+
void CRiskCircuitBreaker::TriggerHalt(const double combined_pnl)
  {
   int closed_count    = 0;
   int failed_closes   = 0;
   int canceled_count  = 0;
   int failed_cancels  = 0;

//--- cleanup runs BEFORE the flag flips; see box header above for why
   m_closer.CloseAll(closed_count, failed_closes);
   m_canceler.CancelAll(canceled_count, failed_cancels);

   m_state.is_halted        = true;
   m_state.halt_time        = ::TimeCurrent();
   m_state.halt_pnl         = combined_pnl;
   m_state.reset_time       = NextMidnight(::TimeCurrent());
   m_state.positions_closed = closed_count;
   m_state.orders_canceled  = canceled_count;

   ::PrintFormat("CRiskCircuitBreaker: TRADING HALTED at %s, combined_pnl=%.2f, limit=%.2f, "
                 "positions_closed=%d (failed=%d), orders_canceled=%d (failed=%d)",
                 ::TimeToString(m_state.halt_time, TIME_DATE | TIME_MINUTES | TIME_SECONDS),
                 combined_pnl, m_daily_loss_limit, closed_count, failed_closes,
                 canceled_count, failed_cancels);
  }

IsHalted() is the gate. It is meant to be called before every single OrderSend() in the EA, without exception, since one unguarded order path defeats the entire circuit breaker. GetStatus() copies the internal state out for the dashboard to read. ForceReset() exists purely so the test script and manual testing can clear a halt without waiting for the actual midnight rollover.

//+------------------------------------------------------------------+
//| IsHalted                                                         |
//| Returns true if the circuit breaker is currently in the HALTED   |
//| state. Intended to be called before every OrderSend() in the EA. |
//+------------------------------------------------------------------+
bool CRiskCircuitBreaker::IsHalted(void) const
  {
   return(m_state.is_halted);
  }

//+------------------------------------------------------------------+
//| GetStatus                                                        |
//| Copies the current state snapshot out for the dashboard to read. |
//+------------------------------------------------------------------+
void CRiskCircuitBreaker::GetStatus(CCircuitBreakerState &state) const
  {
   state = m_state;
  }

//+------------------------------------------------------------------+
//| ForceReset                                                       |
//| Manually clears the HALTED state. Intended for testing only,     |
//| never for use in a live trading path.                            |
//+------------------------------------------------------------------+
void CRiskCircuitBreaker::ForceReset(void)
  {
   m_state.is_halted = false;
   ::Print("CRiskCircuitBreaker: HALTED state force-reset (testing only)");
  }


Section 6: CCircuitBreakerDashboard — the Live Panel

The dashboard exists so a trader watching the chart can see the account's daily risk position at a glance, without opening the Experts log. BuildPanelText() formats every field into fixed-width columns using %-22s for the label and %12s for the right-aligned value. This keeps every row lining up regardless of how many digits a given P&L figure has.

The remaining buffer is computed as combined daily P&L minus the daily loss limit. Since both values are negative numbers representing losses, this framing gives a result that reads naturally. A positive buffer means that much room remains before the limit. A negative buffer means the limit has already been breached by that amount. The status indicator is a plain text tag, [ OK ] or [ HALTED ], chosen deliberately as text rather than a color so it reads correctly even if the terminal's comment rendering strips formatting.

Update() builds the panel text and pushes it to the chart with ChartSetString() against the CHART_COMMENT property. This is the same on-chart comment area other tools use, just set through the properties API instead of the Comment() function. BuildPanelText() is kept separate from Update() on purpose, since it has no dependency on a live chart. This lets the verification script call it directly and check the resulting string without needing a real terminal session.

//+------------------------------------------------------------------+
//|                                     CircuitBreakerDashboard.mqh  |
//+------------------------------------------------------------------+
#ifndef CIRCUITBREAKERDASHBOARD_MQH
#define CIRCUITBREAKERDASHBOARD_MQH

#include "CircuitBreakerState.mqh"

//+------------------------------------------------------------------+
//| CCircuitBreakerDashboard                                         |
//| Renders a chart comment panel showing daily realized P&L,        |
//| floating P&L, combined total, the configured daily loss limit,   |
//| the remaining loss buffer, and a status indicator.               |
//+------------------------------------------------------------------+
class CCircuitBreakerDashboard
  {
private:
   long              m_chart_id;
   string            FormatLine(const string label, const string value) const;

public:
                     CCircuitBreakerDashboard(void);
                    ~CCircuitBreakerDashboard(void);

   void              SetChartId(const long chart_id) { m_chart_id = chart_id; }
   string            BuildPanelText(const double realized_pnl,
                                    const double floating_pnl,
                                    const double daily_loss_limit,
                                    const CCircuitBreakerState &state) const;
   void              Update(const double realized_pnl,
                            const double floating_pnl,
                            const double daily_loss_limit,
                            const CCircuitBreakerState &state);
   void              Clear(void);
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CCircuitBreakerDashboard::CCircuitBreakerDashboard(void)
  {
   m_chart_id = 0;
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CCircuitBreakerDashboard::~CCircuitBreakerDashboard(void)
  {
  }

//+------------------------------------------------------------------+
//| FormatLine                                                       |
//| Formats one label-value row with fixed-width columns so every    |
//| row in the panel lines up regardless of value length.            |
//+------------------------------------------------------------------+
string CCircuitBreakerDashboard::FormatLine(const string label, const string value) const
  {
//--- %-22s left-pads the label, %12s right-aligns the value column
   return(::StringFormat("%-22s%12s\n", label, value));
  }

//+------------------------------------------------------------------+
//| BuildPanelText                                                   |
//| Builds the full panel text. Remaining buffer is combined P&L     |
//| minus the daily loss limit, positive while under the limit and   |
//| negative once breached. Kept separate from Update() so it can    |
//| be tested without touching the chart.                            |
//+------------------------------------------------------------------+
string CCircuitBreakerDashboard::BuildPanelText(const double realized_pnl,
      const double floating_pnl,
      const double daily_loss_limit,
      const CCircuitBreakerState &state) const
  {
   double combined_pnl     = realized_pnl + floating_pnl;
//--- both values are negative for a loss; this framing reads naturally:
//--- positive buffer = room left, negative buffer = breached by that amount
   double remaining_buffer = combined_pnl - daily_loss_limit;
   string status_text      = state.is_halted ? "[ HALTED ]" : "[ OK ]";

   string panel = "";
   panel += "Circuit breaker dashboard\n";
   panel += FormatLine("Daily loss limit:", ::StringFormat("%.2f", daily_loss_limit));
   panel += FormatLine("Realized P&L today:", ::StringFormat("%.2f", realized_pnl));
   panel += FormatLine("Floating P&L:", ::StringFormat("%.2f", floating_pnl));
   panel += FormatLine("Combined daily P&L:", ::StringFormat("%.2f", combined_pnl));
   panel += FormatLine("Remaining buffer:", ::StringFormat("%.2f", remaining_buffer));
   panel += FormatLine("Status:", status_text);

//--- only shows the halt timestamp once there actually is one
   if(state.is_halted)
      panel += ::StringFormat("Halted since %s\n",
                              ::TimeToString(state.halt_time, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

   return(panel);
  }

//+------------------------------------------------------------------+
//| Update                                                           |
//| Builds the panel text and renders it to the chart comment area   |
//| using ChartSetString() against the CHART_COMMENT property.       |
//+------------------------------------------------------------------+
void CCircuitBreakerDashboard::Update(const double realized_pnl,
                                      const double floating_pnl,
                                      const double daily_loss_limit,
                                      const CCircuitBreakerState &state)
  {
   string panel = BuildPanelText(realized_pnl, floating_pnl, daily_loss_limit, state);
   ::ChartSetString(m_chart_id, CHART_COMMENT, panel);
  }

//+------------------------------------------------------------------+
//| Clear                                                            |
//| Clears the chart comment area.                                   |
//+------------------------------------------------------------------+
void CCircuitBreakerDashboard::Clear(void)
  {
   ::ChartSetString(m_chart_id, CHART_COMMENT, "");
  }

#endif // CIRCUITBREAKERDASHBOARD_MQH
//+------------------------------------------------------------------+


Section 7: CircuitBreakerEA.mq5 — Integration

The demo EA wires the whole system together. OnInit() initializes the circuit breaker with the configured daily loss limit and starts a timer for the dashboard. OnTick() calls the circuit breaker's own OnTick() first, then checks IsHalted() before any order logic runs. This is exactly the gate pattern the circuit breaker is designed around. OnTimer() reads the current P&L figures and status, then updates the dashboard independently of the tick stream, so the panel refreshes on a steady interval instead of only when the market moves.

//+------------------------------------------------------------------+
//|                                            CircuitBreakerEA.mq5  |
//+------------------------------------------------------------------+

#include <DailyPnL_and_CircuitBreaker/RiskCircuitBreaker.mqh>
#include <DailyPnL_and_CircuitBreaker/CircuitBreakerDashboard.mqh>
#include <DailyPnL_and_CircuitBreaker/DailyPnlCalculator.mqh>
#include <DailyPnL_and_CircuitBreaker/CircuitBreakerState.mqh>

//--- Inputs
input double InpDailyLossLimit = -500.0;   // Daily loss limit (negative value)
input int    InpTimerSeconds   = 1;        // Dashboard refresh interval in seconds

CRiskCircuitBreaker      g_breaker;
CCircuitBreakerDashboard g_dashboard;
CDailyPnlCalculator      g_pnl_calc;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   g_breaker.Init(InpDailyLossLimit);
   g_dashboard.SetChartId(::ChartID());

//--- drives OnTimer(), which refreshes the dashboard independently of ticks
   ::EventSetTimer(InpTimerSeconds);

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
//--- OnTick() must run first so a fresh breach is caught before any order logic
   g_breaker.OnTick();

   if(g_breaker.IsHalted())
     {
      //--- circuit breaker is active, no new orders are submitted
      return;
     }

//--- any order submission in a real strategy belongs here, always
//--- preceded by the same IsHalted() check performed above
  }

//+------------------------------------------------------------------+
//| Timer function, refreshes the dashboard independently of ticks   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   CCircuitBreakerState state;
   g_breaker.GetStatus(state);

   double realized = g_pnl_calc.GetRealizedPnl();
   double floating = g_pnl_calc.GetFloatingPnl();

   g_dashboard.Update(realized, floating, InpDailyLossLimit, state);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ::EventKillTimer();
   g_dashboard.Clear();
  }
//+------------------------------------------------------------------+

Note the scope of this EA's protection: IsHalted() only gates order submissions written inside CircuitBreakerEA.mq5 itself. A different EA attached to a different chart, or a manual order placed through the terminal, has no way to see this instance's HALTED state and will trade right through it. Section 9 covers how to extend this if account-wide enforcement is required.

Mock-up dashboard panel

Dashboard panel during a halt. Combined daily P&L of -514.20 has breached the -500.00 limit, leaving a remaining buffer of -14.20, and the status has switched to [ HALTED ].


Section 8: Verification — TestCircuitBreaker.mq5

MQL5 has no native assert, so the script defines a small CB_ASSERT macro backed by a TestAssert() function that logs PASS or FAIL and keeps a running count. Five checks are covered: the combined P&L sum, the halt trigger comparison, the midnight reset calculation, the remaining buffer formula, and the dashboard's panel text formatting.

Each check matters independently. A broken combined P&L sum means the circuit breaker evaluates the wrong number every tick. A broken halt trigger means the account either never halts or halts too early. A broken midnight calculation means the account either never resets or resets at the wrong time. A broken buffer formula misleads the trader watching the dashboard. Broken panel formatting can hide the HALTED status entirely.

//+------------------------------------------------------------------+
//|                                           TestCircuitBreaker.mq5 |
//+------------------------------------------------------------------+
#property script_show_inputs

#include <DailyPnL_and_CircuitBreaker/CircuitBreakerState.mqh>
#include <DailyPnL_and_CircuitBreaker/CircuitBreakerDashboard.mqh>

int g_pass_count = 0;
int g_fail_count = 0;

//+------------------------------------------------------------------+
//| ASSERT macro replacement, since MQL5 has no native assert.       |
//+------------------------------------------------------------------+
#define CB_ASSERT(condition, message) TestAssert((condition), (message))

//+------------------------------------------------------------------+
//| TestAssert                                                       |
//| Logs PASS or FAIL for one condition and keeps a running count.   |
//+------------------------------------------------------------------+
void TestAssert(const bool condition, const string message)
  {
   if(condition)
     {
      g_pass_count++;
      ::PrintFormat("PASS: %s", message);
     }
   else
     {
      g_fail_count++;
      ::PrintFormat("FAIL: %s", message);
     }
  }

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart(void)
  {
   ::Print("=== TestCircuitBreaker starting ===");

   TestCombinedPnl();
   TestHaltTrigger();
   TestMidnightReset();
   TestRemainingBuffer();
   TestDashboardFormatting();

   ::PrintFormat("=== TestCircuitBreaker finished: %d passed, %d failed ===",
                  g_pass_count, g_fail_count);
  }

//+------------------------------------------------------------------+
//| TestCombinedPnl                                                  |
//| Covers the combined P&L sum: realized -$312.40 plus floating     |
//| -$201.80 must produce -$514.20.                                  |
//+------------------------------------------------------------------+
void TestCombinedPnl(void)
  {
   ::Print("--- TestCombinedPnl ---");

   double realized = -312.40;
   double floating = -201.80;
   double combined = realized + floating;

   CB_ASSERT(::MathAbs(combined - (-514.20)) < 0.001,
              "Realized -312.40 plus floating -201.80 combines to -514.20");
  }

//+------------------------------------------------------------------+
//| TestHaltTrigger                                                  |
//| Covers the halt trigger check: -514.20 must breach a -500.00     |
//| limit, and -480.00 must not.                                     |
//+------------------------------------------------------------------+
void TestHaltTrigger(void)
  {
   ::Print("--- TestHaltTrigger ---");

   double daily_loss_limit = -500.00;

//--- same comparison CRiskCircuitBreaker::OnTick() uses: combined <= limit
   bool breach_case    = (-514.20 <= daily_loss_limit);
   bool no_breach_case = (-480.00 <= daily_loss_limit);

   CB_ASSERT(breach_case == true, "-514.20 correctly identified as breaching -500.00 limit");
   CB_ASSERT(no_breach_case == false, "-480.00 correctly identified as NOT breaching -500.00 limit");
  }

//+------------------------------------------------------------------+
//| TestMidnightReset                                                |
//| Covers the midnight reset formula: given 2026.07.15 14:32:07,    |
//| the next midnight must be 2026.07.16 00:00:00.                   |
//+------------------------------------------------------------------+
void TestMidnightReset(void)
  {
   ::Print("--- TestMidnightReset ---");

   datetime server_time = ::StringToTime("2026.07.15 14:32:07");

//--- replicates CRiskCircuitBreaker::NextMidnight() without needing an instance
   MqlDateTime dt;
   ::TimeToStruct(server_time, dt);
   dt.hour = 0;
   dt.min  = 0;
   dt.sec  = 0;

   datetime today_midnight = ::StructToTime(dt);
   datetime next_midnight  = today_midnight + 24 * 60 * 60;
   datetime expected        = ::StringToTime("2026.07.16 00:00:00");

   CB_ASSERT(next_midnight == expected,
              "Next midnight from 2026.07.15 14:32:07 is correctly 2026.07.16 00:00:00");
  }

//+------------------------------------------------------------------+
//| TestRemainingBuffer                                              |
//| Covers the remaining buffer formula: combined P&L minus the      |
//| daily loss limit must give -14.20 for the breach case and        |
//| +20.00 for the non-breach case.                                  |
//+------------------------------------------------------------------+
void TestRemainingBuffer(void)
  {
   ::Print("--- TestRemainingBuffer ---");

   double daily_loss_limit = -500.00;

//--- combined_pnl - limit; positive means room left, negative means breached
   double buffer_breach    = -514.20 - daily_loss_limit;
   double buffer_no_breach = -480.00 - daily_loss_limit;

   CB_ASSERT(::MathAbs(buffer_breach - (-14.20)) < 0.001,
              "Remaining buffer at -514.20 combined P&L is correctly -14.20");
   CB_ASSERT(::MathAbs(buffer_no_breach - 20.00) < 0.001,
              "Remaining buffer at -480.00 combined P&L is correctly +20.00");
  }

//+------------------------------------------------------------------+
//| TestDashboardFormatting                                          |
//| Covers CCircuitBreakerDashboard.BuildPanelText(), confirming the |
//| HALTED status, the combined P&L, and the remaining buffer all    |
//| appear correctly in the rendered panel text.                     |
//+------------------------------------------------------------------+
void TestDashboardFormatting(void)
  {
   ::Print("--- TestDashboardFormatting ---");

   CCircuitBreakerDashboard dashboard;
   CCircuitBreakerState state;
   state.is_halted = true;
   state.halt_time = ::StringToTime("2026.07.15 14:32:07");

//--- BuildPanelText() has no chart dependency, so it can be tested directly
   string panel = dashboard.BuildPanelText(-312.40, -201.80, -500.00, state);

   CB_ASSERT(::StringFind(panel, "[ HALTED ]") >= 0,
              "Panel text includes the HALTED status indicator");
   CB_ASSERT(::StringFind(panel, "-514.20") >= 0,
              "Panel text includes the correct combined daily P&L");
   CB_ASSERT(::StringFind(panel, "-14.20") >= 0,
              "Panel text includes the correct remaining buffer");
  }
//+------------------------------------------------------------------+


Section 9: Extending the Circuit Breaker

A weekly loss limit alongside the daily one fits as a second threshold check inside CRiskCircuitBreaker. CDailyPnlCalculator would need to also sum realized P&L from the start of the calendar week, with a separate reset boundary computed for the next Monday midnight instead of the next daily midnight.

A maximum consecutive loss counter as a second halt trigger would track how many realized deals in a row closed with a negative profit. It increments on each losing exit deal and resets to zero on any winning one, with TriggerHalt() called the moment that counter reaches a configured threshold, independent of the P&L-based check already in place.

Sending a push notification through SendNotification() the moment the circuit breaker fires is a one-line addition inside TriggerHalt(), right after the halt message is logged. This gives a trader who is away from the terminal an immediate alert rather than relying on them to notice the chart.

A drawdown limit based on peak equity rather than calendar-day reset would need a new field tracking the highest equity value seen since the account started, updated on every tick. The halt trigger would compare current equity against that peak instead of comparing combined daily P&L against a fixed daily figure. This measures a different kind of risk than the daily loss limit and would run alongside it rather than replacing it.

If you need account-wide protection, extend enforcement beyond a single EA so it covers all automated sources, not only this EA. On halt, CRiskCircuitBreaker::TriggerHalt() can set a shared global variable via GlobalVariableSet(). On reset, it clears the variable. Every other EA running on the same terminal, including any manual trading workflow built on top of MQL5 scripts, can then check GlobalVariableGet() for that same name before its own order submissions. This does not stop a human from clicking a manual buy or sell button directly in the terminal. No MQL5 mechanism can intercept that. It does, however, let every automated strategy on the account share one account-wide halt signal instead of each EA enforcing its own isolated one.


Section 10: Limitations

The circuit breaker's HALTED state is scoped to the single CRiskCircuitBreaker instance that detected the breach. It does not stop a different EA on a different chart from placing orders, and it does not stop a manual order placed through the terminal. IsHalted() only gates order submissions that are written to check it. Anything else on the account, whether another automated strategy or a person clicking buttons, trades right through the halt. Section 9 describes a GlobalVariableSet() and GlobalVariableGet() pattern that extends the halt signal to cooperating EAs, though it still cannot intercept a manual order.

The midnight reset uses server time, which can differ from the trader's own local time by several hours depending on the broker's server location and daylight saving rules. A circuit breaker configured with the daily loss limit in mind as a local-day concept needs to account for this offset, since the actual reset always happens at the broker's midnight, not the trader's own.

Position closing during a high-volatility halt event can experience slippage and, on some brokers, partial fills if the requested volume cannot be filled at once. CPositionCloser logs every close attempt and its result, but it does not retry a partially filled close automatically. A fast-moving market at the exact moment of a halt can leave a small remainder position open until the next tick's halt check catches it.

The floating P&L snapshot used for the halt decision reflects the bid or ask price at the moment of the triggering tick, not the actual prices the positions close moments later. In a fast market these two numbers can diverge. The combined daily P&L value logged as the halt reason is an accurate description of the decision, but not a guarantee of the exact realized result once positions actually close.

The circuit breaker cannot prevent an already-submitted pending order from filling if that fill happens on the trade server side before CancelAll() reaches it. This is a narrow race condition, but on an account with pending orders sitting very close to the current price during a fast-moving halt event, it is a real possibility worth knowing about.


Conclusion

Delivered here is a complete, testable CRiskCircuitBreaker system you can embed into any MQL5 EA. It consists of the P&L calculator, a single shared state snapshot, position closer, order canceler, the central breaker class, a chart dashboard, a demo EA showing the integration points, and a verification script that validates the core formulas and triggers. On each tick the breaker computes combined daily P&L (realized + floating + swap) against a configured daily loss limit; when that threshold is crossed it runs a fixed halt sequence (close all positions, cancel all pending orders) and sets a HALTED flag that you must check via IsHalted() before any OrderSend(). The HALTED state clears automatically at the next server midnight.

Known limitations are documented and deliberate: this HALTED state is local to the CRiskCircuitBreaker instance (one EA on one chart) and cannot stop manual orders placed through the terminal; server midnight may differ from a trader's local day; position closes can experience slippage or partial fills; and there is a narrow race window where a pending order might fill before its cancellation reaches the server. If you need account‑wide enforcement, the article shows a straightforward extension using GlobalVariableSet()/GlobalVariableGet() so cooperating EAs can share the same halt signal. The module is designed to be auditable, reproducible, and easy to integrate — giving you a verifiable “safety switch” for strict daily loss limits.


Programs used in the article:

# Name Type Description
1 CircuitBreakerState.mqh Include File CCircuitBreakerState struct holding the full state snapshot
2 DailyPnlCalculator.mqh Include File CDailyPnlCalculator class computing realized, floating, and combined P&L
3 PositionCloser.mqh Include File CPositionCloser class closing all open positions with market orders
4 OrderCanceler.mqh Include File COrderCanceler class canceling all pending orders via OrderSend()
5 RiskCircuitBreaker.mqh Include File CRiskCircuitBreaker class, the public interface driving the halt sequence
6 CircuitBreakerDashboard.mqh Include File CCircuitBreakerDashboard class rendering the live chart comment panel
7 CircuitBreakerEA.mq5 Demo EA Demo EA wiring the circuit breaker and dashboard together
8 TestCircuitBreaker.mq5 Script Verification script covering P&L math, halt logic, reset, and formatting
9 DailyPnlCircuitBreaker.zip Zip Archive Zip archive containing all the attached files and their paths relative to the terminal's root folder.
Meta-Labeling the Classics (Part 3): Filtering and Sizing Bollinger Band Trades Meta-Labeling the Classics (Part 3): Filtering and Sizing Bollinger Band Trades
Bollinger Band mean reversion degrades in trending regimes when ADX is high and bandwidth expands. We separate direction from trade selection with a two‑stage meta‑labeling pipeline: a gradient‑boosted secondary classifier trained with PurgedKFold on band‑specific features (BBP, BBB, bandwidth regime) outputs action probabilities that drive probability‑based bet sizing. The MQL5 implementation loads the ONNX model and applies position sizing within a two‑EA architecture to filter low‑quality band touches.
Larry Williams Market Secrets (Part 16): Detecting and Trading the Oops Gap Reversal Pattern Larry Williams Market Secrets (Part 16): Detecting and Trading the Oops Gap Reversal Pattern
Learn how to build an MQL5 Expert Advisor that detects and trades Larry Williams’ Oops Gap Reversal pattern using objective gap rules and later-bar confirmation. The EA tracks setup expiration, prepares stop-loss and take-profit levels, supports manual or risk-based position sizing, executes market orders, and is evaluated through historical testing.
From Novice to Expert: Candlestick Momentum Confirmation for Classic Crossover Strategies From Novice to Expert: Candlestick Momentum Confirmation for Classic Crossover Strategies
In this article, we refine a moving average crossover strategy with a momentum candle filter and an immediate retracement bar confirmation. When both conditions are met, a pending stop order is placed using a pivot-based stop loss and a 2R take profit. The complete MQL5 Expert Advisor code, finite-state-machine logic, and chart annotations are detailed.
Does This Entry Filter Really Add Edge? A Block-Permutation Test in MQL5 Does This Entry Filter Really Add Edge? A Block-Permutation Test in MQL5
An MQL5 analyzer reconstructs completed trades, records acceptance labels, and measures the accepted-minus-rejected mean net-profit difference. It benchmarks that statistic against individual permutations, equal-block permutations, and circular shifts while preserving the accepted count. Block-size sensitivity, CSV exports, and coordinated base/filtered passes separate statistical selection evidence from operational effects on profit, drawdown, and efficiency metrics.