100 лучших проходов оптимизации (Часть 1). Cоздание анализатора оптимизаций

Andrey Azatskiy | 4 октября, 2018

Введение

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

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

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



Во-первых, оптимизация советников идет довольно долго. Конечно, это связано с тем, что тестер более качественно генерирует тики (даже при выборе OHLC — генерируются 4 тика на каждую свечу) и прочие дополнения, позволяющие более качественно оценить советника. Однако на домашних компьютерах, которые не столь мощны как бы хотелось, оптимизационный процесс может растянуться на дни или же недели. Часто бывает так, что, выбрав параметры робота, очень скоро понимаешь, что они были некорректны, а кроме выгрузки со статистикой проходов оптимизации и нескольких оценочных коэффициентов — под рукой ничего нет.

Хотелось бы иметь полноценную статистику по каждому проходу оптимизации и возможность фильтрации (в том числе и условные фильтры) для каждого из них по большому числу параметров. Так же было бы неплохо сравнить статистику торгов со стратегией "Buy And Hold" и наложить все статистики друг на друга. Вдобавок, иногда требуется выгрузить все данные торговой истории в файл для последующей обработки результатов каждой из сделок.

Иногда также требуется просмотреть какое проскальзывание сможет выдержать алгоритм и как он ведет себя на определенном временном промежутке. Ведь некоторые стратегии зависят от типа рынка. Примером тому может быть стратегия, заточенная на флет. Во время начала трендовых промежутков она обязательно начнет сливать, во время флета — зарабатывать. Было бы неплохо отдельно от общего графика PL просмотреть определенные промежутки (по датам), и не просто на графике цен, а в виде полного набора коэффициентов и других дополнений.

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

Не маловажным нюансом будет упомянуть «Граали», которыми так славятся многие из строителей торговых стратегий. Некоторые роботы приносят по 1000 и более процентов в месяц. Кажется они более чем обгоняют рынок (имеется ввиду стратегия "Buy and Hold") — однако на практике все выглядит несколько иначе. Как показывает представленная программа, данные роботы и в правду могут принести 1000%, однако они не обгоняют рынок.

В программе существует разделение анализа между торговлей роботом с полным лотом (его наращивание/сокращение и прочие…), а также имитация торговли роботом с использованием одного лота (минимального лота, доступного для торгов). Когда строится график торговли "Buy And Hold", представленная программа учитывает управление лотом, которое делал робот (т. е. докупает актив, когда повышался лот и снижает количество купленного актива, когда лот снижался.). Если сопоставить эти два графика, то выяснится, что мой тестовый робот, который в одном из лучших проходов оптимизации показывал нереальные результаты, так и не смог обогнать рынок. Поэтому для более трезвой оценки торговых стратегий стоит взглянуть на график торговли одним лотом — где как PL робота, так и PL стратегии "Buy and Hold" приведены к торговли минимально допустимым объемом торгов (PL= Profit/Loss — график полученной прибыли по времени).

Далее разберем подробнее, как создавалась данная программа.


Структура анализатора оптимизаций

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


Получившийся Анализатор оптимизаций не привязан ни к какому конкретному роботу и не является его частью. Однако в силу специфики построения графических интерфейсов в MQL5 — Основной программы послужил шаблон MQL5 для создания советников. Так как программа получилось достаточно большой (несколько тысяч строк кода), то для большей конкретики и смысловой последовательности она была разделена на ряд блоков (изображенные выше на диаграмме), которые делятся на классы, из которых состоят. Шаблон робота является лишь отправной точкой для старта приложения. Каждый из блоков будет рассмотрен ниже более детально, сейчас же опишем взаимосвязи между ними. Для полноценной работы с приложением потребуется:

Сам робот может быть написан как угодно (с помощью ООП, просто функции внутри шаблона робота, импортироваться из Dll…), главное — чтобы он использовал шаблон для написания роботов, предоставляемый Мастером MQL5. К нему подключается один файл из блока работы с базой данных, где находится класс, который по прошествии каждого прохода оптимизации выгружает требуемые данные в базу данных. Это является самостоятельной частью, никак не зависящей от работы самого приложения, так как база данных формируется во время запуска робота в тестере стратегий.

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

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

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

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

Сама программа написана в виде проекта MQL5 — это позволяет подойти к ее написанию более структурировано и скомпоновать в одном месте все требуемые файлы с кодом. В проект включен еще один класс, который будет рассмотрен в блоке, описывающем расчеты. Данный класс был написан специально для этой программы, и его задача состоит в фильтрации проходов оптимизации по разработанному мной методу. По сути, он обслуживает всю вкладку «Optimisation selection». А результат его работы — это сокращение выборки данных по определенным критериям.

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

Как понятно из названия, рассматриваемый класс занимается сортировкой данных. Его алгоритм был позаимствован со стороннего сайта — Сортировка выбором.

//+------------------------------------------------------------------+
//| E-num со стилем сортировки                                       |
//+------------------------------------------------------------------+
enum SortMethod
  {
   Sort_Ascending,// По возрастанию
   Sort_Descendingly// По убыванию
  };
//+------------------------------------------------------------------+
//| Класс сортирующий переданный тип данных                          |
//+------------------------------------------------------------------+
class CGenericSorter
  {
public:
   // Конструктор по умолчанию
                     CGenericSorter(){method=Sort_Descendingly;}
   // Сортирующий метод
   template<typename T>
   void              Sort(T &out[],ICustomComparer<T>*comparer);
   // Выбор способа сортировки
   void Method(SortMethod _method){method=_method;}
   // Получение способа сортировки
   SortMethod Method(){return method;}
private:
   // Способ сортировки
   SortMethod        method;
  };

Класс содержит шаблонный метод Sort, который и сортирует данные. Благодаря шаблонному методу, он может сортировать любые переданные данные в том числе классы и структуры. Методика сравнения данных должна быть описана в отдельном классе реализующим интерфейс IСustomComparer<T>. Мне потребовалось создать собственный интерфейс по типу IСomparer, лишь из-за того, что в традиционном интерфейсе IСomparer в методе Compare сопоставляемые данные не передаются по ссылки, а именно передача по ссылке является одним из условий передачи структур в метод в языке MQL5.

Перегрузки метода класса CGenericSorter::Method — возвращают и принимают тип сортировки данных (по возрастанию или же по убыванию). Во всех блоках данной программы, где сортируются данные используется именно этот класс.


Графическая часть

Предупреждение!


Во время создания графического интерфейса в использованной библиотеке (EasyAndFastGUI) обнаружился баг, актуальный на дату публикации статьи, а именно — графический элемент ComboBox не до конца очищал некоторые переменные во время его перезаполнения. Согласно рекомендациям создателя библиотеки, для его исправления требуется внести следующие правки:

m_item_index_focus =WRONG_VALUE;
m_prev_selected_item =WRONG_VALUE;
m_prev_item_index_focus =WRONG_VALUE;

в метод CListView::Clear(const bool redraw=false).

Данный метод находится на 600 строке в файле ListView.mqh. В файле, находящемся по пути:
Include\EasyAndFastGUI\Controls.

Если Вы не добавите эти правки, то иногда будет выскакивать ошибка "Array out of range" во время открытия ComboBox, и приложение будет закрываться аварийно.


Что бы создать окно в MQL5 на основе библиотеки EasyAndFastGUI требуется создать класс, являющийся контейнером для всего последующего наполнения окна, и унаследоваться от класса CwindEvents. Внутри класса требуется переопределить методы:

 //--- Инициализация/деинициализация
   void              OnDeinitEvent(const int reason){CWndEvents::Destroy();};
   //--- Обработчик события графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);//

В общем, заготовка для создания окна должна быть следующей:

class CWindowManager : public CWndEvents
  {
public:
                     CWindowManager(void){presenter = NULL;};
                    ~CWindowManager(void){};
   //===============================================================================   
   // Calling methods and events :
   //===============================================================================
   //--- Инициализация/деинициализация
   void              OnDeinitEvent(const int reason){CWndEvents::Destroy();};
   //--- Обработчик события графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

//--- Создаёт графический интерфейс программы
   bool              CreateGUI(void);


private:
 //--- Главное окно
   CWindow           m_window;
  }

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

bool CWindowManager::CreateWindow(const string text)
  {
//--- Добавим указатель окна в массив окон
   CWndContainer::AddWindow(m_window);
//--- Координаты
   int x=(m_window.X()>0) ? m_window.X() : 1;
   int y=(m_window.Y()>0) ? m_window.Y() : 1;
//--- Свойства
   m_window.XSize(WINDOW_X_SIZE+25);
   m_window.YSize(WINDOW_Y_SIZE);
   m_window.Alpha(200);
   m_window.IconXGap(3);
   m_window.IconYGap(2);
   m_window.IsMovable(true);
   m_window.ResizeMode(false);
   m_window.CloseButtonIsUsed(true);
   m_window.FullscreenButtonIsUsed(false);
   m_window.CollapseButtonIsUsed(true);
   m_window.TooltipsButtonIsUsed(false);
   m_window.RollUpSubwindowMode(true,true);
   m_window.TransparentOnlyCaption(true);

//--- Установим всплывающие подсказки
   m_window.GetCloseButtonPointer().Tooltip("Close");
   m_window.GetFullscreenButtonPointer().Tooltip("Fullscreen/Minimize");
   m_window.GetCollapseButtonPointer().Tooltip("Collapse/Expand");
   m_window.GetTooltipButtonPointer().Tooltip("Tooltips");
//--- Создание формы
   if(!m_window.CreateWindow(m_chart_id,m_subwin,text,x,y))
      return(false);
//---
   return(true);
  }

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

Создаваемое приложение имеет интерфейс, разделенный между собой вкладками. Всего создано 4 контейнера вкладок:

//--- Tabs
   CTabs             main_tab; // Основные вкладки
   CTabs             tab_up_1; // Вкладки с настройками и таблицей результатов
   CTabs             tab_up_2; // Вкладки со статистикой и выбором параметров, а также общими графиками
   CTabs             tab_down; // Вкладки со статистикой и выгрузкой в файл

На форме они выглядят следующим образом (подписаны красным на скриншоте):

Процесс создания вкладок однотипен — различается лишь наполнение. В качестве примера приведу метод, создающий основную вкладку:

//+------------------------------------------------------------------+
//| Main Tab                                                         |
//+------------------------------------------------------------------+
bool CWindowManager::CreateTab_main(const int x_gap,const int y_gap)
  {
//--- Сохраним указатель на главный элемент
   main_tab.MainPointer(m_window);

//--- Массив с шириной для вкладок
   int tabs_width[TAB_MAIN_TOTAL];
   ::ArrayInitialize(tabs_width,45);
   tabs_width[0]=120;
   tabs_width[1]=120;
//---
   string tabs_names[TAB_UP_1_TOTAL]={"Analysis","Optimisation Data"};
//--- Свойства
   main_tab.XSize(WINDOW_X_SIZE-23);
   main_tab.YSize(WINDOW_Y_SIZE);
   main_tab.TabsYSize(TABS_Y_SIZE);
   main_tab.IsCenterText(true);
   main_tab.PositionMode(TABS_LEFT);
   main_tab.AutoXResizeMode(true);
   main_tab.AutoYResizeMode(true);
   main_tab.AutoXResizeRightOffset(3);
   main_tab.AutoYResizeBottomOffset(3);
//---
   main_tab.SelectedTab((main_tab.SelectedTab()==WRONG_VALUE)? 0 : main_tab.SelectedTab());
//--- Добавим вкладки с указанными свойствами
   for(int i=0; i<TAB_MAIN_TOTAL; i++)
      main_tab.AddTab((tabs_names[i]!="")? tabs_names[i]: "Tab "+string(i+1),tabs_width[i]);
//--- Создадим элемент управления
   if(!main_tab.CreateTabs(x_gap,y_gap))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,main_tab);
   return(true);
  }

По мимо наполнения, которое может варьироваться, основными строками кода является:

  1. Добавление указателя на главный элемент — требуется что бы контейнер вкладок знал за каким элементом он закреплен
  2. Строка создания элемента управления
  3. Добавления элемента в общий список элементов управления.

Следующими по иерархии идут элементы управления. В данном приложении было использовано 11 типов элементов управления. Все они создаются однотипно, и поэтому для создания каждого из них были написаны методы, добавляющие данные элементы управления. Рассмотрим реализацию только одного из них:

bool CWindowManager::CreateLable(const string text,
                                 const int x_gap,
                                 const int y_gap,
                                 CTabs &tab_link,
                                 CTextLabel &lable_link,
                                 int tabIndex,
                                 int lable_x_size)
  {
//--- Сохраним указатель на главный элемент
   lable_link.MainPointer(tab_link);
//--- Закрепить за вкладкой
   tab_link.AddToElementsArray(tabIndex,lable_link);

//--- Настройки
   lable_link.XSize(lable_x_size);

//--- Создание
   if(!lable_link.CreateTextLabel(text,x_gap,y_gap))
      return false;

//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,lable_link);
   return true;
  }

Передаваемый элемент управления — CTextLabel, так же как и вкладки, должен запомнить элемент, контейнером которого он является. Контейнер вкладок в свою очередь запоминает на какой именно вкладке находится элемент. После этого идет наполнение элемента требуемыми настройками и первоначальными данными. Завершает всё добавление объекта в общий массив объектов.

По аналогии с лейблами добавляются и другие элементы, определенные внутри класса-контейнера в качестве полей. Я разграничил определенные элементы и часть из них поместил в protected область класса — это те элементы, к которым не понадобится доступ из презентера, а часть в public — это элементы, которые определяют некоторые условия, либо радиокнопки, состояние нажатия которых стоит проверять из презентера. Иначе говоря, все элементы и методы, доступ к которым не желателен — озаглавлены в protected и private части класса, где также находится и ссылка на презентер. Само добавление ссылки на презентер выполнено в виде публичного метода, где вначале проверяется наличие уже добавленного презентера, и только потом, если ссылка на него еще не была добавлена — презентер сохраняется. Это сделано для того, чтобы избежать возможность динамической подмены презентера во время выполнения программы.

Непосредственно создание самого окна происходит в методе CreateGUI:

bool CWindowManager::CreateGUI(void)
  {
//--- Create window
   if(!CreateWindow("Optimisation Selection"))
      return(false);

//--- Create tabs
   if(!CreateTab_main(120,20))
      return false;
   if(!CreateTab_up_1(3,44))
      return(false);
   int indent=WINDOW_Y_SIZE-(TAB_UP_1_BOTTOM_OFFSET+TABS_Y_SIZE-TABS_Y_SIZE);
   if(!CreateTab_up_2(3,indent))
      return(false);
   if(!CreateTab_down(3,33))
      return false;

//--- Create controls 
   if(!Create_all_lables())
      return false;
   if(!Create_all_buttons())
      return false;
   if(!Create_all_comboBoxies())
      return false;
   if(!Create_all_dropCalendars())
      return false;
   if(!Create_all_textEdits())
      return false;
   if(!Create_all_textBoxies())
      return false;
   if(!Create_all_tables())
      return false;
   if(!Create_all_radioButtons())
      return false;
   if(!Create_all_SepLines())
      return false;
   if(!Create_all_Charts())
      return false;
   if(!Create_all_CheckBoxies())
      return false;

// Show window
   CWndEvents::CompletedGUI();

   return(true);
  }

Как видно из его реализации, он сам непосредственно не создает ни одного элемента управления, а лишь вызывает другие методы создания данных элементов. Основной строкой кода, которая непременно должна идти завершающей в данном методе, является CWndEvents::CompletedGUI();

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

//===============================================================================   
// Controls creation:
//===============================================================================
//--- All Lables
   bool              Create_all_lables();
   bool              Create_all_buttons();
   bool              Create_all_comboBoxies();
   bool              Create_all_dropCalendars();
   bool              Create_all_textEdits();
   bool              Create_all_textBoxies();
   bool              Create_all_tables();
   bool              Create_all_radioButtons();
   bool              Create_all_SepLines();
   bool              Create_all_Charts();
   bool              Create_all_CheckBoxies();

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

Создать метод — обработчик события (к примеру нажатие на кнопку). Данный метод должен принимать в качестве параметров id и lparam. Первый параметр указывает на тип графического события, а второй на ID объекта, с которым было взаимодействие. Реализация данных методов схожа во всех случаях:

//+------------------------------------------------------------------+
//| Btn_Update_Click                                                 |
//+------------------------------------------------------------------+
void CWindowManager::Btn_Update_Click(const int id,const long &lparam)
  {
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON && lparam==Btn_update.Id())
     {
      presenter.Btn_Update_Click();
     }
  }

Для начала требуется проверить условие (было ли то нажатие на кнопку, либо выбор элемента списка…). Вторым этапом проверки является проверка по lparam — где сравнивается переданный в метод ID с ID требуемого элемента списка.

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

//+------------------------------------------------------------------+
//| OnEvent                                                          |
//+------------------------------------------------------------------+
void CWindowManager::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   Btn_Update_Click(id,lparam);
   Btn_Load_Click(id,lparam);
   OptimisationData_inMainTable_selected(id,lparam);
   OptimisationData_inResults_selected(id,lparam);
   Update_PLByDays(id,lparam);
   RealPL_pressed(id,lparam);
   OneLotPL_pressed(id,lparam);
   CoverPL_pressed(id,lparam);
   RealPL_pressed_2(id,lparam);
   OneLotPL_pressed_2(id,lparam);
   RealPL_pressed_4(id,lparam);
   OneLotPL_pressed_4(id,lparam);
   SelectHistogrameType(id,lparam);
   SaveToFile_Click(id,lparam);
   Deals_passed(id,lparam);
   BuyAndHold_passed(id,lparam);
   Optimisation_passed(id,lparam);
   OptimisationParam_selected(id,lparam);
   isCover_clicked(id,lparam);
   ChartFlag(id,lparam);
   show_FriquencyChart(id,lparam);
   FriquencyChart_click(id,lparam);
   Filtre_click(id,lparam);
   Reset_click(id,lparam);
   RealPL_pressed_3(id,lparam);
   OneLotPL_pressed_3(id,lparam);
   ShowAll_Click(id,lparam);
   DaySelect(id,lparam);
  }

Который в свою очередь вызывается из шаблона робота. Таким образом, событийная модель тянется от шаблона робота (представлен ниже) к графическому интерфейсу. В GUI события обрабатываются и отфильтровываются интересующие нас, а далее перенаправляются для последующей обработки в презентер. Сам же шаблон робота является отправной точкой старта программы и выглядит следующим образом:

#include "Presenter.mqh"

