Тестирование паттернов, возникающих при торговле корзинами валютных пар. Часть I

Andrei Novichkov | 17 июля, 2017

Введение

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

Инструменты для исследования

Для работы будем использовать индикатор "объединенный WPR", который мы разработали ранее. В предыдущей серии статей он использовался не раз, и именно на нем мы обнаружили большинство паттернов.

Чтобы немного сгладить график индикатора, увеличим период WPR с четырнадцати (14) до двадцати (20). Это позволит "выпрямить" график без потери качества отображения.

Исследования будем проводить на трех таймфреймах: D1, H4 и H1. Результаты на других периодах вы можете получить, используя способы, описанные в этой статье. 

Базовая терминология и основы методики изложены здесь.

Паттерн для исследования

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

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

Сразу же возникает вопрос: где находятся эти самые уровни перекупленности и перепроданности?  Для стандартного индикатора WPR на отдельной валютной паре ответ на этот вопрос известен:

Это дает нам отправную точку в исследованиях. Используя эти данные, попытаемся уточнить расположение уровней для объединенного WPR. Результаты послужат нам не только при проверке рассматриваемого паттерна, но и в других, аналогичных случаях. Пригодится и применяемая методика.

Чтобы график индикатора пробил один из уровней, перед пробоем он должен быть либо выше уровня перекупленности, либо ниже уровня перепроданности. Посмотрим на истории, какое количество потенциальных входов нам предоставлял рынок. На этом этапе не станем использовать советники, а воспользуемся ранее разработанными индикаторами testIndexZig-Zag1.mq5 и testWPReur.mq5. В testWPReur.mq5 мы просто будем подставлять данные по составу корзин. Ну а исходный код индикатора  testIndexZig-Zag1.mq5 мы слегка упростим, ведь мы уже знаем максимальное и минимальное значения индикатора (от 100% до -100%):

#property copyright "Copyright 2016, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 6
#property indicator_plots   3
//--- plot High
#property indicator_label1  "High"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrGreen
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot Low
#property indicator_label2  "Low"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrGreen
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- plot ZigZag
#property indicator_label3  "ZigZag"
#property indicator_type3   DRAW_SECTION
#property indicator_color3  clrRed
#property indicator_style3  STYLE_SOLID
#property indicator_width3  1
//--- plot Direction
#property indicator_label4  "Direction"
#property indicator_type4   DRAW_LINE
#property indicator_style4  STYLE_SOLID
#property indicator_width4  1
//--- plot LastHighBar
#property indicator_label5  "LastHighBar"
#property indicator_type5   DRAW_LINE
#property indicator_style5  STYLE_SOLID
#property indicator_width5  1
//--- plot LastLowBar
#property indicator_label6  "LastLowBar"
#property indicator_type6   DRAW_LINE
#property indicator_style6  STYLE_SOLID
#property indicator_width6  1

#include <ZigZag\CSorceData.mqh>
#include <ZigZag\CZZDirection.mqh>
#include <ZigZag\CZZDraw.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
enum EDirection
  {
   Dir_NBars=0,
   Dir_CCI=1
  };
//--- input parameters
input EDirection  DirSelect=Dir_NBars;
input int                  CCIPeriod   =  14;
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL;
input int                  ZZPeriod=14;

string               name;

CZZDirection*dir;
CZZDraw*zz;

//--- indicator buffers
double         HighBuffer[];
double         LowBuffer[];
double         ZigZagBuffer[];
double         DirectionBuffer[];
double         LastHighBarBuffer[];
double         LastLowBarBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int h;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   switch(DirSelect)
     {
      case Dir_NBars:
         dir=new CNBars(ZZPeriod);
         break;
      case Dir_CCI:
         dir=new CCCIDir(CCIPeriod,CCIPrice);
         break;
     }
   if(!dir.CheckHandle())
     {
      Alert("Ошибка загрузки индикатора 2");
      return(INIT_FAILED);
     }
   zz=new CSimpleDraw();
