preview
Automating Classic Market Methods in MQL5 (Part 5): The Original Turtle Trading Rules

Automating Classic Market Methods in MQL5 (Part 5): The Original Turtle Trading Rules

MetaTrader 5Trading |
280 0
Tola Moses Hector
Tola Moses Hector

Introduction

The Turtle Trading experiment is one of the most famous tests of systematic trading ever conducted. Dennis and Eckhardt did not teach the Turtles to use judgment or intuition. They taught them a set of mechanical rules that could be written down, followed precisely, and applied to any liquid market. The rules were designed to be trend-following, to risk a fixed and consistent percentage of equity on every trade, and to scale into winning positions while cutting losses quickly.

The original rules were kept secret for over a decade. In 2003, former Turtle Curtis Faith published them in full. They have since been studied, debated, and referenced widely—but rarely implemented correctly in MQL5 code. The reason is not complexity. The individual rules are straightforward. Correct implementation requires understanding why each rule exists and what fails when it is applied carelessly.

This article presents a self-contained Expert Advisor, "TurtleEA," that implements both original Turtle systems as described by Curtis Faith in “Way of the Turtle.” Every design decision maps directly to a specific original rule. The EA is tested on EURUSD across a multi-year period to verify that the implementation behaves as the rules intend.

We will cover the following topics:

  1. The Turtle Rules—Theory and Architecture
  2. The N Value—Volatility-Adjusted Risk
  3. System 1 and System 2—The Entry Rules
  4. Implementation in MQL5
  5. Backtesting
  6. Known Limitations
  7. Conclusion


The Turtle Rules—Theory and Architecture

The Turtles traded two systems simultaneously. Dennis referred to them as System 1 and System 2.

The turtle system at a glance

Fig. 1. The turtle system at a glance

System 1 was a 20-day breakout. A long entry occurred when the price crossed above the highest high of the previous 20 days. A short entry occurred when the price crossed below the lowest low of the previous 20 days. The signal was skipped if the previous System 1 breakout in the same direction was a winner—this filter was called the "skip rule" and was designed to prevent entries in established trends that had already run.

System 2 was a 55-day breakout. A long entry occurred when the price crossed above the highest high of the previous 55 days. A short entry occurred when the price crossed below the lowest low of the previous 55 days. System 2 had no skip rule. Every breakout was taken.

The exit rules were identical for both systems. Long positions were exited when the price crossed below the lowest low of the previous 10 days (System 1) or 20 days (System 2). Short positions were exited when the price crossed above the highest high of the same lookback.

The position sizing, stops, and pyramid rules were the same for both systems.

What Made the Rules Work

The rules do not predict direction. They do not use indicators. They do not analyze fundamentals. Likewise, they simply define the conditions under which a price has moved far enough in one direction to warrant a bet that it will continue.

The critical insight is that the rules manage risk, not prediction. The stop is placed at exactly 2N below the entry price for longs and 2N above for shorts, where N is the 20-day average true range. This means the maximum loss on any single unit is exactly 1% of account equity—always, regardless of the instrument or the volatility of the moment. When four units are open on the same position, the maximum loss is still controlled because each unit is sized by the same N formula.

The four-unit pyramid adds to winners at defined intervals. Each add-on occurs when the price moves one additional N in the direction of the trade from the previous entry. The EA adds only to winners and keeps a 2N stop from the most recent entry. As a result, total risk across four units remains bounded.

Architecture of the EA

The EA is built around five components. The "N Value Calculator" computes the 20-day ATR using the Wilder smoothing method, which is the correct method for the original rules. The "System 1 Detector" watches for 20-day breakouts and applies the skip rule. The "System 2 Detector" watches for 55-day breakouts. The "Unit Manager" handles position sizing, pyramid additions, and stop management. The "Exit Monitor" checks exit conditions on every bar.

All five components share a single state structure that tracks the current unit count, the entry prices of all open units, the current unified stop, and whether the last System 1 signal in each direction was a winner.


The N Value—Volatility-Adjusted Risk

N is the foundation of the entire risk management system. Every position size, every stop, and every pyramid interval is expressed in units of N.

Position sizing

Fig. 2. Position sizing

N is the 20-day exponential average of True Range, computed using Wilder's smoothing. True Range for a bar is the maximum of three values: the current high minus the current low, the current high minus the previous close, and the previous close minus the current low.

The Wilder smoothing formula is:

Current N = (Previous N × 19 + Current True Range) / 20.

