Анализ графиков Баланса/Средств по символам и ORDER_MAGIC советников

Vladimir Karputov | 17 апреля, 2017

Содержание


Постановка задачи

С введением хеджинга в MetaTrader 5 появилась отличная возможность одновременной торговли несколькими советниками (или несколькими стратегиями) на одном торговом счёте. Весь торговый счёт можно отследить в сервисе Сигналы и получить по нему статистику. Нерешенным оставался один очень важный для меня вопрос: как визуализировать вклад каждой стратегии в графики Баланс и Средства?

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


1. Комиссия. Своп. Прибыль

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

 Result=Deal commission +Cumulative swap on close+ Deal profit

Эти свойства сделок получают при помощи HistoryDealGetDouble() со следующими идентификаторами:

DEAL_COMMISSION

Deal commission

double

DEAL_SWAP

Cumulative swap on close

double

DEAL_PROFIT

Deal profit

double


Пример получения свойств сделок из торговой истории за заданный промежуток времени в скрипте "HistoryDealGetTicket.mq5".

Результаты работы скрипта (удалены сделки с типом DEAL_ENTRY_IN, поскольку в них отсутствует финансовый результат):

...
  4: deal #36774600 at 2017.02.15 10:17:50 Entry out, sell vol: 0.01 comm: 0 swap: 0.02 profit: 1.52 NZDUSD.m (order #47802989, position ID 47770449)
...
12: deal #36798157 at 2017.02.15 16:44:17 Entry out, buy vol: 0.01 comm: 0 swap: -0.01 profit: 2.98 EURUSD.m (order #47827771, position ID 47685190)
13: deal #36798161 at 2017.02.15 16:44:17 Entry out, buy vol: 0.01 comm: 0 swap: -0.02 profit: 5.99 EURUSD.m (order #47827785, position ID 47665575)
14: deal #36798176 at 2017.02.15 16:44:17 Entry out, buy vol: 0.01 comm: 0 swap: -0.02 profit: 5.93 EURUSD.m (order #47827805, position ID 47605603)
15: deal #36798185 at 2017.02.15 16:44:18 Entry out, buy vol: 0.01 comm: 0 swap: -0.03 profit: 5.98 EURUSD.m (order #47827821, position ID 47502789)
16: deal #36798196 at 2017.02.15 16:44:18 Entry out, buy vol: 0.01 comm: 0 swap: -0.03 profit: 8.96 EURUSD.m (order #47827832, position ID 47419515)
17: deal #36798203 at 2017.02.15 16:44:18 Entry out, buy vol: 0.01 comm: 0 swap: -0.06 profit: 8.92 EURUSD.m (order #47827835, position ID 47130461)
18: deal #36798212 at 2017.02.15 16:44:19 Entry out, sell vol: 0.01 comm: 0 swap: -0.48 profit: -21.07 EURUSD.m (order #47827845, position ID 46868574)
...
25: deal #36824799 at 2017.02.15 19:57:57 Entry out, sell vol: 0.01 comm: 0 swap: 0 profit: 2.96 NZDUSD.m (order #47855548, position ID 47817757)
26: deal #36824800 at 2017.02.15 19:57:58 Entry out, sell vol: 0.01 comm: 0 swap: 0 profit: 3.01 NZDUSD.m (order #47855549, position ID 47790966)
27: deal #36824801 at 2017.02.15 19:57:58 Entry out, sell vol: 0.01 comm: 0 swap: 0.02 profit: 3.07 NZDUSD.m (order #47855550, position ID 47777495)
28: deal #36824802 at 2017.02.15 19:57:58 Entry out, sell vol: 0.01 comm: 0 swap: 0.02 profit: 3 NZDUSD.m (order #47855551, position ID 47759307)
29: deal #36824803 at 2017.02.15 19:57:59 Entry out, sell vol: 0.01 comm: 0 swap: 0.02 profit: 1.52 NZDUSD.m (order #47855552, position ID 47682775)
...
33: deal #36832775 at 2017.02.16 00:58:41 Entry out, sell vol: 0.01 comm: 0 swap: 0.05 profit: 2.96 NZDUSD.m (order #47863883, position ID 47826616)
34: deal #36832776 at 2017.02.16 00:58:41 Entry out, sell vol: 0.01 comm: 0 swap: 0.05 profit: 3.05 NZDUSD.m (order #47863884, position ID 47803010)
35: deal #36832777 at 2017.02.16 00:58:41 Entry out, sell vol: 0.01 comm: 0 swap: 0.05 profit: 2.98 NZDUSD.m (order #47863885, position ID 47792294)
36: deal #36832778 at 2017.02.16 00:58:42 Entry out, sell vol: 0.01 comm: 0 swap: 0.07 profit: 2.88 NZDUSD.m (order #47863886, position ID 47713741)
...

