preview
Automating Classic Market Methods in MQL5 (Part 7): The Nicolas Darvas Box System

Automating Classic Market Methods in MQL5 (Part 7): The Nicolas Darvas Box System

MetaTrader 5Examples |
191 0
Tola Moses Hector
Tola Moses Hector

Introduction

Nicolas Darvas became one of the most celebrated traders of the twentieth century without any formal financial training. He was a dancer—performing across Europe and Asia in the 1950s while studying stock price behavior from weekly newspapers delivered to wherever his tour had taken him that week. He developed his method over several years of losses before discovering the pattern that made his fortune: stocks that formed tight, quiet boxes of price consolidation and then broke above them on expanding volume were about to make huge moves.

His rules were simple enough to apply with nothing more than a notebook and a pencil. A box top was the highest high, after which the price failed to make a new high for three consecutive sessions. A box bottom was the lowest low during the same period. When the price broke above the box top on volume expansion, he bought. The stop went below the box bottom. If the price advanced and formed a new box, he bought more. If the stop was hit, he exited without argument.

The method looks simple, but two aspects make it difficult to automate reliably. The first is the rolling box: each breakout from a box creates a new box immediately above it, and the EA must recognize when the new box has formed, store its boundaries, and retire the old one. The second is the staircase entry: Darvas did not buy once and hold. He pyramided—adding to his position each time the price broke into a new box. The pyramid grows as long as boxes keep forming above the entry. When a stop fires, the entire staircase unwinds.

This article implements both mechanisms correctly. The box detection engine tracks rolling highs and lows, confirms the box top when three consecutive sessions fail to exceed the high, and monitors for the volume-confirmed breakout. The staircase manager tracks all open units, moves the stop to the new box floor on each pyramid add, and closes all units simultaneously when the stop is hit.

In the previous article, we implemented Jesse Livermore's Pivotal Point System; the connection is direct: Darvas and Livermore studied the same market behavior but from different angles. Livermore focused on pivotal points and campaign states. Darvas focused on boxes and staircases. Both were watching volume as the primary confirmation signal. Furthermore, they both pyramided into winning positions and exited without hesitation on defined stop signals. Both avoided averaging down on losers entirely. The automation challenge is the same in both cases—sequential conditions enforced by a state machine.

This EA answers one question: Did price form a valid low-volume box and break out on expanding volume? If so, does it keep forming higher boxes for Add-on's, or has the shared stop been hit?

We will cover the following topics:

  1. The Darvas Box System—Theory and Rules
  2. What Makes a Valid Box in This Implementation
  3. Architecture—The Box Engine and Staircase Manager
  4. Implementation in MQL5
  5. Known Limitations
  6. Conclusion

The Darvas Box System—Theory and Rules

Darvas described his method in "How I Made $2,000,000 in the Stock Market," published in 1960. The rules are precise enough to code directly.


The darvas box system

Fig. 1. The Darvas box system.

Box Formation

A box forms through a specific sequence of the price action. The price reaches a new high. That high is then not exceeded for three consecutive sessions. The highest high of the sequence becomes the box top. The lowest low during the same period becomes the box bottom. The box is now confirmed—it is a defined range of consolidation.

The three-session rule is Darvas's original specification. The number three represents his minimum evidence that the high was genuinely resistant—one session might be random, two might be coincidence, and three might be sufficient confirmation. In the original system, he used daily sessions. This EA uses daily bars by default, making the three-bar rule a direct translation.

Volume During Box Formation

Darvas paid close attention to volume during box formation. A genuine box forms quietly—volume contracts as price consolidates. High volume during the box period suggested institutions were distributing rather than accumulating. Low volume during consolidation, followed by high volume on the breakout, was the signature Darvas considered most reliable.

Breakout Confirmation

When the price closes above the box top, a breakout has occurred. Darvas required volume to expand on the breakout bar—specifically, the breakout volume should exceed the average volume during the box period. A breakout on weak volume was suspicious and sometimes treated as a false signal.

The entry occurred on the breakout bar's close in Darvas's original method or as a stop-buy order placed just above the box top. This EA enters on the close of the breakout bar to use only completed bar data.

The Rolling Stop

The stop is always placed at the box bottom of the most recent box. After the first entry, the stop is at the first box bottom. When the price forms a second box and a second entry is made, the stop moves to the second box bottom. The first unit's stop also moves to the second box's bottom—all units share the same stop level, which is always the floor of the most recent box.

This is a staircase structure: box top 1 → entry 1, stop at floor 1. Box top 2 → entry 2, stop for all units moves to floor 2. Box top 3 → entry 3, stop for all units moves to floor 3. Each new box raises the floor. The staircase only moves upward.

Exit Rule

Darvas's exit was unconditional: if price closes below the current box bottom—the shared stop level—all units close immediately. There is no trailing stop, no time-based exit, and no partial close. The staircase manager tracks all units. On each add, it moves the shared stop to the new box floor. If the stop is hit, it closes all units.

He also exited if price failed to form a new box within a defined period after a breakout. In this EA, if price does not establish a new box top within "InpMaxBarsBetweenBoxes" bars of the last breakout, the position is closed—the trend has stalled.

Bear Boxes

