100 个最佳优化递次(第 1 部分)。 开发优化分析器

26 十一月 2018, 07:21
Andrey Azatskiy
0
775

概述

现代技术已深深扎根于金融交易领域,如今几乎无法想象若是没有它,我们能做什么。 然而,就在不久之前,交易还是手动进行的,并且有一套复杂的手语系统(现在很快被遗忘)来描述买入或卖出资产的份额。

个人电脑将网上在线交易带入我们的家庭,并迅速取代了传统的交易方式。 现在我们可以实时查看资产报价并制定相应的决策。 甚至于,市场行业中在线技术的出现导致手工交易者的行列以越来越快的速度减少。 现在,超过一半的交易是通过算法交易进行的,且值得一提的是 MetaTrader 5 是最方便的终端之一。

但尽管这个平台具有所有优点,我在此还是要略微说明应用程序的许多缺点。 本文所介绍的 EasyAndFastGUI 函数库是完全用 MQL5 编写的程序,该函数库旨在改进交易算法优化参数的选择。 它还为追溯交易分析和一般 EA 评估增加了新功能。



首先,EA 的优化需要花费相当长的时间。 当然,这是因为测试器以更高质量的方式生成逐笔报价(即使选择了 OHLC,每根蜡烛生成四次逐笔报价),以及其它附加功能来更好地评估 EA。 然而,在不那么强大的家用电脑上,优化可能需要几天或几周。 通常情况下,选择 EA 参数后,我们很快意识到它们是不正确的,除了优化递次统计数据和一些评估比率之外,什么都没得到。

最好能依照多个参数提供每个优化递次和过滤能力(包括条件过滤器)的完备统计数据。 将交易统计数据与买入并持有策略进行比较并相互强加所有统计数据也不错。 此外,有时需要将所有交易历史数据存储到文件中,以便后续处理每笔成交的结果。

有时,我们可能还希望看到算法能够承受何种滑点,以及算法在特定时间段内的行为,因为某些策略取决于行情类型。 基于横盘的策略可以作为一个例子。 它在趋势期间亏损并在横盘期间获利。 最好能够从普通的 PL 图形上分别查看一定间隔(按日期)的一组完整比率和其它附加内容(而不是简单地在价格图表上)。

我们还应关注前瞻性测试。 它们非常含有信息量,但它们的图形在策略测试器的标准报告中显示为前一阶段图形的延续。 新入门的交易者可能很容易得出结论,他们的机器人急速亏损了所有利润,然后开始恢复(或者更糟 -— 变为负数)。 在此处描述的程序中,所有数据都根据优化类型(前瞻或历史记录)进行审核。

还有一个重要的事情就是圣杯,许多 EA 开发者都狂热地研究它。 有些机器人每月产生 1000% 或更多的盈利。 它们似乎超越了市场(买入并持有策略),但在实际操作中,一切看起来都截然不同。 正如所描述程序所示,这些机器人可以真的做到 1000%,但它们并没有超过市场。

该程序的特点是分别分析机器人所进行的交易:全部手数(增加/减少,等等),以及交易手数限定为单手(可用于交易的最小手数)。 在构建买入并持有交易图形时,所描述的程序考虑由机器人执行手数管理(即,当手数增加时它买入更多资产,且当手数减少时降低买入资产的数量)。 如果我们比较这两个图形,结果表明我的测试机器人在其最佳优化递次之一中显示出不切实际的结果,无法超越市场。 因此,为了更客观地评估交易策略,我们应该看一下单手交易图形,其中机器人和买入并持有策略的 PL 都显示为交易量最小的交易(PL=盈利/损失 — 按时间获得的利润图形)。

现在,我们来更详细地了解程序的开发方式。


优化分析器结构

程序结构如下图表示:


由此产生的优化分析器不依赖于任何特定的机器人,或其一部分。 但是,由于在 MQL5 中构建图形界面的细节,采用 MQL5 EA 开发模板作为程序的基础。 由于程序相当大(数千行代码),为了更具体和一致性,它被分成许多模块(在上图中显示),这些模块又被划分成类。 机器人模板只是启动应用程序的起点。 下面将更详细地考查每个模块。 在此,我们将描述它们之间的关系。 要使用该应用程序,我们需要:

  • 交易算法
  • Dll Sqlite3
  • 上面提到的 图形界面库 必须要经过编辑(在下面的图形模块中描述)

机器人本身可以用您喜欢的任何方式开发(使用 OOP,机器人模板中的一个函数,从 Dll 导入...)。 最重要的是,它应该应用 MQL5 向导提供的机器人开发模板。 每个优化递次之后,类将所需数据存储到数据库,并从数据库模块连接至一个文件。 这部分是独立的,不依赖于应用程序本身,因为数据库是在策略测试器中启动机器人时形成的。

计算模块 是我上一篇文章 "自定义交易历史记录表述和报告图表的创建" 的持续改进。

数据库和计算模块在所分析的机器人和所描述的应用中均得以采用。 因此,它们被放入 Include 目录中。 这些模块执行大部分工作,并通过演播器类连接到图形界面。

演播器类 连接单独的程序模块。 每个模块在图形界面中都有自己的函数。 它处理按钮按下和其它事件,以及重定向到其它逻辑模块。 从它们中获得的数据被返回到演播器,在此它们被进行处理并绘制相应的图形,填充表格,并与其它图形部分进行交互。

程序的 图形部分 不执行任何概念逻辑。 代之,它仅构建一个具有所需界面的窗口,并在按下按钮事件期间调用相应的演播器函数。