Как видим, своп и прибыль могут быть как со знаком "+", так и со знаком "-". Поэтому в формуле итогового финансового результата для свопа и прибыли используется сложение.


2. Расчёт средств и баланса на истории

Общая схема работы: формируем список "открытые позиции", торговую историю делим на отрезки по 5-15 минут (точнее этот параметр выясним позже). Затем последовательно, для каждого отрезка:

2.1. Список "Открытые позиции"

Для ведения учета открытых позиций на выбранном промежутке нужен будет класс сделок. Реализуем это в классе "CHistoryDeal" — во включаемом файле "HistoryDeal.mqh":

МетодЗначение
TicketDealПолучает/устанавливает свойство сделки "Тикет сделки"
PosIDDealПолучает/устанавливает свойство сделки "Идентификатор позиции"
SymbolDealПолучает/устанавливает свойство сделки "Символ сделки"
TypeDealПолучает/устанавливает свойство сделки "Тип сделки" из перечисления ENUM_DEAL_TYPE
EntryDealПолучает/устанавливает свойство сделки "Направление сделки" из перечисления ENUM_DEAL_ENTRY
VolumeDealПолучает/устанавливает свойство сделки "Объём сделки"
PriceDealПолучает/устанавливает свойство сделки "Цена открытия сделки"


2.2. Формула плавающей прибыли

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

SymbolInfoString(m_name,SYMBOL_CURRENCY_BASE);

rts specification

Р‌ис. 1. Спецификация RTS-3.17

2.3. Страховка: все ли символы доступны

В‌озможные проблемы: 

Для учета возможных ошибок в советнике на уровне глобальных переменных программы (внимание: программы, а не терминала!) вводится массив для "плохих" символов, отсутствующих в "Обзоре рынка" или для которых нельзя пересчитать валюту прибыли в валюту депозита:

string m_arr_defective_symbols[];                        // array for defective symbols

‌Последовательность прохождения страховки (все этапы проходят в функции "SearchDefectiveSymbols"):

  1. создаём временный (вспомогательный) массив из всех символов, доступных в "Обзоре рынка";
  2. создаём временный (вспомогательный) массив из всех символов торговой истории (в заданном интервале дат с ... по ... );
  3. ищем символы из торговой истории в "Обзоре рынка". Если какой-либо из символов не находится, то он заносится в массив "плохих" символов;
  4. символы из торговой истории добавляем (отображаем) в "Обзоре рынка". Если операция заканчивается неудачей, то символ заносится в массив "плохих" символов;
  5. получаем из свойств символа валюту прибыли. Если валюта прибыли символа отличается от валюты депозита, то переходим к подпункту 5.1.
    1. Пытаемся найти в "Обзоре рынка" символ для пересчёта, который соответствует "валюта депозита"+"валюта прибыли" или "валюта прибыли"+"валюта депозита". Если операция заканчивается неудачей, то символ заносится в массив "плохих" символов

После отработки функции "SearchDefectiveSymbols" будет сформирован массив с "плохими" символами из торговой истории. В дальнейших расчётах (при расчёте графика "Баланс" и графика "Средства") "плохие" символы участвовать не будут.

2.4. Учёт открытых и закрытых позиций на истории

Все изменения баланса отображаем в двумерном массиве "m_arr_balance_equity".

Запрашиваем историю в промежутке дат "from date" и "to date" (это входные параметры эксперта) и делим её на количество циклов продолжительностью "timer (minutes)". Работа выполняется в функции "CreateBalanceEquity". Все сделки записываем в динамический массив указателей "ArrOpenPositions". В дальнейшем работаем с этим массивом: добавляем и удаляем сделки.

В зависимости от параметра торговой сделки "Направление сделки" — ENUM_DEAL_ENTRY — б‌удем по-разному рассчитывать графики "Баланс" и "Средства". Если вы хотите проверить поведение открытия, закрытия, частичного закрытия или переворота позиций на хеджинговых и неттинговых счетах, то можете запустить скрипт "HistorySelect.mq5".

