English 中文 Español Deutsch 日本語 Português
Визуализируем оптимизацию торговой стратегии в MetaTrader 5

Визуализируем оптимизацию торговой стратегии в MetaTrader 5

MetaTrader 5Тестер | 21 февраля 2018, 16:29
4 813 64
Anatoli Kazharski
Anatoli Kazharski

Содержание

Введение

При разработке торговых алгоритмов полезно просматривать результаты тестов во время оптимизации параметров. Но для понимания эффективности торгового алгоритма бывает недостаточно одного графика на вкладке График оптимизации. Совсем другое дело — видеть кривые баланса множества тестов одновременно прямо во время оптимизации, а также иметь возможность просмотреть их даже после окончания процесса. В статье Визуализируй стратегию в тестере MetaTrader 5 подобное приложение уже было продемонстрировано. Но с тех пор появилось множество новых возможностей. Поэтому сейчас такое приложение можно реализовать совсем в другом, более высоком качестве.

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

Разработка графического интерфейса

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

Итак, для начала изобразим общую структуру разрабатываемого приложения. В файле Program.mqh будет содержаться класс приложения — CProgram. Этот базовый класс должен быть связан с движком графической библиотеки.

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Класс библиотеки для создания графического интерфейса
#include <EasyAndFastGUI\WndEvents.mqh>
//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
  };

Чтобы не загромождать картину, на схеме библиотека EasyAndFast обозначена одним блоком (Library GUI). Ее полную схему вы можете посмотреть на странице библиотеки

 Рис. 1 – Подключение библиотеки для создания GUI.

Рис. 1. Подключение библиотеки для создания GUI.

Для связи с главными функциями MQL-программы в классе CProgram нужно создать аналогичные методы. Для работы с фреймами оптимизации понадобятся методы из категории OnTesterXXX().

class CProgram : public CWndEvents
  {
public:
   //--- Инициализация/деинициализация
   bool              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);
   //--- Обработчик события "новый тик"
   void              OnTickEvent(void);
   //--- Обработчик торгового события
   void              OnTradeEvent(void);
   //--- Таймер
   void              OnTimerEvent(void);
   //--- Тестер
   double            OnTesterEvent(void);
   void              OnTesterPassEvent(void);
   void              OnTesterInitEvent(void);
   void              OnTesterDeinitEvent(void);
  };

Тогда в главном файле приложения все эти методы нужно вызывать так:

//--- Подключение класса приложения
#include "Program.mqh"
CProgram program;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- Инициализация программы
   if(!program.OnInitEvent())
     {
      ::Print(__FUNCTION__," > Failed to initialize!");
      return(INIT_FAILED);
     }  
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) { program.OnDeinitEvent(reason); }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void) { program.OnTickEvent(); }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(void) { program.OnTimerEvent(); }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
   { program.ChartEvent(id,lparam,dparam,sparam); }
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester(void) { return(program.OnTesterEvent()); }
//+------------------------------------------------------------------+
//| TesterInit function                                              |
//+------------------------------------------------------------------+
void OnTesterInit(void) { program.OnTesterInitEvent(); }
//+------------------------------------------------------------------+
//| TesterPass function                                              |
//+------------------------------------------------------------------+
void OnTesterPass(void) { program.OnTesterPassEvent(); }
//+------------------------------------------------------------------+
//| TesterDeinit function                                            |
//+------------------------------------------------------------------+
void OnTesterDeinit(void) { program.OnTesterDeinitEvent(); }
//+------------------------------------------------------------------+

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

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

  • Форма для элементов управления.
  • Поле ввода для указания количества балансов, которые будут отображаться на графике.
  • Поле ввода для регулирования скорости повторного показа результатов оптимизации.
  • Кнопка для запуска повторного показа.
  • Таблица для отображения статистики результата.
  • Таблица для отображения внешних параметров эксперта.
  • График для отображения кривых баланса.
  • График для отображения всех результатов оптимизации.
  • Строка состояния для показа дополнительной итоговой информации.
  • Индикатор выполнения, который во время повторной прокрутки будет отображать процент показанных результатов из общего количества.

Здесь приведём только объявления экземпляров классов элементов управления и методов для их создания (см. листинг кода ниже). Код самих методов вынесем в отдельный файл — CreateFrameModeGUI.mqh, который свяжем с файлом класса CProgram. По мере того, как растет код разрабатываемого приложения, такой метод распределения по отдельным файлам становится актуальным. Так проще ориентироваться в проекте.

