English
preview
Автоматизация классических рыночных методов на MQL5 (Часть 5): Исходные правила торговой системы "Черепах"

Автоматизация классических рыночных методов на MQL5 (Часть 5): Исходные правила торговой системы "Черепах"

MetaTrader 5 — Трейдинг |
26 0
Tola Moses Hector
Tola Moses Hector

Введение

Эксперимент с торговой системой "Черепах" — одна из самых известных проверок систематической торговли за всю историю. Деннис и Экхардт не учили "Черепах" полагаться на собственные суждения или интуицию. Они научили их набору механических правил, которые можно было записать, точно соблюдать и применять на любом ликвидном рынке. Эти правила предусматривали следование за трендом, риск в размере одного и того же фиксированного процента средств счета в каждой сделке и наращивание прибыльных позиций при быстром ограничении убытков.

Первоначальные правила держались в секрете более десяти лет. В 2003 году Куртис Фейс, бывший участник группы "Черепах", опубликовал их в полном объеме. С тех пор эти правила широко изучают, обсуждают и цитируют, но редко корректно реализуют в коде на MQL5. Причина заключается не в сложности. Отдельные правила довольно просты. Для правильной реализации необходимо понимать, почему существует каждое правило и к каким сбоям приводит его небрежное применение.

В данной статье представлен автономный торговый советник "TurtleEA", реализующий обе исходные системы "Черепах", описанные Куртисом Фейсом в книге "Путь Черепах". Каждое проектное решение напрямую соответствует конкретному исходному правилу. Советник тестируется на EURUSD за многолетний период, чтобы проверить, соответствует ли поведение реализации предусмотренным правилам.

Мы рассмотрим следующие темы:

  1. Правила торговой системы "Черепах" — теория и архитектура
  2. Величина N — риск с поправкой на волатильность
  3. Система 1 и Система 2 — правила входа
  4. Реализация на языке MQL5
  5. Тестирование на исторических данных
  6. Известные ограничения
  7. Заключение:


Правила торговой системы "Черепах" — теория и архитектура

"Черепахи" одновременно торговали по двум системам. Деннис назвал их "Система 1" и "Система 2".

Краткий обзор торговой системы «Черепах»

Рис. 1. Краткий обзор торговой системы "Черепах"

Система 1 основывалась на пробое 20-дневного диапазона. Длинная позиция открывалась, когда цена поднималась выше наибольшего максимума за предыдущие 20 дней. Короткая позиция открывалась, когда цена опускалась ниже наименьшего минимума за предыдущие 20 дней. Сигнал пропускался, если предыдущий пробой по Системе 1 в том же направлении принес прибыль; этот фильтр назывался "правило пропуска сигнала" и предназначался для предотвращения входов в сформировавшиеся тренды, в которых значительное движение уже состоялось.

Система 2 основывалась на пробое 55-дневного диапазона. Длинная позиция открывалась, когда цена поднималась выше наибольшего максимума за предыдущие 55 дней. Короткая позиция открывалась, когда цена опускалась ниже наименьшего минимума за предыдущие 55 дней. В Системе 2 не было правила пропуска сигнала. Позиция открывалась при каждом пробое.

Правила выхода были одинаковыми для обеих систем. Длинные позиции закрывались, когда цена опускалась ниже наименьшего минимума за предыдущие 10 дней (Система 1) или 20 дней (Система 2). Короткие позиции закрывались, когда цена поднималась выше наибольшего максимума за тот же период анализа.

Правила расчета объема позиции, установки стоп-лоссов и пирамидинга были одинаковыми для обеих систем.

Что обеспечило эффективность правил

Правила не прогнозируют направление движения цены. Они не используют индикаторы. Они не анализируют фундаментальные показатели. Они лишь определяют условия, при которых цена прошла достаточно далеко в одном направлении, чтобы оправдать ставку на продолжение движения.

Ключевая идея заключается в том, что правила служат для управления рисками, а не для прогнозирования. Стоп-лосс устанавливается ровно на 2N ниже цены входа для длинных позиций и на 2N выше — для коротких позиций, где N — 20-дневный средний истинный диапазон (ATR). Это означает, что максимальный убыток по любой отдельной торговой единице составляет ровно 1 % средств счета — всегда, независимо от торгового инструмента или текущей волатильности. Когда в рамках одной позиции открыты четыре торговые единицы, максимальный убыток по-прежнему остается под контролем, поскольку объем каждой торговой единицы рассчитывается по одной и той же формуле с использованием N.

