Последний крестовый поход

Roman Zamozhnyy | 13 марта, 2012


Введение

В терминале МetaТrader 5, (впрочем, как и в МetaТrader 4) по умолчанию доступны три способа отображения котировки инструмента: бары, японские свечи, линия. Все они, по сути, являются примерно одним и тем же: временными графиками. Наряду с традиционным способом временного отображения котировок до сих пор живы и пользуются известным успехом инвесторов и спекулянтов способы отображения котировок, не зависящие от времени: графики ренко, каги, трехлинейного прорыва и крестики-нолики.

Не берусь утверждать их преимущество перед классикой, но некоторым торговцам изъятие из поля зрения одной переменной - времени, помогает сосредоточиться на второй переменной - цене. Предлагаю рассмотреть в статье графики крестики-нолики, алгоритм их построения, известные рыночные продукты, предлагающие эти графики и написать самим простенький скрипт, реализующий алгоритм. Нашим букварем будет книга Томаса Дж. Дорси "Метод графического анализа крестики-нолики" (оригинал "Point and Figure Charting: The Essential Application for Forecasting and Tracking Market Prices").

Самым известным продуктом для построения оффлайн-графиков является программа Bull's-Eye Broker. Релиз программы предоставляется на ознакомление сроком на 21 день (знакомится можно много раз), новая бета-версия - до вывода ее в релиз. По релизу мы будем сверять результаты работы нашего скрипта. Одним из лучших онлайн-ресурсов, предоставляющем возможность построения крестиков-ноликов, является StockCharts. Сайт ориентирован на биржевую торговлю, поэтому найти на нем котировки по инструментам форекс, к сожалению, невозможно.

Для контроля результатов работы написанного нами скрипта построим аналогичные графики в программе и на сайте по инструментам: фьючерс на золото, фьючерс на нефть сорта Light crude oil, контракт на разницу S&P 500; только в Bull's-Eye Broker построим график EURUSD (помним об ограничениях StockChart).


Алгоритм построения графиков "крестики-нолики"

Итак, алгоритм.

Ключевыми понятиями при построении графиков крестики-нолики являются лишь два понятия:

  1. Размер бокса - минимальное изменение котировки инструмента, менее которого не происходит никаких изменений на графике;
  2. Разворот - количество боксов, проходимых котировкой в направлении, противоположном текущему направлению графика, после которого это движение будет отображено в новой колонке.

Поскольку для построения графика мы вынуждены использовать историю котировок, хранимой в виде цен Open-High-Low-Close, то принимаются следующие допущения:

  1. График строится по ценам High-Low;
  2. Цена High округляется до размера бокса вниз (MathFloor), цена Low округляется до размера бокса вверх (MathCeil).

Поясню на примере. Допустим, мы хотим строить график Light crude oil, размер бокса принимаем равным 1 (один) $ и разворот устанавливаем в 3 (три) бокса. Это значит, что все цены High мы округляем вниз с точностью 1$, а все цены Low - вверх с той же точностью:

Дата High Low XO High XO Low
2012.02.13 100.86 99.08 100 100
2012.02.14 101.83 100.28 101 101
2012.02.15 102.53 100.62 102 101
2012.02.16 102.68 100.84 102 101
2012.02.17 103.95 102.24 103 102


Движение цены вверх на графике отображается символом "Х" (крестик), движение вниз отображается символом "О" (нолик).

Как принимается решение о первоначальном направлении движения цены (первая колонка - это колонка Х или О):

Запоминаем значения XO High и XO Low бара Bars-1 и ждем, когда:

В нашем примере с нефтью мы запомним цену XO High[Bars-1]=100 и XO Low[Bars-1]=100.

Далее ждем, что случится раньше:

17-го февраля мы получаем представление о первой колонке: цена XO High стала равна 103$ и наша первая колонка - это колонка "Х". Строим ее в виде четырех Х начиная с 100$ и завершая на 103$.

Как принимается решение о дальнейшем построении:

Если текущая колонка - колонка "Х", то проверяем: цена XO High текущего бара больше на Размер бокса текущей цены XO (т.е. 20-го февраля мы сначала будем проверять, цена XO High не стала равна или больше 104$). Если цена XO High[2012.02.20] будет 104$, или 105$, или еще больше, то мы в существующей колонке "Х" просто достраиваем сверху нужное количество "Х".

Если цена XO High текущего бара не больше на Размер бокса текущей цены XO, тогда проверяем, а цена XO Low текущего бара не меньше цены XO High на разворотное число боксов (в примере - цена XO Low[2012.02.20] равна или меньше 103$-3*1$=100$, или 99$, или еще ниже). Если меньше, то справа рядом, начиная со 102$ и продолжая до 100$, строим колонку "О".

При текущей колонке - колонке "О" все вышеприведенные соображения инвертируются.

ВАЖНО: новая колонка "О" всегда начинает строиться справа и на один бокс ниже верхнего значения предыдущей колонки "Х" и новая колонка "Х" всегда начинает строится справа и на один бокс выше нижнего значения предыдущей колонки "О".

С самим графиком разобрались. Теперь о линиях поддержки-сопротивления.

На ортодоксальных графиках крестиков-ноликов линии поддержки-сопротивления всегда идут под углом 45 градусов.

Построение первоначальной линии начинается в зависимости от значения первой колонки. Если первая колонка - колонка "Х", то первая линия будет линией сопротивления, и начнется она на один бокс выше максимума первой колонки, идти она будет под углом 45 градусов ВНИЗ и вправо. Если первая колонка - колонка "О", то первая линия будет линией поддержки и начнется она на один бокс ниже минимума первой колонки, идти она будет под углом 45 градусов ВВЕРХ и вправо. Линия поддержки-сопротивления строится до ее пересечения с графиком цены.

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

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

Все вышесказанное проще всего будет представлено в дальнейшем на примере реальных графиков.

