100 лучших проходов оптимизации (Часть 1). Cоздание анализатора оптимизаций
- Введение
- Структура анализатора оптимизаций
- Графическая часть
- Работа с базой данных
- Расчетная часть
- Презентер
- Заключение
Введение
Современные технологии уже достаточно прочно осели в сфере торговли на финансовых рынках и сейчас практически невозможно представить как бы мы могли обойтись без них в данной сфере. А ведь сравнительно совсем не давно — торговля велась вручную, существовал целый язык жестов (который стремительно уходит в небытие) описывающий какое количество актива требуется купить или же продать.
Компьютерный мир достаточно быстро вытеснил данный способ торговли, а вместе с тем принес интернет-трейдинг в дом каждого желающего. Теперь мы можем смотреть на котировки активов в режиме реального времени и принимать соответствующие решения. Более того, с приходом интернет технологий в биржевую индустрию из данной сферы стремительно начала исчезать ручная торговля. Сейчас больше половины сделок производится торговыми алгоритмами, и не лишним будет сказать, что среди наиболее удобных для этого терминалов — под номером один идет 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 Sqlite3
- Вышеуказанная библиотека графического интерфейса с требуемыми правками (описаны ниже в блоке работы с графикой)
Сам робот может быть написан как угодно (с помощью ООП, просто функции внутри шаблона робота, импортироваться из 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; в метод CListView::Clear(const bool redraw=false). Данный метод находится на 600 строке в файле ListView.mqh. В файле, находящемся по пути: |
---|
Что бы создать окно в 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 — разделяет таблицу со всеми отобранными проходами оптимизации ("Optimisation Data") от остального интерфейса программы. В данную таблицу заполняются все результаты, которые удовлетворяют условиям фильтра на вкладке с настройками. Затем полученные сортируются по коэффициенту выбранному в "ComboBox — Sort by". В отсортированном виде полученные данные переносятся в описываемую таблицу. Вкладка с остальным интерфейсом программы содержит еще 3 Tab контейнера.
- tab_up_1 — содержит разделение на первичные настройки программы и на таблицу с отфильтрованными результатами. Вкладка "Settings" - по мимо описанных условных фильтров служит для выбора базы данных и ввода дополнительной информации. К примеру, можно выбрать — стоит ли заносить в таблицу с результатами отбора информации все те данные, что были занесены в таблицу на вкладке "Optimisation Data", или же определенное количество лучших параметров (фильтрация по убыванию по выбранному коэффициенту).
- tab_up_2 — содержит 3 вкладки, каждая из которых содержит интерфейс, выполняющий 3 разнотипные задачи. Первая вкладка содержит полный отчет по выбранному проходу оптимизации и позволяет моделировать проскальзывание, а также рассматривать историю торгов за определенный временной промежуток. Вторая — служит фильтров оптимизационных проходов и помогает, во-первых, определить чувствительность стратегии к разным параметрам, а во-вторых — сузить количество результатов оптимизации, выбирая наиболее адекватные интервалы интересующих параметров. Последняя вкладка служит графическим представлением таблицы результатов оптимизации и показывает общее количество отобранных параметров оптимизаций.
- tab_down — насчитывает 5 вкладок, 4 из которых — это представление отчета торгов данного робота в процессе оптимизации с выбранными параметрами, а последняя вкладка — выгрузка данных в файл. Первая вкладка представляет таблицу с оценочными коэффициентами. Вторая вкладка — распределение прибыли/убытка по дням торгов. Третья — представление графика прибыли и убытка, наложенного на стратегию "Buy and Hold" (черный график) Четвертая вкладка — представление изменения некоторых выбранных коэффициентов во времени, а также несколько дополнительных интересных и информативных видов графиков, которые можно получить, анализируя результаты торгов робота.
Процесс создания вкладок однотипен — различается лишь наполнение. В качестве примера приведу метод, создающий основную вкладку:
//+------------------------------------------------------------------+ //| 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); }
По мимо наполнения, которое может варьироваться, основными строками кода является:
- Добавление указателя на главный элемент — требуется что бы контейнер вкладок знал за каким элементом он закреплен
- Строка создания элемента управления
- Добавления элемента в общий список элементов управления.
Следующими по иерархии идут элементы управления. В данном приложении было использовано 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 файла, пройдемся по ним в порядке написания.
- Первое что нам требуется — это импорт нужных функций для работы с базой из Dll. Так как была поставлена цель — создание обертки, содержащей минимальный требуемый функционал, мы не импортировали и 1% от общего количества функций, предоставляемых разработчиками базы. Все требуемые функции импортируются в файле «sqlite_amalgmation.mqh». Данные функции хорошо прокомментированы на сайте разработчика, а также подписаны в вышеупомянутом файле. При желании таким же образом можно импортировать весь заголовочный файл, в результате будет получен полноценный список всех функций и, соответственно, доступ к ним. Список импортируемых функций следующий:
#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.
- Вторым делом предстоит написать функциональную обертку для подключения к базе. Это должен быть класс, который создает подключение к базе и освобождает его (отключается от базы) в деструкторе. Также он должен уметь исполнять простые строковые Sql команды, управлять транзакциями и создавать запросы (стейтменты). Весь описанный функционал был реализован в классе CsqliteManager — именно с его создания происходит процесс взаимодействия с базой.
//+------------------------------------------------------------------+ //| Класс соединения и управление базой | //+------------------------------------------------------------------+ 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 метода, конвертирующий строку в требуемый формат данных.
- Следующей задачей идет создание обертки для удобной работы с запросами (statement). Запрос к базе должен создаваться и уничтожаться. Создание запроса возложено на CsqliteManager, а вот памятью никто не управляет. Иначе говоря, после создания запроса его нужно уничтожать, когда он уже не требуется, в противном случае он не даст отключиться от базы, и при попытке завершения работы с базой мы получим исключение, говорящие о том, что база занята. Также класс-обертка стейтмента должен уметь наполнять запрос передаваемыми параметрами (в случаях, когда он формируется по типу «INSERT INTO table_1 VALUES(@ID,@Param_1,@Param_2);»). Вдобавок, данный класс должен уметь исполнять помещенный в него запрос (метод Exequte).
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 для ранее сохраненного запроса.
- Заключающим классом в обертке работы с базой выступает класс CSqliteReader, который должен уметь читать ответ от базы данных. Также по аналогии с прошлыми классами, в своем деструкторе данный класс вызывает метод sqlite3_reset — он скидывает запрос и позволяет работать с ним вновь. В новых версиях базы данную функцию можно не вызывать, однако она была оставлена разработчиками, и на всякий случай употреблена мной в представленной обертке. Также данный класс должен выполнять свои основные обязанности, а именно — чтение ответа от базы построчно с возможностью конвертации прочитанных данных в соответствующий формат.
//+------------------------------------------------------------------+ //| Класс, читающий ответы от баз | //+------------------------------------------------------------------+ 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:
- Time — ось X - отметка временного интервала
- PL_total — прибыль/убыток, если наращивать лот соразмерно роботу
- PL_oneLot — прибыль/убыток, если торговать все время одним лотом
- DD_total — просадка, если торговать лотом, как торговал робот
- DD_oneLot — просадка, если торговать одним лотом
- isForvard — признак форвардного графика
Таблица OptimisationParams:
- ID — Уникальный автозаполняемый номер записи в базе
- HistoryBorder — Дата завершения исторической оптимизации
- TF — Таймфрейм
- Param_1...Param_n — параметр
- InitalBalance — размер начального баланса
Таблица ParamsCoefitients:
- ID — Внешний ключ, ссылка на OptimisationParams(ID)
- isForvard — признак форвардной оптимизации
- isOneLot — признак графика, по которому рассчитывался коэффициент
- DD — просадка
- averagePL — средняя прибыль/убыток по графику PL
- averageDD — средняя просадка
- averageProfit — средняя прибыль
- profitFactor — профит фактор
- recoveryFactor — фактор восстановления
- sharpRatio — коэффициент Шарпа
- altman_Z_Score — Z-счет Альтмана
- VaR_absolute_90 — VaR 90
- VaR_absolute_95 — VaR 95
- VaR_absolute_99 — VaR 99
- VaR_growth_90 — VaR 90
- VaR_growth_95 — VaR 95
- VaR_growth_99 — VaR 99
- winCoef — коэффициент выигрыша
- customCoef — пользовательский коэффициент
Таблица ParamType:
- ParamName — имя параметра робота
- ParamType — тип параметра робота (int / double / string)
Таблица TradingHistory
- ID — Внешний ключ ссылка на OptimisationParams(ID)
- isForvard — признак является ли история форвардного теста
- Symbol — символ
- DT_open — дата открытия
- Day_open — день открытия
- DT_close — дата закрытия
- Day_close — день закрытия
- Volume — Количество лотов
- isLong — признак того, лонг или шорт
- Price_in — цена входа
- Price_out — Цена выхода
- PL_oneLot — Прибыль если торговать одним лотом
- PL_forDeal — Прибыль если торговать как торговали
- OpenComment — комментарий на вход
- 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 — сравнивая при этом:
- Имя переменой робота
- Тип переменной
- Значение переменной
Если первые два сравнения не проходят — значит мы пытаемся сравнить два разных параметра (к примеру период быстрой с периодом медленной скользящих средних), и соответственно, не можем этого сделать — ведь нам требуется сравнивать лишь однотипные данные. Поэтому возвращаем значение 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
- OnTesterEvent
- OnTickEvent
Каждый из них вызывается в соответствующих коллбеках шаблона робота, где в них передаются требуемые параметры. Метод 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; }
Как видно из реализации данного метода, он присваивает полям класса требуемые значения и создает базу. Методы коллбеков должны быть реализованы пользователем лично (в случае, если требуется рассчитать свой коэффициент) либо используется перегрузка без коллбека — в таком случае пользовательский коэффициент равняется нулю. Пользовательский коэффициент — это собственный метод оценки прохода оптимизации робота. Для его реализации были созданы указатели на две функции с двумя типами возможных требуемых данных.
- Первый (customScoring_1) — получает историю торгов и флаг для какого именно прохода оптимизации требуется расчет (реально торгуемый лот или же торговля одним лотом — все данные для расчетов есть в переданном массиве).
- Второй тип коллбека (customScoring_2) — получает доступ к базе, с которой производится работа, однако доступ с правами только на чтение — во избежание непредвиденных правок со стороны пользователя.
- Присваивает значения баланса, таймфрейма и безрисковой ставки.
- Создает соединение с базой и занимает разделяемый ресурс (Mutex)
- Создает в базе таблицы, если те еще не были созданы.
Публичный метод OnTickEvent на каждом тике сохраняет дату минутной свечи. Дело в том, что во время тестирования стратегии нет возможности определить, является ли текущий проход форвардным или же нет, а в базе есть подобный параметр. Но нам известно, что тестер форвардные проходы прогоняет после исторических. Соответственно, перезаписывая на каждом тике переменную с датой, под конец оптимизационного процесса мы узнаем самую последнюю дату. В таблице OptimisationParams существует параметр «HistoryBorder» — он как раз и равняется данной сохраненной дате. Строки в данную таблицу заносятся только во время исторической оптимизации. Во время первого прохода с данными параметрами (он же проход на исторической оптимизации), дата, о которой идет речь, заносится в требуемое поле в базе. Далее, если мы видим в одном из следующих проходах, что запись с этими параметрами уже имеется в базе, тогда существуют два варианта:
- либо пользователь по каким-то причинам остановил историческую оптимизацию и вновь запустил ее,
- либо это форвардная оптимизация.
Для фильтрации одного от другого мы сверяем последнюю дату, сохраненную в текущем проходе, с датой из базы. Если текущая дата больше той, что в базе, то это форвардный проход, если же меньше или равна — исторический. Учитывая тот факт, что оптимизация может быть запущена дважды с одними и теми же коэффициентами, мы вносим в базу только новые данные, либо отменяем все сделанные изменения за текущий проход. Метод 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 интересующие нас таблицы и создающие массивы структур с данными из этих таблиц.
- Первый метод — getBuyAndHold, возвращает по ссылке историю BuyAndHold для форвардного и исторического периодов в зависимости от переданного флага. Если выгрузка произошла успешно, то метод возвращает true, иначе false. Выгрузка производится из таблицы Buy And Hold.
- Метод getTradingHistory также возвращает торговую историю для переданного ID и флага isForvard соответственно. Выгрузка производится из таблицы TradingHistory.
- Метод getRobotParams объединяет в себе выгрузку из двух таблиц: ParamsCoefitients — откуда берутся параметры робота, и OptimisationParams — где находятся рассчитанные оценочные коэффициенты.
Тем самым, написанные классы позволяют работать уже не напрямую с базой данных, а с классами, которые предоставляют требуемые данные, скрывая весь алгоритм работы с базой. Данные классы в свою очередь работают с написанной оберткой для базы данных — что так же упрощает работу с ней. Упомянутая обертка работает с базой через 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 ¶ms[],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. Данные массивы являются хранилищем данных оптимизации. После выгрузки таблицы с проходами оптимизации из базы она разбивается на две таблицы:
- main — попадают все выгруженные данные за минусом тех, что были отметены с учетом условной фильтрации
- result — попадают n лучших данных, выбранных изначально. После этого описываемый класс фильтрует именно эту таблицу и, соответственно, сокращает либо сбрасывает в исходное состояние количество записей в данной таблице.
Описываемые массивы хранят ID, параметры робота и некоторые другие данные из упомянутых таблиц соответственно названию массивов. По сути, данный класс выполняет две функции — удобного хранилища данных для операций с таблицами и фильтрации таблицы результатов отобранных проходов оптимизации. Класс сортировки и два класса-сравнители участвуют в процессе сортировки упомянутых массивов, а также в сортировке распределений, которые строятся по описанным таблицам.
Так как данный класс оперирует с коэффициентами роботов, а именно, с их представлением в виде класса CdataKeeper — то был создан приватный метод «selectCoefByName», выбирающий один требуемый коэффициент и возвращающий результат по ссылке из массива, переданных коэффициентов робота одного конкретного прохода оптимизации.
Метод Add добавляет строку, выгруженную из базы данных, в оба массива (при условии, что параметр addToResult ==true) или только в массив arr_main (если addToResult ==false). ID — уникальный параметр каждого прохода оптимизации, поэтому по нему построена вся работа с определением конкретного выбранного прохода. По данному параметру мы получаем из представленных массивов рассчитанный пользователем коэффициент. Дело в том, что программа сама по себе не знает формулы расчета пользовательской оценки, так как она (оценка) рассчитывается во время оптимизации робота — без участия данной программы. Именно из-за этого мы вынуждены сохранять пользовательскую оценку в данные массивы и, когда она запрашивается, при помощи метода GetCustomCoef по переданному ID мы получаем ее.
Наиболее важными из всех методов класса — являются следующие:
- Filtre — фильтрует таблицу результатов так, чтобы в нее попали значения выбранного коэффициента в переданном диапазоне (from/till).
- ResetFiltre — сбрасывает всю отфильтрованную информацию.
- Get_Distribution(Chart_item &out[],bool isMainTable)— строит распределение по реально наторгованной PL по выбранной таблице, указанной с помощью параметра isMainTable.
- Get_Distribution(Chart_item &out[],string Name,string value) — создает новый массив, где выбранный параметр (Name) равен переданному значению (value). Иначе говоря, производится проход в цикле по массиву arr_result. Во время каждой итерации цикла — из всех параметров робота — выбирается интересующий нас параметр по его имени (при помощи функции selectCoefByName) и проверяется условие — равно ли его значение требуемому (value). Если равно, то данное значение массива arr_result заносится во временный массив. Далее по временному массиву строится распределение и возвращается. Иначе говоря, таким образом мы отбираем все проходы оптимизации, где встречалось значение выбранного по имени параметра, равное переданному значению. Это нужно для того, чтобы оценить на сколько данный конкретный параметр влияет на робота в целом. Реализация описываемого класса в достаточной мере прокомментирована в коде и поэтому я не стану приводить реализацию данных методов внутри текста статьи.
Презентер
Презентер — выполняет стыкующую роль. Это некая прокладка между графическим слоем приложения и его логикой, описанными выше. В данном приложение презентер реализован при помощи абстракций — интерфейс 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 — проект тестового робота
- 2MA_Martin.mq5 — Файл с кодом шаблона робота. В нем подключается файл DBWriter.mqh, сохраняющий данные оптимизации в базу
- Robot.mq5 — Файл с реализацией логики робота
- Robot.mqh — Заголовочный файл, реализующийся в файле Robot.mq5
- Trade.mq5 — Файл с реализацией торговой логики робота
- Trade.mqh — Заголовочный файл, реализующийся в файле Trade.mq5
Experts/OptimisationSelector — проект описанного приложения
- OptimisationSelector.mq5 — Файл с шаблоном робота, где вызывается весь код проекта
- ParamsFiltre.mq5 — Файл с реализацией фильтра и построения распределений по таблицам с результатами
- ParamsFiltre.mqh — Заголовочный файл, реализующийся в файле ParamsFiltre.mq5
- Presenter.mq5 — Файл с реализацией презентера
- Presenter.mqh — Заголовочный файл, реализующийся в файле Presenter.mq5
- Presenter_interface.mqh — Файл интерфейса презентера
- Window_1.mq5 — Файл с реализацией построения графики
- Window_1.mqh — Заголовочный файл, реализующийся в файле Window_1.mq5
Include/CustomGeneric
- GenericSorter.mqh — Файл с реализацией сортировки данных
- ICustomComparer.mqh — Интерфейс ICustomSorter
Include/History manager
- DealHistoryGetter.mqh — Файл с реализацией выгрузки истории торгов из терминала и приведения ее в требуемый вид
- ReportCreator.mqh — Файл с реализацией класса, создающего историю торгов
Include/OptimisationSelector
- DataKeeper.mqh — Файл с реализацией класса хранителя коэффициентов робота, ассоциированного с именем коэффициента
- DBReader.mqh — Файл с реализацией класса, читающего из базы данных требуемые таблицы
- DBWriter.mqh — Файл с реализацией класса, пишущего в базу
Include/Sqlite3
- sqlite_amalgmation.mqh — Файл с импортом функций работы с базой
- SqliteManager.mqh — Файл с реализацией коннектора к базе и класса стейтмента
- SqliteReader.mqh — Файл с реализацией класса, читающего ответы из базы
- memcpy.mqh — Импорт функции memcpy
- Mutex.mqh — Импорт функций создания Mutex
- strcpy.mqh — Импорт функции strcpy
- strlen.mqh — Импорт функции strlen
Libraries
- Sqlite3_32.dll — Dll Sqlite для 32-битных терминалов
- Sqlite3_64.dll — Dll Sqlite для 64-битных терминалов
Тестовая база
- 2MA_Martin optimisation data - База данных Sqlite
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Откройте проект (крикните по файлу проекта) так что бы он отобразился во вкладке "Проект" - тогда все должно получиться.
Можно подробнее, какой файл кликать и где находиться вкладка Проект?
Для начала скопируйте все файлы в соответствующие директории метатрейдера
Затем что бы открыть проект кликните по файлу с расширением "mqproj" - вкладка с проектом откроется автоматически
Для начала скопируйте все файлы в соответствующие директории метатрейдера
Затем что бы открыть проект кликните по файлу с расширением "mqproj" - вкладка с проектом откроется автоматически
Я так и делал. Но система сообщает о невозможности открыть файл и предлагает поиск программы для открытия файла. Что посоветуете?
Я так и делал. Но система сообщает о невозможности открыть файл и предлагает поиск программы для открытия файла. Что посоветуете?
Нужно кликать из редактора кода Метатрейдера, а не из обозревателя папок.
Спасибо за помощь. Проект появился, но выдал ошибку.
Спасибо за помощь. Проект появился, но выдал ошибку.
Благодарю за отзыв. Нужно скачать библиотеку графики EasyAndFast и внести в нее соответствующие правки (в статье есть ссылка и описано какие правки куда вносить)