MQL4: Пишем советник по стратегии «Пирамидинг»

Здравствуйте, уважаемые форекс-программисты!

Сегодняшний урок по MQL4 мы посвятим созданию торгового эксперта, в котором реализуем построение пирамиды отложенных ордеров и последующее её сопровождение. Давайте сначала разберёмся, что представляет собой стратегия «Пирамидинг», насколько она эффективна и какие подводные камни таит в себе. А затем перейдем к написанию советника с пирамидингом в основе.

MQL4 - Пишем советник по стратегии Пирамидинга

Пирамидинг: основные понятия

Надо сразу отметить, что пирамидинг не является самостоятельной торговой системой. Правильнее назвать его методом увеличения прибыли при торговле по тренду. Основная идея пирамидинга заключается в так называемых «доливках», когда при наличии ярко выраженного тренда открываются новые позиции на откатах или в момент выхода из очередной консолидации. При этом растёт совокупный объём, следовательно, и прибыль. Схема подобных действий напоминает ступенчатую пирамиду, отсюда и название – «пирамидинг».

На приведённом выше рисунке мы можем наблюдать работу пирамиды. Было открыто восемь позиций на покупку по тренду, в итоге все закрылись по общему Stop Loss: последняя сделка с убытком, предпоследняя – в безубытке, а шесть остальных – с прибылью. Может возникнуть вопрос: нельзя ли было открыть самую первую позицию сразу большим лотом, а закрыть по трейлинг-стопу? Дело в том, что мы не могли знать наверняка, пойдет ли цена в нужном нам направлении, и в таком случае рисковали бы всем объёмом. Пирамида же позволяет наращивать совокупный объём постепенно, так как очередная позиция открывается только после того, как предыдущая выйдет в прибыль.

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

Точки входа

Как уже отмечалось ранее, пирамидинг – это метод увеличения прибыли при торговле по тренду. Очевидно, что для диапазонной (флетовой) торговли такой подход не годится и может обернуться полной потерей депозита. Поэтому перед построением пирамиды трейдеру важно определить начало тренда. Решение этой задачи мы оставим за рамками сегодняшнего урока. Отметим лишь, что искать подобные точки входа лучше всего на старших таймфреймах от H1 и выше.

После того, как была совершена первая сделка, и мы убедились, что цена идёт в нужном нам направлении, встаёт другой вопрос: в каких точках следует совершать повторные входы? Самый очевидный способ – входить по основному сигналу, но на меньшем таймфрейме. Можно доливаться при касании скользящей средней, входу или выходу осцилляторов в/из зон перекупленности или перепроданности, при появлении свечных моделей продолжения тренда, на больших объёмах, прорывах консолидации, пробитии или тестировании сильных уровней. Но можно поступить проще и входить в новые сделки с фиксированным шагом в пунктах. Такой шаг можно сделать расширяющимся или сужающимся в зависимости от выбранной тактики. Подобный чисто математический подход к построению пирамиды мы будем использовать в нашем торговом эксперте.

Наращивание объёма позиций

Наиболее консервативным и самым распространённым способом наращивания совокупного объёма является открытие каждой новой позиции фиксированным лотом.

При наличии сильного устойчивого тренда допустимо использовать и более агрессивную методику, когда объем каждой новой позиции будет больше предыдущего. Например, 1; 2; 3; 4 или 1; 2; 4; 8; 16. В этом случае важно вовремя зафиксировать прибыль, иначе если последняя позиция закроется по Stop Loss, образовавшийся убыток может перекрыть всю накопленную до этого прибыль.

Существует и обратный предыдущему способ, при котором объём каждой новой позиции меньше предыдущего. Например, 4; 3; 2; 1. Такой подход вполне обоснован, поскольку каждый тренд имеет тенденцию к затуханию, и с каждой новой ступенькой пирамиды вероятность разворота или сильного отката становится выше. Следовательно, убыток по последним позициям с меньшим лотом будет перекрыт прибылью первых. Недостатком такого способа является недополучение возможной прибыли, хотя риски, конечно же, становятся ниже. В нашем будущем советнике мы реализуем обе методики.