С алгоритмом построения самого графика к этому моменту мы с вами разобрались. Теперь прикрутим к нашему скрипту несколько приятных функций:

Алгоритм и требования описаны - скрипт в студию.

//+------------------------------------------------------------------+
//|                                         Point&Figure text charts |
//|                                        BSD Lic. 2012, Roman Rich |
//|                                           http://www.FXRays.info |
//+------------------------------------------------------------------+
#property               copyright "Roman Rich"
#property               link      "http://www.FXRays.info"
#property               version   "1.00"
#property               script_show_inputs
#include                "cIntBMP.mqh"                                       // Включаем файл с классом cIntBMP

input bool              mw=true;                                    // Весь MarketWatch?
input ENUM_TIMEFRAMES   tf=PERIOD_M1;                                 // Таймфрейм
input long              box=2;                                      // Размер бокса в пипсах (0 - авто)
enum                    cChoice{c10=10,c25=25,c50=50,c100=100};
input cChoice           count=c50;                                 // Высота графика в боксах для авто
enum                    rChoice{Two=2,Three,Four,Five,Six,Seven};
input rChoice           reverse=Five;                              // Кол-во боксов для разворота
enum                    vChoice{v10=10,v25=25,v50=50};
input vChoice           vd=v10;                                    // Символов для отображения объемов
enum                    dChoice{Little=15000,Middle=50000,Many=100000,Extremely=1000000};
input dChoice           depth=Little;                              // Глубина истории
input bool              pic=true;                                   // Файл рисунка?
input int               cellsize=10;                                // Размер ячейки в пикселах
//+------------------------------------------------------------------+
//| Класс-наследник cIntBMP                                          |
//+------------------------------------------------------------------+
class cIntBMPEx : public cIntBMP
  {
public:
   void              Rectangle(int aX1,int aY1,int aSizeX,int aSizeY,int aColor);
   void              Bar(int aX1,int aY1,int aSizeX,int aSizeY,int aColor);
   void              LineH(int aX1,int aY1,int aSizeX,int aColor);
   void              LineV(int aX1,int aY1,int aSizeY,int aColor);
   void              DrawBar(int aX1,int aY1,int aX2,int aY2,int aColor);
   void              TypeTextV(int aX,int aY,string aText,int aColor);
  };
cIntBMPEx bmp;    // Экземпляр класса cIntBMPEx
uchar Mask_O[192]= // Наши нолики
  {
   217,210,241,111,87,201,124,102,206,165,150,221,237,234,248,255,255,255,255,255,255,255,255,255,
   73,42,187,137,117,211,201,192,235,140,120,212,60,27,182,178,165,226,255,255,255,255,255,255,
   40,3,174,250,249,253,255,255,255,255,255,255,229,225,245,83,54,190,152,135,216,255,255,255,
   68,36,185,229,225,245,255,255,255,255,255,255,255,255,255,247,246,252,78,48,188,201,192,235,
   140,120,212,145,126,214,255,255,255,255,255,255,255,255,255,255,255,255,188,177,230,124,102,206,
   237,234,248,58,24,181,209,201,238,255,255,255,255,255,255,255,255,255,168,153,222,124,102,206,
   255,255,255,199,189,234,63,30,183,186,174,229,247,246,252,204,195,236,60,27,182,204,195,236,
   255,255,255,255,255,255,232,228,246,117,93,203,52,18,179,83,54,190,196,186,233,255,255,255
  };
uchar Mask_X[192]= // Наши крестики
  {
   254,252,252,189,51,51,236,195,195,255,255,255,255,255,255,235,192,192,248,234,234,255,255,255,
   255,255,255,202,90,90,184,33,33,251,243,243,212,120,120,173,0,0,173,0,0,255,255,255,
   255,255,255,254,252,252,195,69,69,192,60,60,178,15,15,233,186,186,253,249,249,255,255,255,
   255,255,255,255,255,255,241,210,210,173,0,0,209,111,111,255,255,255,255,255,255,255,255,255,
   255,255,255,255,255,255,205,99,99,192,60,60,181,24,24,241,210,210,255,255,255,255,255,255,
   255,255,255,249,237,237,176,9,9,241,213,213,226,165,165,189,51,51,254,252,252,255,255,255,
   255,255,255,230,177,177,185,36,36,255,255,255,255,255,255,189,51,51,222,153,153,255,255,255,
   255,255,255,240,207,207,200,84,84,255,255,255,255,255,255,227,168,168,211,117,117,255,255,255
  };
//+------------------------------------------------------------------+
//| Выбор инструмента                                                |
//+------------------------------------------------------------------+
void OnStart()
  {
   int    mwSymb;
   string symb;
   int    height=0,width=0;
   string pnfArray[];
   if(mw==true)
     {
      mwSymb=0;
      while(mwSymb<SymbolsTotal(true))
        {
         symb=SymbolName(mwSymb,true);
         ArrayFree(pnfArray);
         ArrayResize(pnfArray,0,0);
         PNF(symb,pnfArray,height,width,pic,cellsize);
         pnf2file(symb,pnfArray,0,height);
         mwSymb++;
        };
     }
   else
     {
      symb=Symbol();
      ArrayFree(pnfArray);
      ArrayResize(pnfArray,0,0);
      PNF(symb,pnfArray,height,width,pic,cellsize);
      pnf2file(symb,pnfArray,0,height);
     };
   Alert("Ok.");
  }