class CProgram : public CWndEvents
  {
private:
   //--- Окно
   CWindow           m_window1;
   //--- Статусная строка
   CStatusBar        m_status_bar;
   //--- Поля ввода
   CTextEdit         m_curves_total;
   CTextEdit         m_sleep_ms;
   //--- Кнопки
   CButton           m_reply_frames;
   //--- Таблицы
   CTable            m_table_stat;
   CTable            m_table_param;
   //--- Графики
   CGraph            m_graph1;
   CGraph            m_graph2;
   //--- Индикатор выполнения
   CProgressBar      m_progress_bar;
   //---
public:
   //--- Создаёт графический интерфейс для работы с фреймами в режиме оптимизации
   bool              CreateFrameModeGUI(void);
   //---
private:
   //--- Форма
   bool              CreateWindow(const string text);
   //--- Статусная строка
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- Таблицы
   bool              CreateTableStat(const int x_gap,const int y_gap);
   bool              CreateTableParam(const int x_gap,const int y_gap);
   //--- Поля ввода
   bool              CreateCurvesTotal(const int x_gap,const int y_gap,const string text);
   bool              CreateSleep(const int x_gap,const int y_gap,const string text);
   //--- Кнопки
   bool              CreateReplyFrames(const int x_gap,const int y_gap,const string text);
   //--- Графики
   bool              CreateGraph1(const int x_gap,const int y_gap);
   bool              CreateGraph2(const int x_gap,const int y_gap);
   //--- Индикатор выполнения
   bool              CreateProgressBar(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| Методы для создания элементов управления                         |
//+------------------------------------------------------------------+
#include "CreateFrameModeGUI.mqh"
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//|                                           CreateFrameModeGUI.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Program.mqh"
//+------------------------------------------------------------------+
//| Создаёт графический интерфейс                                    |
//| для анализа результатов оптимизации и работы с фреймами          |
//+------------------------------------------------------------------+
bool CProgram::CreateFrameModeGUI(void)
  {
//--- Создание этого интерфейса только в режиме работы с фреймами оптимизации
   if(!::MQLInfoInteger(MQL_FRAME_MODE))
      return(false);
//--- Создание формы для элементов управления
   if(!CreateWindow("Frame mode"))
      return(false);
//--- Создание элементов управления
   if(!CreateStatusBar(1,23))
      return(false);
   if(!CreateCurvesTotal(7,25,"Curves total:"))
      return(false);
   if(!CreateSleep(145,25,"Sleep:"))
      return(false);
   if(!CreateReplyFrames(255,25,"Replay frames"))
      return(false);
   if(!CreateTableStat(2,50))
      return(false);
   if(!CreateTableParam(2,212))
      return(false);
   if(!CreateGraph1(200,50))
      return(false);
   if(!CreateGraph2(200,159))
      return(false);
//--- Индикатор выполнения
   if(!CreateProgressBar(2,3,"Processing..."))
      return(false);
//--- Завершение создания GUI
   CWndEvents::CompletedGUI();
   return(true);
  }
...

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

 Рис. 2 – Разделение проекта на несколько файлов.

Рис. 2.  Разделение проекта на несколько файлов.



Разработка класса для работы с данными фреймов

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

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

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

В библиотеке EasyAndFast для визуализации данных используется класс CGraphic из стандартной библиотеки. Чтобы иметь доступ к его методам, подключим его к файлу FrameGenerator.mqh.

//+------------------------------------------------------------------+
//|                                               FrameGenerator.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <Graphics\Graphic.mqh>
//+------------------------------------------------------------------+
//| Класс для получения результатов оптимизации                      |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
  };

Схема программы теперь выглядит так:

 Рис. 3 – Подключение к проекту классов для работы.

Рис. 3.  Подключение к проекту классов для работы.

Теперь рассмотрим, как устроен класс CFrameGenerator. В нем тоже нужны методы для обработки событий тестера стратегий (см. листинг кода ниже). Они будут вызываться в аналогичных методах класса приложения, которое мы разрабатываем — CProgram. В метод CFrameGenerator::OnTesterInitEvent() будут передаваться указатели на объекты графиков, на которых отображается текущий процесс оптимизации. 

  • На первом графике (graph_balance) отображается указанное количество последних серий балансов результатов оптимизации.
  • На втором графике (graph_result) отображаются общие результаты оптимизации.
class CFrameGenerator
  {
private:
   //--- Указатели на графики для визуализации данных
   CGraphic         *m_graph_balance;
   CGraphic         *m_graph_results;
   //---
public:
   //--- Обработчики событий тестера стратегий
   void              OnTesterEvent(const double on_tester_value);
   void              OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_result);
   void              OnTesterDeinitEvent(void);
   bool              OnTesterPassEvent(void);
  };
//+------------------------------------------------------------------+
//| Должна вызываться в обработчике OnTesterInit()                   |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results)
  {
   m_graph_balance =graph_balance;
   m_graph_results =graph_results;
  }

На обоих графиках положительные результаты будут отображаться зелёным цветом, а отрицательные —  красным.

В методе CFrameGenerator::OnTesterEvent() мы получаем баланс результата теста и статистические показатели. Эти данные будем передавать во фрейм с помощью методов CFrameGenerator::GetBalanceData() и CFrameGenerator::GetStatData(). В методе CFrameGenerator::GetBalanceData() получаем всю историю теста и суммируем все in-/inout-сделки. Полученный результат пошагово сохраняется в массив m_balance[]. Этот массив, в свою очередь, является членом класса CFrameGenerator.

В метод CFrameGenerator::GetStatData() передаётся динамический массив, который потом будет отправлен во фрейм. Размер для него устанавливается такой же, как для массива ранее полученного баланса результата, плюс добавляется количество элементов, в которые мы получаем некоторые статистические показатели.

//--- Количество статистических показателей
#define STAT_TOTAL 7
//+------------------------------------------------------------------+
//| Класс для работы с результатами оптимизации                      |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Баланс результата
   double            m_balance[];
   //---
private:
   //--- Получает данные баланса
   int               GetBalanceData(void);
   //--- Получает статистические данные
   void              GetStatData(double &dst_array[],double on_tester_value);
  };
//+------------------------------------------------------------------+
//| Получает данные баланса                                          |
//+------------------------------------------------------------------+
int CFrameGenerator::GetBalanceData(void)
  {
   int    data_count      =0;
   double balance_current =0;
//--- Запросим всю торговую историю
   ::HistorySelect(0,LONG_MAX);
   uint deals_total=::HistoryDealsTotal();
//--- Собираем данные о сделках
   for(uint i=0; i<deals_total; i++)
     {
      //--- Получили тикет
      ulong ticket=::HistoryDealGetTicket(i);
      if(ticket<1)
         continue;
      //--- Если начальный баланс или out-/inout-сделка
      long entry=::HistoryDealGetInteger(ticket,DEAL_ENTRY);
      if(i==0 || entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT)
        {
         double swap      =::HistoryDealGetDouble(ticket,DEAL_SWAP);
         double profit    =::HistoryDealGetDouble(ticket,DEAL_PROFIT);
         double commision =::HistoryDealGetDouble(ticket,DEAL_COMMISSION);
         //--- Расчёт баланса
         balance_current+=(profit+swap+commision);
         //--- Сохранить в массив
         data_count++;
         ::ArrayResize(m_balance,data_count,100000);
         m_balance[data_count-1]=balance_current;
        }
     }
//--- Вернуть количество данных
   return(data_count);
  }