Фиксация прибыли

В случае классической пирамиды для фиксации прибыли используется общий Stop Loss. Иными словами, при открытии новой позиции Stop Loss всех имеющихся позиций переносится на уровень безубытка последней открытой. Именно этот механизм мы будем использовать при создании советника и позже разберём более подробно.

Другой распространённый способ – закрытие пирамиды при достижении определённого уровня прибыли. В этом случае трейдер сам определяет величину прибыли, достаточную для закрытия пирамиды.

И, наконец, решение о закрытии пирамиды можно принимать на основе технического анализа при первых признаках разворота или затухании текущей тенденции.

Общий алгоритм советника

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

Здесь мы видим пирамиду на продажу из Sell Stop ордеров. Объём каждого последующего ордера возрастает в арифметической прогрессии с шагом 0.01.

Так выглядит эта же пирамида, когда первые четыре Sell Stop ордера были активированы при движении цены вниз. Теперь у нас открыты четыре рыночных ордера и шесть отложенных. Не хватает только общего Stop Loss у открытых позиций. Но об этом позже.

Алгоритм советника можно записать в общем виде с помощью следующей блок-схемы:

Определимся с полным функционалом будущего советника и опишем его входные параметры.

Money Management

Данная секция содержит параметры, относящиеся к управлению капиталом:

Lots – фиксированный/стартовый лот;

LotsMultMode – режим увеличения/уменьшения лота каждой новой позиции пирамиды, может принимать одно из трёх значений: отключено (все позиции будут иметь одинаковый объём, равный параметру Lots); арифметическая (объём будет возрастать в арифметической прогрессии); геометрическая (объём будет возрастать в геометрической прогрессии);

LotsMultiplicatorAr – разность арифметической прогрессии, каждый новый лот увеличивается на данное значение; если параметр равен 0 (нулю), то в качестве разности прогрессии выступает лот первого ордера серии. Используя отрицательные значения данного параметра, лот можно уменьшать;

LotsMultiplicatorGm – знаменатель геометрической прогрессии, каждый новый лот увеличивается в указанное число раз. Если значение меньше единицы, то лот будет уменьшаться. Например, при 1.5 каждый новый лот будет увеличиваться в полтора раза. А чтобы лот в полтора раза уменьшался, потребуется значение 1/1.5=0.67.

Сигнал

Как уже отмечалось, для успешной торговли нам необходимо построить пирамиду как можно ближе к возможному началу трендового движения. Воспользуемся следующей схемой: строить пирамиду из отложенных ордеров мы будем, когда линия стохастика войдёт в зону перекупленности (для пирамиды на продажу) или в зону перепроданности (для пирамиды на покупку). Если использовать классический сигнал не со входом, а с выходом линии стохастика из зон перекупленности/перепроданности, то из-за запаздывания индикатора ложных входов будет значительно больше. Мы же стараемся действовать на опережение. Если цена не развернётся в нашу сторону, нам нужно только удалить пирамиду (все отложенные ордера) и ждать нового сигнала.

StochasticPeriod – период стохастика;

StochasticSlowing – замедление;

StochasticLevelDn – уровень перепроданности/уровень перекупленности автоматически (например, для StochasticLevelDn = 20 верхний уровень 100 – StochasticLevelDn = 80).

Общие параметры торговли

DistanceToGrid – на каком расстоянии от цены строить сетку после получения сигнала от стохастика;

AfterSignaBarsCount – сколько баров ждать активации сетки. Так как мы строим сетку в момент входа стохастика в зону перекупленности или перепроданности, мы только предполагаем разворот, который может и не произойти. Поэтому если пирамида не была активирована, её надо удалить спустя какое-то время. С помощью данного параметра можно задать этот контрольный период в барах текущего таймфрейма. Очевидно, что удаление пирамиды также происходит и при поступлении обратного сигнала. Если параметр AfterSignaBarsCount равен нулю, то пирамида удаляется исключительно после формирования обратного сигнала;