程序本身 是作为 MQL5 项目 编写的,令您能够以更加结构化的方式开发它,并将所有必需的文件与代码放在一个地方。 该项目还包含另一个将在计算模块中描述的类。 该类是专为此程序编写的。 它使用我开发的方法对优化递次进行排序。 事实上,它服务于整个“优化选择”选项卡,依据某些标准减少了数据采样。

通用排序类 是该程序的独立补充。 它不依赖任何模块,但它仍然是该程序的重要部分。 因此,我们在本文的这一部分对它进行简要地考查。

顾名思义,该类处理数据排序。 其算法取自第三方网站 — 选择排序(俄语)。

//+------------------------------------------------------------------+
//| 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 类型接口,因为在 Compare 方法的传统 IСomparer 接口中,包含的数据不是通过引用传递的,其引用传递是 MQL5 语言中为方法传递结构的条件之一。

CGenericSorter::Method 类方法重载返回和接收数据排序类型(按升序或降序)。 此类在需要对数据进行排序的所有程序模块里都要用到。


图形

警告!


当开发图形界面时,在所采用的函数库(EasyAndFastGUI)中检测到一个错误 — ComboBox 图形元素在重新填充期间清理了一些不完整的变量。 根据函数库开发者的 建议(俄语),应进行以下改变来修复此问题:

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

代码在 CListView::Clear(const bool redraw=false) 方法中。

该方法位于 ListView.mqh 文件的第 600 个字符串。 文件路径:
Include\EasyAndFastGUI\Controls.

如果不添加这些编辑,打开 ComboBox 时有时会弹出“数组超出范围”错误,应用程序将异常关闭。


若要在 MQL5 中基于 EasyAndFastGUI 库创建窗口,需要一个类作为容器以便所有后续窗口填充。 该类应该从 CwindEvents 类派生。 应该在类中重新定义这些方法:

 //--- 初始/逆初
   void              OnDeinitEvent(const int reason){CWndEvents::Destroy();};
   //--- 图表事件处理程序
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);//

用于创建窗口的模块应如下所示:

class CWindowManager : public CWndEvents
  {
public:
                     CWindowManager(void){presenter = NULL;};
                    ~CWindowManager(void){};
   //===============================================================================   
   // 调用方法和事件 :
   //===============================================================================
   //--- 初始/逆初
   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 个选项卡容器:

//--- 选项卡
   CTabs             main_tab; // 主选项卡
   CTabs             tab_up_1; // 设置和结果表格的选项卡
   CTabs             tab_up_2; // 统计数据和参数选择的选项卡,以及共用图形
   CTabs             tab_down; // 统计信息及上传到文件的选项卡

它们在窗体上看起来如下(在屏幕截图上的红色符号):

  • main_tab 将所有选定的优化递次(“优化数据”)的表格从程序界面的其余部分划分开。 此表格包含所有满足设置选项卡上过滤条件的结果。 然后按照在 ComboBox 中选择的比率排序结果 — Sort by。 所获数据以分类的形式转换到所描表格。 程序界面其余部分的选项卡包含另外 3 个选项卡容器。
  • tab_up_1 包含程序初始设置和排序结果表格的分区。 除了上述条件过滤器之外,“Settings”选项卡还用于选择数据库并输入附加数据。 例如,您可以选择是否将已添加到表格的“优化数据”选项卡中的所有数据输入到数据选择结果表格中,或者仅选择一定数量的最佳参数(按所选比率的降序过滤)就足够了。
  • tab_up_2 包括 3 个选项卡。 它们中的每一个都包含执行三种不同类型任务的界面。 第一个选项卡包含有关所选优化递次的完整报告,并允许模拟滑点,以及考虑到特定时间段的交易历史。 第二个用作优化递次的过滤器,帮助定义策略对不同参数的灵敏度,并通过选择感兴趣参数的最适间隔来缩小优化结果的数量。 最后一个选项卡用作优化结果表格的图形表述,并显示所选优化参数的总数。
  • tab_down 具有五个选项卡,其中四个在所选参数进行优化期间显示 EA 的交易报告,而最后一个选项卡是将数据保存到文件。 第一个选项卡呈现一个评估比率的表格。 第二个选卡按交易日提供盈亏分布。 第三个选项卡呈现买入并持有策略(黑色图形)的盈亏图形,而第四个选项卡示意某些选定比率随时间的变化,以及一些通过分析 EA 交易结果获得的其它有趣而信息丰富的图形类型 。

创建选项卡的过程类似 — 唯一的区别是内容。 作为示例,我将提供创建主选项卡的方法:

//+------------------------------------------------------------------+
//| 主选卡                                                            |
//+------------------------------------------------------------------+
bool CWindowManager::CreateTab_main(const int x_gap,const int y_gap)
  {
//--- 将指针保存到主元素
   main_tab.MainPointer(m_window);

//--- 选卡宽度数组
   int tabs_width[TAB_MAIN_TOTAL];
   ::ArrayInitialize(tabs_width,45);
   tabs_width[0]=120;
   tabs_width[1]=120;
//---
   string tabs_names[TAB_UP_1_TOTAL]={"Analysis","Optimisation Data"};
//--- 属性
   main_tab.XSize(WINDOW_X_SIZE-23);
   main_tab.YSize(WINDOW_Y_SIZE);
   main_tab.TabsYSize(TABS_Y_SIZE);
   main_tab.IsCenterText(true);
   main_tab.PositionMode(TABS_LEFT);
   main_tab.AutoXResizeMode(true);
   main_tab.AutoYResizeMode(true);
   main_tab.AutoXResizeRightOffset(3);
   main_tab.AutoYResizeBottomOffset(3);
//---
   main_tab.SelectedTab((main_tab.SelectedTab()==WRONG_VALUE)? 0 : main_tab.SelectedTab());
//--- 添加指定属性的选项卡
   for(int i=0; i<TAB_MAIN_TOTAL; i++)
      main_tab.AddTab((tabs_names[i]!="")? tabs_names[i]: "Tab "+string(i+1),tabs_width[i]);
//--- 创建一个控件元素
   if(!main_tab.CreateTabs(x_gap,y_gap))
      return(false);
//--- 将对象添加到公共对象数组
   CWndContainer::AddToElementsArray(0,main_tab);
   return(true);
  }

除了可能变化的内容之外,主代码如下:

  1. 添加指向主元素的指针 — 选项卡容器应该知道分配给它的元素
  2. 控件元素创建字符串
  3. 将元素添加到常规控件列表中。

控件元素应顺应层次结构。 在应用程序中使用了 11 种控件元素类型。 它们都以类似的方式创建,因此添加控件元素的方法已编写好,用来依次创建它们。 我们仅考察它们当中之一的实现:

bool CWindowManager::CreateLable(const string text,
                                 const int x_gap,
                                 const int y_gap,
                                 CTabs &tab_link,
                                 CTextLabel &lable_link,
                                 int tabIndex,
                                 int lable_x_size)
  {
//--- 将指针保存到主元素
   lable_link.MainPointer(tab_link);
//--- 分配到选卡
   tab_link.AddToElementsArray(tabIndex,lable_link);

//--- 设置
   lable_link.XSize(lable_x_size);

//--- 创建
   if(!lable_link.CreateTextLabel(text,x_gap,y_gap))
      return false;

//--- 将对象添加到常规对象数组
   CWndContainer::AddToElementsArray(0,lable_link);
   return true;
  }

所传递的控件元素(CTextLabel)与选项卡一起,应记住该元素所分配至的容器。 反过来,选项卡容器会记住元素所在的选项卡。 之后,元素按所需设置和初始数据填充。 最终,该对象被添加到常规对象数组中。

与标签类似,在类容器内定义的其它元素字段也要添加。 我将某些元素分开,并将其中一些元素放入类的“protected”区域。 这些是不需要演播器访问的元素。 其它一些元素被放置在 'public'。 这些是定义某些条件或单选按钮的元素,需要由演播器检查其状态。 换言之,所有不希望访问的元素和方法都将其放在类的“protected”或“private”部分,还有演播器的引用。 添加演播器引用是以公有方法形式进行的,其中首先检查已添加的演播器是否存在,如果尚未添加其引用,则保存演播器。 这样做是为了避免在程序执行期间演播器动态替换。

窗口本身是在 CreateGUI 方法中创建的:

bool CWindowManager::CreateGUI(void)
  {
//--- 创建窗口
   if(!CreateWindow("Optimisation Selection"))
      return(false);

//--- 创建选项卡
   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;

//--- 创建控件 
   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;

// 显示窗口
   CWndEvents::CompletedGUI();

   return(true);
  }

从其实现可以看出,它不直接创建任何控件元素本身,而仅调用其它方法来创建这些元素。 在此方法中应作为最后一个包含的主代码是 CWndEvents::CompletedGUI();

此代码完成图形创建并将其绘制在用户屏幕上。 将每个控制元素(可以是分离线,标签或按钮)的创建实现为具有类似内容的方法,并应用上述方法来创建图形控件元素。 方法头可以在类的“private”部分找到:

//===============================================================================   
// 创建控件:
//===============================================================================
//--- 所有标签
   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);
//+------------------------------------------------------------------+
//| 智能系统初始化函数                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!_window.CreateGUI())
     {
      Print(__FUNCTION__," > Failed to create the graphical interface!");
      return(INIT_FAILED);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 智能系统逆初始化函数                                                 |
//+------------------------------------------------------------------+
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 )。 当从两个源操纵数据库时(顾名思义,如果优化分析器在优化期间打开所使用的数据库),程序获得的数据应始终完整。 这就是需要共享资源的原因。 事实证明,如果其中一方(优化过程或分析器)激活数据库,则第二方要等待对方完成其工作。 Sqlite 数据库允许从多个线程读取它。 限于本文的主题内容,我们不会详细研究从 MQL5 操纵 sqlite3 数据库的包装器。 代之,我们仅描述其实现和应用方法的一些要点。 如前所述,使用数据库的包装器位于 Sqlite3 文件夹中。 它当中有三个文件。 我们按照书面顺序审视它们。

  • 我们要做的第一件事是从 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);// 合成请求值 (字符串类型 (C++ 中的 char*))
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

开发人员提供的数据库应放在 Libraries 文件夹中,并根据其 dll 数据库包装器的位数命名为 Sqlite3_32.dll 和 Sqlite3_64.dll。 您可以从文章附件中获取 Dll 数据,从 Sqlite Amalgmation 中自行编译它们,或者从 Sqlite 开发人员的 网站 中获取它们。 它们的存在是该程序的先决条件。 您还需要允许 EA 导入 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)
     {    // 转换为 utf-8 编码的字符数量,并复制到 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”方法,可将字符串转换为所需的数据格式。

  • 下一个任务是创建一个包装器,以便于查询(语句)。 应创建和销毁对数据库的请求。 请求由 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;
  };

参数方法重载填充请求参数。 'set' 方法将传递的语句保存到 '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 数据库的函数类,现在是时候说明如何运用所述程序中的类操纵数据库了。

创建的数据库结构如下:

买入并持有表:

  1. Time — X 轴 (时间段标签)
  2. PL_total — 如果我们成比例地增加机器人手数时的盈亏
  3. PL_oneLot — 如果交易一直按照单一手数的盈亏
  4. DD_total — 如果 EA 以相同方式交易一手时的回撤
  5. DD_oneLot — 如果交易一手时的回撤
  6. isForvard — 前瞻图形属性

OptimisationParams 表:

  1. ID — 数据库中的唯一自动填充条目索引
  2. HistoryBorder — 历史优化完成日期
  3. TF — 时间帧
  4. Param_1...Param_n — 参数
  5. InitalBalance — 初始余额

ParamsCoefitients 表:

  1. ID — 外键引用 OptimisationParams(ID)
  2. isForvard — 前瞻优化属性
  3. isOneLot — 该比率所依据的图表属性
  4. DD — 回撤
  5. averagePL — PL 图形的平均盈利/亏损
  6. averageDD — 平均回撤
  7. averageProfit — 平均盈利
  8. profitFactor — 盈利因子
  9. recoveryFactor — 恢复因子
  10. sharpRatio — 锋锐比率
  11. altman_Z_Score — Altman Z 分数
  12. VaR_absolute_90 — VaR 90
  13. VaR_absolute_95 — VaR 95
  14. VaR_absolute_99 — VaR 99
  15. VaR_growth_90 — VaR 90
  16. VaR_growth_95 — VaR 95
  17. VaR_growth_99 — VaR 99
  18. winCoef — 胜率
  19. customCoef — 自定义比率

ParamType 表:

  1. ParamName — 机器人参数名称
  2. ParamType — 机器人参数类型 (int/double/string)

TradingHistory 表

  1. ID — 外键引用 OptimisationParams(ID)
  2. isForvard — 前瞻测试标志
  3. Symbol — 品种
  4. DT_open — 开盘日期
  5. Day_open — 开盘日
  6. DT_close — 收盘日期
  7. Day_close — 收盘日
  8. Volume — 手数
  9. isLong — 多头/空头属性
  10. Price_in — 入场价格
  11. Price_out — 离场价格
  12. PL_oneLot — 单手交易时的盈利
  13. PL_forDeal — 像我们之前那样交易时的盈利
  14. OpenComment — 入场指令
  15. CloseComment — 离场指令

根据提供的数据库结构,我们可以看到一些表使用外键来引用我们存储 EA 参数的 OptimisationParams 表。 输入参数的每列都有其名称(例如,快/慢 — 快/慢移动平均值)。 此外,每列应具有特定的数据格式。 许多已创建的 Sqlite 数据库,但未定义表列数据格式。 在这种情况下,所有数据都按行存储。 但是,我们需要知道确切的数据格式,因为我们应该通过某个属性来整理比率,这意味着将从数据库获取的数据转换为其原始格式。

为此,我们应在数据输入数据库之前知道它们的格式。 有若干可能的选项:创建模板方法,并将转换器按之变换,或创建一个类,实际上,它是若干种数据类型的通用存储(任何数据类型都可以转换)并与 EA 名称变量相结合。 我选择了第二个选项并创建了 CDataKeeper 类。 所描述的类可以存储 3 种数据类型 [int,double,string],而可以用作 EA 输入格式的所有其它数据类型能够以这种或那种方式转换为它们。

//+------------------------------------------------------------------+
//| EA 参数的输入数据类型                                                |
//+------------------------------------------------------------------+
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(CDataKeepe &ther)方法允许比较两个 CDataKeeper 类型的对象:

  1. EA 变量名
  2. 变量类型
  3. 变量数值

如果前两次比较没有通过,那么我们尝试比较两个不同的参数(例如,快速移动均线的周期和慢速移动均线的周期),只是我们不能这样做,因为我们只需要 比较相同类型的数据。 因此,我们返回 CoefCompareResult 类型的 Coef_Different 值。 在其它情况下,进行比较并返回所需的结果。 比较方法本身实现如下:

//+------------------------------------------------------------------+
//| 将当前参数与传递的参数进行比较                                         |
//+------------------------------------------------------------------+
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);// 填写买入并持有历史
   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;
  }

正如我们在方法实现中所看到的,它为类字段分配了所需的数值并创建了数据库。 回调方法应该由用户亲自实现(如果应计算自定义比率)或者使用没有回调的重载 — 在这种情况下,自定义比率等于零。 用户比率是评估 EA 优化递次的自定义方法。 为了实现它,创建了指向两个函数的指针,分别对应两种可能的所需数据。

  • 第一个(customScoring_1)接收计算所需的交易历史和定义优化递次的标志(实际交易手数或单手交易 — 所有用于计算的数据都存储在所传递的数组中)。
  • 第二个回调类型(customScoring_2)可以访问需要执行语句的数据库,但只能用只读权限,以便避免用户意外编辑。
CreateDB 方法是类的主要方法之一。 它为操作做了充分的准备:

  • 分配余额,时间帧和无风险比率值。
  • 建立与数据库的连接并占用共享资源(Mutex)
  • 如果尚未创建,则创建表数据库。

OnTickEvent 公有逐笔报价在每个逐笔报价保存分钟蜡烛日期。 在测试策略时,无法定义当前递次是否为前瞻,而数据库具有类似的参数。 但是我们知道测试器在历史测试之后会运行前瞻递次。 因此,在每个逐笔报价泳日期覆盖变量时,我们找出优化过程结束时的最后日期。 OptimisationParams 表具有 HistoryBorder 参数。 它等于保存的日期。 仅在历史优化期间将行添加到此表中。 在使用此参数的第一个递次时(与历史优化递次相同),日期将添加到数据库中的所需字段。 如果在下一个传递期间,我们看到具有这些参数的条目已存在于数据库中,则有两个选项:

  1. 用户出于某些原因停止了历史优化,然后重新启动它,
  2. 或者这是一个前瞻优化。