CWindowManager _window;
CPresenter Presenter(&_window);
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!_window.CreateGUI())
     {
      Print(__FUNCTION__," > Не удалось создать графический интерфейс!");
      return(INIT_FAILED);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   _window.OnDeinitEvent(reason);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   _window.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

Работа с базой данных

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

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

Выбор подходящей базы данных сделать было достаточно просто. Требовалась быстрая распространенная база данных, удобная в стыковке и не требующая никакого дополнительного программного обеспечения. Всем критериям отвечает база данных Sqlite, именно из-за этих характеристик она так распространена. Для ее использования требуется подключить к проекту Dll, предоставляемые от поставщика базы. Данные dll написаны на языке C и легко стыкуются с программами на MQL5, что является приятным дополнением — потому что нет необходимости писать ни строчки кода на стороннем языке и — тем самым усложнять проект. Среди минусов такого подхода является то, что в Dll Sqlite не предоставляется удобного API для работы с базой, и поэтому требуется описать хотя бы минимальную обертку для работы с базой. Пример написания подобного функционала был достаточно хорошо представлен в статье «SQL И MQL5: РАБОТАЕМ С БАЗОЙ ДАННЫХ SQLITE». Для данного проекта была использована часть кода из указанной статьи, но лишь та часть, что касается взаимодействия с WinApi и импорта некоторых функций из dll в MQL5. Что касается обертки, то было принято решение написать ее самостоятельно.

В итоге блок работы с базой данных состоит из папки «Sqlite3», где описана удобная обертка работы с базой данных, и папки «OptimisationSelector», созданной специально для написанной программы. Обе папки располагаются в директории MQL5/Include. Также при работе с базой данных, как уже было упомянуто ранее, используется ряд функций стандартной библиотеки, Windows, все функции данной части приложения они размещены в директории WinApi. По мимо упомянутых заимствований — использовался код для создания разделяемого ресурса (Mutex) — взятых из CodeBase. Разделяемый ресурс требуется для того, чтобы во время работы с базой из двух источников (а именно, если анализатор оптимизаций открыл задействованную во время процесса оптимизации базу) данные, полученные программой, всегда были полными. Получается, что если одна из сторон (либо процесс оптимизации, либо программа анализатор) задействует базу данных, то другая ждет завершения работы предыдущей. База данных Sqlite позволяет читать ее из нескольких потоков. В силу тематики статьи мы не станем детально рассматривать полученную в результате обертку для работы с базой данных sqlite3 из MQL5, а лишь опишем некоторые моменты ее реализации и метод использования. Как уже упоминалось, обертка работы с базой находится в папке Sqlite3. В ней располагаются 3 файла, пройдемся по ним в порядке написания.

#import "Sqlite3_32.dll"
int sqlite3_open(const uchar &filename[],sqlite3_p32 &paDb);// Открытие базы
int sqlite3_close(sqlite3_p32 aDb); // Закрытие базы
int sqlite3_finalize(sqlite3_stmt_p32 pStmt);// Завершение стейтмента
int sqlite3_reset(sqlite3_stmt_p32 pStmt); // Сброс стейтмента
int sqlite3_step(sqlite3_stmt_p32 pStmt); // Переход на следующую строку при чтении стейтмента
int sqlite3_column_count(sqlite3_stmt_p32 pStmt); // Подсчет количества колонок
int sqlite3_column_type(sqlite3_stmt_p32 pStmt,int iCol); // Получение типа выбранной колонки
int sqlite3_column_int(sqlite3_stmt_p32 pStmt,int iCol);// Преобразование значения в int
long sqlite3_column_int64(sqlite3_stmt_p32 pStmt,int iCol); // Преобразование значения в int64
double sqlite3_column_double(sqlite3_stmt_p32 pStmt,int iCol); // Преобразование значения в double
const PTR32 sqlite3_column_text(sqlite3_stmt_p32 pStmt,int iCol);// Получение текстового значения
int sqlite3_column_bytes(sqlite3_stmt_p32 apstmt,int iCol); // Получение количества байтов, занимаемых строкой из переданной ячейки
int sqlite3_bind_int64(sqlite3_stmt_p32 apstmt,int icol,long a);// Объединение запроса со значением (типа int64)
int sqlite3_bind_double(sqlite3_stmt_p32 apstmt,int icol,double a);// Объединение запроса со значением (типа double)
int sqlite3_bind_text(sqlite3_stmt_p32 apstmt,int icol,char &a[],int len,PTRPTR32 destr);// Объединение запроса со значением (типа string (char* - в C++))
int sqlite3_prepare_v2(sqlite3_p32 db,const uchar &zSql[],int nByte,PTRPTR32 &ppStmt,PTRPTR32 &pzTail);// Подготовка запроса
int sqlite3_exec(sqlite3_p32 aDb,const char &sql[],PTR32 acallback,PTR32 avoid,PTRPTR32 &errmsg);// Исполнение Sql
int sqlite3_open_v2(const uchar &filename[],sqlite3_p32 &ppDb,int flags,const char &zVfs[]); // Открытие базы с параметрами
#import

Стоит упомянуть, что для работы обертки базы данных dll, предоставляемые разработчиками базы, должны лежать в папке Libraries и наименоваться Sqlite3_32.dll и Sqlite3_64.dll — в соответствии с их разрядностью. Данные Dll Вы можете взять из файлов, приложенных к статье, скомпилировать самостоятельно из Sqlite Amalgmation или же взять с сайта разработчиков Sqlite - на Ваш выбор, но их наличие является обязательным условием работы программы. Также нужно разрешить эксперту импорт Dll.  

//+------------------------------------------------------------------+
//| Класс соединения и управление базой                              |
//+------------------------------------------------------------------+
class CSqliteManager
  {
public:
                     CSqliteManager(){db=NULL;} // Пустой конструктор
                     CSqliteManager(string dbName); // Передается имя
                     CSqliteManager(string dbName,int flags,string zVfs); // Передается имя и флаги соединения
                     CSqliteManager(CSqliteManager  &other) { db=other.db; } // Конструктор копирования
                    ~CSqliteManager(){Disconnect();};// Деструктор

   void              Disconnect(); // Отключение от базы
   bool              Connect(string dbName,int flags,string zVfs); // Параметральное подключение к базе
   bool              Connect(string dbName); // Подключение к базе по имени

   void operator=(CSqliteManager  &other){db=other.db;}// Оператор присвоения

   sqlite3_p64 DB() { return db; }; // Получение указателя на базу

   sqlite3_stmt_p64  Create_statement(const string sql); // Создать стейтмент
   bool              Execute(string sql); // Исполнить команду
   void              Execute(string  sql,int &result_code,string &errMsg); // Исполнить команду и выдать код ошибки и сообщение ошибки

   void              BeginTransaction(); // Начало транзакции
   void              RollbackTransaction(); // Откат транзакции
   void              CommitTransaction(); // Подтверждение транзакции

private:
   sqlite3_p64       db; // База

   void stringToUtf8(const string strToConvert,// Строка, которую необходимо преобразовать в массив в кодировке utf-8
                     uchar &utf8[],// Массив в кодировке utf-8, в который будет помещена преобразованная строка strToConvert
                     const bool untilTerminator=true)
     {    // Количество символов, которые будут скопированы в массив utf8 и соответственно преобразованы в кодировку utf-8
      //---
      int count=untilTerminator ? -1 : StringLen(strToConvert);
      StringToCharArray(strToConvert,utf8,0,count,CP_UTF8);
     }
  };

Как видно из кода, созданный класс имеет возможность создания двух типов подключения в базе (текстовое и с указанием параметров). Метод Create_sttement формирует запрос к базе и возвращает указатель на него. Перегрузки метода Exequte исполняют простые строковые запросы, а методы транзакций — управляют созданием и процессом принятия/отмены транзакций. Подключение к самой базе хранится в переменной db. Если мы воспользовались методом Disconnect или же только что создали класс с помощью конструктора по умолчанию (т.е. не успели еще подключиться к базе), то данная переменная будет иметь значение NULL. При повторном вызове метода Connect мы отключаемся от ранее подключенной базы и подключаемся к новой. Так как при подключении к базе требуется передача строки в формате UTF-8, то в классе имеется специальный private метода, конвертирующий строку в требуемый формат данных.

typedef bool(*statement_callback)(sqlite3_stmt_p64); // коллбек, выполняемый при исполнении запроса, в случае успеха возвращает true
//+------------------------------------------------------------------+
//| Класс запроса к базе                                             |
//+------------------------------------------------------------------+
class CStatement
  {
public:
                     CStatement(){stmt=NULL;} // пустой конструктор
                     CStatement(sqlite3_stmt_p64 _stmt){this.stmt=_stmt;} // Конструктор с параметром - указатель на стейтмент
                    ~CStatement(void){if(stmt!=NULL)Sqlite3_finalize(stmt);} // Деструктор
   sqlite3_stmt_p64 get(){return stmt;} // Получить указатель на стейтмент
   void              set(sqlite3_stmt_p64 _stmt); // Установка указателя на стейтмент

   bool              Execute(statement_callback callback=NULL); // Исполнение стейтмента
   bool              Parameter(int index,const long value); // Добавление параметра
   bool              Parameter(int index,const double value); // Добавление параметра
   bool              Parameter(int index,const string value); // Добавление параметра

private:
   sqlite3_stmt_p64  stmt;
  };

Перегрузки метода Parameter заполняют параметры запроса. Метод set сохраняет переданный statement в переменной stmt: если перед сохранением нового запроса выясняется, что ранее в классе уже был сохранен старый запрос, то вызывается метод Sqlite3_finalize для ранее сохраненного запроса.

//+------------------------------------------------------------------+
//| Класс, читающий ответы от баз                                    |
//+------------------------------------------------------------------+
class CSqliteReader
  {
public:
                     CSqliteReader(){statement=NULL;} // пустой конструктор
                     CSqliteReader(sqlite3_stmt_p64 _statement) { this.statement=_statement; }; // Конструктор принимающий указатель на стейтмент
                     CSqliteReader(CSqliteReader  &other) : statement(other.statement) {} // Конструктор копирования
                    ~CSqliteReader() { Sqlite3_reset(statement); } // Деструктор

   void              set(sqlite3_stmt_p64 _statement); // Добавить ссылку на стейтмент
   void operator=(CSqliteReader  &other){statement=other.statement;}// Оператор присвоения Ридера
   void operator=(sqlite3_stmt_p64 _statement) {set(_statement);}// Оператор присвоения стейтмента

   bool              Read(); // Чтение строки
   int               FieldsCount(); // Подсчет количества столбцов
   int               ColumnType(int col); // Получение типа колонки

   bool              IsNull(int col); // Проверка является ли значение == SQLITE_NULL
   long              GetInt64(int col); // Конвертация в int
   double            GetDouble(int col);// Конвертация в double
   string            GetText(int col);// Конвертация в string

private:
   sqlite3_stmt_p64  statement; // указатель на стейтмент
  };

Реализовав описанные классы при помощи функций работы с базой выгруженных из Sqlite3.dll, можно приступить к описанию классов, работающих с базой из описываемой программы.

Структура создаваемой базы данных — следующая:

Таблица Buy And Hold:

  1. Time — ось X - отметка временного интервала
  2. PL_total — прибыль/убыток, если наращивать лот соразмерно роботу
  3. PL_oneLot — прибыль/убыток, если торговать все время одним лотом
  4. DD_total — просадка, если торговать лотом, как торговал робот
  5. DD_oneLot — просадка, если торговать одним лотом
  6. isForvard — признак форвардного графика

Таблица OptimisationParams:

  1. ID — Уникальный автозаполняемый номер записи в базе
  2. HistoryBorder — Дата завершения исторической оптимизации
  3. TF — Таймфрейм
  4. Param_1...Param_n — параметр
  5. InitalBalance — размер начального баланса

Таблица ParamsCoefitients:

  1. ID — Внешний ключ, ссылка на OptimisationParams(ID)
  2. isForvard — признак форвардной оптимизации
  3. isOneLot — признак графика, по которому рассчитывался коэффициент
  4. DD — просадка
  5. averagePL — средняя прибыль/убыток по графику PL
  6. averageDD — средняя просадка
  7. averageProfit — средняя прибыль
  8. profitFactor — профит фактор
  9. recoveryFactor — фактор восстановления
  10. sharpRatio — коэффициент Шарпа
  11. altman_Z_Score — Z-счет Альтмана
  12. VaR_absolute_90 — VaR 90
  13. VaR_absolute_95 — VaR 95
  14. VaR_absolute_99 — VaR 99
  15. VaR_growth_90 — VaR 90
  16. VaR_growth_95 — VaR 95
  17. VaR_growth_99 — VaR 99
  18. winCoef — коэффициент выигрыша
  19. customCoef — пользовательский коэффициент

Таблица ParamType:

  1. ParamName — имя параметра робота
  2. ParamType — тип параметра робота (int / double / string)

Таблица TradingHistory

  1. ID — Внешний ключ ссылка на OptimisationParams(ID)
  2. isForvard — признак является ли история форвардного теста
  3. Symbol — символ
  4. DT_open — дата открытия
  5. Day_open — день открытия
  6. DT_close — дата закрытия
  7. Day_close — день закрытия
  8. Volume — Количество лотов
  9. isLong — признак того, лонг или шорт
  10. Price_in — цена входа
  11. Price_out — Цена выхода
  12. PL_oneLot — Прибыль если торговать одним лотом
  13. PL_forDeal — Прибыль если торговать как торговали
  14. OpenComment — комментарий на вход
  15. CloseComment — Комментарий на выход

Исходя из представленной структуры базы видно, что некоторые таблицы ссылаются внешним ключом на таблицу OptimisationParams, где мы храним входные параметры робота. Каждый столбец входного параметра носит его имя (к примеру Fast/Slow — Быстрая/Медленная скользящие средние). Также, каждый столбец должен иметь определённый формат данных. Многие базы Sqlite создают без определения формата данных столбцов в таблицах, и тогда все данные хранятся в виде строк, но для наших целей требуется точно знать формат данных — ведь в дальнейшем мы будем фильтровать коэффициенты по определенному признаку, и, соответственно, требуется приведение выгруженных данных из базы к своему исходному формату.

Для этого перед занесением данных в базу мы должны знать их формат — можно поступить несколькими способами: либо создать шаблонный метод и передавать в него конвертер, либо создать класс, который по сути будет являться универсальным хранителем нескольких типов данных совмещенных с именем переменной робота, к которым(речь о формате данных) можно привести практически любой тип данных. Для реализации поставленной задачи был выбран второй вариант и создан класс CDataKeeper. Описываемый класс может хранить в себе 3 типа данных [int, double, string], а все другие типы данных, которые могут быть входными форматами робота, так или иначе могут быть приведены к ним.

//+------------------------------------------------------------------+
//| Типы входных данных параметра робота                             |
//+------------------------------------------------------------------+
enum DataTypes
  {
   Type_INTEGER,// int
   Type_REAL,// double, float
   Type_Text // string
  };
//+------------------------------------------------------------------+
//| Результат сравнения двух CDataKeeper                             |
//+------------------------------------------------------------------+
enum CoefCompareResult
  {
   Coef_Different,// разные типы данных или же имена переменных
   Coef_Equal,// переменные равны
   Coef_Less, // переменная текущая меньше, чем переданная
   Coef_More // переменная текущая больше, чем переданная
  };
//+---------------------------------------------------------------------+
//| Класс который хранит в себе один конкретный входной параметр робота.|
//| Может хранить в себе данные следующих типов: [int, double, string]  |
//+---------------------------------------------------------------------+
class CDataKeeper
  {
public:
                     CDataKeeper(); // Конструктор
                     CDataKeeper(const CDataKeeper&other); // Конструктор копирования
                     CDataKeeper(string _variable_name,int _value); // Параметральный конструктор
                     CDataKeeper(string _variable_name,double _value); // Параметральный конструктор
                     CDataKeeper(string _variable_name,string _value); // Параметральный конструктор

   CoefCompareResult Compare(CDataKeeper &data); // Метод сравнения

   DataTypes         getType(){return variable_type;}; // Получение типа данных
   string            getName(){return variable_name;}; // Получение имени параметра
   string            valueString(){return value_string;}; // Получение параметра
   int               valueInteger(){return value_int;}; // Получение параметра
   double            valueDouble(){return value_double;}; // Получение параметра
   string            ToString(); // Перевод любого параметра в строку. Если это строковый параметр, то к строке добавляются с обеих сторон одинарные кавычки <<'>>

private:
   string            variable_name,value_string; // имя переменной и строковая переменная
   int               value_int; // Int переменная
   double            value_double; // Double переменная
   DataTypes         variable_type; // Тип переменной

   int compareDouble(double x,double y) // Сравнение типов Double с точностью до 10 знака
     {
      double diff=NormalizeDouble(x-y,10);
      if(diff>0) return 1;
      else if(diff<0) return -1;
      else return 0;
     }
  };

Три перегрузки конструктора принимают первым параметром имя переменной, а вторым ее значение, приведенное к одному из упомянутых типов. Данные значения сохраняются в глобальных переменных класса, начинающихся со слова value_, и далее идет указание типа. Метод getType() возвращает тип в виде представленного выше перечисления, а метод getName() — имя переменной. Методы, начинающиеся со слова value возвращают переменную требуемого типа, однако, если вызван метод valueDouble(), а переменная хранимая в классе имеет тип int, то вернется NULL. Метод ToString() приводит значение любой из переменных к строковому формату, однако, если переменная изначально была строкой, то к ней добавляются одинарные кавычки (для удобства формирования запросов SQL). Метод Compare(CDataKeeper &ther) помогает сравнивать два объекта типа CDataKeeper — сравнивая при этом:

  1. Имя переменой робота
  2. Тип переменной
  3. Значение переменной

Если первые два сравнения не проходят — значит мы пытаемся сравнить два разных параметра (к примеру период быстрой с периодом медленной скользящих средних), и соответственно, не можем этого сделать — ведь нам требуется сравнивать лишь однотипные данные. Поэтому возвращаем значение Coef_Different типа CoefCompareResult, в иных случаях происходит сравнение и возвращается требуемый результат. Сам метод сравнения реализован следующим образом:

//+------------------------------------------------------------------+
//| Сравнение текущего параметра с переданным                        |
//+------------------------------------------------------------------+
CoefCompareResult CDataKeeper::Compare(CDataKeeper &data)
  {
   CoefCompareResult ans=Coef_Different;

   if(StringCompare(this. variable_name,data.getName())==0 && 
      this.variable_type==data.getType()) // Сравнение имени и типов
     {
      switch(this.variable_type) // Сравнение значений
        {
         case Type_INTEGER :
            ans=(this.value_int==data.valueInteger() ? Coef_Equal :(this.value_int>data.valueInteger() ? Coef_More : Coef_Less));
            break;
         case Type_REAL :
            ans=(compareDouble(this.value_double,data.valueDouble())==0 ? Coef_Equal :(compareDouble(this.value_double,data.valueDouble())>0 ? Coef_More : Coef_Less));
            break;
         case Type_Text :
            ans=(StringCompare(this.value_string,data.valueString())==0 ? Coef_Equal :(StringCompare(this.value_string,data.valueString())>0 ? Coef_More : Coef_Less));
            break;
        }
     }
   return ans;
  }

Типонезависимое представление переменных позволяет оперировать с ними (переменными) в более удобной форме, при этом учитывая как имя, тип данных переменной, так и ее значение.

Следующей задачей стоит формирование описанной выше базы данных. Для этих целей был создан класс CDatabaseWriter.

//+------------------------------------------------------------------+
//| Коллбек, рассчитывающий пользовательский коэффициент             |
//| На вход передаются данные истории и флаг для какого типа истории |
//| требуется расчет коэффициента                                    |
//+------------------------------------------------------------------+
typedef double(*customScoring_1)(const DealDetales &history[],bool isOneLot);
//+------------------------------------------------------------------+
//| Коллбек, рассчитывающий пользовательский коэффициент             |
//| На вход передаются подключение к базе данных (только чтение)     |
//| история, флаг типа запрашиваемого коэффициента                   |
//+------------------------------------------------------------------+
typedef double(*customScoring_2)(CSqliteManager *dbManager,const DealDetales &history[],bool isOneLot);
//+------------------------------------------------------------------+
//| Класс, сохраняющий данные в базе и создающий базу перед этим     |
//+------------------------------------------------------------------+
class CDBWriter
  {
public:
   // Вызывать в OnInit одну из перегрузок
   void              OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); // коллбек №1
   void              OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],customScoring_2 scoringFunction,double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); // Коллбек №2
   void              OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT);// Без коллбека и пользовательского коэффициента (равен нулю)
   double            OnTesterEvent();// Вызывать в OnTester
   void              OnTickEvent();// Вызывать в OnTick