Magic – «магический номер», идентификатор ордеров, открытых советником;

Slippage – максимально допустимое проскальзывание.

Параметры сетки

В нашем советнике пирамида будет строиться в виде сетки отложенных ордеров, которая представляет собой установленные на определённом расстоянии друг от друга ордера Buy Stop или Sell Stop. Отложенные ордера такого типа активируются в направлении движения цены, а значит, именно они подходят для трендовой торговли. В данной секции устанавливаются параметры ордеров и всей сетки в целом.

OrdersAmount – количество отложенных ордеров в сетке;

GridStep – шаг сетки, то есть расстояние между отложенными ордерами сетки. Это расстояние может быть как фиксированным, так и увеличивающимся (уменьшающимся) в зависимости от параметра StepMultMode. В случае динамического шага здесь задаётся его стартовое значение;

StepMultMode – режим расширения/уменьшения расстояния между ордерами в сетке по мере удаления от точки входа. Шаг по аналогии с объёмом позиции может изменяться в арифметической или геометрической прогрессии;

StepMultiplicatorAr – разность арифметической прогрессии, каждый ордер будет размещён на расстоянии от предыдущего, которое увеличивается на данное значение; если параметр равен нулю, то в качестве разности прогрессии выступает значение параметра GridStep. Отрицательное значение параметра уменьшает шаг на эту величину;

StepMultiplicatorGm – знаменатель геометрической прогрессии, шаг увеличивается в указанное число раз. Если значение меньше единицы, то шаг будет уменьшаться. Например, при 1.5 шаг будет увеличиваться в полтора раза. Чтобы шаг в полтора раза уменьшался, потребуется значение 1/1.5=0.67;

StopLossRatio – отношение Stop Loss к шагу. Классическая пирамида строится таким образом, что Stop Loss каждого следующего ордера равен цене открытия предыдущего. Однако с помощью этого параметра можно увязать размер Stop Loss с величиной шага;

TakeProfit – общий тейк-профит. Для фиксации прибыли в нашем советнике используется общий Stop Loss, но может случиться так, что будут активированы все ордера пирамиды и цена уйдёт дальше по тренду. Чтобы не терять в таких редких случаях прибыль, используется страховочный Take Profit, иначе нам придётся ждать возвращения цены и срабатывания общего Stop Loss. При этом лучше всё-таки использовать больше ордеров в пирамиде.

Функции советника

Начнём реализацию алгоритма, опираясь на размещённую выше блок-схему. Каждый элемент этой схемы (блок) выполним в виде функции. При этом блок «Новый тик» соответствует обработчику OnTick(), внутри которого мы будем производить вызов прочих функций.

Функция RefreshState()

Начнём с функции, которая не представлена отдельным блоком в нашей схеме, так как относится к категории служебных, однако ей стоит уделить особое внимание. Функция RefreshState() собирает информацию об открытых советником ордерах и помещает её в переданную структуру state:

// структура для хранения информации об открытых советником ордерах
struct state
  {
   double            BuyTopPrice;         // цена открытия самого верхнего ордера Buy
   double            SellBottomPrice;     // цена открытия самого нижнего ордера Sell

   double            BuyTopSL;            // стоплосс самого верхнего ордера Buy
   double            BuyBottomSL;         // стоплосс самого нижнего ордера Buy
   double            SellTopSL;           // стоплосс самого верхнего ордера Sell
   double            SellBottomSL;        // стоплосс самого нижнего ордера Sell

   datetime          BuyStopStart;        // время установки пирамиды (открытия самого первого ордера Buy Stop в сетке)
   datetime          SellStopStart;       // время установки пирамиды (открытия самого первого ордера Sell Stop в сетке)

   int               BuyCount;            // количество ордеров Buy
   int               SellCount;           // количество ордеров Sell

   int               BuyStopCount;        // количество ордеров Buy Stop
   int               SellStopCount;       // количество ордеров Sell Stop
  };

