在交易中应用OLAP(第4部分):测试人员报告的定量和可视化分析

2020年六月25日15:20
斯坦尼斯拉夫·科罗特基
0
21 225

在本文中,我们将继续考虑OLAP(在线分析处理)及其在交易中的适用性。

在较早的文章中,我们讨论了构建用于累积和分析多维数组的类的通用技术,以及在图形界面中处理分析结果的可视化。从应用程序的角度来看,前两篇文章讨论了以各种方式获得的交易报告:从策略测试器,在线交易历史,HTML和CSV文件(包括MQL5交易信号)获得。在第三篇文章中对代码进行了少量的重构之后,将OLAP用于报价分析和制定交易策略。请阅读以前的文章,以便能够理解新材料(查看方括号,以了解您应特别注意的内容):

在本文中,我们将通过分析MetaTrader 5优化结果来扩展OLAP范围。

为了能够执行该项目,我们首先需要改进在第2部分中先前考虑的图形用户界面。在第3部分中执行的所有代码改进都直接与OLAP引擎有关。但是,没有执行任何相关的可视化升级。这就是我们将使用第二篇文章中的OLAPGUI贸易报告分析器作为当前文章中的测试任务来进行的工作。我们还将统一此图形部分,以便可以将其轻松应用于任何其他新的应用领域,尤其是计划中的优化结果分析器。

在应用程序图形上

OLAP的GUI中心是专门开发的CGraphicInPlot可视组件。第2条介绍的第一个实施方案有一些缺点。其中包括在轴上显示标签。必要时,我们设法在水平X轴上显示选择器单元格的名称(例如星期几的名称或货币的名称)。但是,在所有其他情况下,数字均按“原样”显示,这并不总是用户友好的。 Y轴需要另一项自定义,通常显示汇总值。根据设置,它可以显示选择器值,这是需要改进的地方。方法显示不佳的一个示例是请求符号的平均位置保持时间。

平均位置寿命(以符号为单位)(秒)

平均位置寿命(以符号为单位)(秒)

因为Y并未显示选择器(其中的值四舍五入到立方像元的大小),而是一个以秒为单位的合计持续时间值,所以很难理解这么大的数字。为了解决此问题,让我们尝试将秒除以当前时间范围栏的持续时间。在这种情况下,这些值将代表条的数量。为此,我们需要将某个标志传递给CGraphicInPlot类,并进一步传递给处理类CAxis的轴。更改操作模式的标志可能很多。因此,在文件Plot.mqh中为他们保留一个名为AxisCustomizer的特殊新类。

  class AxisCustomizer
  {
    public:
      const CGraphicInPlot *parent;
      const bool y; // true for Y, false for X
      const bool periodDivider;
      const bool hide;
      AxisCustomizer(const CGraphicInPlot *p, const bool axisY,
        const bool pd = false, const bool h = false):
        parent(p), y(axisY), periodDivider(pd), hide(h) {}
  };

可能会将各种标签显示功能添加到该类中。但是,目前,它仅存储轴类型(X或Y)的符号和一些逻辑选项,例如periodDivider和'hide'。第一个选项意味着值应除以PeriodSeconds()。第二种选择将在以后考虑。

此类的对象通过特殊方法进入CGraphicInPlot中:

  class CGraphicInPlot: public CGraphic
  {
    ...
      void InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL);
      void InitXAxis(const AxisCustomizer *custom = NULL);
      void InitYAxis(const AxisCustomizer *custom = NULL);
  };
  
  void CGraphicInPlot::InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL)
  {
    if(custom)
    {
      axe.Type(AXIS_TYPE_CUSTOM);
      axe.ValuesFunctionFormat(CustomDoubleToStringFunction);
      axe.ValuesFunctionFormatCBData((AxisCustomizer *)custom);
    }
    else
    {
      axe.Type(AXIS_TYPE_DOUBLE);
    }
  }
  
  void CGraphicInPlot::InitXAxis(const AxisCustomizer *custom = NULL)
  {
    InitAxes(m_x, custom);
  }
  
  void CGraphicInPlot::InitYAxis(const AxisCustomizer *custom = NULL)
  {
    InitAxes(m_y, custom);
  }

当未创建此类对象且未将其传递给图形类时,标准库将以通常的方式显示值,即数字AXIS_TYPE_DOUBLE。

在这里,我们使用标准库方法自定义轴上的标签:轴类型设置为等于AXIS_TYPE_CUSTOM,并且通过ValuesFunctionFormatCBData传递指向AxisCustomizer的指针。此外,它由CGraphic基类传递给CustomDoubleToStringFunction标签绘制函数(由上述代码中的ValuesFunctionFormat调用设置)。当然,我们需要CustomDoubleToStringFunction函数,该函数先前以简化形式实现,没有AxisCustomizer类对象(CGraphicInPlot图表充当设置对象)。

  string CustomDoubleToStringFunction(double value, void *ptr)
  {
    AxisCustomizer *custom = dynamic_cast<AxisCustomizer *>(ptr);
    if(custom == NULL) return NULL;
    
    // check options
    if(!custom.y && custom.hide) return NULL; // case of X axis and "no marks" mode
    
    // in simple cases return a string
    if(custom.y) return (string)(float)value;  
    
    const CGraphicInPlot *self = custom.parent; // obtain actual object with cache 
    if(self != NULL)
    {
      ... // retrieve selector mark for value
    }
  }

AxisCustomizer定制对象存储在CPlot类中,CPlot类是一个GUI控件(从CWndClient继承)和CGraphicInPlot的容器:

  class CPlot: public CWndClient
  {
    private:
      CGraphicInPlot *m_graphic;
      ENUM_CURVE_TYPE type;
      
      AxisCustomizer *m_customX;
      AxisCustomizer *m_customY;
      ...
    
    public:
      void InitXAxis(const AxisCustomizer *custom = NULL)
      {
        if(CheckPointer(m_graphic) != POINTER_INVALID)
        {
          if(CheckPointer(m_customX) != POINTER_INVALID) delete m_customX;
          m_customX = (AxisCustomizer *)custom;
          m_graphic.InitXAxis(custom);
        }
      }
      ...
  };

因此,m_customX和m_customY对象中的轴设置不仅可以在CustomDoubleToStringFunction中的值格式化阶段使用,而且在仅使用CurveAdd方法之一将数据数组传递给CPlot时,可以更早地使用它们。例如:

  CCurve *CPlot::CurveAdd(const PairArray *data, const string name = NULL)
  {
    if(CheckPointer(m_customY) != POINTER_INVALID) && m_customY.periodDivider)
    {
      for(int i = 0; i < ArraySize(data.array); i++)
      {
        data.array[i].value /= PeriodSeconds();
      }
    }
    
    return m_graphic.CurveAdd(data, type, name);
  }

该代码显示了periodDivider选项的用法,该选项将所有值除以PeriodSeconds()。在标准库接收数据并计算它们的网格大小之前执行此操作。此步骤很重要,因为在对网格进行计数之后,在CustomDoubleToStringFunction函数中自定义为时已晚。

对话框中的调用者代码必须在多维数据集构建时创建并初始化AxisCustomizer对象。例如:

  AGGREGATORS at = ...  // get aggregator type from GUI
  ENUM_FIELDS af = ...  // get aggregator field from GUI
  SORT_BY sb = ...      // get sorting mode from GUI
  
  int dimension = 0;    // calculate cube dimensions from GUI
  for(int i = 0; i < AXES_NUMBER; i++)
  {
    if(Selectors[i] != SELECTOR_NONE) dimension++;
  }
  
  bool hideMarksOnX = (dimension > 1 && SORT_VALUE(sb));
  
  AxisCustomizer *customX = NULL;
  AxisCustomizer *customY = NULL;
  
  customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, hideMarksOnX);
  if(af == FIELD_DURATION)
  {
    customY = new AxisCustomizer(m_plot.getGraphic(), true, true);
  }
  
  m_plot.InitXAxis(customX);
  m_plot.InitYAxis(customY);

这里,m_plot是存储CPlot控件的对话框变量。下面的OLAPDialog :: process方法的完整代码显示了实际上是如何执行的。这是上面的示例,其中的periodDivider模式自动启用:

平均位置生命周期(按符号)(当前时间框架,D1)

平均位置生命周期(按符号)(当前时间框架,D1)

