English Deutsch 日本語 Português
preview
Разработка и тестирование торговых систем на основе Канала Кельтнера

Разработка и тестирование торговых систем на основе Канала Кельтнера

MetaTrader 5Трейдинг | 6 июня 2024, 14:00
705 0
Mohamed Abdelmaaboud
Mohamed Abdelmaaboud

Введение

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

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

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

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


Определение волатильности

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

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

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

Когда дело доходит до измерения волатильности, мы видим, что существует множество методов, например, бета-коэффициент и стандартные отклонения. Существует также множество технических индикаторов, которые можно использовать для измерения волатильности на рынке, таких как полосы Боллинджера, средний истинный диапазон (ATR), индекс волатильности (VIX) и канал Кельтнера, который мы рассматриваем в этой статье. Измерение волатильности может быть очень полезным для оценки колебаний финансового актива.

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


Определение канала Кельтнера

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

Канал Кельтнера был впервые представлен Честером Кельтнером в 1960-х годах в его книге "Как зарабатывать деньги на сырьевых товарах" (How to Make Money in Commodities). В своих расчетах он использовал простую скользящую среднюю и диапазон максимумов и минимумов. Сейчас для расчетов обычно применяется средний истинный диапазон (ATR). Типичная настройка скользящей средней составляет 20 периодов, верхняя и нижняя полосы рассчитываются как удвоенный ATR выше и ниже EMA. Эти настройки можно регулировать в соответствии с предпочтениями пользователя и торговыми целями.

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

Ниже показано, как мы можем рассчитать канал Кельтнера:

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

Итак,

Средняя линия канала Кельтнера = экспоненциальная скользящая средняя (EMA)

Верхняя полоса канала Кельтнера =  экспоненциальная скользящая средняя (EMA) + 2 * средний истинный диапазон (ATR)

Нижняя полоса канала Кельтнера =  экспоненциальная скользящая средняя (EMA) - 2 * средний истинный диапазон (ATR)


Стратегии канала Кельтнера

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

  • Отскок от полос: мы разместим ордер на покупку при отскоке выше нижней полосы и разместим ордер на продажу при отскоке ниже верхней полосы.
  • Прорыв полос: мы разместим ордер на покупку при пробитии верхней полосы и ордер на продажу при пробитии нижней полосы.

Стратегия первая: отскок от полос

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

Схематично это можно представить так:

Предпоследняя цена закрытия < нижняя полоса и последняя цена закрытия > нижняя полоса ==> сигнал на покупку

Предпоследняя цена закрытия > верхняя полоса и последняя цена закрытия < верхняя полоса ==> сигнал на продажу

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

Схематично это можно представить так:

Предпоследняя цена закрытия < верхняя полоса и последняя цена закрытия > верхняя полоса ==> сигнал на покупку

Предпоследняя цена закрытия > нижняя полоса и последняя цена закрытия < нижняя полоса ==> сигнал на продажу


Торговая система канала Кельтнера

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

Используем препроцессор #property для определения места индикатора и используем indicator_chart_window для отображения индикатора на графике.

#property indicator_chart_window

При определении индикатора буфера через идентификатор Indicator_buffers мы используем значение 3, а при определении количества отображаемых линий (plots) также используем значение 3.

#property indicator_buffers 3
#property indicator_plots 3

Настроим индикаторы по типу, стилю, ширине, цвету и метке для верхней, средней и нижней линий.

#property indicator_type1 DRAW_LINE
#property indicator_style1 STYLE_SOLID
#property indicator_width1 2
#property indicator_color1 clrRed
#property indicator_label1 "Keltner Upper Band"


#property indicator_type2 DRAW_LINE
#property indicator_style2 STYLE_SOLID
#property indicator_width2 1
#property indicator_color2 clrBlue
#property indicator_label2 "Keltner Middle Line"


#property indicator_type3 DRAW_LINE
#property indicator_style3 STYLE_SOLID
#property indicator_width3 2
#property indicator_color3 clrGreen
#property indicator_label3 "Keltner Lower Band"

Настроим период скользящей средней, множитель канала, применение ATR при расчете полос, метод скользящего среднего и тип цены.

input int      maPeriod            = 10;           // Moving Average Period
input double   multiInp             = 2.0;          // Channel multiplier
input bool     isAtr                 = false;        // ATR
input ENUM_MA_METHOD     maMode       = MODE_EMA;     // Moving Average Mode
input ENUM_APPLIED_PRICE priceType = PRICE_TYPICAL;// Price Type