为了相互过滤,我们将当前递次中存储的最后日期与数据库中的日期进行比较。 如果当前日期大于数据库中的日期,那么这是一个前瞻递次,如果它小于或等于,则您的成交含有历史日期。 考虑到优化应以相同的比率启动两次,我们只将新数据输入数据库或取消当前递次期间所做的所有更改。 OnTesterEvent() 方法将数据保存在数据库中。 它按以下方式实现:

//+------------------------------------------------------------------+
//| 保存所有数据至数据库并返回                                            |
//| 一个自定义比率                                                      |
//+------------------------------------------------------------------+
double CDBWriter::OnTesterEvent()
  {

   DealDetales history[];

   CDealHistoryGetter historyGetter;
   historyGetter.getDealsDetales(history,0,TimeCurrent()); // 获取交易历史

   CMutexSync sync; // 同步对象
   if(!sync.Create(getMutexName(DBPath))) { Print(Symbol()+" MutexSync create ERROR!"); return 0; }
   CMutexLock lock(sync,(DWORD)INFINITE); // 将片段锁定在括号内

   bool IsForvard=isForvard(); // 找出当前测试器迭代是否为前瞻测试
   CReportCreator rc;
   string Symb[];
   rc.Get_Symb(history,Symb); // 获取品种列表
   rc.Create(history,Symb,balance,r); // 创建报告(自动创建“购买并持有”报告)

   double ans=0;
   dbManager.BeginTransaction(); // 事务开始

   CStatement stmt(dbManager.Create_statement("INSERT OR IGNORE INTO ParamsType VALUES(@ParamName,@ParamType);")); // 请求保存 EA 参数类型列表
   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); // 保存 EA 参数以及评估比率并获取ID
   if(ID>0)// 如果 ID > 0, 参数保存成功 
     {
      if(setTraidingHistory(IsForvard,history,ID)) // 保存交易历史记录并检查是否已保存
        {
         setBuyAndHold(IsForvard,&rc); // 保存买入并持有历史记录(仅保存一次 - 在第一次保存期间)
         dbManager.CommitTransaction(); // 确认一笔事务结束
        }
      else dbManager.RollbackTransaction(); // 否则,取消事务
     }
   else dbManager.RollbackTransaction(); // 否则,取消事务

   return ans;
  }

该方法所做的第一件事就是用我在上一篇文章中描述的类来形成交易历史。 然后它获取共享资源(Mutex)并保存数据。 为实现此目的,首先定义当前优化递次是否为前瞻(根据上述方法),然后获取品种列表(所有交易过的品种)。

有鉴于此,如果实例为测试差价交易 EA,则会加载所处理的两个品种的交易历史记录。 之后,生成报告(使用下面评测的类)并写入数据库。 为正确记录创建了一个事务。 如果在填写任何表时发生错误或获得了不正确的数据,则取消该事务。 首先,保存比率,然后,如果一切顺利,我们保存交易历史记录,然后是买入并持有历史记录。 后者在第一次数据输入期间仅保存一次。 如果出现数据保存错误,将在 Common/Files 文件夹中生成日志文件。

创建数据库后,则应读取它。 数据库读取类已运用在所描述的程序当中。 它更简单,看起来如下:

//+------------------------------------------------------------------+
//| 从数据库中读取数据的类                                               |
//+------------------------------------------------------------------+
class CDBReader
  {
public:
   void              Connect(string DBPath);// 连接数据库的方法

   bool              getBuyAndHold(BuyAndHoldChart_item &data[],bool isForvard);// 计算买入并持有历史的方法
   bool              getTraidingHistory(DealDetales &data[],long ID,bool isForvard);// 计算 EA 交易历史的方法
   bool              getRobotParams(CoefData_item &data[],bool isForvard);// 计算 EA 参数和比率的方法

private:
   CSqliteManager    dbManager; // 数据库管理器
   string            DBPath; // 数据库的路径

   bool              getParamTypes(ParamType_item &data[]);// 计算输入类型及其名称。
  };

它实现了 3 个公有方法,读取我们感兴趣的 4 个表,并使用这些表中的数据创建结构数组。

  • 第一种方法(getBuyAndHold)根据传递的标志,返回前瞻和历史时段测试的 BuyAndHold 历史记录引用。 如果上传成功,则该方法返回' true',否则返回 'false'。 上传是从“买入并持有”表中执行的。
  • getTradingHistory 方法还相应地返回所传递 ID 和 isForvard 标志的交易历史。 上传是从 TradingHistory 表执行的。
  • getRobotParams 方法结合了两个表的上传:ParamsCoefitients — 从中获取机器人参数,以及 OptimisationParams,其内是计算的估值比率。

因此,编写的类令您不必再直接访问数据库,而是利用隐藏了整个算法的类来操纵数据库并提供所需数据。 反过来,这些类使用编写好的包装器访问数据库,这也简化了工作。 提及包装器通过数据库开发人员提供的 Dll 与数据库一起工作。 数据库本身满足所有必需条件,实际上它是一个文件,便于在此程序和其它分析形应用程序中进行传输和处理。 这种方法的另一个优点是,单个算法的长期运行令您能够从每次优化中收集数据库,从而累积历史记录并跟踪参数变化形态。


计算

