Automating Classic Market Methods in MQL5 (Part 5): The Original Turtle Trading Rules
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:
- The Turtle Rules—Theory and Architecture
- The N Value—Volatility-Adjusted Risk
- System 1 and System 2—The Entry Rules
- Implementation in MQL5
- Backtesting
- Known Limitations
- Conclusion
The Turtle Rules—Theory and Architecture
The Turtles traded two systems simultaneously. Dennis referred to them as System 1 and System 2.

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.

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 RuleA 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 RuleA 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.

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

Fig. 4. Demonstration input parameters

Fig. 5. Demonstration

Fig. 6. Balance and equity graph

Fig. 7. Test results

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.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Price Action Analysis Toolkit Development (Part 76): One-Click Symbol Dashboard for Centralized Multi-Chart Management in MQL5
Automating Classic Market Methods in MQL5 (Part 4): Mark Minervini's Trend Template
Formulating Dynamic Multi-Pair EA (Part 10): Asymmetric Stop-Loss Logic Based on Pair-Specific Volatility Signatures
Developing a Multi-Currency Expert Advisor (Part 29): Improving the Conveyor
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use