English 中文 Español Deutsch 日本語 Português
Пишем скальперский стакан цен на основе графической библиотеки CGraphic

Пишем скальперский стакан цен на основе графической библиотеки CGraphic

MetaTrader 5Примеры | 23 июня 2017, 13:52
11 747 35
Vasiliy Sokolov
Vasiliy Sokolov

Оглавление

Введение

Эта статья логически продолжает описание библиотеки по работе со стаканом цен, опубликованное два года назад. С тех пор в MQL5 появился доступ к тиковой истории. Кроме того, силами MetaQuotes была разработана библиотека CGraphic для визуализации пользовательских данных в виде сложной статистической графики. CGraphic по выполняемым задачам аналогична функции plot в языке программирования R. Работа с этой библиотекой подробно описана в отдельной статье

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

Рис. 1. Стакан цен с тиковым графиком.

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

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

Изменения, сделанные с момента выхода предыдущей версии

После публикации статьи "Рецепты MQL5 — Пишем свой стакан цен" я много использовал CMarketBook на практике и в процессе обнаружил ряд ошибок в коде. Постепенно я дорабатывал интерфейс, и в результате накопились следующие изменения:

  1. Первоначально вся графика в стакане цен была очень минималистичной. Ячейки в таблице цен отображались с помощью нескольких простейших классов. Спустя некоторое время эти классы обзавелись дополнительным функционалом, а их простота и легковесность оказались очень удобными при проектировании других видов панелей. В итоге получился целый набор классов, библиотека CPanel, которая отделилась в независимый проект. Она располагается в папке Include.
  2. Улучшился внешний вид стакана. Например, вместо небольшого треугольничка появилась большая квадратная кнопка, открывающая и закрывающая стакан. Исправлена ошибка наложения элементов друг на друга, когда при повторном открытии таблицы элементы стакана отрисовывались еще раз, поверх уже отображенных.
  3. Добавлены настройки, позиционирующие кнопку открывания/закрывания стакана по осям X и Y графика. Часто из-за нестандартного имени инструмента и дополнительной торговой панели кнопка открытия/закрытия стакана закрывала собой другие активные элементы графика. Теперь, когда есть возможность вручную установить расположение кнопки, такого наложения можно избежать.
  4. Сильно изменился и сам класс CMarketBook. В нем исправлены ошибки выхода за диапазон массива (array out of range); ошибки, возникающие при пустом или частично заполненном стакане; ошибка деления на ноль при смене символа. Класс CMarketBook стал независимым модулем и располагается в директории MQL5\Include\Trade;
  5. Сделан ряд более мелких изменений для улучшения общей стабильности индикатора.

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

Краткий обзор графической библиотеки CPanel

Созданию пользовательских интерфейсов в MQL5 посвящено много статей. Среди них особенно выделяется серия Анатолия Кажарского "Графические интерфейсы", после которой сложно сказать что-то новое на эту тему. Поэтому мы не будем подробно вдаваться в построение графического интерфейса. Но, как уже было сказано выше, графическая часть стакана цен превратилась в полноценную библиотеку CPanel. Ее базовую архитектуру необходимо описать, ведь на ее основе будет создан специальный графический элемент: тиковый график. Его мы совместим с таблицей цен, сделав полноценную панель с несколькими элементами.

Итак, подробно рассмотрим CPanel, чтобы понять принцип дальнейшей работы. Графические элементы в MQL5 представлены несколькими графическими примитивами. Это:

  • текстовая метка;
  • кнопка;
  • поле ввода;
  • прямоугольная метка;
  • графическая метка.

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

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

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

Рис. 2. Пример формы с надписью без рамки.

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

Рис. 3. Пример формы с надписью в рамке.

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

Именно так устроена библиотека графических элементов CPanel. По своей сути, CPanel — набор классов, каждый из которых представляет тот или иной элемент высокоуровневого графического интерфейса. Для инициализации такого элемента нужно указать тип графического примитива, на котором он будет основан. У каждого такого класса есть один общий родитель — класс CNode, который выполняет только одну функцию — хранить тип базового примитива. Его единственный защищенный конструктор требует указать этот тип в момент создания элемента. 

Уникальных высокоуровневых графических элементов очень мало. "Уникальность" зависит от набора свойств, которыми нужно снабдить универсальный базовый элемент, чтобы он стал уникальным. Таким универсальным элементом в CPanel является класс CElChart. Как и все остальные классы, он наследуется от CNode и содержит методы для настройки следующих свойств:

  • длина и высота элемента;
  • координаты X и Y элемента относительно графика;
  • ширина и высота элемента;
  • цвет фона и рамки элемента (если такие свойства поддерживаются);
  • тип рамки элемента (если такое свойство поддерживается);
  • текст внутри элемента, его шрифт и размер, выравнивание (если такие свойства поддерживаются).

CElChart предоставляет методы для установки тех или иных свойств, но не гарантирует, что эти свойства в действительности будут выставлены. Будет ли CElChart поддерживать то или иное свойство, целиком определяется базовым элементом. Как и CNode, CElChart требует указать тип графического примитива, на котором он будет основан. Таким образом, с помощью одного CElChart можно создать как обычную форму, так, например, и кнопку или текстовое поле. На практике это оказывается очень удобным.

Пример: нарисуем панель, как на рисунке 3. Для этого понадобятся два элемента: фон с рамкой и текст с рамкой. Оба они — экземпляры одного и того же класса CElChart. Но в них используются два разных графических примитива:  OBJ_RECTANGLE_LABEL и BJ_BUTTON. Получается такой код:

//+------------------------------------------------------------------+
//|                                       MarketBookArticlePanel.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Panel\ElChart.mqh>