Darvas traded primarily on the long side because he believed in trading with the overall market trend. He also described the inverse: a bear box forms when the price reaches a new low, that low holds for three sessions, and a breakout below the box bottom on volume signals a short entry. This EA implements both directions—bear boxes for short campaigns mirror bull boxes for long campaigns exactly.

What Makes a Valid Box in This Implementation?

The box engine enforces five conditions before confirming a box.

  1. The new extreme condition: for a bull box, the box period must begin with a new N-bar high, where N is "InpLookbackBars." For a bear box, a new N-bar low. This ensures boxes form at genuine price extremes rather than inside existing ranges.
  2. The three-session hold condition: the box top must not be exceeded for three consecutive sessions after it is set. Each new session that fails to exceed the top increments the confirmation counter. When the counter reaches three, the box is confirmed.
  3. The volume contraction condition: the average volume during the box formation period must be below "InpBoxVolMult" times the longer-term average volume. This confirms that the consolidation is quiet—institutions are not actively distributing from the box.
  4. The minimum box height condition: the box height—top minus bottom—must exceed "InpMinBoxATR" times the ATR. This prevents trivially narrow boxes from generating signals.
  5. The breakout volume condition: the breakout bar's volume must exceed the box period's average volume by "InpBreakVolMult." The default is 1.3—breakout volume must be at least 30% above the box average.

When all five conditions pass, the breakout is confirmed, the first entry opens, and the staircase begins.

Architecture—The Box Engine and Staircase Manager

The EA is built around two components.

  • The box engine scans bar data to identify new price extremes, tracks the three-session confirmation counter, monitors box period volume, and detects breakouts. When a breakout is confirmed, it fires the entry signal and resets to begin scanning for the next box immediately.
  • The staircase manager maintains an array of all open units—their entry prices, lot sizes, and position tickets. After each new box breakout, it opens a new unit and moves the shared stop level to the new box bottom. When the stop is hit, it closes all units simultaneously.

Architecture overview

Fig. 2. Architecture overview: the state machine.

The state machine has five states: "STATE_IDLE" when no extreme has been identified; "STATE_BOX_FORMING" when an extreme has been set and the three-session counter is running; "STATE_WAITING_BREAKOUT" when the box is confirmed and a breakout is being watched; "STATE_IN_TRADE" when at least one unit is open; and "STATE_WATCHING_NEW_BOX" when a unit is open and the engine is simultaneously watching for the next box to form above it.

The final state is the automation insight that most Darvas implementations miss. While a trade is open, the engine does not stop watching for new boxes. "STATE_IN_TRADE" and "STATE_WATCHING_NEW_BOX" coexist—the staircase is active, and the detection engine is running simultaneously. A new breakout while in a trade means a new entry, not a new standalone trade.

Implementation in MQL5

The EA is built section by section.

Includes, Enumerations, and Input Parameters

//+------------------------------------------------------------------+
//|                                                   DarvasBoxEA.mq5|
//|                                Copyright 2026, Tola Moses Hector |
//|                                          https://t.me/tolahector |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Tola Moses Hector"
#property link      "https://t.me/tolahector"
#property version   "1.00"
#property description "Automating Classic Market Methods in MQL5 Part 7"
#property description "Nicolas Darvas Box System — Box Detection and Staircase Pyramid"
#property description "Daily timeframe recommended"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| EA State Machine                                                 |
//+------------------------------------------------------------------+
enum ENUM_DARVAS_STATE
  {
   STATE_IDLE,                              // No extreme identified—scanning
   STATE_BOX_FORMING,                       // Extreme set—counting confirmation sessions
   STATE_WAITING_BREAKOUT,                  // Box confirmed—watching for breakout
   STATE_IN_TRADE,                          // At least one staircase unit open
   STATE_WATCHING_NEW_BOX                   // In trade and watching for next box simultaneously
  };

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input group "=== Box Detection ==="
input int    InpLookbackBars      = 50;     // Bars to look back for new extreme
input int    InpConfirmSessions   = 3;      // Sessions the extreme must hold to confirm box
input double InpMinBoxATR         = 0.5;    // Minimum box height in ATR units
input double InpBoxVolMult        = 1.0;    // Max box volume as multiple of average (quiet = below 1)
input double InpBreakVolMult      = 1.3;    // Minimum breakout volume vs box average

input group "=== Staircase Settings ==="
input int    InpMaxUnits          = 4;      // Maximum staircase units
input int    InpMaxBarsBetweenBoxes = 20;   // Max bars to wait for next box before exiting
input double InpStopBuffer        = 0.3;    // Stop buffer below box bottom in ATR

input group "=== Risk ==="
input double InpRiskPercent       = 1.0;    // Risk per unit as percent of balance
input int    InpATRPeriod         = 14;     // ATR period

input group "=== General ==="
input int    InpMagicNumber       = 100701; // Magic number
input int    InpSlippage          = 10;     // Slippage in points
input bool   InpShowLabels        = true;   // Draw box and label objects on chart

"InpLookbackBars" defines how far back the engine looks when deciding whether a new high is a genuine extreme worth watching. Setting this to 50 on a daily chart means the high must be the highest high of the last 50 days—a meaningful new extreme, not just a local bounce. "InpConfirmSessions" is Darvas's original three-session rule but made configurable. "InpMaxBarsBetweenBoxes" is the stall detection timer that prevents the EA from holding open units through an extended sideways market where no new boxes are forming.