Объявим три массива (верхний, средний и нижний) глобальных переменных, два хэндла скользящей средней и ATR, а также minBars.

double upper[], middle[], lower[];
int maHandle, atrHandle;
static int minBars = maPeriod + 1;

В функции OnInit() мы свяжем индикаторный буфер с массивами с помощью функции SetIndexBuffer со следующими параметрами:

  • index - буфер индекса: 0 - верхний, 1 - средний, 2 - нижний.
  • buffer[] - массив (upper - верхний, middle - средний, lower - нижний).
  • data_type - данные для хранения индикатора (может быть одним из ENUM_INDEXBUFFER_TYPE, по умолчанию - INDICATOR_DATA, именно его и будем использовать).
   SetIndexBuffer(0,upper,     INDICATOR_DATA);
   SetIndexBuffer(1,middle,  INDICATOR_DATA);
   SetIndexBuffer(2,lower,  INDICATOR_DATA);

Установим флаг AS_Series для массивов с помощью функции ArraySetAsSeries и ее параметров:

  • array[] - массив по ссылке (upper, middle и lower).
  • flag - направление индексации массива (true - успех, false - ошибка).
   ArraySetAsSeries(upper, true);
   ArraySetAsSeries(middle, true);
   ArraySetAsSeries(lower, true);

Установим имя индикатора с помощью функции IndicatorSetString и ее параметров:

  • prop_id - идентификатор (может быть одним из ENUM_CUSTOMIND_PROPERTY_STRING). Мы будем использовать INDICATOR_SHORTNAME для установки имени индикатора.
  • prop_value - желаемое имя индикатора.
IndicatorSetString(INDICATOR_SHORTNAME,"Custom Keltner Channel " + IntegerToString(maPeriod));

Установим значения индикатора с помощью функции IndicatorSetInteger. Ее параметры такие же, как у IndicatorSetString, за исключением того, что тип данных будет prop_value вместо string.

IndicatorSetInteger(INDICATOR_DIGITS,_Digits);

Определим хэндл скользящего среднего с помощью функции iMA. Ее параметры:

  • symbol - имя символа. NULL - текущий символ.
  • period - период. 0 - текущий период.
  • ma_period - период скользящей средней. Мы используем maPeriod.
  • ma_shift - горизонтальный сдвиг при необходимости.
  • ma_method - метод скользящей средней. Мы используем maMode.
  • applied_price - тип цены. Используем priceType.
maHandle = iMA(NULL, 0, maPeriod, 0, maMode, priceType);

Определим ATR в соответствии с параметрами ATR. true - использовать при расчете полос, false - не использовать.

   if(isAtr)
     {
      atrHandle = iATR(NULL, 0, maPeriod);
      if(atrHandle == INVALID_HANDLE)
        {
         Print("Handle Error");
         return(INIT_FAILED);
        }
     }
   else
      atrHandle = INVALID_HANDLE;

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

void indValue(const double& h[], const double& l[], int shift)
  {
   double ma[1];
   if(CopyBuffer(maHandle, 0, shift, 1, ma) <= 0)
      return;
   middle[shift] = ma[0];
   double average = AVG(h, l, shift);
   upper[shift]    = middle[shift] + average * multiInp;
   lower[shift] = middle[shift] - average * multiInp;
  }

Объявим функции AVG для расчета положения границ на основе рассчитанного множителя

double AVG(const double& High[],const double& Low[], int shift)
  {
   double sum = 0.0;
   if(atrHandle == INVALID_HANDLE)
     {
      for(int i = shift; i < shift + maPeriod; i++)
         sum += High[i] - Low[i];
     }
   else
     {
      double t[];
      ArrayResize(t, maPeriod);
      ArrayInitialize(t, 0);
      if(CopyBuffer(atrHandle, 0, shift, maPeriod, t) <= 0)
         return sum;
      for(int i = 0; i < maPeriod; i++)
         sum += t[i];
     }
   return sum / maPeriod;
  }

В функции OnCalculate рассчитаем значения индикатора

   if(rates_total <= minBars)
      return 0;
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low,  true);
   int limit = rates_total - prev_calculated;
   if(limit == 0)             
     {
     }
   else
      if(limit == 1)      
        {
         indValue(high, low, 1);
         return(rates_total);
        }
      else
         if(limit > 1)       
           {
            ArrayInitialize(middle, EMPTY_VALUE);
            ArrayInitialize(upper,    EMPTY_VALUE);
            ArrayInitialize(lower, EMPTY_VALUE);
            limit = rates_total - minBars;
            for(int i = limit; i >= 1 && !IsStopped(); i--)
               indValue(high, low, i);
            return(rates_total);
           }
   indValue(high, low, 0);
   return(rates_total);