private:
   CSqliteManager    dbManager; // Коннектор к базе
   CDataKeeper       coef_array[]; // Входные параметры
   datetime          DT_Border; // Дата самой последней свечи (вычисляется в OnTickEvent)
   double            r; // Безрисковая ставка

   customScoring_1   scoring_1; // Коллбек
   customScoring_2   scoring_2; // Коллбек
   int               scoring_type; // Тип коллбека [1,2]
   string            DBPath; // Путь к базе 
   double            balance; // Баланс
   ENUM_TIMEFRAMES   TF; // Таймфрейм

   void              CreateDB(const string DBPath,const CDataKeeper &inputData_array[],double r,ENUM_TIMEFRAMES TF);// Создается база и все прилагающееся
   bool              isForvard();// Определение типа текущей оптимизации (историческая / форвардная)
   void              WriteLog(string s,string where);// Запись лог файла

   int               setParams(bool IsForvard,CReportCreator *reportCreator,DealDetales &history[],double &customCoef);// Заполнение таблицы входных параметров
   void              setBuyAndHold(bool IsForvard,CReportCreator *reportCreator);// Заполнение истории Buy And Hold
   bool              setTraidingHistory(bool IsForvard,DealDetales &history[],int ID);// Заполнение истории торгов
   bool              setTotalResult(TotalResult &coefData,bool isOneLot,long ID,bool IsForvard,double customCoef);// Заполнение таблицы с коэффициентами
   bool              isHistoryItem(bool IsForvard,DealDetales &item,int ID); // Проверка существуют ли уже эти параметры в таблице истории торгов или же нет
  };