The Box State Structure

All data for the current box—top, bottom, confirmation counter, and volume—lives in one structure.

//+------------------------------------------------------------------+
//| Current box data                                                 |
//+------------------------------------------------------------------+
struct SDarvasBox
  {
   double            top;                   // Box top price (confirmed extreme)
   double            bottom;                // Box bottom price (lowest low during box period)
   int               confirm_count;         // Sessions the top has held without being exceeded
   double            avg_box_vol;           // Average tick volume during box period
   int               box_bars;              // Number of bars in the current box period
   bool              is_bull;               // true = bull box, false = bear box
   datetime          top_time;              // Time of the bar that set the box top
  };

//+------------------------------------------------------------------+
//| Staircase unit data                                              |
//+------------------------------------------------------------------+
struct SStaircaseUnit
  {
   double            entry_price;           // Entry price of this unit
   double            lots;                  // Lot size of this unit
   ulong             ticket;                // Position ticket
   double            box_top;               // Box top that triggered this unit's entry
   double            box_bottom;            // Box bottom at time of this unit's entry
  };

Global Variables

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
ENUM_DARVAS_STATE g_state    = STATE_IDLE;  // Current state machine state
SDarvasBox        g_box;                    // Current box being formed
SStaircaseUnit    g_units[4];               // Open staircase units
int               g_unit_count  = 0;        // Number of open units
double            g_shared_stop = 0;        // Shared stop level for all units
int               g_bars_since_break = 0;   // Bars since last breakout (stall detection)
CTrade            g_trade;                  // Trade execution
int               g_atr_handle = INVALID_HANDLE; // ATR handle
datetime          g_last_bar   = 0;         // Last processed bar time

Utility Functions

//+------------------------------------------------------------------+
//| Returns pip size for the current symbol                          |
//+------------------------------------------------------------------+
double PipSize()
  {
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   return (digits == 3 || digits == 5) ? _Point * 10.0 : _Point;
  }

//+------------------------------------------------------------------+
//| Computes lot size from risk percent and stop distance in pips    |
//+------------------------------------------------------------------+
double CalcLots(double sl_pips)
  {
   double balance   = AccountInfoDouble(ACCOUNT_BALANCE);
   double risk_amt  = balance * InpRiskPercent / 100.0;
   double tick_val  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   double pip_size  = PipSize();
   if(tick_size <= 0 || tick_val <= 0 || sl_pips <= 0)
      return 0;
   double pip_value = (pip_size / tick_size) * tick_val;
   double lots      = risk_amt / (sl_pips * pip_value);
   double step      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double min_lot   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double max_lot   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   lots = MathFloor(lots / step) * step;
   return MathMax(min_lot, MathMin(max_lot, lots));
  }

Chart Drawing Helpers