//+------------------------------------------------------------------+
//| Непосредственно расчет и построение графика                      |
//+------------------------------------------------------------------+
void PNF(string sName,      // инструмент
         string& array[],  // массив, куда направляем вывод
         int& y,           // высота массива
         int& z,           // ширина массива
         bool toPic,       // если true-выведем и рисунок
         int cs)           // примем размер ячейки для рисования
  {
   string      s,ps;
   datetime    d[];
   double      o[],h[],l[],c[];
   long        v[];
   uchar       matrix[];
   long        VolByPrice[],VolByCol[],HVolumeMax,VVolumeMax;
   int         tMin[],tMax[];
   datetime    DateByCol[];
   MqlDateTime bMDT,eMDT;
   string      strDBC[];
   uchar       pnf='.';
   int         sd;
   int         b,i,j,k=0,m=0;
   int         GlobalMin,GlobalMax,StartMin,StartMax,CurMin,CurMax,RevMin,RevMax,ContMin,ContMax;
   int         height,width,beg=0,end=0;
   double      dBox,price;
   int         thBeg=1,thEnd=2,tv=0;
   uchar       trend='.';
// --------------------------------- BMP -----------------------------------------
   int RowVolWidth=10*cs;
//--- отступ для цен
   int startX=5*cs;
   int yshift=cs*7;
// --------------------------------- BMP -----------------------------------------
   if(SymbolInfoInteger(sName,SYMBOL_DIGITS)<=3) sd=2; else sd=4;
   b=MathMin(Bars(sName,tf),depth);
   ArrayFree(d);
   ArrayFree(o);
   ArrayFree(h);
   ArrayFree(l);
   ArrayFree(c);
   ArrayFree(v);
   ArrayFree(matrix);
   ArrayFree(VolByPrice);
   ArrayFree(VolByCol);
   ArrayFree(DateByCol);
   ArrayFree(tMin);
   ArrayFree(tMax);
   ArrayResize(d,b,0);
   ArrayResize(o,b,0);
   ArrayResize(h,b,0);
   ArrayResize(l,b,0);
   ArrayResize(c,b,0);
   ArrayResize(v,b,0);
   ArrayInitialize(d,NULL);
   ArrayInitialize(o,NULL);
   ArrayInitialize(h,NULL);
   ArrayInitialize(l,NULL);
   ArrayInitialize(c,NULL);
   ArrayInitialize(v,NULL);
   CopyTime(sName,tf,0,b,d);
   CopyOpen(sName,tf,0,b,o);
   CopyHigh(sName,tf,0,b,h);
   CopyLow(sName,tf,0,b,l);
   CopyClose(sName,tf,0,b,c);
   CopyTickVolume(sName,tf,0,b,v);
   if(box!=0)
     {
      dBox=box/MathPow(10.0,(double)sd);
     }
   else
     {
      dBox=MathNorm((h[ArrayMaximum(h,0,WHOLE_ARRAY)]-l[ArrayMinimum(l,0,WHOLE_ARRAY)])/count,
                      1/MathPow(10.0,(double)sd),true)/MathPow(10.0,(double)sd);
     };
   GlobalMin=MathNorm(l[ArrayMinimum(l,0,WHOLE_ARRAY)],dBox,true)-(int)(reverse);
   GlobalMax=MathNorm(h[ArrayMaximum(h,0,WHOLE_ARRAY)],dBox,false)+(int)(reverse);
   StartMin=MathNorm(l[0],dBox,true);
   StartMax=MathNorm(h[0],dBox,false);
   ContMin=(int)(StartMin-1);
   ContMax=(int)(StartMax+1);
   RevMin=(int)(StartMax-reverse);
   RevMax=(int)(StartMin+reverse);
   height=(int)(GlobalMax-GlobalMin);
   width=1;
   ArrayResize(matrix,height*width,0);
   ArrayInitialize(matrix,'.');
   ArrayResize(VolByPrice,height,0);
   ArrayInitialize(VolByPrice,0);
   ArrayResize(VolByCol,width,0);
   ArrayInitialize(VolByCol,0);
   ArrayResize(DateByCol,width,0);
   ArrayInitialize(DateByCol,D'01.01.1971');
   ArrayResize(tMin,width,0);
   ArrayInitialize(tMin,0);
   ArrayResize(tMax,width,0);
   ArrayInitialize(tMax,0);
   for(i=1;i<b;i++)
     {
      CurMin=MathNorm(l[i],dBox,true);
      CurMax=MathNorm(h[i],dBox,false);
      switch(pnf)
        {
         case '.':
           {
            if(CurMax>=RevMax)
              {
               pnf='X';
               ContMax=(int)(CurMax+1);
               RevMin=(int)(CurMax-reverse);
               beg=(int)(StartMin-GlobalMin-1);
               end=(int)(CurMax-GlobalMin-1);
               SetMatrix(matrix,beg,end,height,(int)(width-1),pnf);
               SetVector(VolByPrice,beg,end,v[i]);
               VolByCol[width-1]=VolByCol[width-1]+v[i];
               DateByCol[width-1]=d[i];
               trend='D';
               break;
              };
            if(CurMin<=RevMin)
              {
               pnf='O';
               ContMin=(int)(CurMin-1);
               RevMax=(int)(CurMin+reverse);
               beg=(int)(CurMin-GlobalMin-1);
               end=(int)(StartMax-GlobalMin-1);
               SetMatrix(matrix,beg,end,height,(int)(width-1),pnf);
               SetVector(VolByPrice,beg,end,v[i]);
               VolByCol[width-1]=VolByCol[width-1]+v[i];
               DateByCol[width-1]=d[i];
               trend='U';
               break;
              };
            break;
           };
         case 'X':
           {
            if(CurMax>=ContMax)
              {
               pnf='X';
               ContMax=(int)(CurMax+1);
               RevMin=(int)(CurMax-reverse);
               end=(int)(CurMax-GlobalMin-1);
               SetMatrix(matrix,beg,end,height,(int)(width-1),pnf);
               SetVector(VolByPrice,beg,end,v[i]);
               VolByCol[width-1]=VolByCol[width-1]+v[i];
               DateByCol[width-1]=d[i];
               break;
              };
            if(CurMin<=RevMin)
              {
               pnf='O';
               ContMin=(int)(CurMin-1);
               RevMax=(int)(CurMin+reverse);
               tMin[width-1]=beg-1;
               tMax[width-1]=end+1;
               beg=(int)(CurMin-GlobalMin-1);
               end--;
               width++;
               ArrayResize(matrix,height*width,0);
               ArrayResize(VolByCol,width,0);
               ArrayResize(DateByCol,width,0);
               ArrayResize(tMin,width,0);
               ArrayResize(tMax,width,0);
               SetMatrix(matrix,0,(int)(height-1),height,(int)(width-1),'.');
               SetMatrix(matrix,beg,end,height,(int)(width-1),pnf);
               SetVector(VolByPrice,beg,end,v[i]);
               VolByCol[width-1]=0;
               VolByCol[width-1]=VolByCol[width-1]+v[i];
               DateByCol[width-1]=d[i];
               tMin[width-1]=beg-1;
               tMax[width-1]=end+1;
               break;
              };
            break;
           };
         case 'O':
           {
            if(CurMin<=ContMin)
              {
               pnf='O';
               ContMin=(int)(CurMin-1);
               RevMax=(int)(CurMin+reverse);
               beg=(int)(CurMin-GlobalMin-1);
               SetMatrix(matrix,beg,end,height,(int)(width-1),pnf);
               SetVector(VolByPrice,beg,end,v[i]);
               VolByCol[width-1]=VolByCol[width-1]+v[i];
               DateByCol[width-1]=d[i];
               break;
              };
            if(CurMax>=RevMax)
              {
               pnf='X';
               ContMax=(int)(CurMax+1);
               RevMin=(int)(CurMax-reverse);
               tMin[width-1]=beg-1;
               tMax[width-1]=end+1;
               beg++;
               end=(int)(CurMax-GlobalMin-1);
               width++;
               ArrayResize(matrix,height*width,0);
               ArrayResize(VolByCol,width,0);
               ArrayResize(DateByCol,width,0);
               ArrayResize(tMin,width,0);
               ArrayResize(tMax,width,0);
               SetMatrix(matrix,0,(int)(height-1),height,(int)(width-1),'.');
               SetMatrix(matrix,beg,end,height,(int)(width-1),pnf);
               SetVector(VolByPrice,beg,end,v[i]);
               VolByCol[width-1]=0;
               VolByCol[width-1]=VolByCol[width-1]+v[i];
               DateByCol[width-1]=d[i];
               tMin[width-1]=beg-1;
               tMax[width-1]=end+1;
               break;
              };
            break;
           };
        };
     };
//--- не забываем себя
   s="BSD License, 2012, FXRays.info by Roman Rich";
   k++;
   ArrayResize(array,k,0);
   array[k-1]=s;
   s=SymbolInfoString(sName,SYMBOL_DESCRIPTION)+",
                      Box-"+DoubleToString(box,0)+",Reverse-"+DoubleToString(reverse,0);
   k++;
   ArrayResize(array,k,0);
   array[k-1]=s;
// --------------------------------- BMP -----------------------------------------
   if(toPic==true)
     {
      //-- размеры изображения BMP на экране графика
      int XSize=cs*width+2*startX+RowVolWidth;
      int YSize=cs*height+yshift+70;
      //-- создаем bmp-картинку размером XSize x YSize c фоном цвета clrWhite
      bmp.Create(XSize,YSize,clrWhite);
      //-- отображение клеток основного поля
      for(i=height-1;i>=0;i--)
         for(j=0;j<=width-1;j++)
           {
            bmp.Bar(RowVolWidth+startX+cs*j,yshift+cs*i,cs,cs,clrWhite);
            bmp.Rectangle(RowVolWidth+startX+cs*j,yshift+cs*i,cs,cs,clrLightGray);
           }
      bmp.TypeText(10,yshift+cs*(height)+50,array[k-2],clrDarkGray);
      bmp.TypeText(10,yshift+cs*(height)+35,array[k-1],clrGray);
     }
// --------------------------------- BMP -----------------------------------------
//--- рассчитываем линии тренда
   i=0;
   while(thEnd<width-1)
     {
      while(thBeg+i<thEnd)
        {
         if(trend=='U')
           {
            i=ArrayMinimum(tMin,thBeg,thEnd-thBeg);
            j=tMin[i];
           }
         else
           {
            i=ArrayMaximum(tMax,thBeg,thEnd-thBeg);
            j=tMax[i];
           }
         thBeg=i;
         tv=j;
         i=0;
         while(GetMatrix(matrix,j,height,(long)(thBeg+i))=='.')
           {
            i++;
            if(trend=='U') j++; else j--;
            if(thBeg+i==width-1)
              {
               thEnd=width-1;
               break;
              };
           };
         if(thBeg+i<thEnd)
           {
            thBeg=thBeg+2;
            i=0;
           };
        };
      thEnd=thBeg+i;
      if(thEnd==thBeg) thEnd++;
      for(i=thBeg;i<thEnd;i++)
        {
         SetMatrix(matrix,tv,tv,height,(long)(i),'+');
         // --------------------------------- BMP -----------------------------------------
         if(toPic==true)
           {
            //--- линии поддержки и сопротивления
            if(trend=='U') { bmp.DrawLine(RowVolWidth+startX+i*cs,yshift+tv*cs,
                                         RowVolWidth+startX+(i+1)*cs,yshift+(tv+1)*cs,clrGreen); }
            if(trend=='D') { bmp.DrawLine(RowVolWidth+startX+i*cs,yshift+(tv+1)*cs,
                                         RowVolWidth+startX+(i+1)*cs,yshift+(tv)*cs,clrRed); }
            //--- "уширение" линий поддержки/сопротивления
            if(trend=='U') { bmp.DrawLine(RowVolWidth+1+startX+i*cs,yshift+tv*cs,
                                         RowVolWidth+1+startX+(i+1)*cs,yshift+(tv+1)*cs,clrGreen); }
            if(trend=='D') { bmp.DrawLine(RowVolWidth+1+startX+i*cs,yshift+(tv+1)*cs,
                                         RowVolWidth+1+startX+(i+1)*cs,yshift+(tv)*cs,clrRed); }
           }
         // --------------------------------- BMP -----------------------------------------
         if(trend=='U') tv++; else tv--;
        };
      if(trend=='U') trend='D'; else trend='U';
      i=0;
     };
//--- отображаем даты по столбцам
   ArrayResize(strDBC,width,0);
   TimeToStruct(DateByCol[0],bMDT);
   TimeToStruct(DateByCol[width-1],eMDT);
   if((DateByCol[width-1]-DateByCol[0])>=50000000)
     {
      for(i=0;i<=width-1;i++) StringInit(strDBC[i],4,' ');
      for(i=1;i<=width-1;i++)
        {
         TimeToStruct(DateByCol[i-1],bMDT);
         TimeToStruct(DateByCol[i],eMDT);
         if(bMDT.year!=eMDT.year) strDBC[i]=DoubleToString(eMDT.year,0);
        };
      for(i=0;i<=3;i++)
        {
         StringInit(s,vd,' ');
         s=s+"            : ";
         for(j=0;j<=width-1;j++) s=s+StringSubstr(strDBC[j],i,1);
         s=s+" : ";
         k++;
         ArrayResize(array,k,0);
         array[k-1]=s;
        };
     }
   else
     {
      if((DateByCol[width-1]-DateByCol[0])>=5000000)
        {
         for(i=0;i<=width-1;i++) StringInit(strDBC[i],7,' ');
         for(i=1;i<=width-1;i++)
           {
            TimeToStruct(DateByCol[i-1],bMDT);
            TimeToStruct(DateByCol[i],eMDT);
            if(bMDT.mon!=eMDT.mon)
              {
               if(eMDT.mon<10) strDBC[i]=DoubleToString(eMDT.year,0)+".0"+DoubleToString(eMDT.mon,0);
               if(eMDT.mon>=10) strDBC[i]=DoubleToString(eMDT.year,0)+"."+DoubleToString(eMDT.mon,0);
              }
           };
         for(i=0;i<=6;i++)
           {
            StringInit(s,vd,' ');
            s=s+"            : ";
            for(j=0;j<=width-1;j++) s=s+StringSubstr(strDBC[j],i,1);
            s=s+" : ";
            k++;
            ArrayResize(array,k,0);
            array[k-1]=s;
           };
        }
      else
        {
         for(i=0;i<=width-1;i++) StringInit(strDBC[i],10,' ');
         for(i=1;i<=width-1;i++)
           {
            TimeToStruct(DateByCol[i-1],bMDT);
            TimeToStruct(DateByCol[i],eMDT);
            if(bMDT.day!=eMDT.day)
              {
               if(eMDT.mon<10 && eMDT.day<10) strDBC[i]=DoubleToString(eMDT.year,0)+".0"
                                                       +DoubleToString(eMDT.mon,0)+".0"+DoubleToString(eMDT.day,0);
               if(eMDT.mon<10 && eMDT.day>=10) strDBC[i]=DoubleToString(eMDT.year,0)+".0"
                                                       +DoubleToString(eMDT.mon,0)+"."+DoubleToString(eMDT.day,0);
               if(eMDT.mon>=10&&eMDT.day< 10) strDBC[i]=DoubleToString(eMDT.year,0)+"." 
                                                      +DoubleToString(eMDT.mon,0)+".0"+DoubleToString(eMDT.day,0);
               if(eMDT.mon>=10&&eMDT.day>=10) strDBC[i]=DoubleToString(eMDT.year,0)+"." 
                                                      +DoubleToString(eMDT.mon,0)+"." +DoubleToString(eMDT.day,0);
              }
           };
         for(i=0;i<=9;i++)
           {
            StringInit(s,vd,' ');
            s=s+"            : ";
            for(j=0;j<=width-1;j++) s=s+StringSubstr(strDBC[j],i,1);
            s=s+" : ";
            k++;
            ArrayResize(array,k,0);
            array[k-1]=s;
           };
        };
     };
   StringInit(s,25+vd+width,'-');
   k++;
   ArrayResize(array,k,0);
   array[k-1]=s;
//--- отображаем ценовой график
   price=GlobalMax*dBox;
   HVolumeMax=VolByPrice[ArrayMaximum(VolByPrice,0,WHOLE_ARRAY)];
   s="";
   for(i=height-1;i>=0;i--)
     {
      StringInit(ps,8-StringLen(DoubleToString(price,sd)),' ');
      s=s+ps+DoubleToString(price,sd)+" : ";
      for(j=0;j<vd;j++) if(VolByPrice[i]>HVolumeMax*j/vd) s=s+"*"; else s=s+" ";
      s=s+" : ";
      for(j=0;j<=width-1;j++) s=s+CharToString(matrix[j*height+i]);
      s=s+" : "+ps+DoubleToString(price,sd);
      k++;
      ArrayResize(array,k,0);
      array[k-1]=s;
      s="";
      price=price-dBox;
     };
   StringInit(s,25+vd+width,'-');
   k++;
   ArrayResize(array,k,0);
   array[k-1]=s;
//--- простенькая разметка через 10-ку
   StringInit(s,vd,' ');
   s=s+"            : ";
   for(j=0;j<=width-1;j++) if(StringGetCharacter(DoubleToString(j,0),
                                                    StringLen(DoubleToString(j,0))-1)==57) s=s+"|"; else s=s+" ";
   s=s+" : ";
   k++;
   ArrayResize(array,k,0);
   array[k-1]=s;
//--- отображаем график объемов по столбцам
   VVolumeMax=VolByCol[ArrayMaximum(VolByCol,0,WHOLE_ARRAY)];
   for(i=vd-1;i>=0;i--)
     {
      StringInit(s,vd,' ');
      s=s+"            : ";
      for(j=0;j<=width-1;j++) if(VolByCol[j]>VVolumeMax*i/vd) s=s+"*"; else s=s+" ";
      s=s+" : ";
      k++;
      ArrayResize(array,k,0);
      array[k-1]=s;
     };
   StringInit(s,25+vd+width,'-');
   k++;
   ArrayResize(array,k,0);
   array[k-1]=s;
//--- история колонок
   s="     | Start Date/Time     | End Date/Time       | ";
   k++;
   ArrayResize(array,k,0);
   array[k-1]=s;
   TimeToStruct(DateByCol[0],bMDT);
   s="   1 | 0000/00/00 00:00:00 | ";
   s=s+DoubleToString(bMDT.year,0)+"/";
   if(bMDT.mon >=10) s=s+DoubleToString(bMDT.mon ,0)+"/"; else s=s+"0"+DoubleToString(bMDT.mon ,0)+"/";
   if(bMDT.day >=10) s=s+DoubleToString(bMDT.day ,0)+" "; else s=s+"0"+DoubleToString(bMDT.day ,0)+" ";
   if(bMDT.hour>=10) s=s+DoubleToString(bMDT.hour,0)+":"; else s=s+"0"+DoubleToString(bMDT.hour,0)+":";
   if(bMDT.min >=10) s=s+DoubleToString(bMDT.min ,0)+":"; else s=s+"0"+DoubleToString(bMDT.min ,0)+":";
   if(bMDT.sec >=10) s=s+DoubleToString(bMDT.sec ,0)+" | "; else s=s+"0"+DoubleToString(bMDT.sec ,0)+" | ";
   k++;
   ArrayResize(array,k,0);
   array[k-1]=s;
   for(i=1;i<=width-1;i++)
     {
      TimeToStruct(DateByCol[i-1],bMDT);
      TimeToStruct(DateByCol[i],eMDT);
      s="";
      StringInit(ps,4-StringLen(DoubleToString(i+1,0)),' ');
      s=s+ps+DoubleToString(i+1,0)+" | ";
      s=s+DoubleToString(bMDT.year,0)+"/";
      if(bMDT.mon >=10) s=s+DoubleToString(bMDT.mon ,0)+"/"; else s=s+"0"+DoubleToString(bMDT.mon ,0)+"/";
      if(bMDT.day >=10) s=s+DoubleToString(bMDT.day ,0)+" "; else s=s+"0"+DoubleToString(bMDT.day ,0)+" ";
      if(bMDT.hour>=10) s=s+DoubleToString(bMDT.hour,0)+":"; else s=s+"0"+DoubleToString(bMDT.hour,0)+":";
      if(bMDT.min >=10) s=s+DoubleToString(bMDT.min ,0)+":"; else s=s+"0"+DoubleToString(bMDT.min ,0)+":";
      if(bMDT.sec >=10) s=s+DoubleToString(bMDT.sec ,0)+" | "; else s=s+"0"+DoubleToString(bMDT.sec ,0)+" | ";
      s=s+DoubleToString(eMDT.year,0)+"/";
      if(eMDT.mon >=10) s=s+DoubleToString(eMDT.mon ,0)+"/"; else s=s+"0"+DoubleToString(eMDT.mon ,0)+"/";
      if(eMDT.day >=10) s=s+DoubleToString(eMDT.day ,0)+" "; else s=s+"0"+DoubleToString(eMDT.day ,0)+" ";
      if(eMDT.hour>=10) s=s+DoubleToString(eMDT.hour,0)+":"; else s=s+"0"+DoubleToString(eMDT.hour,0)+":";
      if(eMDT.min >=10) s=s+DoubleToString(eMDT.min ,0)+":"; else s=s+"0"+DoubleToString(eMDT.min ,0)+":";
      if(eMDT.sec >=10) s=s+DoubleToString(eMDT.sec ,0)+" | "; else s=s+"0"+DoubleToString(eMDT.sec ,0)+" | ";
      k++;
      ArrayResize(array,k,0);
      array[k-1]=s;
     };
   y=k;
   z=25+vd+width;
// --------------------------------- BMP -----------------------------------------
   if(toPic==true)
     {
      //--- отображение дат в виде YYYY/MM/DD
      for(j=0;j<=width-1;j++)
        {
         string s0=strDBC[j];
         StringReplace(s0,".","/");
         bmp.TypeTextV(RowVolWidth+startX+cs*j,yshift+cs*(height-1)+5,s0,clrDimGray);
        }
      //--- подложка с клетками для объемов
      for(i=height-1;i>=0;i--)
         for(j=0;j<vd;j++)
           {
            bmp.Bar(cs+startX+cs*(j-1),yshift+cs*i,cs,cs,0xF6F6F6);
            bmp.Rectangle(cs+startX+cs*(j-1),yshift+cs*i,cs,cs,clrLightGray);
           }
      for(i=0; i>-7;i--)
         for(j=0;j<=vd;j++)
           {
            bmp.Bar(cs+startX+cs*(j-1),yshift+cs*i,cs,cs,clrWhite);
            bmp.Rectangle(cs+startX+cs*(j-1),yshift+cs*i,cs,cs,clrLightGray);
           }
      //--- объемы точные
      for(i=height-1;i>=0;i--)
         bmp.Bar(startX,yshift+cs*i,int(10*cs*VolByPrice[i]/HVolumeMax),cs,0xB5ABAB);
      //--- отображение крестиков и ноликов
      for(i=height-1;i>=0;i--)
         for(j=0;j<=width-1;j++)
           {
            int xpos=RowVolWidth+startX+cs*j+1;
            int ypos=yshift+cs*i+1;
            if(CharToString(matrix[j*height+i])=="X") ShowCell(xpos,ypos,'X');
            else
               if(CharToString(matrix[j*height+i])=="O") ShowCell(xpos,ypos,'O');
           }
      //--- подложка для объемов внизу
      for(i=0;i<=60/cs;i++)
         for(j=0;j<=width-1;j++)
           {
            bmp.Bar(RowVolWidth+startX+cs*j,12+cs*i,cs,cs,0xF6F6F6);
            bmp.Rectangle(RowVolWidth+startX+cs*j,12+cs*i,cs,cs,clrLightGray);
           }
      //--- отображение объемов
      for(j=0;j<=width-1;j++) bmp.Bar(RowVolWidth+startX+cs*j,yshift-60,
                                     cs,int(60*VolByCol[j]/VVolumeMax),0xB5ABAB);
      //--- отображение рамки основного поля
      bmp.Rectangle(RowVolWidth+startX+cs*0,yshift+cs*0,cs*(width),cs*(height),clrSilver);
      //--- отображение цен и шкалы
      bmp.LineV(startX,yshift,cs*height,clrBlack);
      bmp.LineV(RowVolWidth+startX+cs*width,yshift,cs*height,clrBlack);
      price=GlobalMax*dBox;
      for(i=height-1;i>=0;i--)
        {
         //-- цены слева
         bmp.TypeText(cs,yshift+cs*i,DoubleToString(price,sd),clrBlack);
         bmp.LineH(0,yshift+cs*i,startX,clrLightGray);
         bmp.LineH(0+startX-3,yshift+cs*i,6,clrBlack);
         //-- цены справа     
         int dx=RowVolWidth+cs*width;
         bmp.TypeText(10+startX+dx,yshift+cs*i,DoubleToString(price,sd),clrBlack);
         bmp.LineH(startX+dx,yshift+cs*i,40,clrLightGray);
         bmp.LineH(startX+dx-3,yshift+cs*i,6,clrBlack);
         price=price-dBox;
        }
      //-- сохраняем полученную картинку в файл  
      bmp.Save(sName,true);
     }
// --------------------------------- BMP -----------------------------------------
  }