Данный класс используется только лишь в самом пользовательском роботе и призван создать входной параметр для описываемой программы, а именно — базу с требуемой структурой и наполнением. Как видно, он имеет 3 публичных метода (перегруженный метод считаю за один):

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

//+------------------------------------------------------------------+
//| Создание базы и подключения                                      |
//+------------------------------------------------------------------+
void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],customScoring_2 scoringFunction,double _r,ENUM_TIMEFRAMES _TF)
  {
   CreateDB(_DBPath,inputData_array,_r,_TF);
   scoring_2=scoringFunction;
   scoring_type=2;
  }
//+------------------------------------------------------------------+
//| Создание базы и подключения                                      |
//+------------------------------------------------------------------+
void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,double _r,ENUM_TIMEFRAMES _TF)
  {
   CreateDB(_DBPath,inputData_array,_r,_TF);
   scoring_1=scoringFunction;
   scoring_type=1;
  }
//+------------------------------------------------------------------+
//| Создание базы и подключения                                      |
//+------------------------------------------------------------------+
void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],double _r,ENUM_TIMEFRAMES _TF)
  {
   CreateDB(_DBPath,inputData_array,_r,_TF);
   scoring_type=0;
  }

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

Метод CreateDB является одним из основных методов класса — он выполняет полную подготовку к работе:

Публичный метод OnTickEvent на каждом тике сохраняет дату минутной свечи. Дело в том, что во время тестирования стратегии нет возможности определить, является ли текущий проход форвардным или же нет, а в базе есть подобный параметр. Но нам известно, что тестер форвардные проходы прогоняет после исторических. Соответственно, перезаписывая на каждом тике переменную с датой, под конец оптимизационного процесса мы узнаем самую последнюю дату. В таблице OptimisationParams существует параметр «HistoryBorder» — он как раз и равняется данной сохраненной дате. Строки в данную таблицу заносятся только во время исторической оптимизации. Во время первого прохода с данными параметрами (он же проход на исторической оптимизации), дата, о которой идет речь, заносится в требуемое поле в базе. Далее, если мы видим в одном из следующих проходах, что запись с этими параметрами уже имеется в базе, тогда существуют два варианта:

  1. либо пользователь по каким-то причинам остановил историческую оптимизацию и вновь запустил ее,
  2. либо это форвардная оптимизация.

Для фильтрации одного от другого мы сверяем последнюю дату, сохраненную в текущем проходе, с датой из базы. Если текущая дата больше той, что в базе, то это форвардный проход, если же меньше или равна — исторический. Учитывая тот факт, что оптимизация может быть запущена дважды с одними и теми же коэффициентами, мы вносим в базу только новые данные, либо отменяем все сделанные изменения за текущий проход. Метод OnTesterEvent() сохраняет данные в базе. Он реализован следующим образом:

//+------------------------------------------------------------------+
//| Сохранение всех данных в базе и возврат                          |
//| пользовательского коэффициента                                   |
//+------------------------------------------------------------------+
double CDBWriter::OnTesterEvent()
  {

   DealDetales history[];

   CDealHistoryGetter historyGetter;
   historyGetter.getDealsDetales(history,0,TimeCurrent()); // Получение истории торгов

   CMutexSync sync; // сам объект синхронизации 
   if(!sync.Create(getMutexName(DBPath))) { Print(Symbol()+" MutexSync create ERROR!"); return 0; }
   CMutexLock lock(sync,(DWORD)INFINITE); // лочим участок в этих скобках

   bool IsForvard=isForvard(); // Узнаем является ли текущая итерация тестера - форвардной
   CReportCreator rc;
   string Symb[];
   rc.Get_Symb(history,Symb); // Получаем список символов
   rc.Create(history,Symb,balance,r); // Создаем отчет (отчет Buy And Hold - создается самостоятельно)

   double ans=0;
   dbManager.BeginTransaction(); // Начало транзакции

   CStatement stmt(dbManager.Create_statement("INSERT OR IGNORE INTO ParamsType VALUES(@ParamName,@ParamType);")); // Запрос на сохранение списка типов параметров робота
   if(stmt.get()!=NULL)
     {
      for(int i=0;i<ArraySize(coef_array);i++)
        {
         stmt.Parameter(1,coef_array[i].getName());
         stmt.Parameter(2,(int)coef_array[i].getType());
         stmt.Execute(); // сохраняем типы параметров и из наименования
        }
     }

   int ID=setParams(IsForvard,&rc,history,ans); // Сохраняем параметры робота, оценочные коэффициенты и получаем ID
   if(ID>0)// Если ID > 0 то параметры сохранены успешно 
     {
      if(setTraidingHistory(IsForvard,history,ID)) // Сохраняем торговую историю и проверяем сохранилась ли она
        {
         setBuyAndHold(IsForvard,&rc); // Сохраняем историю Buy And Hold (сохранится лишь раз - во время первого сохранения)
         dbManager.CommitTransaction(); // Подтверждаем завершение транзакции
        }
      else dbManager.RollbackTransaction(); // Иначе - отменяем транзакцию
     }
   else dbManager.RollbackTransaction(); // Иначе отменяем транзакцию

   return ans;
  }

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

Соответственно если тестировался советник торгующий, к примеру, спредами — то и история торгов будет выгружена по обоим символам, по которым происходила торговля. Затем создается отчет при помощи класса, рассмотрение которого будет произведено ниже, и производится запись в базу. Для корректной записи создается транзакция, которая отменяется в случае, если при заполнении какой-либо таблицы произошла ошибка, либо были получены некорректные данные. Сперва сохраняются коэффициенты, а затем, если все прошло гладко — сохраняем торговую историю, далее историю "Buy And Hold", но она сохраняется лишь раз при первом внесении данных. В случае ошибки сохранения данных формируется Log File в папке Common/Files.

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

//+------------------------------------------------------------------+
//| Класс, считывающий данные из базы                                |
//+------------------------------------------------------------------+
class CDBReader
  {
public:
   void              Connect(string DBPath);// Метод, подключающийся к базе

   bool              getBuyAndHold(BuyAndHoldChart_item &data[],bool isForvard);// Метод, считывающий историю Buy And Hold 
   bool              getTraidingHistory(DealDetales &data[],long ID,bool isForvard);// Метод, считывающий наторгованную роботом историю 
   bool              getRobotParams(CoefData_item &data[],bool isForvard);// Метод, считывающий параметры робота и коэффициенты

private:
   CSqliteManager    dbManager; // Менеджер базы данных 
   string            DBPath; // Путь к базе

   bool              getParamTypes(ParamType_item &data[]);// Считывает типы входных параметров и их имена.
  };

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

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


Расчетная часть

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

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

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

Начнем рассмотрение данного блока по порядку — с класса, создающего отчет торгов. Описываемый класс находится в директории Include в папке «History manager», который озаглавлен следующим образом:

//+------------------------------------------------------------------+
//| Класс создания статистики истории торгов                         |
//+------------------------------------------------------------------+
class CReportCreator
  {
public:

   //=============================================================================================================================================
   // Calculation/ Recalculation:
   //=============================================================================================================================================

   void              Create(DealDetales &history[],DealDetales &BH_history[],const double balance,const string &Symb[],double r);
   void              Create(DealDetales &history[],DealDetales &BH_history[],const string &Symb[],double r);
   void              Create(DealDetales &history[],const string &Symb[],const double balance,double r);
   void              Create(DealDetales &history[],double r);
   void              Create(const string &Symb[],double r);
   void              Create(double r=0);

   //=============================================================================================================================================
   // Getters:
   //=============================================================================================================================================

   bool              GetChart(ChartType chart_type,CalcType calc_type,PLChart_item &out[]); // Получение графиков PL
   bool              GetDistributionChart(bool isOneLot,DistributionChart &out); // Получение графиков распределения
   bool              GetCoefChart(bool isOneLot,CoefChartType type,CoefChart_item &out[]); // Получение графиков коэффициентов
   bool              GetDailyPL(DailyPL_calcBy calcBy,DailyPL_calcType calcType,DailyPL &out); // Получение графика PL по дням
   bool              GetRatioTable(bool isOneLot,ProfitDrawdownType type,ProfitDrawdown &out); // Получение таблицы крайних точек
   bool              GetTotalResult(TotalResult &out); // Получение таблицы TotalResult
   bool              GetPL_detales(PL_detales &out); // Получение таблицы PL_detales
   void              Get_Symb(const DealDetales &history[],string &Symb[]); // Получение массива инструментов, которые участвовали в торгах
   void              Clear(); // Очистка статистики

private:
   //=============================================================================================================================================
   // Private data types:
   //=============================================================================================================================================
   // Структура типов графика PL
   struct PL_keeper
     {
      PLChart_item      PL_total[];
      PLChart_item      PL_oneLot[];
      PLChart_item      PL_Indicative[];
     };
   // Структура типов графика дневной Прибыли/Убытка
   struct DailyPL_keeper
     {
      DailyPL           avarage_open,avarage_close,absolute_open,absolute_close;
     };
   // Структура таблиц крайних точек
   struct RatioTable_keeper
     {
      ProfitDrawdown    Total_max,Total_absolute,Total_percent;
      ProfitDrawdown    OneLot_max,OneLot_absolute,OneLot_percent;
     };
   // Структуры для подсчета количества прибылей и убытка подряд
   struct S_dealsCounter
     {
      int               Profit,DD;
     };
   struct S_dealsInARow : public S_dealsCounter
     {
      S_dealsCounter    Counter;
     };
   // Структуры для расчета вспомогательных данных
   struct CalculationData_item
     {
      S_dealsInARow     dealsCounter;
      int               R_arr[];
      double            DD_percent;
      double            Accomulated_DD,Accomulated_Profit;
      double            PL;
      double            Max_DD_forDeal,Max_Profit_forDeal;
      double            Max_DD_byPL,Max_Profit_byPL;
      datetime          DT_Max_DD_byPL,DT_Max_Profit_byPL;
      datetime          DT_Max_DD_forDeal,DT_Max_Profit_forDeal;
      int               Total_DD_numDeals,Total_Profit_numDeals;
     };
   struct CalculationData
     {
      CalculationData_item total,oneLot;
      int               num_deals;
      bool              isNot_firstDeal;
     };
   // Структура для создания графиков коэффициентов
   struct CoefChart_keeper
     {
      CoefChart_item    OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
      CoefChart_item    OneLot_WinCoef_chart[],Total_WinCoef_chart[];
      CoefChart_item    OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
      CoefChart_item    OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
      CoefChart_item    OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
     };
   // Класс, участвующий в сортировки истории торгов по дате закрытия.
   class CHistoryComparer : public ICustomComparer<DealDetales>
     {
   public:
      int               Compare(DealDetales &x,DealDetales &y);
     };
   //=============================================================================================================================================
   // Keepers:
   //=============================================================================================================================================
   CHistoryComparer  historyComparer; // Сравнивающий класс
   CChartComparer    chartComparer; // Сравнивающий класс

                                    // Вспомогательные структуры
   PL_keeper         PL,PL_hist,BH,BH_hist;
   DailyPL_keeper    DailyPL_data;
   RatioTable_keeper RatioTable_data;
   TotalResult       TotalResult_data;
   PL_detales        PL_detales_data;
   DistributionChart OneLot_PDF_chart,Total_PDF_chart;
   CoefChart_keeper  CoefChart_data;

   double            balance,r; // Начальный депозит и безрисковая ставка
                                // Класс сортировщик
   CGenericSorter    sorter;

   //=============================================================================================================================================
   // Calculations:
   //=============================================================================================================================================
   // Подсчет PL
   void              CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type);
   // Подсчет Гистограмм PL
   void              CalcPLHist(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type);
   // Подсчет вспомогательных структур, по которым все строится
   void              CalcData(const DealDetales &deal,CalculationData &out,bool isBH);
   void              CalcData_item(const DealDetales &deal,CalculationData_item &out,bool isOneLot);
   // Подсчет Дневного прибыли/убытка
   void              CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal);
   void              cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy);
   void              avarageDay(PLDrawdown &day);
   // Сопоставление символов
   bool              isSymb(const string &Symb[],string symbol);
   // Подсчет Профит фактора
   void              ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot);
   // Подсчет Фактора восстановления
   void              RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot);
   // Подсчет Коэффициента выигрыша
   void              WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot);
   // Подсчет Коэффициента Шарпа
   double            ShartRatio_calc(PLChart_item &data[]);
   void              ShartRatio_chart_calc(CoefChart_item &out[],PLChart_item &data[],const DealDetales &deal);
   // Подсчет Распределения
   void              NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[]);
   double            PDF_calc(double Mx,double Std,double x);
   // Подсчет VaR
   double            VaR(double quantile,double Mx,double Std);
   // Подсчет Z-счета 
   void              AltmanZScore_chart_calc(CoefChart_item &out[],double N,double R,double W,double L,const DealDetales &deal);
   // Подсчет структуры TotalResult_item
   void              CalcTotalResult(CalculationData &data,bool isOneLot,TotalResult_item &out);
   // Подсчет структуры PL_detales_item
   void              CalcPL_detales(CalculationData_item &data,int deals_num,PL_detales_item &out);
   // Получение дня из даты
   ENUM_DAY_OF_WEEK  getDay(datetime DT);
   // Очистка данных
   void              Clear_PL_keeper(PL_keeper &data);
   void              Clear_DailyPL(DailyPL &data);
   void              Clear_RatioTable(RatioTable_keeper &data);
   void              Clear_TotalResult_item(TotalResult_item &data);
   void              Clear_PL_detales(PL_detales &data);
   void              Clear_DistributionChart(DistributionChart &data);
   void              Clear_CoefChart_keeper(CoefChart_keeper &data);

   //=============================================================================================================================================
   // Copy:
   //=============================================================================================================================================
   void              CopyPL(const PLChart_item &src[],PLChart_item &out[]); // Копирование графиков PL
   void              CopyCoefChart(const CoefChart_item &src[],CoefChart_item &out[]); // Копирование графиков коэффициентов

  };

Данный класс в отличие от своей предыдущей версии рассчитывает раза в два больше данных и строит больше типов графиков. Также перегрузки метода Create рассчитывают отчет.

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

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

Опишем данные, получаемые при вызове каждого из геттеров:

Метод Параметры тип графика
GetChart chart_type = _PL, calc_type = _Total График PL — по реально наторгованной истории
GetChart chart_type = _PL, calc_type = _OneLot График PL — при торговле одним лотом
GetChart chart_type = _PL, calc_type = _Indicative График PL — индикативный
GetChart chart_type = _BH, calc_type = _Total График BH — если управлять лотом как робот
GetChart chart_type = _BH, calc_type = _OneLot График BH — если торговать одним лотом
GetChart chart_type = _BH, calc_type = _Indicative График BH — индикативный
GetChart chart_type = _Hist_PL, calc_type = _Total Гистограмма PL — по реально наторгованной истории
GetChart chart_type = _Hist_PL, calc_type = _OneLot Гистограмма PL — если торговать одним лотом
GetChart chart_type = _Hist_PL, calc_type = _Indicative Гистограмма PL — Индикативная
GetChart chart_type = _Hist_BH, calc_type = _Total Гистограмма BH — по если управлять лотом как робот
GetChart chart_type = _Hist_BH, calc_type = _OneLot Гистограмма BH — если торговать одним лотом
GetChart chart_type = _Hist_BH, calc_type = _Indicative Гистограмма BH — Индикативная
GetDistributionChart isOneLot = true Распределения и VaR при торговле одним лотом
GetDistributionChart isOneLot = false Распределения и VaR при торговле как торговали
GetCoefChart isOneLot = true, type=_ShartRatio_chart Коэффициент Шарпа по времени при торговле одним лотом
GetCoefChart isOneLot = true, type=_WinCoef_chart Коэффициент выигрыша по времени при торговле одним лотом
GetCoefChart isOneLot = true, type=_RecoveryFactor_chart Фактор восстановления по времени при торговле одним лотом
GetCoefChart isOneLot = true, type=_ProfitFactor_chart Профит фактор по времени при торговле одним лотом
GetCoefChart isOneLot = true, type=_AltmanZScore_chart Z — счет Альтмана по времени при торговле одним лотом
GetCoefChart isOneLot = false, type=_ShartRatio_chart Коэффициент Шарпа по времени при торговле как торговали
GetCoefChart isOneLot = false, type=_WinCoef_chart Коэффициент выигрыша по времени при торговле как торговали
GetCoefChart isOneLot = false, type=_RecoveryFactor_chart Фактор восстановления по времени при торговле как торговали
GetCoefChart isOneLot = false, type=_ProfitFactor_chart Профит фактор по времени при торговле как торговали
GetCoefChart isOneLot = false, type=_AltmanZScore_chart Z — счет Альтмана по времени при торговле как торговали
GetDailyPL calcBy=CALC_FOR_CLOSE, calcType=AVERAGE_DATA PL по дням на момент закрытия усредненная
GetDailyPL calcBy=CALC_FOR_CLOSE, calcType=ABSOLUTE_DATA PL по дням на момент закрытия суммарная
GetDailyPL calcBy=CALC_FOR_OPEN, calcType=AVERAGE_DATA PL по дням на момент открытия усредненная
GetDailyPL calcBy=CALC_FOR_OPEN, calcType=ABSOLUTE_DATA PL по дням на момент открытия суммарная
GetRatioTable isOneLot = true, type = _Max Если торговать одним лотом — максимально достигнутые прибыль / убыток за один трейд
GetRatioTable isOneLot = true, type = _Absolute Если торговать одним лотом — суммарные прибыль / убыток
GetRatioTable isOneLot = true, type = _Percent Если торговать одним лотом — количество прибылей / убытков в %
GetRatioTable isOneLot = false, type = _Max Если торговать как торговали — максимально достигнутые прибыль / убыток за один трейд
GetRatioTable isOneLot = false, type = _Absolute Если торговать как торговали — суммарные прибыль / убыток
GetRatioTable isOneLot = false, type = _Percent Если торговать как торговали — количество прибылей / убытков в %
GetTotalResult
Таблица с коэффициентами
GetPL_detales
Краткая сводка по кривой PL
Get_Symb
Массив символов что были в торговой истории