//+------------------------------------------------------------------+
//| Draws a rectangle representing the current box                   |
//+------------------------------------------------------------------+
void DrawBox(string name, datetime t1, datetime t2,
             double top, double bottom, color clr)
  {
   if(!InpShowLabels)
      return;
   string obj = "DVS_BOX_" + name;
   ObjectDelete(0, obj);
   ObjectCreate(0, obj, OBJ_RECTANGLE, 0, t1, top, t2, bottom);
   ObjectSetInteger(0, obj, OBJPROP_COLOR,   clr);
   ObjectSetInteger(0, obj, OBJPROP_STYLE,   STYLE_SOLID);
   ObjectSetInteger(0, obj, OBJPROP_WIDTH,   1);
   ObjectSetInteger(0, obj, OBJPROP_FILL,    true);
   ObjectSetInteger(0, obj, OBJPROP_BACK,    true);
   ObjectSetInteger(0, obj, OBJPROP_SELECTABLE, false);
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Places a text label at the specified time and price              |
//+------------------------------------------------------------------+
void DrawLabel(string name, datetime time, double price, string text, color clr)
  {
   if(!InpShowLabels)
      return;
   string obj = "DVS_" + name;
   ObjectDelete(0, obj);
   ObjectCreate(0, obj, OBJ_TEXT, 0, time, price);
   ObjectSetString(0,  obj, OBJPROP_TEXT, text);
   ObjectSetInteger(0, obj, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, 9);
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Removes all chart objects created by this EA                     |
//+------------------------------------------------------------------+
void ClearLabels()
  {
   int total = ObjectsTotal(0);
   for(int i = total - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i);
      if(StringFind(name, "DVS_") == 0)
         ObjectDelete(0, name);
     }
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Resets the current box structure to empty                        |
//+------------------------------------------------------------------+
void ResetBox()
  {
   g_box.top           = 0;
   g_box.bottom        = DBL_MAX;
   g_box.confirm_count = 0;
   g_box.avg_box_vol   = 0;
   g_box.box_bars      = 0;
   g_box.is_bull       = true;
   g_box.top_time      = 0;
  }

The box rectangle drawn on the chart is one of the most useful visual diagnostics in this EA. It shows exactly which price range the engine has identified as the current box, making it immediately obvious whether the detection logic is finding the same formations a manual Darvas trader would identify.

Scanning for a New Extreme

"CheckForExtreme()" runs in "STATE_IDLE." It checks whether the last completed bar set a new N-bar high or low—the prerequisite for box formation.

//+------------------------------------------------------------------+
//| Checks if the last bar set a new N-bar extreme                   |
//| Returns 1 for new high, -1 for new low, 0 for neither            |
//+------------------------------------------------------------------+
int CheckForExtreme(double atr)
  {
   double high1 = iHigh(_Symbol, PERIOD_CURRENT, 1);                      // Last bar high
   double low1  = iLow(_Symbol,  PERIOD_CURRENT, 1);                      // Last bar low
//--- New N-bar high
   double lookback_high = 0;
   for(int i = 2; i <= InpLookbackBars; i++)                              // From bar 2 to exclude current extreme bar
      if(iHigh(_Symbol, PERIOD_CURRENT, i) > lookback_high)
         lookback_high = iHigh(_Symbol, PERIOD_CURRENT, i);
   if(high1 > lookback_high)
     {
      g_box.top           = high1;                                        // Set box top
      g_box.bottom        = iLow(_Symbol, PERIOD_CURRENT, 1);             // Initial bottom
      g_box.confirm_count = 0;                                            // Reset counter
      g_box.avg_box_vol   = (double)iTickVolume(_Symbol, PERIOD_CURRENT, 1);
      g_box.box_bars      = 1;
      g_box.is_bull       = true;
      g_box.top_time      = iTime(_Symbol, PERIOD_CURRENT, 1);
      return 1;                                                           // Bull extreme found
     }
//--- New N-bar low
   double lookback_low = DBL_MAX;
   for(int i = 2; i <= InpLookbackBars; i++)
      if(iLow(_Symbol, PERIOD_CURRENT, i) < lookback_low)
         lookback_low = iLow(_Symbol, PERIOD_CURRENT, i);
   if(low1 < lookback_low)
     {
      g_box.top           = iHigh(_Symbol, PERIOD_CURRENT, 1);            // Initial top
      g_box.bottom        = low1;                                         // Set box bottom
      g_box.confirm_count = 0;
      g_box.avg_box_vol   = (double)iTickVolume(_Symbol, PERIOD_CURRENT, 1);
      g_box.box_bars      = 1;
      g_box.is_bull       = false;
      g_box.top_time      = iTime(_Symbol, PERIOD_CURRENT, 1);
      return -1;                                                          // Bear extreme found
     }
   return 0;                                                              // No new extreme
  }

The lookback starts at bar 2—not bar 1—when building the comparison range. This prevents the extreme bar from competing against itself in the lookback. The bar that sets the new high must be higher than all bars from index 2 to index InpLookbackBars," meaning it genuinely represents a new level the market has not reached in that period.

Building the Box

"UpdateBoxFormation()" runs in "STATE_BOX_FORMING." It updates the box bottom, tracks volume, increments the confirmation counter, and returns true when the box is confirmed.

//+------------------------------------------------------------------+
//| Updates the box being formed — returns true when box confirmed   |
//+------------------------------------------------------------------+
bool UpdateBoxFormation(double atr)
  {
   double high1 = iHigh(_Symbol, PERIOD_CURRENT, 1);
   double low1  = iLow(_Symbol,  PERIOD_CURRENT, 1);
   long   vol1  = iTickVolume(_Symbol, PERIOD_CURRENT, 1);
   g_box.box_bars++;                                                      // Increment box bar count
//--- Update running average volume during box period
   g_box.avg_box_vol = (g_box.avg_box_vol * (g_box.box_bars - 1) +
                        (double)vol1) / g_box.box_bars;                   // Running average
   if(g_box.is_bull)
     {
      //--- Update box bottom to track the low during formation
      if(low1 < g_box.bottom)
         g_box.bottom = low1;                                             // Expand bottom
      //--- If the high exceeded the box top, the top resets
      if(high1 > g_box.top)
        {
         g_box.top           = high1;                                     // New top
         g_box.confirm_count = 0;                                         // Reset counter
         g_box.top_time      = iTime(_Symbol, PERIOD_CURRENT, 1);         // Update time
         Print(StringFormat("DarvasBoxEA: Box top reset | New top:%.5f", g_box.top));
         return false;                                                    // Not yet confirmed
        }
      //--- Bar did not exceed top — increment confirmation counter
      g_box.confirm_count++;
      Print(StringFormat("DarvasBoxEA: Box confirm %d/%d | Top:%.5f | Bottom:%.5f",
                         g_box.confirm_count, InpConfirmSessions, g_box.top, g_box.bottom));
     }
   else
     {
      if(high1 > g_box.top)
         g_box.top = high1;                                               // Expand top
      if(low1 < g_box.bottom)
        {
         g_box.bottom        = low1;                                      // New bottom
         g_box.confirm_count = 0;
         Print(StringFormat("DarvasBoxEA: Box bottom reset | New bottom:%.5f", g_box.bottom));
         return false;
        }
      g_box.confirm_count++;
     }
//--- Box confirmed when confirmation counter reaches target
   if(g_box.confirm_count >= InpConfirmSessions)
     {
      //--- Validate box height
      double box_height = g_box.top - g_box.bottom;
      if(box_height < atr * InpMinBoxATR)
        {
         Print("DarvasBoxEA: Box too narrow — resetting.");
         return false;                                                    // Height check failed
        }
      //--- Draw the confirmed box on chart
      DrawBox(TimeToString(g_box.top_time, TIME_DATE),
              g_box.top_time,
              iTime(_Symbol, PERIOD_CURRENT, 1),
              g_box.top, g_box.bottom,
              g_box.is_bull ? C'200,230,200' : C'230,200,200');           // Green for bull, red for bear
      Print(StringFormat("DarvasBoxEA: Box confirmed | Top:%.5f | Bottom:%.5f | AvgVol:%.0f",
                         g_box.top, g_box.bottom, g_box.avg_box_vol));
      return true;                                                        // Box confirmed
     }
   return false;                                                          // Not yet confirmed
  }

The box top reset logic is critical. When the price exceeds the current box top during the confirmation period, the top is not immediately abandoned—it is simply reset to the new high, and the confirmation counter restarts. This allows Darvas's method to naturally extend the box when price makes a series of slightly higher highs before truly holding. The counter only reaches "InpConfirmSessions" when three consecutive bars all fail to exceed the current top.

Checking for a Breakout

"CheckBreakout()" runs in "STATE_WAITING_BREAKOUT." It validates the volume condition and the close above the box top.

//+------------------------------------------------------------------+
//| Checks whether the last bar broke out of the confirmed box       |
//| Returns 1 for bull breakout, -1 for bear, 0 for none             |
//+------------------------------------------------------------------+
int CheckBreakout(double atr)
  {
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                    // Last bar close
   long   vol1   = iTickVolume(_Symbol, PERIOD_CURRENT, 1);               // Last bar volume
//--- Volume check: breakout volume must exceed box average by InpBreakVolMult
   if((double)vol1 < g_box.avg_box_vol * InpBreakVolMult)
      return 0;                                                           // Volume insufficient
//--- Bull breakout: close above box top
   if(g_box.is_bull && close1 > g_box.top)
     {
      Print(StringFormat("DarvasBoxEA: BULL breakout | Close:%.5f > Top:%.5f | Vol:%I64d | AvgVol:%.0f",
                         close1, g_box.top, vol1, g_box.avg_box_vol));
      return 1;
     }
//--- Bear breakout: close below box bottom
   if(!g_box.is_bull && close1 < g_box.bottom)
     {
      Print(StringFormat("DarvasBoxEA: BEAR breakout | Close:%.5f < Bottom:%.5f | Vol:%I64d | AvgVol:%.0f",
                         close1, g_box.bottom, vol1, g_box.avg_box_vol));
      return -1;
     }
   return 0;                                                              // No breakout
  }

Opening a Staircase Unit

"OpenUnit()" opens one staircase unit and stores its data. It is called on both the first breakout and on each subsequent box breakout while the trade is open.

//+------------------------------------------------------------------+
//| Opens one staircase unit and stores it in the units array        |
//+------------------------------------------------------------------+
bool OpenUnit(bool is_long, double stop_level, double atr)
  {
   if(g_unit_count >= InpMaxUnits)
      return false;                                                       // Unit cap reached
   double sl_buffer = atr * InpStopBuffer;                                // ATR buffer below/above box bottom
   double sl        = is_long
                      ? NormalizeDouble(stop_level - sl_buffer, _Digits)  // Stop below box bottom
                      : NormalizeDouble(stop_level + sl_buffer, _Digits); // Stop above box top
   double sl_pips   = MathAbs((is_long
                               ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
                               : SymbolInfoDouble(_Symbol, SYMBOL_BID)) - sl) / PipSize();
   double lots      = CalcLots(sl_pips);
   if(lots <= 0)
      return false;
   long   stop_lv   = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double min_dist  = stop_lv * _Point;
   bool   ok        = false;
   if(is_long)
     {
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      if(ask - sl < min_dist)
         sl = NormalizeDouble(ask - min_dist - _Point, _Digits);
      ok = g_trade.Buy(lots, _Symbol, ask, sl, 0,
                       "Darvas U" + IntegerToString(g_unit_count + 1));
      if(ok)
        {
         g_units[g_unit_count].entry_price = ask;
         g_units[g_unit_count].lots        = lots;
         ulong deal = g_trade.ResultDeal();
         if(deal > 0 && HistoryDealSelect(deal))
            g_units[g_unit_count].ticket = (ulong)HistoryDealGetInteger(deal, DEAL_POSITION_ID);
         else
            g_units[g_unit_count].ticket = g_trade.ResultOrder();
         g_units[g_unit_count].box_top    = g_box.top;
         g_units[g_unit_count].box_bottom = g_box.bottom;
         g_shared_stop = sl;                                              // Update shared stop
         g_unit_count++;
         datetime t = iTime(_Symbol, PERIOD_CURRENT, 1);
         DrawLabel("U" + IntegerToString(g_unit_count), t,
                   iLow(_Symbol, PERIOD_CURRENT, 1) - PipSize() * 5,
                   "U" + IntegerToString(g_unit_count), clrDodgerBlue);   // Draw unit label
         Print(StringFormat("DarvasBoxEA: LONG U%d | Lots:%.2f | Entry:%.5f | SL:%.5f",
                            g_unit_count, lots, ask, sl));
        }
     }
   else
     {
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      if(sl - bid < min_dist)
         sl = NormalizeDouble(bid + min_dist + _Point, _Digits);
      ok = g_trade.Sell(lots, _Symbol, bid, sl, 0,
                        "Darvas U" + IntegerToString(g_unit_count + 1));
      if(ok)
        {
         g_units[g_unit_count].entry_price = bid;
         g_units[g_unit_count].lots        = lots;
         ulong deal = g_trade.ResultDeal();
         if(deal > 0 && HistoryDealSelect(deal))
            g_units[g_unit_count].ticket = (ulong)HistoryDealGetInteger(deal, DEAL_POSITION_ID);
         else
            g_units[g_unit_count].ticket = g_trade.ResultOrder();
         g_units[g_unit_count].box_top    = g_box.top;
         g_units[g_unit_count].box_bottom = g_box.bottom;
         g_shared_stop = sl;
         g_unit_count++;
         datetime t = iTime(_Symbol, PERIOD_CURRENT, 1);
         DrawLabel("U" + IntegerToString(g_unit_count), t,
                   iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize() * 5,
                   "U" + IntegerToString(g_unit_count), clrOrangeRed);
         Print(StringFormat("DarvasBoxEA: SHORT U%d | Lots:%.2f | Entry:%.5f | SL:%.5f",
                            g_unit_count, lots, bid, sl));
        }
     }
   return ok;
  }

Moving the Shared Stop

When a new box forms and a new unit is added, all existing units' stops move to the new box bottom. This is the staircase mechanism—every unit that was entered on a lower box now gets a higher floor.

//+------------------------------------------------------------------+
//| Moves the stop for all open units to the new shared stop level   |
//+------------------------------------------------------------------+
void MoveSharedStop(double new_stop, bool is_long)
  {
//--- Only advance the stop, never retreat
   if(is_long  && new_stop <= g_shared_stop)
      return;
   if(!is_long && g_shared_stop > 0 && new_stop >= g_shared_stop)
      return;
   bool all_ok = true;
   for(int i = 0; i < g_unit_count; i++)                                  // Modify each unit
     {
      if(!PositionSelectByTicket(g_units[i].ticket))
         continue;
      if(!g_trade.PositionModify(g_units[i].ticket, new_stop, 0))
        {
         Print("DarvasBoxEA: Stop modify failed | Ticket:", g_units[i].ticket);
         all_ok = false;
        }
     }
   if(all_ok)
     {
      g_shared_stop = new_stop;                                           // Update shared stop
      Print(StringFormat("DarvasBoxEA: Shared stop moved to %.5f", new_stop));
     }
  }

Closing All Units

"CloseAllUnits()" closes all open staircase units and resets the state completely.

//+------------------------------------------------------------------+
//| Closes all open staircase units and resets the EA state          |
//+------------------------------------------------------------------+
void CloseAllUnits(string reason)
  {
   double total_profit = 0;
   for(int i = g_unit_count - 1; i >= 0; i--)                             // Close in reverse order
     {
      if(!PositionSelectByTicket(g_units[i].ticket))
         continue;
      total_profit += PositionGetDouble(POSITION_PROFIT);
      if(!g_trade.PositionClose(g_units[i].ticket))
         Print("DarvasBoxEA: Close failed | Ticket:", g_units[i].ticket);
     }
   Print(StringFormat("DarvasBoxEA: Staircase closed | Reason:%s | P&L:%.2f | Units:%d",
                      reason, total_profit, g_unit_count));
   g_unit_count       = 0;
   g_shared_stop      = 0;
   g_bars_since_break = 0;
   g_state            = STATE_IDLE;
   ResetBox();
   ClearLabels();
  }

Checking the Stop

"CheckStop()" runs on every bar while units are open. It checks whether the last bar's close has violated the shared stop level.

//+------------------------------------------------------------------+
//| Returns true if the shared stop has been violated                |
//+------------------------------------------------------------------+
bool CheckStop(bool is_long)
  {
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);
   if(is_long  && close1 < g_shared_stop)
      return true;                                                        // Close below bull stop
   if(!is_long && close1 > g_shared_stop)
      return true;                                                        // Close above bear stop
   return false;
  }

The State Machine

"ProcessStateMachine()" is called once per bar and routes execution through each state.

//+------------------------------------------------------------------+
//| Processes the Darvas Box state machine for one bar               |
//+------------------------------------------------------------------+
void ProcessStateMachine(double atr)
  {
   switch(g_state)
     {
      //--------------------------------------------------------------
      case STATE_IDLE:
        {
         int extreme = CheckForExtreme(atr);
         if(extreme != 0)
           {
            g_state = STATE_BOX_FORMING;                                  // Extreme found—begin box formation
            Print(StringFormat("DarvasBoxEA: Extreme set | %s | Price:%.5f",
                               extreme > 0 ? "BULL" : "BEAR",
                               extreme > 0 ? g_box.top : g_box.bottom));
           }
        }
      break;
      //--------------------------------------------------------------
      case STATE_BOX_FORMING:
        {
         if(UpdateBoxFormation(atr))
            g_state = STATE_WAITING_BREAKOUT;                             // Box confirmed
        }
      break;
      //--------------------------------------------------------------
      case STATE_WAITING_BREAKOUT:
        {
         int breakout = CheckBreakout(atr);
         if(breakout == 0)
            break;                                                        // No breakout yet
         bool   is_long   = (breakout == 1);
         double stop_level = is_long ? g_box.bottom : g_box.top;          // Stop at opposite boundary
         if(OpenUnit(is_long, stop_level, atr))                           // Open first unit
           {
            g_bars_since_break = 0;
            g_state = STATE_IN_TRADE;
            ResetBox();                                                   // Reset box — watch for next
           }
         else
           {
            g_state = STATE_IDLE;
            ResetBox();
           }
        }
      break;
      //--------------------------------------------------------------
      case STATE_IN_TRADE:
        {
         bool is_long = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
         if(!PositionSelectByTicket(g_units[0].ticket))
           {
            //--- All positions were closed externally (stop hit by broker)
            CloseAllUnits("External Close");
            break;
           }
         //--- Stop check
         if(CheckStop(is_long))
           { CloseAllUnits("Stop Hit"); break; }
         //--- Stall detection: too many bars without a new box
         g_bars_since_break++;
         if(g_bars_since_break > InpMaxBarsBetweenBoxes)
           { CloseAllUnits("Stall—No New Box"); break; }
         //--- Begin watching for next box
         g_state = STATE_WATCHING_NEW_BOX;
         ResetBox();                                                      // Fresh box scan
        }
      break;
      //--------------------------------------------------------------
      case STATE_WATCHING_NEW_BOX:
        {
         bool is_long = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
         if(!PositionSelectByTicket(g_units[0].ticket))
           { CloseAllUnits("External Close"); break; }
         //--- Stop check always runs first
         if(CheckStop(is_long))
           { CloseAllUnits("Stop Hit"); break; }
         g_bars_since_break++;
         if(g_bars_since_break > InpMaxBarsBetweenBoxes)
           { CloseAllUnits("Stall—No New Box"); break; }
         //--- Run box detection in parallel with open trade
         if(g_box.top == 0 && g_box.bottom == DBL_MAX)                    // No box yet started
           {
            int extreme = CheckForExtreme(atr);
            if(extreme != 0)
               Print(StringFormat("DarvasBoxEA: Next extreme set | %s | Price:%.5f",
                                  extreme > 0 ? "BULL" : "BEAR",
                                  extreme > 0 ? g_box.top : g_box.bottom));
           }
         else
            if(g_box.confirm_count < InpConfirmSessions)
              {
               UpdateBoxFormation(atr);                                   // Continue building new box
              }
            else
              {
               //--- New box confirmed — check for breakout and add unit
               int breakout = CheckBreakout(atr);
               if(breakout != 0 && g_unit_count < InpMaxUnits)
                 {
                  double stop_level = is_long ? g_box.bottom : g_box.top;
                  double sl_with_buf = is_long
                                       ? NormalizeDouble(stop_level - atr * InpStopBuffer, _Digits)
                                       : NormalizeDouble(stop_level + atr * InpStopBuffer, _Digits);
                  if(OpenUnit(is_long, stop_level, atr))
                    {
                     MoveSharedStop(sl_with_buf, is_long);                // Raise stop for all units
                     g_bars_since_break = 0;                              // Reset stall counter
                     ResetBox();                                          // Watch for next box
                     Print(StringFormat("DarvasBoxEA: Staircase now %d units | SharedStop:%.5f",
                                        g_unit_count, g_shared_stop));
                    }
                 }
              }
        }
      break;
     }
  }
"STATE WATCHING NEW BOX" is the most complex part. It monitors stop violations while also running the next-box detection steps: extreme scan, box formation, and breakout check. The stall counter runs in both trade states, ensuring the EA does not hold open units indefinitely through a sideways market where no new boxes are forming.

OnInit, OnDeinit, and OnTick

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   g_atr_handle = iATR(_Symbol, PERIOD_CURRENT, InpATRPeriod);            // Create ATR handle
   if(g_atr_handle == INVALID_HANDLE)
     {
      Print("DarvasBoxEA: ATR handle creation failed.");
      return INIT_FAILED;
     }
   g_trade.SetExpertMagicNumber(InpMagicNumber);
   g_trade.SetDeviationInPoints(InpSlippage);
   g_state            = STATE_IDLE;
   g_unit_count       = 0;
   g_shared_stop      = 0;
   g_bars_since_break = 0;
   g_last_bar         = 0;
   ResetBox();
   Print(StringFormat("DarvasBoxEA initialized | Symbol:%s | TF:%s | Magic:%d",
                      _Symbol, EnumToString(Period()), InpMagicNumber));
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(g_atr_handle);                                         // Release ATR handle
   ClearLabels();                                                          // Remove chart objects
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(current_bar == g_last_bar)
      return;                                                             // One execution per bar
   g_last_bar = current_bar;
   double atr_buf[];
   ArraySetAsSeries(atr_buf, true);
   if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
      return;
   double atr = atr_buf[0];                                               // ATR from last completed bar
   ProcessStateMachine(atr);                                              // Run state machine
  }