//--- indicator buffers mapping
   SetIndexBuffer(0,HighBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,LowBuffer,INDICATOR_DATA);
   SetIndexBuffer(2,ZigZagBuffer,INDICATOR_DATA);
   SetIndexBuffer(3,DirectionBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,LastHighBarBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,LastLowBarBuffer,INDICATOR_CALCULATIONS);
   name = _Symbol + TimeFrameToShortString(Period()) + ".txt";
   h=FileOpen(name,FILE_CSV|FILE_WRITE|FILE_ANSI,',');
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

   if(CheckPointer(dir)==POINTER_DYNAMIC)
     {
      delete(dir);
     }
   if(CheckPointer(zz)==POINTER_DYNAMIC)
     {
      delete(zz);
     }
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int ind=0;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[]
                )
  {
   int start;

   if(prev_calculated==0)
     {
      start=0;
     }
   else
     {
      start=prev_calculated-1;
     }

   for(int i=start;i<rates_total;i++)
     {
      HighBuffer[i]=price[i];
      LowBuffer[i]=price[i];
     }

   int rv;
   rv=dir.Calculate(rates_total,
                    prev_calculated,
                    HighBuffer,
                    LowBuffer,
                    DirectionBuffer);
   if(rv==0)return(0);
   zz.Calculate(rates_total,
                prev_calculated,
                HighBuffer,
                LowBuffer,
                DirectionBuffer,
                LastHighBarBuffer,
                LastLowBarBuffer,
                ZigZagBuffer);

   if(ind<= 10) ind++;
   if(ind == 10)
     {
      double mx=100,mn=-100;
      double lg;
      lg=mx-mn;
      lg/=100;
      double levels[100];
      int    count[100];
      ArrayInitialize(count,0);
      for(int i=1; i<101; i++) levels[i-1]=NormalizeDouble(lg*i + mn,_Digits);
      for(int i=0;i<rates_total;i++)
        {
         if(ZigZagBuffer[i]==0 || ZigZagBuffer[i]==EMPTY_VALUE) continue;
         else 
           {
            for(int j=0; j<100; j++) 
              {
               if(ZigZagBuffer[i]<levels[j]) 
                 {
                  count[j]++;
                  break;
                 }
              }
           }
        }
      for(int i=0; i<100; i++)
        {
         FileWrite(h,i,levels[i],count[i]);
        }
      FileClose(h);
      Print("Work complete: ",name);
     }
   return(rates_total);
  }
//+------------------------------------------------------------------+


string TimeFrameToShortString(ENUM_TIMEFRAMES period)
{
   switch (period )
   {
      case PERIOD_M1:  return ("M1");
      case PERIOD_M2:  return ("M2");
      case PERIOD_M3:  return ("M3");
      case PERIOD_M4:  return ("M4");
      case PERIOD_M5:  return ("M5");
      case PERIOD_M6:  return ("M6");
      case PERIOD_M10: return ("M10");
      case PERIOD_M12: return ("M12");
      case PERIOD_M15: return ("M15");
      case PERIOD_M20: return ("M20");
      case PERIOD_M30: return ("M30");
      case PERIOD_H1:  return ("H1");
      case PERIOD_H2:  return ("H2");
      case PERIOD_H3:  return ("H3");
      case PERIOD_H4:  return ("H4");
      case PERIOD_H6:  return ("H6");
      case PERIOD_H8:  return ("H8");
      case PERIOD_H12: return ("H12");
      case PERIOD_D1:  return ("D1");
      case PERIOD_W1:  return ("W1");
      case PERIOD_MN1: return ("MN1");
   }
   return ("");
} 

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

Возможное количество сделок

Диапазон объединенного WPR колеблется от -100% до +100%, поэтому пока будем считать, что уровень перекупленности находится на уровне +60%, а перепроданности — на уровне -60%, что соответствует стандартному значению. Выясним, сколько раз график индикатора выходил за уровни перекупленности / перепроданности. Для этого воспользуемся описанным  здесь способом:

Для удобства полученные данные сведены в таблицу по всем корзинам и по всем выбранным тайфреймам. Фрагмент этой таблицы, посвященный корзине по EUR, приведен ниже. Поясним смысл значений в строках и столбцах этой таблицы:



EUR ----
Num. Indicator Timeframe ----
D1 H4 H1 ----
0 -98 2 3 4 ----
1 -96 0 1 1 ----
2 -94 0 0 1 ----
3 -92 0 3 3 ----
4 -90 1 4 5 ----
5 -88 3 4 10 ----
6 -86 1 2 7 ----
7 -84 2 8 7 ----
8 -82 1 8 21 ----
9 -80 4 6 22 ----
---- ---- ---- ---- ---- ----
95 92 0 2 6 ----
96 94 0 1 4 ----
97 96 0 0 3 ----
98 98 0 3 0 ----
99 100 0 0 0 ----
History Depth 2000.11.09 2005.04.12 2006.01.17 ----





----
Trade count (80%) 25 83 165 ----
Trade count (70%) 45 207 449 ----












Trade total (80%) 3793


Trade total (70%) 7885


Таблица приложена к статье и находится в архиве Pair.zip.

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

Форма паттерна

Определим форму паттерна, после идентификации которой трейдер может войти в рынок.

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

Значит, дальше нам потребуется советник.

Советник для тестирования паттерна

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

Покажем еще раз, какие паттерны будем искать:

Искомый паттерн
Отсутствие паттерна



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

Анализировать начнем с корзины валют по USD, разместив следующий советник на паре EURUSD на ранее выбранных таймфреймах:

//+------------------------------------------------------------------+
//|                                                   testBasket.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//--- input parameters

#include <Trade\\Trade.mqh>


#define LG 7

input int SELLPROFIT =   0;
input int SELL1LIMIT =  70;
input int SELL2FROM  =  60;
input int SELL2TO    =  50;

input int BUYPROFIT  =   0;
input int BUY1LIMIT  = -70;
input int BUY2FROM   = -60;
input int BUY2TO     = -50;

input int WPR=20;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
enum BSTATE 
  {
   BCLOSE = 0,
   BBUY   = 1,
   BSELL  = 2
  };

string pair[]={"EURUSD","GBPUSD","AUDUSD","NZDUSD","USDCAD","USDCHF","USDJPY"};
bool bDirect[]={false,false,false,false,true,true,true};
datetime TimeParam[3];

double dWpr[3];
ulong  Ticket[LG];
double TradeResult[LG];
double TradeCurrency;
double Drw;
string sLog;

double TradeTotalResult[LG];
double TradeTotalCurrency;
int    iTradeCount;
double mDrw;