CElChart Fon(OBJ_RECTANGLE_LABEL);
CElChart Label(OBJ_BUTTON);
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
//---
   Fon.Height(100);
   Fon.Width(250);
   Fon.YCoord(200);
   Fon.XCoord(200);
   Fon.BackgroundColor(clrWhite);
   Fon.BorderType(BORDER_FLAT);
   Fon.BorderColor(clrBlack);
   Fon.Show();
   
   Label.Text("Meta Quotes Language");
   Label.BorderType(BORDER_FLAT);
   Label.BorderColor(clrBlack);
   Label.Width(150);
   Label.Show();
   Label.YCoord(240);
   Label.XCoord(250);
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

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

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

Графический движок CPanel поддерживает вложенность. Это означает, что внутри одного элемента можно разместить дополнительные. Благодаря вложенности достигается универсальность управления. Например, команда, подаваемая для глобальной формы, может рассылаться всем ее подэлементам. Изменим приведенный выше пример:

//+------------------------------------------------------------------+
//|                                       MarketBookArticlePanel.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Panel\ElChart.mqh>

CElChart Fon(OBJ_RECTANGLE_LABEL);
CElChart *Label;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
//---
   Fon.Height(100);
   Fon.Width(250);
   Fon.YCoord(200);
   Fon.XCoord(200);
   Fon.BackgroundColor(clrWhite);
   Fon.BorderType(BORDER_FLAT);
   Fon.BorderColor(clrBlack);
   
   Label = new CElChart(OBJ_BUTTON);
   Label.Text("Meta Quotes Language");
   Label.BorderType(BORDER_FLAT);
   Label.BorderColor(clrBlack);
   Label.Width(150);
   Label.YCoord(240);
   Label.XCoord(250);
   //Label.Show();
   Fon.AddElement(Label);
   
   Fon.Show();
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }

Теперь в процессе выполнения программы динамически создается CLabel — указатель на элемент CElCahrt. После создания и выставления соответствующих свойств он добавляется в форму Form. Теперь его не надо отображать отдельной командой Show. Вместо этого достаточно выполнить команду Show только для элемента Fon — главной формы нашего приложения. Специфика этой команды такова, что она выполнится для всех вложенных подэлементов, в том числе и для Label. 

CPanel не только задает свойства элемента, но и поддерживает развитую событийную модель. Событием в CPanel может быть все что угодно, а не только событие, получаемое с графика. За это отвечают класс CEvent и метод Event. Класс CEvent абстрактный. На нем базируется множество уже более конкретных классов — например, CEventChartObjClick.

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

CElChart Fon(OBJ_RECTANGLE_LABEL);
...
...
void OnChartEvent(const int id,         // идентификатор события   
                  const long& lparam,   // параметр события типа long 
                  const double& dparam, // параметр события типа double 
                  const string& sparam  // параметр события типа string 
  )
{ 
   CEvent* event = NULL;
   switch(id)
   {
      case CHARTEVENT_OBJECT_CLICK:
         event = new CEventChartObjClick(sparam);
         break;
   }
   if(event != NULL)
   {
      Fon.Event(event);
      delete event;
   }
}

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

Пусть наша метка Meta Quotes Language обрабатывает такое нажатие — меняет свой текст на "Enjoy". Для этого создадим класс CEnjoy и снабдим его необходимой логикой: переопределим метод OnClick — обработчик одноименного события:

//+------------------------------------------------------------------+
//|                                       MarketBookArticlePanel.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Panel\ElChart.mqh>
#include <Panel\Node.mqh>

CElChart Fon(OBJ_RECTANGLE_LABEL);

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
class CEnjoy : public CElChart
{
protected:
   virtual void OnClick(void);
public:
                CEnjoy(void);
   
};