Опираясь именно на собранную функцией RefreshState() статистику, советник отслеживает текущую обстановку на торговом счёте и определяет, на каком именно шаге алгоритма он находится.

Например, следующий код определяет, что все рыночные ордера пирамиды закрылись по Stop Loss и теперь требуется удалить несработавшие отложенные:

// сетка ордеров закрылась по общему стоп-лоссу, удаляем оставшиеся отложки
   if((State.BuyStopCount>0 && State.BuyStopCount<OrdersAmount && State.BuyCount==0) ||
      (State.SellStopCount>0 && State.SellStopCount<OrdersAmount && State.SellCount==0))
     {
      // удаляем отложенные ордера
      DeletePendingOrders();
     }

Здесь мы сначала проверяем, есть ли вообще отложенные ордера; если есть, то смотрим, меньше ли их количество первоначального значения, то есть получается, что какие-то из них сработали с момента построения сетки. Теперь, если нет открытых рыночных ордеров, можно сделать вывод, что они закрылись по Stop Loss. Следовательно, оставшиеся отложенные ордера являются несработавшими и их надо удалить.

В следующем участке кода решение о построении новой сетки мы принимаем только в случае, если нет ни рыночных, ни отложенных ордеров, открытых нашим советником:

      // если сформирован новый бар, опрашиваем индикаторы с помощью вызова функции GetSignal()
      // и записываем результат в переменную TradeSignal
      int TradeSignal=GetSignal();

      // на открытии нового бара нет ни отложенных ни рыночных ордеров
      if(State.BuyCount==0 && State.SellCount==0 && State.BuyStopCount==0 && State.SellStopCount==0)
        {
         // если при этом сформирован торговый сигнал, строим пирамиду указанного типа
         if(TradeSignal!=EMPTY)
            BuildPyramid(TradeSignal);
         // выходим, так как дальнейшая обработка не имеет смысла, так как
         // либо никаких ордеров нет вовсе, либо мы только что построили пирамиду
         return;
        }

Используя информацию об открытых ордерах, можно конструировать и более сложные условия. Следующий код удаляет сетку, если с момента её построения было сформировано больше AfterSignaBarsCount баров или был сформирован противоположный сигнал:

      // если с момента построения сетки было сформировано больше AfterSignaBarsCount баров,
      // или был сформирован противоположный сигнал, удаляем сетку
      if((State.BuyStopCount>0 && State.BuyCount==0 &&
          ((AfterSignaBarsCount>0 && State.BuyStopStart>0 && iTime(_Symbol,_Period,0)-State.BuyStopStart>=BarsInSeconds) || TradeSignal==OP_SELL))
         ||
         (State.SellStopStart>0 && State.SellCount==0 &&
          ((AfterSignaBarsCount>0 && State.SellStopStart>0 && iTime(_Symbol,_Period,0)-State.SellStopStart>=BarsInSeconds) || TradeSignal==OP_BUY)))
        {
         // удаляем отложенные ордера
         DeletePendingOrders();
         return;
        }

Функция GetSignal()

Эта функция опрашивает индикатор и возвращает: OP_BUY (0 – сигнал на покупку), если стохастик входит в зону перепроданности (пересекает нижний сигнальный уровень сверху вниз); OP_SELL (1 – сигнал на продажу), если стохастик входит в зону перекупленности (пересекает верхний сигнальный уровень снизу вверх). Если сигнал не сформирован, функция возвратит EMPTY (-1):

