English 中文 Español Deutsch 日本語 Português
Индикатор для построения графика "шпинделей" (веретён)

Индикатор для построения графика "шпинделей" (веретён)

MetaTrader 5Примеры | 15 сентября 2015, 09:21
4 989 6
Dmitriy Zabudskiy
Dmitriy Zabudskiy

Введение

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

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

График "шпинделей" похож на график японских свечей, на нем также присутствуют цены открытия и закрытия, минимумы и максимумы. Однако в дополнение к этому используется средневзвешенная цена по объему (Volume Weighted Moving Average — VWMA) и коэффициент объема (Volume Ratio — VR), тем самым образуя фигуру, похожую на веретено (рис. 1).

Рис. 1. Сравнение "японской свечи" со "шпинделем"

Рис. 1. Сравнение "японской свечи" со "шпинделем"

Как мы видим из рисунка 1, два добавившихся параметра (VWMA — средневзвешенная цена по объему и VR — коэффициент объема) лишь дополняют свечу, образуя при этом новую фигуру, похожую на всем известную с детства юлу. Это и есть так называемый "шпиндель".

Рассмотрим, как образуются VR и VWMA. Средневзвешенная цена по объему (VWMA) представляет собой ни что иное, как своеобразную скользящую среднюю, и рассчитывается по формуле (1):

Расчёт VWMA

где P — цена, а V — объем. На словах это звучит примерно так: "средневзвешенная цена по объему равна сумме всех произведений цены на объем указанного периода, деленной на сумму объемов того же периода".

Коэффициент объема (VR) — это нечто вроде скользящей средней, но на графике он представляется иначе, так как, во-первых, имеет совсем не ценовое значение диапазона, во-вторых, он отвечает за активность рынка относительно предыдущих периодов, поэтому его лучше представить либо на отдельном графике, как тиковые объемы, либо в виде ширины каждого "шпинделя". Рассчитывается он по формуле (2):

Расчёт VR

где V — объем. Получается: "коэффициент объема равен текущему объему, деленному на среднее арифметическое объемов выбранного периода".

Итак, после всех таких манипуляций получается график "шпинделей" (рис. 2).

Рис. 2. График "шпинделей"

Рис. 2. График "шпинделей"

Естественно задаться вопросом: "почему на рисунке 2 "шпиндели" не заполнены цветом, как на рисунке 1 ?". Этот вопрос мы раскроем в следующей главе — Основы построения.


Основы построения

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

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

Визуальное представление "шпинделя" представлено на рисунке 1, при этом тело "шпинделя" полностью заливается. Такое построение довольно сложное и ресурсоемкое для реализации через объект "Рисунок". Рассмотрим, как это происходит, на рисунке 3:

Рис. 3. Техническое представление построения "шпинделя" при помощи объекта "Рисунок"

Рис. 3. Техническое представление построения "шпинделя" при помощи объекта "Рисунок"

На рисунке 3 представлено три возможных варианта технического представления "залитого шпинделя", где:

  • p — точки привязки объекта "Рисунок";
  • x — углы используемых рисунков для формирования картинки;
  • римскими цифрами указанны части формирования "шпинделя".

Итак, для построения первого "шпинделя" (рис. 3, a), назовем его "пухлый ромб", необходимо четыре объекта типа "Рисунок" (рис. 3, a; части: I, II, III, IV). При этом в зависимости от вида ромба, его ширины и высоты (т.е. Open, Close, VWMA и VR) нужны картинки на выбор под разным углом (рис. 3, a; углы: x1, x2, x3, x4). Картинка представляет собой квадратный точечный рисунок формата BMP, где из одного угла выходит луч под определенным углом относительно одной из ближайших сторон, который делит квадрат на две области: окрашенную и прозрачную.

Как именно происходит вычисление определенного рисунка, обсудим ниже. Однако уже видно, что для формирования данной модели при разных значениях ее ширины и высоты (т.е. Open, Close, VWMA и VR) понадобится 360 точечных рисунков (исходя из точности построения в один градус) из расчета на один цвет, а если брать два цвета, то это уже 720 рисунков.

С со вторым "шпинделем" (рис. 3, b) дела обстоят гораздо сложнее, несмотря на то, что формирование фигуры (назовем ее "острая стрелка") состоит из двух частей. Комбинаций углов здесь гораздо больше, так как нужно еще учитывать расстояние между ценами открытия и закрытия (т.е. Open, Close). Дальнейшее построение можно не рассматривать, ввиду наличия альтернативного (рис.3, c).

В третьем случае (рис.3, c) построение реализуется из четырех частей, первые две (I и II) такие же, как и у "пухлого ромба", вторые же две (III и IV) выполняют закрытие лишних частей от первых. При такой реализации есть вероятность перекрытия соседних "шпинделей", а также присутствует привязанность к фону. Итого 180 частей, таких как у "пухлого ромба", и 180 частей для закрытия.

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

Теперь рассмотрим менее сложный и более быстрый вариант построения с незалитыми "шпинделями" (рис. 2). Забегая вперед, количество точечных рисунков составило 360 (180 одного цвета и 180 другого) вне зависимости от фона графика.

Рис. 4. Техническое представление построения "незалитого шпинделя" при помощи объекта "Рисунок"

Рис. 4. Техническое представление построения "незалитого шпинделя" при помощи объекта "Рисунок"

Так же, как и в предыдущем варианте, "незалитый шпиндель" строится из четырех рисунков, которые представляют собой цветные полоски под разным углом (от 0 до 180). Здесь нет надобности строить 360 точечных рисунков, так как угол меняется в зависимости от точки привязки объекта. Точек привязки всего две (рис. 4, p1 и p2): два объекта на одну точку, два на другую.

Поясню еще раз, почему здесь используется меньше точечных рисунков. Представьте, что на рисунке 4 (а) — симметричный ромб, тогда часть I можно было бы заменить частью IV, для этого нужно было бы поменять точку привязки с верхнего правого угла на нижний левый угол объекта. В итоге нам нужно подготовить всего 180 объектов одного цвета и менять точку привязки в зависимости от стороны использования.

Теперь немного математики, а точнее — геометрии. Рассмотрим процесс расчета и выбора рисунка для построения "незалитого шпинделя" (рис. 5 и 6).

Рис. 5. Математический расчет "пухлого ромба"

Рис. 5. Математический расчет "пухлого ромба"

На рисунке 5 представлен уже знакомый нам "пухлый ромб" (а) и его правая часть (b). Все отмеченные расстояния (a, b, c, d) легко рассчитать, зная Open, Close, VWMA и VR, то есть:

  • a = Close - VWMA,
  • b = VWMA - Open,
  • c = Close - Open,
  • d = VR / 2.

Зная стороны a, b, d, можно рассчитать гипотенузы в прямоугольном треугольнике e и f по формулам 3.1 и 3.3. Соответственно, зная, что в прямоугольном треугольнике, катет, деленный на гипотенузу, равен синусу противолежащего угла, получаем синус угла x1 и x2 по формулам 3.2 и 3.4. Далее по таблице или при помощи калькулятора находим углы x1 и x2, а уже через x2 вычисляем x3. Такая же система построения и у фигуры "острая стрелка", рисунок 6:

Рис. 6. Математический расчет "острая стрелка"

Рис. 6. Математический расчет "острой стрелки"

Пройдя основы построения, разберем код индикатора.


Код индикатора