int h1[LG];
BSTATE bstate;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double GetValue1(int shift)
  {
   double dBuf[1];
   double res=0.0;
   for(int i=0; i<LG; i++)
     {
      CopyBuffer(h1[i],0,shift,1,dBuf);
      if(bDirect[i]==true)
         res+=dBuf[0];
      else
         res+=-(dBuf[0]+100);
     }//end for (int i = 0; i < iCount; i++)      
   res=res/LG;
   return (NormalizeDouble((res + 50) * 2, _Digits) );
  }

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int lh;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   EventSetTimer(1);

   for(int i=0; i<LG; i++)
     {
      h1[i]=iWPR(pair[i],0,WPR);
     }
   bstate=BCLOSE;

   ArrayInitialize(TradeTotalResult,0);
   ArrayInitialize(dWpr,EMPTY_VALUE);
   TradeTotalCurrency=0;
   iTradeCount=0;
   mDrw=1000000;

   lh=INVALID_HANDLE;
   string lname = _Symbol + "_" + TimeFrameToShortString(Period() );
   string t1, t = lname;
   int i=0;
   for(;;) 
     {
      t+=".html";
      long lg=FileFindFirst(t,t1);
      if(lg==INVALID_HANDLE) 
        {
         lh= FileOpen(t,FILE_WRITE | FILE_TXT | FILE_ANSI);
         Print("CREATE ",t);
         break;
        }
      FileFindClose(lg);
      t=lname+"_"+IntegerToString(i++);
     }

   FileWriteString(lh,"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\r\n");
   FileWriteString(lh,"<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n");
   FileWriteString(lh,"<head>\r\n");
   FileWriteString(lh,"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\r\n");
   FileWriteString(lh,"<title>"+lname+"</title>\r\n");
   FileWriteString(lh,"</head>\r\n<body>\r\n");
   FileWriteString(lh,"<H2>"+_Symbol+" "+TimeFrameToShortString(Period())+"</H2>\r\n");
   FileWriteString(lh,"<H3>Pattern Params:</H3>\r\n");
   FileWriteString(lh,"<table width=\"100%\" cellspacing=\"0\" cellpadding=\"5\">\r\n");
   FileWriteString(lh,"<thead>\r\n<tr>\r\n<th>BUY</th>\r\n<th>SELL</th>\r\n</tr>\r\n</thead>\r\n<tbody>\r\n<tr>\r\n");
   t=StringFormat("Point 1: %d Point 2 from: %d to: %d Close at: %d",BUY1LIMIT,BUY2FROM,BUY2TO,BUYPROFIT);
   FileWriteString(lh,"<td style=\"text-align:center;\">\r\n<ul>\r\n<li>"+t+"</li>\r\n</ul>\r\n</td>\r\n");
   t=StringFormat("Point 1: %d Point 2 from: %d to: %d Close at: %d",SELL1LIMIT,SELL2FROM,SELL2TO,SELLPROFIT);
   FileWriteString(lh,"<td style=\"text-align:center;\">\r\n<ul>\r\n<li>"+t+"</li>\r\n</ul>\r\n</td>\r\n");
   FileWriteString(lh,"</tr>\r\n</tbody>\r\n</table>\r\n");
   FileWriteString(lh,"<H2>"+"Tester Result"+"</H2>\r\n");
   FileWriteString(lh,"<table border=\"1\" width=\"100%\" cellspacing=\"0\" cellpadding=\"5\">\r\n");
   FileWriteString(lh,"<thead>\r\n<th>Num.</th>\r\n<th>Type</th>\r\n<th>WPR(P1/P2)</th>\r\n<th>Time(Begin/End/Length)</th>\r\n<th>Drawdown/<br/>Profit</th>\r\n<th>Pair Profit</th>\r\n</tr>\r\n</thead>\r\n<tbody>\r\n");

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void PushWpr(double wpr) 
  {
   dWpr[2] = dWpr[1]; dWpr[1] = dWpr[0];
   dWpr[0] = wpr;
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void Stat() 
  {

   double d=0;
   for(int i=0; i<LG; i++) 
     {
      PositionSelectByTicket(Ticket[i]);
      d+=PositionGetDouble(POSITION_PROFIT);
     }
   if(d<Drw) Drw=d;
   if(Drw<mDrw) 
     {
      mDrw=Drw;
      TimeParam[2]=TimeCurrent();
     }
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(bstate!=BCLOSE) 
     {
      Stat();
     }
   if(IsNewCandle()) 
     {
      double res=GetValue1(0);
      PushWpr(res);
      if(dWpr[1]!=EMPTY_VALUE) 
        {
         if(bstate==BBUY && (dWpr[0]>=BUYPROFIT )) 
           {
            CloseAllPos();
            bstate=BCLOSE;
           }
         if(bstate==BSELL && (dWpr[0]<=SELLPROFIT )) 
           {
            CloseAllPos();
            bstate=BCLOSE;
           }
         if(bstate==BCLOSE && dWpr[0]<=SELL2FROM && dWpr[0]>=SELL2TO && dWpr[1]>=SELL1LIMIT) 
           {
            EnterSell(0.01);
            bstate=BSELL;
            TimeParam[0]=TimeCurrent();
            TradeCurrency=0;
            Drw=1000000;
            iTradeCount++;
            sLog=StringFormat("<tr>\r\n<td>%d</td>\r\n<td>SELL</td>\r\n<td>%.2f/<br/>%.2f</td>\r\n<td>%s/<br/>",iTradeCount,dWpr[1],dWpr[0],TimeToString(TimeCurrent()));
            return;
           }
         if(bstate==BCLOSE && dWpr[0]>=BUY2FROM && dWpr[0]<=BUY2TO && dWpr[1]<=BUY1LIMIT) 
           {
            EnterBuy(0.01);
            bstate=BBUY;
            TimeParam[0]=TimeCurrent();
            TradeCurrency=0;
            Drw=1000000;
            iTradeCount++;
            sLog=StringFormat("<tr>\r\n<td>%d</td>\r\n<td>BUY</td>\r\n<td>%.2f/<br/>%.2f</td>\r\n<td>%s/<br/>",iTradeCount,dWpr[1],dWpr[0],TimeToString(TimeCurrent()));
            return;
           }
        }//if (stc.Pick(1) != EMPTY_VALUE)
     }
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CloseAllPos() 
  {

   CTrade Trade;
   Trade.LogLevel(LOG_LEVEL_NO);

   TimeParam[1]=TimeCurrent();
   string p="<td>";
   for(int i=0; i<LG; i++) 
     {
      TradeResult[i]=PositionGetDouble(POSITION_PROFIT)+PositionGetDouble(POSITION_SWAP);
      p+=StringFormat("%s = %.2f<br/>",pair[i],TradeResult[i]);
      TradeCurrency       += TradeResult[i];
      TradeTotalResult[i] += TradeResult[i];
      Trade.PositionClose(Ticket[i]);
     }
   p+="</td>\r\n";
   TradeTotalCurrency+=TradeCurrency;
   sLog += StringFormat("%s/<br/>%s</td>\r\n<td>%.2f/<br/>%.2f</td>\r\n",TimeToString(TimeParam[1]), TimeIntervalToStr(TimeParam[0], TimeParam[1]), Drw, TradeCurrency );
   sLog += p;
   FileWriteString(lh,sLog);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void EnterBuy(double lot) 
  {

   CTrade Trade;
   Trade.LogLevel(LOG_LEVEL_NO);

   for(int i=0; i<LG; i++) 
     {
      if(bDirect[i]) 
        { //send buy
         Trade.Buy(lot,pair[i]);
         Ticket[i]=Trade.ResultDeal();
        }
      else 
        { //send sell
         Trade.Sell(lot,pair[i]);
         Ticket[i]=Trade.ResultDeal();
        }
     }
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void EnterSell(double lot) 
  {

   CTrade Trade;
   Trade.LogLevel(LOG_LEVEL_NO);

   for(int i=0; i<LG; i++) 
     {
      if(bDirect[i]) 
        { //send sell
         Trade.Sell(lot,pair[i]);
         Ticket[i]=Trade.ResultDeal();
        }
      else 
        { //send buy
         Trade.Buy(lot,pair[i]);
         Ticket[i]=Trade.ResultDeal();
        }
     }
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- destroy timer
   EventKillTimer();

   FileWriteString(lh,"</tbody>\r\n</table>\r\n");
   FileWriteString(lh,"<H2>Total Result</H2>\r\n");
   FileWriteString(lh,"<table border=\"1\" width=\"100%\" cellspacing=\"0\" cellpadding=\"5\">\r\n");
   FileWriteString(lh,"<thead>\r\n<tr>\r\n<th>Deal's<br/>Count</th>\r\n<th>Profit</th>\r\n<th>Max.Drawdown</th>\r\n<th>Pair's Profit</th>\r\n</tr>\r\n</thead>\r\n<tbody>\r\n");
   string p = StringFormat("<tr><td>%d</td>\r\n<td>%.2f</td>\r\n<td>%.2f at<br/>%s</td>\r\n<td>",iTradeCount,TradeTotalCurrency,mDrw,TimeToString(TimeParam[2]));
   for(int i=0; i<LG; i++)
     {
      if(h1[i]!=INVALID_HANDLE) IndicatorRelease(h1[i]);
      p+=StringFormat("%s = %.2f<br/>",pair[i],TradeTotalResult[i]);
     }
   p+="</td>\r\n</tr>\r\n";
   FileWriteString(lh,p);
   FileWriteString(lh,"</tbody>\r\n</table>\r\n");
   FileWrite(lh,"</body>\r\n</html>"); //End log
   FileClose(lh);
  }
//+------------------------------------------------------------------+

bool IsNewCandle() 
  {

   static int candle=-1;

   int t1=0;
   switch(_Period)
     {
      case PERIOD_H1:  t1 = Hour();   break;
      case PERIOD_H4:  t1 = Hour4();  break;
      case PERIOD_D1:  t1 = Day();    break;
     }
   if(t1!=candle) {candle=t1; return(true);}
   return (false);
  }
int Hour4(){return((int)Hour()/4);}
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int Day()
  {
   MqlDateTime tm;
   TimeCurrent(tm);
   return(tm.day);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int Hour()
  {
   MqlDateTime tm;
   TimeCurrent(tm);
   return(tm.hour);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string TimeIntervalToStr(datetime dt1,datetime dt2) 
  {
   string tm;
   if(dt1 >= dt2)   tm = TimeToString(dt1 - dt2);
   else tm = TimeToString(dt2 - dt1,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
   string ta[],ta1[];
   StringSplit(tm,StringGetCharacter(" ",0),ta);
   StringSplit(ta[0],StringGetCharacter(".",0),ta1);
   ta1[0] = IntegerToString( StringToInteger(ta1[0]) - 1970);
   ta1[1] = IntegerToString( StringToInteger(ta1[1]) - 1);
   ta1[2] = IntegerToString( StringToInteger(ta1[2]) - 1);
   return (ta1[0] + "." + ta1[1] + "." + ta1[2] + " " + ta[1]);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string TimeFrameToShortString(ENUM_TIMEFRAMES period)
  {
   switch(period)
     {
      case PERIOD_H1:  return ("H1");
      case PERIOD_H4:  return ("H4");
      case PERIOD_D1:  return ("D1");
     }
   return ("");
  }
//+------------------------------------------------------------------+

Первую версию советника для тестирования можно найти и в прилагаемом файле testBasket.mq5. В самом алгоритме нет ничего необычного, зато много внимание уделено формам отчета. Проясним смысл входных параметров советника:

Далее тестируем советник в Тестере, начиная с января 2016г. Выбранная дата тестирования зависит от наличия качественной истории. Анализировать будем две формы паттерна. Первая описана выше и задана по умолчанию. Вторая смещена относительно первой таким образом:

Итог — отчеты в формате html.

Форма отчетов советника.

Разобраться в отчетах советника совсем не сложно, рассмотрим структуру отчетов на примере отчета по корзине EUR.

Первая строка — заголовок с названиями чата и таймфрейма, на котором работал советник.

Затем идут параметры паттерна, с которыми работал советник раздельно для корзин на покупку и на продажу: Point 1 — это положение точки №1: SELL1LIMIT (BUY1LIMIT). Point 2 from: ... to: ... —  положение точки №2: SELL2FROM (BUY2FROM) и SELL2TO (BUY2TO). Close at — положение точки закрытия паттерна SELLPROFIT (BUYPROFIT):

EURUSD H4

Pattern Params:

BUY SELL
  • Point 1: -80 Point 2 from: -70 to: -50 Close at: 0
  • Point 1: 80 Point 2 from: 70 to: 50 Close at: 0

Следующей идет таблица "Tester Result" с информацией по каждой сделке за период тестирования в следующем порядке:

Вот фрагмент этой таблицы. Можно видеть, что первая сделка длилась восемь часов и принесла убыток в 16.34 USD. В частности, ордер, открытый по EURUSD был закрыт с убытком 2.55 USD:

Num. Type WPR(P1/P2) Time(Begin/End/Length) Drawdown/
Profit
Pair Profit
1 SELL 86.26/
67.15
2016.03.23 20:00/
2016.03.24 04:00/
0.0.0 08:00:00
-21.70/
-16.34
EURUSD = -2.55
GBPUSD = -1.58
AUDUSD = -2.02
NZDUSD = -3.66
USDCAD = -2.15
USDCHF = -2.97
USDJPY = -1.41


Последней идет таблица "Total Result" с суммарными данными по периоду тестирования в следующем порядке:

Вот эта таблица непосредственно из отчета:

Deal's
Count
Profit Max.Drawdown Pair's Profit
22 189.06 -52.37 at
2016.05.02 19:53
EURUSD = 52.43
GBPUSD = 24.50
AUDUSD = 45.88
NZDUSD = 13.94
USDCAD = 15.73
USDCHF = 12.26
USDJPY = 24.32


Полученные отчеты находятся в прилагаемом архиве DocUSD.zip.

Обращает на себя внимание, что сделок на таймфрейме D1 неожиданно мало. Но есть и обнадеживающие сигналы:

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

  1. Паттерн крайне редко обнаруживается на дневном таймфрейме. Скорее всего, эта тенденция сохранится и на тайфреймах старше дневного.
  2. Уровень перекупленности / перепроданности находится в диапазоне 60% — 70%, если мы говорим о перекупленности, и -60% — -70%, если о перепроданности. Выше 70% и ниже -70% сделок явно мало. Чтобы паттерн мог быть идентифицирован в этом случае, точка №1 должна находиться выше 90%, либо ниже -90%, а это достаточно редкая ситуация. Ниже 60% и выше -60% точка №2 оказывается в районе 40% или -40% и приближается к зоне возможного начала бокового движения. Для этой зоны характерны еще более высокая волатильность в показаниях индикатора и, как следствие, множественные ложные сигналы на вход.

Немного доработаем советник и продолжим со следующей валютой корзины — NZD. Внесем изменения в форму отчета, а именно — выведем значение "положительной просадки". Поясним это понятие. Корзина закрывается не по конкретному значению профита, не по выставленным торговым уровням, а по показаниям индикатора. До момента закрытия, ордера, входящие в корзину ордеров, могут находиться в просадке, величину которой советник мониторит. Но эти же ордера могут находиться и в существенном профите, который не будет зафиксирован, т.к. индикатор еще не достиг нужных значений для закрытия. Эту величину мы назовем "положительной просадкой", а её максимальные значения выведем в отчет. Теперь мы будем знать, каким мог быть профит, насколько корзина "уходила в плюс".

Добавим это значение в предпоследнюю колонку таблицы "Tester Result". Та колонка, кторая называлась "Drawdown/Profit" теперь будет называться Drawdown/+Drawdown/Profit и данные в каждой ячейке этой колонки будут идти в следующем порядке: Просада / Положительная просадка / Профит. Все данные будут в валюте депозита.

Кроме того, покажем максимальное значение положительной просадки в таблице "Total Result". Введем дополнительную предпоследнюю колонку "Max.+Drawdown" и будем показывать в ней максимальную положительную просадку за весь период тестирования и когда эта просадка была зафиксирована.

Исходный код этой следующей версии советника — в прилагаемом файле testBasket1.mq5.

Полученные отчеты по корзине NZD находятся в архиве DocNZD.zip. Выводы пока следующие:

Завершим исследование, используя оставшиеся валюты корзин: AUD, EUR, GBP, CAD, JPY, CHF. Отчеты по этим валютам можно найти в прикрепленном архиве Doc.zip. А нам пора подводить итоги проделанной работы:

Итоги

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

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

Заключение

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

Программы, используемые в статье:

 # Имя
Тип
 Описание
1 Pair.zip Архив
Результаты подсчета количества возможных сделок по всем валютам корзины по трем выбранным таймфреймам.
2
testBasket.mq5 Советник
Советник для тестирования.
3
DocUSD.zip Архив Отчеты в формате html по работе советника testBasket.mq5 с корзиной по USD.
 4 DocNZD.zip Архив Отчеты в формате html по работе советника testBasket1.mq5 с корзиной по NZD.
5
testBasket1.mq5 Советник Советник для тестирования - следующая версия.
 6  Doc.zip Архив
Отчеты в формате html по работе советника testBasket1.mq5 по остальным корзинам.
7test.zip
 Архив Архив с индикаторами testIndexZig-Zag1.mq5 и testWPReur.mq5