Пирамида позиций из четырех торговых единиц предусматривает наращивание прибыльной позиции через заданные интервалы движения цены. Каждое добавление торговой единицы происходит, когда цена проходит еще N в направлении сделки от цены предыдущего входа. Советник добавляет торговые единицы только к прибыльным позициям и поддерживает стоп-лосс на расстоянии 2N от цены последнего входа. В результате совокупный риск по четырем торговым единицам остается ограниченным.

Архитектура советника

Советник построен на основе пяти компонентов. "Калькулятор величины N" вычисляет 20-дневный средний истинный диапазон (ATR) с использованием сглаживания Уайлдера — именно этот метод предусмотрен исходными правилами. "Детектор Системы 1" отслеживает пробои границ 20-дневного диапазона и применяет правило пропуска сигнала. "Детектор Системы 2" отслеживает пробои границ 55-дневного диапазона. "Менеджер торговых единиц" отвечает за расчет объема позиции, добавление торговых единиц в пирамиду позиций и управление стоп-лоссами. "Монитор выхода" проверяет условия выхода на каждом баре.

Все пять компонентов используют единую структуру состояния, которая отслеживает текущее количество торговых единиц, цены входа для всех открытых торговых единиц, текущий единый стоп-лосс, а также то, был ли последний сигнал Системы 1 в каждом направлении прибыльным.


Величина N — риск с поправкой на волатильность

N является основой всей системы управления рисками. Объем каждой позиции, каждый стоп-лосс и каждый интервал наращивания пирамиды позиций выражаются в единицах N.

Расчет объема позиции

Рис. 2. Расчет объема позиции

N — это 20-дневное экспоненциальное среднее истинного диапазона, вычисляемое с использованием сглаживания Уайлдера. Истинный диапазон бара — это максимальное из трех значений: текущий максимум минус текущий минимум, текущий максимум минус предыдущая цена закрытия и предыдущая цена закрытия минус текущий минимум.

Формула сглаживания Уайлдера выглядит следующим образом:

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

Этот расчет отличается от расчета стандартного 20-периодного ATR способом задания начального значения, хотя коэффициент сглаживания остается тем же. Чтобы правильно рассчитать N, в качестве начального значения берем простое среднее первых 20 истинных диапазонов, а затем, начиная с 21-го бара, применяем сглаживание Уайлдера.

Стоимость одного N в долларах рассчитывается с использованием того же подхода на основе стоимости тика, что и в предыдущих статьях этой серии:

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

Таким образом, N преобразуется из ценового интервала в денежную сумму за стандартный лот. Чтобы рисковать ровно 1 % средств счета на каждую торговую единицу, делим денежную сумму риска на N_dollars и получаем объем в лотах.

Объем позиции на одну торговую единицу = (Средства счета × 0.01) / N_dollars

Эта формула гарантирует, что при срабатывании стоп-лосса на расстоянии 2N от цены входа максимальный убыток составит ровно 1 % средств счета. "Черепахи" называли это одной торговой единицей. Полная позиция из четырех торговых единиц предполагала риск в размере 4 % средств счета при одновременном срабатывании всех стоп-лоссов, что считалось приемлемым в рамках их системы.


Система 1 и Система 2 — правила входа

Система 1: 20-дневный пробой с правилом пропуска сигнала

Сигнал на покупку возникает, когда максимум текущего бара превышает максимум предыдущих 20 баров. Сигнал на продажу возникает, когда минимум текущего бара опускается ниже наименьшего минимума предыдущих 20 баров.

Правило пропуска сигнала: если предыдущий пробой Системы 1 в том же направлении привел к прибыльной сделке — то есть цена двигалась в благоприятном направлении до срабатывания стоп-лосса или сигнала выхода, — текущий пробой пропускается. Только пробои, приведшие к убыточным сделкам, восстанавливают разрешение на торговлю. Это правило было разработано, чтобы предотвращать входы в ходе уже сформировавшихся трендов, когда, по мнению "Черепах", движение уже было использовано для получения прибыли.