//+------------------------------------------------------------------+
//| Функция возвращает:                                              |
//| OP_BUY (сигнал на покупку), если стохастик входит в зону         |
//| перепроданности (пересекает нижний сигнальный уровень            |
//| сверху вниз)                                                     |
//| OP_SELL (сигнал на продажу), если стохастик входит в зону        |
//| перекупленности (пересекает верхний сигнальный уровень           |
//| снизу вверх)                                                     |
//| EMPTY - сигнала нет                                              |
//+------------------------------------------------------------------+
int GetSignal()
  {
// получаем значение стохастика на баре с индексом 1
   double StochasticOnFirstBar=iStochastic(_Symbol,_Period,StochasticPeriod,1,StochasticSlowing,MODE_SMA,0,MODE_MAIN,1);

// стохастик пересекает нижний сигнальный уровень сверху вниз
   if(StochasticOnFirstBar<StochasticLevelDn)
     {
      // для экономии ресурсов получаем значение стохастика на баре с индексом 2 только после того,
      // как убедились, что первая часть условия выполняется
      if(iStochastic(_Symbol,_Period,StochasticPeriod,1,StochasticSlowing,MODE_SMA,0,MODE_MAIN,2)>StochasticLevelDn)
         return(OP_BUY);
     }
// стохастик пересекает верхний сигнальный уровень снизу вверх
   if(StochasticOnFirstBar>100-StochasticLevelDn)
     {
      // для экономии ресурсов получаем значение стохастика на баре с индексом 2 только после того,
      // как убедились, что первая часть условия выполняется
      if(iStochastic(_Symbol,_Period,StochasticPeriod,1,StochasticSlowing,MODE_SMA,0,MODE_MAIN,2)<100-StochasticLevelDn)
         return(OP_SELL);
     }

// в любом другом случае возвращаем EMPTY
   return(EMPTY);
  }

Функция BuildPyramid()

Данная функция строит пирамиду отложенных ордеров Buy Stop, если переданный в неё параметр signal равен OP_BUY, и пирамиду ордеров Sell Stop, если signal имеет значение OP_SELL.

Сетка строится на расстоянии, равном значению входного параметра DistanceToGrid от цены Ask для ордеров Buy Stop и Bid для сетки Sell Stop. В случае, если это значение меньше минимально разрешённого, сетка строится на расстоянии Sell Stop для данного символа.

Если предполагается, что лот каждого нового ордера или шаг, на котором он выставляется от предыдущего, изменяется в прогрессии, происходит вызов служебной функции GetProgressionMember(). Эта функция возвращает n-ый член арифметической или геометрической прогрессии.

//+------------------------------------------------------------------+
//| Строит пирамиду из отложенных ордеров                            |
//| Buy Stop, если параметр signal равен OP_BUY                      |
//| Sell Stop, если параметр signal равен OP_SELL                    |
//| на расстоянии DistanceToGrid от текущей цены                     |
//+------------------------------------------------------------------+
void BuildPyramid(int signal)
  {
// определяем корректную величину Stop Level
   int StopLevel=GetTrueStopLevel();

// если расстояние, на котором предполагается строить пирамиду меньше Stop Level,
// то устанавливаем его значение, равным Stop Level
   int Distance=(DistanceToGrid<StopLevel)?StopLevel:DistanceToGrid;
   int Com;//тип отложенного ордера


// проверим корректность лотов перед установкой сетки
   if((Lots<SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN)) ||
      (Lots>SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX)))
     {
      Print("Некорректное значение стартового торгового объёма, пирамида не будет построена");
      return;
     }

   if(LotsMultMode>mlNone)
     {
      double LastOrderLots=NormalizeDouble(GetProgressionMember(LotsMultMode,OrdersAmount-1,((LotsMultMode==mlArithmetical)?LotsMultiplicatorAr:LotsMultiplicatorGm),Lots),2);
      if((LastOrderLots<SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN)) ||
         (LastOrderLots>SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX)))
        {
         Print("Обнаружены некорректные значения торгового объёма, пирамида не будет построена");
         return;
        }
     }

// проверим корректность шага перед выставлением последнего ордера пирамиды
   if((GridStep<StopLevel)  ||
      (StepMultMode>mlNone &&
       (int)GetProgressionMember(StepMultMode,OrdersAmount-1,((StepMultMode==mlArithmetical)?StepMultiplicatorAr:StepMultiplicatorGm),GridStep)<StopLevel))
     {
      Print("Обнаружены некорректные значения шага, пирамида не будет построена");
      return;
     }