AxisCustomizer中的另一个变量“ hide”提供了沿X轴完全隐藏标签的功能。选择由所述多维阵列中的值进行排序时,需要该模式。在这种情况下,每行中的标签都有其自己的顺序,因此沿X轴没有任何显示。多维多维数据集支持排序,可以在其他模式下使用排序,尤其是通过标签。

“隐藏”选项在CustomDoubleToStringFunction内部运行。该函数的标准行为表示存在选择器。选择器的标签被缓存在专门的CurveSubtitles类中的X轴上,并通过网格划分索引返回到图表。但是,设置的“隐藏”标志会在任何横坐标开始时终止此过程,并且该函数将返回NULL(不可显示的值)。

需要在图形中解决的第二个问题与直方图的呈现有关。当图表中显示几行(数据矢量)时,直方图条相互重叠,并且其中最大的条可以完全隐藏其他所有条。

CGraphic基本类具有虚拟HistogramPlot方法。必须覆盖它,以便在视觉上分隔列。最好在CCurve对象中有一个自定义字段,用于存储任意数据(该数据将根据需要由客户端代码解释)。不幸的是,这样的字段不存在。因此,我们将使用当前项目中未使用的标准属性之一。我选择了LinesSmoothStep。使用CCurve :: LinesSmoothStep setter方法,我们的调用者代码将序列号写入其中。通过在新的HistogramPlot实现中使用CCurve :: LinesSmoothStep getter方法,可以轻松获得此代码。这是在LinesSmoothStep中如何写行号的示例:

  CCurve *CGraphicInPlot::CurveAdd(const double &x[], const double &y[], ENUM_CURVE_TYPE type, const string name = NULL)
  {
    CCurve *c = CGraphic::CurveAdd(x, y, type, name);
    c.LinesSmoothStep((int)CGraphic::CurvesTotal());    // +
    ...
    return CacheIt(c);
  }

了解行的总数和当前行的总数后,可以在渲染时将其每个点稍微向左移动或向左移动。这是HistogramPlot的改编版本。更新后的行以带有“ *”的注释标记;新添加的行标有“ +”。

  void CGraphicInPlot::HistogramPlot(CCurve *curve) override
  {
      const int size = curve.Size();
      const double offset = curve.LinesSmoothStep() - 1;                   // +
      double x[], y[];
  
      int histogram_width = curve.HistogramWidth();
      if(histogram_width <= 0) return;
      
      curve.GetX(x);
      curve.GetY(y);
  
      if(ArraySize(x) == 0 || ArraySize(y) == 0) return;
      
      const int w = m_width / size / 2 / CGraphic::CurvesTotal();          // +
      const int t = CGraphic::CurvesTotal() / 2;                           // +
      const int half = ((CGraphic::CurvesTotal() + 1) % 2) * (w / 2);      // +
  
      int originalY = m_height - m_down;
      int yc0 = ScaleY(0.0);
  
      uint clr = curve.Color();
  
      for(int i = 0; i < size; i++)
      {
        if(!MathIsValidNumber(x[i]) || !MathIsValidNumber(y[i])) continue;
        int xc = ScaleX(x[i]);
        int yc = ScaleY(y[i]);
        int xc1 = xc - histogram_width / 2 + (int)(offset - t) * w + half; // *
        int xc2 = xc + histogram_width / 2 + (int)(offset - t) * w + half; // *
        int yc1 = yc;
        int yc2 = (originalY > yc0 && yc0 > 0) ? yc0 : originalY;
  
        if(yc1 > yc2) yc2++;
        else yc2--;
  
        m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr);
      }
  }

很快我们将检查其外观。

另一个烦人的时刻与线条显示的标准实现有关。如果数据具有非数字值,则CGraphic将换行。这对我们的任务不利,因为某些多维数据集单元可能不包含数据,并且聚合器将NaN写入此类单元格。某些多维数据集(例如,多个部分中的累积余额总计)显示不正确,因为每笔交易中的值仅在一个部分中更改。要查看折线的负面影响,请查看第2条中的图“每个符号的平衡曲线”。

为了解决此问题,另外重新定义了LinesPlot方法(请参见源代码,文件Plot.mqh)。操作结果如下所示,在与测试仪标准文件处理有关的部分中。

最后,最后一个图形问题与标准库中零轴的定义有关。在CGraphic :: CreateGrid方法中以以下简单方式搜索零(显示Y的情况;以相同的方式处理X轴):

  if(StringToDouble(m_yvalues[i]) == 0.0)
  ...

请注意,m_yvalues是字符串标签。显然,任何不包含数字的标签都将产生0。即使我们为图表设置了AXIS_TYPE_CUSTOM显示模式,也会发生这种情况。结果,在按值,星期几,交易类型和其他选择器的图表中,当在整个网格中的循环中检查所有值时,所有值均被视为零。但是,最终值取决于最后一个样本,该样本以粗体显示(尽管不为零)。此外,随着每个样本成为0(即使是暂时的)的候选项,它会跳过简单网格线的渲染,因此整个网格都消失了。

由于CreateGrid方法也是虚拟的,因此我们将使用更智能的0对其进行重新定义。此检查是作为辅助isZero函数实现的。

  bool CGraphicInPlot::isZero(const string &value)
  {
    if(value == NULL) return false;
    double y = StringToDouble(value);
    if(y != 0.0) return false;
    string temp = value;
    StringReplace(temp, "0", "");
    ushort c = StringGetCharacter(temp, 0);
    return c == 0 || c == '.';
  }
  
  void CGraphicInPlot::CreateGrid(void) override
  {
    int xc0 = -1.0;
    int yc0 = -1.0;
    for(int i = 1; i < m_ysize - 1; i++)
    {
      m_canvas.LineHorizontal(m_left + 1, m_width - m_right, m_yc[i], m_grid.clr_line);     // *
      if(isZero(m_yvalues[i])) yc0 = m_yc[i];                                               // *
      
      for(int j = 1; j < m_xsize - 1; j++)
      {
        if(i == 1)
        {
          m_canvas.LineVertical(m_xc[j], m_height - m_down - 1, m_up + 1, m_grid.clr_line); // *
          if(isZero(m_xvalues[j])) xc0 = m_xc[j];                                           // *
        }
        
        if(m_grid.has_circle)
        {
          m_canvas.FillCircle(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle);
          m_canvas.CircleWu(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle);
        }
      }
    }
    
    if(yc0 > 0) m_canvas.LineHorizontal(m_left + 1, m_width - m_right, yc0, m_grid.clr_axis_line);
    if(xc0 > 0) m_canvas.LineVertical(xc0, m_height - m_down - 1, m_up + 1, m_grid.clr_axis_line);
  }

OLAP GUI

我们已经在图形中实现了所需的修复程序。现在,让我们修改窗口界面,使其通用。在第二篇文章的非交易EA OLAPGUI中,使用对话框的操作在OLAPGUI.mqh头文件中实现。它存储了先前任务的许多应用功能,即交易报告的分析。由于我们将对任意数据使用相同的对话框,因此需要将文件分为两部分:一个将实现常规的界面行为,另一个将具有特定项目的设置。

将ex OLAPDialog类重命名为OLAPDialogBase。实际描述对话框控件的硬编码统计数组“选择器”,“设置”,“默认值”将是空的动态模板,然后由派生类填充。变量:

    OLAPWrapper *olapcore;    // <-- template <typename S,typename T> class OLAPEngine, since part 3
    OLAPDisplay *olapdisplay;

也将被继承,因为它们需要根据选择器和记录字段的类型进行标准化,这些选择器和记录字段在每个OLAP引擎的应用程序部分中定义。记住,旧的OLAPWrapper类已转换为OLAPEngine<S,T>第3条中的重构过程中的模板类

为主要逻辑保留了两种新的抽象方法:

  virtual void setup() = 0;
  virtual int process() = 0;

第一个,设置,配置界面:第二个,过程,启动分析。该设置是从OLAPDialogBase :: Create调用的

  bool OLAPDialogBase::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    setup(); // +
    ...
  }

