English Deutsch 日本語
preview
Как создать и оптимизировать торговую систему на основе циклов (Detrended Price Oscillator — DPO)

Как создать и оптимизировать торговую систему на основе циклов (Detrended Price Oscillator — DPO)

MetaTrader 5Торговые системы |
73 2
Mohamed Abdelmaaboud
Mohamed Abdelmaaboud

В этой статье мы рассмотрим новую концепцию, изучив технический индикатор и оценив его потенциальную полезность в торговых системах. Наше внимание будет сосредоточено на индикаторе Detrended Price Oscillator (DPO).

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

Мы рассмотрим индикатор Detrended Price Oscillator (DPO) в следующих разделах:

  1. Определение индикатора Detrended Price Oscillator (DPO): Узнав как можно больше об определении и методе расчета этого технического инструмента, мы получим четкое представление о нем.
  2. Пользовательский индикатор Detrended Price Oscillator (DPO): Процесс обучения кодированию пользовательского индикатора включает в себя модификацию индикатора или применение собственных предпочтений.
  3. Стратегии индикатора Detrended Price Oscillator (DPO): Мы рассмотрим простые стратегии с индикатором Detrended Price Oscillator (DPO), которые могут быть включены в нашу торговую систему, чтобы лучше понимать способы их применения.
  4. Торговая система с использованием индикатора Detrended Price Oscillator (DPO): Будет проведена разработка, бэктестинг и оптимизация этих простых торговых систем.
  5. Заключение

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



Определение индикатора Detrended Price Oscillator (DPO)

Индикатор Detrended Price Oscillator (DPO) это инструмент, который можно использовать для выявления ценовых циклов. Это делается путем отфильтровывания долгосрочных тенденций. Он фокусируется на краткосрочных ценовых циклах. С помощью этого индикатора можно определить состояние перекупленности или перепроданности, а также расположение точек разворота.

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

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

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

  • Выбрать период DPO (N).
  • Рассчитать SMA (простую скользящую среднюю) за период, определенный на шаге 1.

SMA = (Price1 + Price 2 + ....) / N

  • Сдвинуть SMA (простую скользящую среднюю) влево на половину периода плюс единицу.

Shift = (N/2)+1

  • Вычесть из цены смещенную SMA.

Сегодняшнее значение DPO = Сегодняшняя цена - Сегодняшнее значение SMA (сдвинутое назад на величину сдвига)



Пользовательский индикатор Detrended Price Oscillator (DPO)

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

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

Сначала мы укажем описание индикатора, используя препроцессорное свойство #property, а затем укажем значение идентификатора.

#property description "Custom Detrended Price Oscillator"

Используем препроцессор #include для включения MQH-файла со скользящими средними, указав имя файла так, как оно указано во включаемом файле. Это позволит использовать его позже в нашем коде.

#include <MovingAverages.mqh>

Укажем желаемое местоположение (indicator_separate_window) для отображения индикатора как часть настроек с помощью #property.

#property indicator_separate_window
Количество буферов для расчета индикатора, используя константу (indicator_buffers), мы устанавливаем ее равной 2.
#property indicator_buffers 2

Количество графических рядов в индикаторе, используя константу (indicator_plots), мы устанавливаем ее равной 1.

#property indicator_plots   1

Метка индикатора задается с помощью indicator_labelN, которая устанавливает строковое значение метки как "DPO".

#property indicator_label1 "DPO"

Тип индикатора задается с помощью параметра indicator_typeN для установки типа графического построения, который мы будем использовать как DRAW_HISTOGRAM

#property indicator_type1   DRAW_HISTOGRAM

Цвет отображаемого графика задается с помощью параметра indicator_colorN, который будет иметь значение clrRoyalBlue

#property indicator_color1  clrRoyalBlue

Ширина или толщина отображаемого графика определяется с помощью параметра indicator_widthN, который равен 2.

#property indicator_width1  2

Добавление нулевого уровня для отображения с помощью параметра indicator_levelN

#property indicator_level1 0

Используя функцию ввода, мы объявим целочисленную переменную для индикатора DPO, который пользователь сможет указать в соответствии со своими предпочтениями. Мы установим значение 20 в качестве значения по умолчанию.

input int detrendPeriodInp=20; // Period

Объявляем два массива типа double: dpoBuffer и maBuffer, а также еще один массив типа integer: maPeriod.

double    dpoBuffer[];
double    maBuffer[];
int       maPeriod;