Перед тем как писать код, необходимо было подготовить графические ресурсы индикатора, а точнее — точечные рисунки формата BMP размером 540 х 540 пикселей с прозрачным фоном. Рисунки содержат луч, проходящий от угла. В первых 89 рисунках луч идет из верхнего левого угла, меняя угол от 1 до 89 градусов, во вторых 89 рисунках луч идет из нижнего левого угла от 91 до 179 градусов (относительно горизонтали — от 1 до 89 градусов). Рисунки с углами 0, 90, 180 имеют размер 1 х 540 пикселей и 540 х 1 соответственно и не нуждаются в прозрачном фоне.

Итого получается 181 рисунок одного цвета и 181 рисунок другого (рисунки 1 и 181 одинаковые) — это 362 рисунка. Названия файлов были выбраны, учитывая цвет линии, красный (первая латинская буква "r") и синий (первая латинская буква "b"), а также учитывая, под каким углом она расположена (0 - 180 градусов).


Первая часть

Первая часть кода доходит до функции OnInit. Рассмотрим по порядку:

  • Обозначение специфических параметров (#property), в данном случае — 11 буферов и 4 вида графических построений (одна гистограмма и три линии).
  • Включение ресурсов в исполняемый файл (#resource), здесь их много — как упоминалось ранее, 362 файла. Следует учитывать, что каждый файл нужно добавлять отдельной строкой, иначе он не прикрепится, ввиду этого большая часть строк в представленном ниже коде заменена на многоточие.
  • Следом идет меню входных параметров и используемые переменные, буферы.
//+------------------------------------------------------------------+
//|                                                          SPC.mq5 |
//|                                   Azotskiy Aktiniy ICQ:695710750 |
//|                          https://login.mql5.com/ru/users/aktiniy |
//+------------------------------------------------------------------+
#property copyright "Azotskiy Aktiniy ICQ:695710750"
#property link      "https://login.mql5.com/ru/users/aktiniy"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 11
#property indicator_plots 4
//---
#property indicator_label1  "Shadow"
#property indicator_type1   DRAW_COLOR_HISTOGRAM2
#property indicator_color1  clrRed,clrBlue,clrGray
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//---
#property indicator_label2  "Open"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrRed
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//---
#property indicator_label3  "Close"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrBlue
#property indicator_style3  STYLE_SOLID
#property indicator_width3  1
//---
#property indicator_label4  "VWMA"
#property indicator_type4   DRAW_LINE
#property indicator_color4  clrMagenta
#property indicator_style4  STYLE_SOLID
#property indicator_width4  1
//--- загружаем ресурсные файлы
#resource "\\Images\\for_SPC\\b0.bmp";
#resource "\\Images\\for_SPC\\b1.bmp";
#resource "\\Images\\for_SPC\\b2.bmp";
#resource "\\Images\\for_SPC\\b3.bmp";
//...
//...
//...
#resource "\\Images\\for_SPC\\b176.bmp";
#resource "\\Images\\for_SPC\\b177.bmp";
#resource "\\Images\\for_SPC\\b178.bmp";
#resource "\\Images\\for_SPC\\b179.bmp";
#resource "\\Images\\for_SPC\\b180.bmp";
#resource "\\Images\\for_SPC\\r0.bmp";
#resource "\\Images\\for_SPC\\r1.bmp";
#resource "\\Images\\for_SPC\\r2.bmp";
#resource "\\Images\\for_SPC\\r3.bmp";
//...
//...
//...
#resource "\\Images\\for_SPC\\r176.bmp";
#resource "\\Images\\for_SPC\\r177.bmp";
#resource "\\Images\\for_SPC\\r178.bmp";
#resource "\\Images\\for_SPC\\r179.bmp";
#resource "\\Images\\for_SPC\\r180.bmp";
//+------------------------------------------------------------------+
//| Type Drawing                                                     |
//+------------------------------------------------------------------+
enum type_drawing
  {
   spindles=0,       // Шпиндели
   line_histogram=1, // Линия и гистограмма
  };
//+------------------------------------------------------------------+
//| Type Price                                                       |
//+------------------------------------------------------------------+
enum type_price
  {
   open=0,   // Open
   high=1,   // High
   low=2,    // Low
   close=3,  // Close
   middle=4, // Middle
  };
//--- входные параметры
input long         magic_numb=65758473787389; // Магический номер
input type_drawing type_draw=0;               // Тип отрисовки индикатора
input int          period_VR=10;              // Период формирования коэффициента объема
input int          correct_VR=4;              // Число для корректировки коэффициента объема
input int          period_VWMA=10;            // Период формирования средневзвешенной цены по объему
input int          spindles_num=1000;         // Количество шпинделей
input type_price   type_price_VWMA=0;         // Вид цены для построения средневзвешенной цены по объему
                                              // open=0; high=1; low=2; close=3; middle=4
//--- выходные переменные
int ext_period_VR=0;
int ext_correct_VR;
int ext_period_VWMA=0;
int ext_spin_num=0;
int long_period=0;
//--- переменные параметра графика
double win_price_max_ext=0; // максимальное значение графика
double win_price_min_ext=0; // минимальное значение графика
double win_height_pixels_ext=0; // высота в пикселях
double win_width_pixels_ext=0;  // ширина в пикселях
double win_bars_ext=0; // ширина в барах
//--- вспомогательные переменные
int end_bar;
//--- индикаторные буферы
double         Buff_up[];   // буфер верхних точек гистограммы
double         Buff_down[]; // буфер нижних точек гистограммы
double         Buff_color_up_down[]; // буфер цвета гистограммы
double         Buff_open_ext[];  // выводной буфер цен открытия
double         Buff_close_ext[]; // выводной буфер цен закрытия
double         Buff_VWMA_ext[];  // выводной буфер средневзвешенной цены по объему
double         Buff_open[];  // буфер цен открытия
double         Buff_close[]; // буфер цен закрытия
double         Buff_VWMA[];  // буфер средневзвешенной цены по объему
double         Buff_VR[];   // буфер коэффициента объема
double         Buff_time[]; // буфер времени открытия бара

Здесь видно, что вводимых параметров немного:

  • Магический номер — вводится для различия индикаторов;
  • Тип отрисовки индикатора — можно классическим видом (шпиндели) или те же точки, только в виде линий;
  • Период формирования коэффициента объема — период для построения VR;
  • Число для корректировки коэффициента объема — так как объем (VR) влияет на ширину, его можно корректировать этим параметром;
  • Период формирования средневзвешенной цены по объему — период для построения VWMA;
  • Количество шпинделей — для уменьшения нагрузки на систему можно уменьшить количество отображаемых "шпинделей";
  • Вид цены для построения средневзвешенной цены по объему — выбор типа цены для построения VWMA.

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

Заключает первую часть кода объявление буферов индикатора, здесь их 11.


Функция OnInit

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- проверка входящих переменных
   if(period_VR<=0)
     {
      ext_period_VR=10; // изменение значения переменной
      Alert("Период формирования коэффициента объема введен некорректно и был изменен.");
     }
   else ext_period_VR=period_VR;
   if(correct_VR<=0)
     {
      ext_correct_VR=10; // изменение значения переменной
      Alert("Число для корректировки коэффициента объема введено некорректно и было изменено.");
     }
   else ext_correct_VR=correct_VR;
   if(period_VWMA<=0)
     {
      ext_period_VWMA=10; // изменение значения переменной
      Alert("Период формирования средневзвешенной цены по объему введен некорректно и был изменен.");
     }
   else ext_period_VWMA=period_VWMA;
   if(spindles_num<=0)
     {
      ext_spin_num=10; // изменение значения переменной
      Alert("Количество шпинделей введено некорректно и было изменено.");
     }
   else ext_spin_num=spindles_num;
//--- поиск самого длинного периода для построения графика
   if(ext_period_VR>ext_period_VWMA)long_period=ext_period_VR;
   else long_period=ext_period_VWMA;
//--- indicator buffers mapping
   SetIndexBuffer(0,Buff_up,INDICATOR_DATA);
   SetIndexBuffer(1,Buff_down,INDICATOR_DATA);
   SetIndexBuffer(2,Buff_color_up_down,INDICATOR_COLOR_INDEX);
   SetIndexBuffer(3,Buff_open_ext,INDICATOR_DATA);
   SetIndexBuffer(4,Buff_close_ext,INDICATOR_DATA);
   SetIndexBuffer(5,Buff_VWMA_ext,INDICATOR_DATA);
   SetIndexBuffer(6,Buff_open,INDICATOR_CALCULATIONS);
   SetIndexBuffer(7,Buff_close,INDICATOR_CALCULATIONS);
   SetIndexBuffer(8,Buff_VWMA,INDICATOR_CALCULATIONS);
   SetIndexBuffer(9,Buff_VR,INDICATOR_CALCULATIONS);
   SetIndexBuffer(10,Buff_time,INDICATOR_CALCULATIONS);
//--- установка имени индикатора
   IndicatorSetString(INDICATOR_SHORTNAME,"SPC "+IntegerToString(magic_numb));
   PlotIndexSetString(0,PLOT_LABEL,"SPC");
//--- установка точности
   IndicatorSetInteger(INDICATOR_DIGITS,_Digits+1);
//--- установка первого бара, с которого начнется рисование индикатора
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,long_period+1);
//--- запрещаем показ результатов текущих значений для индикатора
   PlotIndexSetInteger(0,PLOT_SHOW_DATA,false);
   PlotIndexSetInteger(1,PLOT_SHOW_DATA,false);
   PlotIndexSetInteger(2,PLOT_SHOW_DATA,false);
   PlotIndexSetInteger(3,PLOT_SHOW_DATA,false);
//--- устанавливаем значения, которые не будут отображаться
   PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0);
   PlotIndexSetDouble(2,PLOT_EMPTY_VALUE,0);
   PlotIndexSetDouble(3,PLOT_EMPTY_VALUE,0);