//+------------------------------------------------------------------+
//| Получает статистические данные                                   |
//+------------------------------------------------------------------+
void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value)
  {
   ::ArrayResize(dst_array,::ArraySize(m_balance)+STAT_TOTAL);
   ::ArrayCopy(dst_array,m_balance,STAT_TOTAL,0);
//--- Заполним первые значения массива (STAT_TOTAL) результатами тестирования
   dst_array[0] =::TesterStatistics(STAT_PROFIT);               // чистая прибыль
   dst_array[1] =::TesterStatistics(STAT_PROFIT_FACTOR);        // фактор прибыльности
   dst_array[2] =::TesterStatistics(STAT_RECOVERY_FACTOR);      // фактор восстановления
   dst_array[3] =::TesterStatistics(STAT_TRADES);               // количество трейдов
   dst_array[4] =::TesterStatistics(STAT_DEALS);                // количество сделок
   dst_array[5] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // максимальная просадка средств в процентах
   dst_array[6] =on_tester_value;                               // значение пользовательского критерия оптимизации
  }

Методы CFrameGenerator::GetBalanceData() и CFrameGenerator::GetStatData() вызываются в обработчике событий окончания тестирования — CFrameGenerator::OnTesterEvent(). Данные получены. Отправляем их во фрейме в терминал

//+------------------------------------------------------------------+
//| Готовит массив значений баланса и отправляет его во фрейме       |
//| Функция должна вызываться в эксперте в обработчике  OnTester()   |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterEvent(const double on_tester_value)
  {
//--- Получим данные баланса
   int data_count=GetBalanceData();
//--- Массив для отправки данных во фрейм
   double stat_data[];
   GetStatData(stat_data,on_tester_value);
//--- Cоздадим фрейм с данными и отправим его в терминал
   if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME),1,data_count,stat_data))
      ::Print(__FUNCTION__," > Frame add error: ",::GetLastError());
   else
      ::Print(__FUNCTION__," > Frame added, Ok");
  }

Теперь рассмотрим методы, которые будут использоваться в обработчике событий прихода фреймов во время оптимизации — CFrameGenerator::OnTesterPassEvent(). Понадобятся переменные для работы с фреймами: имя, идентификатор, номер прохода, принимаемое значение и принимаемый массив данных. Все эти данные отправляются во фрейм с помощью функции FrameAdd(), показанной выше.

class CFrameGenerator
  {
private:
   //--- Переменные для работы с фреймами
   string            m_name;
   ulong             m_pass;
   long              m_id;
   double            m_value;
   double            m_data[];
  };

В методе CFrameGenerator::SaveStatData() из массива, который мы приняли во фрейме, будем забирать статистические показатели и сохранять их в отдельный строковый массив. Здесь данные будут содержать название показателя и его значение. В качестве разделителя используется символ '='.

class CFrameGenerator
  {
private:
   //--- Массив со статистическими показателями
   string            m_stat_data[];
   //---
private:
   //--- Сохранить статистические данные 
   void              SaveStatData(void);
  };
//+------------------------------------------------------------------+
//| Сохраняет статистические показатели результата в массив          |
//+------------------------------------------------------------------+
void CFrameGenerator::SaveStatData(void)
  {
//--- Массив для приёма статистических показателей фрейма
   double stat[];
   ::ArrayCopy(stat,m_data,0,0,STAT_TOTAL);
   ::ArrayResize(m_stat_data,STAT_TOTAL);
//--- Заполним массив результатами тестирования
   m_stat_data[0] ="Net profit="+::StringFormat("%.2f",stat[0]);
   m_stat_data[1] ="Profit Factor="+::StringFormat("%.2f",stat[1]);
   m_stat_data[2] ="Factor Recovery="+::StringFormat("%.2f",stat[2]);
   m_stat_data[3] ="Trades="+::StringFormat("%G",stat[3]);
   m_stat_data[4] ="Deals="+::StringFormat("%G",stat[4]);
   m_stat_data[5] ="Equity DD="+::StringFormat("%.2f%%",stat[5]);
   m_stat_data[6] ="OnTester()="+::StringFormat("%G",stat[6]);
  }

Статистические данные нужно сохранять в отдельный массив, чтобы потом их можно было бы получить в классе приложения (CProgram) для заполнения таблицы. Для их получения вызывается публичный метод CFrameGenerator::CopyStatData(), передав массив для копирования.

class CFrameGenerator
  {
public:
   //--- Возвращает статистические показатели в переданный массив
   int               CopyStatData(string &dst_array[]) { return(::ArrayCopy(dst_array,m_stat_data)); }
  };

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

//--- Резервный размер для массивов
#define RESERVE_FRAMES 1000000
//+------------------------------------------------------------------+
//| Класс для работы с результатами оптимизации                      |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Счётчик фреймов
   ulong             m_frames_counter;
   //--- Данные положительных и отрицательных исходов
   double            m_loss_x[];
   double            m_loss_y[];
   double            m_profit_x[];
   double            m_profit_y[];
   //---
private:
   //--- Добавляет (1) отрицательный и (2) положительный результат в массивы
   void              AddLoss(const double loss);
   void              AddProfit(const double profit);
  };