This differs from a standard 20‑period ATR because the initial value is seeded differently, even though the smoothing ratio is the same. To compute N correctly, we initialize with the simple average of the first 20 true ranges and then apply Wilder smoothing from bar 21 onward.

The dollar value of one N is computed using the same tick value approach from previous articles in this series:

N_dollars = (N / tick_size) × tick_value × 1_lot

This converts N from a price distance to a monetary amount per standard lot. To risk exactly 1% of equity per unit, we divide the equity risk amount by N_dollars to get the lot size.

Position size per unit = (Account Equity × 0.01) / N_dollars

This formula ensures that when the stop fires—2N away from entry—the maximum loss is exactly 1% of equity. The Turtles called this one unit. A full position of four units risked 4% of equity if all stops fired simultaneously, which was acceptable under their framework.


System 1 and System 2—The Entry Rules

System 1: 20-Day Breakout with Skip Rule

A long signal fires when the current bar's high exceeds the highest high of the previous 20 bars. A short signal fires when the current bar's low falls below the lowest low of the previous 20 bars.

The skip rule: if the previous System 1 breakout in the same direction was a profitable trade—meaning the price moved favorably before the stop or exit fired—the current breakout is skipped. Only losing breakouts reset the permission to trade. This rule was designed to prevent entries during established trends where the Turtles believed the move had already been captured.

System 2: 55-Day Breakout, No Skip Rule

A long signal fires when the current bar's high exceeds the highest high of the previous 55 bars. A short signal fires when the current bar's low falls below the lowest low of the previous 55 bars. Every signal is taken.

Pyramid Rules

After the initial entry, the EA adds one unit each time the price moves N/2 in the direction of the trade from the previous entry price. A maximum of four units may be open simultaneously on a single market in a single direction.

Pyramid rules

Fig. 3. Pyramid rules

Stop Management

All open units share a single stop. When a new unit is added, the stop for all existing units is moved to 2N below the new entry price for longs or 2N above for shorts. This means earlier units end up with tighter stops as the pyramid grows, locking in profit from the earlier entries while protecting the new unit at the maximum allowed risk.

When to Exit

System 1 longs exit when the price crosses below the lowest low of the previous 10 bars, while system 1 shorts exit when the price crosses above the highest high of the previous 10 bars. Likewise, System 2 longs exit when the price crosses below the lowest low of the previous 20 bars, while System 2 shorts exit when the price crosses above the highest high of the previous 20 bars.

An emergency exit fires if the price crosses 2N in the wrong direction from the most recent entry. This is the hard stop and fires regardless of the system.


Implementation in MQL5

The EA is built section by section.

Includes, Enumeration, and Input Parameters
//+------------------------------------------------------------------+
//|                                                     TurtleEA.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 "Original Turtle Trading Rules — System 1 and System 2"
#property description "N-based position sizing, four-unit pyramid, 2N stop"
#property description "Daily timeframe recommended"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Which system to run                                              |
//+------------------------------------------------------------------+
enum ENUM_TURTLE_SYSTEM
  {
   SYSTEM_1,    // System 1 — 20-day breakout with skip rule
   SYSTEM_2,    // System 2 — 55-day breakout, no skip rule
   BOTH         // Run both simultaneously
  };

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input group "=== System Selection ==="
input ENUM_TURTLE_SYSTEM InpSystem     = BOTH;       // Turtle system to use

input group "=== System 1 ==="
input int    InpS1Entry               = 20;          // System 1 entry breakout period
input int    InpS1Exit                = 10;          // System 1 exit breakout period

input group "=== System 2 ==="
input int    InpS2Entry               = 55;          // System 2 entry breakout period
input int    InpS2Exit                = 20;          // System 2 exit breakout period

input group "=== Risk and Sizing ==="
input double InpRiskPerUnit           = 1.0;         // Risk per unit as percent of equity
input int    InpMaxUnits              = 4;           // Maximum units per direction
input int    InpATRPeriod             = 20;          // N calculation period

input group "=== General ==="
input int    InpMagicNumber           = 333001;      // Magic number
input int    InpSlippage              = 10;          // Slippage in points
input bool   InpShowLabels            = true;        // Draw labels on chart

The inputs are split by system. System 1 defaults to a 20-day entry and a 10-day exit. System 2 defaults to a 55-day entry and a 20-day exit. These are the exact parameters Curtis Faith documented. The "BOTH" option runs both systems simultaneously, which is how the original Turtles operated—system 1 caught more trades, and system 2 caught bigger trends.