CEnjoy::CEnjoy(void) : CElChart(OBJ_BUTTON)
{
}
void CEnjoy::OnClick(void)
{
   if(Text() != "Enjoy!")
      Text("Enjoy!");
   else
      Text("Meta Quotes Language");
}
CEnjoy Label;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
//---
   Fon.Height(100);
   Fon.Width(250);
   Fon.YCoord(200);
   Fon.XCoord(200);
   Fon.BackgroundColor(clrWhite);
   Fon.BorderType(BORDER_FLAT);
   Fon.BorderColor(clrBlack);
   
   Label.Text("Meta Quotes Language");
   Label.BorderType(BORDER_FLAT);
   Label.BorderColor(clrBlack);
   Label.Width(150);
   Label.YCoord(240);
   Label.XCoord(250);
   //Label.Show();
   Fon.AddElement(&Label);
   
   Fon.Show();
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
}
void OnChartEvent(const int id,         // идентификатор события   
                  const long& lparam,   // параметр события типа long 
                  const double& dparam, // параметр события типа double 
                  const string& sparam  // параметр события типа string 
  )
{ 
   CEvent* event = NULL;
   switch(id)
   {
      case CHARTEVENT_OBJECT_CLICK:
         event = new CEventChartObjClick(sparam);
         break;
   }
   if(event != NULL)
   {
      Fon.Event(event);
      delete event;
   }
   ChartRedraw();
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

Может показаться странным, что мы отправляем событие CEventObjClick в форму Form через метод Event, а обрабатываем — в методе OnClick. Действительно, у многих стандартных событий (например, у клика мыши) есть свои специальные методы-события. Если переопределить их, соответствующее событие будет приходить в них. Если же этого не сделать, все события будут обрабатываться уровнем выше, в методе Event. Это тоже виртуальный метод, его можно переопределить так же, как и метод OnClick. На этом уровне работа с событием происходит через анализ пришедшего экземпляра CEvent. 

Пока оставим эти подробности и укажем основные свойства CPanel.

  • Все классы СPanel, реализующие элементы графического интерфейса, могут базироваться на любом выбранном графическом примитиве. Он выбирается и указывается в момент создания экземпляра класса.
  • Каждый произвольный элемент CPanel может содержать неограниченное множество других элементов CPanel. Так реализуется вложенность, а значит, и универсальность управления. Все события распространяются по дереву вложенности, и таким образом каждый элемент получает доступ к каждому событию.
  • Событийная модель CPanel имеет два уровня. В основе низкоуровневой модели — метод Event и классы типа CEvent. Так можно обрабатывать абсолютно любые события, даже не поддерживающиеся в MQL. Также события, посылаемые через CEvent, всегда широковещательны.  На более высоком уровне стандартные события преобразуются в вызовы соответствующих методов. Например, событие CEventChartObjClick преобразуется в вызов OnClick, а вызов метода Show порождает рекурсивный вызов методов OnShow всех дочерних элементов. На этом уровне событие можно вызвать напрямую. Так, если вызвать метод Show() — он отобразит панель, а вызов метода Refresh обновит отображение этой панели.

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

Синхронизация тикового потока и таблицы заявок

Таблица заявок — динамичная структура, изменяющая значения на ликвидных рынках десятки раз в секунду. Для доступа к текущему срезу таблицы заявок необходимо обрабатывать специальное событие BookEvent в одноименном обработчике события — функции OnBookEvent. В момент изменения в таблице заявок терминал вызывает событие OnBookEvent, с указанием символа, на котором эти изменения произошли. Напомним, что в предыдущей версии статьи был разработан класс CMarketBook, который предоставлял удобный доступ к текущему срезу стакана цен. Чтобы получить текущий срез стакана, в этом классе достаточно было вызвать метод Regresh() в функции OnBookEvent. Выглядело это примерно так:

//+------------------------------------------------------------------+
//|                                                   MarketBook.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
#include <MarketBook.mqh>

CMarketBook MarketBook.mqh
double fake_buffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   MarketBook.SetMarketBookSymbol(Symbol());
//--- indicator buffers mapping
   SetIndexBuffer(0,fake_buffer,INDICATOR_CALCULATIONS);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| MarketBook change event                                          |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
{
   if(symbol != MarketBook.GetMarketBookSymbol())
      return;
   MarketBook.Refresh();
   ChartRedraw();
}

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

  • Получение последнего известного тика через функцию SymbolInfoTick;
  • Обработка события прихода нового тика в функции OnTick для экспертов и в OnCalculate — для индикаторов;
  • Получение тиковой истории с помощью функций CopyTicks и CopyTicksRange.

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

Чтобы понять, как образуются тики, давайте обратимся к статье "Основы биржевого ценообразования, на примере Московской биржи"  и рассмотрим условный стакан цен на золото: 

Цена, $ за тройскую унцию золота Кол-во унций (контрактов)
1280.8 17
1280.3 3
1280.1 5
1280.0 1
1279.8 2
1279.7 15
1279.3 3
1278.8 13

Рис 4. Пример стакана заявок.

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

Предположим что нашелся покупатель, готовый приобрести 9 контрактов на золото. Купив, он совершит как минимум три сделки. Если же на уровне 1280.1 или 1280.3 будет несколько продавцов, то сделок будет еще больше. Совершив одно действие (покупку), он создаст сразу несколько сделок, которые произойдут одновременно. Таким образом, тики в терминал MetaTrader 5 придут тоже "пачкой". Поэтому, если в OnCalculate воспользоваться функцией SymbolInfoTick, она вернет лишь последний тик из этой серии, а предшествующие будут потеряны.

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

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

Рис. 5. Таблица всех сделок с примером одинаковых сделок.

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

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

Синхронизируем класс CMarketBook с тиковым потоком: добавим в него массив MqlTiks, содержащий новые тики, пришедшие с момента предыдущего обновления. Сами новые тики будет рассчитывать внутренний метод CompareTiks:

//+------------------------------------------------------------------+
//| Compare two tiks collections and find new tiks                   |
//+------------------------------------------------------------------+
void CMarketBook::CompareTiks(void)
{
   MqlTick n_tiks[];
   ulong t_begin = (TimeCurrent()-(1*20))*1000; // from 20 sec ago
   int total = CopyTicks(m_symbol, n_tiks, COPY_TICKS_ALL, t_begin, 1000);
   if(total<1)
   {
      printf("Не удалось получить тики");
      return;
   }
   if(ArraySize(m_ticks) == 0)
   {
      ArrayCopy(m_ticks, n_tiks, 0, 0, WHOLE_ARRAY);
      return;
   }
   int k = ArraySize(m_ticks)-1;
   int n_t = 0;
   int limit_comp = 20;
   int comp_sucess = 0;
   //Перебираем новые полученные торговые сделки начиная с самой последней
   for(int i = ArraySize(n_tiks)-1; i >= 0 && k >= 0; i--)
   {
      if(!CompareTiks(n_tiks[i], m_ticks[k]))
      {
         n_t = ArraySize(n_tiks) - i;
         k = ArraySize(m_ticks)-1;
         comp_sucess = 0;
      }
      else
      {
         comp_sucess += 1;
         if(comp_sucess >= limit_comp)
            break;
         k--;
      }
   }
   //Запоминаем полученные тики
   ArrayResize(m_ticks, total);
   ArrayCopy(m_ticks, n_tiks, 0, 0, WHOLE_ARRAY);
   //Рассчитываем индекс начала новых тиков и копируем их в буфер для доступа
   ArrayResize(LastTicks, n_t);
   if(n_t > 0)
   {
      int index = ArraySize(n_tiks)-n_t;
      ArrayCopy(LastTicks, m_ticks, 0, index, n_t);
   }
}

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

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

Рис. 6. Схема синхронизации повторяющихся серий.

При сравнении мы видим, что числа с 6 по 14 первого массива равны числам с 1 по 8 второго массива. Следовательно, в Array2 есть пять новых значений — это элементы с 9 по 14. Алгоритм работает в различных комбинациях: массивы могут иметь разную длину, не иметь общих элементов или быть абсолютно идентичными друг другу. Во всех этих случаях количество новых значений определится правильно.

После того, как количество новых тиков определено, копируем их в массив LastTiks. Этот массив определен как публичное поле внутри класса CMarketBook.

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

//+------------------------------------------------------------------+
//| MarketBook change event                                          |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
{
   
   if(symbol != MarketBook.GetMarketBookSymbol())
      return;
   MarketBook.Refresh();
   string new_tiks = (string)ArraySize(MarketBook.LastTicks);
   printf("Пришло " + new_tiks + " новых тиков");
}

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

Основы CGraphic

В арсенале библиотеки CGraphic — линии, гистограммы, точки, сложные геометрические фигуры. Для наших целей нужна лишь малая часть ее возможностей. Нам понадобятся две линии для отображения уровней Ask и Bid и специальные точки для отображения сделок Last. По своей сути, CGraphic — контейнер, содержащий объекты CCurve. Каждый такой объект, как нетрудно догадаться по названию, — некая кривая, состоящая из точек с координатами X и Y. В зависимости от вида отображения, они могут соединяться линиями, могут быть вершинами столбцов гистограммы или же отображаться как есть — в виде точки. 

Из-за некоторых особенностей представления сделок Last мы будем работать с двухмерными графиками. Попробуем создать простой двухмерный график в виде линии:

//+------------------------------------------------------------------+
//|                                                   TestCanvas.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Graphics\Graphic.mqh>

CGraphic Graph;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   double x[] = {1,2,3,4,5,6,7,8,9,10};
   double y[] = {1,2,3,2,4,3,5,6,4,3};
   CCurve* cur = Graph.CurveAdd(x, y, CURVE_LINES, "Line");   
   Graph.CurvePlotAll();
   Graph.Create(ChartID(), "Ticks", 0, (int)50, (int)60, 510, 300); 
   Graph.Redraw(true);
   Graph.Update();
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

Если запустить его в виде эксперта, на графике появится такая картинка:

Рис. 7. Пример двухмерного линейного графика, созданного с помощью CGraphic

Теперь попробуем поменять представление, для чего сменим тип отображения у нашей двухмерной кривой CCurve на точки:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   double x[] = {1,2,3,4,5,6,7,8,9,10};
   double y[] = {1,2,3,2,4,3,5,6,4,3};
   CCurve* cur = Graph.CurveAdd(x, y, CURVE_POINTS, "Points");   
   cur.PointsType(POINT_CIRCLE);
   Graph.CurvePlotAll();
   Graph.Create(ChartID(), "Ticks", 0, (int)50, (int)60, 510, 300); 
   Graph.Redraw(true);
   Graph.Update();
   return(INIT_SUCCEEDED);
}

