English Deutsch 日本語
preview
Модифицированный советник Grid-Hedge в MQL5 (Часть I): Создание простого хеджирующего советника

Модифицированный советник Grid-Hedge в MQL5 (Часть I): Создание простого хеджирующего советника

MetaTrader 5Торговые системы | 10 апреля 2024, 11:10
621 4
Kailash Bai Mina
Kailash Bai Mina

Введение

Вы хотите окунуться в мир торговли с помощью советников (EA), но постоянно сталкиваетесь с фразой: "Не используется опасное хеджирование/сетка/мартингейл"? Вы можете задаться вопросом, что не так с этими стратегиями. Почему люди продолжают говорить, что они рискованны, и что на самом деле стоит за этими заявлениями? Возможно, вы даже думаете: "Можем ли мы изменить эти стратегии, чтобы сделать их более безопасными?" Почему трейдеры вообще беспокоятся об этих стратегиях? Что в них хорошего и плохого? Если эти мысли приходили вам в голову, вы попали по адресу.

Мы начнем с создания простого хеджирующего советника. Это будет первым шагом к нашему более масштабному проекту — советнику Grid-Hedge. Он будет представлять собой смесь классической сетки и хедж-стратегий. К концу этой статьи вы узнаете, как разработать базовую стратегию хеджирования, и получите представление о том, настолько прибыльна эта стратегия.

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

Вот краткий обзор того, что мы рассмотрим в этой статье:

  1. Классическое хеджирование
  2. Автоматизация классической стратегии хеджирования
  3. Тестирование классической стратегии хеджирования на истории
  4. Заключение


Классическое хеджирование

Прежде всего, нам следует обсудить стратегию.

Сначала открываем позицию на покупку, скажем для, на уровне цены 1000 и ставим стоп-лосс на уровне 950, а прибыль фиксируем на уровне 1050. То есть, если мы достигнем стоп-лосса, мы потеряем 50 USD, а если достигнем тейк-профита, то заработаем 50 USD. По достижении тейк-профита мы фиксируем прибыль и на этом всё заканчивается. При срабатывании стоп-лосса мы теряем 50 USD. Мы размещаем позицию на продажу на уровне 950, устанавливаем тейк-профит на уровне 900 и стоп-лосс на уровне 1000. Если эта новая позиция на продажу достигнет тейк-профита, мы получим 50 USD. Так как мы уже потеряли 50 USD, наша чистая прибыль составит 0 USD, и в случае, если она достигнет стоп-лосса, то есть ценового уровня 1000, мы снова потеряем 50 USD, поэтому наш общий убыток составит 100 USD, но теперь в этот момент мы снова размещаем позицию на покупку с тем же тейк-профитом и стоп-лоссом, что и предыдущая позиция на покупку. Если эта новая позиция на покупку достигнет тейк-профита, мы получим 50 USD, и наша общая чистая прибыль составит -50-50+50 = -50 USD, то есть убыток в 50 USD. В случае, если она достигнет стоп-лосса, наш общий убыток составит -50-50-50=-150 USD.

Для простоты мы пока игнорируем спреды и комиссии.

Вы можете подумать: "Как в такой ситуации можно заработать?" Ответ кроется в размере лота. Что если мы будем увеличивать размер лота каждой последующей позиции? Давайте вернемся к нашей стратегии.

Мы открываем позицию на покупку 0,01 лота (минимально возможная) по цене 1000:

  • если мы достигнем тейк-профита (1050), мы закрываемся с прибылью в 50 USD.
  • если мы достигнем стоп-лосса (950), мы потеряем 50 USD.

Согласно нашей стратегии, в этом случае мы немедленно разместим позицию на продажу размером 0,02 (удвоенного) лота по цене 950:

  • Если мы достигнем тейк-профита (900), мы получим общую чистую прибыль в размере -50+100 = 50 USD.
  • если мы достигнем стоп-лосса (1000), мы потеряем в общей сложности 50+100 = 150 USD.

В этом случае мы немедленно разместим позицию на покупку размером 0,04 (снова удвоенного) лота по цене 1000:

  • если мы достигнем тейк-профита (1050), мы получим общую чистую прибыль в размере -50-100+200 = 50 USD.
  • если сработает стоп-лосс (950), мы потеряем в общей сложности 50+100+150 = 350 USD.

В этом случае мы немедленно разместим позицию на продажу размером 0,08 (снова удвоенного) лота по цене 950:

  • Если мы достигнем тейк-профита (900), мы получим общую чистую прибыль в размере -50-100-150+400 = 50 USD.
  • если мы достигнем стоп-лосса (1000), мы потеряем в общей сложности 50+100+150+200 = 500 USD.

... 

Как вы уже могли заметить, в любом случае мы получаем прибыль в размере 50 USD. Если нет, то стратегия продолжается. Стратегия будет действовать до тех пор, пока мы не достигнем тейк-профита на уровне 900 или 1050; цена в конечном итоге достигнет любой из этих двух точек, и мы наверняка получим гарантированную прибыль в размере 50 USD.