Система 2: 55-дневный пробой без правила пропуска сигнала

Сигнал на покупку возникает, когда максимум текущего бара превышает максимум предыдущих 55 баров. Сигнал на продажу возникает, когда минимум текущего бара опускается ниже наименьшего минимума предыдущих 55 баров. Каждый сигнал используется для входа в рынок.

Правила пирамидинга

После первоначального входа советник добавляет одну торговую единицу каждый раз, когда цена проходит N/2 в направлении сделки от цены предыдущего входа. По одному торговому инструменту в одном направлении одновременно могут быть открыты не более четырех торговых единиц.

Правила пирамидинга

Рис. 3. Правила пирамидинга

Управление стоп-лоссами

Для всех открытых торговых единиц установлен единый стоп-лосс. При добавлении новой торговой единицы единый стоп-лосс для всех уже открытых торговых единиц переносится на уровень на 2N ниже новой цены входа для длинных позиций или на 2N выше нее для коротких. Это означает, что по мере наращивания пирамиды позиций стоп-лоссы ранее открытых торговых единиц становятся более тесными, защищая накопленную прибыль от предыдущих входов и одновременно ограничивая риск новой торговой единицы максимально допустимым уровнем.

Когда выходить из рынка

Длинные позиции по Системе 1 закрываются, когда цена опускается ниже наименьшего минимума за предыдущие 10 баров, а короткие позиции по Системе 1 — когда цена поднимается выше наибольшего максимума за предыдущие 10 баров. Аналогичным образом длинные позиции по Системе 2 закрываются, когда цена опускается ниже наименьшего минимума за предыдущие 20 баров, а короткие позиции по Системе 2 — когда цена поднимается выше наибольшего максимума за предыдущие 20 баров.

Экстренный выход срабатывает, если цена пересекает уровень, расположенный на расстоянии 2N от цены последнего входа в неблагоприятном для позиции направлении. Это жесткий стоп-лосс, который срабатывает независимо от используемой системы.


Реализация на языке MQL5

Советник создается по разделам.

Подключаемые файлы, перечисление и входные параметры
//+------------------------------------------------------------------+
//|                                                     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

Входные параметры сгруппированы по системам. По умолчанию в Системе 1 используется 20-дневный период входа и 10-дневный период выхода. По умолчанию в Системе 2 используется 55-дневный период входа и 20-дневный период выхода. Именно такие параметры зафиксировал Куртис Фейс. Режим "BOTH" запускает обе системы одновременно — именно так действовали участники первоначального эксперимента "Черепахи": Система 1 давала больше сделок, а Система 2 позволяла использовать более крупные тренды.

Структура состояния торговой системы "Черепах"

Структура состояния хранит всю необходимую информацию на протяжении всего времени существования открытой позиции. Один экземпляр обрабатывает длинные позиции, а другой — короткие.

//+------------------------------------------------------------------+
//| 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?
  };

Флаг "last_s1_winner" реализует правило пропуска сигнала. Этот флаг устанавливается в true, когда позиция по Системе 1 закрывается с прибылью, и сбрасывается в false, когда позиция по Системе 1 закрывается с убытком. Следующий сигнал Системы 1 в этом направлении используется для входа только в том случае, если этот флаг имеет значение false.

Глобальные переменные и величина N

//+------------------------------------------------------------------+
//| 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 хранится в глобальной переменной и обновляется один раз на бар. Обе системы используют одну и ту же величину N, рассчитанную с использованием одного и того же периода ATR.

Вычисление N

"Черепахи" использовали ATR со сглаживанием Уайлдера, а не стандартный ATR, рассчитанный как простое среднее. Мы рассчитываем этот показатель вручную, чтобы обеспечить точность.

//+------------------------------------------------------------------+
//| 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;
  }

Функция копирует данные за число баров, втрое превышающее период ATR, чтобы начальное среднее рассчитывалось по действительно историческим данным. Затем функция последовательно применяет сглаживание Уайлдера в хронологическом порядке вплоть до последнего завершенного бара. В результате получается величина N, которую использовали бы участники первоначального эксперимента "Черепах".