//+------------------------------------------------------------------+
//| Добавляет отрицательный результат в массив                       |
//+------------------------------------------------------------------+
void CFrameGenerator::AddLoss(const double loss)
  {
   int size=::ArraySize(m_loss_y);
   ::ArrayResize(m_loss_y,size+1,RESERVE_FRAMES);
   ::ArrayResize(m_loss_x,size+1,RESERVE_FRAMES);
   m_loss_y[size] =loss;
   m_loss_x[size] =(double)m_frames_counter;
  }
//+------------------------------------------------------------------+
//| Добавляет положительный результат в массив                       |
//+------------------------------------------------------------------+
void CFrameGenerator::AddProfit(const double profit)
  {
   int size=::ArraySize(m_profit_y);
   ::ArrayResize(m_profit_y,size+1,RESERVE_FRAMES);
   ::ArrayResize(m_profit_x,size+1,RESERVE_FRAMES);
   m_profit_y[size] =profit;
   m_profit_x[size] =(double)m_frames_counter;
  }

Здесь основные методы обновления графиков — CFrameGenerator::UpdateResultsGraph() и CFrameGenerator::UpdateBalanceGraph():

class CFrameGenerator
  {
private:
   //--- Обновить график результатов
   void              UpdateResultsGraph(void);
   //--- Обновить график балансов
   void              UpdateBalanceGraph(void);
  };

В методе CFrameGenerator::UpdateResultsGraph() результаты тестов (положительная/отрицательная прибыль) добавляются в массивы. Затем эти данные отображаются на соответствующем графике. В названиях серий этого графика будем показывать текущее количество положительных и отрицательных исходов. 

//+------------------------------------------------------------------+
//| Обновить график результатов                                      |
//+------------------------------------------------------------------+
void CFrameGenerator::UpdateResultsGraph(void)
  {
//--- Отрицательный результат
   if(m_data[0]<0)
      AddLoss(m_data[0]);
//--- Положительный результат
   else
      AddProfit(m_data[0]);
//--- Обновить серии на графике результатов оптимизации
   CCurve *curve=m_graph_results.CurveGetByIndex(0);
   curve.Name("P: "+(string)ProfitsTotal());
   curve.Update(m_profit_x,m_profit_y);
//---
   curve=m_graph_results.CurveGetByIndex(1);
   curve.Name("L: "+(string)LossesTotal());
   curve.Update(m_loss_x,m_loss_y);
//--- Свойства горизонтальной оси
   CAxis *x_axis=m_graph_results.XAxis();
   x_axis.Min(0);
   x_axis.Max(m_frames_counter);
   x_axis.DefaultStep((int)(m_frames_counter/8.0));
//--- Обновить график
   m_graph_results.CalculateMaxMinValues();
   m_graph_results.CurvePlotAll();
   m_graph_results.Update();
  }

В самом начале метода CFrameGenerator::UpdateBalanceGraph() из массива данных, переданных во фрейме, извлекаются те, которые относятся к балансу. Так как на графике одновременно может отображаться несколько серий, то сделаем так, чтобы обновление серий было последовательным. Для этого будем использовать отдельный счётчик серий. Чтобы настроить количество одновременно отображаемых серий балансов на графике, нужен публичный метод CFrameGenerator::SetCurvesTotal(). Как только счётчик серий в нем доходит до установленного лимита, то отсчёт начинается сначала. В качестве названий серий будет выступать счётчик фреймов. Цвет серий тоже будет зависеть от результата: зеленый при положительном результате, красный — при отрицательном.

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

class CFrameGenerator
  {
private:
   //--- Количество серий
   uint              m_curves_total;
   //--- Индекс текущей серии на графике
   uint              m_last_serie_index;
   //--- Для определения максимальной серии
   double            m_curve_max[];
   //---
public:
   //--- Установка количества серий для отображения на графике
   void              SetCurvesTotal(const uint total);
  };
//+------------------------------------------------------------------+
//| Установка количества серий для отображения на графике            |
//+------------------------------------------------------------------+
void CFrameGenerator::SetCurvesTotal(const uint total)
  {
   m_curves_total=total;
   ::ArrayResize(m_curve_max,total);
   ::ArrayInitialize(m_curve_max,0);
  }
//+------------------------------------------------------------------+
//| Обновить график балансов                                         |
//+------------------------------------------------------------------+
void CFrameGenerator::UpdateBalanceGraph(void)
  {
//--- Массив для приема значений баланса текущего фрейма
   double serie[];
   ::ArrayCopy(serie,m_data,0,STAT_TOTAL,::ArraySize(m_data)-STAT_TOTAL);
//--- Отправим массив для вывода на график баланса
   CCurve *curve=m_graph_balance.CurveGetByIndex(m_last_serie_index);
   curve.Name((string)m_frames_counter);
   curve.Color((m_data[0]>=0)? ::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed));
   curve.Update(serie);
//--- Получим размер серии
   int serie_size=::ArraySize(serie);
   m_curve_max[m_last_serie_index]=serie_size;
//--- Определим ряд с максимальным количеством элементов
   double x_max=0;
   for(uint i=0; i<m_curves_total; i++)
      x_max=::fmax(x_max,m_curve_max[i]);
//--- Свойства горизонтальной оси
   CAxis *x_axis=m_graph_balance.XAxis();
   x_axis.Min(0);
   x_axis.Max(x_max);
   x_axis.DefaultStep((int)(x_max/8.0));
//--- Обновить график
   m_graph_balance.CalculateMaxMinValues();
   m_graph_balance.CurvePlotAll();
   m_graph_balance.Update();
//--- Увеличим счётчик серий
   m_last_serie_index++;
//--- Если дошли до лимита, обнулим счётчик серий
   if(m_last_serie_index>=m_curves_total)
      m_last_serie_index=0;
  }