В разделе OnInit() мы определим функцию инициализации для DPO. После загрузки индикатора на график эта функция будет запущена автоматически один раз.

void OnInit()
{

}

 Внутри функции OnInit() мы определим переменную maPeriod, которая будет задавать длину цикла, используемого для сглаживания в DPO.

maPeriod=detrendPeriodInp/2+1;

Сохраняем и отображаем значения DPO и MA в виде буферов.

   SetIndexBuffer(0,dpoBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,maBuffer,INDICATOR_CALCULATIONS);

Используем IndicatorSetInteger для установки количества десятичных знаков или цифр для DPO и его параметров:

  • prop_id: для определения целочисленного значения в качестве идентификатора свойства индикатора, которым является (INDICATOR_DIGITS).
  • prop_value: для определения значения свойства, которое равно (_Digits), чтобы установить количество цифр символа плюс 1.
IndicatorSetInteger(INDICATOR_DIGITS,_Digits+1);

Используем функцию PlotIndexSetInteger, чтобы задать параметры первого бара, с которого начнется отрисовка графика:

  • plot_index: определим индекс построения графика, который равен 0.
  • prop_id: определим идентификатор свойства, который равен (PLOT_DRAW_BEGIN).
  • prop_value: определим значение, которое нужно установить, оно будет равно (maPeriod-1).
PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,maPeriod-1);

Объявим строковую переменную (indShortName) для определения короткого имени индикатора, которое будет отображаться на индикаторе, используя StringFormat с двумя параметрами (имя индикатора) и (период индикатора, заданный пользователем).

string indShortName=StringFormat("Custom DPO(%d)",detrendPeriodInp);
Установим заданное имя с помощью функции IndicatorSetString с двумя параметрами (идентификатор свойств, который равен INDICATOR_SHORTNAME, и строковым значением свойства, которое является заданным именем).
IndicatorSetString(INDICATOR_SHORTNAME,indShortName);

Функция OnCalculate для вычисления индикатора

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {

  }
Внутри функции OnCalculate мы объявляем целочисленную переменную с именем start. Объявим и определим еще один объект с именем index1, равный (begin+maPeriod-1)
   int start;
   int index1=begin+maPeriod-1;

Обработка инициализации и установка точки начала вычислений, если первое вычисление имеет значение true, корректно:

  • Используем (ArrayInitialize) для инициализации массива нулем.
  • Обновляем значение переменной start, чтобы оно стало равным index1
  • Если значение параметра begin больше 0, установим значение соответствующего свойства соответствующей индикаторной строки, используя (PlotIndexSetInteger)

В противном случае, обновляем значение переменной start, чтобы оно стало равным (prev_calculated-1)

   if(prev_calculated<index1)
     {
      ArrayInitialize(dpoBuffer,0.0);
      start=index1;
      if(begin>0)
         PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,index1);
     }
   else
      start=prev_calculated-1;

Рассчитаем простую скользящую среднюю, используя определенную функцию (SimpleMAOnBuffer), которая находится в файле MovingAverages.mqh.

SimpleMAOnBuffer(rates_total,prev_calculated,begin,maPeriod,price,maBuffer);

Создадим цикл для вычислений, который будет перебирать каждый бар от start до rates_total, чтобы определить dpoBuffer[i] до тех пор, пока индикатор не будет удален или терминал не завершит работу

for(int i=start; i<rates_total && !IsStopped(); i++)
   dpoBuffer[i]=price[i]-maBuffer[i];

Возвращаем новое значение после выполнения OnCalculate

return(rates_total);

Это полный код пользовательского индикатора DPO, поскольку мы можем поместить его в один блок кода, как показано ниже

//+------------------------------------------------------------------+
//|                                                    customDPO.mq5 |
//+------------------------------------------------------------------+
#property description "Custom Detrended Price Oscillator"
#include <MovingAverages.mqh>
#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   1
#property indicator_label1 "DPO"
#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  clrRoyalBlue
#property indicator_width1  2
#property indicator_level1 0
input int detrendPeriodInp=20; // Period
double    dpoBuffer[];
double    maBuffer[];
int       maPeriod;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
void OnInit()
  {
   maPeriod=detrendPeriodInp/2+1;
   SetIndexBuffer(0,dpoBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,maBuffer,INDICATOR_CALCULATIONS);
   IndicatorSetInteger(INDICATOR_DIGITS,_Digits+1);
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,maPeriod-1);
   string indShortName=StringFormat("Custom DPO(%d)",detrendPeriodInp);
   IndicatorSetString(INDICATOR_SHORTNAME,indShortName);
  }