Расчет объема позиции

Объем одной торговой единицы рассчитывается так, чтобы при срабатывании стоп-лосса на расстоянии 2N убыток составлял 1 % средств счета.

//+------------------------------------------------------------------+
//| 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));
  }

Сумма риска составляет 1 % средств счета. Стоп-лосс находится на расстоянии 2N от цены входа. Итак, чтобы определить объем позиции в лотах, при котором движение на 2N соответствует 1 % средств счета, мы делим сумму риска на (n_dollars × 2). Именно такой расчет использовали "Черепахи".

Обнаружение пробоя

Нам нужны максимум и минимум за заданный период анализа, рассчитанные на момент закрытия последнего бара.

//+------------------------------------------------------------------+
//| 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;
  }

Эти функции копируют данные начиная с бара 1, а не с бара 0, поэтому расчеты всегда выполняются по полностью завершенным барам. Это позволяет избежать ложных сигналов от текущего открытого бара.

Вспомогательные функции рисования на графике

//+------------------------------------------------------------------+
//| 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);
  }

Открытие торговой единицы

//+------------------------------------------------------------------+
//| 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;
  }

После открытия каждой торговой единицы стоп-лосс устанавливается на 2N ниже новой цены входа для длинной позиции или на 2N выше нее для короткой позиции. Ранее открытые торговые единицы получают преимущество, поскольку их стоп-лосс теперь находится ближе к текущей цене, чем в момент их открытия.

Перемещение единого стоп-лосса

При добавлении новой торговой единицы стоп-лосс для всех уже открытых торговых единиц необходимо переместить на уровень на 2N ниже цены последнего входа.

//+------------------------------------------------------------------+
//| 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));
     }
  }

Состояние обновляется только в том случае, если все изменения были успешно выполнены. Это предотвращает рассинхронизацию между внутренним состоянием и стоп-лоссами на стороне брокера.

Закрытие всех торговых единиц

При срабатывании условия выхода все торговые единицы одного направления закрываются одновременно.

//+------------------------------------------------------------------+
//| 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();
  }

Флаг "was_winner" возвращается вызывающей функции, которая присваивает его значение "state.last_s1_winner". Это определяет применение правила пропуска сигнала Системы 1 при следующем пробое.

Обработка Системы 1

Система 1 выполняется на каждом баре и последовательно проверяет четыре условия: сначала условия выхода из любой открытой позиции, затем условия входа, если открытых позиций нет.

//+------------------------------------------------------------------+
//| 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;
              }
           }
     }
  }

Проверка правила пропуска сигнала — "!g_long.last_s1_winner": если последняя сделка с длинной позицией по Системе 1 была прибыльной, это условие истинно и вход блокируется. Это правило блокирует вход только по Системе 1. В Системе 2 такого фильтра нет.

Обработка Системы 2

Система 2 проще: в ней нет правила пропуска сигнала, период анализа длиннее, а диапазон выхода шире.

//+------------------------------------------------------------------+
//| 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;
              }
           }
     }
  }

Логика идентична логике Системы 1, за исключением более длительных периодов анализа и отсутствия условия правила пропуска сигнала. Каждый пробой 55-дневного диапазона приводит к входу в рынок.

OnInit, OnDeinit и 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()" выполняет всю логику советника один раз на каждом баре. Сначала пересчитывается N, поскольку расчет объема позиции, стоп-лоссы и интервалы пирамидинга зависят от текущей величины N. Затем обе системы обрабатываются по порядку. При выборе значения BOTH сначала выполняется Система 1, которая может открывать или закрывать позиции; затем Система 2 выполняется с учетом обновленного состояния.


Тестирование на исторических данных

Чтобы протестировать советник, откройте тестер стратегий MetaTrader 5 и задайте следующие параметры: торговый инструмент: EURUSD; таймфрейм: дневной; моделирование: каждый тик на основе реальных тиков; начальный депозит: 10 000 $; период: 2018.01.01–2024.12.31. Затем задайте следующие входные параметры: InpSystem=BOTH, InpS1Entry=20, InpS1Exit=10, InpS2Entry=55, InpS2Exit=20, InpRiskPerUnit=1.0, InpMaxUnits=4 и InpATRPeriod=20.