// проверим корректность Stop Loss
   if((int(StopLossRatio*GridStep)<StopLevel) ||
      (StepMultMode>mlNone &&
       int(StopLossRatio*GetProgressionMember(StepMultMode,OrdersAmount-1,((StepMultMode==mlArithmetical)?StepMultiplicatorAr:StepMultiplicatorGm),GridStep))<StopLevel))
     {
      Print("Обнаружены некорректные значения Stop Loss, пирамида не будет построена");
      return;
     }

// проверим корректность Take Profit
   if(TakeProfit>0 && TakeProfit<StopLevel)
     {
      Print("Общий Take Profit ",TakeProfit," меньше минимально допустимого значения ", StopLevel,", пирамида не будет построена");
      return;
     }

   double Price, Sign, Step=GridStep;

   MqlTick tick;
   if(!SymbolInfoTick(_Symbol,tick))
      return;

// определяем стартовую цену и тип отложенных ордеров
   if(signal==OP_BUY)
     {
      Price=tick.ask;
      Sign=1.0;
      Com=OP_BUYSTOP;
     }
   else
      if(signal==OP_SELL)
        {
         Price=tick.bid;
         Sign=-1.0;
         Com=OP_SELLSTOP;
        }
      else
         return;

// ордера Buy Stop выставляются выше от цены, а Sell Stop - ниже
   Price+=Sign*Distance*_Point;

   for(int i=0; i<OrdersAmount; i++)
     {
      // вычисляем лот для очередного ордера сетки
      double TradeLots;
      // увеличиваем или уменьшаем текущий лот в зависимости от типа прогрессии
      if(LotsMultMode>mlNone)
         TradeLots=GetProgressionMember(LotsMultMode,i,((LotsMultMode==mlArithmetical)?LotsMultiplicatorAr:LotsMultiplicatorGm),Lots);
      else
         TradeLots=Lots;

      double StopLoss,StopLossInPoints;
      // вычисляем StopLoss пропорционально шагу Step
      StopLossInPoints=StopLossRatio*Step;

      // Stop Loss для ордеров Buy Stop располагается ниже цены открытия, для Sell Stop - выше
      StopLoss=Price+EMPTY*Sign*StopLossInPoints*_Point;

      // пять попыток выставить очередной ордер пирамиды
      for(int j=0; j<5; j++)
        {
         if(OrderSend(_Symbol,Com,NormalizeDouble(TradeLots,2),NormalizeDouble(Price,_Digits),Slippage,NormalizeDouble(StopLoss,_Digits),0,NULL,Magic,0,OrderColor(Com))!=EMPTY)
            break;
         else
            Sleep(1000);
        }

      // увеличиваем или уменьшаем текущий шаг в зависимости от типа прогрессии
      if(StepMultMode>mlNone)
         Step=GetProgressionMember(StepMultMode,i,((StepMultMode==mlArithmetical)?StepMultiplicatorAr:StepMultiplicatorGm),GridStep);

      // ордера Buy Stop выставляются выше от цены, а Sell Stop - ниже
      Price+=Sign*Step*_Point;

     }
// устанавливаем общий тейкпрофит только что выставленным отложенным ордерам
   if(TakeProfit>0)
     {
      double tp=NormalizeDouble(Price+Sign*(TakeProfit<StopLevel?StopLevel:TakeProfit)*_Point,_Digits);

      for(int i=OrdersTotal()-1; i>=0; i--)
         if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES) && OrderMagicNumber()==Magic && OrderSymbol()==_Symbol && OrderType()>OP_SELL)
           {
            if(NormalizeDouble(OrderTakeProfit(),_Digits)!=tp)
              {
               for(int j=0; j<5; j++)
                  if(OrderModify(OrderTicket(),OrderOpenPrice(),OrderStopLoss(),tp,0))
                     break;
                  else
                     Sleep(1000);
              }
           }
     }
  }

Функция CommonStopLoss()