Итак, мы рассмотрели методы, необходимые, чтобы организовать работу в обработчике фреймов. Теперь разберём, как устроен сам этот метод-обработчик CFrameGenerator::OnTesterPassEvent(). Он возвращает true, пока идёт процесс оптимизации и функция FrameNext() получает данные фреймов. По завершении оптимизации метод вернёт false.

В списке параметров эксперта, которые можно получить с помощью функции FrameInputs(), сначала идут выставленные на оптимизацию параметры, а уже потом те, которые не участвуют в оптимизации. 

Если данные фрейма получены, то с помощью функции FrameInputs() получаем параметры эксперта на текущем проходе оптимизации. Далее сохраняем статистические показатели, обновляем графики и увеличиваем счётчик фреймов. После этого метод CFrameGenerator::OnTesterPassEvent() возвращает true до следующего вызова.

class CFrameGenerator
  {
private:
   //--- Параметры эксперта
   string            m_param_data[];
   uint              m_par_count;
  };
//+------------------------------------------------------------------+
//| Получает фрейм с данными при оптимизации и отображает график     |
//+------------------------------------------------------------------+
bool CFrameGenerator::OnTesterPassEvent(void)
  {
//--- При получении нового фрейма пытаемся получить из него данные
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Получим входные параметры эксперта, для которых сформирован фрейм
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- Сохраняем статистические показатели результата в массив
      SaveStatData();
      //--- Обновить график результатов и балансов
      UpdateResultsGraph();
      UpdateBalanceGraph();
      //--- Увеличим счетчик обработанных фреймов
      m_frames_counter++;
      return(true);
     }
//---
   return(false);
  }

По окончании оптимизации в режиме обработки фреймов генерируется событие TesterDeinit и вызывается метод CFrameGenerator::OnTesterDeinitEvent(). На текущий момент не все фреймы можно обработать во время процесса оптимизации, поэтому график визуализации результатов будет неполным. Чтобы увидеть полную картину, нужно сразу после оптимизации пройтись в цикле по всем фреймам методом CFrameGenerator::FinalRecalculateFrames() и снова обновить график.

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

class CFrameGenerator
  {
private:
   //--- Освободить массивы
   void              ArraysFree(void);
   //--- Финальный пересчёт данных со всех фреймов после оптимизации
   void              FinalRecalculateFrames(void);
  };
//+------------------------------------------------------------------+
//| Освободить массивы                                               |
//+------------------------------------------------------------------+
void CFrameGenerator::ArraysFree(void)
  {
   ::ArrayFree(m_loss_y);
   ::ArrayFree(m_loss_x);
   ::ArrayFree(m_profit_y);
   ::ArrayFree(m_profit_x);
  }
//+------------------------------------------------------------------+
//| Финальный пересчёт данных со всех фреймов после оптимизации      |
//+------------------------------------------------------------------+
void CFrameGenerator::FinalRecalculateFrames(void)
  {
//--- Переводим указатель фреймов в начало
   ::FrameFirst();
//--- Сброс счётчика и массивов
   ArraysFree();
   m_frames_counter=0;
//--- Запускаем перебор фреймов
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Отрицательный результат
      if(m_data[0]<0)
         AddLoss(m_data[0]);
      //--- Положительный результат
      else
         AddProfit(m_data[0]);
      //--- Увеличим счётчик обработанных фреймов
      m_frames_counter++;
     }
//--- Обновить серии на графике
   CCurve *curve=m_graph_results.CurveGetByIndex(0);
   curve.Name("P: "+(string)ProfitsTotal());
   curve.Update(m_profit_x,m_profit_y);
//---
   curve=m_graph_results.CurveGetByIndex(1);
   curve.Name("L: "+(string)LossesTotal());
   curve.Update(m_loss_x,m_loss_y);
//--- Свойства горизонтальной оси
   CAxis *x_axis=m_graph_results.XAxis();
   x_axis.Min(0);
   x_axis.Max(m_frames_counter);
   x_axis.DefaultStep((int)(m_frames_counter/8.0));
//--- Обновить график
   m_graph_results.CalculateMaxMinValues();
   m_graph_results.CurvePlotAll();
   m_graph_results.Update();
  }

Тогда код метода CFrameGenerator::OnTesterDeinitEvent() будет таким, как показано в листинге ниже. Здесь же запоминаем общее количество фреймов и обнуляем счётчик.

//+------------------------------------------------------------------+
//| Должна вызываться в обработчике OnTesterDeinit()                 |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterDeinitEvent(void)
  {
//--- Финальный пересчёт данных со всех фреймов после оптимизации
   FinalRecalculateFrames();
//--- Запоминаем общее количество фреймов и обнуляем счётчики
   m_frames_total     =m_frames_counter;
   m_frames_counter   =0;
   m_last_serie_index =0;
  }

Далее рассмотрим, как использовать методы класса CFrameGenerator в классе приложения. 

Работа с данными оптимизации в классе приложения

Графический интерфейс нашего приложения будем создавать в методе инициализации теста — CProgram::OnTesterInitEvent(). После создания нужно сделать графический интерфейс недоступным. Для этого понадобятся дополнительные методы CProgram::IsAvailableGUI() и CProgram::IsLockedGUI(), которые будут использоваться и в других методах класса CProgram.

Инициализируем генератор фреймов: для этого передадим указатели на графики, на которых будут визуализированы результаты оптимизации.

class CProgram : public CWndEvents
  {
private:
   //--- Доступность интерфейса
   void              IsAvailableGUI(const bool state);
   void              IsLockedGUI(const bool state);
  }