The Turtle State Structure

The state structure tracks everything needed across the life of an open position. One instance handles longs, and another handles shorts.

//+------------------------------------------------------------------+
//| State for one direction (long or short)                          |
//+------------------------------------------------------------------+
struct STurtleState
  {
   int               units_open;         // Number of units currently open (0-4)
   double            entry_prices[4];    // Entry price of each unit
   double            lots[4];            // Lot size of each unit
   ulong             tickets[4];         // Position ticket of each unit
   double            unified_stop;       // Current stop for all units
   double            last_n;             // N value at first entry
   bool              last_s1_winner;     // Was the last System 1 trade a winner?
   bool              active;             // Is this direction currently active?
  };

The "last_s1_winner" flag implements the skip rule. It is set to true when a System 1 position closes with a profit and resets to false when a System 1 position closes with a loss. The next System 1 signal in that direction is only acted on if this flag is false.

Global Variables and the N Value

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
STurtleState  g_long;                    // Long position state
STurtleState  g_short;                   // Short position state
CTrade        g_trade;                   // Trade execution
double        g_n         = 0;           // Current N value
datetime      g_last_bar  = 0;           // Last processed bar time

N is stored as a global and updated once per bar. Both systems use the same N value, computed from the same ATR period.

Computing N

The Turtles used Wilder's smoothed ATR, not the standard simple-average ATR. We compute this manually to ensure accuracy.

//+------------------------------------------------------------------+
//| 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 N using Wilder smoothing — correct Turtle method        |
//+------------------------------------------------------------------+
double ComputeN()
  {
   int bars_needed = InpATRPeriod * 3 + 2;
   double high[], low[], close[];
   ArraySetAsSeries(high,  true);
   ArraySetAsSeries(low,   true);
   ArraySetAsSeries(close, true);
   if(CopyHigh(_Symbol,  PERIOD_CURRENT, 1, bars_needed, high)  < bars_needed)
      return g_n;
   if(CopyLow(_Symbol,   PERIOD_CURRENT, 1, bars_needed, low)   < bars_needed)
      return g_n;
   if(CopyClose(_Symbol, PERIOD_CURRENT, 1, bars_needed, close) < bars_needed)
      return g_n;
//--- Compute true ranges oldest to newest (index 0 = most recent bar 1)
   double tr[];
   ArrayResize(tr, bars_needed);
   for(int i = bars_needed - 2; i >= 0; i--)
     {
      double hl  = high[i] - low[i];
      double hpc = MathAbs(high[i]  - close[i + 1]);
      double lpc = MathAbs(low[i]   - close[i + 1]);
      tr[i] = MathMax(hl, MathMax(hpc, lpc));
     }
//--- Seed: simple average of oldest InpATRPeriod true ranges
   int    start = bars_needed - 2; // oldest valid bar (needs prev close)
   double seed  = 0;
   for(int i = 0; i < InpATRPeriod; i++)
      seed += tr[start - i];
   seed /= InpATRPeriod;
//--- Wilder smoothing forward to bar 1
   double n = seed;
   for(int i = start - InpATRPeriod; i >= 0; i--)
      n = (n * (InpATRPeriod - 1) + tr[i]) / InpATRPeriod;
   return n;
  }

The function copies three times the ATR period in bars to ensure the seed average is computed on genuinely historical data. It then applies Wilder smoothing forward to the most recent completed bar. The result is the N value that the original Turtles would have used.

Position Sizing

One unit is sized so that 1% of equity is at risk when the stop fires at 2N away.

//+------------------------------------------------------------------+
//| Computes one unit lot size — risk 1% on a 2N stop                |
//+------------------------------------------------------------------+
double CalcUnitLots(double n_value)
  {
   if(n_value <= 0)
      return 0;
   double equity    = AccountInfoDouble(ACCOUNT_EQUITY);
   double risk_amt  = equity * InpRiskPerUnit / 100.0;
   double tick_val  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   if(tick_size <= 0 || tick_val <= 0)
      return 0;
   double n_dollars = (n_value / tick_size) * tick_val; // monetary value of 1N per lot
   if(n_dollars <= 0)
      return 0;
   double lots = risk_amt / (n_dollars * 2.0); // 2N stop distance
   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));
  }

The risk amount is 1% of equity. The stop is 2N away. So to find the lot size where a 2N move equals 1% of equity, we divide the risk amount by (n_dollars × 2). This is the exact calculation the Turtles used.