//--- создание используемых объектов
   if(type_draw==0)
     {
      for(int x=0; x<=ext_spin_num; x++)
        {
         ObjectCreate(0,"SPC"+IntegerToString(magic_numb)+IntegerToString(x)+"1",OBJ_BITMAP,ChartWindowFind(),__DATE__,0);
         ObjectCreate(0,"SPC"+IntegerToString(magic_numb)+IntegerToString(x)+"2",OBJ_BITMAP,ChartWindowFind(),__DATE__,0);
         ObjectCreate(0,"SPC"+IntegerToString(magic_numb)+IntegerToString(x)+"3",OBJ_BITMAP,ChartWindowFind(),__DATE__,0);
         ObjectCreate(0,"SPC"+IntegerToString(magic_numb)+IntegerToString(x)+"4",OBJ_BITMAP,ChartWindowFind(),__DATE__,0);
        }
     }
//---
   return(INIT_SUCCEEDED);
  }

Здесь проверяем корректность введенных параметров, при необходимости исправляем, используя ранее объявленные переменные (выходные переменные). Узнаем, какой из используемых периодов больше, инициализируем буферы и настраиваем внешний вид индикатора. Небольшим массивом (ограниченным параметром "количество шпинделей") создаем графические объекты для дальнейшей работы с ними.


Функция OnChartEvent

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- событие нажатия клавиши на клавиатуре
   if(id==CHARTEVENT_KEYDOWN)
     {
      if(lparam==82)
        {
         if(ChartGetDouble(0,CHART_PRICE_MAX,ChartWindowFind())>0)// проверяем наличие данных на графике
           {
            if(func_check_chart()==true)
              {
               if(type_draw==0)func_drawing(true,ext_spin_num,end_bar);
              }
           }
        }
     }
  }

Данная функция назначает на кнопку "R" (код 82) обновление графика, а точнее — его перерисовку. Служит она для того, чтобы произвести корректировку графика (перерисовать), если размеры окна индикатора изменились. Это связано с тем, что картинки вытягиваются в случае изменения размеров окна. Естественно, график перерисовывается и в случае наступления события по изменению цены, но иногда требуется быстро обновить построение, для этого и нужна эта функция.

Сама функция полностью состоит из условных операторов if-else и включает функцию проверки изменения размерности окна индикатора (func_check_chart), а также функцию формирования (рисования) графика (func_drawing).


Функция проверки окна индикатора

//+------------------------------------------------------------------+
//| Func Check Chart                                                 |
//+------------------------------------------------------------------+
bool func_check_chart()
  {
//--- ответная переменная
   bool x=false;
//--- узнаем размер графика
   int win=ChartWindowFind(); // определяем подокно, так как индикатор работает в отдельном окне
   double win_price_max=ChartGetDouble(0,CHART_PRICE_MAX,win); // максимальное значение графика
   double win_price_min=ChartGetDouble(0,CHART_PRICE_MIN,win); // минимальное значение графика
   double win_height_pixels=(double)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,win); // высота в пикселях
   double win_width_pixels=(double)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,win); // ширина в пикселях
   double win_bars=(double)ChartGetInteger(0,CHART_WIDTH_IN_BARS,win); // ширина в барах

//--- проверяем поменялись ли значения
   int factor=(int)MathPow(10,_Digits);// устанавливаем множитель для перевода типа double в тип int
   if(int(win_price_max*factor)!=int(win_price_max_ext*factor))
     {
      win_price_max_ext=win_price_max;
      x=true;
     }
   if(int(win_price_min*factor)!=int(win_price_min_ext*factor))
     {
      win_price_min_ext=win_price_min;
      x=true;
     }
   if(int(win_height_pixels*factor)!=int(win_height_pixels_ext*factor))
     {
      win_height_pixels_ext=win_height_pixels;
      x=true;
     }
   if(int(win_width_pixels*factor)!=int(win_width_pixels_ext*factor))
     {
      win_width_pixels_ext=win_width_pixels;
      x=true;
     }
   if(int(win_bars*factor)!=int(win_bars_ext*factor))
     {
      win_bars_ext=win_bars;
      x=true;
     }
   if(func_new_bar(PERIOD_CURRENT)==true)
     {
      x=true;
     }
   return(x);
  }

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


Функция управления графическим построением

//+------------------------------------------------------------------+
//| Func Drawing                                                     |
//+------------------------------------------------------------------+
void func_drawing(bool type_action,// вид действия модификации: 0-два последних, 1-все
                  int num,         // количество прорисовываемых шпинделей
                  int end_bar_now) // текущий последний бар
  {
   int begin;
   if(end_bar_now>num)begin=end_bar_now-num;
   else begin=long_period+1;
//--- поиск максимального значения VR   
   double VR_max=0;
   for(int x=begin; x<end_bar_now-1; x++)
     {
      if(Buff_VR[x]<Buff_VR[x+1])VR_max=Buff_VR[x+1];
      else VR_max=Buff_VR[x];
     }
//--- расчет масштаба
   double scale_height=win_height_pixels_ext/(win_price_max_ext-win_price_min_ext);
   double scale_width=win_width_pixels_ext/win_bars_ext;
//--- построение (x-часть названия объекта, y-индекс массива данных для построения)
   if(type_action==false)// ложь - обновление двух последних шпинделей
     {
      for(int x=num-2,y=end_bar_now-2; y<end_bar_now; y++,x++)
        {
         func_picture("SPC"+IntegerToString(magic_numb)+IntegerToString(x),Buff_open[y],Buff_close[y],datetime(Buff_time[y]),Buff_VR[y],VR_max,Buff_VWMA[y],ext_correct_VR,scale_height,scale_width);
        }
     }
//---
   if(type_action==true)// истина - обновление всех шпинделей
     {
      for(int x=0,y=begin; y<end_bar_now; y++,x++)
        {
         func_picture("SPC"+IntegerToString(magic_numb)+IntegerToString(x),Buff_open[y],Buff_close[y],datetime(Buff_time[y]),Buff_VR[y],VR_max,Buff_VWMA[y],ext_correct_VR,scale_height,scale_width);
        }
     }
   ChartRedraw();
  }

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