В приведенном выше случае мы сначала разместили позицию на покупку, но начинать именно с нее не обязательно. Как вариант, мы можем запустить стратегию с продажи 0,01 лота (в нашем случае).

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

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

  • если мы достигнем тейк-профита (900), мы фиксируем прибыль в 50 USD.
  • если мы достигнем стоп-лосса (1000), мы потеряем 50 USD.

В этом случае мы немедленно разместим позицию на покупку размером 0,02 (удвоенного) лота по цене 1000:

  • Если мы достигнем тейк-профита (1050), мы получим общую чистую прибыль в размере -50+100 = 50 USD.
  • если мы достигнем стоп-лосса (950), мы потеряем в общей сложности 50+100 = 150 USD.

Согласно нашей стратегии, в этом случае мы немедленно разместим позицию на продажу размером 0,04 (удвоенного) лота по цене 950:

  • если мы достигнем тейк-профита (900), мы получим общую чистую прибыль в размере -50-100+200 = 50 USD.
  • если мы достигнем стоп-лосса (1000), мы потеряем в общей сложности 50+100+150 = 350 USD.

... и так далее.

Опять же, как мы видим, эта стратегия заканчивается только тогда, когда мы достигаем ценового уровня 900 или 1050, что наверняка приводит к прибыли в размере 50 долларов. Если мы не достигнем этих ценовых уровней, стратегия продолжится до тех пор, пока мы в конечном итоге не достигнем их.

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

На этом мы завершаем обсуждение классической стратегии хеджирования.


Автоматизация классической стратегии хеджирования