该模块由两个类组成。 第一个用于生成交易报告,且它是前一篇文章中所述生成交易报告类的改进版本。

第二个是过滤类。 它对递次范围内的优化样本进行排序,并能够创建一个图形,显示每个单独优化比率值的盈亏交易频率。 该类的另一个目的在于优化结束时为实际交易的盈亏创建正态分布图(即整个优化区间的盈亏)。 换言之,如果有 1000 个优化递次,我们有 1000 个优化结果(盈亏则为优化结束)。 我们所感兴趣的,正是基于它们的分布。

该分布展示出所获数值在哪个方向上有不对称偏移。 如果尾部较大且分布中心位于利润区域,则机器人产生的优化递次大部分可盈利,当然就很好,否则它就会产生大部分无利可图的递次。 如果不对称明显转移到亏损区,这意味着所选参数会主要导致亏损而非盈利。

我们从生成交易报告的类开始查看这个模块。 所描述的类位于“历史记录管理器”文件夹的 Include 目录中,并具有以下头部:

//+------------------------------------------------------------------+
//| 用于生成交易历史统计的类                                              |
//+------------------------------------------------------------------+
class CReportCreator
  {
public:

   //=============================================================================================================================================
   // 计算/重计算:
   //=============================================================================================================================================

   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[]); // 获取盈亏图
   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); // 按日获取盈亏图
   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:
   //=============================================================================================================================================
   // 私有数据类型:
   //=============================================================================================================================================
   // 盈亏图类型的结构
   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;

   //=============================================================================================================================================
   // 计算:
   //=============================================================================================================================================
   // 计算盈亏
   void              CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type);
   // 计算盈亏直方图
   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);

   //=============================================================================================================================================
   // 复制:
   //=============================================================================================================================================
   void              CopyPL(const PLChart_item &src[],PLChart_item &out[]); // 复制盈亏图形
   void              CopyCoefChart(const CoefChart_item &src[],CoefChart_item &out[]); // 复制比率图形

  };

与以前的版本不同,这个类计算的数据量是其两倍,并构建了更多类型的图形。 重载的 'Create' 方法也计算报告。

实际上,报告只生成一次 — 在调用 Create 方法时。 稍后,在从 Get 单词开始的方法中仅获得先前计算的数据。 在 Create 方法中具有最多参数,其主循环仅迭代输入参数一次。 此方法迭代参数并立即计算一系列数据,根据这些数据在同一次迭代中构建所有必需的数据。

这允许在单个递次中构建我们感兴趣的所有内容,而这个类的先前版本则用来依据初始数据再次迭代图形。 因此,所有比率的计算持续数毫秒,而获得所需数据则需要较少时间。 在类的“private”区域中,有一系列结构仅在该类中仅是为了更方便的用作数据容器。 使用上述 Generic 排序方法执行排序交易历史。

我们来描述在调用每个 getter 时获得的数据:

方法 参数 图表类型
GetChart chart_type = _PL, calc_type = _Total PL graph — 根据实际交易历史
GetChart chart_type = _PL, calc_type = _OneLot PL graph — 当单手交易时
GetChart chart_type = _PL, calc_type = _Indicative PL graph — 指示
GetChart chart_type = _BH, calc_type = _Total BH graph — 如果机器人管理手数
GetChart chart_type = _BH, calc_type = _OneLot BH graph — 如果单手交易
GetChart chart_type = _BH, calc_type = _Indicative BH graph — 指示
GetChart chart_type = _Hist_PL, calc_type = _Total PL histogram — 根据实际交易历史
GetChart chart_type = _Hist_PL, calc_type = _OneLot PL histogram — 如果单手交易
GetChart chart_type = _Hist_PL, calc_type = _Indicative PL histogram — 指示
GetChart chart_type = _Hist_BH, calc_type = _Total BH histogram — 如果机器人管理手数
GetChart chart_type = _Hist_BH, calc_type = _OneLot BH histogram — 如果单手交易
GetChart chart_type = _Hist_BH, calc_type = _Indicative BH histogram — 指示
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 — 单手交易时,按时间的 Altman 得分
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 — 与我们之前所作交易一样时的时分 Altman 分数
GetDailyPL calcBy=CALC_FOR_CLOSE, calcType=AVERAGE_DATA 收盘时间的日分平均盈亏
GetDailyPL calcBy=CALC_FOR_CLOSE, calcType=ABSOLUTE_DATA 收盘时间的日分总盈利
GetDailyPL calcBy=CALC_FOR_OPEN, calcType=AVERAGE_DATA 开盘时间的日分平均盈亏
GetDailyPL calcBy=CALC_FOR_OPEN, calcType=ABSOLUTE_DATA 开盘时间的日分总盈亏
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
盈亏曲线简要汇总
Get_Symb
交易历史中存在的品种数组

PL graph — 根据实际交易历史:

该图形等于通常的盈亏图。 在所有测试递次后,我们可以在终端中看到这一点。

PL graph — 单手交易时:

该图形类似于先前描述过的交易量不同的图形。 它的计算如同我们一直在单手交易。 入场和离场价格按 EA 市价入场和离场总数的平均价格计算。 交易盈利也是根据 EA 交易的利润计算的,但它转换为获得的利润,就像通过该比例单手交易一样。

PL graph — 指示:

正常盈亏图。 如果 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);
     }
// 排序购买并持有历史记录和相应的循环
   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);
     }

// 平均每日盈亏(平均类型)
   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; // Total PL
               // 最大盈利 / 回撤
   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;
     }