Ниже представлен полный код в одном блоке пользовательского канала Кельтнера:

//+------------------------------------------------------------------+
//|                                       Custom_Keltner_Channel.mq5 |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots 3
#property indicator_type1 DRAW_LINE
#property indicator_style1 STYLE_SOLID
#property indicator_width1 2
#property indicator_color1 clrRed
#property indicator_label1 "Keltner Upper Band"
#property indicator_type2 DRAW_LINE
#property indicator_style2 STYLE_SOLID
#property indicator_width2 1
#property indicator_color2 clrBlue
#property indicator_label2 "Keltner Middle Line"
#property indicator_type3 DRAW_LINE
#property indicator_style3 STYLE_SOLID
#property indicator_width3 2
#property indicator_color3 clrGreen
#property indicator_label3 "Keltner Lower Band"
input int      maPeriod            = 10;           // Moving Average Period
input double   multiInp             = 2.0;          // Channel multiplier
input bool     isAtr                 = false;        // ATR
input ENUM_MA_METHOD     maMode       = MODE_EMA;     // Moving Average Mode
input ENUM_APPLIED_PRICE priceType = PRICE_TYPICAL;// Price Type
double upper[], middle[], lower[];
int maHandle, atrHandle;
static int minBars = maPeriod + 1;
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0,upper, INDICATOR_DATA);
   SetIndexBuffer(1,middle, INDICATOR_DATA);
   SetIndexBuffer(2,lower, INDICATOR_DATA);
   ArraySetAsSeries(upper, true);
   ArraySetAsSeries(middle, true);
   ArraySetAsSeries(lower, true);
   IndicatorSetString(INDICATOR_SHORTNAME,"Custom Keltner Channel " + IntegerToString(maPeriod));
   IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
   maHandle = iMA(NULL, 0, maPeriod, 0, maMode, priceType);
   if(isAtr)
     {
      atrHandle = iATR(NULL, 0, maPeriod);
      if(atrHandle == INVALID_HANDLE)
        {
         Print("Handle Error");
         return(INIT_FAILED);
        }
     }
   else
      atrHandle = INVALID_HANDLE;
   return INIT_SUCCEEDED;
  }
void indValue(const double& h[], const double& l[], int shift)
  {
   double ma[1];
   if(CopyBuffer(maHandle, 0, shift, 1, ma) <= 0)
      return;
   middle[shift] = ma[0];
   double average = AVG(h, l, shift);
   upper[shift]    = middle[shift] + average * multiInp;
   lower[shift] = middle[shift] - average * multiInp;
  }
double AVG(const double& High[],const double& Low[], int shift)
  {
   double sum = 0.0;
   if(atrHandle == INVALID_HANDLE)
     {
      for(int i = shift; i < shift + maPeriod; i++)
         sum += High[i] - Low[i];
     }
   else
     {
      double t[];
      ArrayResize(t, maPeriod);
      ArrayInitialize(t, 0);
      if(CopyBuffer(atrHandle, 0, shift, maPeriod, t) <= 0)
         return sum;
      for(int i = 0; i < maPeriod; i++)
         sum += t[i];
     }
   return sum / maPeriod;
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(rates_total <= minBars)
      return 0;
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low,  true);
   int limit = rates_total - prev_calculated;
   if(limit == 0)             
     {
     }
   else
      if(limit == 1)      
        {
         indValue(high, low, 1);
         return(rates_total);
        }
      else
         if(limit > 1)       
           {
            ArrayInitialize(middle, EMPTY_VALUE);
            ArrayInitialize(upper,    EMPTY_VALUE);
            ArrayInitialize(lower, EMPTY_VALUE);
            limit = rates_total - minBars;
            for(int i = limit; i >= 1 && !IsStopped(); i--)
               indValue(high, low, i);
            return(rates_total);
           }
   indValue(high, low, 0);
   return(rates_total);
  }
//+------------------------------------------------------------------+

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

KCH_insert