Во-первых, нам нужно обсудить план создания советника. Разработку можно осуществлять разными способами. Рассмотрим два основных подхода:

  • Первый: Определение четырех уровней (переменных), как указано в стратегии, и размещение позиций всякий раз, когда цена снова пересекает эти линии, как указано в нашей стратегии.
  • Второй: Использование отложенных ордеров. При исполнении отложенного ордера размещаются следующие.

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


    Автоматизация классической стратегии хеджирования

    Во-первых, объявим несколько переменных в глобальном пространстве:

    input bool initialPositionBuy = true;
    input double buyTP = 15;
    input double sellTP = 15;
    input double buySellDiff = 15;
    input double initialLotSize = 0.01;
    input double lotSizeMultiplier = 2;

    1. isPositionBuy - логическая переменная, определяющая, какой тип позиции (на покупку или на продажу) будет размещен следующим. При true следующей будет покупка, в противном случае — продажа.
    2. buyTP - расстояние между A и B, которое является тейк-профитом позиций на покупку (в пипсах). A и B будут определены позже.
    3. sellTP - расстояние между C и D, которое является тейк-профитом позиций на продажу (в пипсах). C и D будут определены позже.
    4. buySellDiff - расстояние между B и C, которое представляет собой уровень цен покупки и уровня цен продажи (в пипсах).
    5. intialLotSize - размер лота первой позиции.
    6. lotSizeMultiplier - множитель размера лота для последующей позиции.
    A, B, C, D — это, по сути, уровни цен в порядке убывания сверху вниз.

    Примечание: Переменные будут использоваться позже для оптимизации стратегии.

    Например, мы установили buyTP, SellTP и buySellDiff равными 15 пипсам, но мы изменим их позже и посмотрим, какие значения дадут нам оптимальную прибыль и просадку.

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

    Теперь мы создаем еще несколько переменных в глобальном пространстве:

    double A, B, C, D;
    bool isPositionBuy;
    bool hedgeCycleRunning = false;
    double lastPositionLotSize;

    1. Сначала мы определили четыре уровня с именами A, B, C, D как двойные переменные:
      • A: Уровень тейк-профита для всех позиций на покупку.
      • B: Цена открытия для всех позиций на покупку и стоп-лосс для всех позиций на продажу.
      • C: Цена открытия для всех позиций на продажу и стоп-лосс для всех позиций на покупку.
      • D: Уровень тейк-профита для всех позиций на продажу.
    2. isPositionBuy - логическая переменная. При true начальная позиция — покупка, при false - продажа.
    3. hedgeCycleRunning - логическая переменная. true означает, что выполняется один цикл хеджирования, то есть первоначальный ордер был открыт, но уровни цен A или D, которые мы определили выше, еще не достигнуты, а false означает, что уровень цен достиг либо A, либо D, и затем начнется новый цикл, который мы увидим позже. Кроме того, эта переменная по умолчанию будет иметь значение false.
    4. lastPositionLotSize - как следует из названия, эта переменная двойного типа всегда содержит размер лота последнего открытого ордера и, если цикл не был запущен, она примет значение, равное входной переменной initialLotSize, которую мы установим позже.

    Создадим функцию: 
    //+------------------------------------------------------------------+
    //| Hedge Cycle Intialization Function                               |
    //+------------------------------------------------------------------+
    void StartHedgeCycle()
       {
        isPositionBuy = initialPositionBuy;
        double initialPrice = isPositionBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
        A = isPositionBuy ? initialPrice + buyTP * _Point * 10 : initialPrice + (buySellDiff + buyTP) * _Point * 10;
        B = isPositionBuy ? initialPrice : initialPrice + buySellDiff * _Point * 10;
        C = isPositionBuy ? initialPrice - buySellDiff * _Point * 10 : initialPrice;
        D = isPositionBuy ? initialPrice - (buySellDiff + sellTP) * _Point * 10 : initialPrice - sellTP * _Point * 10;
    
        ObjectCreate(0, "A", OBJ_HLINE, 0, 0, A);
        ObjectSetInteger(0, "A", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "B", OBJ_HLINE, 0, 0, B);
        ObjectSetInteger(0, "B", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "C", OBJ_HLINE, 0, 0, C);
        ObjectSetInteger(0, "C", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "D", OBJ_HLINE, 0, 0, D);
        ObjectSetInteger(0, "D", OBJPROP_COLOR, clrGreen);
    
        ENUM_ORDER_TYPE positionType = isPositionBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
        double SL = isPositionBuy ? C : B;
        double TP = isPositionBuy ? A : D;
        CTrade trade;
        trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);
        
        lastPositionLotSize = initialLotSize;
        if(trade.ResultRetcode() == 10009) hedgeCycleRunning = true;
        isPositionBuy = isPositionBuy ? false : true;
       }
    //+------------------------------------------------------------------+

    Тип функции — void, то есть нам не нужно, чтобы она что-либо возвращала. Функция работает следующим образом:

    Сначала мы устанавливаем переменную isPositionBuy (bool), равную входной переменной InitialPositionBuy, которая сообщает нам, какой тип позиции размещать в начале каждого цикла. Вам может быть интересно, зачем нам нужны две переменные, если они обе одинаковы, но учтите, что мы будем менять isPositionBuy альтернативно (последняя строка приведенного выше блока кода). Однако параметр InitialPositionBuy всегда фиксирован и мы его не меняем.

    Затем мы определяем новую переменную (двойного типа) InitialPrice, которую мы устанавливаем равной Ask или Bid, используя тернарный оператор. Если isPositionBuy равен true, то InitialPrice становится равной цене Ask в этот момент времени и цене Bid в противном случае.

    Затем мы определяем переменные (двойного типа), которые мы кратко обсуждали ранее, то есть переменные A, B, C, D, используя тернарный оператор следующим образом:

    1. Если isPositionBuy равен True:
      • A равен сумме initialPrice и buyTP (входная переменная), где buyTP умножается на коэффициент (_Point*10), а _Point - предопределенная функция Point().
      • B равен initialPrice 
      • C равен initialPrice минус buySellDiff (входная переменная), где  buySellDiff умножается на коэффициент (_Point*10).
      • D равен initialPrice минус сумма buySellDiff и  sellTP, которая умножается на коэффициент (_Point*10).

    2. Если isPositionBuy равен False:
      • A равен сумме initialPrice и (buySellDiff + buyTP), которая умножается на коэффициент (_Point*10).
      • B равен сумме initialPrice и buySellDiff, где buySellDiff умножается на коэффициент (_Point*10).
      • C равен to initialPrice 
      • D равен initialPrice минус sellTP, где sellTP умножается на коэффициент (_Point*10).

    Теперь для визуализации мы нарисуем на графике несколько линий, представляющих уровни цен A, B, C, D, используя ObjectCreate, и установим для свойства цвета значение clrGreen, используя ObjectSetInteger (цвет может быть любым).

    Теперь нам нужно открыть первоначальный ордер на покупку или продажу в зависимости от переменной isPositionBuy. Для этого мы определяем три переменные: positionType, SL, TP.

    1. positionType: Тип этой переменной ENUM_ORDER _TYPE - предопределенный тип пользовательской переменной, которая может принимать целочисленные значения от 0 до 8 в соответствии со следующей таблицей:

      Целочисленные значения Идентификатор
       0 ORDER_TYPE_BUY
       1 ORDER_TYPE_SELL
       2 ORDER_TYPE_BUY_LIMIT
       3 ORDER_TYPE_SELL_LIMIT
       4 ORDER_TYPE_BUY_STOP
       5 ORDER_TYPE_SELL_STOP
       6 ORDER_TYPE_BUY_STOP_LIMIT
       7 ORDER_TYPE_SELL_STOP_LIMIT
       8 ORDER_TYPE_CLOSE_BY

      Как видим, 0 представляет ORDER_TYPE_BUY, а 1 - ORDER_TYPE_SELL. Нам нужны только эти два значения. Мы будем использовать идентификатор, а не целочисленные значения, поскольку их трудно запомнить.

    2. SL: если isPositionBuy равен true, SL равен уровню цен C, в противном случае - B

    3. TP: если isPositionBuy равен true, TP равен уровню цен A, в противном случае - D

    Используя эти три переменные, нам нужно разместить позицию следующим образом:

    Сначала мы импортируем стандартную торговую библиотеку, используя #include:

    #include <Trade/Trade.mqh>
    

    Непосредственно перед открытием позиции мы создаем экземпляр класса CTrade, используя:

    CTrade trade;
    
    trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);

    Используя этот экземпляр, мы размещаем позицию с функцией PositionOpen в этом экземпляре со следующими параметрами:

    1. _Symbol указывает текущий символ, к которому прикреплен советник.
    2. positionType - это переменная ENUM_ORDER_TYPE, которую мы определили ранее.
    3. initialLotSize - начальный размер лота.
    4. initialPrice - цена открытия ордера, которая равна либо Ask (для позиций на покупку), либо Bid (для позиций на продажу).
    5. Наконец, мы предоставляем уровни цен SL и TP.

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

    Нам осталось сделать еще две вещи:

    if(trade.ResultRetcode() == 10009) hedgeCycleRunning = true;
    isPositionBuy = isPositionBuy ? false : true;

    Здесь мы устанавливаем для hedgeCycleRunning значение true только в случае успешного размещения позиции. Это определяется функцией ResultRetcode() в экземпляре CTrade с именем trade, которая возвращает "10009", указывающее на успешное размещение (вы можете увидеть все эти коды возврата здесь). Причина использования hedgeCycleRunning будет раскрыта в дальнейшем коде.

    И последнее: мы используем тернарный оператор для изменения значения isPositionBuy. Если оно было равно false, оно станет равным true, и наоборот. Мы делаем это, потому что наша стратегия гласит, что после открытия начальной позиции продажа будет размещена после покупки, а покупка будет размещена после продажи, то есть они чередуются.

    На этом мы завершаем обсуждение нашей принципиально важной функции StartHedgeCycle(). Мы будем использовать ее очень часто.

    Теперь давайте рассмотрим последний фрагмент кода.

    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
       {
        double _Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double _Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    
        if(!hedgeCycleRunning)
           {
            StartHedgeCycle();
           }
    
        if(_Bid <= C && !isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
            lastPositionLotSize = newPositionLotSize;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
        if(_Ask >= B && isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
            lastPositionLotSize = newPositionLotSize;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
        if(_Bid >= A || _Ask <= D)
           {
            hedgeCycleRunning = false;
           }
       }
    //+------------------------------------------------------------------+

    Первые две строки говорят сами за себя, они просто определяют _Ask и _Bid (двойные переменные), которые хранят Ask и Bid в данный момент времени.

    Затем мы используем оператор if, чтобы запустить цикл хеджирования с помощью функции StartHedgeCycle(), если переменная hedgeCycleRunning имеет значение false. Мы уже знаем, что делает функция StartHedgeCycle(), но давайте подведем итог. Эта функция:

    1. Определяет ценовые уровни A, B, C, D.
    2. Рисует горизонтальные зеленые линии на ценовых уровнях A, B, C, D для визуализации.
    3. Открывает позицию.
    4. Хранит размер лота позиции в переменной LastPositionLotSize, определенной в глобальном пространстве, чтобы ее можно было использовать повсюду.
    5. Устанавливает hedgeCycleRunning на true. Именно поэтому мы выполняем функцию StartHedgeCycle().
    6. Наконец, меняет значения переменной isPositionBuy с true на false и с false на true, как указано в нашей стратегии.

    Мы выполняем StartHedgeCycle() только один раз, при hedgeCycleRunning равном false, а в конце функции мы меняем его на false. Если мы снова не установим hedgeCycleRunning на false, StartHedgeCycle() не будет выполняться снова.

    Давайте пока пропустим следующие два оператора if и вернемся к ним позже. Давайте посмотрим на последний оператор if:

    if(_Bid >= A || _Ask <= D)
       {
        hedgeCycleRunning = false;
       }

    Он обеспечивает перезапуск цикла. Как мы обсуждали ранее, если мы установим для hedgeCycleRunning значение true, цикл перезапустится, и все, что мы обсуждали ранее, произойдет снова. Кроме того, я позаботился о том, чтобы при перезапуске цикла все позиции предыдущего цикла были закрыты по тейк-профиту (будь то позиция на покупку или продажу).

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

    Мы пропустили код, который обрабатывает это поведение, поэтому вернемся к нему.

    if(_Bid <= C && !isPositionBuy)
       {
        double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
        CTrade trade;
        trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
        lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
        isPositionBuy = isPositionBuy ? false : true;
       }
    
    if(_Ask >= B && isPositionBuy)
       {
        double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
        CTrade trade;
        trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
        lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
        isPositionBuy = isPositionBuy ? false : true;
       }

     Таким образом, эти два оператора if обрабатывают открытие ордера между циклом (между размещением начальной позиции и до завершения цикла).

    1. Первый оператор IF: Управляет открытием ордера на продажу. Если переменная Bid (которая содержит цену Bid на данный момент времени) ниже или равна уровню C и isPositionBuy имеет значение false, то мы определяем двойную переменную с именем newPositionLotSize. Она устанавливается равной LastPositionLotSize, умноженной на lotSizeMultiplier, а затем двойное значение нормализуется до двух десятичных знаков с помощью предопределенной функции под названием NormalizeDouble.

      Затем мы размещаем позицию на продажу, используя предопределенную функцию PositionOpen() из экземпляра CTrade с именем trade, передавая newPositionLotSize в качестве параметра. Наконец, мы устанавливаем для LastPositionLotSize этот новый размер лота (без нормализации), чтобы мы могли умножать его в дальнейших позициях, и, наконец, мы чередуем значение isPositionBuy с true на false или с false на true. 

    2. Второй оператор IF: управляет открытием ордера на покупку. Если цена Ask (переменная, содержащая цену Ask на данный момент времени) равна или превышает уровень B и isPositionBuy имеет значение true, то мы определяем двойную переменную с именем newPositionLotSize. Мы устанавливаем newPositionLotSize равной LastPositionLotSize, умноженной на lotSizeMultiplier, и нормализуем двойное значение до двух десятичных знаков, используя предопределенную функцию NormalizeDouble, как и раньше.

      Затем мы размещаем позицию на покупку, используя предопределенную функцию PositionOpen() из экземпляра CTrade с именем trade, передавая newPositionLotSize в качестве параметра. Наконец, мы устанавливаем для LastPositionLotSize этот новый размер лота (без нормализации), чтобы мы могли умножать его в дальнейших позициях. Наконец, мы чередуем значения isPositionBuy с true на false или с false на true.

    Здесь следует обратить внимание на два очень важных момента:

    • В первом операторе IF мы использовали цену Bid и указали, что нужно открывать позицию, когда Bid опускается ниже или равен C, а isBuyPosition имеет значение false. Почему мы использовали здесь Bid?

      Предположим, мы используем цену Ask, тогда есть вероятность, что предыдущая позиция на покупку будет закрыта, но новая позиция на продажу не откроется. Мы знаем, что покупка открывается по цене Ask и закрывается по цене Bid, оставляя возможность того, что покупка будет закрыта, когда Bid пересечет или сравняется с линией ценового уровня C сверху. Это закроет ордер на покупку по стоп-лоссу, который мы установили ранее при открытии позиции, но ордер на продажу еще не будет открыт. Таким образом, если цены Bid и Ask растут, наша стратегия не реализуется. Именно по этой причине мы использовали Bid вместо Ask.

      Симметрично, во втором операторе IF мы использовали цену Ask и заявили, что откроем позицию, когда Ask поднимется выше или станет равным B, а значение isBuyPosition будет равно true. Почему мы использовали здесь Ask?

      Предположим, мы используем цену Bid, тогда есть вероятность, что предыдущая позиция на продажу будет закрыта, но новая позиция на покупку не откроется. Мы знаем, что продажа открывается по цене Bid и закрывается по цене Ask, что оставляет вероятность того, что продажа будет закрыта, когда Ask пересечет или сравняется с линией ценового уровня B снизу, таким образом закрывая ордер на продажу по стоп-лоссу, который мы установили ранее при открытии позиции. Однако ордер на покупку еще не открыт. Таким образом, если цены Bid и Ask падают, наша стратегия не реализуется. Вот почему мы использовали Ask вместо Bid.

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

    • В обоих операторах IF мы упомянули, что при установке значения LastPositionLotSize мы приравниваем его к (lastPositionLotSize * lotSizeMultiplier), а не к newPositionLotSize, которое равно нормализованному значению (lastPositionLotSize * lotSizeMultiplier) с точностью до двух десятичных знаков с использованием предопределённой функции NormalizeDouble().
      NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2)
      Для чего мы это сделали? При приравнивании нормализованному значению, наша стратегия будет соблюдаться правильно. Например, предположим, что мы установили начальный размер лота 0,01 и множитель 1,5, тогда первый размер лота, конечно, будет равен 0,01, а следующий - 0,01. *1,5 = 0,015. Конечно, мы не можем открыть лот размером 0,015. Это не разрешено брокером. Множитель должен быть целым числом, кратным 0,01. Именно поэтому мы нормализовали размер лота до 2 знаков после запятой. Таким образом, откроется 0,01. Теперь у нас есть 2 варианта установки значения lastPositionLotSize: 0,01 (0,010) или 0,015. Предположим, мы выбираем 0,01 (0,010), тогда в следующий раз, когда мы разместим позицию, мы будем использовать 0,01 * 1,5 = 0,015. После нормализации оно станет 0,01 и так далее. Итак, мы использовали множитель 1,5 и начали с размера лота 0,01, но размер лота никогда не увеличивался, и мы застряли в цикле. Все позиции размещаются с размером лота 0,01, что означает, что мы не должны приравнивать LastPositionLotSize значению 0,01 (0,010), поэтому вместо этого мы выбираем вариант 0,015, то есть значение до нормализации.

      Именно поэтому мы устанавливаем значение lastPositionLotSize равным (lastPositionLotSize * lotSizeMultiplier), а не NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2).

    Наконец, весь наш код выглядит так:

    #include <Trade/Trade.mqh>
    
    input bool initialPositionBuy = true;
    input double buyTP = 15;
    input double sellTP = 15;
    input double buySellDiff = 15;
    input double initialLotSize = 0.01;
    input double lotSizeMultiplier = 2;
    
    
    
    double A, B, C, D;
    bool isPositionBuy;
    bool hedgeCycleRunning = false;
    double lastPositionLotSize;
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
       {
        return(INIT_SUCCEEDED);
       }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
       {
        ObjectDelete(0, "A");
        ObjectDelete(0, "B");
        ObjectDelete(0, "C");
        ObjectDelete(0, "D");
       }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
       {
        double _Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double _Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    
        if(!hedgeCycleRunning)
           {
            StartHedgeCycle();
           }
    
        if(_Bid <= C && !isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            CTrade trade;
            trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
            lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
        if(_Ask >= B && isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            CTrade trade;
            trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
            lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
    if(_Bid >= A || _Ask <= D)
       {
        hedgeCycleRunning = false;
       }
       }
    //+------------------------------------------------------------------+
    
    //+------------------------------------------------------------------+
    //| Hedge Cycle Intialization Function                               |
    //+------------------------------------------------------------------+
    void StartHedgeCycle()
       {
        isPositionBuy = initialPositionBuy;
        double initialPrice = isPositionBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
        A = isPositionBuy ? initialPrice + buyTP * _Point * 10 : initialPrice + (buySellDiff + buyTP) * _Point * 10;
        B = isPositionBuy ? initialPrice : initialPrice + buySellDiff * _Point * 10;
        C = isPositionBuy ? initialPrice - buySellDiff * _Point * 10 : initialPrice;
        D = isPositionBuy ? initialPrice - (buySellDiff + sellTP) * _Point * 10 : initialPrice - sellTP * _Point * 10;
    
        ObjectCreate(0, "A", OBJ_HLINE, 0, 0, A);
        ObjectSetInteger(0, "A", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "B", OBJ_HLINE, 0, 0, B);
        ObjectSetInteger(0, "B", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "C", OBJ_HLINE, 0, 0, C);
        ObjectSetInteger(0, "C", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "D", OBJ_HLINE, 0, 0, D);
        ObjectSetInteger(0, "D", OBJPROP_COLOR, clrGreen);
    
        ENUM_ORDER_TYPE positionType = isPositionBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
        double SL = isPositionBuy ? C : B;
        double TP = isPositionBuy ? A : D;
        CTrade trade;
        trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);
        
        lastPositionLotSize = initialLotSize;
        if(trade.ResultRetcode() == 10009) hedgeCycleRunning = true;
        isPositionBuy = isPositionBuy ? false : true;
       }
    //+------------------------------------------------------------------+
    

    На этом мы завершаем тему автоматизации нашей классической стратегии хеджирования.


    Тестирование классической стратегии хеджирования на истории

    Теперь, когда мы создали советника для автоматического следования нашей стратегии, логично протестировать его и увидеть результаты.

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

    1. initialBuyPosition: true
    2. buyTP: 15
    3. sellTP: 15
    4. buySellDiff: 15
    5. initialLotSize: 0,01
    6. lotSizeMultiplier: 2,0

    Я протестирую его на EURUSD с 1 января 2023 г. по 6 декабря 2023 г. с кредитным плечом 1:500 и суммой депозита в 10 000 долларов США. Если вас интересуют временные рамки, то для нашей стратегии это не имеет значения, поэтому я выберу любой (это никак не повлияет на наши результаты). Посмотрим на результаты ниже:


    Просто взглянув на график, вы можете подумать, что это прибыльная стратегия, но давайте посмотрим на другие данные и обсудим несколько моментов на графике:

    Как вы можете видеть, наша чистая прибыль (Net Profit) составила 1470,62 USD, общая прибыль (Gross Profit) составила 13 153,68 USD, а общий убыток (Gross Loss) равен 11 683,06 USD.

    Также давайте посмотрим на просадку баланса и эквити:

    Абсолютная просадка по балансу (Balance Drawdown Absolute) 1170,10 USD
    Balance Drawdown Maximal 1563,12 USD (15,04%)
    Относительная просадка по балансу (Balance Drawdown Relative) 15,04% (1563,13 USD)
    Абсолютная просадка по эквити (Equity Drawdown Absolute) 2388,66 USD
    Максимальная просадка по средствам (Equity Drawdown Maximal) 2781,97 USD (26,77%)
    Относительная просадка по средствам (Equity Drawdown Relative) 26,77% (2781,97 USD)

    Давайте разберемся в этих терминах:

    1. Абсолютная просадка баланса - разница между начальным капиталом, который в нашем случае составляет 10 000 USD, минус минимальный баланс, который является самой низкой точкой баланса (минимальный баланс).
    2. Максимальная просадка баланса - разница между самой высокой точкой баланса (пиковым балансом) и самой низкой точкой баланса (минимальным балансом).
    3. Максимальная просадка баланса - процент максимальной просадки баланса от наивысшей точки баланса (пикового баланса).

    Определения эквити симметричны:

    1. Абсолютная просадка средств - разница между начальным капиталом, который в нашем случае составляет 10 000 USD, минус минимальный капитал, который является самой низкой точкой капитала (минимальный капитал).
    2. Максимальная просадка баланса - разница между самой высокой точкой эквити (пиковой эквити) и самой низкой точкой эквити (минимальной эквити).
    3. Относительная просадка баланса - процент максимальной просадки эквити от наивысшей точки эквити (пикового эквити).

    Ниже приведены формулы для всех шести параметров:


    Анализируя приведенные выше данные, просадки баланса вызывать наименьшее беспокойство, поскольку их перекрывают просадки эквити. В некотором смысле мы можем сказать, что просадки баланса — это подмножества просадок капитала. Кроме того, просадка эквити является нашей самой большой проблемой при следовании нашей стратегии, поскольку мы удваиваем размер лота в каждом ордере, что приводит к экспоненциальному росту размера лота. Это можно визуализировать с помощью таблицы ниже:

    Количество открытых позиций Размер лота следующей позиции (еще не открыта) Требуемая маржа для следующей позиции (EURUSD) 
    0 0,01 2,16 USD
    1 0,02 4,32  USD
    2 0,04 8,64 USD
    3 0,08 17,28 USD
    4 0,16 34,56 USD
    5 0,32 69,12 USD
    6 0,64 138,24 USD
    7 1,28 276,48 USD
    8 2,56 552,96 USD
    9 5,12 1105,92 USD
    10 10,24 2211,84 USD
    11 20,48 4423,68 USD
    12 40,96 8847,36 USD
    13 80,92 17 694,72 USD
    14 163,84 35 389,44 USD

    В нашем исследовании мы в настоящее время используем EURUSD в качестве торговой пары. Важно отметить, что требуемая маржа для лота размером 0,01 составляет 2,16 USD, хотя эта цифра может быть изменена.

    По мере исследования мы наблюдаем примечательную тенденцию: требуемая маржа для последующих позиций увеличивается в геометрической прогрессии. Например, после 12-го ордера мы столкнулись с финансовым затруднением. Требуемая маржа взлетает до 17 694,44 USD, при том что наши первоначальные инвестиции составляли 10 000 USD. Этот сценарий даже не учитывает наши стоп-лоссы.

    Разберем эту проблему дальше. Если бы мы включили стоп-лоссы, установленные на уровне 15 пунктов на сделку, и понесли убытки в первые 12 сделок, наш совокупный размер лота составил бы ошеломляющее значение в 81,91 (сумма ряда: 0,01+0,02+0,04+...+ 20,48+40,96). Это означает общий убыток в размере 12 286,5 USD, рассчитанный с использованием значения EURUSD, равного 1 USD за 10 пунктов для размера лота 0,01. Расчет прост: (81,91/0,01) * 1,5 = 12 286,5 USD. Убыток не только превышает наш первоначальный капитал, но и делает невозможным сохранение 12 позиций в цикле с инвестициями в 10 000 USD в EURUSD.

    Давайте рассмотрим немного другой сценарий: сможем ли мы сохранить 11 позиций с нашими 10 000 USD в EURUSD?

    Представим, что мы достигли 10 позиций. Это означает, что мы уже столкнулись со стоп-лоссами на 9 позициях и вот-вот потеряем 10-ю. Если мы планируем открыть 11-ю позицию, общий размер лота для 10 позиций составит 10,23, что приведет к убытку в размере 1534,5 USD. Расчет производится как и раньше, с учетом курса EURUSD и размера лота. Требуемая маржа для следующей позиции составит 4 423,68 USD. Суммируя эти суммы, мы получаем 5 958,18 USD, что значительно ниже нашего порога в 10 000 USD. Следовательно, вполне возможно пережить 10 позиций и открыть 11-ю.

    Однако возникает вопрос: можно ли с теми же 10 000  USD увеличить общее количество позиций до 12?

    Для этого предположим, что мы достигли границы 11 позиций. Здесь мы уже понесли потери по 10 позициям и находимся на грани потери 11-й. Общий размер лота для этих 11 позиций составляет 20,47, что приводит к убытку в размере 3070,5 USD. Если добавить необходимую маржу для 12-й позиции, которая составляет внушительные 8 847,36 USD, наши общие расходы взлетят до 11 917,86 USD, превысив наши первоначальные инвестиции. Поэтому понятно, что открывать 12-ю позицию губительно в финансовом отношении. Мы бы уже потеряли 3070,5 USD, оставшись с 6929,5 USD.

    Из статистики тестирования мы видим, что стратегия опасно близка к краху даже при инвестициях в размере 10 000 USD в относительно стабильную валюту, такую как EURUSD. Максимальные последовательные потери составляют 10, что указывает на то, что мы были всего в нескольких пипсах от катастрофической 11-й позиции. Если 11-я позиция также достигнет своего стоп-лосса, стратегия развалится, что приведет к значительным потерям.

    В нашем отчете абсолютная просадка отмечена на уровне 2388,66 USD. Если бы мы достигли стоп-лосса на 11-й позиции, наши потери выросли бы до 3070,5 USD. В этом случае от полного провала стратегии нас отделяли бы 681,84  USD (3070,5–2388,66).

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

    Обратите внимание на участки, отмеченные красным. В этих случаях, несмотря на выигрыш в сделке (выигрыш соответствует получению наибольшего лота в последней сделке), нам не удалось получить никакой прибыли. Эта аномалия объясняется спредом. Его изменчивый характер еще больше усложняет нашу стратегию, что требует более глубокого анализа в следующей части этой серии.

    Мы также должны учитывать ограничения классической стратегии хеджирования. Одним из существенных недостатков является значительная удерживающая способность, необходимая для поддержания большого количества ордеров, если уровень тейк-профита не будет достигнут раньше. Эта стратегия может обеспечить прибыль только в том случае, если множитель размера лота равен 2 или больше (в случае buyTP = SellTP = buySellDiff, не считая спред). Если оно меньше 2, существует риск уйти в минус при увеличении количества ордеров. В следующей части нашей серии мы рассмотрим эту динамику и то, как оптимизировать классическую стратегию хеджирования для получения максимальной прибыли.


    Заключение

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

    До сих пор мы использовали произвольные фиксированные значения lotSizeMultiplier, initialLotSize, buySellDiff, sellTP и buyTP, но мы можем оптимизировать эту стратегию и найти оптимальные значения этих входных параметров, которые дадут нам максимально возможную отдачу. Также мы выясним, выгодно ли изначально начинать с покупки или продажи, что также может зависеть от различных валют и рыночных условий. Итак, в следующей части серии мы расскажем много полезного, так что следите за обновлениями. 

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

    Удачного программирования и торговли!


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

    Прикрепленные файлы |
    Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
    DidMa
    DidMa | 9 дек. 2023 в 04:01
    Отличная статья, большое спасибо. Жду не дождусь второй части, так как я сам кодер и собираюсь написать систему хеджирования, но по другой стратегии. Спасибо за вашу работу и за то, что делитесь!
    Kailash Bai Mina
    Kailash Bai Mina | 9 дек. 2023 в 15:25
    DidMa #:
    Отличная статья, большое спасибо. Жду не дождусь второй части, так как я сам кодер и собираюсь написать систему хеджирования, но по другой стратегии. Спасибо за вашу работу и за то, что делитесь с нами!

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

    С уважением.

    Lionel Niquet
    Lionel Niquet | 24 янв. 2024 в 03:44

    Очень хороший подход. Четкий и лаконичный.

    С уважением.

    greatrufai1
    greatrufai1 | 20 февр. 2024 в 01:17
    Отличная работа @Kailash Bai Mina. Я пытался использовать автоматизированную стратегию, которая анализирует структуру рынка для определения начальной позиции. Я с удовольствием поделюсь ею с вами здесь, но не буду опережать самого себя. Когда вы напишете об этом, пожалуйста, ??????
    Фильтрация и извлечение признаков в частотной области Фильтрация и извлечение признаков в частотной области
    В этой статье мы рассмотрим применение цифровых фильтров к временным рядам, представленным в частотной области, с целью извлечения уникальных признаков, которые могут быть полезными для моделей прогнозирования.
    Машинное обучение и Data Science (Часть 16): Свежий взгляд на деревья решений Машинное обучение и Data Science (Часть 16): Свежий взгляд на деревья решений
    В последней части нашей серии о машинном обучении и работе с большими данными мы снова возвращаемся к деревьям решений. Эта статья предназначена для трейдеров, которые хотят понять роль деревьев решений в анализе рыночных тенденций. В ней собрана вся основная информация о структуре, предназначении и использовании таких деревьев. Мы рассмотри корни и ветви алгоритмических деревьев и узнаем, в чем же заключается их потенциал применительно к принятию торговых решений. Давайте вместе по-новому взглянем на деревья решений и посмотри, как они могут помочь преодолевать сложности на финансовых рынках.
    Возможности Мастера MQL5, которые вам нужно знать (Часть 09): Сочетание кластеризации k-средних с фрактальными волнами Возможности Мастера MQL5, которые вам нужно знать (Часть 09): Сочетание кластеризации k-средних с фрактальными волнами
    Кластеризация k-средних использует подход к группировке точек данных в виде процесса, изначально фокусирующегося на макропредставлении набора данных, в котором применяются случайно сгенерированные центроиды кластера. Затем эти центроиды масштабируются и настраиваются для точного представления набора данных. В статье рассматриваются кластеризация и несколько вариантов ее использования.
    Популяционные алгоритмы оптимизации: Алгоритм боидов, или алгоритм стайного поведения (Boids Algorithm, Boids) Популяционные алгоритмы оптимизации: Алгоритм боидов, или алгоритм стайного поведения (Boids Algorithm, Boids)
    В данной статье мы проводим исследование алгоритма Boids, в основе которого лежат уникальные примеры стайного поведения животных. Алгоритм Boids, в свою очередь, послужил основой для создания целого класса алгоритмов, объединенных под названием "Роевый интеллект".