//+------------------------------------------------------------------+
//| Detrended Price Oscillator                                       |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   int start;
   int index1=begin+maPeriod-1;
   if(prev_calculated<index1)
     {
      ArrayInitialize(dpoBuffer,0.0);
      start=index1;
      if(begin>0)
         PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,index1);
     }
   else
      start=prev_calculated-1;
   SimpleMAOnBuffer(rates_total,prev_calculated,begin,maPeriod,price,maBuffer);
   for(int i=start; i<rates_total && !IsStopped(); i++)
      dpoBuffer[i]=price[i]-maBuffer[i];
   return(rates_total);
  }
//+------------------------------------------------------------------+

После компиляции кода без каких-либо ошибок, при вставке в график мы можем найти, что он такой же, как показано ниже:

DPOIndicator



Стратегии индикатора Detrended Price Oscillator (DPO)

В этом разделе мы изложим простые стратегии создания этой торговой системы с использованием DPO. Они такие же, как приведенные ниже:

  • Стратегия Пересечения нуля DPO (DPO Zero Crossover).
  • Стратегия Подтверждения тренда DPO (DPO Trend Validation).

Стратегия DPO Zero Crossover:

Эта стратегия размещает позиции на покупку и продажу на основе пересечения значения DPO и нулевого уровня. Позиция на покупку открывается, когда предыдущее значение DPO ниже 0, а текущее выше 0, а позиция на продажу открывается, когда предыдущее значение DPO выше 0, а текущее ниже нулевого уровня.

Имеем следующее:

Предыдущее значение DPO < 0 и текущее значение DPO > 0 --> Покупка

Предыдущее значение DPO > 0 и текущее значение DPO < 0 --> Продажа

Стратегия DPO Trend Validation:

Эта стратегия размещает позиции на покупку и продажу на основе пересечения скользящей средней и пересечения значения DPO и нулевого уровня для подтверждения пересечения нуля DPO, особенно с краткосрочным сигналом в целях оптимизации. Таким образом, позиция на покупку будет размещена, когда цена закрытия меньше предыдущего значения скользящей средней, цена ask текущего бара больше текущего значения скользящей средней, а значение DPO выше 0. В то же время позиция на продажу будет открываться, когда цена закрытия превышает предыдущее значение скользящей средней, цена bid текущего бара меньше текущего значения скользящей средней, а значение DPO ниже 0.

Имеем следующее:

Закрытие < Предыдущее значение MA и цена Ask > Значение MA и значение DPO > 0 --> Покупка
Закрытие > Предыдущее значение MA и цена Bid < Значение MA и значение DPO < 0 --> Продажа

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



Торговая система Detrended Price Oscillator (DPO)

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

Далее мы настроим входные параметры советника (EA), чтобы указать нужный период, который будет использоваться при вызове индикатора.

input int                 period=20; // Period

Объявляем целочисленную переменную для dpo

int dpo;

В событии OnInit() мы инициализируем наш пользовательский индикатор DPO, вызвав функцию iCustom. Его параметры указаны ниже:

  • Символ: Имя символа в виде строки. Здесь мы используем _Symbol, чтобы применить его к текущему символу графика.
  • Период: Таймфрейм как ENUM_TIMEFRAMES. Мы используем PERIOD_CURRENT, чтобы применить его к текущему таймфрейму графика.
  • Имя: Имя пользовательского индикатора, включая правильный путь на локальном компьютере (записанный как папка/custom_indicator_name), в нашем случае имя индикатора (customDPO).
  • Входные параметры: Список входных параметров индикатора, если применимо. Здесь это (период).

Затем мы используем возвращаемое значение INIT_SUCCEEDED, чтобы подтвердить успешную инициализацию и перейти к следующей части кода.

int OnInit()
  {
   dpo = iCustom(_Symbol,PERIOD_CURRENT,"customDPO",period);
   return(INIT_SUCCEEDED);
  }

В событии OnDeinit() мы укажем, что сообщение советника удаляется при возникновении события деинициализации.

void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }

В событии OnTick(), которое вызывается в советниках при возникновении нового тикового события

void OnTick()
  {

  }

Мы объявим массив для dpoInd[] как тип данных double