//+------------------------------------------------------------------+
//|Выводим в текстовый файл                                          |
//+------------------------------------------------------------------+
void pnf2file(string sName,        // инструмент для имени файла
              string& array[],    // массив строк, который записываем в файл
              int beg,            // с какой строки массива начинаем записывать в файл
              int end)            // и какой строкой заканчиваем
  {
   string fn;
   int    handle;
   fn=sName+"_b"+DoubleToString(box,0)+"_r"+DoubleToString(reverse,0)+".txt";
   handle=FileOpen(fn,FILE_WRITE|FILE_TXT|FILE_ANSI,';');
   for(int i=beg;i<end;i++) FileWrite(handle,array[i]);
   FileClose(handle);
  }
//+------------------------------------------------------------------+
//| Приводим цену к размеру бокса                                    |
//+------------------------------------------------------------------+
int MathNorm(double value,     // любое double-число преобразуем в long-число
             double prec,      // доводим его до точности
             bool vect)        // и если true - округляем вверх, false - округляем вниз
  {
   if(vect==true)
      return((int)(MathCeil(value/prec)));
   else
      return((int)(MathFloor(value/prec)));
  }
//+------------------------------------------------------------------+
//| Заполняем массив                                                 |
//| Символьный одномерный массив представляем в виде матрицы         |
//+------------------------------------------------------------------+
void SetMatrix(uchar& array[],      // передаем его по ссылке для замены в нем
               long pbeg,          // начиная с этого места
               long pend,          // по это место
               long pheight,       // в колонке такой высоты
               long pwidth,        // с таким номером из числа всех колонок массива
               uchar ppnf)         // вот этим символом
  {
   long offset=0;
   for(offset=pheight*pwidth+pbeg;offset<=pheight*pwidth+pend;offset++) array[(int)offset]=ppnf;
  }