Как мы видим на графике, у нас есть канал, окружающий цену. Это и есть канал Кельтнера. Теперь нам нужно создать нашу торговую систему с использованием индикатора на основе упомянутых стратегий. Мы можем сделать это так, как показано ниже.

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


Стратегия первая: отскок от полос:

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

Включим файл торговли с помощью препроцессора include

#include <trade/trade.mqh>

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

input int      maPeriod            = 10;           // Moving Average Period
input double   multiInp             = 2.0;          // Channel multiplier
input bool     isAtr                 = false;        // ATR
input ENUM_MA_METHOD     maMode       = MODE_EMA;     // Moving Average Mode
input ENUM_APPLIED_PRICE priceType = PRICE_TYPICAL;// Price Type
input double      lotSize=1;
input double slPips = 150;
input double tpPips = 300;

Объявим глобальные переменные для Keltner, barTotal и торгового объекта

int keltner;
int barsTotal;
CTrade trade;

В функции OnInit() определим переменную barsTotal, которая будет использоваться позже при оценке наличия или отсутствия нового бара с помощью функции iBars. Ее параметры:

  • symbol - имя символа, к которому примнеяется код. _Symbol - текущий символ.
  • timeframe - таймфрейм. PERIOD_CURRENT - текущий таймфрейм.
barsTotal=iBars(_Symbol,PERIOD_CURRENT);

Определим переменную Кельтнера, которая будет использоваться в качестве хэндла индикатора, с помощью функции iCustom и ее параметров:

  • symbol - имя символа. _Symbl - текущий символ.
  • period - таймфрейм. Мы используем текущий (PERIOD_CURRENT).
  • name - имя индикатора.
  • ...: параметры индикатора.
keltner = iCustom(_Symbol,PERIOD_CURRENT,"Custom_Keltner_Channel",maPeriod,multiInp, isAtr, maMode, priceType);

В функции OnDeinit выведем сообщение при удалении советника

Print("EA is removed");

В функции OnTick() мы объявим и определим целочисленную переменную баров, которые будут сравниваться с barsTotal, чтобы проверить, есть ли новый бар

int bars=iBars(_Symbol,PERIOD_CURRENT);

Убедимся, что barsTotal меньше bars

if(barsTotal < bars)

Если barsTotal меньше bars, присвоим bars значению barsTotal

barsTotal=bars;

Объявим три массива для upper, middle и lower

double upper[], middle[], lower[];

Получим данные индикаторного буфера с помощью функции CopyBuffer. Ее параметры:

  • indicator_handle - хэндл индикатора. Мы используем Keltner для upper, middle и lower.
  • buffer_num - указать номер буфера индикатора (0 - upper, 1 - middle, 2 - lower).
  • start_pos - начальная позиция для расчета. Мы используем 0.
  • count - копируемый объем. Мы используем 3.
  • buffer[] - целевой массив для копирования. Мы будем использовать массивы upper, middle и lower.
      CopyBuffer(keltner,0,0,3,upper);
      CopyBuffer(keltner,1,0,3,middle);
      CopyBuffer(keltner,2,0,3,lower);

Установим флаг AS_SERIES с помощью функции ArraySetAsSeries

      ArraySetAsSeries(upper,true);
      ArraySetAsSeries(middle,true);
      ArraySetAsSeries(lower,true);

Объявим и определим значения prevUpper, middle и lower предпоследнего значения индикатора

      double prevUpperValue = NormalizeDouble(upper[2], 5);
      double prevMiddleValue = NormalizeDouble(middle[2], 5);
      double prevLowerValue = NormalizeDouble(lower[2], 5);

Объявим и определим значения upper, middle и lower последнего значения индикатора

      double upperValue = NormalizeDouble(upper[1], 5);
      double middleValue = NormalizeDouble(middle[1], 5);
      double lowerValue = NormalizeDouble(lower[1], 5);

Объявим и определим последнюю и предыдущую цены закрытия с помощью функции iClose. Ее параметры:

  • symbol - имя символа.
  • timeframe - применяемый таймфрейм.
  • shift - нужен ли нам сдвиг назад или нет.
      double lastClose=iClose(_Symbol,PERIOD_CURRENT,1);
      double prevLastClose=iClose(_Symbol,PERIOD_CURRENT,2);

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

      if(prevLastClose<prevLowerValue && lastClose>lowerValue)
        {
         double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
         double slVal = lowerValue - slPips*_Point;
         double tpVal = middleValue + tpPips*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }

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

      if(prevLastClose>prevUpperValue && lastClose<upperValue)
        {
         double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
         double slVal = upperValue + slPips*_Point;
         double tpVal = middleValue - tpPips*_Point;
         trade.Sell(lotSize,_Symbol,bid,upperValue,tpVal);
        }