Breakout Detection

We need the highest high and lowest low of a given lookback, evaluated on the bar just closed.

//+------------------------------------------------------------------+
//| Highest high of bars [from_bar .. from_bar+period-1]             |
//| from_bar=1 means start at last completed bar                     |
//| from_bar=2 means EXCLUDE last bar — used for breakout comparison |
//+------------------------------------------------------------------+
double GetHighest(int from_bar, int period)
  {
   double high[];
   ArraySetAsSeries(high, true);
   if(CopyHigh(_Symbol, PERIOD_CURRENT, from_bar, period, high) < period)
      return 0;
   double result = high[0];
   for(int i = 1; i < period; i++)
      if(high[i] > result)
         result = high[i];
   return result;
  }

//+------------------------------------------------------------------+
//| Lowest low of bars [from_bar .. from_bar+period-1]               |
//+------------------------------------------------------------------+
double GetLowest(int from_bar, int period)
  {
   double low[];
   ArraySetAsSeries(low, true);
   if(CopyLow(_Symbol, PERIOD_CURRENT, from_bar, period, low) < period)
      return DBL_MAX;
   double result = low[0];
   for(int i = 1; i < period; i++)
      if(low[i] < result)
         result = low[i];
   return result;
  }

These functions copy from bar 1, not bar 0, so they always evaluate on fully completed bars. This prevents false signals from the current open bar.

Chart Drawing Helpers

//+------------------------------------------------------------------+
//| Places a text label on the chart                                 |
//+------------------------------------------------------------------+
void DrawLabel(string name, datetime time, double price, string text, color clr)
  {
   if(!InpShowLabels)
      return;
   string obj = "TRT_" + 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, "TRT_") == 0)
         ObjectDelete(0, name);
     }
   ChartRedraw(0);
  }

Opening a Unit

//+------------------------------------------------------------------+
//| Resets one state structure to empty                              |
//+------------------------------------------------------------------+
void ResetState(STurtleState &state)
  {
   state.units_open   = 0;
   state.unified_stop = 0;
   state.last_n       = 0;
   state.active       = false;
   for(int i = 0; i < 4; i++)
     {
      state.entry_prices[i] = 0;
      state.lots[i]         = 0;
      state.tickets[i]      = 0;
     }
  }

//+------------------------------------------------------------------+
//| Opens one unit and updates the state structure                   |
//+------------------------------------------------------------------+
bool OpenUnit(STurtleState &state, bool is_long, double n_value, string label)
  {
   if(state.units_open >= InpMaxUnits)
      return false;
   double lots = CalcUnitLots(n_value);
   if(lots <= 0)
      return false;
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double entry_price = is_long ? ask : bid;
   double stop_dist   = n_value * 2.0;
   double new_stop    = is_long
                        ? NormalizeDouble(entry_price - stop_dist, _Digits)
                        : NormalizeDouble(entry_price + stop_dist, _Digits);
   long   stop_lv  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double min_dist = stop_lv * _Point;
   if(is_long  && entry_price - new_stop < min_dist)
      new_stop = NormalizeDouble(entry_price - min_dist - _Point, _Digits);
   if(!is_long && new_stop - entry_price < min_dist)
      new_stop = NormalizeDouble(entry_price + min_dist + _Point, _Digits);
   bool ok = is_long
             ? g_trade.Buy(lots,  _Symbol, ask, new_stop, 0, "Turtle " + label)
             : g_trade.Sell(lots, _Symbol, bid, new_stop, 0, "Turtle " + label);
   if(!ok)
     {
      Print("TurtleEA: Order failed | Retcode:", g_trade.ResultRetcode(),
            " | Error:", GetLastError());
      return false;
     }
//--- Capture position ticket via deal record (reliable on hedging)
   ulong ticket = 0;
   ulong deal   = g_trade.ResultDeal();
   if(deal > 0 && HistoryDealSelect(deal))
      ticket = (ulong)HistoryDealGetInteger(deal, DEAL_POSITION_ID);
   if(ticket == 0)
      ticket = g_trade.ResultOrder();
   int idx = state.units_open;
   state.entry_prices[idx] = entry_price;
   state.lots[idx]         = lots;
   state.tickets[idx]      = ticket;
   state.unified_stop      = new_stop;
   if(state.units_open == 0)
      state.last_n = n_value; // store N at first entry only
   state.units_open++;
   state.active = true;
   Print(StringFormat("TurtleEA: Unit %d opened | %s | Entry:%.5f | Stop:%.5f | Lots:%.2f | N:%.5f",
                      state.units_open, label, entry_price, new_stop, lots, n_value));
   datetime t = iTime(_Symbol, PERIOD_CURRENT, 1);
   DrawLabel(label + IntegerToString(state.units_open), t,
             is_long ? iLow(_Symbol, PERIOD_CURRENT, 1)  - PipSize() * 5
             : iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize() * 5,
             "U" + IntegerToString(state.units_open),
             is_long ? clrDodgerBlue : clrOrangeRed);
   return true;
  }