Ключевая функция, которая отслеживает активацию отложенных ордеров и модифицирует Stop Loss рыночных ордеров. Каждый новый активированный рыночный ордер является самым дальним по отношению к стартовой цене пирамиды. Поэтому при его появлении Stop Loss всех предыдущих рыночных ордеров устанавливается на уровень Stop Loss нового ордера:

//+------------------------------------------------------------------+
//| Обновляет общий стоплосс рыночных ордеров пирамиды, то есть      |
//| устанавливает стоплоссы всех рыночных ордеров на один уровень    |
//+------------------------------------------------------------------+
void CommonStopLoss(state &Orders)
  {
   int type;
   bool refresh;
   double sl;

// из рыночных ордеров имеются только Buy, значит изначально была выставлена пирамида отложенных Buy Stop ордеров
   if(Orders.BuyCount>0 && Orders.SellCount==0)
     {
      type=OP_BUY;
      // стоплосс самого верхнего ордера Buy не совпадает со стоплосс самого нижнего, значит необходимо
      // установить всем ордерам стоплосс самого верхнего, иными словами, "подтянуть" в безубыток
      refresh=(NormalizeDouble(Orders.BuyTopSL,_Digits)!=NormalizeDouble(Orders.BuyBottomSL,_Digits));
      sl=NormalizeDouble(Orders.BuyTopSL,_Digits);
     }
   else
      // из рыночных ордеров имеются только Sell, значит изначально была выставлена пирамида отложенных Sell Stop ордеров
      if(Orders.BuyCount==0 && Orders.SellCount>0)
        {
         type=OP_SELL;
         // стоплосс самого верхнего ордера Sell не совпадает со стоплосс самого нижнего, значит необходимо
         // установить всем ордерам стоплосс самого нижнего, иными словами, "подтянуть" в безубыток
         refresh=(NormalizeDouble(Orders.SellTopSL,_Digits)!=NormalizeDouble(Orders.SellBottomSL,_Digits));
         sl=NormalizeDouble(Orders.SellBottomSL,_Digits);
        }
      else
         return;

   if(refresh)
      for(int i=OrdersTotal()-1; i>=0; i--)
         if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES) && OrderMagicNumber()==Magic && OrderSymbol()==_Symbol && OrderType()==type)
           {
            if(NormalizeDouble(OrderStopLoss(),_Digits)!=sl)
              {
               for(int j=0; j<5; j++)
                  if(OrderModify(OrderTicket(),OrderOpenPrice(),sl,OrderTakeProfit(),0))
                     break;
                  else
                     Sleep(1000);
              }
           }
  }

Более наглядно механизм работы функции можно наблюдать в следующей анимации:

Обработчик OnTick()

Завершая создание торгового эксперта, закодируем алгоритм вызова описанных блоков в соответствии с блок-схемой. Полученный код поместим внутрь обработчика OnTick():

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   state State;

// получаем информацию об открытых советником ордерах и помещаем её в структуру State
   RefreshState(State);

// проверяем, сформировался ли новый бар
   if(IsNewBar())
     {
      // если сформирован новый бар, опрашиваем индикаторы с помощью вызова функции GetSignal()
      // и записываем результат в переменную TradeSignal
      int TradeSignal=GetSignal();

      // на открытии нового бара нет ни отложенных ни рыночных ордеров
      if(State.BuyCount==0 && State.SellCount==0 && State.BuyStopCount==0 && State.SellStopCount==0)
        {
         // если при этом сформирован торговый сигнал, строим пирамиду указанного типа
         if(TradeSignal!=EMPTY)
            BuildPyramid(TradeSignal);
         // выходим, так как дальнейшая обработка не имеет смысла, так как
         // либо никаких ордеров нет вовсе, либо мы только что построили пирамиду
         return;
        }

      // если с момента построения сетки было сформировано больше AfterSignaBarsCount баров,
      // или был сформирован противоположный сигнал, удаляем сетку
      if((State.BuyStopCount>0 && State.BuyCount==0 &&
          ((AfterSignaBarsCount>0 && State.BuyStopStart>0 && iTime(_Symbol,_Period,0)-State.BuyStopStart>=BarsInSeconds) || TradeSignal==OP_SELL))
         ||
         (State.SellStopStart>0 && State.SellCount==0 &&
          ((AfterSignaBarsCount>0 && State.SellStopStart>0 && iTime(_Symbol,_Period,0)-State.SellStopStart>=BarsInSeconds) || TradeSignal==OP_BUY)))
        {
         // удаляем отложенные ордера
         DeletePendingOrders();
         return;
        }
     }