Тот же график в виде точек стал выглядеть так:

Рис. 8. Пример точечного двухмерного графика, созданного с помощью CGraphic

Как видно, основные действия заключаются в создании объекта кривой, значения которой предварительно должны содержаться в массивах x и y: 

double x[] = {1,2,3,4,5,6,7,8,9,10};
double y[] = {1,2,3,2,4,3,5,6,4,3};
CCurve* cur = Graph.CurveAdd(x, y, CURVE_POINTS, "Points"); 

Созданный объект размещается внутри CGraphic, а метод CurveAdd возвращает ссылку на него. Сделано это, чтобы можно было задать нужные свойства этой кривой, что мы и сделали во втором примере, задав тип кривой как CURVE_POINTS и указав тип значка в виде кружка:

cur.PointsType(POINT_CIRCLE);

После того, как все линии добавлены, надо отобразить график на чарте, выполнив команды Create и Redraw.

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

Интеграция CGraphic с библиотекой CPanel

Мы прояснили основные моменты работы с CGraphic. Теперь настала пора внедрить этот класс в библиотеку CPanel. Как уже было сказано, CPanel предоставляет доступ к необходимым событиям, правильно располагает графические элементы, управляет их свойствами. Все это нужно, чтобы сделать тиковый график органичной частью единой панели стакана цен. Поэтому сначала напишем специальный элемент CElTickGraph — часть CPanel, которая интегрирует CGraphic в панель. Кроме того, CElTickGraph будет получать обновленный тиковый поток цен и перерисовывать тиковый график. Последняя задача — самая сложная. Вкратце перечислим, что именно должен уметь делать CElTickGraph.

  • CElTickGraph размечает прямоугольную область внутри общей панели стакана цен. Область выделена черной рамкой. 
  • Внутри области CElTickGraph размещается график CGraphic, отображающий тиковый поток цен.
  • Тиковый поток отображает N последних тиков. Число N можно изменять в настройках.
  • CElTickGraph обновляет значения кривых CCurve, входящих в состав CGraphic, так, что старые тики удаляются с графика, а новые добавляются в него. Благодаря этому CElTickGraph создает эффект плавно меняющегося тикового графика.

Чтобы упростить задачу CElTickGraph, используем вспомогательное решение: кольцевой буфер, принцип действия которого уже был подробно описан в отдельной статье.

Создадим четыре кольцевых буфера для отображения следующих величин:

  • уровня Ask (отображается в виде красной линии);
  • уровня Bid (отображается в виде синей линии);
  • последней сделки со стороны покупки (отображается в виде синего треугольника, направленного вниз);
  • последней сделки со стороны продажи (отображается в виде красного треугольника, направленного вверх).

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