After each unit opens, the stop is 2N below (long) or above (short) the new entry. Earlier units benefit because their stop is now closer to the current price than when they were opened.

Moving the Unified Stop

When a new unit is added, the stop for all existing units must be moved to 2N below the newest entry price.

//+------------------------------------------------------------------+
//| Moves the stop for all open units — only advances, never retreats|
//+------------------------------------------------------------------+
void MoveUnifiedStop(STurtleState &state, bool is_long, double new_stop)
  {
   if(is_long  && new_stop <= state.unified_stop)
      return;
   if(!is_long && state.unified_stop > 0 && new_stop >= state.unified_stop)
      return;
   bool all_ok = true;
   for(int i = 0; i < state.units_open; i++)
     {
      if(!PositionSelectByTicket(state.tickets[i]))
         continue;
      double cur_sl = PositionGetDouble(POSITION_SL);
      if(is_long  && new_stop <= cur_sl)
         continue;
      if(!is_long && cur_sl > 0 && new_stop >= cur_sl)
         continue;
      if(!g_trade.PositionModify(state.tickets[i], new_stop, 0))
        {
         Print("TurtleEA: Stop modify failed | Ticket:", state.tickets[i],
               " | Retcode:", g_trade.ResultRetcode());
         all_ok = false;
        }
     }
   if(all_ok)
     {
      state.unified_stop = new_stop;
      Print("TurtleEA: Unified stop moved to ", DoubleToString(new_stop, _Digits));
     }
  }

The state is only updated when all modifications succeed. This prevents desynchronization between the internal state and the broker-side stops.

Closing All Units

When the exit condition fires, all units in the same direction close simultaneously.

//+------------------------------------------------------------------+
//| Closes all open units and records whether the trade was a winner |
//+------------------------------------------------------------------+
void CloseAllUnits(STurtleState &state, bool is_long, bool &was_winner)
  {
   double total_profit = 0;
   for(int i = state.units_open - 1; i >= 0; i--)
     {
      if(!PositionSelectByTicket(state.tickets[i]))
         continue;
      total_profit += PositionGetDouble(POSITION_PROFIT);
      if(!g_trade.PositionClose(state.tickets[i]))
         Print("TurtleEA: Close failed | Ticket:", state.tickets[i]);
     }
   was_winner = (total_profit > 0);
   Print(StringFormat("TurtleEA: %s closed | Units:%d | P&L:%.2f | Winner:%s",
                      is_long ? "LONG" : "SHORT",
                      state.units_open, total_profit,
                      was_winner ? "YES" : "NO"));
   ResetState(state);
   ClearLabels();
  }

The "was_winner" flag is passed back to the caller, which assigns it to "state.last_s1_winner." This drives the System 1 skip rule on the next breakout.

Processing System 1

System 1 runs every bar and checks four things in order: exit conditions for any open position, then entry conditions if no position is open.