// есть открытые ордера BUY или SELL, значит построена пирамида,
// и какие-то отложенные ордера сработали
   if(State.BuyCount>0 || State.SellCount>0)
     {
      // обновляем общий Stop Loss рыночных ордеров пирамиды, если требуется
      CommonStopLoss(State);
      return;
     }

// сетка ордеров закрылась по общему стоп-лоссу, удаляем оставшиеся отложки
   if((State.BuyStopCount>0 && State.BuyStopCount<OrdersAmount && State.BuyCount==0) ||
      (State.SellStopCount>0 && State.SellStopCount<OrdersAmount && State.SellCount==0))
     {
      // удаляем отложенные ордера
      DeletePendingOrders();
     }
  }

Тесты на истории

Чтобы проверить работоспособность нашего советника и дать оценку его торговому алгоритму, сделаем несколько тестов на исторических данных. Тестировать будем на паре EURUSD с качеством 99.9%. Период тестирования 01.01.2017 – 01.01.2020. Таймфрейм H1 (один час). Начальный депозит 10000 единиц.

Параметры по умолчанию: фиксированный лот 0.01, расстояние до сетки 100 п., шаг фиксированный 300 п., в сетке 15 ордеров, настройки стохастика стандартные, уровень перепроданности 20, отношение Stop Loss к шагу 1.0, ждать срабатывания сетки будем до появления обратного сигнала.

Cо значениями по умолчанию советник не блещет результатами, впрочем, и не сливает. Давайте немного «отодвинем» уровни перепроданности и перекупленности в настройках индикатора, установим параметр StochasticLevelDn на 10 вместо 20. Входы будут реже, но, возможно, точнее. Другие настройки трогать не будем.

На этот раз результаты заметно лучше, а ведь мы изменили только один параметр.

Посмотрим, как советник поведёт себя при увеличении лота в арифметической прогрессии. Для этого выберем в настройках режим «Арифметическая», параметру LotsMultiplicatorAr (разность арифметической прогрессии) присвоим значение 0.01. Можно оставить и ноль, в таком случае параметр автоматически приравнивается к начальному лоту, а он у нас равен 0.01. В итоге первый ордер пирамиды будет иметь объём 0.01, а последний откроется лотом 0.15. Получим следующую картину:

Теперь будем уменьшать лот в арифметической прогрессии от 0.15 до 0.01. Для этого параметру LotsMultiplicatorAr присвоим отрицательное значение -0.01, а размер стартового лота сделаем 0.15:

Исходя из особенностей трендовой торговли, вариант с уменьшением лота выглядит предпочтительнее, поскольку по мере затухания тренда мы уменьшаем и объём наших «доливок». Конечно, в этом случае прибыль ожидаемо больше, но пропорционально растёт и просадка.

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

Заключение

Созданный нами советник реализует математический вариант стратегии «Пирамидинг». Впрочем, более сложные разновидности данного метода слабо поддаются формализации. Всё дело в цене вопроса: стоит ли затраченных усилий учёт многочисленных нюансов построения пирамид, например, по торговым уровням или по фигурам технического анализа, которые надо ещё суметь идентифицировать, или же для советника достаточно сетки с оптимизируемым шагом?

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

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

Исходный код советника снабжён подробными, практически построчными комментариями, так что его можно рассматривать как самостоятельный учебный материал.

Скачать исходник советника из урока

Тема на форуме

С уважением, Юрий Лосев aka lsv107
Tlap.com

Уроки по MQL4 , , , , ,