//+------------------------------------------------------------------+
//| Событие начала процесса оптимизации                              |
//+------------------------------------------------------------------+
void CProgram::OnTesterInitEvent(void)
  {
//--- Создание графического интерфейса
   if(!CreateFrameModeGUI())
     {
      ::Print(__FUNCTION__," > Could not create the GUI!");
      return;
     }
//--- Сделать интерфейс недоступным
   IsLockedGUI(false);
//--- Инициализация генератора фреймов
   m_frame_gen.OnTesterInitEvent(m_graph1.GetGraphicPointer(),m_graph2.GetGraphicPointer());
  }
//+------------------------------------------------------------------+
//| Доступность интерфейса                                           |
//+------------------------------------------------------------------+
void CProgram::IsAvailableGUI(const bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsAvailable(state);
   m_curves_total.IsAvailable(state);
   m_reply_frames.IsAvailable(state);
  }
//+------------------------------------------------------------------+
//| Блокировка интерфейса                                            |
//+------------------------------------------------------------------+
void CProgram::IsLockedGUI(const bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsLocked(!state);
   m_curves_total.IsLocked(!state);
   m_reply_frames.IsLocked(!state);
  }

Выше мы уже говорили о том, что данные в таблицах будем обновлять в классе приложения с помощью методов CProgram::UpdateStatTable() и CProgram::UpdateParamTable(). Код обеих таблиц идентичен, поэтому приведём для примера только один из них. Названия показателей/параметров и их значения в одной строке отображены через разделитель ‘=’. Поэтому в цикле проходим по ним и расщепляем в отдельный массив на два элемента. Затем заносим эти значения в ячейки таблицы.

class CProgram : public CWndEvents
  {
private:
   //--- Обновление статистической таблицы
   void              UpdateStatTable(void);
   //--- Обновление таблицы параметров
   void              UpdateParamTable(void);
  }
//+------------------------------------------------------------------+
//| Обновление статистической таблицы                                |
//+------------------------------------------------------------------+
void CProgram::UpdateStatTable(void)
  {
//--- Получим массив данных для статистической таблицы
   string stat_data[];
   int total=m_frame_gen.CopyStatData(stat_data);
   for(int i=0; i<total; i++)
     {
      //--- Расщепим на две строки и занесём в таблицу
      string array[];
      if(::StringSplit(stat_data[i],'=',array)==2)
        {
         if(m_frame_gen.CurrentFrame()>1)
            m_table_stat.SetValue(1,i,array[1],0,true);
         else
           {
            m_table_stat.SetValue(0,i,array[0],0,true);
            m_table_stat.SetValue(1,i,array[1],0,true);
           }
        }
     }
//--- Обновить таблицу
   m_table_stat.Update();
  }

Оба метода для обновления данных в таблицах вызываются в методе CProgram::OnTesterPassEvent() по положительному ответу из одноимённого метода CFrameGenerator::OnTesterPassEvent():

//+------------------------------------------------------------------+
//| Событие обработки прохода оптимизации                            |
//+------------------------------------------------------------------+
void CProgram::OnTesterPassEvent(void)
  {
//--- Обрабатываем полученные результаты тестирования и выводим графику
   if(m_frame_gen.OnTesterPassEvent())
     {
      UpdateStatTable();
      UpdateParamTable();
     }
  }

По окончании оптимизации метод CProgram::CalculateProfitsAndLosses() рассчитает процентное соотношение положительных и отрицательных результатов и выведет эту информацию в строке состояния:

class CProgram : public CWndEvents
  {
private:
   //--- Расчёт соотношения положительных и отрицательных исходов
   void              CalculateProfitsAndLosses(void);
  }
//+------------------------------------------------------------------+
//| Расчёт соотношения положительных и отрицательных исходов         |
//+------------------------------------------------------------------+
void CProgram::CalculateProfitsAndLosses(void)
  {
//--- Выйти, если нет фреймов
   if(m_frame_gen.FramesTotal()<1)
      return;
//--- Количество отрицательных и положительных результатов
   int losses  =m_frame_gen.LossesTotal();
   int profits =m_frame_gen.ProfitsTotal();
//--- Процентное соотношение
   string pl =::DoubleToString(((double)losses/(double)m_frame_gen.FramesTotal())*100,2);
   string pp =::DoubleToString(((double)profits/(double)m_frame_gen.FramesTotal())*100,2);;
//--- Вывод на строку состояния
   m_status_bar.SetValue(1,"Profits: "+(string)profits+" ("+pp+"%)"+" / Losses: "+(string)losses+" ("+pl+"%)");
   m_status_bar.GetItemPointer(1).Update(true);
  }

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

//+------------------------------------------------------------------+
//| Событие окончания процесса оптимизации                           |
//+------------------------------------------------------------------+
void CProgram::OnTesterDeinitEvent(void)
  {
//--- Завершение оптимизации
   m_frame_gen.OnTesterDeinitEvent();
//--- Сделать интерфейс доступным
   IsLockedGUI(true);
//--- Расчёт соотношения положительных и отрицательных исходов
   CalculateProfitsAndLosses();
//--- Инициализация ядра GUI
   CWndEvents::InitializeCore();
  }

Теперь мы можем работать с данными фреймов и по окончании оптимизации. Эксперт находится на графике в терминале, и к фреймам есть доступ для анализа результатов. Графический интерфейс делает это всё интуитивно понятным. В методе-обработчике событий CProgram::OnEvent() будем отслеживать:

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

Для обновления графика после изменения количества серий используется метод CProgram::UpdateBalanceGraph(). Здесь устанавливаем количество серий для работы в генераторе фреймов, а затем резервируем это же количество на графике.

class CProgram : public CWndEvents
  {
private:
   //--- Обновление графика
   void              UpdateBalanceGraph(void);
  };