//+------------------------------------------------------------------+

What to Expect

Darvas boxes on EURUSD (D1) are not rare, but genuine staircases—three or four boxes in sequence—are the exception rather than the rule. Expect between 10 and 20 initial breakout signals per year, with perhaps 5 to 10 of those developing into multi-unit staircases. The journal provides complete diagnostics at every stage: extreme detection, box formation progress, confirmation count, breakout validation with volume ratio, unit opens, shared stop moves, and all close reasons.

The box rectangles drawn on the chart during testing are the most useful visual verification tool. Each confirmed box should be visible as a shaded rectangle with the breakout bar visible above or below it. The staircase of unit labels—U1, U2, and U3—should appear above each successive box breakout bar.

If the EA is generating too many false signals, increase "InpBreakVolMult" to 1.5 or increase "InpLookbackBars" to 75. Furthermore, if staircase exits are firing prematurely, increase "InpMaxBarsBetweenBoxes" to 30. If no boxes are forming at all, reduce "InpMinBoxATR" to 0.3 or reduce "InpLookbackBars" to 30.


DarvasBoxEA demonstration

Fig. 3. Visual demonstration of the system.

Known Limitations

The box formation detection requires the price not to exceed the box top for at least "InpConfirmSessions" consecutive sessions. In markets where price makes a series of very similar highs that differ only by a few pips, the top resets repeatedly, and the box takes much longer to confirm than Darvas intended. On daily timeframes with significant daily ranges, this is rarely a problem. On shorter timeframes where daily ATR is small relative to the three-pip difference between similar highs, it can cause the box to rebuild repeatedly. The EA is recommended on daily timeframes for this reason.