用户通过单击按钮来启动分析启动,因此OLAPDialogBase :: OnClickButton方法进行了最多的改动:大多数代码已从其中删除,并删除了相应的功能(读取控件属性并基于它们启动OLAP引擎)已委派给该“过程”方法。

  void OLAPDialogBase::OnClickButton(void)
  {
    if(processing) return; // prevent re-entrancy
    
    if(browsing)           // 3D-cube browsing support
    {
      currentZ = (currentZ + 1) % maxZ;
      validateZ();
    }
  
    processing = true;
    const int n = process();
    if(n == 0 && processing)
    {
      finalize();
    }
  }

请注意,OLAPDialogBase类实现了整个操作接口逻辑,从创建控件开始一直到处理影响控件状态的事件开始。但是,它对控件的内容一无所知。

OLAPDisplay类从OLAPCommon.mqh实现了Display虚拟接口(在第3条中进行了讨论)。众所周知,Display接口是OLAP内核的回调,旨在提供分析结果(在MetaCube类对象的第一个参数中传递)。指向OLAPDisplay类中“父”窗口的指针可以组织链,以将多维数据集数据进一步传递给对话框(由于MQL5不提供多重继承,因此需要进行此转发)。

  class OLAPDisplay: public Display
  {
    private:
      OLAPDialogBase *parent;
  
    public:
      OLAPDisplay(OLAPDialogBase *ptr,): parent(ptr) {}
      virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override;
  };

在这里,我将提到与从派生适配器类获取自定义字段的实名有关的特定功能。以前,我们在第二部分中将自定义字段(例如MFE和MAE)添加到标准字段中。因此,它们是事先已知的,并已内置到代码中。但是,在使用优化报告时,我们将需要根据EA的输入参数对其进行分析,而这些参数(其名称)只能从分析数据中获取。

适配器使用新的assignCustomFields方法将自定义字段的名称传递给聚合器(metacube)。这始终是在“幕后”完成的,即在Analyst :: acquireData方法中自动完成。因此,当在OLAPDisplay :: display内部调用metaData.getDimensionTitle方法以获取长轴上的截面指定时,并且当字段n的序数超过内置字段枚举的容量时,我们知道我们正在处理扩展字段,可以从多维数据集请求描述。 OLAPDisplay :: display方法的常规结构未更改。您可以通过将下面的源代码与第2条中的代码进行比较来进行检查。

另外,必须事先在对话框中知道自定义字段的名称,以便填写界面元素。为此,OLAPDialogBase类包括用于设置自定义字段的新setCustomFields方法。

    int customFieldCount;
    string customFields[];
    
    virtual void setCustomFields(const DataAdapter &adapter)
    {
      string names[];
      if(adapter.getCustomFields(names) > 0)
      {
        customFieldCount = ArrayCopy(customFields, names);
      }
    }

当然,我们需要使用此方法将对话框和适配器绑定到测试EA中。之后,有意义的字段名称(而不是编号为“ custom 1”等)将在对话框控件中可见。这是一个临时解决方案。除其他外,这方面需要进一步的代码优化。但是它们在本文中被认为无关紧要。

修改后的OLAPGUI中的接口设置的应用程序部分已从OLAPGUI.mqh“移动”到OLAPGUI_Trades.mqh头文件。对话框类名称未更改:OLAPDialog。但是,它取决于模板参数,这些参数随后用于专门化OLAPEngine对象:

  template<typename S, typename F>
  class OLAPDialog: public OLAPDialogBase
  {
    private:
      OLAPEngine<S,F> *olapcore;
      OLAPDisplay *olapdisplay;
  
    public:
      OLAPDialog(OLAPEngine<S,F> &olapimpl);
      ~OLAPDialog(void);
      virtual int process() override;
      virtual void setup() override;
  };
  
  template<typename S, typename F>
  OLAPDialog::OLAPDialog(OLAPEngine<S,F> &olapimpl)
  {
    curveType = CURVE_POINTS;
    olapcore = &olapimpl;
    olapdisplay = new OLAPDisplay(&this);
  }
  
  template<typename S, typename F>
  OLAPDialog::~OLAPDialog(void)
  {
    delete olapdisplay;
  }

所有工作都在方法“设置”和“过程”中执行。 “设置”方法使用相同的值填充“设置”,“选择器”,“默认”数组,这在第二篇文章中已经为我们所知(界面外观未发生变化)。 “处理”方法将在指定部分启动分析,并且与之前的处理程序OnClickButton几乎完全相同。

  template<typename S, typename F>
  int OLAPDialog::process() override
  {
    SELECTORS Selectors[4];
    ENUM_FIELDS Fields[4];
    AGGREGATORS at = (AGGREGATORS)m_algo[0].Value();
    ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value();
    SORT_BY sb = (SORT_BY)m_algo[2].Value();
  
    ArrayInitialize(Selectors, SELECTOR_NONE);
    ArrayInitialize(Fields, FIELD_NONE);
  
    int matches[10] = // selectors in combo-boxes (specific record fields are bound internally)
    {
      SELECTOR_NONE, SELECTOR_SERIAL, SELECTOR_SYMBOL, SELECTOR_TYPE, SELECTOR_MAGIC,
      SELECTOR_WEEKDAY, SELECTOR_WEEKDAY, SELECTOR_DAYHOUR, SELECTOR_DAYHOUR, SELECTOR_DURATION
    };
    
    int subfields[] = // record fields listed in combo-boxes after selectors and accessible directly  
    {
      FIELD_LOT, FIELD_PROFIT_AMOUNT, FIELD_PROFIT_PERCENT, FIELD_PROFIT_POINT,
      FIELD_COMMISSION, FIELD_SWAP, FIELD_CUSTOM_1, FIELD_CUSTOM_2
    };
    
    for(int i = 0; i < AXES_NUMBER; i++) // up to 3 orthogonal axes are supported
    {
      if(!m_axis[i].IsVisible()) continue;
      int v = (int)m_axis[i].Value();
      if(v < 10) // selectors (every on e is specialized for a field already)
      {
        Selectors[i] = (SELECTORS)matches[v];
        if(v == 5 || v == 7) Fields[i] = FIELD_OPEN_DATETIME;
        else if(v == 6 || v == 8) Fields[i] = FIELD_CLOSE_DATETIME;
      }
      else // pure fields
      {
        Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS;
        Fields[i] = (TRADE_RECORD_FIELDS)subfields[v - 10];
      }
    }
  
    m_plot.CurvesRemoveAll();
    AxisCustomizer *customX = NULL;
    AxisCustomizer *customY = NULL;
  
    if(at == AGGREGATOR_IDENTITY || at == AGGREGATOR_COUNT) af = FIELD_NONE;
    
    if(at != AGGREGATOR_PROGRESSIVE)
    {
      customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, (dimension > 1 && SORT_VALUE(sb)));
    }
    
    if((af == FIELD_DURATION)
    || (at == AGGREGATOR_IDENTITY && Selectors[1] == SELECTOR_DURATION))
    {
      customY = new AxisCustomizer(m_plot.getGraphic(), true, true);
    }
    
    m_plot.InitXAxis(customX);
    m_plot.InitYAxis(customY);
    m_button_ok.Text("Processing...");
    return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb);
  }

在方法末尾创建了前面介绍的用于设置轴的AxisCustomizer对象。对于两个轴(X和Y),在使用工期字段(在聚合器或选择器中,如果聚合器类型为AGGREGATOR_IDENTITY)时,均启用PeriodSeconds()除法-在这种情况下,选择器不分配字段的内容命名单元之间,但内容直接传递到多维数据集)。当多维数据集尺寸大于1且选择排序时,将禁用X轴。

现在,让我们看一下OLAPGUI.mq5程序文件。与先前版本的其他不同之处在于,头文件的连接顺序已更改。之前,报表适配器包含在核心中(因为没有其他数据源)。现在,应将它们显式编写为HTMLcube.mqh和CSVcube.mqh。此外,在OnInit代码中,根据输入数据准备适当的适配器类型,然后通过调用_defaultEngine.setAdapter将适配器传递给引擎。该代码部分已经在第3条的OLAPRPRT.mq5程序中使用过,我们在其中测试了将其分解为通用部分和应用部分的正确方法。但是,OLAPRPRT在上一部分中没有图形界面。现在修复此缺陷。