//+------------------------------------------------------------------+
//| Обновление графика                                               |
//+------------------------------------------------------------------+
void CProgram::UpdateBalanceGraph(void)
  {
//--- Установить количество серий для работы
   int curves_total=(int)m_curves_total.GetValue();
   m_frame_gen.SetCurvesTotal(curves_total);
//--- Удалить серии
   CGraphic *graph=m_graph1.GetGraphicPointer();
   int total=graph.CurvesTotal();
   for(int i=total-1; i>=0; i--)
      graph.CurveRemoveByIndex(i);
//--- Добавить серии
   double data[];
   for(int i=0; i<curves_total; i++)
      graph.CurveAdd(data,CURVE_LINES,"");
//--- Обновить график
   graph.CurvePlotAll();
   graph.Update();
  }

В обработчике событий метод CProgram::UpdateBalanceGraph() вызывается в случае переключения кнопок в поле ввода (ON_CLICK_BUTTON) и в случае, когда значение вводится в поле ввода с клавиатуры (ON_END_EDIT):

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- События нажатия на кнопках
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Изменить количество серий на графике
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         return;
        }
      return;
     }
//--- События ввода значения в поле ввода
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      //--- Изменить количество серий на графике
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         return;
        }
      return;
     }
  }

Для просмотра результатов после оптимизации в классе CFrameGenerator реализован публичный метод CFrameGenerator::ReplayFrames(). Здесь в самом начале по счётчику фреймов определяем: если процесс только что запущен, то массивы обнуляются, и указатель фреймов переводится в самое начало списка. Далее запускается перебор фреймов и осуществляются такие же действия, как и в ранее описанном методе CFrameGenerator::OnTesterPassEvent(). Если фрейм получен, то метод возвращает true. По окончании процесса счётчики фреймов и серий обнуляются и метод возвращает false

class CFrameGenerator
  {
public:
   //--- Перебор фреймов
   bool              ReplayFrames(void);
  };
//+------------------------------------------------------------------+
//| Повторное проигрывание фреймов после окончания оптимизации       |
//+------------------------------------------------------------------+
bool CFrameGenerator::ReplayFrames(void)
  {
//--- Переводим указатель фреймов в начало
   if(m_frames_counter<1)
     {
      ArraysFree();
      ::FrameFirst();
     }
//--- Запускаем перебор фреймов
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Получим входные параметры эксперта, для которых сформирован фрейм
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- Сохраняем статистические показатели результата в массив
      SaveStatData();
      //--- Обновить график результатов и балансов
      UpdateResultsGraph();
      UpdateBalanceGraph();
      //--- Увеличим счетчик обработанных фреймов
      m_frames_counter++;
      return(true);
     }
//--- Закончили перебор
   m_frames_counter   =0;
   m_last_serie_index =0;
   return(false);
  }

Метод CFrameGenerator::ReplayFrames() вызывается в классе CProgram из метода ViewOptimizationResults(). Перед запуском проигрывания фреймов графический интерфейс становится недоступным. Скорость прокрутки можно регулировать, указывая паузу в поле ввода Sleep. В это время в статусной строке будет показываться прогресс-бар для определения того, сколько времени осталось до окончания процесса.

class CFrameGenerator
  {
private:
   //--- Просмотреть результаты оптимизации
   void              ViewOptimizationResults(void);
  };
//+------------------------------------------------------------------+
//| Просмотреть результаты оптимизации                               |
//+------------------------------------------------------------------+
void CProgram::ViewOptimizationResults(void)
  {
//--- Сделать интерфейс недоступным
   IsAvailableGUI(false);
//--- Пауза
   int pause=(int)m_sleep_ms.GetValue();
//--- Запуск воспроизведения фреймов
   while(m_frame_gen.ReplayFrames() && !::IsStopped())
     {
      //--- Обновить таблицы
      UpdateStatTable();
      UpdateParamTable();
      //--- Обновить прогресс-бар
      m_progress_bar.Show();
      m_progress_bar.LabelText("Replay frames: "+string(m_frame_gen.CurrentFrame())+"/"+string(m_frame_gen.FramesTotal()));
      m_progress_bar.Update((int)m_frame_gen.CurrentFrame(),(int)m_frame_gen.FramesTotal());
      //--- Пауза
      ::Sleep(pause);
     }
//--- Расчёт соотношения положительных и отрицательных исходов
   CalculateProfitsAndLosses();
//--- Скрыть прогресс-бар
   m_progress_bar.Hide();
//--- Сделать интерфейс доступным
   IsAvailableGUI(true);
   m_reply_frames.MouseFocus(false);
   m_reply_frames.Update(true);
  }

Вызов метода CProgram::ViewOptimizationResults() происходит по нажатию кнопки Replay frames на графическом интерфейсе приложения. Генерируется событие ON_CLICK_BUTTON.

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- События нажатия на кнопках
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Просмотреть результаты оптимизации 
      if(lparam==m_reply_frames.Id())
        {
         ViewOptimizationResults();
         return;
        }
      //--- 
      ...
      return;
     }
  }

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


Демонстрация полученного результата

Для тестов будем использовать торговый алгоритм из стандартной поставки — Moving Average. Оформим его в виде класса, как есть, без дополнений и исправлений. Все файлы разрабатываемого приложения будут располагаться в одной папке. Файл со стратегией подключаем к файлу Program.mqh.

В качестве дополнения здесь подключен файл FormatString.mqh с функциями для форматирования строк. Они пока не являются частью какого-нибудь класса, поэтому обозначим стрелку чёрным цветом. В итоге схема приложения выглядит так:

Рис. 4 – Подключение класса с торговой стратегией и файла с дополнительными функциями. 

Рис. 4. Подключение класса с торговой стратегией и файла с дополнительными функциями.

Попробуем оптимизировать параметры и посмотрим, как это выглядит на графике в терминале. Настройки тестера: символ EURUSD, таймфрейм H1, временной диапазон 2017.01.01 – 2018.01.01.