//+------------------------------------------------------------------+
//| Processes System 1 for the current bar                           |
//+------------------------------------------------------------------+
void ProcessSystem1()
  {
//--- Breakout levels: exclude bar 1 from the lookback (from_bar=2)
//--- so we compare bar 1's price against the prior N bars, not itself
   double high_20 = GetHighest(2, InpS1Entry);             // highest of bars 2..21
   double low_20  = GetLowest(2,  InpS1Entry);             // lowest  of bars 2..21
   double high_10 = GetHighest(2, InpS1Exit);              // highest of bars 2..11
   double low_10  = GetLowest(2,  InpS1Exit);              // lowest  of bars 2..11
   double bar1_high  = iHigh(_Symbol,  PERIOD_CURRENT, 1); // last bar high
   double bar1_low   = iLow(_Symbol,   PERIOD_CURRENT, 1); // last bar low
   double bar1_close = iClose(_Symbol, PERIOD_CURRENT, 1); // last bar close
//--- Exit long: close crosses below 10-day low
   if(g_long.active && bar1_close < low_10)
     {
      bool winner = false;
      CloseAllUnits(g_long, true, winner);
      g_long.last_s1_winner = winner;
      return;
     }
//--- Exit short: close crosses above 10-day high
   if(g_short.active && bar1_close > high_10)
     {
      bool winner = false;
      CloseAllUnits(g_short, false, winner);
      g_short.last_s1_winner = winner;
      return;
     }
//--- Pyramid long: add unit every N/2 in favor
   if(g_long.active && g_long.units_open < InpMaxUnits)
     {
      double last_entry = g_long.entry_prices[g_long.units_open - 1];
      if(bar1_close >= last_entry + g_n * 0.5)
        {
         if(OpenUnit(g_long, true, g_n, "S1L"))
           {
            double new_stop = NormalizeDouble(
                                 g_long.entry_prices[g_long.units_open - 1] - g_n * 2.0, _Digits);
            MoveUnifiedStop(g_long, true, new_stop);
           }
        }
      return;
     }
//--- Pyramid short: add unit every N/2 in favor
   if(g_short.active && g_short.units_open < InpMaxUnits)
     {
      double last_entry = g_short.entry_prices[g_short.units_open - 1];
      if(bar1_close <= last_entry - g_n * 0.5)
        {
         if(OpenUnit(g_short, false, g_n, "S1S"))
           {
            double new_stop = NormalizeDouble(
                                 g_short.entry_prices[g_short.units_open - 1] + g_n * 2.0, _Digits);
            MoveUnifiedStop(g_short, false, new_stop);
           }
        }
      return;
     }
//--- New entry: only when no position open in either direction
   if(!g_long.active && !g_short.active)
     {
      //--- Long: bar1 high exceeded prior 20-day high and skip rule permits
      if(bar1_high > high_20 && !g_long.last_s1_winner)
        {
         if(OpenUnit(g_long, true, g_n, "S1L"))
           {
            double new_stop = NormalizeDouble(
                                 g_long.entry_prices[0] - g_n * 2.0, _Digits);
            g_long.unified_stop = new_stop;
           }
        }
      //--- Short: bar1 low exceeded prior 20-day low and skip rule permits
      else
         if(bar1_low < low_20 && !g_short.last_s1_winner)
           {
            if(OpenUnit(g_short, false, g_n, "S1S"))
              {
               double new_stop = NormalizeDouble(
                                    g_short.entry_prices[0] + g_n * 2.0, _Digits);
               g_short.unified_stop = new_stop;
              }
           }
     }
  }

The skip rule check is "!g_long.last_s1_winner"—if the last System 1 long trade was a winner, this is true and the entry is blocked. The rule only blocks System 1. System 2 has no such filter.

Processing System 2

System 2 is simpler—no skip rule, longer lookback, wider exit.

//+------------------------------------------------------------------+
//| Processes System 2 for the current bar                           |
//+------------------------------------------------------------------+
void ProcessSystem2()
  {
   double high_55 = GetHighest(2, InpS2Entry); // prior 55-day high
   double low_55  = GetLowest(2,  InpS2Entry); // prior 55-day low
   double high_20 = GetHighest(2, InpS2Exit);  // prior 20-day high for exit
   double low_20  = GetLowest(2,  InpS2Exit);  // prior 20-day low for exit
   double bar1_high  = iHigh(_Symbol,  PERIOD_CURRENT, 1);
   double bar1_low   = iLow(_Symbol,   PERIOD_CURRENT, 1);
   double bar1_close = iClose(_Symbol, PERIOD_CURRENT, 1);
//--- Exit long
   if(g_long.active && bar1_close < low_20)
     {
      bool winner = false;
      CloseAllUnits(g_long, true, winner);
      return;
     }
//--- Exit short
   if(g_short.active && bar1_close > high_20)
     {
      bool winner = false;
      CloseAllUnits(g_short, false, winner);
      return;
     }
//--- Pyramid long
   if(g_long.active && g_long.units_open < InpMaxUnits)
     {
      double last_entry = g_long.entry_prices[g_long.units_open - 1];
      if(bar1_close >= last_entry + g_n * 0.5)
        {
         if(OpenUnit(g_long, true, g_n, "S2L"))
           {
            double new_stop = NormalizeDouble(
                                 g_long.entry_prices[g_long.units_open - 1] - g_n * 2.0, _Digits);
            MoveUnifiedStop(g_long, true, new_stop);
           }
        }
      return;
     }
//--- Pyramid short
   if(g_short.active && g_short.units_open < InpMaxUnits)
     {
      double last_entry = g_short.entry_prices[g_short.units_open - 1];
      if(bar1_close <= last_entry - g_n * 0.5)
        {
         if(OpenUnit(g_short, false, g_n, "S2S"))
           {
            double new_stop = NormalizeDouble(
                                 g_short.entry_prices[g_short.units_open - 1] + g_n * 2.0, _Digits);
            MoveUnifiedStop(g_short, false, new_stop);
           }
        }
      return;
     }
//--- New entry — no skip rule for System 2
   if(!g_long.active && !g_short.active)
     {
      if(bar1_high > high_55)
        {
         if(OpenUnit(g_long, true, g_n, "S2L"))
           {
            double new_stop = NormalizeDouble(
                                 g_long.entry_prices[0] - g_n * 2.0, _Digits);
            g_long.unified_stop = new_stop;
           }
        }
      else
         if(bar1_low < low_55)
           {
            if(OpenUnit(g_short, false, g_n, "S2S"))
              {
               double new_stop = NormalizeDouble(
                                    g_short.entry_prices[0] + g_n * 2.0, _Digits);
               g_short.unified_stop = new_stop;
              }
           }
     }
  }