2.5. Как считать плавающую прибыль (Средства)

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

а также имеем конечную дату, по которой запрошена торговая история. Не хватает только цен по символу данной сделки на конечную дату, по которую запрошена торговая история. Цену будем получать при помощи CopyTicks в "CalculationFloatingProfit". Напомню, что формулы прибыли берём из описания перечисления ENUM_SYMBOL_CALC_MODE

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

//--- caching results "CopyTicks"
   static string arr_name[];
   static double arr_ask[];
   static double arr_bid[];
   static datetime prev_time=0;
   int number=-1;
   if(time>prev_time)
     {
      prev_time=time;
      ArrayFree(arr_name);
      ArrayFree(arr_ask);
      ArrayFree(arr_bid);
     }
   found=false;
   size=ArraySize(arr_name);
   if(size>0)
      for(int i=0;i<size;i++)
        {
         if(name==arr_name[i])
           {
            number=i;
            found=true;
            break;
           }
        }
   if(found)
     {
      ArrayResize(ticks_array,1);
      ticks_array[0].ask=arr_ask[number];
      ticks_array[0].bid=arr_bid[number];
     }
   else
     {
      //---
      int copy_ticks=-1;
      int count=0;
      while(copy_ticks==-1 && count<5)
        {
         copy_ticks=CopyTicks(name,ticks_array,COPY_TICKS_INFO,1000*(ulong)time,1);
         //if(copy_ticks==-1)

— результаты bid и ask сохраняются в локальные массивы: если в данном временном промежутке для символа уже были получены тики, то эти тики просто берутся из локальных массивов. Такой подход позволил ускориться в 10-15 раз. 


3. Интегрируем советник в панель диалогов

Теперь, когда основа советника работоспособна, можно снабдить советник более удобным интерфейсом. Исходя из названия статьи "Анализ графиков Баланса/Средств по символам и ORDER_MAGIC советников", необходимо предусмотреть несколько возможностей фильтрации:

Для учета всех торговых символов у нас уже есть массив "m_arr_all_trade_symbols". Он объявлен на глобальном программном уровне. Нужно ввести ещё один массив — "m_arr_all_magics", для учета всех magic'ов. Для этого модернизируем функцию "FillArrayTradeSymbols": теперь в ней будет заполняться ещё и массив "m_arr_all_magics".

3.1. Общий вид панели диалогов

panel

Рис. 2. Общий вид панели

После формирования массивов "плохих" символов, всех торговых символов и magic'ов в панели заполняются два списка (элементы на базе класса CComboBox): левый список — всеми торговыми символами, правый список — всеми magic'ами. В списках на первом месте стоит выбор всех символов и всех magic'ов:

panel combo box

Рис. 3. Выпадающие списки

Логика работы советника панели будет следующая: после и только после нажатия на кнопку "Start" будет проходить проверка того, что именно выбрано в двух выпадающих списках. В зависимости от выбранных параметров, в этих списках будет проходить построение графиков Баланс / Средства на базе торговой истории.

3.2. Взаимодействие с панелью

Я решил, что перенос всего кода советника в класс панели ("APHDialog.mqh") — это слишком долго. Альтернативное решение: в классе панели вводится внутренние переменные m_ready, m_symbol и m_magic:

//+------------------------------------------------------------------+
//| Class CAPHDialog                                                 |
//| Usage: main dialog of the Controls application                   |
//+------------------------------------------------------------------+
class CAPHDialog : public CAppDialog
  {
private:
   CLabel            m_label_symbols;                 // the label object
   CComboBox         m_combo_box_symbols;             // the combo boxp object
   CLabel            m_label_magics;                  // the label object
   CComboBox         m_combo_box_magics;              // the combo box object
   CButton           m_button_start;                  // the button object
   //---
   bool              m_ready;                         // true -> you can build graphics
   string            m_symbol;
   ulong             m_magic;

public:

Решение о состоянии переменной m_ready будет приниматься в процедуре обработки клика на кнопке "Start":

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CAPHDialog::OnClickButtonStart(void)
  {
   if(m_combo_box_symbols.Select()!=" " && m_combo_box_magics.Select()!=" ")
      m_ready=true;
   else
      m_ready=false;
//Comment(__FUNCTION__+" ButtonStartclick"+"\n"+
//        "Symbols: "+"\""+m_combo_box_symbols.Select()+"\""+"\n"+
//        "Magic: "+"\""+m_combo_box_magics.Select()+"\""+"\n"+
//        "m_ready: "+IntegerToString(m_ready));
  }

Обратите внимание на выделенную строку: сразу после создания и заполнения выпадающих списков (у нас это элементы m_combo_box_symbols и m_combo_box_magics) в списках установлен элемент со значением " ", то есть пробел.

Решение о состоянии переменных m_symbol и m_magic будет приниматься в процедурах обработки клика на соответствующих выпадающих списках:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CAPHDialog::OnChangeComboBoxSymbols(void)
  {
//Comment(__FUNCTION__+" \""+m_combo_box_symbols.Select()+"\"");
   if(m_combo_box_symbols.Select()=="All symbols")
      m_symbol="";
   else
      m_symbol=m_combo_box_symbols.Select();
  }
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CAPHDialog::OnChangeComboBoxMagics(void)
  {
//Comment(__FUNCTION__+" \""+m_combo_box_magics.Select()+"\"");
   if(m_combo_box_magics.Select()=="All magics")
      m_magic=-1;
   else
      m_magic=StringToInteger(m_combo_box_magics.Select());
  }

Таким образом, после клика на кнопке "Start" будут заполнены три переменные: m_ready, m_symbol и m_magic. Осталось придумать, как сообщить советнику, что на панели осуществлён выбор параметров. Решение простое: в советнике запускаем таймер с интервалом 3 секунды, который и будет опрашивать панель. Для этого в панели напишем метод "CAPHDialog::IsReady"

//+------------------------------------------------------------------+
//| On the panel there are chosen parameters                         |
//+------------------------------------------------------------------+
bool CAPHDialog::IsReady(string &symbol,ulong &magic)
  {
   if(m_ready)
     {
      symbol=m_symbol;
      magic=m_magic;
      m_ready=false;
      return(true);
     }
   else
      return(false);
  }

В этом методе записываем значение внутренних переменных в переменные переданные по ссылке и сбрасываем внутреннюю переменную m_ready.

3.3. Небольшая корректировка — учёт выбранного magic'а

Отбор сделок происходит согласно заданным условиям: по символу или по всем символам и по magic'у или по всем magic'ам, в "GetHistory":

//--- for all deals 
   for(int i=0;i<deals;i++)
     {
      deal_ticket          = HistoryDealGetTicket(i);
      deal_position_ID     = HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);
      deal_symbol          = HistoryDealGetString(deal_ticket,DEAL_SYMBOL);
      deal_type            = (ENUM_DEAL_TYPE)HistoryDealGetInteger(deal_ticket,DEAL_TYPE);
      deal_entry           = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket,DEAL_ENTRY);
      deal_volume          = HistoryDealGetDouble(deal_ticket,DEAL_VOLUME);
      deal_price           = HistoryDealGetDouble(deal_ticket,DEAL_PRICE);
      deal_commission      = HistoryDealGetDouble(deal_ticket,DEAL_COMMISSION);
      deal_swap            = HistoryDealGetDouble(deal_ticket,DEAL_SWAP);
      deal_profit          = HistoryDealGetDouble(deal_ticket,DEAL_PROFIT);
      deal_magic           = HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);
      if(sSymbol!="")
         if(deal_symbol!=sSymbol)
            continue;
      if(uMagic!=ULONG_MAX)
         if(deal_magic!=uMagic)
            continue;
      //--- onle BUY or SELL

Обратите внимание, что для переменной uMagic значение ULONG_MAX означает "все magic'и", а для переменной sSymbol значение "" означает "Все символы".

Видео работы построения баланса и плавающей прибыли на основе торговой истории (сначала для всех символов, затем — только для одного символа):


Видео


4. Графики распределения MFE и MAE

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

4.1. Принцип учёта MFE и MAE

На глобальном программном уровне объявляются два массива — "m_arr_pos_id" для учета позиций и "m_arr_mfe_mae" для учета MFE, итогового финансового результата и MAE: 

long   m_arr_pos_id[];
double m_arr_mfe_mae[][3];  // [][0] - mfe, [][1] - final financial result,[][2] - mae

Впоследствии, на основании массива "m_arr_mfe_mae", будут строиться точечные графики распределения MFE и MAE.

Массивы "m_arr_pos_id" для учета позиций и "m_arr_mfe_mae" для учета mfe всегда имеют один размер в первом измерении — таким образом, для позиции с индексом "i" (m_arr_pos_id[i]) всегда будет однозначное соответствие m_arr_mfe_mae[i][][][]. Размеры этих двух массивов задаются в "GetHistory":

         if(deal_symbol=="")
            DebugBreak();
         ArrOpenPositions.Add(HistoryDeal);
         //--- mfe, mae
         int size=ArraySize(m_arr_pos_id);
         ArrayResize(m_arr_pos_id,size+1,10);
         ArrayResize(m_arr_mfe_mae,size+1,10);
         m_arr_pos_id[size]=deal_position_ID;
         // [][0] - mfe, [][1] - final financial result,[][2] - mae
         m_arr_mfe_mae[size][0]=0.0;
         m_arr_mfe_mae[size][1]=0.0;
         m_arr_mfe_mae[size][2]=0.0;
         continue;
        }
      //--- 
      if(deal_entry==DEAL_ENTRY_OUT)

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