The stall detection mechanism—"InpMaxBarsBetweenBoxes"—closes all units if no new box forms within the configured bar count. In a strong trend where price moves steadily in one direction without forming a new consolidation box, this will close the staircase prematurely. Darvas himself held positions through extended trends without new boxes as long as the price did not close below the last box bottom. A future extension would replace the stall counter with a trailing stop that activates when no new box forms, rather than a hard close.

Volume is measured in tick volume. On retail forex platforms, tick volume is the standard proxy for real activity. The specific multipliers—"InpBoxVolMult" for box period volume and "InpBreakVolMult" for breakout confirmation—may need calibration per broker. Tick volume behavior varies across data providers, and a multiplier that produces reliable signals on one broker's EURUSD feed may be too tight or too loose on another's.

The staircase uses equal risk sizing per unit. Darvas's original approach was more aggressive—later units in a successful staircase sometimes received larger allocations because the prior units were already profitable and the risk to the overall account was lower. The equal-risk approach is more conservative and more appropriate for modern Forex account structures.

Conclusion

Nicolas Darvas proved that a retail trader with no financial background, no access to real-time data, and no direct market contact could outperform professional fund managers by following a systematic, rules-based method. The method he used was not complicated. Its power came from two things: the clarity of its signals—a box, a breakout, and a stop—and the discipline with which he applied them.