double dpoInd[];

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

  • indicator_handle: Хэндл индикатора DPO.
  • buffer_num: Номер буфера, из которого осуществляется копирование; 0 для индикатора DPO.
  • start_pos: Начальная позиция, установленная здесь на 0.
  • count: Количество значений для копирования; в данном случае 3.
  • buffer[]: Целевой массив, в котором будут храниться скопированные значения (здесь dpoInd[]).
CopyBuffer(dpo,0,0,3,dpoInd);

Установим флаг AS_SERIES для массива dpoInd[ ], чтобы индексировать его элементы как временной ряд. Используем имя массива и значение флага (true) в качестве параметров; функция вернет true в случае успеха.

ArraySetAsSeries(dpoInd,true);

Объявим и определим переменную double для dpoVal, чтобы вернуть текущий индекс dpo и нормализовать его до 3 цифр.

double dpoVal = NormalizeDouble(dpoInd[0], 3);

Прокомментируем значение текущего DPO на графике

comment("DPO value = ",dpoVal);

Полный код в одном блоке может выглядеть так

//+------------------------------------------------------------------+
//|                                                      DPO_Val.mq5 |
//+------------------------------------------------------------------+
input int                 period=20; // Period
int dpo;
int OnInit()
  {
   dpo = iCustom(_Symbol,PERIOD_CURRENT,"customDPO",period);
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }
void OnTick()
  {
    double dpoInd[];
    CopyBuffer(dpo,0,0,3,dpoInd);
    ArraySetAsSeries(dpoInd,true);
    double dpoVal = NormalizeDouble(dpoInd[0], 3);
    Comment("DPO value = ",dpoVal);
  }

После компиляции без ошибок и прикрепления советника мы видим значение DPO в виде динамического комментария на графике, как показано ниже:

DPOVal

Как мы видим, значение DPO на графике такое же, как и значение, показанное на вставленном индикаторе (1794.363). Мы можем использовать это в качестве основы для других советников стратегий.

Стратегия DPO Zero Crossover:

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

Ниже приведен полный код программы этого типа, которую можно запустить на MetaTrader 5:

//+------------------------------------------------------------------+
//|                                           DPO_Zero_Crossover.mq5 |
//+------------------------------------------------------------------+
#include <trade/trade.mqh>
input int                 period=20; // Periods
input double      lotSize=1;
input double      slLvl=300;
input double      tpLvl=900;
int dpo;
CTrade trade;
int barsTotal;
int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   dpo = iCustom(_Symbol,PERIOD_CURRENT,"customDPO",period);
   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 dpoInd[];
      CopyBuffer(dpo,0,0,3,dpoInd);
      ArraySetAsSeries(dpoInd,true);
      double dpoVal = NormalizeDouble(dpoInd[1], 6);
      double dpoPreVal = NormalizeDouble(dpoInd[2], 6);
      double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
      if(dpoPreVal<0 && dpoVal>0)
        {
         double slVal=ask - slLvl*_Point;
         double tpVal=ask + tpLvl*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }
      if(dpoPreVal>0 && dpoVal<0)
        {
         double slVal=bid + slLvl*_Point;
         double tpVal=bid - tpLvl*_Point;
         trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
        }
     }
  }
//+------------------------------------------------------------------+

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

Включим файл trade.mqh для размещения ордеров

#include <trade/trade.mqh>

Добавим дополнительные входные параметры со значениями по умолчанию для размера лота, уровня стоп-лосса и уровня тейк-профита, которые будут указаны пользователем.

input int         period=20; // Periods
input double      lotSize=1;
input double      slLvl=300;
input double      tpLvl=900;

Объявим используемый торговый объект и целочисленную переменную barsTotal.

CTrade trade;
int barsTotal;

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

barsTotal=iBars(_Symbol,PERIOD_CURRENT);

В OnTick() мы объявим бары как целочисленную переменную и определим ее с помощью функции iBars.

int bars=iBars(_Symbol,PERIOD_CURRENT);

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

if(barsTotal != bars)

В новом баре обновим значение barsTotal на значение баров.

barsTotal=bars;

Обновим последнее закрытое значение с индексом 1 и объявим дополнительную переменную типа double для предыдущего значения DPO с индексом 2.

double dpoVal = NormalizeDouble(dpoInd[1], 6);
double dpoPreVal = NormalizeDouble(dpoInd[2], 6);

Объявим и определим цены ask и bid.

double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);

Состояние стратегии, объявление и определение стоп-лосса, тейк-профита и размещение ордеров.