//+------------------------------------------------------------------+
//| Add Result Mfe Mae                                               |
//+------------------------------------------------------------------+
void AddResultMfeMae(const long pos_id,const double floating_profit,const double financial_result)
  {
// [][0] - mfe (profit), [][1] - final financial result,[][2] - mae (loss)
//--- search pos_id
   int position=-1;
   int size=ArraySize(m_arr_pos_id);
   for(int i=0;i<size;i++)
      if(m_arr_pos_id[i]==pos_id)
        {
         position=i;
         break;
        }
   if(position==-1)
      return;

//---
   if(floating_profit==0.0)
      return;

   if(floating_profit>0.0) // profit
     {
      if(m_arr_mfe_mae[position][0]<floating_profit)
         m_arr_mfe_mae[position][0]=floating_profit;
     }
   else // loss
     {
      if(m_arr_mfe_mae[position][2]>floating_profit)
         m_arr_mfe_mae[position][2]=floating_profit;
     }
   m_arr_mfe_mae[position][1]=financial_result;
  }

Здесь правило: нужно передавать все три параметра. То есть, если наша виртуальная позиция ещё находится в списке открытых позиций "ArrOpenPositions" и её плавающая прибыль равна "-20.2", то вызов будет иметь вид:

    AddResultMfeMae(pos_id,-20.2,0.0);