Рис. 5 – Демонстрация результата эксперта Moving Average из стандартной поставки.

Рис. 5.  Демонстрация результата эксперта Moving Average из стандартной поставки.

Как видим, получилось довольно информативно. Практически все результаты по этому торговому алгоритму отрицательные (95.23%). Если увеличить временной диапазон, то они станут ещё хуже. Но мы знаем, что при разработке торговой системы нужно стремиться к тому, чтобы большинство результатов были положительными. Иначе алгоритм будет убыточным, и его крайне не рекомендуется использовать. Нужно оптимизировать параметры на большем количестве данных и смотреть, чтобы сделок было как можно больше.  

Попробуем протестировать другой торговый алгоритм из стандартной поставки — MACD Sample.mq5. Он уже оформлен в виде класса. После небольших доработок его можно просто подключить к нашему приложению, как и предыдущий. Протестируем его на том же символе и таймфрейме, но увеличим временной диапазон, чтобы увеличить количество сделок в тестах (2010.01.01 – 2018.01.01). Вот результат оптимизации этого торгового эксперта:

 Рис. 6 – Демонстрация результата эксперта MACD Sample из стандартной поставки.

Рис. 6.  Демонстрация результата оптимизации эксперта MACD Sample.

Здесь мы видим совсем другой результат: 90,89% положительных исходов.

Оптимизация параметров может занять очень много времени, в зависимости от объема использованных данных. Необязательно сидеть перед монитором в течение всего этого процесса. После оптимизации можно запустить повторный просмотр результатов в ускоренном режиме, нажав на кнопку Replay frames. Давайте запустим процесс проигрывания фреймов, установив для показа лимит в 25 серий. Вот как это выглядит:

Рис. 7 - Демонстрация результата эксперта MACD Sample после оптимизации.

Рис. 7. Демонстрация результата эксперта MACD Sample после оптимизации.


Заключение

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

Минус, или, скорее, временная недоработка такого решения — тот факт, что после завершения оптимизации в режиме обработки фреймов нельзя запустить таймер. Это накладывает некоторые ограничения по работе с тем же графическим интерфейсом. Вторая проблема: при удалении эксперта с графика не срабатывает деинициализация в функции OnDeinit(), а это препятствует правильной обработке этого события. Возможно, в одном из следующих билдов MetaTrader 5 эти проблемы будут решены.

Прикрепленные файлы |
MQL5.zip (35.04 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (64)
Вячеслав Новиков
Вячеслав Новиков | 5 мар. 2018 в 15:06
Не получилось. Где я должен увидеть эту картинку? В вкладке бэктест или на графике?
Anatoli Kazharski
Anatoli Kazharski | 5 мар. 2018 в 15:42
Вячеслав Новиков:
Не получилось. Где я должен увидеть эту картинку? В вкладке бэктест или на графике?

На графике в терминале.

Nextor
Nextor | 13 мар. 2018 в 13:11

Добрый день, подскажите:

Как записать результаты оптимизации в файл при использовании Local Network Farm или MQL5 Cloud Network ?

Есть процедура в OnTester(), использует:

string toWrite = "test";
fileHandle=FileOpen(fileName,FILE_CSV|FILE_READ|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_COMMON,",");
FileWrite(fileHandle,toWrite);
FileClose(fileHandle);

При использовании Local агентов файл с результатами оптимизации создается в общей папке, при использовании Local Network Farm или MQL5 Cloud Network файла нет.

Roffild
Roffild | 14 мар. 2018 в 00:05

Смотри мой пост в .

Или нужно использовать FrameAdd()

Вячеслав Новиков
Вячеслав Новиков | 23 мар. 2018 в 21:38
Anatoli Kazharski:

На графике в терминале.

Все получилось, спасибо. График работает. 

Просто не включил оптимизацию. Тупкую, второй месяц только занимаюсь разработкой роботов...)))

Управляемая оптимизация: метод отжига Управляемая оптимизация: метод отжига
В тестере стратегий торговой платформы MetaTrader 5 есть только два варианта оптимизации: полный перебор параметров и генетический алгоритм. В этой статье предложен новый вариант оптимизации торговых стратегий — метод отжига. Приводится алгоритм метода, его реализация и способ подключения к любому советнику. Разработанный алгоритм протестирован на советнике Moving Average.
ZUP - зигзаг универсальный с паттернами Песавенто. Поиск паттернов ZUP - зигзаг универсальный с паттернами Песавенто. Поиск паттернов
Индикаторная платформа ZUP позволяет производить поиск множества известных паттернов, параметры которых уже заданы. Но можно также и подстраивать эти параметры в соответствии со своими требованиями. Есть и возможность создавать новые паттерны с помощью графического интерфейса ZUP и сохранять их параметры в файл. После этого можно быстро проверить, встречаются ли новые паттерны на графиках.
Глубокие нейросети (Часть VI). Ансамбль нейросетевых классификаторов: bagging Глубокие нейросети (Часть VI). Ансамбль нейросетевых классификаторов: bagging
Рассмотрим методы построения и обучения ансамблей нейросетей со структурой bagging. Определим особенности оптимизации гиперпараметров индивидуальных нейросетевых классификаторов, составляющих ансамбль. Сравним качество оптимизированной нейросети, полученной в предыдущей статье серии, и созданного ансамбля нейросетей. Рассмотрим возможности дальнейшего улучшения качества классификации полученного ансамбля.
LifeHack для трейдера: замешиваем ForEach на дефайнах (#define) LifeHack для трейдера: замешиваем ForEach на дефайнах (#define)
Промежуточная ступенька для тех, кто всё ещё пишет на MQL4, но никак не может перейти на MQL5. Мы продолжаем искать возможности для написания кода в стиле MQL4. На этот раз рассмотрим макроподстановку препроцессора - #define.