if(dpoPreVal<0 && dpoVal>0)
  {
   double slVal=ask - slLvl*_Point;
   double tpVal=ask + tpLvl*_Point;
   trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
  }
if(dpoPreVal>0 && dpoVal<0)
   {
    double slVal=bid + slLvl*_Point;
    double tpVal=bid - tpLvl*_Point;
    trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
   }

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

Позиция на покупку:

DPO_Crossover_Buy

Позиция на продажу:

DPO_Crossover_Sell

Теперь нам нужно протестировать все стратегии по паре EUR/USD на таймфреймах 5 минут и 15 минут в период с 1 января по 31 декабря 2023 года. Мы будем использовать значение стоп-лосса в размере 300 пунктов и тейк-профита в размере 900 пунктов. Основываясь на результатах, наш основной подход к оптимизации будет заключаться в тестировании дополнительных концепций или инструментов, таких как интеграция скользящей средней. Также стоит протестировать различные таймфреймы, чтобы определить, какой из них дает наилучший результат.

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

  • Чистая прибыль: Она рассчитывается путем вычитания валового убытка из валовой прибыли, а наибольшее значение является наилучшим.
  • Относительная просадка по балансу: Это максимальный убыток, который несет счет во время сделок, а самый низкий показатель является лучшим.
  • Коэффициент прибыльности: Это отношение валовой прибыли к валовому убытку. Самый высокий коэффициент является наилучшим.
  • Ожидаемая прибыль на сделку: Это средняя прибыль или убыток сделки. Самое большое значение является наилучшим.
  • Фактор восстановления: Измеряет насколько хорошо протестированная стратегия восстанавливается после потерь. Самый высокий коэффициент является наилучшим.
  • Коэффициент Шарпа: Определяет риск и стабильность протестированной торговой системы, сравнивая доходность с безрисковой доходностью. Самый высокий коэффициент Шарпа является наилучшим.

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

Results_0CO_15m-700

Results2_0CO_15m-700

Results3_0CO_15m-700

По результатам тестирования на 15-минутном таймфрейме получены следующие ключевые показатели:

  • Чистая прибыль: 91520.53 USD.
  • Относительная просадка по балансу: 35.81%.
  • Коэффициент прибыльности: 1.09.
  • Ожидаемая прибыль на сделку: 21.17.
  • Фактор восстановления: 1.48.
  • Коэффициент Шарпа: 1.07.

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

Results_0CO_5m-700

Results2_0CO_5m-700

Results3_0CO_5m-700

По результатам тестирования на 5-минутном таймфрейме получены следующие ключевые показатели для этого теста:

  • Чистая прибыль: 62258.14.
  • Относительная просадка по балансу: 97.34%.
  • Коэффициент прибыльности: 1.02.
  • Ожидаемая прибыль на сделку: 4.78.
  • Фактор восстановления: 0.30.
  • Коэффициент Шарпа: 0.17.

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

Стратегия DPO Trend Validation:

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

Размещение ордера на покупку, когда:

  • Предыдущая цена закрытия ниже предыдущего значения скользящей средней.
  • Цена ask выше текущего значения скользящей средней
  • Текущее значение DPO больше нуля. 

Размещение ордера на продажу, когда:

  • Предыдущая цена закрытия выше предыдущего значения скользящей средней.
  • Цена bid ниже текущего значения скользящей средней
  • Текущее значение DPO меньше нуля. 

Ниже приведен полный код программы этого типа, которую можно запустить на MT5:

//+------------------------------------------------------------------+
//|                                          DPO_trendValidation.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#include <trade/trade.mqh>
input int         period=20; // Periods
input int         maPeriodInp=20; //MA Period
input double      lotSize=1;
input double      slLvl=300;
input double      tpLvl=900;
int dpo;
int ma;
CTrade trade;
int barsTotal;
int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   dpo = iCustom(_Symbol,PERIOD_CURRENT,"customDPO",period);
   ma = iMA(_Symbol,PERIOD_CURRENT,maPeriodInp,0,MODE_SMA,PRICE_CLOSE);
   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 dpoInd[];
      double maInd[];
      CopyBuffer(dpo,0,0,3,dpoInd);
      CopyBuffer(ma,0,0,3,maInd);
      ArraySetAsSeries(dpoInd,true);
      ArraySetAsSeries(maInd,true);
      double dpoVal = NormalizeDouble(dpoInd[0], 6);
      double maVal= NormalizeDouble(maInd[0],5);
      double dpoPreVal = NormalizeDouble(dpoInd[1], 5);
      double maPreVal = NormalizeDouble(maInd[1],5);;
      double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
      double prevClose = iClose(_Symbol,PERIOD_CURRENT,1);
      if(prevClose<maPreVal && ask>maVal)
        {
         if(dpoVal>0)
           {
            double slVal=ask - slLvl*_Point;
            double tpVal=ask + tpLvl*_Point;
            trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
           }
        }
      if(prevClose>maPreVal && bid<maVal)
        {
         if(dpoVal<0)
           {
            double slVal=bid + slLvl*_Point;
            double tpVal=bid - tpLvl*_Point;
            trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
           }
        }
     }
  }