если наша виртуальная позиция ещё находится в списке открытых позиций "ArrOpenPositions" и её плавающая прибыль равна "+5.81", то вызов будет иметь вид:

    AddResultMfeMae(pos_id,5.81,0.0);

а если наша виртуальная позиция удаляется из списка открытых позиций "ArrOpenPositions" и её итоговый финансовый результат равен  "-3.06", то вызов будет иметь вид:

    AddResultMfeMae(pos_id,0.0,-3.06);

То есть, если есть плавающая прибыль, значит, итоговый финансовый результат равен "0", если позиция закрывается — значит плавающая прибыль равна "0".

4.2. Обработка позиций

Также, в "GetHistory", если нужно провести пересчёт плавающей прибыли, то для каждой позиции в массив "m_arr_mfe_mae" записываем [плавающая прибыль][итоговый финансовый результат 0.0].

4.3. Изменённая панель

Для того, чтобы отображать и график Баланс / Средства и MFE и MAE немного видоизменится панель:

panel 2

Рис. 4. Изменённая панель

Графики MFE (максимальная прибыль) и MAE (максимальный убыток) строятся по двум координатам: координата "X" - значение итогового финансового результата позиции, а по оси "Y" - значение MFE или MAE соответственно.

MFE

Рис. 5. MFE

MAE

Рис. 6. MAE


Заключение

Теперь на hedge счетах, при одновременной торговли нескольких советников, можно просмотреть статистику баланса и средств по каждому символу и каждому magic'у - то есть визуально определить, каков вклад конкретного советника (ORDER_MAGIC) в общий баланс и, самое главное, какие были просадки у каждого конкретного советника.

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

#
 Имя
Тип
Описание
1
HistoryDeal.mqhБиблиотека Класс ведения учета открытых позиций на выбранном промежутке
2
HistoryDealGetTicket.mq5Эксперт Пример получения свойств сделок из торговой истории за заданный промежуток времени
3
APHDialog.mqhБиблиотека Класс панели диалога советника
4
Accounting_positions_on_history.mq5Эксперт Главный советник - по мотивам статьи
5
MQL5.zipАрхив Архив с главным советником и его включаемыми файлами.