为了演示标准字段和自定义字段的严格分离,将计算MFE和MAE字段的CustomTradeRecord类从OLAPTrades.mqh移到了OLAPTradesCustom.mqh(已附加代码)。因此,如果需要,我们可以简化基于交易的其他自定义字段的开发。只需在OLAPTradesCustom.mqh中更改算法,而OLAP内核不变。所有标准组件,例如交易记录字段,连接的选择器,TradeRecord基类,OLAPEngineTrade引擎和历史记录适配器都位于OLAPTrades.mqh中。 OLAPTradesCustom.mqh具有指向OLAPTrades.mqh的链接,该链接允许将以上所有内容包括在项目中。

  #include <OLAP/OLAPTradesCustom.mqh> // internally includes OLAPTrades.mqh 
  #include <OLAP/HTMLcube.mqh>
  #include <OLAP/CSVcube.mqh>
  #include <OLAP/GUI/OLAPGUI_trades.mqh>
  
  OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine);
  
  int OnInit()
  {
    if(ReportFile == "")
    {
      Print("Analyzing account history");
      _defaultEngine.setAdapter(&_defaultHistoryAdapter);
    }
    else
    {
      if(StringFind(ReportFile, ".htm") > 0 && _defaultHTMLReportAdapter.load(ReportFile))
      {
        _defaultEngine.setAdapter(&_defaultHTMLReportAdapter);
      }
      else
      if(StringFind(ReportFile, ".csv") > 0 && _defaultCSVReportAdapter.load(ReportFile))
      {
        _defaultEngine.setAdapter(&_defaultCSVReportAdapter);
      }
      else
      {
        Print("Unknown file format: ", ReportFile);
        return INIT_PARAMETERS_INCORRECT;
      }
    }
    
    ...
    
    if(!dialog.Create(0, "OLAPGUI" + (ReportFile != "" ? " : " + ReportFile : ""), 0,  0, 0, 750, 560)) return INIT_FAILED;
    
    if(!dialog.Run()) return INIT_FAILED;
    return INIT_SUCCEEDED;
  }

启动更新的OLAPGUI.mq5并构建几个数据部分,以确保动态启用内核对所应用适配器和记录类型的依赖性的新原理正常运行。我们还将检查更改的视觉效果。

您可以将以下结果与文章2中的屏幕截图进行比较。以下是每笔交易的“利润”和“持续时间”字段的依存关系。现在,沿X轴的持续时间以当前时间栏(此处为D1)表示,而不是以秒表示。

利润对持续时间的依赖性(在当前时间范围内,D1)

利润对持续时间的依赖性(在当前时间范围内,D1)

按符号和星期几细分的利润显示直方图条状分布和正确的网格。

通过符号和星期几获利

通过符号和星期几获利

下面的屏幕快照显示了按交易手数进行的利润分析。与文章2不同,批次值直接显示在X轴上,而不显示在日志中。

手数获利

手数获利

最后一个选项是“按交易品种和类型划分的交易数量”。在以前的版本中,使用了线条,因为直方图是重叠的。该问题不再相关。

按交易品种和类型划分的交易数量(直方图)

按交易品种和类型划分的交易数量(直方图)

我们已经考虑了与交易报告分析相关的所有要素。值得一提的另一件事是MQL程序员可以使用的新数据源,即内部测试器格式的tst文件。

连接标准测试仪文件(* .tst)

MetaTrader 5开发人员最近打开了测试仪保存的文件格式。特别是,单次通过的数据(我们只能在导出到HTML报表后才能进行分析)现在可以直接从tst文件中读取。

我们不会深入探讨有关文件内部结构的细节。相反,让我们使用一个就绪的库来读取tst文件- SingleTesterCache by fxsaber。通过在“黑匣子”基础上使用它,很容易获得一系列交易记录。交易由TradeDeal类在库中显示。要获取交易列表,请连接库,创建主类对象SINGLETESTERCACHE,然后使用“加载”方法加载所需的文件。

  #include <fxsaber / SingleTesterCache / SingleTesterCache.mqh>
  ...
  SINGLETESTERCACHE SingleTesterCache;
  if(SingleTesterCache.Load(file))
  {
    Print("测试仪 cache import: ", ArraySize(SingleTesterCache.Deals), " deals");
  }

SingleTesterCache.Deals数组包含所有交易。测试器中现有的每个交易的数据也可以在适当的字段中获得。

基于交易生成交易头寸的算法与导入HTML报告时完全相同。好的OOP样式需要在基类中实现通用代码部分,然后从中继承HTMLReportAdapter和TesterReportAdapter。

报告的共同祖先是BaseReportAdapter类(文件ReportCubeBase.mqh)。您可以在上下文中将此文件与旧的HTMLcube.mqh类进行比较,以自己查看几乎没有什么区别(新类名除外)。最引人注目的是“加载”方法的简约内容。现在它充当虚拟存根:

    virtual bool load(const string file)
    {
      reset();
      TradeRecord::reset();
      return false;
    }

子方法必须重写此方法。

“生成”方法中的代码也已更改。此方法将交易转换为头寸。现在,在此方法的开头调用了一个虚拟的空“存根” fillDealsArray。

    virtual bool fillDealsArray() = 0;
    
    int generate()
    {
      ...
      if(!fillDealsArray()) return 0;
      ...
    }

用于处理HTML报表的部分现有代码已移至HTMLReportAdapter类中的新虚拟方法。请注意:整个HTMLReportAdapter类如下所示。主要代码部分在基类中,因此这里仅需要定义2个虚方法。

  template<typename T>
  class HTMLReportAdapter: public BaseReportAdapter<T>
  {
    protected:
      IndexMap *data;
      
      virtual bool fillDealsArray() override
      {
        for(int i = 0; i < data.getSize(); ++i)
        {
          IndexMap *row = data[i];
          if(CheckPointer(row) == POINTER_INVALID || row.getSize() != COLUMNS_COUNT) return false; // something is broken
          string s = row[COLUMN_SYMBOL].get<string>();
          StringTrimLeft(s);
          if(StringLen(s) > 0) // there is a symbol -> this is a deal
          {
            array << new Deal(row);
          }
          else if(row[COLUMN_TYPE].get<string>() == "balance")
          {
            string t = row[COLUMN_PROFIT].get<string>();
            StringReplace(t, " ", "");
            balance += StringToDouble(t);
          }
        }
        return true;
      }
    
    public:
      ~HTMLReportAdapter()
      {
        if(CheckPointer(data) == POINTER_DYNAMIC) delete data;
      }
      
      virtual bool load(const string file) override
      {
        BaseReportAdapter<T>::load(file);
        if(CheckPointer(data) == POINTER_DYNAMIC) delete data;
        data = NULL;
        if(StringFind(file, ".htm") > 0)
        {
          data = HTMLConverter::convertReport2Map(file, true);
          if(data != NULL)
          {
            size = generate();
            Print(data.getSize(), " deals transferred to ", size, " trades");
          }
        }
        return data != NULL;
      }
  };

两种方法的代码都与以前的版本相似,没有任何更改。

现在让我们看一下新的TesterReportAdapter适配器的实现。首先,我必须添加从ReportCubeBase.mqh中定义的Deal类派生的TesterDeal类(Deal是以前位于HTMLcube.mqh中的旧类)。 测试仪Deal具有一个带有TradeDeal参数的构造函数,该参数是SingleTesterCache库中的一项交易。此外,TesterDeal定义了一些帮助程序方法,用于将类型和交易方向枚举转换为字符串。

  class 测试仪Deal: public Deal
  {
    public:
      测试仪Deal(const TradeDeal &td)
      {
        time = (datetime)td.time_create + TimeShift;
        price = td.price_open;
        string t = dealType(td.action);
        type = t == "buy" ? +1 : (t == "sell" ? -1 : 0);
        t = dealDir(td.entry);
        direction = 0;
        if(StringFind(t, "in") > -1) ++direction;
        if(StringFind(t, "out") > -1) --direction;
        volume = (double)td.volume;
        profit = td.profit;
        deal = (long)td.deal;
        order = (long)td.order;
        comment = td.comment[];
        symbol = td.symbol[];
        commission = td.commission;
        swap = td.storage;
      }
      
      static string dealType(const ENUM_DEAL_TYPE type)
      {
        return type == DEAL_TYPE_BUY ? "buy" : (type == DEAL_TYPE_SELL ? "sell" : "balance");
      }
      
      static string dealDir(const ENUM_DEAL_ENTRY entry)
      {
        string result = "";
        if(entry == DEAL_ENTRY_IN) result += "in";
        else if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_OUT_BY) result += "out";
        else if(entry == DEAL_ENTRY_INOUT) result += "in out";
        return result;
      }
  };