The automation challenge is that same discipline. The box detection must run correctly through the three-session confirmation without premature firing. The staircase must add units on each new box breakout while simultaneously monitoring all existing units for stop violations. The shared stop must advance with each new box and never retreat. None of these rules require sophisticated mathematics. All of them require precise sequential logic that breaks down the moment the sequence is violated.

The state machine in this article enforces that sequence exactly. "STATE_WATCHING_NEW_BOX" is the state that most Darvas implementations omit—the parallel operation of monitoring open trades and scanning for the next box simultaneously. Without it, the staircase mechanism cannot work. With it, the EA produces the behavior Darvas described: enter on the first box breakout, add on each subsequent box, raise the floor with each new box, and exit when the floor is violated.

The EA code was compiled and tested in MetaTrader 5. Copy "DarvasBoxEA.mq5" to "MQL5\Experts\" and compile in MetaEditor with no additional dependencies. Recommended for EURUSD and GBPUSD on the daily timeframe. Always test on a demo account before live deployment.

Attached files |
DarvasBoxEA.mq5 (29.81 KB)
Machine Learning in Pure MQL5 (Part 1): Logistic Regression from Scratch with SGD Machine Learning in Pure MQL5 (Part 1): Logistic Regression from Scratch with SGD
The series develops machine learning in 100% native MQL5 with no external dependencies. Part 1 delivers logistic regression from first principles: a CLogReg class with standardization, a stable sigmoid, SGD training, and model persistence, plus a script that builds ATR-normalized features, labels the next bar, and tests out-of-sample against a baseline. Readers get a compact include file and a clear template for leakage-free evaluation.
From Basic to Intermediate: Queues, Lists, and Trees (VII) From Basic to Intermediate: Queues, Lists, and Trees (VII)
In this article, we will clearly and simply demonstrate and explain how to remove a node from a tree. This process usually confuses beginners rather than helping them understand how it's done and why it needs to be done that way.
Beetle Swarm Optimization (BSO) Beetle Swarm Optimization (BSO)
We consider a BAS+PSO (BSO) hybrid, where BAS provides a local direction signal and PSO facilitates the exchange of best solutions within the swarm. The article presents a mathematical model, pseudocode, an implementation of the class in MQL5, and test results from a standard test bench. This material allows reproducing the algorithm, configuring its parameters, and understanding how three objective-function evaluations per iteration affect efficiency.
Market Simulation: Position View (XVII) Market Simulation: Position View (XVII)
In the previous article, we configured the indicator to display the financial result. However, not everyone likes using this display mode. The reasons differ from one trader to another, although in some cases they seem quite reasonable and justified to me. Adapting the code to provide this capability is by no means one of the most difficult tasks. It's actually pretty simple. In this article, we'll look at how to do this.