//+------------------------------------------------------------------+
//| Получаем из массива единичное значение                           |
//| Символьный одномерный массив представляем в виде матрицы         |
//+------------------------------------------------------------------+
uchar GetMatrix(uchar& array[],      // передаем его по ссылке для получения из него символа...
                long pbeg,          // в этом месте
                long pheight,       // в колонке такой высоты
                long pwidth)        // с таким номером из числа всех колонок массива
  {
   return(array[(int)pheight*(int)pwidth+(int)pbeg]);
  }
//+------------------------------------------------------------------+
//|Заполняем вектор                                                  |
//+------------------------------------------------------------------+
void SetVector(long &array[],      // long-массив передаем по ссылке для замены в нем
               long pbeg,         // начиная с этого места
               long pend,         // по это место
               long pv)           // вот этим значением
  {
   long offset=0;
   for(offset=pbeg;offset<=pend;offset++) array[(int)offset]=array[(int)offset]+pv;
  }
//+------------------------------------------------------------------+
//| Отображение горизонтальной линии                                 |
//+------------------------------------------------------------------+
void cIntBMPEx::LineH(int aX1,int aY1,int aSizeX,int aColor)
  {
   DrawLine(aX1,aY1,aX1+aSizeX,aY1,aColor);
  }
//+------------------------------------------------------------------+
//| Отображение вертикальной линии                                   |
//+------------------------------------------------------------------+  
void cIntBMPEx::LineV(int aX1,int aY1,int aSizeY,int aColor)
  {
   DrawLine(aX1,aY1,aX1,aY1+aSizeY,aColor);
  }