The logic is identical to System 1, except the lookback periods are longer and the skip rule condition is absent. Every 55-day breakout produces an entry.

OnInit, OnDeinit, and OnTick

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   g_trade.SetExpertMagicNumber(InpMagicNumber);
   g_trade.SetDeviationInPoints(InpSlippage);
   ResetState(g_long);
   ResetState(g_short);
   g_long.last_s1_winner  = false;
   g_short.last_s1_winner = false;
   g_last_bar = 0;
   g_n        = 0;
   Print("TurtleEA initialized | Symbol:", _Symbol,
         " | TF:", EnumToString(Period()),
         " | System:", EnumToString(InpSystem),
         " | Magic:", InpMagicNumber);
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ClearLabels();
  }

//+------------------------------------------------------------------+
//| 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;
   g_n = ComputeN(); // update N each bar
   if(g_n <= 0)
     {
      Print("TurtleEA: N not ready yet — waiting for more bars.");
      return;
     }
   if(InpSystem == SYSTEM_1 || InpSystem == BOTH)
      ProcessSystem1();
   if(InpSystem == SYSTEM_2 || InpSystem == BOTH)
      ProcessSystem2();
  }
//+------------------------------------------------------------------+

"OnTick()" runs the entire EA logic once per bar. N is recomputed first because position sizing, stops, and pyramid intervals all depend on the current N. The two systems are then processed in order. When "BOTH" is selected, System 1 runs first and may open or close positions; System 2 then runs against the updated state.


Backtesting

To test the EA, open the MetaTrader 5 Strategy Tester and set the following: Symbol: EURUSD; Timeframe: Daily; Modeling: Every tick (real ticks); Initial deposit: $10,000; Period: 2018.01.01–2024.12.31. Then apply the following inputs: InpSystem=BOTH, InpS1Entry=20, InpS1Exit=10, InpS2Entry=55, InpS2Exit=20, InpRiskPerUnit=1.0, InpMaxUnits=4, and InpATRPeriod=20.

What to Expect

The Turtle rules are trend-following. On EURUSD—a pair that spends significant time in choppy, non-trending conditions—expect a win rate below 50%. The original Turtles expected win rates between 30% and 40% on most instruments. The edge comes entirely from the size of winners relative to losers. System 2 trades will have larger average winners than System 1 trades but will fire less frequently. Expect significant drawdown periods during ranging markets. This is characteristic of any trend-following system and is not a signal that the implementation is incorrect.

Check the journal output to verify N calculations: at the start of each new trade, the logged N value should be approximately equal to the 20-period ATR displayed on a standard ATR indicator, with slight differences due to the Wilder smoothing initialization method.

Test Results

Demonstration input parameters

Fig. 4. Demonstration input parameters

Turtle EA Demonstration

Fig. 5. Demonstration

equity and balance curve

Fig. 6. Balance and equity graph

test results

Fig. 7. Test results

entries

Fig. 8. Test results—entries


Known Limitations

The original Turtle rules were designed for futures markets with real exchange volume. On forex, tick volume is used as a proxy for activity, but this does not affect any calculation in the EA—the rules use only price and ATR.