Далее происходит расчет, с какого бара начать. Если информации о барах меньше, чем количество "шпинделей", тогда начало построения берется по самому большому периоду формирования графика.

Затем находим самый большой коэффициент объема (нужен как передаваемый параметр функции func_picture, рассматриваемой ниже) и рассчитываем масштаб для построения графика. В зависимости от параметра модификации вызываем цикл модификации "шпинделей" (ранее созданных графических объектов, при помощи функции func_picture).


Функция графического построения

//+------------------------------------------------------------------+
//| Func Picture                                                     |
//+------------------------------------------------------------------+
void func_picture(string name,        // имя объекта
                  double open,        // цена открытия бара
                  double close,       // цена закрытия бара
                  datetime time,      // время бара
                  double VR,          // значение коэффициента объема
                  double VR_maximum,  // максимальное значение коэффициента объема
                  double VWMA,        // значение средневзвешенной цены по объему
                  int correct,        // параметр корректировки коэффициента объема при отображении
                  double scale_height,// масштаб высоты (пиксели/цена)
                  double scale_width) // масштаб ширины (пиксели/бары)
  {
   string first_name;// первая буква названия файла, используемого в построении
   string second_name_right;// остальное название файла, используемого в построении справа
   string second_name_left; // остальное название файла, используемого в построении слева
   double cathetus_a;// катет a
   double cathetus_b;// катет b
   double hypotenuse;// гипотенуза
   int corner;// угол
//--- находим "углы" открытия и закрытия бара
   cathetus_b=int(VR/VR_maximum/correct*scale_width);// ширина в пикселях
                                                     //picture 540
   if(open<=close) first_name="r";// восходящий бар или доджи
   if(open>close) first_name="b"; // нисходящий бар
//---
   if(open<VWMA)// VWMA находится выше цены открытия
     {
      cathetus_a=int((VWMA-open)*scale_height);
      hypotenuse=MathCeil(MathSqrt(MathPow(cathetus_a,2)+MathPow(cathetus_b,2)));
      if(hypotenuse<=0) hypotenuse=1;
      corner=int(180-(MathArcsin(cathetus_b/hypotenuse)*360/(M_PI*2)));
      second_name_right=IntegerToString(corner);
      second_name_left=IntegerToString(180-corner);
      func_obj_mod(name+"1","::Images\\for_SPC\\"+first_name+second_name_right+".bmp",int(cathetus_b+1),int(cathetus_a+1),540-int(cathetus_a+2),ANCHOR_LEFT_LOWER,time,open);
      func_obj_mod(name+"2","::Images\\for_SPC\\"+first_name+second_name_left+".bmp",int(cathetus_b+1),int(cathetus_a+1),0,ANCHOR_RIGHT_LOWER,time,open);
     }
   if(open>VWMA)// VWMA находится ниже цены открытия
     {
      cathetus_a=int((open-VWMA)*scale_height);
      hypotenuse=MathCeil(MathSqrt(MathPow(cathetus_a,2)+MathPow(cathetus_b,2)));
      if(hypotenuse<=0) hypotenuse=1;
      corner=int((MathArcsin(cathetus_b/hypotenuse)*360/(M_PI*2)));
      second_name_right=IntegerToString(corner);
      second_name_left=IntegerToString(180-corner);
      func_obj_mod(name+"1","::Images\\for_SPC\\"+first_name+second_name_right+".bmp",int(cathetus_b+1),int(cathetus_a+1),0,ANCHOR_LEFT_UPPER,time,open);
      func_obj_mod(name+"2","::Images\\for_SPC\\"+first_name+second_name_left+".bmp",int(cathetus_b+1),int(cathetus_a+1),540-int(cathetus_a+2),ANCHOR_RIGHT_UPPER,time,open);
     }
   if(open==VWMA)// VWMA находится на уровне цены открытия
     {
      func_obj_mod(name+"1","::Images\\for_SPC\\"+first_name+"90"+".bmp",int(cathetus_b+1),2,0,ANCHOR_LEFT,time,open);
      func_obj_mod(name+"2","::Images\\for_SPC\\"+first_name+"90"+".bmp",int(cathetus_b+1),2,0,ANCHOR_RIGHT,time,open);
     }
   if(close<VWMA)// VWMA находится выше цены закрытия
     {
      cathetus_a=int((VWMA-close)*scale_height);
      hypotenuse=MathCeil(MathSqrt(MathPow(cathetus_a,2)+MathPow(cathetus_b,2)));
      if(hypotenuse<=0) hypotenuse=1;
      corner=int(180-(MathArcsin(cathetus_b/hypotenuse)*360/(M_PI*2)));
      second_name_right=IntegerToString(corner);
      second_name_left=IntegerToString(180-corner);
      func_obj_mod(name+"3","::Images\\for_SPC\\"+first_name+second_name_right+".bmp",int(cathetus_b+1),int(cathetus_a+1),540-int(cathetus_a+2),ANCHOR_LEFT_LOWER,time,close);
      func_obj_mod(name+"4","::Images\\for_SPC\\"+first_name+second_name_left+".bmp",int(cathetus_b+1),int(cathetus_a+1),0,ANCHOR_RIGHT_LOWER,time,close);
     }
   if(close>VWMA)// VWMA находится ниже цены закрытия
     {
      cathetus_a=int((close-VWMA)*scale_height);
      hypotenuse=MathCeil(MathSqrt(MathPow(cathetus_a,2)+MathPow(cathetus_b,2)));
      if(hypotenuse<=0) hypotenuse=1;
      corner=int((MathArcsin(cathetus_b/hypotenuse)*360/(M_PI*2)));
      second_name_right=IntegerToString(corner);
      second_name_left=IntegerToString(180-corner);
      func_obj_mod(name+"3","::Images\\for_SPC\\"+first_name+second_name_right+".bmp",int(cathetus_b+1),int(cathetus_a+1),0,ANCHOR_LEFT_UPPER,time,close);
      func_obj_mod(name+"4","::Images\\for_SPC\\"+first_name+second_name_left+".bmp",int(cathetus_b+1),int(cathetus_a+1),540-int(cathetus_a+2),ANCHOR_RIGHT_UPPER,time,close);
     }
   if(close==VWMA)// VWMA находится на уровне цены закрытия
     {
      func_obj_mod(name+"3","::Images\\for_SPC\\"+first_name+"90"+".bmp",int(cathetus_b+1),2,0,ANCHOR_LEFT,time,close);
      func_obj_mod(name+"4","::Images\\for_SPC\\"+first_name+"90"+".bmp",int(cathetus_b+1),2,0,ANCHOR_RIGHT,time,close);
     }
  }

