Фильтрация сигналов на основе статистических данных о корреляции цен

Михаил Тарачков | 4 марта, 2011


С этого все и началось

Идея, приведшая к написанию данной статьи, зародилась у меня спустя некоторое время после прочтения книги Ларри Вильямса "Долгосрочные секреты краткосрочной торговли", в которой рекордсмен мира по инвестициям (за 1987 год увеличил капитал на 11000%) начисто развеял мифы "...профессоров колледжей и прочих академиков, богатых на теории и бедных на знание рынка..." об отсутствии всяческой корреляции между поведением цены в прошлом и будущими трендами.

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

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

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

Вот код:

//+------------------------------------------------------------------+
//|                                                     explorer.mq5 |
//|                        Copyright 2011, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//---переменные---
double profit_percent,open_cur,close_cur;
double profit_trades=0,loss_trades=0,day_cur,hour_cur,min_cur,count;
double open[],close[];
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
/* расcчитываем процент закрытий с повышением от общего числа */
   profit_percent=NormalizeDouble(profit_trades*100/(profit_trades+loss_trades),2);
   Print("Процент закрытий с повышением ",profit_percent,"%");   // выводим данные в журнал
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---узнаем время---
   MqlDateTime time;                        // создаем структуру для хранения времени
   TimeToStruct(TimeCurrent(),time);         // структурируем данные
   day_cur=time.day_of_week;              // получаем значение текущего дня
   hour_cur=time.hour;                    // получаем текущий час
   min_cur=time.min;                      // получаем текущую минуту
//---узнаем цены---
   CopyOpen(NULL,0,0,4,open);ArraySetAsSeries(open,true);
   CopyClose(NULL,0,0,4,close);ArraySetAsSeries(close,true);

   if(close[1]<open[1]/*&&close[2]<open[2]&&close[3]<open[3]*/ && count==0) // если состоялось закрытие с понижением
     {
      open_cur=open[0];                   // запоминаем цену открытия тек. бара
      count=1;
     }
   if(open_cur!=open[0] && count==1)      // текущий бар сформировался
     {
      close_cur=close[1];                 // запоминаем цену закрытия сформированного бара
      count=0;
      if(close_cur>=open_cur)profit_trades+=1;  // если цена закрытия больше цены открытия,
      else loss_trades+=1;                      // +1 к закрытиям с повышением, иначе +1 к закрытиям с понижением
     }
  }
//+------------------------------------------------------------------+

Тест будем проводить по паре EUR/USD на интервале с 1-го января 2000 года по 31-е декабря 2010 года:

Рисунок 1. Процент закрытий с повышением.

Рисунок 1. Процент закрытий с повышением
(Первый столбик - всего за период, второй, третий, четвертый - после однократного, двукратного и трехкратного закрытия вниз)

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


Еще один шаг вперед 

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

Итак, что нам нужно:

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

Из книги Л. Вильямса я почерпнул массу полезных идей. Одной из них и поделюсь сейчас с вами.

Стратегия торгового дня недели - TDW (Trade Day Of Week). Она позволит нам увидеть, что произойдет, если в одни из дней недели мы будем только покупать, а в другие - только открывать шорты. Ведь можно предположить, что цена внутри одного дня растет в большем проценте случаев, нежели внутри другого. Что виной тому? Геополитическая ситуация, макроэкономическая статистика или, как написал в своей книге А. Элдер, понедельники и вторники - дни дилетантов, а четверги и пятницы - время действия профессионалов? Разберемся!

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

Система основана на двух "машках" и MACDake. Сигналы:                                                            

  1. Если быстрый мувинг пересекает медленный снизу вверх и гистограмма MACD ниже нулевой линии - BUY.
  2. Если быстрая скользящая средняя пересекает медленную сверху вниз и MACD выше нуля - SELL

Выход по скользящему стопу от одного пункта. Лот фиксированный - 0,1. 

 Для удобства класс советника я разместил в отдельном заголовочном файле:

//+------------------------------------------------------------------+
//|                                                       moving.mqh |
//|                        Copyright 2011, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
//+------------------------------------------------------------------+
//| Класс my_expert                                                  |
//+------------------------------------------------------------------+
class my_expert
  {                                                  // создаем класс
   // закрытые члена класса
private:
   int               ma_red_per,ma_yel_per;          // периоды мувингов
   int               ma_red_han,ma_yel_han,macd_han; // хэндлы
   double            sl,ts;                          // стоп-приказы
   double            lots;                           // лот
   double            MA_RED[],MA_YEL[],MACD[];       // массивы для значений индикаторов
   MqlTradeRequest   request;                         // структура торгового запроса
   MqlTradeResult    result;                          // структура ответа сервера
                                                    // открытые члены класса   
public:
   void              ma_expert();                                   // конструктор
   void get_lot(double lot){lots=lot;}                               // получаем лот  
   void get_periods(int red,int yel){ma_red_per=red;ma_yel_per=yel;} // получаем периоды мувингов
   void get_stops(double SL,double TS){sl=SL;ts=TS;}                  // получаем значения стопов
   void              init();                                         // получение значений индикаторов
   bool              check_for_buy();                                // проверка для buy
   bool              check_for_sell();                               // проверка для sell
   void              open_buy();                                     // открытие buy
   void              open_sell();                                    // открытие sell
   void              position_modify();                              // модификация позиций
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
/* определение функций */
//---конструктор---
void my_expert::ma_expert(void)
  {
//--- обнуляем значения переменных
   ZeroMemory(ma_red_han);
   ZeroMemory(ma_yel_han);
   ZeroMemory(macd_han);
  }
//---функция получения значений индикаторов---
void  my_expert::init(void)
  {
   ma_red_han=iMA(_Symbol,_Period,ma_red_per,0,MODE_EMA,PRICE_CLOSE); // хэндл медленной машки
   ma_yel_han=iMA(_Symbol,_Period,ma_yel_per,0,MODE_EMA,PRICE_CLOSE); // хэндл быстрой машки
   macd_han=iMACD(_Symbol,_Period,12,26,9,PRICE_CLOSE);               // хэндл MACDaka
//---копируем данные в массивы и устанавливаем индексацию как в тайм-сессии---
   CopyBuffer(ma_red_han,0,0,4,MA_RED);
   CopyBuffer(ma_yel_han,0,0,4,MA_YEL);
   CopyBuffer(macd_han,0,0,2,MACD);
   ArraySetAsSeries(MA_RED,true);
   ArraySetAsSeries(MA_YEL,true);
   ArraySetAsSeries(MACD,true);
  }
//---функция проверки условий открытия buy---   
bool my_expert::check_for_buy(void)
  {
   init();  //получаем значения индикаторных буферов
/* если быстрый мувинг пересек медленный снизу вверх в промежутке между 3-им и 2-ым баром, 
   и не было обратного пересечения. MACD-hist меньше нуля */
   if(MA_RED[3]>MA_YEL[3] && MA_RED[1]<MA_YEL[1] && MA_RED[0]<MA_YEL[0] && MACD[1]<0)
     {
      return(true);
     }
   return(false);
  }
//---функция проверки условий открытия sell---
bool my_expert::check_for_sell(void)
  {
   init();  //получаем значения индикаторных буферов
/* если быстрый мувинг пересек медленный сверху вниз в промежутке между 3-им и 2-ым баром,
   и не было обратного пересечения. MACD-hist больше нуля */
   if(MA_RED[3]<MA_YEL[3] && MA_RED[1]>MA_YEL[1] && MA_RED[0]>MA_YEL[0] && MACD[1]>0)
     {
      return(true);
     }
   return(false);
  }
//---открытие buy---
/* формируем стандартный торговый запрос на покупку */
void my_expert::open_buy(void)
  {
   request.action=TRADE_ACTION_DEAL;
   request.symbol=_Symbol;
   request.volume=lots;
   request.price=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   request.sl=request.price-sl*_Point;
   request.tp=0;
   request.deviation=10;
   request.type=ORDER_TYPE_BUY;
   request.type_filling=ORDER_FILLING_FOK;
   OrderSend(request,result);
   return;
  }
//---открытие sell---
/* формируем стандартный торговый запрос на продажу */
void my_expert::open_sell(void)
  {
   request.action=TRADE_ACTION_DEAL;
   request.symbol=_Symbol;
   request.volume=lots;
   request.price=SymbolInfoDouble(Symbol(),SYMBOL_BID);
   request.sl=request.price+sl*_Point;
   request.tp=0;
   request.deviation=10;
   request.type=ORDER_TYPE_SELL;
   request.type_filling=ORDER_FILLING_FOK;
   OrderSend(request,result);
   return;
  }
//---модификация позиций---
void my_expert::position_modify(void)
  {
   if(PositionGetSymbol(0)==_Symbol)
     {     //если позиция по нашему символу
      request.action=TRADE_ACTION_SLTP;
      request.symbol=_Symbol;
      request.deviation=10;
      //---если позиция на покупку---
      if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
        {
/* если расстояние от цены до стоп-лосса больше трейлинг-стопа
   и новый стоп-лосс не меньше предыдущего */
         if(SymbolInfoDouble(Symbol(),SYMBOL_BID)-PositionGetDouble(POSITION_SL)>_Point*ts)
           {
            if(PositionGetDouble(POSITION_SL)<SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*ts)
              {
               request.sl=SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*ts;
               request.tp=PositionGetDouble(POSITION_TP);
               OrderSend(request,result);
              }
           }
        }
      //---если на продажу---                
      else if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
        {
/* если расстояние от стоп-лосса до цены оказывается больше величины трейлинг-стопа
   и новый стоп-лосс не выше старого. Или стоп-лосс с момента открытия равен нулю */
         if((PositionGetDouble(POSITION_SL)-SymbolInfoDouble(Symbol(),SYMBOL_ASK))>(_Point*ts))
           {
            if((PositionGetDouble(POSITION_SL)>(SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*ts)) || 
               (PositionGetDouble(POSITION_SL)==0))
              {
               request.sl=SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*ts;
               request.tp=PositionGetDouble(POSITION_TP);
               OrderSend(request,result);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------

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

Файл с классом прикручиваем к основному коду эксперта, создаем объект, инициализируем функции:

//+------------------------------------------------------------------+
//|                                                       Moving.mq5 |
//|                        Copyright 2011, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//---включаем файл с классом---
#include <moving.mqh>
//---внешние переменные---
input int MA_RED_PERIOD=7; // период медленного мувинга
input int MA_YEL_PERIOD=2; // период быстрого мувинга
input int STOP_LOSS=800;   // лось
input int TRAL_STOP=800;   // трал
input double LOTS=0.1;     // лот
//---создаем объект---
my_expert expert;
//---инициализируем структуру MqlDataTime---
MqlDateTime time;
int day_of_week;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---инициализируем советник
   expert.get_periods(MA_RED_PERIOD,MA_YEL_PERIOD);   // устанавливаем периоды мувингов
   expert.get_lot(LOTS);                              // устанавливаем лот
   expert.get_stops(STOP_LOSS,TRAL_STOP);             // устанавливаем стоп-приказы  
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   TimeToStruct(TimeCurrent(),time);
   day_of_week=time.day_of_week;
   if(PositionsTotal()<1)
     {
      if(day_of_week==5 && expert.check_for_buy()==true){expert.open_buy();}
      else if(day_of_week==1 && expert.check_for_sell()==true){expert.open_sell();}
     }
   else expert.position_modify();
  }
//+------------------------------------------------------------------+

Готово! Хочу отметить некоторые особенности. Для идентификации дней недели на программном уровне я использовал структуру MqlDateTime. Сначала преобразуем текущее время сервера в структурный формат. Получаем индекс текущего дня (1-Понедельник, ..., 5-Пятница) и сравниваем его с установленным нами значением.

Тестируем! Чтобы не обременять вас нудными изысканиями и лишними цифрами, я сведу все полученные результаты в таблицы.

Получилось вот так: 

Таблица 1. Покупка в каждый из дней недели. Сводная информация.

Таблица 1. Сводная информация о покупках в каждый из дней недели

Таблица 2. Продажи в каждый из дней недели. Сводная информация.

Таблица 2. Сводная информация о продажах в каждый из дней недели

Лучшие результаты выделены зеленым цветом, худшие - оранжевым.

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

Очевидно, эффективнее всего система покупала в пятницу, продавала в понедельник. Совместим оба этих условия:

if(PositionsTotal()<1){
      if(day_of_week==5&&expert.check_for_buy()==true){expert.open_buy();}
      else if(day_of_week==1&&expert.check_for_sell()==true){expert.open_sell();}}
   else expert.position_modify();

Теперь эксперт открывает позиции в обоих направлениях, но в строго определенные для этого дни. Для наглядности я нарисую графики, полученные до и после того, как мы прикрутили фильтр к советнику:

                    Рисунок 2. Результаты тестирования советника без использования фильтра (EURUSD, H1, 01.01.2010-31.12.2010, )

Рисунок 2. Результаты тестирования советника без использования фильтра (EURUSD, H1, 01.01.2010-31.12.2010, )

Рисунок 3. Результаты тестирования советника с использованием фильтра (EURUSD, H1, 01.01.2010-31.12.2010)

Рисунок 3. Результаты тестирования советника с использованием фильтра ( (EURUSD, H1, 01.01.2010-31.12.2010)

Ну и как вам результат? С использованием фильтра торговая система стала более стабильной. Если до модификации эксперт в основном приращивал капитал в первой половине исследуемого периода, то после "подъем в гору" не прекращался на всем его протяжении.

Сравним отчеты:

Рисунок 2. Результаты тестирования до и после использования фильтра

Таблица 3. Результаты тестирования до и после использования фильтра

Единственный огорчающий фактор, с которым нельзя не считаться, это падение чистой прибыли почти на 1000 USD (26%). Зато мы сокращаем количество сделок почти в 3,5 раза, т.е. существенно уменьшаем, во-первых, потенциальную возможность совершить отрицательный трейд и, во-вторых, расходы на оплату спреда (218*2-62*2=312 USD и это только для EUR/USD). Процент прибыльных сделок подскакивает до 57% - это уже весомо. А прибыль на одну сделку увеличивается на 14% до 113 USD. Как бы сказал Л. Вильямс: "Это сумма, ради которой стоит торговать!"


Заключение 

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

Не забывайте, что каким бы совершенным фильтр ни был, он отсеивает и прибыльные сделки - ваш профит... Удачи!