测试仪ReportAdapter类包含'load'和fillDealsArray方法,以及指向SINGLETESTERCACHE对象的指针,该对象是SingleTesterCache库的主要类。该对象根据请求加载tst文件。如果成功,则该方法将填充Deals数组,fillDealsArray数组将基于此数组进行操作。

  template<typename T>
  class 测试仪ReportAdapter: public BaseReportAdapter<T>
  {
    protected:
      SINGLETESTERCACHE *ptrSingleTesterCache;
      
      virtual bool fillDealsArray() override
      {
        for(int i = 0; i < ArraySize(ptrSingleTesterCache.Deals); i++)
        {
          if(TesterDeal::dealType(ptrSingleTesterCache.Deals[i].action) == "balance")
          {
            balance += ptrSingleTesterCache.Deals[i].profit;
          }
          else
          {
            array << new 测试仪Deal(ptrSingleTesterCache.Deals[i]);
          }
        }
        return true;
      }
      
    public:
      ~TesterReportAdapter()
      {
        if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache;
      }
      
      virtual bool load(const string file) override
      {
        if(StringFind(file, ".tst") > 0)
        {
          // default cleanup
          BaseReportAdapter<T>::load(file);
          
          // specific cleanup
          if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache;
          
          ptrSingleTesterCache = new SINGLETESTERCACHE();
          if(!ptrSingleTesterCache.Load(file))
          {
            delete ptrSingleTesterCache;
            ptrSingleTesterCache = NULL;
            return false;
          }
          size = generate();
          
          Print("测试仪 cache import: ", size, " trades from ", ArraySize(ptrSingleTesterCache.Deals), " deals");
        }
        return true;
      }
  };
  
  测试仪ReportAdapter<RECORD_CLASS> _defaultTSTReportAdapter;

最后,将创建RECORD_CLASS模板类型的默认适配器实例。我们的项目包括OLAPTradesCustom.mqh文件,该文件定义了CustomTradeRecord自定义记录类。在此文件中,该类由预处理器指令定义为RECORD_CLASS宏。因此,一旦新适配器连接到项目,并且用户在输入中指定了一个tst文件,适配器将开始生成CustomTradeRecord类对象,将自动为其生成MFE和MAE自定义字段。

让我们看看新适配器如何执行其任务。下面是一个来自tst文件中的符号的平衡曲线示例。

通过符号平衡曲线

通过符号平衡曲线

请注意,行是不间断的,这意味着我们的CGraphicInPlot :: LinesPlot实现正确运行。当使用“渐进式”聚合器(累积式)时,第一个选择器应始终为记录的序列号(或索引)。

测试器优化报告作为OLAP分析应用程序领域

除了单个测试文件,MetaQuotes现在还允许使用优化缓存访问opt文件。可以使用 测试缓存 库(再次由创建 fxsaber)。在此库的基础上,我们可以轻松地为优化结果的OLAP分析创建应用程序层。为此,我们需要:记录类,其中包含存储每个优化过程数据的字段,适配器和选择器(可选)。我们具有其他应用程序领域的组件的实现,从而可以将其用作指南(计划)。此外,我们将添加一个图形界面(几乎所有内容都准备就绪,我们只需要更改设置)。

将创建OLAPOpts.mqh文件,其目的类似于OLAPTrades.mqh。 测试缓存.mqh头文件将添加到其中。

  #include <fxsaber / 测试缓存 / 测试缓存.mqh>

Define an enumeration with all fields of the optimizer. I used fields from the ExpTrade总结 structure (it is located in fxsaber / 测试缓存 / ExpTradeSummary.mqh, the file is automatically connected to the library).

  enum OPT_CACHE_RECORD_FIELDS
  {
    FIELD_NONE,
    FIELD_INDEX,
    FIELD_PASS,
  
    FIELD_DEPOSIT,
    FIELD_WITHDRAWAL,
    FIELD_PROFIT,
    FIELD_GROSS_PROFIT,
    FIELD_GROSS_LOSS,
    FIELD_MAX_TRADE_PROFIT,
    FIELD_MAX_TRADE_LOSS,
    FIELD_LONGEST_SERIAL_PROFIT,
    FIELD_MAX_SERIAL_PROFIT,
    FIELD_LONGEST_SERIAL_LOSS,
    FIELD_MAX_SERIAL_LOSS,
    FIELD_MIN_BALANCE,
    FIELD_MAX_DRAWDOWN,
    FIELD_MAX_DRAWDOWN_PCT,
    FIELD_REL_DRAWDOWN,
    FIELD_REL_DRAWDOWN_PCT,
    FIELD_MIN_EQUITY,
    FIELD_MAX_DRAWDOWN_EQ,
    FIELD_MAX_DRAWDOWN_PCT_EQ,
    FIELD_REL_DRAWDOWN_EQ,
    FIELD_REL_DRAWDOWN_PCT_EQ,
    FIELD_EXPECTED_PAYOFF,
    FIELD_PROFIT_FACTOR,
    FIELD_RECOVERY_FACTOR,
    FIELD_SHARPE_RATIO,
    FIELD_MARGIN_LEVEL,
    FIELD_CUSTOM_FITNESS,
  
    FIELD_DEALS,
    FIELD_TRADES,
    FIELD_PROFIT_TRADES,
    FIELD_LOSS_TRADES,
    FIELD_LONG_TRADES,
    FIELD_SHORT_TRADES,
    FIELD_WIN_LONG_TRADES,
    FIELD_WIN_SHORT_TRADES,
    FIELD_LONGEST_WIN_CHAIN,
    FIELD_MAX_PROFIT_CHAIN,
    FIELD_LONGEST_LOSS_CHAIN,
    FIELD_MAX_LOSS_CHAIN,
    FIELD_AVERAGE_SERIAL_WIN_TRADES,
    FIELD_AVERAGE_SERIAL_LOSS_TRADES
  };
  
  #define OPT_CACHE_RECORD_FIELDS_LAST (FIELD_AVERAGE_SERIAL_LOSS_TRADES + 1)

该结构具有所有常规变量,例如利润,余额和提取权益,交易操作数,夏普比率等。我们添加的唯一字段是FIELD_INDEX:记录索引。结构中的字段具有不同的类型:long,double,int。所有这些都将添加到从Record派生的OptCacheRecord记录类中,并将存储在其double-type数组中。

将通过特殊的OptCacheRecordInternal结构访问该库:

  struct OptCacheRecordInternal
  {
    ExpTrade总结 summary;
    MqlParam params[][5]; // [][name, current, low, step, high]
  };

