Автоматизация классических рыночных методов на MQL5 (Часть 5): Исходные правила торговой системы "Черепах"
Введение
Эксперимент с торговой системой "Черепах" — одна из самых известных проверок систематической торговли за всю историю. Деннис и Экхардт не учили "Черепах" полагаться на собственные суждения или интуицию. Они научили их набору механических правил, которые можно было записать, точно соблюдать и применять на любом ликвидном рынке. Эти правила предусматривали следование за трендом, риск в размере одного и того же фиксированного процента средств счета в каждой сделке и наращивание прибыльных позиций при быстром ограничении убытков.
Первоначальные правила держались в секрете более десяти лет. В 2003 году Куртис Фейс, бывший участник группы "Черепах", опубликовал их в полном объеме. С тех пор эти правила широко изучают, обсуждают и цитируют, но редко корректно реализуют в коде на MQL5. Причина заключается не в сложности. Отдельные правила довольно просты. Для правильной реализации необходимо понимать, почему существует каждое правило и к каким сбоям приводит его небрежное применение.
В данной статье представлен автономный торговый советник "TurtleEA", реализующий обе исходные системы "Черепах", описанные Куртисом Фейсом в книге "Путь Черепах". Каждое проектное решение напрямую соответствует конкретному исходному правилу. Советник тестируется на EURUSD за многолетний период, чтобы проверить, соответствует ли поведение реализации предусмотренным правилам.
Мы рассмотрим следующие темы:
- Правила торговой системы "Черепах" — теория и архитектура
- Величина N — риск с поправкой на волатильность
- Система 1 и Система 2 — правила входа
- Реализация на языке MQL5
- Тестирование на исторических данных
- Известные ограничения
- Заключение:
Правила торговой системы "Черепах" — теория и архитектура
"Черепахи" одновременно торговали по двум системам. Деннис назвал их "Система 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. Демонстрационные входные параметры

Рис. 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.