The skip rule in System 1 is implemented per direction. The original rules describe the skip rule as applying to the previous signal in the same direction, which is what this implementation does. Some interpretations apply the skip rule differently; the implementation here follows Curtis Faith's description from “Way of the Turtle.”

The Turtle rules do not specify a take profit. Positions are held until the exit breakout fires. This can mean holding a position through a significant retracement before the 10-day or 20-day exit level is reached. This is by design—the rules were built to capture the full extent of major trends.

The four-unit cap applies per direction. The original rules also defined limits on correlated markets—no more than 12 units in any one direction across all correlated instruments. This implementation does not enforce the correlated market cap because it runs on a single symbol. Developers applying this EA across multiple instruments should implement portfolio-level unit counting separately.

N becomes stable only after approximately 60 bars of history are available. On startup, if fewer bars are available than three times the ATR period, "ComputeN()" returns the previous N value or zero. The EA will not trade until N is valid.

The EA requires a hedging account. Each pyramid unit is a separate position with its own ticket. On netting accounts, adding to a position does not create a new ticket, and the unit tracking logic will not work as intended.


Conclusion

The Turtle experiment proved that a complete mechanical system, given to people with no prior trading experience, could generate consistent profits. It also proved something the trading industry has been slower to accept: that the edge in trend-following systems comes almost entirely from position management, not from signal generation. The entry rules themselves—buy 20-day highs, sell 20-day lows—are trivially simple. The edge comes from risking the same amount on every trade, from scaling into winners with a formula, from never adding to losers, and from letting winning trades run until the exit rule fires.

The EA in this article implements all of those rules exactly as the Turtles were taught them. The N calculation, the 2N stop, the N/2 pyramid interval, the four-unit cap, and the System 1 skip rule are all present and correct. The result is not a perfect equity curve—no trend-following system produces one—but it is a faithful implementation of one of the most tested systematic trading frameworks in history.

All code was compiled and tested in MetaTrader 5. Copy "TurtleEA.mq5" to MQL5\Experts\” and compile in MetaEditor with no additional dependencies. Recommended for use on daily timeframes on liquid instruments with sufficient historical data for N initialization. Always test on a demo account before live deployment.

Attached files |
TurtleEA.mq5 (22.42 KB)
Price Action Analysis Toolkit Development (Part 76): One-Click Symbol Dashboard for Centralized Multi-Chart Management in MQL5 Price Action Analysis Toolkit Development (Part 76): One-Click Symbol Dashboard for Centralized Multi-Chart Management in MQL5
Learn to assemble an MT5 Expert Advisor that hosts a chart management dashboard written in MQL5. The guide walks through shared definitions, symbol acquisition and filtering, chart lifecycle functions, and a UI panel with search, scrolling, and state indicators, all driven by events and a timer. The result is a reproducible tool that reduces clicks and accelerates multi-symbol analysis.
Automating Classic Market Methods in MQL5 (Part 4): Mark Minervini's Trend Template Automating Classic Market Methods in MQL5 (Part 4): Mark Minervini's Trend Template
This article presents TrendTemplateEA, an Expert Advisor implementing Mark Minervini's eight-condition trend template for daily forex charts. It evaluates all conditions on every bar and enters only when they are simultaneously satisfied, using RSI above 50 in place of the stock market RS rating. The entry trigger is a 20-bar high breakout on expanding volume, with all rules coded and testable in MQL5.
Formulating Dynamic Multi-Pair EA (Part 10): Asymmetric Stop-Loss Logic Based on Pair-Specific Volatility Signatures Formulating Dynamic Multi-Pair EA (Part 10): Asymmetric Stop-Loss Logic Based on Pair-Specific Volatility Signatures
The EA learns each symbol's volatility profile before trading by processing 1000 bars and summarizing candle ranges, bodies and wicks, noise ratio, trend runs, pullback size, and true‑range dispersion. A classifier assigns regime and structure labels per pair. The stop‑loss optimizer maps those labels to a symbol‑specific ATR multiplier, and the risk module sizes lots to maintain constant percentage risk.
Developing a Multi-Currency Expert Advisor (Part 29): Improving the Conveyor Developing a Multi-Currency Expert Advisor (Part 29): Improving the Conveyor
We are going to improve the usability of the automated optimization conveyor: we will explore the process from creating an optimization project to testing the final EA. For clarity, let us walk through the entire process step by step creating the final EA, while stopping to make any desired corrections.