每个测试通过不仅仅通过性能变量来表征,而且还与一组特定的输入参数相关联。在此结构中,将输入参数作为MqlParam数组添加到ExpTradeSummary之后。有了这个结构,您可以轻松地编写OptCacheRecord类,该类中填充了优化程序格式的数据。

  class OptCacheRecord: public Record
  {
    protected:
      static int counter; // number of passes
      
      void fillByTesterPass(const OptCacheRecordInternal &internal)
      {
        const ExpTrade总结 record = internal.summary;
        set(FIELD_INDEX, counter++);
        set(FIELD_PASS, record.Pass);
        set(FIELD_DEPOSIT, record.initial_deposit);
        set(FIELD_WITHDRAWAL, record.withdrawal);
        set(FIELD_PROFIT, record.profit);
        set(FIELD_GROSS_PROFIT, record.grossprofit);
        set(FIELD_GROSS_LOSS, record.grossloss);
        set(FIELD_MAX_TRADE_PROFIT, record.maxprofit);
        set(FIELD_MAX_TRADE_LOSS, record.minprofit);
        set(FIELD_LONGEST_SERIAL_PROFIT, record.conprofitmax);
        set(FIELD_MAX_SERIAL_PROFIT, record.maxconprofit);
        set(FIELD_LONGEST_SERIAL_LOSS, record.conlossmax);
        set(FIELD_MAX_SERIAL_LOSS, record.maxconloss);
        set(FIELD_MIN_BALANCE, record.balance_min);
        set(FIELD_MAX_DRAWDOWN, record.maxdrawdown);
        set(FIELD_MAX_DRAWDOWN_PCT, record.drawdownpercent);
        set(FIELD_REL_DRAWDOWN, record.reldrawdown);
        set(FIELD_REL_DRAWDOWN_PCT, record.reldrawdownpercent);
        set(FIELD_MIN_EQUITY, record.equity_min);
        set(FIELD_MAX_DRAWDOWN_EQ, record.maxdrawdown_e);
        set(FIELD_MAX_DRAWDOWN_PCT_EQ, record.drawdownpercent_e);
        set(FIELD_REL_DRAWDOWN_EQ, record.reldrawdown_e);
        set(FIELD_REL_DRAWDOWN_PCT_EQ, record.reldrawdownpercnt_e);
        set(FIELD_EXPECTED_PAYOFF, record.expected_payoff);
        set(FIELD_PROFIT_FACTOR, record.profit_factor);
        set(FIELD_RECOVERY_FACTOR, record.recovery_factor);
        set(FIELD_SHARPE_RATIO, record.sharpe_ratio);
        set(FIELD_MARGIN_LEVEL, record.margin_level);
        set(FIELD_CUSTOM_FITNESS, record.custom_fitness);
      
        set(FIELD_DEALS, record.deals);
        set(FIELD_TRADES, record.trades);
        set(FIELD_PROFIT_TRADES, record.profittrades);
        set(FIELD_LOSS_TRADES, record.losstrades);
        set(FIELD_LONG_TRADES, record.longtrades);
        set(FIELD_SHORT_TRADES, record.shorttrades);
        set(FIELD_WIN_LONG_TRADES, record.winlongtrades);
        set(FIELD_WIN_SHORT_TRADES, record.winshorttrades);
        set(FIELD_LONGEST_WIN_CHAIN, record.conprofitmax_trades);
        set(FIELD_MAX_PROFIT_CHAIN, record.maxconprofit_trades);
        set(FIELD_LONGEST_LOSS_CHAIN, record.conlossmax_trades);
        set(FIELD_MAX_LOSS_CHAIN, record.maxconloss_trades);
        set(FIELD_AVERAGE_SERIAL_WIN_TRADES, record.avgconwinners);
        set(FIELD_AVERAGE_SERIAL_LOSS_TRADES, record.avgconloosers);
        
        const int n = ArrayRange(internal.params, 0);
        for(int i = 0; i < n; i++)
        {
          set(OPT_CACHE_RECORD_FIELDS_LAST + i, internal.params[i][PARAM_VALUE].double_value);
        }
      }
    
    public:
      OptCacheRecord(const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields)
      {
      }
      
      OptCacheRecord(const OptCacheRecordInternal &record, const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields)
      {
        fillByTesterPass(record);
      }
      
      static int getRecordCount()
      {
        return counter;
      }
  
      static void reset()
      {
        counter = 0;
      }
  };
  
  static int OptCacheRecord::counter = 0;

The fillByTesterPass method clearly shows the correspondence between the enumeration elements and ExpTrade总结 fields. The constructor accepts a populated OptCacheRecordInternal structure as a parameter.

测试缓存库和OLAP之间的中介是专门的数据适配器。适配器将生成OptCacheRecord记录。

  template<typename T>
  class OptCacheDataAdapter: public DataAdapter
  {
    private:
      int size;
      int cursor;
      int paramCount;
      string paramNames[];
      TESTERCACHE<ExpTradeSummary> Cache;

“大小”字段-记录总数,游标-高速缓存中当前记录的数量,paramCount-优化参数的数量。参数的名称存储在paramNames数组中。 TESTERCACHE的Cache变量<ExpTradeSummary>type是TesterCache库的工作对象。

最初,优化缓存被初始化,并在reset,load和customize方法中读取。

      void customize()
      {
        size = (int)Cache.Header.passes_passed;
        paramCount = (int)Cache.Header.opt_params_total;
        const int n = ArraySize(Cache.Inputs);
  
        ArrayResize(paramNames, n);
        int k = 0;
        
        for(int i = 0; i < n; i++)
        {
          if(Cache.Inputs[i].flag)
          {
            paramNames[k++] = Cache.Inputs[i].name[];
          }
        }
        if(k > 0)
        {
          ArrayResize(paramNames, k);
          Print("Optimized Parameters (", paramCount, " of ", n, "):");
          ArrayPrint(paramNames);
        }
      }
  
    public:
      OptCacheDataAdapter()
      {
        reset();
      }
      
      void load(const string optName)
      {
        if(Cache.Load(optName))
        {
          customize();
          reset();
        }
        else
        {
          cursor = -1;
        }
      }
      
      virtual void reset() override
      {
        cursor = 0;
        if(Cache.Header.version == 0) return;
        T::reset();
      }
      
      virtual int getFieldCount() const override
      {
        return OPT_CACHE_RECORD_FIELDS_LAST;
      }

opt文件将通过load方法加载,在该方法中将调用库的Cache.Load方法。如果成功,则从标题(在帮助器方法“ customize”中)中选择Expert Advisor参数。 “重置”方法重置当前记录号,下次getNext迭代OLAP内核的所有记录时,该记录号将递增。在这里,OptCacheRecordInternal结构中填充了来自优化缓存的数据。在此基础上,将创建模板参数类别(T)的新记录。

      virtual Record *getNext() override
      {
        if(cursor < size)
        {
          OptCacheRecordInternal internal;
          internal.summary = Cache[cursor];
          Cache.GetInputs(cursor, internal.params);
          cursor++;
          return new T(internal, paramCount);
        }
        return NULL;
      }
      ...
  };

模板参数是上述OptCacheRecord类。

  #ifndef RECORD_CLASS
  #define RECORD_CLASS OptCacheRecord
  #endif
  
  OptCacheDataAdapter<RECORD_CLASS> _defaultOptCacheAdapter;

它也被定义为宏,类似于OLAP内核其他部分中使用的RECORD_CLASS。以下是具有所有受支持的先前数据适配器和新适配器的类的图。

数据适配器类图

数据适配器类图

现在,我们需要确定哪些选择器类型可用于分析优化结果。建议将以下枚举作为第一个最小选项。

  enum OPT_CACHE_SELECTORS
  {
    SELECTOR_NONE,       // none
    SELECTOR_INDEX,      // ordinal number
    /* all the next require a field as parameter */
    SELECTOR_SCALAR,     // scalar(field)
    SELECTOR_QUANTS,     // quants(field)
    SELECTOR_FILTER      // filter(field)
  };

所有记录字段都属于以下两种类型之一:交易统计信息和EA参数。一个方便的解决方案是将参数组织到与测试值完全对应的单元格中。例如,如果参数包括使用10个值的MA期间,则OLAP多维数据集必须为此参数包含10个单元格。这是通过量化选择器(SELECTOR_QUANTS)完成的,该选择器的“篮子”大小为零。

对于可变字段,最好在特定步骤设置单元格。例如,您可以按100个单位的步长查看按利润分配的通行证。同样,这可以通过量化选择器来完成。尽管“篮子”的大小必须设置为所需的步骤。其他添加的选择器执行其他服务功能。例如,SELECTOR_INDEX用于计算累计总数。 SELECTOR_SCALAR允许接收一个数字作为整个选择的特征。

选择器类已准备就绪,位于OLAPCommon.mqh文件中。

让我们为这些选择器类型编写OLAPEngine类的模板特化中的createSelector方法:

  class OLAPEngineOptCache: public OLAPEngine<OPT_CACHE_SELECTORS,OPT_CACHE_RECORD_FIELDS>
  {
    protected:
      virtual Selector<OPT_CACHE_RECORD_FIELDS> *createSelector(const OPT_CACHE_SELECTORS selector, const OPT_CACHE_RECORD_FIELDS field) override
      {
        const int standard = adapter.getFieldCount();
        switch(selector)
        {
          case SELECTOR_INDEX:
            return new SerialNumberSelector<OPT_CACHE_RECORD_FIELDS,OptCacheRecord>(FIELD_INDEX);
          case SELECTOR_SCALAR:
            return new OptCacheSelector(field);
          case SELECTOR_QUANTS:
            return field != FIELD_NONE ? new QuantizationSelector<OPT_CACHE_RECORD_FIELDS>(field, (int)field < standard ? quantGranularity : 0) : NULL;
        }
        return NULL;
      }
  
    public:
      OLAPEngineOptCache(): OLAPEngine() {}
      OLAPEngineOptCache(DataAdapter *ptr): OLAPEngine(ptr) {}
  };
  
  OLAPEngineOptCache _defaultEngine;