Вторая трудность — в том, что количество точек между Ask/Bid и ценами Last не совпадает. Когда мы чертим непрерывную линию, для каждой точки на оси X есть значение на оси Y. Но если вместо линии используются точки, точка в момент X может находиться на графике, а может и отсутствовать. Нужно учесть это свойство и использовать двухмерный график.  Допустим, у нас есть точка, имеющая следующие X-Y координаты: 1000-57034. Тогда в момент прихода нового тика эта же точка будет иметь координаты 999-57034. Еще через пять тиков она сместится на позицию 994-57034. Последней ее позицией станет 0-57034. Затем она исчезнет с графика. Следующая за ней точка может отстоять от нее на разное количество шагов. Когда точка 1 будет иметь координаты 994-57034, точка 2 будет находиться на 995:57035 или на 998:57035. Комбинируя линии на двухмерном графике, мы можем отображать эти зазоры верно, не сваливая тиковый поток в непрерывную очередь.

Представим себе гипотетическую таблицу тиков, отображающую поток сделок с учетом индексов:

Index Ask Bid Buy Sell
999 57034 57032 57034
998 57034 57032
57032
997 57034 57031 57034
996 57035 57033 57035
995 57036 57035

994 57036 57035

993 57036 57035
57035
992 57036 57034
57034
991 57036 57035 57035
...



Таблица 1. Схема Синхронизации тикового потока в двухмерной таблице (графике).

В ней Ask и Bid заполнены полностью, а вот сделки на покупку (Buy) и продажу (Sell) порою отсутствуют. Размещение показаний по индексам правильно синхронизирует серии разной длины. Сколько бы ни было сделок Last, они всегда будут соотноситься с нужными уровнями Ask и Bid.

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

//+------------------------------------------------------------------+
//|                                                         Graf.mqh |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#include <Panel\ElChart.mqh>
#include <RingBuffer\RiBuffDbl.mqh>
#include <RingBuffer\RiBuffInt.mqh>
#include <RingBuffer\RiMaxMin.mqh>
#include "GlobalMarketBook.mqh"
#include "GraphicMain.mqh"
#include "EventNewTick.mqh"

input int TicksHistoryTotal = 200;
//+------------------------------------------------------------------+
//| Определяет номер кривой в графическом объекте CGraphic           |
//+------------------------------------------------------------------+
enum ENUM_TICK_LINES
{
   ASK_LINE,
   BID_LINE,
   LAST_BUY,
   LAST_SELL,
   LAST_LINE,
   VOL_LINE
};
//+------------------------------------------------------------------+
//| Графический элемент, отображающий тиковый график                 |
//+------------------------------------------------------------------+
class CElTickGraph : public CElChart
{
private:
   
   CGraphicMain m_graf;
   /* Кольцевые буферы для быстрой работы с тиковым потоком*/
   CRiMaxMin    m_ask;
   CRiMaxMin    m_bid;
   CRiMaxMin    m_last;
   CRiBuffDbl   m_last_buy;
   CRiMaxMin    m_last_sell;
   CRiBuffInt   m_vol;
   CRiBuffInt   m_flags;
   
   double       m_xpoints[];  // Массив индексов
   void         RefreshCurves();
   void         SetMaxMin(void);
public:
                CElTickGraph(void);
   virtual void Event(CEvent* event);
   void         SetTiksTotal(int tiks);
   int          GetTiksTotal(void);
   void         Redraw(void);
   virtual void Show(void);
   virtual void OnHide(void);
   virtual void OnRefresh(CEventRefresh* refresh);
   void         AddLastTick();
};
//+------------------------------------------------------------------+
//| Инициализация графика                                            |
//+------------------------------------------------------------------+
CElTickGraph::CElTickGraph(void) : CElChart(OBJ_RECTANGLE_LABEL)
{
   double y[] = {0};
   y[0] = MarketBook.InfoGetDouble(MBOOK_BEST_ASK_PRICE);
   double x[] = {0};
   
   CCurve* cur = m_graf.CurveAdd(x, y, CURVE_LINES, "Ask");   
   cur.Color(ColorToARGB(clrLightCoral, 255));
   cur.LinesEndStyle(LINE_END_ROUND);
   
   cur = m_graf.CurveAdd(x, y, CURVE_LINES, "Bid");
   cur.Color(ColorToARGB(clrCornflowerBlue, 255));
   
   cur = m_graf.CurveAdd(x, y, CURVE_POINTS, "Buy");
   cur.PointsType(POINT_TRIANGLE_DOWN);
   cur.PointsColor(ColorToARGB(clrCornflowerBlue, 255));
   cur.Color(ColorToARGB(clrCornflowerBlue, 255));
   cur.PointsFill(true);
   cur.PointsSize(5);
   
   
   cur = m_graf.CurveAdd(x, y, CURVE_POINTS, "Sell");
   cur.PointsType(POINT_TRIANGLE);
   cur.PointsColor(ColorToARGB(clrLightCoral, 255));
   cur.Color(ColorToARGB(clrLightCoral, 255));
   cur.PointsFill(true);
   cur.PointsSize(5);
   
   m_graf.CurvePlotAll();
   m_graf.IndentRight(1);
   m_graf.GapSize(1);
   SetTiksTotal(TicksHistoryTotal);
}
//+------------------------------------------------------------------+
//| Устанавливает количество тиков в окне графика                    |
//+------------------------------------------------------------------+
void CElTickGraph::SetTiksTotal(int tiks)
{
   m_last.SetMaxTotal(tiks);
   m_last_buy.SetMaxTotal(tiks);
   m_last_sell.SetMaxTotal(tiks);
   m_ask.SetMaxTotal(tiks);
   m_bid.SetMaxTotal(tiks);
   m_vol.SetMaxTotal(tiks);
   ArrayResize(m_xpoints, tiks);
   for(int i = 0; i < ArraySize(m_xpoints); i++)
      m_xpoints[i] = i;
}