// 累计利润/ 回撤
   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*/)// yet another check is required for saving the date
     {
      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);}; // 卸载参数总数(根据优化数据表)
   void              Clear(){ArrayFree(arr_main);ArrayFree(arr_result);}; // 清除所有数组
   void              Add(LotDependency_item &customCoef,CDataKeeper &params[],long ID,double total_PL,bool addToResult); // 向数组添加新值
   double            GetCustomCoef(long ID,bool isOneLot);// 按 ID 获取自定义比率
   void              GetParamNames(CArrayString &out);// 获取 EA 参数名称
   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[]; // 优化数据表等效衡
   CoefStruct        arr_result[];// 结果表等效
  };

分析类的结构并更详细讲述一些方法。 我们可以看到,该类有两个全局数组:arr_main 和 arr_result。 数组存储优化数据。 从数据库中卸载含有优化递次的表后,它被拆分为两个表:

  • 主表 — 除了在条件排序期间丢弃的数据之外,所有获取的卸载数据
  • 结构 — 获得的 n 个最初选择的最佳数据。 之后,所描述的类对该特定表进行排序,并相应地减少或重置其条目的数量。

所描述的数组存储 EA 的 ID 和参数,以及根据数组名称存储上表中的一些其它数据。 从本质上讲,这个类执行两个功能 — 一是便洁地操纵表来存储数据,以及针对所选优化递次的结果表进行排序。 排序类和两个比较器类涉及所提数组的排序过程,以及根据所描述的表构建分布排序。

由于此类按 EA 比率运行,即它们以 CdataKeeper 类的形式表示,因此创建了私有方法 selectCoefByName。 它选择一个必要的比率,并通过特定优化递次传递给 EA 的比率数组引用返回结果。

考虑到 addToResult == true,Add 方法会在数据库里加上一行(两个数组两者),或是如果 addToResult == false,则只添加到 arr_main 数组。 ID 是每个优化递次的唯一参数,因此所有操作都基于它选定的特定递次的定义。 我们从所提供的数组中获取此参数的由用户计算的比率。 程序本身不知道计算自定义估值的方程,因为估值是在 EA 优化期间计算的,没有程序参与。 这就是我们为何需要在这些数组里保存自定义估值的原因。 在请求时,我们以所传递 ID 使用 GetCustomCoef 方法获取它。

最重要的类方法如下:

  • Filtre — 对结果表进行排序,令其包含递次范围(从/到)中的所选比率值。
  • ResetFiltre — 重置整个排序信息。
  • Get_Distribution(Chart_item &out[],bool isMainTable) — 使用 isMainTable 参数选择指定的表,依据实际交易的盈亏构建分布。
  • Get_Distribution(Chart_item &out[],string Name,string value) — 创建一个新数组,其中所选参数(Name)等于传递的值(value)。 换言之,在循环中顺次传递 arr_result 数组值执行。 在循环的每次迭代期间,我们通过其名称(使用 selectCoefByName 函数)从所有 EA 参数中选择感兴趣的参数。 此外,检查其值是否等于所需的值(value)。 如果是,则将 arr_result 数组值添加到临时数组。 然后,创建并返回临时数组的分布。 换句话说,此即我们如何选择所有优化递次,其中按名称所选参数的值经检测等于传递的值。 这是必要的,以便评估该特定参数对整体 EA 的影响程度。 所描述的类的实现已在代码中得到了充分的讨论,因此我无需在此提供这些方法的实现。


演播器

演播器用作连接器。 这是应用程序的图形层与其上述逻辑之间的一种联系。 在此应用程序中,演播器使用抽象实现 — IPresenter 接口。 该接口包含所需回调方法的名称; 反过来,它们是在演播器类中实现的,它应该继承所需的接口。 此部分是为了最终确定应用程序而创建的。 如果您需要重写演播器模块,可以轻松完成,而不会影响图形模块或应用程序逻辑。 所描述的接口如下所示:

//+------------------------------------------------------------------+
//| 演播器接口                                                         |
//+------------------------------------------------------------------+
interface IPresenter
  {
   void Btn_Update_Click(); // 下载数据并构建整个窗体
   void Btn_Load_Click(); // 创建报告
   void OptimisationData(bool isMainTable);// 在表中选择优化行
   void Update_PLByDays(); // 按天上传损益
   void DaySelect();// 从盈亏表中按周内天数选择一天
   void PL_pressed(PLSelected_type type);// 按选定历史记录构建盈亏图 
   void PL_pressed_2(bool isRealPL);// 构建 "其它图表" 图形
   void SaveToFile_Click();// 保存数据文件(到沙箱)
   void SaveParam_passed(SaveParam_type type);// 选择要写入文件的数据
   void OptimisationParam_selected(); // 选择优化参数并填写“优化选择”选项卡
   void CompareTables(bool isChecked);// 通过结果表构建分布(用于与共用(主要)表的关联)
   void show_FriquencyChart(bool isChecked);// 显示盈亏频率图
   void FriquencyChart_click();// 在比率表中选择一行并构建分布
   void Filtre_click();// 按选定条件排序
   void Reset_click();// 重置过滤器
   void PL_pressed_3(bool isRealPL);// 按结果表中的所有数据构建盈亏图
   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);// 按选定历史记录构建盈亏图
   void              PL_pressed_2(bool isRealPL);// 构建 "其它图表" 图形
   void              SaveToFile_Click();// 保存数据文件(到沙箱)
   void              SaveParam_passed(SaveParam_type type);// 选择要写入文件的数据
   void              OptimisationParam_selected();// 选择优化参数并填写“优化选择”选项卡
   void              CompareTables(bool isChecked);// 通过结果表构建分布(用于与共用(主要)表的关联)
   void              show_FriquencyChart(bool isChecked);// 显示盈亏频率图
   void              FriquencyChart_click();// 在比率表中选择一行并构建分布
   void              Filtre_click();// 按选定条件排序
   void              PL_pressed_3(bool isRealPL);// 按结果表中的所有数据构建盈亏图
   void              PL_pressed_4(bool isRealPL);// 构建统计表
   void              DaySelect();// 从盈亏表中按周内天数选择一天
   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[]); // 将数据插入结果表和“Main”表(表内为优化递次比率)
   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);// 将图形添加到其它图表
   bool              curveAdd(CGraphic *chart_ptr,const CoefChart_item &data[],double borderPoint);// 将图形添加到其它图表
   bool              curveAdd(CGraphic *chart_ptr,const Distribution_item &data);// 将图形添加到其它图表
   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();// 通过“Main”表构建分布(优化数据)
   void              updateDT(CDropCalendar *dt_ptr,datetime DT);// 更新下拉日历

   CParamsFiltre     coefKeeper;// 排序优化递次(按分布)
   CArrayString      headder; // 比率表标题

   bool              _isUpbateClick; // 按下按钮的更新,以及从数据库加载数据的标志
   long              _selectedID; // 所有选定盈亏图系列的 ID(如果亏损则为红色,如果盈利则为绿色)
   long              _ID,_ID_Arr[];// 上载数据后为结果表所选择的 ID 数组
   bool              _IsForvard_inTables,_IsForvard_inReport; // 优化递次表中优化数据类型的标志
   datetime          _DT_from,_DT_till;
   double            _Gap; // 已保存的添加间隙类型(点差/或滑点模拟...)
  };