"Сердце" графического построения — функция расчета и замены картинок графических объектов. Именно в этой функции происходит расчет используемой на том или ином баре картинки (точнее четырех картинок) для построения "шпинделя". Затем при помощи функции func_obj_mod меняется картинка графического объекта (все графические объекты были созданы в самом начале кода, в конце функции OnInit).

Функции передаются параметры текущего модифицируемого бара, здесь же присутствует упомянутый ранее максимальный коэффициент объема, служащий как некий относительный параметр для расчета так называемого катета b (рис. 5, b; отмечен как размер d).

Далее вводятся вспомогательные переменные для расчета (первая буква — цвет, остальное название файла левой и правой стороны — угол в названии файла, катет a и b, гипотенуза и угол), определяется цвет "шпинделя" условным оператором if. Затем, в зависимости от уровня цены открытия и закрытия относительно средневзвешенной цены по объему (WVMA), происходит расчет (четырех рисунков) по известным формулам из рисунков 5 и 6, а также модификация графического объекта при помощи функции func_obj_mod.


Функция модификации объекта

//+------------------------------------------------------------------+
//| Func Obj Mod                                                     |
//+------------------------------------------------------------------+
void func_obj_mod(string name,             // имя объекта
                  string file,             // путь к ресурсу файла
                  int pix_x_b,             // видимость по X
                  int pix_y_a,             // видимость по Y
                  int shift_y,             // сдвиг по Y
                  ENUM_ANCHOR_POINT anchor,// точка привязки
                  datetime time,           // координата времени
                  double price)            // координата цены
  {
   ObjectSetString(0,name,OBJPROP_BMPFILE,file);
   ObjectSetInteger(0,name,OBJPROP_XSIZE,pix_x_b);// видимость по X
   ObjectSetInteger(0,name,OBJPROP_YSIZE,pix_y_a);// видимость по Y
   ObjectSetInteger(0,name,OBJPROP_XOFFSET,0);// без сдвига по оси X
   ObjectSetInteger(0,name,OBJPROP_YOFFSET,shift_y);// устанавливаем сдвиг по оси Y
   ObjectSetInteger(0,name,OBJPROP_BACK,false);// отобразим на переднем плане
   ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);// отключаем режим перемещения
   ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
   ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);// скроем имя графического объекта
   ObjectSetInteger(0,name,OBJPROP_ANCHOR,anchor);// устанавливаем точку привязки
   ObjectSetInteger(0,name,OBJPROP_TIME,time);// устанавливаем координату времени
   ObjectSetDouble(0,name,OBJPROP_PRICE,price);// устанавливаем координату цены
  }

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


Функция OnCalculate

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//--- провека наличия истории по периоду
   if(rates_total<long_period)
     {
      Alert("Период VR или VWMA больше, чем исторические данные, или исторические данные не загружены.");
      return(0);
     }
//--- поиск позиции
   int position=prev_calculated-1;
   if(position<long_period)position=long_period; // изменение позиции
//--- главный цикл расчета буферов
   for(int i=position; i<rates_total; i++)
     {
      //--- заполняем буферы гистограммы
      Buff_up[i]=high[i];
      Buff_down[i]=low[i];
      if(open[i]<close[i])Buff_color_up_down[i]=0;// восходящий бар
      if(open[i]>close[i])Buff_color_up_down[i]=1;// нисходящий бар
      if(open[i]==close[i])Buff_color_up_down[i]=2;// доджи бар
      //--- заполняем вспомогательные буферы
      Buff_open[i]=open[i];
      Buff_close[i]=close[i];
      Buff_time[i]=double(time[i]);
      //--- производим расчет коэффициента объема
      double mid_vol=0;
      int x=0;
      for(x=i-ext_period_VR; x<=i; x++)
        {
         mid_vol+=double(tick_volume[x]);
        }
      mid_vol/=x;
      Buff_VR[i]=tick_volume[i]/mid_vol; // подсчет VR
      //--- производим расчет средневзвешенной цены по объему
      long vol=0;
      double price_vol=0;
      x=0;
      switch(type_price_VWMA)
        {
         case 0:
           {
            for(x=i-ext_period_VWMA; x<=i; x++)
              {
               price_vol+=double(open[x]*tick_volume[x]);
               vol+=tick_volume[x];
              }
           }
         break;
         //---
         case 1:
           {
            for(x=i-ext_period_VWMA; x<=i; x++)
              {
               price_vol+=double(high[x]*tick_volume[x]);
               vol+=tick_volume[x];
              }
           }
         break;
         //---
         case 2:
           {
            for(x=i-ext_period_VWMA; x<=i; x++)
              {
               price_vol+=double(low[x]*tick_volume[x]);
               vol+=tick_volume[x];
              }
           }
         break;
         //---
         case 3:
           {
            for(x=i-ext_period_VWMA; x<=i; x++)
              {
               price_vol+=double(close[x]*tick_volume[x]);
               vol+=tick_volume[x];
              }
           }
         break;
         //---
         case 4:
           {
            for(x=i-ext_period_VWMA; x<=i; x++)
              {
               double price=(open[x]+high[x]+low[x]+close[x])/4;
               price_vol+=double(price*tick_volume[x]);
               vol+=tick_volume[x];
              }
           }
         break;
        }
      Buff_VWMA[i]=price_vol/vol; // подсчет VWMA
      //---
      if(type_draw==1)
        {
         Buff_open_ext[i]=Buff_open[i];
         Buff_close_ext[i]=Buff_close[i];
         Buff_VWMA_ext[i]=Buff_VWMA[i];
        }
      else
        {
         //--- уменьшаем размер неиспользуемых массивов
         ArrayResize(Buff_open_ext,1);
         ArrayResize(Buff_close_ext,1);
         ArrayResize(Buff_VWMA_ext,1);
         //--- обнуляем неиспользуемые массивы
         ZeroMemory(Buff_open_ext);
         ZeroMemory(Buff_close_ext);
         ZeroMemory(Buff_VWMA_ext);
        }
     }
   end_bar=rates_total;// определяем номер последнего бара
//---
   if(ChartGetDouble(0,CHART_PRICE_MAX,ChartWindowFind())>0 && type_draw==0)// проверяем наличие данных в окне индикатора для начала построения
     {
      func_drawing(func_check_chart(),ext_spin_num,end_bar);
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }

Стандартная функция индикатора производит расчет и заполнение буферов данными. Вначале происходит проверка периода VR и WVMA и данных по барам, в случае несоответствия выводится сообщение. Далее находится позиция, с которой нужно начинать заполнение буферов. Производится заполнение буфера "гистограммы", обозначающей наивысшую и наименьшую цены. Следом рассчитываются и заполняются буферы "коэффициент объема" (VR) и "средневзвешенная цена по объему" (WVMA) по формулам 1 и 2 (обозначенных в главе Введение).


Остальные функции

Для более корректной работы индикатора также присутствует функция определения нового бара func_new_bar и функция деинициализации индикатора OnDeinit.

Функция func_new_bar определяет появление нового бара на графике и служит вспомогательной в функции func_check_chart.

//+------------------------------------------------------------------+
//| Func New Bar                                                     |
//+------------------------------------------------------------------+
bool func_new_bar(ENUM_TIMEFRAMES period_time)
  {
   static datetime old_times; // переменная хранения старых значений
   bool res=false;            // переменная результата анализа  
   datetime new_time[1];      // время нового бара
   int copied=CopyTime(_Symbol,period_time,0,1,new_time); // скопируем время последнего бара в ячейку new_time  
   if(copied>0) // данные скопированы
     {
      if(old_times!=new_time[0]) // если старое время бара не равно новому
        {
         if(old_times!=0) res=true; // если это не первый запуск, то истина = новый бар
         old_times=new_time[0];     // запоминаем время бара
        }
     }
   return(res);
  }