//+------------------------------------------------------------------+
//| Рисует прямоугольник (заданного размера)                         |
//+------------------------------------------------------------------+
void cIntBMPEx::Rectangle(int aX1,int aY1,int aSizeX,int aSizeY,int aColor)
  {
   DrawRectangle(aX1,aY1,aX1+aSizeX,aY1+aSizeY,aColor);
  }
//+------------------------------------------------------------------+
//| Рисует закрашенный прямоугольник (заданного размера)             |
//+------------------------------------------------------------------+
void cIntBMPEx::Bar(int aX1,int aY1,int aSizeX,int aSizeY,int aColor)
  {
   DrawBar(aX1,aY1,aX1+aSizeX,aY1+aSizeY,aColor);
  }
//+------------------------------------------------------------------+
//| Рисует закрашенный прямоугольник                                 |
//+------------------------------------------------------------------+
void cIntBMPEx::DrawBar(int aX1,int aY1,int aX2,int aY2,int aColor)
  {
   for(int i=aX1; i<=aX2; i++)
      for(int j=aY1; j<=aY2; j++)
        {
         DrawDot(i,j,aColor);
        }
  }
//+------------------------------------------------------------------+
//| Вертикальное отображение текста                                  |
//+------------------------------------------------------------------+
void cIntBMPEx::TypeTextV(int aX,int aY,string aText,int aColor)
  {
   SetDrawWidth(1);
   for(int j=0;j<StringLen(aText);j++)
     {
      string TypeChar=StringSubstr(aText,j,1);
      if(TypeChar==" ")
        {
         aY+=5;
        }
      else
        {
         int Pointer=0;
         for(int i=0;i<ArraySize(CA);i++)
           {
            if(CA[i]==TypeChar)
              {
               Pointer=i;
              }
           }
         for(int i=PA[Pointer];i<PA[Pointer+1];i++)
           {
            DrawDot(aX+YA[i],aY+MaxHeight+XA[i],aColor);
           }
         aY+=WA[Pointer]+1;
        }
     }
  }