//+------------------------------------------------------------------+

Различия в приведенном выше коде такие же, как и в следующем.

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

input int         maPeriodInp=20; //MA Period

Объявляем целочисленную переменную для ma.

int ma;

Определение переменной ma с использованием функции iMA для возврата хэндла индикатора скользящей средней.

ma = iMA(_Symbol,PERIOD_CURRENT,maPeriodInp,0,MODE_SMA,PRICE_CLOSE);

Объявим массив для maInd[] как тип данных double.

double maInd[];

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

CopyBuffer(ma,0,0,3,maInd);

Установим флаг AS_SERIES для массива maInd[ ], чтобы индексировать его элементы как временной ряд.

ArraySetAsSeries(maInd,true);

Объявим и определим maVal. Нормализуем его.

double maVal= NormalizeDouble(maInd[0],5);

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

double prevClose = iClose(_Symbol,PERIOD_CURRENT,1);

Условия стратегии в случае покупки и продажи.

 if(prevClose<maPreVal && ask>maVal)
   {
    if(dpoVal>0)
      {
       double slVal=ask - slLvl*_Point;
       double tpVal=ask + tpLvl*_Point;
       trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
      }
   }
if(prevClose>maPreVal && bid<maVal)
   {
    if(dpoVal<0)
      {
       double slVal=bid + slLvl*_Point;
       double tpVal=bid - tpLvl*_Point;
       trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
       }
   }

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

Позиция на покупку:

DPO_TV_Buy

Позиция на продажу:

DPO_TV_Sell

Нам нужно протестировать эту стратегию на паре EURUSD в течение того же периода всего 2023 года, как упоминалось выше, на 15-минутных и 5-минутных таймфреймах. Результаты тестирования стратегии DPO trend validation на 15-минутном таймфрейме будут такими, как показано ниже:

Results_TV_15m-700

Results2_TV_15m-700

Results3_TV_15m-700

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

  • Чистая прибыль: 21866.08.
  • Относительная просадка по балансу: 16.22%.
  • Коэффициент прибыльности: 1.19.
  • Ожидаемая прибыль на сделку: 42.79.
  • Фактор восстановления: 1.24.
  • Коэффициент Шарпа: 1.42.

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

Results_TV_5m-700

Results2_TV_5m-700

Results3_TV_5m-700

В зависимости от приведенных выше важных показателей результаты теста на 15-минутном таймфрейме будут такими, как показано ниже:

  • Чистая прибыль: 87010.62.
  • Относительная просадка по балансу: 30.24%.
  • Коэффициент прибыльности: 1.24.
  • Ожидаемая прибыль на сделку: 51.67.
  • Фактор восстановления: 2.08.
  • Коэффициент Шарпа: 1.36.

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

  • Самая высокая чистая прибыль: 91520.53. 15-минутный таймфрейм стратегии DPO Zero Crossover.
  • Самая низкая относительная просадка по балансу: 16.22% для 15-минутного таймфрейма стратегии DPO trend validation.
  • Самый высокий коэффициент прибыльности: 1.24 для 5-минутного таймфрейма стратегии DPO trend validation.
  • Самая высокая ожидаемая прибыль на сделку: 51.67 для 5-минутного таймфрейма стратегии DPO trend validation.
  • Самый высокий фактор восстановления: 2.08 для 5-минутного таймфрейма стратегии DPO trend validation.
  • Самый высокий коэффициент Шарпа: 1.42 для 15-минутного таймфрейма стратегии DPO trend validation.

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



Заключение