创建量化选择器时,根据字段是“标准”(存储标准测试人员统计信息)还是自定义(“ EA交易”参数),将购物篮大小设置为quantGranularity变量或设置为零。 OLAPEngine基类中描述了quantGranularity字段。可以在引擎构造函数中设置它,也可以稍后使用setQuant方法设置它。

OptCacheSelector是BaseSelector的简单包装<OPT_CACHE_RECORD_FIELDS>.

图形界面,用于分析测试仪优化报告

优化结果的分析将使用与交易报告相同的界面来可视化。实际上,我们可以使用新名称OLAPGUI_Opts.mqh复制OLAPGUI_Trade.mqh文件,并对其进行较小的调整。调整涉及虚拟方法“设置”和“过程”。

  template<typename S, typename F>
  void OLAPDialog::setup() override
  {
    static const string _settings[ALGO_NUMBER][MAX_ALGO_CHOICES] =
    {
      // enum AGGREGATORS 1:1, default - sum
      {"sum", "average", "max", "min", "count", "profit factor", "progressive total", "identity", "variance"},
      // enum RECORD_FIELDS 1:1, default - profit amount
      {""},
      // enum SORT_BY, default - none
      {"none", "value ascending", "value descending", "label ascending", "label descending"},
      // enum ENUM_CURVE_TYPE partially, default - points
      {"points", "lines", "points/lines", "steps", "histogram"}
    };
    
    static const int _defaults[ALGO_NUMBER] = {0, FIELD_PROFIT, 0, 0};
  
    const int std = EnumSize<F,PackedEnum>(0);
    const int fields = std + customFieldCount;
  
    ArrayResize(settings, fields);
    ArrayResize(selectors, fields);
    selectors[0] = "(<selector>/field)"; // none
    selectors[1] = "<serial number>"; // the on ly selector, which can be chosen explicitly, it corresponds to the 'index' field
  
    for(int i = 0; i < ALGO_NUMBER; i++)
    {
      if(i == 1) // pure fields
      {
        for(int j = 0; j < fields; j++)
        {
          settings[j][i] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std];
        }
      }
      else
      {
        for(int j = 0; j < MAX_ALGO_CHOICES; j++)
        {
          settings[j][i] = _settings[i][j];
        }
      }
    }
  
    for(int j = 2; j < fields; j++) // 0-th is none
    {
      selectors[j] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std];
    }
    
    ArrayCopy(defaults, _defaults);
  }

字段和选择器之间几乎没有区别,因为任何字段都意味着同一字段的量化选择器。换句话说,量化选择器负责一切。在与报表和报价相关的早期项目中,我们对各个字段使用了特殊的选择器(例如获利能力选择器,星期几选择器,烛台类型选择器等)。

带有字段的下拉列表的所有元素的名称(也用作X,Y,Z轴的选择器)由OPT_CACHE_RECORD_FIELDS枚举元素的名称以及EA参数的customFields数组组成。之前,我们考虑了OLAPDialogBase基类中的setCustomFields方法,该方法使用适配器中的名称填充customFields数组。可以在OLAPGUI_Opts.mq5分析EA的代码中将这两种方法链接在一起(请参见下文)。

标准字段按枚举元素的顺序显示。标准字段后跟与正在优化的EA参数相关的自定义字段。自定义字段的顺序与opt文件中参数的顺序相对应。

控制状态的读取和分析过程的启动以“过程”方法执行。

  template<typename S, typename F>
  int OLAPDialog::process() override
  {
    SELECTORS Selectors[4];
    ENUM_FIELDS Fields[4];
    AGGREGATORS at = (AGGREGATORS)m_algo[0].Value();
    ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value();
    SORT_BY sb = (SORT_BY)m_algo[2].Value();
    
    if(at == AGGREGATOR_IDENTITY)
    {
      Print("Sorting is disabled for Identity");
      sb = SORT_BY_NONE;
    }
  
    ArrayInitialize(Selectors, SELECTOR_NONE);
    ArrayInitialize(Fields, FIELD_NONE);
  
    int matches[2] =
    {
      SELECTOR_NONE,
      SELECTOR_INDEX
    };
    
    for(int i = 0; i < AXES_NUMBER; i++)
    {
      if(!m_axis[i].IsVisible()) continue;
      int v = (int)m_axis[i].Value();
      if(v < 2) // selectors (which is specialized for a field already)
      {
        Selectors[i] = (SELECTORS)matches[v];
      }
      else // pure fields
      {
        Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS;
        Fields[i] = (ENUM_FIELDS)(v);
      }
    }
    
    m_plot.CurvesRemoveAll();
  
    if(at == AGGREGATOR_IDENTITY) af = FIELD_NONE;
  
    m_plot.InitXAxis(at != AGGREGATOR_PROGRESSIVE ? new AxisCustomizer(m_plot.getGraphic(), false) : NULL);
    m_plot.InitYAxis(at == AGGREGATOR_IDENTITY ? new AxisCustomizer(m_plot.getGraphic(), true) : NULL);
  
    m_button_ok.Text("Processing...");
    return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb);
  }

OLAP分析和优化报告的可视化

MetaTrader测试器提供了多种测试优化结果的方法,但是仅限于标准集。可用集可以通过使用创建的OLAP引擎进行扩展。例如,内置的2D可视化总是显示两个EA参数组合的最大利润值,但是通常有两个以上的参数。在表面的每个点上,我们看到其他参数的不同组合的结果,这些结果未显示在轴上。这可能导致对显示参数的特定值的获利能力过于乐观。可以从平均利润值及其值的范围获得更平衡的评估。除其他评估外,该评估还可以使用OLAP进行。

优化报告的OLAP分析将由新的非交易EA交易OLAPGUI_Opts.mq5执行。其结构与OLAPGUI.mq5完全相同。此外,它更简单,因为无需根据指定的文件类型连接适配器。这将始终是优化结果的选择文件。

在输入中指定文件名,并为统计参数指定量化步骤。

  input string OptFileName = "完整性选择";
  input uint QuantGranularity = 0;

请注意,希望每个场都有一个单独的量化步骤。但是,现在我们只设置了一次,而该值并未从GUI更改。此缺陷提供了进一步改进的潜在领域。请记住,步长值可能适合一个字段,而不适合另一个字段(它可能太大或太小)。因此,在从OLAP界面的下拉列表中选择字段之前,必要时调用EA属性对话框以更改量子。

在将头文件包含在所有类中之后,创建一个对话框实例并将其绑定到OLAP引擎。

  #include <OLAP/OLAPOpts.mqh>
  #include <OLAP/GUI/OLAPGUI_Opts.mqh>
  
  OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine);

在OnInit处理程序中,将新适配器连接到引擎并启动从文件加载数据。

  int OnInit()
  {
    _defaultEngine.setAdapter(&_defaultOptCacheAdapter);
    _defaultEngine.setShortTitles(true);
    _defaultEngine.setQuant(QuantGranularity);
    _defaultOptCacheAdapter.load(OptFileName);
    dialog.setCustomFields(_defaultOptCacheAdapter);
  
    if(!dialog.Create(0, "OLAPGUI" + (OptFileName != "" ? " : " + OptFileName : ""), 0,  0, 0, 750, 560)) return INIT_FAILED;
    if(!dialog.Run()) return INIT_FAILED;
    
    return INIT_SUCCEEDED;
  }

让我们尝试为QuantGranularity = 100的Integrity.opt文件构建一些分析部分。在优化过程中选择了以下三个参数:PricePeriod,Momentum,Sigma。

以下屏幕截图显示了按PricePeriod值细分的利润。

平均利润取决于EA参数值

平均利润取决于EA参数值

结果几乎没有分散的信息。

利润分散取决于EA参数值

利润分散取决于EA参数值

通过比较这两个直方图,我们可以估计色散不超过平均值的哪个参数值,这意味着盈亏平衡。更好的解决方案是在同一张图表上自动执行比较。但这超出了本文的范围。

另外,让我们查看该参数的获利能力(所有通行证的获利比)。

策略利润因子取决于EA参数值

策略利润因子取决于EA参数值