//+------------------------------------------------------------------+
//| Преобразует компоненты в цвет                                    |
//+------------------------------------------------------------------+
int RGB256(int aR,int aG,int aB)
  {
   return(aR+256*aG+65536*aB);
  }
//+------------------------------------------------------------------+
//| Рисует "X" или "0" в виде картинки                               |
//+------------------------------------------------------------------+
void ShowCell(int x,int y,uchar img)
  {
   uchar r,g,b;
   for(int i=0; i<8; i++)
     {
      for(int j=0; j<8; j++)
        {
         switch(img)
           {
            case 'X':
               r=Mask_X[3*(j*8+i)];
               g=Mask_X[3*(j*8+i)+1];
               b=Mask_X[3*(j*8+i)+2];
               break;
            case 'O':
               r=Mask_O[3*(j*8+i)];
               g=Mask_O[3*(j*8+i)+1];
               b=Mask_O[3*(j*8+i)+2];
               break;
           };
         int col=RGB256(r,g,b);
         bmp.DrawDot(x+i,y+j,col);
        }
     }
  }
//+------------------------------------------------------------------+

В зависимости от значения входного параметра pic результатом работы скрипта будут текстовые файлы с файлами картинок (каталог_данных_терминала\MQL5\Images) или только текстовые файлы (сохраняются в папке: каталог_данных_терминала\MQL5\Files).