После прочтения этой статьи вы будете понимать, что такое технический индикатор Detrended price Oscillator (DPO), через понимание того, что он измеряет, как его можно рассчитать и как трейдеры могут его использовать. В дополнение к этому, вы поняли, как создать собственный индикатор, исходя из ваших предпочтений, и самое интересное - как создать торговую систему, основанную на двух простых стратегиях:

  • Стратегия DPO Zero Crossover
  • Стратегия DPO Trend Validation

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

Вы можете найти мои прикрепленные файлы с исходным кодом ниже:

Название файла Описание
customDPO О пользовательском индикаторе DPO
DPO_Val О советнике динамических значений DPO
DPO_Zero_Crossover О советнике стратегии DPO Zero Crossover
DPO_trendValidation О советнике стратегии DPO Trend Validation 

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

Прикрепленные файлы |
customDPO.mq5 (2.19 KB)
DPO_Val.mq5 (0.65 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Dan Dumbraveanu
Dan Dumbraveanu | 27 сент. 2025 в 18:05
Это очень интересно, но у меня есть некоторые трудности с соотнесением теории с кодом. Если я правильно понял, то для расчета SMA, которая будет использоваться для текущего бара, нужно сдвинуть рассматриваемый период на N/2 + 1 бар назад, а затем рассчитать SMA на N баров назад. Я всего лишь новичок, поэтому не претендую на хорошее понимание кода индикатора, но из того, что мне удалось расшифровать, мне кажется, что N используется только для задания имени, но не в расчетах, как период SMA, а вместо этого N/2 + 1 (maPeriod в коде) используется как период SMA, но не для выполнения какого-либо сдвига. Простите, если я все неправильно понял, но, пожалуйста, укажите мне, где я не прав, чтобы я мог понять это лучше.
Mark Anthony Graham
Mark Anthony Graham | 29 сент. 2025 в 04:06

Спасибо, сэр.

В принципе, мне нравится ваш индикатор.

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

Разработка инструментария для анализа Price Action (Часть 33): Инструмент на основе теории свечного диапазона Разработка инструментария для анализа Price Action (Часть 33): Инструмент на основе теории свечного диапазона
Улучшите свое понимание рынка с помощью набора инструментов Candle-Range Theory для MetaTrader 5 – полностью нативного решения на MQL5 на основе теории свечного диапазона, которое превращает необработанные ценовые бары в информацию о волатильности в реальном времени. Легковесная библиотека CRangePattern сопоставляет истинный диапазон каждой свечи с адаптивным ATR и классифицирует ее в момент закрытия; затем CRT Indicator отображает эти классификации на графике в виде четких цветовых прямоугольников и стрелок, которые сразу показывают зоны сжатия, резкие пробои и полное поглощение диапазона.
Разработка инструментария для анализа Price Action (Часть 32): Модуль распознавания свечных паттернов на Python (II) – Распознавание с помощью Ta-Lib Разработка инструментария для анализа Price Action (Часть 32): Модуль распознавания свечных паттернов на Python (II) – Распознавание с помощью Ta-Lib
В этой статье мы перешли от ручной реализации распознавания свечных паттернов на Python к использованию TA-Lib – библиотеки, распознающей более шестидесяти различных паттернов. Эти формации дают ценную информацию о возможных разворотах рынка и продолжении тренда. Читайте дальше, чтобы узнать больше.
Популяционные алгоритмы оптимизации: строим защиту от читеров Популяционные алгоритмы оптимизации: строим защиту от читеров
Проведён повторный прогон алгоритмов на обновлённых функциях и предложен метод быстрой проверки их «честности». Составной тест объединяет пять разных ландшафтов и исключает выигрыш за счёт геометрии отдельных задач, позволяя быстро оценить реальную поисковую способность алгоритма. Прилагается скрипт для предварительной валидации алгоритмов перед применением к оптимизации торговых стратегий.
Использование регрессии Ренко-баров с корректировкой ошибок Использование регрессии Ренко-баров с корректировкой ошибок
В статье показан регрессионный подход к прогнозированию Ренко-баров с помощью CatBoost: модель оценивает логарифмическую доходность следующего бара и неопределённость прогноза. Разобран каскад residual-моделей с OOF-валидацией через TimeSeriesSplit, shrinkage и общим early stopping, а также условная коррекция смещения. На EURUSD D1 получено снижение OOF-MAE и около 65% точности по направлению. Приведён рабочий скрипт для MetaTrader 5, формирующий сигнал, размер позиции, SL и TP в единицах кирпича.