//+------------------------------------------------------------------+

Данная функция была представлена и в предыдущих статьях, так что в комментариях не нуждается.

Функция OnDeinit служит для удаления графических объектов, ранее созданных в функции OnInit. Функция является стандартной для индикатора и вызывается при удалении индикатора с графика.

//+------------------------------------------------------------------+
//| OnDeinit                                                         |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- удаление используемых объектов
   if(type_draw==0)
     {
      for(int x=0; x<=ext_spin_num; x++)
        {
         ObjectDelete(0,"SPC"+IntegerToString(magic_numb)+IntegerToString(x)+"1");
         ObjectDelete(0,"SPC"+IntegerToString(magic_numb)+IntegerToString(x)+"2");
         ObjectDelete(0,"SPC"+IntegerToString(magic_numb)+IntegerToString(x)+"3");
         ObjectDelete(0,"SPC"+IntegerToString(magic_numb)+IntegerToString(x)+"4");
        }
     }
  }

На этом код индикатора закончен, по каким-либо определениям и вопросам прошу обращаться в комментариях к статье или в личных сообщениях.


Эксперт и торговая стратегия

Перед тем как рассматривать торговую стратегию, проверим, как будет работать советник на данном индикаторе. Проверку будем осуществлять на советнике, который работает, используя для анализа своих действий всего один "шпиндель". При этом не используется VR (коэффициент объема). Получается, что анализ будет происходит на своего рода паттернах, состоящих из одного "шпинделя". Всего таких вариантов приходится порядка 30, рассмотрим подробнее на рисунке 7:

Рис. 7. Возможные образования "шпинделей"

Рис. 7. Возможные образования "шпинделей"

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

Предположим, первое отличие "шпинделей" — это их цвет, а точнее — восходящий или нисходящий рынок в рассматриваемый период (рис. 7, колонка 1). На рисунке 7, в первой колонке, (0) — восходящий (красный) и (1) — нисходящий (синий). Перейдя на следующую колонку, видим различие в теле B (цена открытия и закрытия) относительно тени S (наивысшая и наименьшая цены за период). Это различие в данном примере разделено всего на три части (рис. 7, колонка 2). Третья колонка учитывает в сравнении уровень VWMA (средневзвешенной цены по объему) к уровню наивысшей и наименьшей цены (High и Low). Он может находиться выше (1), ниже (2) и между (3) наивысшей и наименьшей ценой. В третьей колонке "шпиндель" (3) может отличаться еще и ценой открытия (Open) и закрытия (Close) периода относительно VWMA, тем самым на рисунке 7 образуется еще колонка 3-3 (производная от колонки 3 (3)).

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

Все цифры на рисунке 7 назначены, учитывая ответы функций в торговом эксперте, код которого рассмотрен ниже.


Параметры эксперта

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

//+------------------------------------------------------------------+
//|                                                        EASPC.mq5 |
//|                                   Azotskiy Aktiniy ICQ:695710750 |
//|                          https://login.mql5.com/ru/users/aktiniy |
//+------------------------------------------------------------------+
#property copyright "Azotskiy Aktiniy ICQ:695710750"
#property link      "https://login.mql5.com/ru/users/aktiniy"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Type Drawing                                                     |
//+------------------------------------------------------------------+
enum type_drawing
  {
   spindles=0,       // Шпиндели
   line_histogram=1, // Линия и гистограмма
  };
//+------------------------------------------------------------------+
//| Type Price                                                       |
//+------------------------------------------------------------------+
enum type_price
  {
   open=0,   // Open
   high=1,   // High
   low=2,    // Low
   close=3,  // Close
   middle=4, // Middle
  };
//--- input parameters
input long         magic_numb=65758473787389; // Магический номер
input type_drawing type_draw=1;               // Тип отрисовки индикатора
input int          period_VR=10;              // Период формирования коэффициента объема
input int          correct_VR=4;              // Число для корректировки коэффициента объема
input int          period_VWMA=10;            // Период формирования средневзвешенной цены по объему
input int          spindles_num=10;           // Количество шпинделей
input type_price   type_price_VWMA=0;         // Вид цены для построения средневзвешенной цены по объему
                                              // open=0; high=1; low=2; close=3; middle=4
input double lot=0.01;                        // Размер лота
input int    stop=1000;                       // Стоп Лосс
//---
input char   p1=1;                            // Действия на паттернах 1-купить, 2-продать, 3-закрыть позицию, 4-ничего не делать
input char   p2=1;
input char   p3=1;
input char   p4=1;
input char   p5=1;
input char   p6=1;
input char   p7=1;
input char   p8=1;
input char   p9=1;
input char   p10=1;
input char   p11=1;
input char   p12=1;
input char   p13=1;
input char   p14=1;
input char   p15=1;
input char   p16=1;
input char   p17=1;
input char   p18=1;
input char   p19=1;
input char   p20=1;
input char   p21=1;
input char   p22=1;
input char   p23=1;
input char   p24=1;
input char   p25=1;
input char   p26=1;
input char   p27=1;
input char   p28=1;
input char   p29=1;
input char   p30=1;
//---
int handle_SPC; // хендл индикатора
long position_type; // тип позиции
//--- буферы копируемых значений индикатора
double         Buff_up[3]; // буфер верхних точек гистограммы
double         Buff_down[3]; // буфер нижних точек гистограммы
double         Buff_color_up_down[3]; // буфер цвета гистограммы
double         Buff_open_ext[3]; // буфер цен открытия
double         Buff_close_ext[3]; // буфер цен закрытия
double         Buff_VWMA_ext[3]; // буфер средневзвешенной цены по объему
Функция OnInit, в ней происходит инициализация хендла индикатора.
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   handle_SPC=iCustom(_Symbol,PERIOD_CURRENT,"SPC.ex5",magic_numb,type_draw,period_VR,correct_VR,period_VWMA,spindles_num,type_price_VWMA);
//---
   return(INIT_SUCCEEDED);
  }


Функции отправки ордеров на сервер

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

//+------------------------------------------------------------------+
//| Func Send Order                                                  |
//+------------------------------------------------------------------+
bool func_send_order(ENUM_ORDER_TYPE type_order,// тип выставляемого ордера
                     double volume)             // объем сделки лота
  {
   bool x=false; // переменная для ответа
//--- вводим переменные для отправки ордера
   MqlTradeRequest order_request={0};
   MqlTradeResult order_result={0};
//--- заполняем переменную для отправки ордера
   order_request.action=TRADE_ACTION_DEAL;
   order_request.deviation=3;
   order_request.magic=555;
   order_request.symbol=_Symbol;
   order_request.type=type_order;
   order_request.type_filling=ORDER_FILLING_FOK;
   order_request.volume=volume;
   if(type_order==ORDER_TYPE_BUY)
     {
      order_request.price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      order_request.sl=order_request.price-(_Point*stop);
     }
   if(type_order==ORDER_TYPE_SELL)
     {
      order_request.price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
      order_request.sl=order_request.price+(_Point*stop);
     }
//--- отсылаем ордер
   bool y=OrderSend(order_request,order_result);
   if(y!=true)Alert("Ошибка отправки ордера.");
//--- проверяем результат
   if(order_result.retcode==10008 || order_result.retcode==10009) x=true;
   return(x);
  }