Полный код торговой системы, работающей по стратегии отскока от полос:

//+------------------------------------------------------------------+
//|                                       Keltner_Trading_System.mq5 |
//+------------------------------------------------------------------+
#include <trade/trade.mqh>
input int      maPeriod            = 10;           // Moving Average Period
input double   multiInp             = 2.0;          // Channel multiplier
input bool     isAtr                 = false;        // ATR
input ENUM_MA_METHOD     maMode       = MODE_EMA;     // Moving Average Mode
input ENUM_APPLIED_PRICE priceType = PRICE_TYPICAL;// Price Type
input double      lotSize=1;
input double slPips = 150;
input double tpPips = 300; 
int keltner;
int barsTotal;
CTrade trade;
int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   keltner = iCustom(_Symbol,PERIOD_CURRENT,"Custom_Keltner_Channel",maPeriod,multiInp, isAtr, maMode, priceType);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }
//+------------------------------------------------------------------+
void OnTick()
  {
   int bars=iBars(_Symbol,PERIOD_CURRENT);
   if(barsTotal < bars)
     {
      barsTotal=bars;
      double upper[], middle[], lower[];
      CopyBuffer(keltner,0,0,3,upper);
      CopyBuffer(keltner,1,0,3,middle);
      CopyBuffer(keltner,2,0,3,lower);
      ArraySetAsSeries(upper,true);
      ArraySetAsSeries(middle,true);
      ArraySetAsSeries(lower,true);
      double prevUpperValue = NormalizeDouble(upper[2], 5);
      double prevMiddleValue = NormalizeDouble(middle[2], 5);
      double prevLowerValue = NormalizeDouble(lower[2], 5);
      double upperValue = NormalizeDouble(upper[1], 5);
      double middleValue = NormalizeDouble(middle[1], 5);
      double lowerValue = NormalizeDouble(lower[1], 5);
      double lastClose=iClose(_Symbol,PERIOD_CURRENT,1);
      double prevLastClose=iClose(_Symbol,PERIOD_CURRENT,2);
      if(prevLastClose<prevLowerValue && lastClose>lowerValue)
        {
         double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
         double slVal = lowerValue - slPips*_Point;
         double tpVal = middleValue + tpPips*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }
      if(prevLastClose>prevUpperValue && lastClose<upperValue)
        {
         double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
         double slVal = upperValue + slPips*_Point;
         double tpVal = middleValue - tpPips*_Point;
         trade.Sell(lotSize,_Symbol,bid,upperValue,tpVal);
        }
     }
  }
//+------------------------------------------------------------------+


Стратегия вторая: прорыв полос:

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

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

      if(prevLastClose<prevUpperValue && lastClose>upperValue)
        {
         double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
         double slVal = upperValue - slPips*_Point;
         double tpVal = upperValue + tpPips*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }

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

      if(prevLastClose>prevLowerValue && lastClose<lowerValue)
        {
         double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
         double slVal = lowerValue + slPips*_Point;
         double tpVal = lowerValue - tpPips*_Point;
         trade.Sell(lotSize,_Symbol,bid,upperValue,tpVal);
        }

Полный код торговой системы, работающей по стратегии отскока от полос:

//+------------------------------------------------------------------+
//|                                      Keltner_Trading_System2.mq5 |
//+------------------------------------------------------------------+
#include <trade/trade.mqh>
input int      maPeriod            = 10;           // Moving Average Period
input double   multiInp             = 2.0;          // Channel multiplier
input bool     isAtr                 = false;        // ATR
input ENUM_MA_METHOD     maMode       = MODE_EMA;     // Moving Average Mode
input ENUM_APPLIED_PRICE priceType = PRICE_TYPICAL;// Price Type
input double      lotSize=1;
input double slPips = 150;
input double tpPips = 500; 
int keltner;
int barsTotal;
CTrade trade;
int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   keltner = iCustom(_Symbol,PERIOD_CURRENT,"Custom_Keltner_Channel",maPeriod,multiInp, isAtr, maMode, priceType);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }
//+------------------------------------------------------------------+
void OnTick()
  {
   int bars=iBars(_Symbol,PERIOD_CURRENT);
   if(barsTotal < bars)
     {
      barsTotal=bars;
      double upper[], middle[], lower[];
      CopyBuffer(keltner,0,0,3,upper);
      CopyBuffer(keltner,1,0,3,middle);
      CopyBuffer(keltner,2,0,3,lower);
      ArraySetAsSeries(upper,true);
      ArraySetAsSeries(middle,true);
      ArraySetAsSeries(lower,true);
      double prevUpperValue = NormalizeDouble(upper[2], 5);
      double prevMiddleValue = NormalizeDouble(middle[2], 5);
      double prevLowerValue = NormalizeDouble(lower[2], 5);
      double upperValue = NormalizeDouble(upper[1], 5);
      double middleValue = NormalizeDouble(middle[1], 5);
      double lowerValue = NormalizeDouble(lower[1], 5);
      double lastClose=iClose(_Symbol,PERIOD_CURRENT,1);
      double prevLastClose=iClose(_Symbol,PERIOD_CURRENT,2);
      if(prevLastClose<prevUpperValue && lastClose>upperValue)
        {
         double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
         double slVal = upperValue - slPips*_Point;
         double tpVal = upperValue + tpPips*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }
      if(prevLastClose>prevLowerValue && lastClose<lowerValue)
        {
         double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
         double slVal = lowerValue + slPips*_Point;
         double tpVal = lowerValue - tpPips*_Point;
         trade.Sell(lotSize,_Symbol,bid,upperValue,tpVal);
        }
     }
  }
//+------------------------------------------------------------------+

Протестируем эти советники двух торговых стратегий на золоте (XAUUSD), EURUSD, GBPUSD, используя таймфрейм H1 с настройками входных данных по умолчанию на период с 1 января по 31 декабря 2023 года.

Мы сосредоточимся на следующих ключевых измерениях, чтобы провести сравнение между ними:

  • Чистая прибыль (Net Profit) - рассчитывается путем вычитания валового убытка из валовой прибыли. Чем выше значение, тем лучше.
  • Относительная просадка по балансу (Balance DD relative) - максимальный убыток на счете во время работы. Чем ниже значение, тем лучше.
  • Профит-фактор (Profit Factor) - отношение валовой прибыли к валовому убытку. Чем выше значение, тем лучше.
  • Матожидание выигрыша (Expected payoff) - средняя прибыль или убыток сделки. Чем выше значение, тем лучше.
  • Фактор восстановления (Recovery factor) - насколько хорошо протестированная стратегия восстанавливается после потерь. Чем выше значение, тем лучше.
  • Коэффициент Шарпа (Sharpe Ratio) - риск и стабильность тестируемой торговой системы путем сравнения доходности с безрисковой доходностью. Чем выше значение, тем лучше.


Стратегия первая: отскок от полос:

Тестирование на XAUUSD

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

график

backtest

backtest2

Как мы видим из результатов тестирования на XAUUSD, у нас есть важные значения:

  • Чистая прибыль: USD 11918,60.
  • Относительная просадка по балансу: 3,67%.
  • Профит-фактор: 1,36.
  • Матожидание выигрыша: 75,91.
  • Фактор восстановления: 2,85.
  • Коэффициент Шарпа: 4,06.

Тестирование на EURUSD

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

график

backtest

backtest2

Как мы видим из результатов тестирования на EURUSD, у нас есть важные значения

  • Чистая прибыль: USD 2221,20.
  • Относительная просадка по балансу: 2,86%.
  • Профит-фактор: 1,11.
  • Матожидание выигрыша: 13,63.
  • Фактор восстановления: 0,68.
  • Коэффициент Шарпа: 1,09.

Тестирование на GBPUSD

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

график

backtest

backtest2

Как мы видим из результатов тестирования на GBPUSD, у нас есть важные значения

  • Чистая прибыль: USD -1389,40.
  • Относительная просадка по балансу: 4,56%.
  • Профит-фактор: 0,94.
  • Матожидание выигрыша: -8,91.
  • Фактор восстановления: -0,28.
  • Коэффициент Шарпа: -0,78.


Стратегия вторая: прорыв полос:

Тестирование на XAUUSD

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

график

backtest

backtest2

Как мы видим из результатов тестирования на XAUUSD, у нас есть важные значения:

  • Чистая прибыль: USD -11783.
  • Относительная просадка по балансу: 12,89%.
  • Профит-фактор: 0,56.
  • Матожидание выигрыша: -96,58.
  • Фактор восстановления: -0,83.
  • Коэффициент Шарпа: -5,00.