Чего ожидать

Правила торговой системы "Черепах" основаны на следовании за трендом. На EURUSD — паре, для которой значительную часть времени характерно неустойчивое движение без выраженного тренда, — следует ожидать долю прибыльных сделок ниже 50%. Участники первоначального эксперимента "Черепахи" ожидали долю прибыльных сделок от 30% до 40% по большинству торговых инструментов. Торговое преимущество полностью обусловлено соотношением величины прибыли в прибыльных сделках и величины убытка в убыточных. Средняя прибыль прибыльных сделок по Системе 2 будет выше, чем по Системе 1, но сделки будут совершаться реже. В условиях бокового рынка следует ожидать периодов значительной просадки. Это характерно для любой системы, следующей за трендом, и не является признаком неправильной реализации.

Проверьте записи в журнале, чтобы убедиться в правильности расчетов N: при открытии каждой новой сделки записанная в журнал величина N должна быть примерно равна значению стандартного индикатора ATR с периодом 20; небольшие различия обусловлены методом инициализации сглаживания Уайлдера.

Результаты тестирования

Демонстрационные входные параметры

Рис. 4. Демонстрационные входные параметры

Демонстрация советника «Turtle EA»

Рис. 5. Демонстрация

кривые эквити и баланса

Рис. 6. График баланса и эквити

результаты тестирования

Рис. 7. Результаты тестирования

входы в рынок

Рис. 8. Результаты тестирования — входы в рынок


Известные ограничения

Исходные правила торговой системы "Черепах" были разработаны для фьючерсных рынков с реальным биржевым объемом торгов. На рынке Форекс тиковый объем используется как косвенный показатель активности, однако это не влияет ни на какие расчеты в советнике — правила используют только цену и ATR.

Правило пропуска сигнала в Системе 1 реализовано отдельно для каждого направления. В исходных правилах указано, что правило пропуска сигнала применяется к предыдущему сигналу в том же направлении; именно так оно и реализовано здесь. В некоторых интерпретациях правило пропуска сигнала применяется иначе; представленная здесь реализация соответствует описанию Куртиса Фейса из книги "Путь Черепах".

Правила торговой системы "Черепах" не предусматривают тейк-профит. Позиции удерживаются до пробоя уровня выхода. Это может означать удержание позиции во время значительного отката до достижения 10-дневного или 20-дневного уровня выхода. Так и задумано — правила были разработаны, чтобы максимально полно использовать движение в рамках крупных трендов.

Ограничение в четыре торговые единицы действует отдельно для каждого направления. Исходные правила также устанавливали ограничения для коррелирующих рынков — не более 12 торговых единиц в одном направлении суммарно по всем коррелирующим инструментам. Данная реализация не обеспечивает соблюдение ограничения на число торговых единиц на коррелирующих рынках, поскольку работает только с одним торговым инструментом. Разработчики, применяющие этот советник для торговли несколькими инструментами, должны отдельно реализовать подсчет торговых единиц на уровне портфеля.

N становится стабильным только после того, как будет собрано примерно 60 баров исторических данных. При запуске, если число доступных баров меньше утроенного периода ATR, функция "ComputeN()" возвращает предыдущее значение N или ноль. Советник не будет совершать сделки, пока не будет получено корректное значение N.

Для работы советника требуется хеджинговый счет. Каждая торговая единица в пирамиде позиций представляет собой отдельную позицию со своим тикетом. На неттинговых счетах увеличение позиции не приводит к созданию нового тикета, и логика отслеживания торговых единиц не будет работать должным образом.


Заключение

Эксперимент "Черепах" доказал, что полноценная механическая торговая система, переданная людям без предшествующего опыта торговли, способна приносить стабильную прибыль. Он также доказал то, что торговая индустрия не спешит признавать: торговое преимущество систем следования за трендом почти полностью обусловлено управлением позициями, а не формированием сигналов. Сами по себе правила входа — покупать на 20-дневных максимумах, продавать на 20-дневных минимумах — тривиально просты. Торговое преимущество достигается за счет риска одной и той же суммой в каждой сделке, наращивания прибыльных позиций по формуле, полного отказа от наращивания убыточных позиций и удержания прибыльных позиций до срабатывания правила выхода.