//+------------------------------------------------------------------+
//| Func Delete Position                                             |
//+------------------------------------------------------------------+
bool func_delete_position()
  {
   bool x=false;
//--- отмечаем позицию, с которой будем работать
   PositionSelect(_Symbol);
   double vol=PositionGetDouble(POSITION_VOLUME);
   long type=PositionGetInteger(POSITION_TYPE);
   ENUM_ORDER_TYPE type_order;
   if(type==POSITION_TYPE_BUY)type_order=ORDER_TYPE_SELL;
   else type_order=ORDER_TYPE_BUY;
//--- вводим переменные для отправки ордера
   MqlTradeRequest order_request={0};
   MqlTradeResult order_result={0};
//--- заполняем переменную для отправки ордера
   order_request.action=TRADE_ACTION_DEAL;
   order_request.deviation=3;
   order_request.magic=555;
   order_request.symbol=_Symbol;
   order_request.type=type_order;
   order_request.type_filling=ORDER_FILLING_FOK;
   order_request.volume=vol;
   if(type_order==ORDER_TYPE_BUY)order_request.price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   if(type_order==ORDER_TYPE_SELL)order_request.price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
//--- отсылаем ордер
   bool y=OrderSend(order_request,order_result);
   if(y!=true)Alert("Ошибка отправки ордера.");
//--- проверяем результат
   if(order_result.retcode==10008 || order_result.retcode==10009) x=true;
   return(x);
  }

В коде также присутствует вспомогательная функция func_new_bar, которая определяет появление нового бара на графике. Она описана выше и не требует публикации кода.

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

В функции OnTick происходит, собственно, консолидация всех действий. Для начала с помощью функции CopyBuffer заполняются буферы, используемые для расчетов. Затем проверяется, есть ли какая-либо позиция на текущем инструменте, это нужно для другой функции управления выставлением и удалением ордеров (позиций). После подготавливаются размеры для определения паттерна, это тело — расстояние между ценой открытия и закрытия, тень — расстояние между наибольшей и наименьшей ценами за текущий период, а также находится их отношение, которое в дальнейшем передается функции func_one (рис. 7, колонка 2).

Далее задействуются функции func_two и func_three, на рисунке 7 это колонки 3 и 3-3 соответственно. После этого проверяем оператором-переключателем switch цвет "шпинделя", согласно рисунку 7, колонка 1. Таким образом, получаем дерево решения по функциям, когда следующий оператор-переключатель switch в зависимости от значения переменной afun_1_1 переключает на функцию func_pre_work (рассмотрим далее) в соответствии с колонкой 2 рисунка 7 на основании размера тела и тени.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(func_new_bar(PERIOD_CURRENT)==true)
     {
      //--- скопируем буферы индикатора
      CopyBuffer(handle_SPC,0,1,3,Buff_up);
      CopyBuffer(handle_SPC,1,1,3,Buff_down);
      CopyBuffer(handle_SPC,2,1,3,Buff_color_up_down);
      CopyBuffer(handle_SPC,3,1,3,Buff_open_ext);
      CopyBuffer(handle_SPC,4,1,3,Buff_close_ext);
      CopyBuffer(handle_SPC,5,1,3,Buff_VWMA_ext);
      //--- анализируем ситуацию
      //--- проверяем наличие установленного ордера
      if(PositionSelect(_Symbol)==true)
        {
         position_type=PositionGetInteger(POSITION_TYPE); // BUY=0, SELL=1
        }
      else
        {
         position_type=-1; // нет позиции по инструменту
        }
      //--- подготовка величин для сравнения
      double body=Buff_open_ext[2]-Buff_close_ext[2];
      body=MathAbs(body);
      double shadow=Buff_up[2]-Buff_down[2];
      shadow=MathAbs(shadow);
      if(shadow==0)shadow=1;// предотвращаем деление на ноль
      double body_shadow=body/shadow;
      //--- переменные ответа функций
      char afun_1_1=func_one(body_shadow);
      char afun_2_1=func_two(Buff_up[2],Buff_down[2],Buff_VWMA_ext[2]);
      char afun_3_1=func_three(Buff_open_ext[2],Buff_close_ext[2],Buff_VWMA_ext[2]);
      //---
      switch(int(Buff_color_up_down[2]))
        {
         case 0:
           {
            switch(afun_1_1)
              {
               case 1:
                  func_pre_work(afun_2_1,afun_3_1,p1,p2,p3,p4,p5);
                  break;
               case 2:
                  func_pre_work(afun_2_1,afun_3_1,p6,p7,p8,p9,p10);
                  break;
               case 3:
                  func_pre_work(afun_2_1,afun_3_1,p11,p12,p13,p14,p15);
                  break;
              }
           }
         break;
         case 1:
           {
            switch(afun_1_1)
              {
               case 1:
                  func_pre_work(afun_2_1,afun_3_1,p16,p17,p18,p19,p20);
                  break;
               case 2:
                  func_pre_work(afun_2_1,afun_3_1,p21,p22,p23,p24,p25);
                  break;
               case 3:
                  func_pre_work(afun_2_1,afun_3_1,p26,p27,p28,p29,p30);
                  break;
              }
           }
         break;
        }
     }
  }

Функция func_pre_work продолжает разветвление уже сформировавшегося дерева функций, получается программная реализация кода на основании рисунка 7, колонок 3 (переменная f_2) и 3-3 (переменная f_3), переключение производится на последнюю функцию из дерева — func_work.

//+------------------------------------------------------------------+
//| Func Pre Work                                                    |
//+------------------------------------------------------------------+
void func_pre_work(char f_2,     // результат функции Func Two
                   char f_3,     // результат функции Func Three
                   char pat_1,   // паттерн 1
                   char pat_2,   // паттерн 2
                   char pat_3_1, // паттерн 3_1
                   char pat_3_2, // паттерн 3_2
                   char pat_3_3) // паттерн 3_3
  {
   switch(f_2)
     {
      case 1: //1
         func_work(pat_1);
         break;
      case 2: //2
         func_work(pat_2);
         break;
      case 3:
        {
         switch(f_3)
           {
            case 1: //3_1
               func_work(pat_3_1);
               break;
            case 2: //3_2
               func_work(pat_3_2);
               break;
            case 3: //3_3
               func_work(pat_3_3);
               break;
           }
        }
      break;
     }
  }

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

//+------------------------------------------------------------------+
//| Func Work                                                        |
//+------------------------------------------------------------------+
void func_work(char pattern)
  {
   switch(pattern)
     {
      case 1: // купить
         if(position_type!=-1)func_delete_position();
         func_send_order(ORDER_TYPE_BUY,lot);
         break;
      case 2: // продать
         if(position_type!=-1)func_delete_position();
         func_send_order(ORDER_TYPE_SELL,lot);
         break;
      case 3: // закрыть позицию
         if(position_type!=-1)func_delete_position();
         break;
      case 4: // ничего не делать
         break;
     }
  }

Остались последние упомянутые ранее функции: func_one, func_two и func_three. Они служат для преобразования переданных данных в виде ценовых значений в целочисленные данные для оператора-переключателя switch. Если вернуться к рисунку 7, то func_one — это реализация колонки 2, func_two — колонки 3 и func_three — колонки 3-3. Возвращаемыми значениями этих функций являются целые числа 1, 2 и 3, которые также соответствуют цифрам из рисунка 7.