График PL — по реально наторгованной истории:

Данный график равняется обычному графику PL, что мы видим в терминале по окончании всех проходов тестера.

График PL — при торговле одним лотом:

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

График PL — индикативный:

График нормированной PL. Если PL > 0, то PL делится на максимально достигнутую к этому момент убыточную сделку, иначе PL делится на максимально достигнутую к данному моменту прибыльную сделку.

Графики гистограмм строятся схожим образом.

Распределения и VaR

VaR - параметрический строится как по абсолютным данным, так и по приростам.

График распределения также строится как по абсолютным данным, так и по приростам.

Графики коэффициентов:

Строятся на каждой итерации цикла по соответствующим формулам по всей имеющейся на данную конкретную итерацию истории.

Графики дневной прибыли:

Строятся по 4 возможным упомянутым в таблице комбинациям прибыли. Выглядит в виде гистограммы.

Создающий все перечисленные данные метод выглядит следующим образом:

//+------------------------------------------------------------------+
//| Расчет / Пересчет коэффициентов                                   |
//+------------------------------------------------------------------+
void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r)
  {
   Clear(); // Очистка данных
            // Сохранение баланса
   this.balance=_balance;
   if(this.balance<=0)
     {
      CDealHistoryGetter dealGetter;
      this.balance=dealGetter.getBalance(history[ArraySize(history)-1].DT_open);
     }
   if(this.balance<0)
      this.balance=0;
// Сохранение ставки без риска
   if(_r<0) _r=0;
   this.r=r;

// Вспомогательные структуры
   CalculationData data_H,data_BH;
   ZeroMemory(data_H);
   ZeroMemory(data_BH);
// Сортировка истории торгов
   sorter.Method(Sort_Ascending);
   sorter.Sort<DealDetales>(history,&historyComparer);
// Цикл по истории торгов
   for(int i=0;i<ArraySize(history);i++)
     {
      if(isSymb(Symb,history[i].symbol))
         CalcData(history[i],data_H,false);
     }
// Сортировка истории Buy And Hold и цикл по ней
   sorter.Sort<DealDetales>(BH_history,&historyComparer);
   for(int i=0;i<ArraySize(BH_history);i++)
     {
      if(isSymb(Symb,BH_history[i].symbol))
         CalcData(BH_history[i],data_BH,true);
     }

// усредняем дневные PL (усредненного типа)
   avarageDay(DailyPL_data.avarage_close.Mn);
   avarageDay(DailyPL_data.avarage_close.Tu);
   avarageDay(DailyPL_data.avarage_close.We);
   avarageDay(DailyPL_data.avarage_close.Th);
   avarageDay(DailyPL_data.avarage_close.Fr);

   avarageDay(DailyPL_data.avarage_open.Mn);
   avarageDay(DailyPL_data.avarage_open.Tu);
   avarageDay(DailyPL_data.avarage_open.We);
   avarageDay(DailyPL_data.avarage_open.Th);
   avarageDay(DailyPL_data.avarage_open.Fr);

// Заполняем таблицы соотношений прибылей и убытков
   RatioTable_data.data_H.oneLot.Accomulated_Profit;
   RatioTable_data.data_H.oneLot.Accomulated_DD;
   RatioTable_data.data_H.oneLot.Max_Profit_forDeal;
   RatioTable_data.data_H.oneLot.Max_DD_forDeal;
   RatioTable_data.data_H.oneLot.Total_Profit_numDeals/data_H.num_deals;
   RatioTable_data.data_H.oneLot.Total_DD_numDeals/data_H.num_deals;

   RatioTable_data.Total_absolute.Profit=data_H.total.Accomulated_Profit;
   RatioTable_data.Total_absolute.Drawdown=data_H.total.Accomulated_DD;
   RatioTable_data.Total_max.Profit=data_H.total.Max_Profit_forDeal;
   RatioTable_data.Total_max.Drawdown=data_H.total.Max_DD_forDeal;
   RatioTable_data.Total_percent.Profit=data_H.total.Total_Profit_numDeals/data_H.num_deals;
   RatioTable_data.Total_percent.Drawdown=data_H.total.Total_DD_numDeals/data_H.num_deals;

// Подсчет нормального распределения
   NormalPDF_chart_calc(OneLot_PDF_chart,PL.PL_oneLot);
   NormalPDF_chart_calc(Total_PDF_chart,PL.PL_total);

// TotalResult
   CalcTotalResult(data_H,true,TotalResult_data.oneLot);
   CalcTotalResult(data_H,false,TotalResult_data.total);

// PL_detales
   CalcPL_detales(data_H.oneLot,data_H.num_deals,PL_detales_data.oneLot);
   CalcPL_detales(data_H.total,data_H.num_deals,PL_detales_data.total);
  }

Как видно из его реализации, часть данных рассчитываются по мере прохождения цикла по истории, а часть — после прохождения всех циклов на основании данных из структур: CalculationData data_H,data_BH.

Метод CalcData реализован схожим образом, что и метод Create. Только он вызывает те методы, которые должны вести расчет на каждой итерации. Все методы, ведущие расчет конечных данных, рассчитывают их исходя из информации, содержащейся в упомянутых структурах. Заполнение/перезаполнение описанных структур ведется следующим методом:

//+------------------------------------------------------------------+
//| Подсчет вспомогательных данных                                   |
//+------------------------------------------------------------------+
void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out,
                                   bool isOneLot)
  {
   double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); // PL
   int n=0;
// Кол - прибылей и убытков
   if(pl>=0)
     {
      out.Total_Profit_numDeals++;
      n=1;
      out.dealsCounter.Counter.DD=0;
      out.dealsCounter.Counter.Profit++;
     }
   else
     {
      out.Total_DD_numDeals++;
      out.dealsCounter.Counter.DD++;
      out.dealsCounter.Counter.Profit=0;
     }
   out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD);
   out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit);

// Серии из прибылей и убытков
   int s=ArraySize(out.R_arr);
   if(!(s>0 && out.R_arr[s-1]==n))
     {
      ArrayResize(out.R_arr,s+1,s+1);
      out.R_arr[s]=n;
     }

   out.PL+=pl; // PL общий
               // Макс Profit / DD
   if(out.Max_DD_forDeal>pl)
     {
      out.Max_DD_forDeal=pl;
      out.DT_Max_DD_forDeal=deal.DT_close;
     }
   if(out.Max_Profit_forDeal<pl)
     {
      out.Max_Profit_forDeal=pl;
      out.DT_Max_Profit_forDeal=deal.DT_close;
     }
// Накопленная Profit / DD
   out.Accomulated_DD+=(pl>0 ? 0 : pl);
   out.Accomulated_Profit+=(pl>0 ? pl : 0);
// Крайние точки по прибыли
   double maxPL=MathMax(out.Max_Profit_byPL,out.PL);
   if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// для сохранения даты нужна еще одна проверка
     {
      out.DT_Max_Profit_byPL=deal.DT_close;
      out.Max_Profit_byPL=maxPL;
     }
   double maxDD=out.Max_DD_byPL;
   double DD=0;
   if(out.PL>0)DD=out.PL-maxPL;
   else DD=-(MathAbs(out.PL)+maxPL);
   maxDD=MathMin(maxDD,DD);
   if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// для сохранения даты нужна еще одна проверка
     {
      out.Max_DD_byPL=maxDD;
      out.DT_Max_DD_byPL=deal.DT_close;
     }
   out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0));
  }

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

Класс фильтра результатов прохода оптимизаций озаглавлен следующим образом:

//+------------------------------------------------------------------+
//| Класс, фильтрующий проходы оптимизации после их выгрузки из базы.|
//+------------------------------------------------------------------+
class CParamsFiltre
  {
public:
                     CParamsFiltre(){sorter.Method(Sort_Ascending);} // Конструктор по умолчанию. 
   int               Total(){return ArraySize(arr_main);}; // Общее количество выгруженных параметров (по таблице Optimisation Data)
   void              Clear(){ArrayFree(arr_main);ArrayFree(arr_result);}; // Отчистка всех массивов
   void              Add(LotDependency_item &customCoef,CDataKeeper &params[],long ID,double total_PL,bool addToResult); // Добавление нового значения в массив
   double            GetCustomCoef(long ID,bool isOneLot);// Получение пользовательского коэффициента по ID
   void              GetParamNames(CArrayString &out);// Получение наименования параметров робота
   void              Get_UniqueCoef(UniqCoefData_item &data[],string paramName,CArrayString &coefValue); // Получение уникальных коэффициентов
   void              Filtre(string Name,string from,string till,long &ID_Arr[]);// Фильтрация массива arr_result
   void              ResetFiltre(long &ID_arr[]);// Сброс фильтра

   bool              Get_Distribution(Chart_item &out[],bool isMainTable);// Построение распределения по обоим массивам
   bool              Get_Distribution(Chart_item &out[],string Name,string value);// Построение распределения по выбранным данным

private:
   CGenericSorter    sorter; // Сортировщик
   CCoefComparer     cmp_coef;// Сопоставление коэффициентов
   CChartComparer    cmp_chart;// Сопоставление графиков

   bool              selectCoefByName(CDataKeeper &_input[],CDataKeeper &out,string Name);// Выбор коэффициентов по имени
   double            Mx(CoefStruct &_arr[]);// Среднеарифметическое
   double            Std(CoefStruct &_arr[],double _Mx);// Стандартно квадратическое отклонение

   CoefStruct        arr_main[]; // Аналог таблицы Optimisation data
   CoefStruct        arr_result[];// Аналог таблицы Result
  };

Разберем структуру данного класса и расскажем поподробнее о некоторых из методов. Как видно — класс имеет два глобальных массива: arr_main и arr_result. Данные массивы являются хранилищем данных оптимизации. После выгрузки таблицы с проходами оптимизации из базы она разбивается на две таблицы:

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

Так как данный класс оперирует с коэффициентами роботов, а именно, с их представлением в виде класса CdataKeeper — то был создан приватный метод «selectCoefByName», выбирающий один требуемый коэффициент и возвращающий результат по ссылке из массива, переданных коэффициентов робота одного конкретного прохода оптимизации.

Метод Add добавляет строку, выгруженную из базы данных, в оба массива (при условии, что параметр addToResult ==true) или только в массив arr_main (если addToResult ==false). ID — уникальный параметр каждого прохода оптимизации, поэтому по нему построена вся работа с определением конкретного выбранного прохода. По данному параметру мы получаем из представленных массивов рассчитанный пользователем коэффициент. Дело в том, что программа сама по себе не знает формулы расчета пользовательской оценки, так как она (оценка) рассчитывается во время оптимизации робота — без участия данной программы. Именно из-за этого мы вынуждены сохранять пользовательскую оценку в данные массивы и, когда она запрашивается, при помощи метода GetCustomCoef по переданному ID мы получаем ее.

Наиболее важными из всех методов класса — являются следующие:


Презентер

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

//+------------------------------------------------------------------+
//| Интерфейс презентера                                             |
//+------------------------------------------------------------------+
interface IPresenter
  {
   void Btn_Update_Click(); // Загрузка данных и построение всей формы
   void Btn_Load_Click(); // Создание отчета
   void OptimisationData(bool isMainTable);// Выбор строки оптимизации в таблицах 
   void Update_PLByDays(); // Выгрузка прибыли и убытка по дням
   void DaySelect();// Выбор Дня из таблицы PL по дням недели
   void PL_pressed(PLSelected_type type);// Построение графика PL по выбранной истории 
   void PL_pressed_2(bool isRealPL);// Построение графиков "Other charts"
   void SaveToFile_Click();// Сохранение файла с данными (в песочницы)
   void SaveParam_passed(SaveParam_type type);// Выбор данных для записи в файл
   void OptimisationParam_selected(); // Выбор параметра оптимизации и заполнение вкладки "Optimisation selection"
   void CompareTables(bool isChecked);// Построение распределение по таблицы с результатами (для соотношения с общей(главной) таблицей)
   void show_FriquencyChart(bool isChecked);// Показ графика частот выпадания прибыли/убытка
   void FriquencyChart_click();// Выбор строки в таблице с коэффициентами и построение распределения
   void Filtre_click();// Фильтрация по выбранным условиям
   void Reset_click();// Сброс фильтров
   void PL_pressed_3(bool isRealPL);// Построение графиков прибыли / убытка по всем данным из таблице Result
   void PL_pressed_4(bool isRealPL);// Построение таблиц со статистикой
   void setChartFlag(bool isPlot);// Условие строить (или же не строить) графики из метода PL_pressed_3(bool isRealPL);
  };

Класс презентера реализует требуемый интерфейс и выглядит следующим образом:

class CPresenter : public IPresenter
  {
public:
                     CPresenter(CWindowManager *_windowManager); // Конструктор

   void              Btn_Update_Click();// Загрузка данных и построение всей формы
   void              Btn_Load_Click();// Создание отчета
   void              OptimisationData(bool isMainTable);// Выбор строки оптимизации в таблицах 
   void              Update_PLByDays();// Выгрузка прибыли и убытка по дням
   void              PL_pressed(PLSelected_type type);// Построение графика PL по выбранной истории 
   void              PL_pressed_2(bool isRealPL);// Построение графиков "Other charts"
   void              SaveToFile_Click();// Сохранение файла с данными (в песочницы)
   void              SaveParam_passed(SaveParam_type type);// Выбор данных для записи в файл
   void              OptimisationParam_selected();// Выбор параметра оптимизации и заполнение вкладки "Optimisation selection"
   void              CompareTables(bool isChecked);// Построение распределение по таблицы с результатами (для соотношения с общей(главной) таблицей)
   void              show_FriquencyChart(bool isChecked);// Показ графика частот выпадания прибыли/убытка
   void              FriquencyChart_click();// Выбор строки в таблице с коэффициентами и построение распределения
   void              Filtre_click();// Фильтрация по выбранным условиям
   void              PL_pressed_3(bool isRealPL);// Построение графиков прибыли / убытка по всем данным из таблице Result
   void              PL_pressed_4(bool isRealPL);// Построение таблиц со статистикой
   void              DaySelect();// Выбор Дня из таблицы PL по дням недели
   void              Reset_click();// Сброс фильтров
   void              setChartFlag(bool isPlot);// Условие строить (или же не строить) графики из метода PL_pressed_3(bool isRealPL);

private:
   CWindowManager   *windowManager;// Ссылка на класс, представляющий окно
   CDBReader         dbReader;// Класс, работающий с базой
   CReportCreator    reportCreator; // Класс, обрабатывающий данные

   CGenericSorter    sorter; // Класс сортировки
   CoefData_comparer coefComparer; // Класс, сопоставляющий данные

   void              loadData();// Выгрузка данных из базы и заполнение таблиц

   void              insertDataTo_main_Table(bool isResult,const CoefData_item &data[]); // Вставляет данные в таблицу с результатами и в "Главную" таблицу (таблицы с коэффициентами проходов оптимизаций)
   void              insertRowTo_main_Table(CTable *tb,int n,const CoefData_item &data); // Непосредственно вставка данных в таблицы с проходами оптимизации
   void              selectChartByID(long ID,bool recalc=true);// Выбор графиков по ID
   void              createReport();// Создание отчета
   string            getCorrectPath(string path,string name);// Получение корректного пути к файлу
   bool              getPLChart(PLChart_item &data[],bool isOneLot,long ID);

   bool              curveAdd(CGraphic *chart_ptr,const PLChart_item &data[],bool isHist);// Добавление графика в Other Charts
   bool              curveAdd(CGraphic *chart_ptr,const CoefChart_item &data[],double borderPoint);// Добавление графика в Other Charts
   bool              curveAdd(CGraphic *chart_ptr,const Distribution_item &data);// Добавление графика в Other Charts
   void              setCombobox(CComboBox *cb_ptr,CArrayString &arr,bool isFirstIndex=true);// Установка параметров комбобокса
   void              addPDF_line(CGraphic *chart_ptr,double &x[],color clr,int width,string _name=NULL);// Добавление плавной линии графика распределения
   void              plotMainPDF();// Построение распределения по "Главной" таблице (Optimisation Data)
   void              updateDT(CDropCalendar *dt_ptr,datetime DT);// Обновление выпадающих календарей

   CParamsFiltre     coefKeeper;// Сортировщик проходов оптимизации (по распределениям сортируем)
   CArrayString      headder; // Заголовок таблиц с коэффициентами

   bool              _isUpbateClick; // Признак нажатия кнопки "Update" - и выгрузки данных из базы.
   long              _selectedID; // ID выделенный серии графика всех PL (красным если убыточный и зеленым если прибыльный)
   long              _ID,_ID_Arr[];// Массив ID отобранных в таблицу Result после загрузки данных
   bool              _IsForvard_inTables,_IsForvard_inReport; // Флаг типа данных оптимизаций в таблицах проходов оптимизаций
   datetime          _DT_from,_DT_till;
   double            _Gap; // Запомненный тип добавленного гэпа (имитация расширения спреда / или же проскальзывания...) прошлого выбранного графика оптимизаций
  };

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


Заключение

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

Самый простой способ получить искомые 100 лучших проходов оптимизации — это подключить класс CDBWriter к Вашему роботу как в роботе с примером (находится в приложенных файлах), установить условный фильтр (к примеру Профит Фактор >= 1 — это разом отметет все проигрышные сочетания) и нажать на кнопку Update, оставив при этом параметр настроек "Show n params" равным 100. Тогда в таблице с результатами отфильтруются 100 лучших проходов оптимизаций (по установленному Вами фильтру). Более детально каждая из опций получившегося приложения, а также более детальные методы отбора коэффициентов будут рассмотрены в следующей статье.


К статье приложены следующие файлы:

Experts/2MA_Martin — проект тестового робота

Experts/OptimisationSelector — проект описанного приложения

Include/CustomGeneric

Include/History manager

Include/OptimisationSelector

Include/Sqlite3

Include/WinApi

Libraries

Тестовая база