Советник, описанный в этой статье, реализует все эти правила в точном соответствии с тем, как им обучали "Черепах". Расчет N, стоп-лосс на расстоянии 2N, интервал добавления торговых единиц N/2, ограничение в четыре торговые единицы и правило пропуска сигнала Системы 1 — все это реализовано корректно. Результат — не идеальная кривая средств счета: ни одна система следования за трендом не дает такой кривой. Однако это точная реализация одного из наиболее проверенных подходов к системной торговле в истории.

Весь код был скомпилирован и протестирован в MetaTrader 5. Скопируйте файл "TurtleEA.mq5" в папку "MQL5\Experts\" и скомпилируйте его в MetaEditor; дополнительные зависимости не требуются. Рекомендуется использовать на дневных таймфреймах для ликвидных инструментов, по которым имеется достаточно исторических данных для инициализации параметра N. Перед запуском в реальной торговле всегда проводите тестирование на демо-счете.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23448

Прикрепленные файлы |
TurtleEA.mq5 (22.42 KB)
Конструирование признаков для машинного обучения (Часть 12): Фрактальные признаки в MQL5 Конструирование признаков для машинного обучения (Часть 12): Фрактальные признаки в MQL5
При прямом переносе детектора фракталов на MQL5 каждый паттерн записывается на центральном баре, поэтому буфер, считываемый советником (EA), содержит значение, которое стало доступно лишь на n баров позже. Мы реализуем CFractalFeatures.mqh с методом ProcessBar для побарной обработки и методом Compute для пересчёта всего ряда; реализация включает обнаружение фракталов, оценку их силы относительно фиксированного или масштабируемого по волатильности минимального порога силы, кольцевой буфер событий для уровней поддержки и сопротивления, а также сигналы пробоя с фильтрацией по тренду. Результат — восемнадцать буферов, значения в которых публикуются на баре подтверждения и проверены на соответствие эталонной реализации на Python с точностью до 1e-13.
Управление позициями: Построение самокалибрующейся лестницы выхода по историческим данным MFE в MQL5 Управление позициями: Построение самокалибрующейся лестницы выхода по историческим данным MFE в MQL5
Мы реализуем три класса MQL5, которые заменяют фиксированные целевые уровни 1R/2R/3R на уровни поэтапного закрытия, определяемые на основе данных. CExcursionTracker фиксирует максимальное благоприятное отклонение (MFE) в единицах R для каждой закрытой позиции, CExitLadderCalibrator формирует ступени на основе процентилей распределения с учетом окна ретроспективного анализа и минимального числа наблюдений, а CLadderExecutor выполняет закрытие на этих ступенях для открытых позиций. Лестница выхода перекалибруется по мере накопления закрытых позиций. Пока наблюдений недостаточно, используется резервная лестница выхода.
Генетическое программирование — Genetic Programming (GP) Генетическое программирование — Genetic Programming (GP)
Разбираем алгоритм Genetic Programming (GP) — классический метод, который выращивает не числа, а формулы. Почему знаменитый алгоритм в каноническом виде проигрывает обычному случайному поиску во всех тестах, и какая одна строка возвращает ему силу? Ответ касается любого оптимизатора с пропорциональным отбором. В конце статьи — готовый код GP на MQL5, который можно сразу использовать для своих задач.
Анализ рекуррентных сетей (RNA) в MQL5: От матриц рекуррентности к сложным сетям Анализ рекуррентных сетей (RNA) в MQL5: От матриц рекуррентности к сложным сетям
В статье библиотека анализа рекуррентности на MQL5 расширяется средствами анализа рекуррентных сетей (RNA): матрицы рекуррентности рассматриваются как матрицы смежности неориентированных графов. В ней реализованы основные сетевые метрики — кластеризация, транзитивность, средняя длина пути, посредничество, ассортативность и плотность, — которые применяются в скользящих окнах для анализа рекуррентных сетей (RNA) одного временного ряда и совместного анализа рекуррентных сетей (JRNA). Модульный механизм расчета метрик и два индикатора позволяют визуализировать динамику структуры сети на графиках MetaTrader 5 для практического анализа временных рядов.