//+------------------------------------------------------------------+
//| Func One                                                         |
//+------------------------------------------------------------------+
char func_one(double body_shadow_in)
  {
   char x=0; // переменная для ответа
   if(body_shadow_in<=(double(1)/double(3))) x=1;
   if(body_shadow_in>(double(1)/double(3)) && body_shadow_in<=(double(2)/double(3))) x=2;
   if(body_shadow_in>(double(2)/double(3)) && body_shadow_in<=1) x=3;
   return(x);
  }
//+------------------------------------------------------------------+
//| Func Two                                                         |
//+------------------------------------------------------------------+
char func_two(double up,// high [Buff_up]
              double down,// low [Buff_down]
              double VWMA) // VWMA [Buff_VWMA_ext]
  {
   char x=0; // переменная для ответа
   if(VWMA>=up) x=1;
   if(VWMA<=down) x=2;
   else x=3;
   return(x);
  }
//+------------------------------------------------------------------+
//| Func Three                                                       |
//+------------------------------------------------------------------+
char func_three(double open,// open [Buff_open_ext]
                double close,// close [Buff_close_ext]
                double VWMA) // VWMA [Buff_VWMA_ext]
  {
   char x=0; // переменная для ответа
   if(open>=VWMA && close>=VWMA) x=1;
   if(open<=VWMA && close<=VWMA) x=2;
   else x=3;
   return(x);
  }
//+------------------------------------------------------------------+

Теперь, когда советник готов к работе, испытаем его. Для начала определимся с параметрами:

  • символ и таймфрейм — EURUSD, H1;
  • период тестирования — с 01.01.2013 по 01.01.2015 (2 года);
  • Stop Loss — 1000;
  • сервер — MetaQuotes-Demo.

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

Настройки Тестера стратегий изображены на рисунке 8:

Рис. 8. Настройки тестера стратегий

Рис. 8. Настройки тестера стратегий

Оптимизировать будем, как было сказано ранее, по действиям в зависимости от паттерна и по периоду VWMA в диапазоне от 10 до 500 баров, рисунок 9:

Рис. 9. Параметры оптимизации

Рис. 9. Параметры оптимизации

В ходе оптимизации получаем график, рисунок 10:

Рис. 10. График оптимизации

Рис. 10. График оптимизации

В результате оптимизации получаем прибыль 138.71, учитывая размер лота 0.01 и начальный депозит 10000, при просадке 2.74% (примерно 28 условных единиц), рисунок 11:

Рис. 11. Результаты оптимизации

Рис. 11. Результаты оптимизации

Увеличим размер лота до 0.1, уменьшим начальный депозит до 1000 и проведем повторное тестирование, используя полученные в результате оптимизации параметры. При этом изменим режим торговли на OHLC на M1 для увеличения точности тестирования, получаем рисунок 12:

Рис. 12. Результат тестирования (бэктест)

Рис. 12. Результат тестирования (бэктест)

В результате за два года было совершено 742 трейда (примерно 3 трейда в день), при этом максимальная просадка составила 252, а чистая прибыль — 1407, примерно 60 (6% от суммы вложения) в месяц. Теоретически все получается довольно красиво, но не факт, что так же красиво будет на практике.

Естественно, этот эксперт нужно дальше модернизировать и улучшать, возможно, ввести еще дополнительный "шпиндель" в паттерн и добавить уровень VR. Это тема для дальнейших размышлений, но даже на столь малых параметрах эксперт на данном индикаторе показал довольно интересные результаты.

Торговая стратегия при работе с индикатором довольно проста — нужно покупать, когда стрелка указывает вверх, и продавать, когда вниз. Ромб — это нечто вроде "доджи", указывает на разворот. Это хорошо видно на рисунке 13:

Рис. 13. Индикатор в деле

Рис. 13. Индикатор в деле

Как видно до цифры 1 (рис. 13), индикатор рисует стрелки, направленные вверх, далее появляется ромб синего цвета, осведомляющий о возможном изменении движения тренда. Тренд меняется, и до цифры 2 цены идут вниз, далее появляется красный ромб, который также предвещает изменение тренда, так оно и происходит.


Заключение

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

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

Прикрепленные файлы |
spc.mq5 (38.86 KB)
easpc.mq5 (13.42 KB)
images.zip (1210.6 KB)
reporttester.zip (140.31 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (6)
Dmitriy Zabudskiy
Dmitriy Zabudskiy | 5 окт. 2015 в 19:01
Yury Kirillov:

1. Статья интересная.

2. Ссылка на "профиль рынка" в начале статьи не работает.

3. Визуализация метода плохо читается с экрана при большом количестве "веретен".

Спасибо! 

Спасибо за комментарий. Только что проверил ссылку, у меня работает, это странно, но всё равно спасибо. На счёт визуализацией при большом количестве, то тут не по споришь, это своеобразный недостаток графика, да и реализации на картинках. Это так сказать начало, далее думаю тему буду развивать в положительную сторону, но не скоро.
Alexandr Saprykin
Alexandr Saprykin | 11 сент. 2016 в 21:05
Dmitriy Zabudskiy:
Спасибо за комментарий. Только что проверил ссылку, у меня работает, это странно, но всё равно спасибо. На счёт визуализацией при большом количестве, то тут не по споришь, это своеобразный недостаток графика, да и реализации на картинках. Это так сказать начало, далее думаю тему буду развивать в положительную сторону, но не скоро.
Тема заглохла или все-таки получила развитие?
Dmitriy Zabudskiy
Dmitriy Zabudskiy | 15 окт. 2016 в 17:55
Alexandr Saprykin:
Тема заглохла или все-таки получила развитие?
Не занимаюсь ею, нет свободного времени. Можно сказать законсервированная.
Alexandr Saprykin
Alexandr Saprykin | 15 окт. 2016 в 20:27
Dmitriy Zabudskiy:
Не занимаюсь ею, нет свободного времени. Можно сказать законсервированная.
Очень-очень жаль
ivan2007007
ivan2007007 | 22 нояб. 2017 в 16:12

Отлично! Если это все реализовать для NinjaTrader, и применить футпринт, то цены не будет ему!!! жаль что все встало((

Работаем с ZIP-архивами средствами MQL5 без использования сторонних библиотек Работаем с ZIP-архивами средствами MQL5 без использования сторонних библиотек
Язык MQL5 развивается, и в него постоянно добавляются новые функции для работы с данными. С некоторых пор, благодаря нововведениям, стало возможно работать с ZIP-архивами штатными средствами MQL5 без привлечения сторонних библиотек DLL. Данная статья подробно описывает, как это делается, на примере описания класса CZip — универсального инструмента для чтения, создания и модификации ZIP-архивов.
Использование утверждений (assertions) при разработке программ на MQL5 Использование утверждений (assertions) при разработке программ на MQL5
В данной статье рассматриваются утверждения (assertions) в рамках языка MQL5. Даются два примера реализации механизма утверждений, а также приводятся общие рекомендации по применению утверждений.
Применение нечеткой логики в трейдинге средствами MQL4 Применение нечеткой логики в трейдинге средствами MQL4
В данной статье предлагаются примеры применения теории нечетких множеств в трейдинге средствами MQL4. Описывается разработка индикатора и советника с использованием библиотеки FuzzyNet для MQL4.
Введение в теорию нечеткой логики Введение в теорию нечеткой логики
Нечеткая логика расширяет привычные нам границы математической логики и теории множеств. В статье раскрыты основные принципы этой теории, а также описаны две системы нечеткого логического вывода типа Мамдани и Сугено. Приведены примеры реализации нечетких моделей на основе этих двух систем средствами библиотеки FuzzyNet для MQL5.