另一种棘手的评估方法是评估按利润水平细分的平均期间大小,以100为增量(该步骤在QuantGranularity输入参数中设置)。

各种范围内的获利参数的平均值(以100个单位为增量)

各种范围内的获利参数的平均值(以100个单位为增量)

下图显示了根据期间的利润分配(通过使用“身份”聚合器显示所有通行证)。

所有头寸的利润与参数值

所有头寸的利润与参数值

动量和西格玛的利润细分如下。

平均利润有两个参数

平均利润有两个参数

要以100为增量按级别查看利润的一般分布,请从沿X轴的统计信息中选择“利润”字段,并选择“计数”汇总器。

利润分配范围为100个单位

所有利润的分配范围为100个单位

通过使用“身份”聚合器,我们可以评估交易数量对利润的影响。通常,此聚合器可以对许多其他依赖项进行可视化评估。

利润与交易数量

利润与交易数量

结论

在本文中,我们扩展了MQL OLAP的范围。现在,它可以用于通过单次通过和优化来分析测试人员报告。更新的类结构可进一步扩展OLAP功能。所提出的实施方式不是理想的,并且可以进行很大的改进(特别是在3D可视化,交互式GUI中不同轴上的过滤设置和量化的实施方面)。但是,它只是一个最小的起点,有助于更轻松地熟悉OLAP。 OLAP分析使交易者可以处理大量原始数据并获得新知识,以进行进一步的决策。

附加的文件:

专家级

  • OLAPRPRT.mq5-用于分析帐户历史记录以及HTML和CSV报表(第3条中的更新文件,不带GUI)的EA交易
  • OLAPQTS.mq5-用于分析报价的EA交易(第3条中的更新文件,不带GUI)
  • OLAPGUI.mq5-专家顾问,用于分析帐户历史记录,HTML和CSV格式的报告以及TST标准测试程序文件(第2条中的更新文件,不带GUI)
  • OLAPGUI_Opts.mq5-用于分析来自标准OPT测试器文件(新的GUI)的优化结果的EA交易

包括

核心

  • OLAP / OLAPCommon.mqh —具有OLAP类的主头文件
  • OLAP / OLAPTrades.mqh-OLAP交易历史分析的标准类
  • OLAP / OLAPTradesCustom.mqh-用于OLAP交易历史分析的自定义类
  • OLAP / OLAPQuotes.mqh-OLAP报价分析类
  • OLAP / OLAPOpts.mqh-用于EA交易优化结果的OLAP分析的类
  • OLAP / ReportCubeBase.mqh-OLAP交易历史分析的基本类
  • OLAP / HTMLcube.mqh-用于以HTML格式对交易历史进行OLAP分析的类
  • OLAP / CSVcube.mqh-用于以CSV格式进行交易历史记录的OLAP分析的类
  • OLAP / TSTcube.mqh-用于以TST格式对交易历史进行OLAP分析的类
  • OLAP / PairArray.mqh —支持所有排序类型的对[value; name]对数组的一个类
  • OLAP / GroupReportInputs.mqh —一组用于分析交易报告的输入参数
  • MT4Bridge / MT4Orders.mqh — MT4orders库,用于处理MetaTrader 4和MetaTrader 5单一样式的订单
  • MT4Bridge / MT4Time.mqh —一个辅助头文件,以MetaTrader 4样式实现数据处理功能
  • 市场eer / IndexMap.mqh —一个辅助头文件,该文件通过具有基于键和索引的组合访问来实现一个数组
  • 市场eer / Converter.mqh-用于转换数据类型的辅助头文件
  • 市场eer / GroupSettings.mqh —一个辅助头文件,其中包含输入参数的组设置
  • 市场eer / WebDataExtractor.mqh — HTML解析器
  • 市场eer / empty_strings.h —空HTML标签的列表
  • 市场eer / HTMLcolumns.mqh-HTML报告中列索引的定义
  • 市场eer / RubbArray.mqh —具有“ rubber”数组的辅助头文件
  • 市场eer / CSVReader.mqh-CSV解析器
  • 市场eer / CSVcolumns.mqh-CSV报告中列索引的定义

图形界面

  • OLAP / GUI / OLAPGUI.mqh-交互式窗口界面的常规实现
  • OLAP / GUI / OLAPGUI_Trades.mqh-图形界面的专业化,用于分析交易报告
  • OLAP / GUI / OLAPGUI_Opts.mqh-图形界面的专业化,用于分析优化结果
  • Layouts / Box.mqh-控件容器
  • Layouts / ComboBoxResizable.mqh-下拉控件,可以动态调整大小
  • Layouts / MaximizableAppDialog.mqh-对话框窗口,可以动态调整大小
  • PairPlot / Plot.mqh —具有图表图形的控件,支持动态调整大小
  • Layouts / res / expand2.bmp-窗口最大化按钮
  • Layouts / res / size6.bmp-调整大小按钮
  • Layouts / res / size10.bmp-调整大小按钮

TypeToBytes

  • TypeToBytes.mqh

SingleTesterCache

  • fxsaber / SingleTesterCache / SingleTesterCache.mqh
  • fxsaber / SingleTesterCache / SingleTestCacheHeader.mqh
  • fxsaber / SingleTesterCache / String.mqh
  • fxsaber / SingleTesterCache / ExpTradeSummaryExt.mqh
  • fxsaber / SingleTesterCache / ExpTradeSummarySingle.mqh
  • fxsaber / SingleTesterCache / TradeDeal.mqh
  • fxsaber / SingleTesterCache / TradeOrder.mqh
  • fxsaber / SingleTesterCache / 测试仪PositionProfit.mqh
  • fxsaber / SingleTesterCache / 测试仪TradeState.mqh

测试缓存

  • fxsaber / 测试缓存 / 测试缓存.mqh
  • fxsaber / 测试缓存 / TestCacheHeader.mqh
  • fxsaber / 测试缓存 / String.mqh
  • fxsaber / 测试缓存 / ExpTradeSummary.mqh
  • fxsaber / 测试缓存 / TestCacheInput.mqh
  • fxsaber / 测试缓存 / TestInputRange.mqh
  • fxsaber / 测试缓存 / Mathematics.mqh
  • fxsaber / 测试缓存 / TestCacheRecord.mqh
  • fxsaber / 测试缓存 / TestCacheSymbolRecord.mqh

标准库补丁

  • 控件/Dialog.mqh
  • 控件/ComboBox.mqh

档案

  • 518562.history.csv
  • 诚信
  • 完整性选择

由MetaQuotes Software Corp.从俄语翻译而来。
来源文章: //www.tbxfkj.com/ru/articles/7656

附加的文件 |
MQLOLAP4.zip (365.7 KB)
DoEasy库中的时间序列(第37部分):时间序列集合-按符号和周期的时间序列数据库 DoEasy库中的时间序列(第37部分):时间序列集合-按符号和周期的时间序列数据库

本文讨论了程序中使用的所有符号的指定时间范围的时间序列集合的开发。我们将开发时间序列集合,设置集合的方法'的时间序列参数,并使用历史数据对已开发时间序列进行初始填充。

连续漫游优化(第5部分):Auto Optimizer项目概述和GUI的创建 连续漫游优化(第5部分):Auto Optimizer项目概述和GUI的创建

本文提供了对MetaTrader 5终端中的前向优化的进一步描述。在先前的文章中,我们考虑了生成和过滤优化报告的方法,并开始分析负责优化过程的应用程序的内部结构。自动优化器是作为C#应用程序实现的,并且具有自己的图形界面。第五篇文章专门介绍了此图形界面的创建。

交易信号的多币种监控(第3部分):介绍搜索算法 交易信号的多币种监控(第3部分):介绍搜索算法

在上一篇文章中,我们开发了应用程序的可视部分以及GUI元素的基本交互。这次,我们将添加内部逻辑和交易信号数据准备的算法,以及建立信号,搜索信号并在监视器中对其进行可视化的功能。

DoEasy库中的时间序列(第38部分):时间序列集合-实时更新和从程序访问数据 DoEasy库中的时间序列(第38部分):时间序列集合-实时更新和从程序访问数据

本文考虑了时间序列数据的实时更新并发送有关"New bar"从所有符号的所有时间序列到控制程序图表的事件,以便能够在自定义程序中处理这些事件。的"New tick"类用于确定是否需要更新非当前图表符号和时段的时间序列。