//+------------------------------------------------------------------+
//| Обновляет линии тиков                                            |
//+------------------------------------------------------------------+
void CElTickGraph::RefreshCurves(void) 
{
   int total_last = m_last.GetTotal();
   int total_ask = m_ask.GetTotal();
   int total_bid = m_bid.GetTotal();
   int total = 10;
   for(int i = 0; i < m_graf.CurvesTotal(); i++)
   {
      CCurve* curve = m_graf.CurveGetByIndex(i);
      double y_points[];
      double x_points[];
      switch(i)
      {
         case LAST_LINE:
         {
            m_last.ToArray(y_points);
            if(ArraySize(x_points) < ArraySize(y_points))
               ArrayCopy(x_points, m_xpoints, 0, 0, ArraySize(y_points));
            curve.Update(x_points, y_points);
            break;
         }
         case ASK_LINE:
            m_ask.ToArray(y_points);
            if(ArraySize(x_points) < ArraySize(y_points))
               ArrayCopy(x_points, m_xpoints, 0, 0, ArraySize(y_points));
            curve.Update(x_points, y_points);
            break;
         case BID_LINE:
            m_bid.ToArray(y_points);
            if(ArraySize(x_points) < ArraySize(y_points))
               ArrayCopy(x_points, m_xpoints, 0, 0, ArraySize(y_points));
            curve.Update(x_points, y_points);
            break;
         case LAST_BUY:
         {
            m_last_buy.ToArray(y_points);
            CPoint2D points[];
            ArrayResize(points, ArraySize(y_points));
            int k = 0;
            for(int p = 0; p < ArraySize(y_points);p++)
            {
               if(y_points[p] == -1)
                  continue;
               points[k].x = p;
               points[k].y = y_points[p];
               k++;
            }
            if(k > 0)
            {
               ArrayResize(points, k);
               curve.Update(points);
            }
            break;
         }
         case LAST_SELL:
         {
            m_last_sell.ToArray(y_points);
            CPoint2D points[];
            ArrayResize(points, ArraySize(y_points));
            int k = 0;
            for(int p = 0; p < ArraySize(y_points);p++)
            {
               if(y_points[p] == -1)
                  continue;
               points[k].x = p;
               points[k].y = y_points[p];
               k++;
            }
            if(k > 0)
            {
               ArrayResize(points, k);
               curve.Update(points);
            }
            break;
         }
      }
   }
   
}
//+------------------------------------------------------------------+
//| Возвращает количество тиков в окне графика                       |
//+------------------------------------------------------------------+
int CElTickGraph::GetTiksTotal(void)
{
   return m_ask.GetMaxTotal();
}
//+------------------------------------------------------------------+
//| Обновляет график в момент обновления стакана                     |
//+------------------------------------------------------------------+
void CElTickGraph::OnRefresh(CEventRefresh* refresh)
{
   //Вычерчиваем на графике последние поступившие тики
   int dbg = 5;
   int total = ArraySize(MarketBook.LastTicks);
   for(int i = 0; i < ArraySize(MarketBook.LastTicks); i++)
   {
      MqlTick tick = MarketBook.LastTicks[i];
      if((tick.flags & TICK_FLAG_BUY)==TICK_FLAG_BUY)
      {
         m_last_buy.AddValue(tick.last);
         m_last_sell.AddValue(-1);
         m_ask.AddValue(tick.last);
         m_bid.AddValue(tick.bid);
      }
      if((tick.flags & TICK_FLAG_SELL)==TICK_FLAG_SELL)
      {
         m_last_sell.AddValue(tick.last);
         m_last_buy.AddValue(-1);
         m_bid.AddValue(tick.last);
         m_ask.AddValue(tick.ask);
      }
      if((tick.flags & TICK_FLAG_ASK)==TICK_FLAG_ASK ||
         (tick.flags & TICK_FLAG_BID)==TICK_FLAG_BID)
      {
         m_last_sell.AddValue(-1);
         m_last_buy.AddValue(-1);
         m_bid.AddValue(tick.bid);
         m_ask.AddValue(tick.ask);
      }
   }
   MqlTick tick;
   if(!SymbolInfoTick(Symbol(), tick))
       return;
   if(ArraySize(MarketBook.LastTicks)>0)
   {
      RefreshCurves();
      m_graf.Redraw(true);
      m_graf.Update();
   }
}
void CElTickGraph::Event(CEvent *event)
{
   CElChart::Event(event);
   if(event.EventType() != EVENT_CHART_CUSTOM)
      return;
   CEventNewTick* ent = dynamic_cast<CEventNewTick*>(event);
   if(ent == NULL)
      return;
   MqlTick tick;
   ent.GetNewTick(tick);
   if((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY)
   {
      int last = m_last_buy.GetTotal()-1;
      if(last >= 0)
         m_last_buy.ChangeValue(last, tick.last);
   }
}
//+------------------------------------------------------------------+
//| Рассчитывает масштаб по осям так, что бы текущая цена всегда была|
//| на середине ценового графика                                     |
//+------------------------------------------------------------------+
void CElTickGraph::SetMaxMin(void)
{
   double max = m_last.MaxValue();
   double min = m_last.MinValue();
   double curr = m_last.GetValue(m_last.GetTotal()-1);
   double max_delta = max - curr;
   double min_delta = curr - min;
   if(max_delta > min_delta)
      m_graf.SetMaxMinValues(0, m_last.GetTotal(), (max-max_delta*2.0), max);
   else
      m_graf.SetMaxMinValues(0, m_last.GetTotal(), min, (min+min_delta*2.0));
}
//+------------------------------------------------------------------+
//| Обновляет график                                                 |
//+------------------------------------------------------------------+
void CElTickGraph::Redraw(void)
{
   m_graf.Redraw(true);
   m_graf.Update();
}
//+------------------------------------------------------------------+
//| Перехватывает отображение графика, меняя приоритет отображения   |
//+------------------------------------------------------------------+
void CElTickGraph::Show(void)
{
   BackgroundColor(clrNONE);
   BorderColor(clrBlack);
   Text("Ticks:");
   //m_graf.BackgroundColor(clrWhiteSmoke);
   m_graf.Create(ChartID(), "Ticks", 0, (int)XCoord()+20, (int)YCoord()+30, 610, 600); 
   m_graf.Redraw(true);
   m_graf.Update();
   CElChart::Show();
}

//+------------------------------------------------------------------+
//| В момент отображения показываем график                           |
//+------------------------------------------------------------------+
void CElTickGraph::OnHide(void)
{
   m_graf.Destroy();
   CNode::OnHide();
}

Разберем этот код подробнее. Начнем с конструктора класса CElTickGraph::CElTickGraph. Из его декларации ясно, что сам класс базируется на графическом примитиве OBJ_RECTANGLE_LABEL, т.е. на обычной прямоугольной метке. В конструкторе создаются несколько кривых типа CCurve, каждая из которых отвечает за свой тип данных. Для каждой из них устанавливаются свойства: название линии, ее тип и цвет. В момент создания кривой значения, которые она будет представлять, еще неизвестны, поэтому используем фейковые массивы double x и y, содержащие координаты первой точки. После того, как кривые созданы и размещены в объекте CGraphic, конфигурируются кольцевые буферы в методе SetTiksTotal. Конфигурация сводится к установке предельного количества запоминаемых тиков, которое задается внешним параметров TicksHistoryTotal.

Когда в CGraphic добавлены все необходимые кривые, а кольцевые буферы должным образом сконфигурированы, стакан цен готов к работе. В процессе работы вызываются два основных метода: это CElTickGraph::OnRefresh и CElTickGraph::RefreshCurves. Рассмотрим их.

Метод OnRefresh вызывается вслед за изменением стакана. Такие изменения отслеживаются с помощью функции OnBookEvent:

//+------------------------------------------------------------------+
//| MarketBook change event                                          |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
{
   
   if(symbol != MarketBook.GetMarketBookSymbol())
      return;
   MarketBook.Refresh();
   MButton.Refresh();
   ChartRedraw();
}

Сначала обновляется стакан цен (MarketBook.Refresh()), затем — отображающая его панель: MButton.Refresh(). Так как панель отображается в виде кнопки и ее можно свернуть/развернуть, то сама эта кнопка — родительский элемент всей панели. Поэтому все события, в том числе приказ обновления, поступают через эту кнопку (MButton). Приказ обновления проходит через все вложенные в кнопку элементы и наконец доходит до CElTickGraph, в котором размещен алгоритм, обновляющий сам график. Алгоритм реализован в методе OnRefresh.

Вначале алгоритм получает количество тиков, которые успели возникнуть с момента предыдущего обновления. Затем значения каждого из этого тиков добавляются в соответствующий кольцевой буфер. Цена Ask тика добавляется в кольцевой буфер m_ask, цена Bid — в буфер m_bid и т.д. Если тип последнего тика — это сделка Last, то цены Ask и Bid принудительно синхронизируются с ценой Last. Сделано это, потому что сам терминал такой синхронизации не производит, а выдает значения Ask и Bid предыдущих тиков. Таким образом, сделки Last всегда гарантированно находятся либо на уровне Ask, либо на уровне Bid. Заметим, что стандартный стакан цен такую синхронизацию не делает, и Last в нем визуально может находиться между этими двумя линиями.

После того, как последняя очередь тиков размещена в кольцевых буферах, вызывается метод OnRefreshCurves, ответственный за отрисовку тиков на графике. В методе размещен цикл, в котором перебираются все доступные кривые CCurve. Для каждой из кривой делается полное обновление точек, с помощью метода curve.Update. Точки для осей Y  мы получаем копированием всех значений из кольцевого буфера в обычный массив double. Точки для осей X получаются более изощренным образом. С помощью полного перебора, каждой точке у заменяется x-координата на x-1. Т.е., если элемент x имел значение 1000, то после этого перебора он будет иметь значение 999. Так образом достигается эффект движения, когда график чертит новые значения, а старые бесследно исчезают с него.

После того, как все значения размещены по нужным индексам и кривые CCurve обновлены, остается обновить сам стакан цен, для чего в методе OnRefresh вызываются методы обновления графика:  m_graf.Redraw и m_graf.Update.

Алгоритм отображения тикового графика позволяет выбрать два режима:

  • Тиковый график отображается без привязки последних цен к середине стакана. Максимумы и минимумы графика рассчитываются автоматически, внутри CGraphic.
  • Тиковый график отображается с учетом привязки последних цен к середине стакана. Где бы ни находился максимум и минимум цен, текущая (последняя) цена всегда будет на середине графика.

В первом случае вызывается автоматическое масштабирование, производимое самой CGraphic. Во втором случае масштабированием занимается метод SetMaxMin.

Установка. Работа в динамике. Сравнительные характеристики стаканов цен

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

  • Файлы графической библиотеки CPanel. Расположены в MQL5\Include\Panel;
  • Файл класса стакана цен MarketBook. Расположен в MQL5\Include\Trade;
  • Файлы классов кольцевых буферов. Расположены в MQL5\Include\RingBuffer;
  • Собственно файлы скальперского стакан цен. Расположены в MQL5\Indicators\MarketBookArticle.

Прилагаемый архив содержит все эти файлы в соответствующих каталогах. Для установки программы просто распакуйте архив в папку MQL5. Создавать каких-либо подпапок не надо. После распаковки скомпилируйте файл MQL5\Indicators\MarketBookArticle\MarketBook.mq5. После компиляции появится соответствующий индикатор, который появится в окне Навигатора MetaTrader 5. 

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


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

Стандартный стакан цен MetaTrader 5 Разработанный стакан цен
Цены Last, Ask и Bid не взаимосвязаны. Цена Last может быть на уровнях, отличных от Ask и Bid. Цены Last, Ask, Bid синхронизированы между собой. Цена Last всегда находится либо на уровне Ask, либо на уровне Bid.
Цены Last отображаются в виде кружков различного диаметра, прямо пропорционального объему сделок. Круг с максимальным диаметром соответствует сделке с максимальным объемом, совершённой за последние N тиков, где N — период скользящего окна тикового графика. Сделки на покупку отображаются в виде синего треугольника, направленного вниз, сделки на продажу — в виде красного треугольника, направленного вверх. Выделение сделок в зависимости от объема не производится.
Масштаб тикового графика синхронизирован с высотой таблицы отложенных заявок. Таким образом, любой уровень в таблице сделок соответствует этому же уровню на тиковом графике. Минус такого решения — невозможность отображения тикового графика в своем, более крупном масштабе. Плюс — наглядность цен и полное соответствие уровней стакана с тиковым графиком. Масштабы тикового графика и таблицы отложенных заявок не соответствуют друг другу. Текущая цена тикового графика может лишь примерно соответствовать середине таблицы заявок. Минус такого решения — отсутствие визуального соответствия уровней таблицы заявок с тиковым графиком. Плюс — возможность установить практически любой масштаб тикового графика.
Тиковый график снабжен дополнительной гистограммой объемов, расположенной под ним.  Тиковый график не содержит каких-либо дополнений.

Таблица 2. Сравнительные характеристики стандартного и разработанного стакана цен.

Заключение

Мы рассмотрели все основные моменты разработки скальперского стакана цен.

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

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

  • Возможность выставлять лимитные заявки прямо на панели стакана
  • Возможность отслеживать крупные заявки на тиковом графике
  • Дифференцировать сделки Last по объему, отображая их разными способами на графике
  • Отображать дополнительные индикаторы параллельно тиковому графику. Например, под тиковым графиком отображать гистограмму отношения всех Buy Limit заявок ко всем Sell Limit заявкам.
  • И, наконец, самое важное: загружать и сохранять историю стакана, чтобы можно было строить торговые стратегии в режиме offline-тестирования.

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (35)
Vasiliy Sokolov
Vasiliy Sokolov | 15 апр. 2020 в 12:53
Konstantin Seredkin:

Здравствуйте.

Подскажите пожалуйста, можно ли в классе MarketBook.mqh фиксировать - запоминать время найденного индекса ?

В данном методе мы находим индекс цены на котором стоит максимальный объем по стороне Аск и Бид.

Хочу еще знать еще  в какое терминальное время мы получили данный индекс.

Структура стакана

Дает нам 4 параметра, но в ней нет времени.

Кто может направить в русло как запомнить время найденного индекса

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

Konstantin Seredkin
Konstantin Seredkin | 15 апр. 2020 в 19:24
Vasiliy Sokolov:

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

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

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

Вам спасибо что откликнулись.

Konstantin Seredkin
Konstantin Seredkin | 20 апр. 2020 в 09:40
А вот такой еще вопрос, можно ли в мт5 работать одновременно с 2-мя стаканами цен ? и можно ли используя функционал методов с данной темы как то инициализировать 2 стакана и получать с них данные.
Vladimir Pastushak
Vladimir Pastushak | 2 сент. 2020 в 13:03
Не работает, ибо есть ошибки....
__zeus__
__zeus__ | 30 нояб. 2020 в 17:51
На брент фортс бессмысленно использовать такой стакан , так как на брент несколько фьючерсов
Графические интерфейсы XI: Рефакторинг кода библиотеки (build 14.1) Графические интерфейсы XI: Рефакторинг кода библиотеки (build 14.1)
По мере разрастания библиотеки снова необходимо оптимизировать ее код, чтобы уменьшить его объем. Версия библиотеки, описанная в этой статье, стала ещё более объектно-ориентированной. Это сделало код понятнее для изучения. Подробное описание последних изменений позволит читателю самостоятельно развивать библиотеку, исходя из собственных потребностей.
Пользовательские индикаторы и инфографика в CCanvas Пользовательские индикаторы и инфографика в CCanvas
В статье рассматриваются новые виды индикаторов с более сложной структурной реализацией. Описывается построение псевдообъемных типов индикаторов и создание динамично изменяющейся инфографики.
Использование облачных хранилищ для обмена данными между терминалами Использование облачных хранилищ для обмена данными между терминалами
Все большее распространение получают облачные технологии. К нашим услугам — как платные, так и бесплатные хранилища. Можем ли мы их использовать в трейдинге? В этой статье предлагается технология для обмена данными между терминалами с использованием облачных хранилищ.
Кроссплатформенный торговый советник: Сигналы Кроссплатформенный торговый советник: Сигналы
В статье обсуждаются классы CSignal и CSignals, которые будут использоваться в кроссплатформенных торговых советниках. Рассмотрены различия между MQL4 и MQL5 в организации данных, необходимых для оценки полученных торговых сигналов. Итог — код, совместимый с компиляторами обеих версий.