Сравнение результатов

Для сравнения результатов построим график Light Crude Oil с параметрами: бокс - 1$, разворот - 3 бокса.

StockCharts.com:

Рис. 1. Результат построения графика крестики-нолики для Light Crude Oil программой StockCharts.com

Рис. 1. Результат построения графика "крестики-нолики" для Light Crude Oil программой StockCharts.com

Bull's-Eye Broker:

Рис. 2. Результат построения графика "крестики-нолики" для Light Crude Oil программой Bull's-Eye Broker

Рис. 2. Результат построения графика "крестики-нолики" для Light Crude Oil программой Bull's-Eye Broker


Результат работы нашего скрипта:

Рис. 3. Результат построения графика крестики-нолики для Light Crude Oil при помощи нашего скрипта

Рис. 3. Результат построения графика "крестики-нолики" для Light Crude Oil при помощи нашего скрипта

Все три графика идентичны, поздравим себя, мы освоили построение крестиков-ноликов.


Типичные паттерны графиков "крестики-нолики"

Как все это можно использовать?

Сначала рассмотрим типичные паттерны, благо, в крестиках-ноликах их все можно перечесть на пальцах.

Вот они:

Рис. 4. Ценовые паттерны Двойная вершина, Тройная вершина, Двойное основание и Тройное основание

Рис. 4. Ценовые паттерны "Двойная вершина", "Тройная вершина", "Двойное основание" и "Тройное основание"

далее:

Рис. 5. Ценовые паттерны Бычий треугольник и Медвежий треугольник

Рис. 5. Ценовые паттерны "Бычий треугольник" и "Медвежий треугольник"

и наконец:

Рис. 6. Ценовые паттерны Бычья катапульта и Медвежья катапульта

Рис. 6. Ценовые паттерны "Бычья катапульта" и "Медвежья катапульта"

Теперь несколько советов.

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

Остановлюсь подробнее на правиле вертикального счета на примере.

В декабре 2011 года колонка "Х", начавшаяся от цены 76$, пошла вверх, превысила предыдущую колонку "Х" на уровне 85$, пробила на 87$ сопротивление и дошла до 89$. Правило вертикального счета гласит, что при таких раскладах цена может пройти вверх до цены, равной 76$+(89$-75$)*3 (разворот 3 бокса)=118$.

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

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

Оценим возможные потери, они могут составить 90$-84$=6$ на один фьючерс. Возможная прибыль может составить 118$-90$=28$. Соотношение возможной прибыли к возможному убытку 28$/6$>4.5 раз. По-моему, удачный бизнес. Уже сейчас наша прибыль составляла бы 105$-90$=15$ на каждый фьючерс.


Лиценезии

Скрипт написан и распространяется на условиях BSD автором Roman Rich. Текст лицензии в файле Lic.txt. Библиотека cIntBMP написана Дмитрием aka Integer. Торговые марки StockCharts.com и Bull's-Eye Broker принадлежат соответствующим правообладателям.


Заключение

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