Тестирование на EURUSD

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

график

backtest

backtest2

Как мы видим из результатов тестирования на EURUSD, у нас есть важные значения

  • Чистая прибыль: USD -1358,30.
  • Относительная просадка по балансу: 6,53%.
  • Профит-фактор: 0,94.
  • Матожидание выигрыша: -8,54.
  • Фактор восстановления: -0,20.
  • Коэффициент Шарпа: -0,59.

Тестирование на GBPUSD

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

график

backtest

backtest2

Как мы видим из результатов тестирования на GBPUSD, у нас есть важные значения

  • Чистая прибыль: USD -3930,60.
  • Относительная просадка по балансу: 5,06%.
  • Профит-фактор: 0,84.
  • Матожидание выигрыша: -25,69.
  • Фактор восстановления: -0,75.
  • Коэффициент Шарпа: -2,05.

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

all_results2

Основываясь на таблице выше, мы можем найти наилучшие значения, соответствующие протестированной стратегии и таймфрейму:
  • Чистая прибыль: Наилучший показатель (USD 11918,60) получен при использовании стратегии отскока от полос при тестировании на XAUUSD.
  • Относительная просадка по балансу: Наилучший показатель (2,86 %) получен при использовании стратегии отскока от полос при тестировании на EURUSD.
  • Профит-фактор: Наилучший показатель (1,36) получен при использовании стратегии отскока от полос при тестировании на XAUUSD.
  • Матожидание: Наилучший показатель (75,91) получен при использовании стратегии отскока от полос при тестировании на XAUUSD
  • Коэффициент восстановления: Наилучший показатель (2,85) получен при использовании стратегии отскока от полос при тестировании на XAUUSD
  • Коэффициент Шарпа: Наилучший показатель (4,06) получен при использовании стратегии отскока от полос при тестировании на XAUUSD.

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


Заключение

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

В этой статье я показал результаты тестирования простых стратегий, основанных на канале Кельтнера.

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

  • Отскок от полос: мы размещаем ордер на покупку при отскоке выше нижней полосы и разместим ордер на продажу при отскоке ниже верхней полосы.
  • Прорыв полос: мы разместим ордер на покупку при пробитии верхней полосы и ордер на продажу при пробитии нижней полосы.

Затем мы протестировали их и по итогам результатов каждой стратегии определили важные значения для XAUUSD, EURUSD и GBPUSD.

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

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

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

Прикрепленные файлы |
Введение в MQL5 (Часть 3): Изучаем основные элементы MQL5 Введение в MQL5 (Часть 3): Изучаем основные элементы MQL5
В этой статье мы продолжаем изучать основы программирования на MQL5. Мы рассмотрим массивы, пользовательские функции, препроцессоры и обработку событий. Для наглядности каждый шаг всех объяснений будет сопровождаться кодом. Эта серия статей закладывает основу для изучения MQL5, уделяя особое внимание объяснению каждой строки кода.
Нейросети — это просто (Часть 93): Адаптивное прогнозирование в частотной и временной областях (Окончание) Нейросети — это просто (Часть 93): Адаптивное прогнозирование в частотной и временной областях (Окончание)
В данной статье мы продолжаем реализацию подходов ATFNet — модели, которая адаптивно объединяет результаты 2 блоков (частотного и временного) прогнозирования временных рядов
Наиболее известные модификации алгоритма искусственного кооперативного поиска (Artificial Cooperative Search, ACSm) Наиболее известные модификации алгоритма искусственного кооперативного поиска (Artificial Cooperative Search, ACSm)
В данной статье рассмотрим эволюцию алгоритма ACS: три модификации в направлении улучшения характеристик сходимости и результативности алгоритма. Трансформация одного из ведущих алгоритмов оптимизации. От модификаций матриц до революционных подходов к формированию популяций.
Алгоритм искусственного кооперативного поиска (Artificial Cooperative Search, ACS) Алгоритм искусственного кооперативного поиска (Artificial Cooperative Search, ACS)
Представляем вам алгоритм Artificial Cooperative Search (ACS). Этот инновационный метод использует бинарную матрицу и несколько динамичных популяций, основанных на мутуалистических отношениях и кооперации, для быстрого и точного нахождения оптимальных решений. Уникальный подход ACS к "хищникам" и "жертвам" позволяет добиваться отличных результатов в задачах численной оптимизации.