每个回调都经过良好的讨论,所以没有必要在这里详述。 应用程序中唯一需要说明的是实现所有表单行为的部分。 它包含构建图形、填写组合框,从数据库中调用上传和处理数据的方法,以及贯通各种类的其它操作。


结束语

我们已开发了处理表的应用程序,可处理来自测试器的所有可能优化参数,以及在 EA 中添加了保存所有优化递次至数据库。 除了可得到我们所选择感兴趣参数的详细交易报告之外,该程序还允许我们自整个优化历史中按选定时间、以及给定时间段来查看所有比率。 还可以通过增加 Gap 参数来模拟滑点,并查看它如何影响图形和比率的行为。 另一个附加能力是能够以特定的比率值间隔对优化结果进行排序。

获得 100 个最佳优化递次的最简单方法是将 CDBWriter 类连接到机器人,就像样本 EA(在附加文件中)一样,设置条件过滤器(例如,利润因子>= 1 立即排除所有亏损,等进行组合)并单击“更新”,保留“Show n params”参数等于 100。 在这种情况下,结果表中会显示 100 个最佳优化递次(根据您的过滤器)。 结果应用程序的每个选项,以及选择比率更精细的方法将在下一篇文章中详论。


文章所附文件如下:

Experts/2MA_Martin — 测试 EA 项目

  • 2MA_Martin.mq5 — EA 模板代码。 DBWriter.mqh 中包含将优化数据保存到数据库的文件。
  • Robot.mq5 — EA 逻辑
  • Robot.mqh — 在 Robot.mq5 文件中实现的头文件
  • Trade.mq5 — EA 交易逻辑
  • Trade.mqh — 在 Trade.mq5 文件中实现的头文件

Experts/OptimisationSelector — 描述的应用项目

  • OptimisationSelector.mq5 — 调用整个项目代码的 EA 模板
  • 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 — 用比率名称关联存储 EA 比率的类
  • DBReader.mqh — 从数据库中读取所需表的类
  • DBWriter.mqh — 写入数据库的类

Include/Sqlite3

  • sqlite_amalgmation.mqh — 导入用于处理数据库的函数
  • SqliteManager.mqh — 连接数据库和语句的类
  • SqliteReader.mqh — 从数据库中读取响应的类

Include/WinApi

  • memcpy.mqh — 导入 memcpy 函数
  • Mutex.mqh — 导入 Mutex 创建函数
  • strcpy.mqh — 导入 strcpy 函数
  • strlen.mqh — 导入 strlen 函数

Libraries

  • Sqlite3_32.dll — 用于 32-位终端的 Sqlite Dll
  • Sqlite3_64.dll — 用于 64-位终端的 Sqlite Dll

测试数据库

  • 2MA_Martin 优化数据 - Sqlite 数据库

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/5214

附加的文件 |
MQL5.zip (6783.27 KB)
根据指定的分布法则为自定义品种的时间序列建模 根据指定的分布法则为自定义品种的时间序列建模

本文概述终端创建和运用自定义品种的能力,提供了使用自定义品种模拟交易历史、趋势和各种图表形态的选项。

自动优化 MetaTrader 5 专用 EA 自动优化 MetaTrader 5 专用 EA

本文描述 MetaTrader 5 下自我优化机制的实现。

EA 遥控方法 EA 遥控方法

交易机器人的主要优势在于能够在远程 VPS 服务器上每天 24 小时不间断工作。 但有时候有必要干预它们的工作,而此刻可能无法直接访问服务器。 是否可以遥控管理 EA? 本文提出了一种通过外部命令控制 EA 的选项。

走势延续模型 - 搜索图表和执行统计 走势延续模型 - 搜索图表和执行统计

本文提供了一种走势延续模型的程序化定义。 主要思路是定义两个波浪 — 主浪和修正浪。 对于极值点,我应用分形以及“潜在”分形 - 尚未形成分形的极值点。