在交易中应用 OLAP(第 3 部分):为开发交易策略而分析报价

26 四月 2020, 14:49
Stanislav Korotky
0
1 033

在本文中,我们将继续研讨在交易中应用 OLAP(在线分析处理)技术。 前两篇文章阐述创建代码类的通用技术,这些类能够汇集并分析多维数据,以及可在图形界面中直观分析结果 。 两篇文章处理来自不同途径接收到的交易报告:来自策略测试器,来自在线交易历史,以及来自 HTML 和 CSV 文件(包括 MQL5 交易信号)。 然而,OLAP 还可以应用于其他领域。 特别是,OLAP 是一种分析报价和制定交易策略的便捷技术。

概述

这是上一篇文章中所实现内容的摘要(如果您还不曾阅读过它们,强烈建议您从前两篇文章开始)。 核心位于 OLAPcube.mqh 文件中,该文件包含:

  • 选择器和聚合器的所有基类
  • 带有源数据的操作记录类(抽象基类 “Record”,和一些特殊的 “TradeRecord” 子类,其内包含成交数据)
  • 读取各种(抽象)数据源,并从中形成操作记录数组的基本适配器
  • 帐户交易历史记录的特定适配器 HistoryDataAdapter
  • 显示结果的基类,及其运用数据记录(Display,LogDisplay)的最简单实现
  • 以 Analyst 类形式的独立控制面板,该面板会把适配器、聚合器和显示链接在一起

在 HTMLcube.mqh 文件中实现了与 HTML 报告相关的特定字段,其中定义了从 HTML 报告里提取交易的 HTMLTradeRecord 类,以及生成报告的 HTMLReportAdapter 适配器。

类似地,在 CSVcube.mqh 文件中分别实现了从 CSV 报告提取交易的 CSVTradeRecord 类,以及适配器 CSVReportAdapter。

最后,为了简化 MQL5 程序与 OLAP 的集成,编写了 OLAPcore.mqh 文件。 其内的 OLAPWrapper 包装器类,包含了 OLAP 示范项目中要用到的整体功能。

由于新的 OLAP 处理任务定位在一个新区域,故此我们需要将现有代码进行重构,并选择其中不仅对于交易历史记录,而且对于报价或任何数据源都是通用的那些部分。

重构

基于 OLAPcube.mqh 创建了一个新文件:OLAPCommon.mqh,不过仅拥有基本类型。 首先,剔除的部分包括描述数据字段的用途含义的枚举,例如 SELECTORS 和 TRADE_RECORD_FIELDS。 还有,与交易相关的选择器类和记录类也被排除在外。 当然,所有这些部分并没有真的删除,而是转移到了一个新文件 OLAPTrades.mqh 之中,该文件是为处理交易历史记录和报告而创建的。

以前的包装器类 OLAPWrapper 已变为模板,并重命名为 OLAPEngine,该类已被移至 OLAPCommon.mqh 文件。 工作字段的枚举将用作参数化参数(例如,TRADE_RECORD_FIELDS 将用于取自第一篇和第二篇文章中的项目适配,详情参阅如下)。

OLAPTrades.mqh 文件包含以下类型(已在第一篇和第二篇文章中论述):

  • enumerations TRADE_SELECTORS (former SELECTORS), TRADE_RECORD_FIELDS
  • selectors TradeSelector, TypeSelector, SymbolSelector, MagicSelector, ProfitableSelector, DaysRangeSelector
  • record classes TradeRecord, CustomTradeRecord, HistoryTradeRecord
  • adapter HistoryDataAdapter
  • OLAPEngineTrade engine — OLAPEngine<TRADE_RECORD_FIELDS> specialization

注意 DaysRangeSelector 选择器的存在,它已成为分析交易历史的标准选择器。 在早期版本中,它位于 OLAPcore.mqh 文件当中,作为自定义选择器的示例。

在文件末尾创建了一个默认的适配器实例:

  HistoryDataAdapter<RECORD_CLASS> _defaultHistoryAdapter;

连同 OLAP 引擎实例:

  OLAPEngineTrade _defaultEngine;

从客户端源代码可以方便地使用这些对象。 会在其他应用领域(头文件)里定义表述已就绪对象的方法,尤其是在已计划的报价分析器中。

文件 HTMLcube.mqh 和 CSVcube.mqh 几乎保持不变。 所有之前存在的交易历史和报告分析功能均予以保留。 下面附有一个新的用于演示的测试智能交易系统 OLAPRPRT.mq5;它与第一篇文章中的 OLAPDEMO.mq5 类似。

以 OLAPTrades.mqh 为例,可以轻松地为其他数据类型创建 OLAP 专用的类实现。

我们不断添加新功能令项目日趋复杂化。 所以,于此不会考虑 OLAP 与图形界面集成的所有方面。 在本文中,我们将专注于数据分析,而不去涉及可视化(甚或,可能会有不同的可视化方法)。 阅读本文之后,您可以把更新的引擎与第二篇文章中的 GUI 部分结合起来运用。

改进

在报价分析的背景下,我们也许需要新的逻辑剖析和数据汇集方法。 所需的类将被添加到 OLAPCommon.mqh,因为本质上它们是最基本的。 因此,它们将可供任何应用程序块使用,包括来自 OLAPTrades.mqh 的前者。

添加了以下内容:

  • selector MonthSelector
  • selector WorkWeekDaySelector
  • aggregator VarianceAggregator

MonthSelector 将启用按月数据分组。 该选择器在以前的实现中被省略了。

  template<typename E>
  class MonthSelector: public DateTimeSelector<E>
  {
    public:
      MonthSelector(const E f): DateTimeSelector(f, 12)
      {
        _typename = typename(this);
       }
      
      virtual bool select(const Record *r, int &index) const
      {
        double d = r.get(selector);
        datetime t = (datetime)d;
        index = TimeMonth(t) - 1;
        return true;
       }
      
      virtual string getLabel(const int index) const
      {
        static string months[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
        return months[index];
       }
  };

WorkWeekDaySelector 是 WeekDaySelector 的模拟,但它按工作日(从 1 到 5)拆分数据。 这是分析行情的便捷解决方案,因为不会在周末进行交易:周末的数值始终为零,因此无需为它们保留超数据块单元。

VarianceAggregator 允许计算数据差异,故此它是 AverageAggregator 的补充。 新聚合器的思路在于将其与平均真实范围(ATR)的数值进行比较,尽管聚合器可计算任何数据样本(例如,分别按日内小时数,或周内星期), 以及其他数据源(例如,交易历史记录中的回报变化值)。

  template<typename E>
  class VarianceAggregator: public Aggregator<E>
  {
    protected:
      int counters[];
      double sumx[];
      double sumx2[];
      
    public:
      VarianceAggregator(const E f, const Selector<E> *&s[], const Filter<E> *&t[]): Aggregator(f, s, t)
      {
        _typename = typename(this);
       }
      
      virtual void setSelectorBounds(const int length = 0) override
      {
        Aggregator<E>::setSelectorBounds();
        ArrayResize(counters, ArraySize(totals));
        ArrayResize(sumx, ArraySize(totals));
        ArrayResize(sumx2, ArraySize(totals));
        ArrayInitialize(counters, 0);
        ArrayInitialize(sumx, 0);
        ArrayInitialize(sumx2, 0);
       }
  
      virtual void update(const int index, const double value) override
      {
        counters[index]++;
        sumx[index] += value;
        sumx2[index] += value * value;
        
        const int n = counters[index];
        const double variance = (sumx2[index] - sumx[index] * sumx[index] / n) / MathMax(n - 1, 1);
        totals[index] = MathSqrt(variance);
       }
  };

图例 1 聚合器类的示意图

图例 1 聚合器类的示意图

选择器 QuantizationSelector 和 SerialNumberSelector 衍生自 BaseSelector,而不是更具体的 TradeSelector。 QuantizationSelector 已得到一个新的构造函数参数,该参数允许设置选择器的粒度。 默认情况下,它等于零,这意味着数据按相应字段值精确匹配分组(该字段在选择器中指定)。 例如,在上一篇文章中,我们按手数进行量化,从而获得按手数细分的利润报告。 交易历史中包含的数据块单元很多,例如 0.01、0.1 等。 有时,以指定的步长(单元大小)进行量化更方便。 可以用新的构造函数参数指定该步长。 在以下源代码中,新添加的部分用 + 注释标记。

  template<typename T>
  class QuantizationSelector: public BaseSelector<T>
  {
    protected:
      Vocabulary<double> quants;
      uint cell;                 // +
  
    public:
      QuantizationSelector(const T field, const uint granularity = 0 /* + */): BaseSelector<T>(field), cell(granularity)
      {
        _typename = typename(this);
       }
  
      virtual void prepare(const Record *r) override
      {
        double value = r.get(selector);
        if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; // +
        quants.add(value);
       }
      
      virtual bool select(const Record *r, int &index) const override
      {
        double value = r.get(selector);
        if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; // +
        index = quants.get(value);
        return (index >= 0);
       }
      
      virtual int getRange() const override
      {
        return quants.size();
       }
      
      virtual string getLabel(const int index) const override
      {
        return (string)(float)quants[index];
       }
  };

此外,还对现有的类进行了其他改进。 现在,Filter 和 FilterRange 过滤器类支持按字段值进行比较,而不仅仅只能按所加入数值的单元索引进行比较。 从用户的角度来看,这很方便,因为并不总能事先知晓单元索引。 如果选择器返回的索引等于 -1,则启用新模式(新添加的代码行用 + 注释标记):

  template<typename E>
  class Filter
  {
    protected:
      Selector<E> *selector;
      double filter;
      
    public:
      Filter(Selector<E> &s, const double value): selector(&s), filter(value)
      {
       }
      
      virtual bool matches(const Record *r) const
      {
        int index;
        if(selector.select(r, index))
        {
          if(index == -1)                                             // +
          {                                                           // +
            if(dynamic_cast<FilterSelector<E> *>(selector) != NULL)   // +
            {                                                         // +
              return r.get(selector.getField()) == filter;            // +
            }                                                         // +
          }                                                           // +
          else                                                        // +
          {                                                           // +
            if(index == (int)filter) return true;
          }                                                           // +
         }
        return false;
       }
      
      Selector<E> *getSelector() const
      {
        return selector;
       }
      
      virtual string getTitle() const
      {
        return selector.getTitle() + "[" + (string)filter + "]";
       }
  };

当然,我们需要一个可以返回 -1 作为索引的选择器。 它可称为 FilterSelector。

  template<typename T>
  class FilterSelector: public BaseSelector<T>
  {
    public:
      FilterSelector(const T field): BaseSelector(field)
      {
        _typename = typename(this);
       }
  
      virtual bool select(const Record *r, int &index) const override
      {
        index = -1;
        return true;
       }
      
      virtual int getRange() const override
      {
        return 0;
       }
      
      virtual double getMin() const override
      {
        return 0;
       }
      
      virtual double getMax() const override
      {
        return 0;
       }
      
      virtual string getLabel(const int index) const override
      {
        return EnumToString(selector);
       }
  };

如您所见,选择器对任何记录返回 true,这意味着该记录应予以处理,并返回 -1 作为索引。 根据该数值,过滤器知道用户的数据过滤请求不是按索引而是按字段值。 下面将研究其用法示例。

此外,日志显示现在支持按数值针对多维数据块进行排序。 以前,多维数据块不能排序。 多维数据块的排序仅部分可用,即,只有那些选择器的统一格式化标签是按字典顺序排列的字符串才有可能。 尤其是,新的工作日选择器提供的标签:"1`Monday", "2`Tuesday", "3`Wednesday", "4`Thursday", "5`Friday"。 开头的周内日编号能够正确排序。 否则,就需要标签比较功能才能正确实现。 进而,对于某些“顺序”聚合器,如 IdentityAggregator、ProgressiveTotalAggregator,也许有必要设置数据块侧的优先级,因为在这些聚合器中,X 轴始终显示记录的序列号,然而在排序时不应将其作为第一个条件(甚至应作为最后一个标准)。

这些只是源代码中的部分修改。 您可以通过比较源代码来检查所有这些代码。

将 OLAP 扩展到报价应用领域

我们拥 OLAPCommon.mqh 中的基类作为基础,并创建一个类似于 OLAPTrades.mqh 的报价分析类文件:OLAPQuotes.mqh。 首先,我们描述以下类型:

  • enumerations QUOTE_SELECTORS, QUOTE_RECORD_FIELDS
  • selectors QuoteSelector, ShapeSelector
  • record classes QuotesRecord, CustomQuotesBaseRecord
  • adapter QuotesDataAdapter
  • OLAPEngineQuotes — OLAPEngine<QUOTE_RECORD_FIELDS> specialization

QUOTE_SELECTORS 枚举定义如下:

  enum QUOTE_SELECTORS
  {
    SELECTOR_NONE,       // none
    SELECTOR_SHAPE,      // type
    SELECTOR_INDEX,      // ordinal number
    /* below datetime field assumed */
    SELECTOR_MONTH,      // month-of-year
    SELECTOR_WEEKDAY,    // day-of-week
    SELECTOR_DAYHOUR,    // hour-of-day
    SELECTOR_HOURMINUTE, // minute-of-hour
    /* the next require a field as parameter */
    SELECTOR_SCALAR,     // scalar(field)
    SELECTOR_QUANTS,     // quants(field)
    SELECTOR_FILTER      // filter(field)
  };

shape 选择器根据价格类型区分柱线:看涨,看跌和中性,具体取决于价格走势方向。

index 选择器对应于在基类(文件 OLAPCommon.mqh)中定义的 SerialNumberSelector 类。 在进行交易操作时,这些是成交的序列号。 现在,柱线编号将用于报价。

上面研讨了 month 选择器。 其他选择器继承自先前的文章。

报价中的数据字段则由以下枚举描述:

  enum QUOTE_RECORD_FIELDS
  {
    FIELD_NONE,          // none
    FIELD_INDEX,         // index (bar number)
    FIELD_SHAPE,         // type (bearish/flat/bullish)
    FIELD_DATETIME,      // datetime
    FIELD_PRICE_OPEN,    // open price
    FIELD_PRICE_HIGH,    // high price
    FIELD_PRICE_LOW,     // low price
    FIELD_PRICE_CLOSE,   // close price
    FIELD_PRICE_RANGE_OC,// price range (OC)
    FIELD_PRICE_RANGE_HL,// price range (HL)
    FIELD_SPREAD,        // spread
    FIELD_TICK_VOLUME,   // tick volume
    FIELD_REAL_VOLUME,   // real volume
    FIELD_CUSTOM1,       // custom 1
    FIELD_CUSTOM2,       // custom 2
    FIELD_CUSTOM3,       // custom 3
    FIELD_CUSTOM4,       // custom 4
    QUOTE_RECORD_FIELDS_LAST
  };

从名称和注释中就能清楚地了解它们的目的。

以上两个枚举作为宏实现:

  #define SELECTORS QUOTE_SELECTORS
  #define ENUM_FIELDS QUOTE_RECORD_FIELDS

注意类似的宏定义 — SELECTORS 和 ENUM_FIELDS — 在所有“被应用”的头文件里均可待用。 到目前为止,我们有两个文件(OLAPTrades.mqh,OLAPQuotes.mqh — 交易操作历史记录和报价),但这样的文件可以有更多。 因此,在任何使用 OLAP 的项目中,现在都可以一次仅分析一个应用领域(例如 OLAPTrades.mqh 或 OLAPQuotes.mqh,但一次不能分析全部两者)。 另一个可能需要重构的小地方是启用不同数据块的交叉分析。 本文未涵盖此部分,因为需要并行分析多个元数据块的任务似乎很少。 若您需要,您可自行完成这样的重构。

报价的父选择器是特殊的 BaseSelector,含有 QUOTE_RECORD_FIELDS 字段:

  class QuoteSelector: public BaseSelector<QUOTE_RECORD_FIELDS>
  {
    public:
      QuoteSelector(const QUOTE_RECORD_FIELDS field): BaseSelector(field)
      {
       }
  };

柱线类型选择器(看涨或看跌)的实现如下:

  class ShapeSelector: public QuoteSelector
  {
    public:
      ShapeSelector(): QuoteSelector(FIELD_SHAPE)
      {
        _typename = typename(this);
       }
  
      virtual bool select(const Record *r, int &index) const
      {
        index = (int)r.get(selector);
        index += 1; // shift from -1, 0, +1 to [0..2]
        return index >= getMin() && index <= getMax();
       }
      
      virtual int getRange() const
      {
        return 3; // 0 through 2
       }
      
      virtual string getLabel(const int index) const
      {
        const static string types[3] = {"bearish", "flat", "bullish"};
        return types[index];
       }
  };

这里有 3 个保留值代表类型:-1 表示下行走势,0 表示横盘,+1 表示上行走势。 因此,单元索引的范围是 0 到 2(含)。 下面的 QuotesRecord 类演示了如何填充与特定柱线类型相对应的相关字段数值。

图例 2 选择器类示意图

图例 2 选择器类示意图

这是存储有关特定柱线信息记录的类:

  class QuotesRecord: public Record
  {
    protected:
      static int counter; // number of bars
      
      void fillByQuotes(const MqlRates &rate)
      {
        set(FIELD_INDEX, counter++);
        set(FIELD_SHAPE, rate.close > rate.open ? +1 : (rate.close < rate.open ? -1 : 0));
        set(FIELD_DATETIME, (double)rate.time);
        set(FIELD_PRICE_OPEN, rate.open);
        set(FIELD_PRICE_HIGH, rate.high);
        set(FIELD_PRICE_LOW, rate.low);
        set(FIELD_PRICE_CLOSE, rate.close);
        set(FIELD_PRICE_RANGE_OC, (rate.close - rate.open) / _Point);
        set(FIELD_PRICE_RANGE_HL, (rate.high - rate.low) * MathSign(rate.close - rate.open) / _Point);
        set(FIELD_SPREAD, (double)rate.spread);
        set(FIELD_TICK_VOLUME, (double)rate.tick_volume);
        set(FIELD_REAL_VOLUME, (double)rate.real_volume);
       }
    
    public:
      QuotesRecord(): Record(QUOTE_RECORD_FIELDS_LAST)
      {
       }
      
      QuotesRecord(const MqlRates &rate): Record(QUOTE_RECORD_FIELDS_LAST)
      {
        fillByQuotes(rate);
       }
      
      static int getRecordCount()
      {
        return counter;
       }
  
      static void reset()
      {
        counter = 0;
       }
  
      virtual string legend(const int index) const override
      {
        if(index >= 0 && index < QUOTE_RECORD_FIELDS_LAST)
        {
          return EnumToString((QUOTE_RECORD_FIELDS)index);
         }
        return "unknown";
       }
  };

从 MqlRates 结构接收所有信息。 类实例的创建后续会在适配器实现中展示。

字段的应用在同一个类里定义(整数型、实数型、日期型)。 我们必须如此,因为所有记录字段在技术上都存储在双精度型数组之中。

  class QuotesRecord: public Record
  {
    protected:
      const static char datatypes[QUOTE_RECORD_FIELDS_LAST];
  
    public:
      ...
      static char datatype(const int index)
      {
        return datatypes[index];
       }
  };
  
  const static char QuotesRecord::datatypes[QUOTE_RECORD_FIELDS_LAST] =
  {
    0,   // none
    'i', // index, serial number
    'i', // type (-1 down/0/+1 up)
    't', // datetime
    'd', // open price
    'd', // high price
    'd', // low price
    'd', // close price
    'd', // range OC
    'd', // range HL
    'i', // spread
    'i', // tick
    'i', // real
    'd',    // custom 1
    'd',    // custom 2
    'd',    // custom 3
    'd'     // custom 4
  };

特殊字段的存在标志允许在用户界面中调整数据输入/输出,这将在下面进行演示。

所提供的过渡类能够填充自定义字段。 其主要目的是调用来自自定义类中的 fillCustomFields,按基值的指定使用一个模板(因此,在调用 CustomQuotesBaseRecord 构造函数时,我们的自定义对象已被创建,并根据计算自定义字段所需填充了常用的标准字段):

  template<typename T>
  class CustomQuotesBaseRecord: public T
  {
    public:
      CustomQuotesBaseRecord(const MqlRates &rate): T(rate)
      {
        fillCustomFields();
       }
  };

它会在报价适配器中用到:

  template<typename T>
  class QuotesDataAdapter: public DataAdapter
  {
    private:
      int size;
      int cursor;
      
    public:
      QuotesDataAdapter()
      {
        reset();
       }
  
      virtual void reset() override
      {
        size = MathMin(Bars(_Symbol, _Period), TerminalInfoInteger(TERMINAL_MAXBARS));
        cursor = size - 1;
        T::reset();
       }
      
      virtual int reservedSize()
      {
        return size;
       }
      
      virtual Record *getNext()
      {
        if(cursor >= 0)
        {
          MqlRates rate[1];
          if(CopyRates(_Symbol, _Period, cursor, 1, rate) > 0)
          {
            cursor--;
            return new CustomQuotesBaseRecord<T>(rate[0]);
           }
          
          Print(__FILE__, " ", __LINE__, " ", GetLastError());
          
          return NULL;
         }
        return NULL;
       }
  };

该类按时间顺序从旧到新遍历柱线。 这意味着索引(FIELD_INDEX字段)的执行方式如同常规数组,而非时间系列排序。

最后,OLAP 报价引擎如下:

  class OLAPEngineQuotes: public OLAPEngine<QUOTE_SELECTORS,QUOTE_RECORD_FIELDS>
  {
    protected:
      virtual Selector<QUOTE_RECORD_FIELDS> *createSelector(const QUOTE_SELECTORS selector, const QUOTE_RECORD_FIELDS field) override
      {
        switch(selector)
        {
          case SELECTOR_SHAPE:
            return new ShapeSelector();
          case SELECTOR_INDEX:
            return new SerialNumberSelector<QUOTE_RECORD_FIELDS,QuotesRecord>(FIELD_INDEX);
          case SELECTOR_MONTH:
            return new MonthSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_WEEKDAY:
            return new WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_DAYHOUR:
            return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_HOURMINUTE:
            return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_SCALAR:
            return field != FIELD_NONE ? new BaseSelector<QUOTE_RECORD_FIELDS>(field) : NULL;
          case SELECTOR_QUANTS:
            return field != FIELD_NONE ? new QuantizationSelector<QUOTE_RECORD_FIELDS>(field, QuantGranularity) : NULL;
          case SELECTOR_FILTER:
            return field != FIELD_NONE ? new FilterSelector<QUOTE_RECORD_FIELDS>(field) : NULL;
         }
        return NULL;
       }
  
      virtual void initialize() override
      {
        Print("Bars read: ", QuotesRecord::getRecordCount());
       }
  
    public:
      OLAPEngineQuotes(): OLAPEngine() {}
      OLAPEngineQuotes(DataAdapter *ptr): OLAPEngine(ptr) {}
    
  };

在第一篇文章中探讨的 OLAPEngine 基类中(其名称为 OLAPWrapper),所有主要函数仍然可用。 在此,我们只需要创建特定报价的选择器。

默认适配器和 OLAP 引擎实例将作为现成的对象出现:

  QuotesDataAdapter<RECORD_CLASS> _defaultQuotesAdapter;
  OLAPEngineQuotes _defaultEngine;

基于为两个分析应用领域(OLAPTrades.mqh,OLAPQuotes.mqh)所创建的类,OLAP 功能可轻松地扩展至其他用途,例如处理优化结果,或处理从外部资源接收的数据。

图例 3 OLAP 控件类示意图

图例 3 OLAP 控件类示意图

OLAP 报价分析智能交易系统

一切准备就绪,可以开始运用所创建的类了。 我们来开发一个非交易的智能交易系统 OLAPQTS.mq5。 其结构类似于分析交易报告的 OLAPRPRT.mq5。

这就是 CustomQuotesRecord 类,它展示了自定义字段的计算/填充。 它继承自 QuotesRecord。 我们用一些自定义字段来判断报价中的形态,这些形态可作为构建交易策略的根基。 在 fillCustomFields 方法中会填充所有这些字段。 稍后将对其进行详细研讨。

  class CustomQuotesRecord: public QuotesRecord
  {
    public:
      CustomQuotesRecord(): QuotesRecord() {}
      CustomQuotesRecord(const MqlRates &rate): QuotesRecord(rate)
      {
       }
      
      virtual void fillCustomFields() override
      {
  
        // ...
        
       }
      
      virtual string legend(const int index) const override
      {
        // ...
        return QuotesRecord::legend(index);
       }
  };

为了令适配器“知晓”我们的记录类 CustomQuotesRecord,并创建其实例,应在包含 OLAPQuotes.mqh 之前定义以下宏:

  // this line plugs our class into default adapter in OLAPQuotes.mqh
  #define RECORD_CLASS CustomQuotesRecord
  
  #include <OLAP/OLAPQuotes.mqh>

智能交易系统经由参数进行管理,这与交易历史分析项目中所用的参数相似。 数据可以按三个元数据块维度进行汇集,故此可以从 X、Y 和 Z 轴的选择器当中选择。 也可以按一个值或一个数值范围进行过滤。 最后,用户应选择聚合器类型(某些聚合器要求指定聚合字段,其他聚合器要求指定特定字段)和可选的排序类型。

  sinput string X = "————— X axis —————"; // · X ·
  input SELECTORS SelectorX = DEFAULT_SELECTOR_TYPE; // · SelectorX
  input ENUM_FIELDS FieldX = DEFAULT_SELECTOR_FIELD /* field does matter only for some selectors */; // · FieldX
  
  sinput string Y = "————— Y axis —————"; // · Y ·
  input SELECTORS SelectorY = SELECTOR_NONE; // · SelectorY
  input ENUM_FIELDS FieldY = FIELD_NONE; // · FieldY
  
  sinput string Z = "————— Z axis —————"; // · Z ·
  input SELECTORS SelectorZ = SELECTOR_NONE; // · SelectorZ
  input ENUM_FIELDS FieldZ = FIELD_NONE; // · FieldZ
  
  sinput string F = "————— Filter —————"; // · F ·
  input SELECTORS _Filter1 = SELECTOR_NONE; // · Filter1
  input ENUM_FIELDS _Filter1Field = FIELD_NONE; // · Filter1Field
  input string _Filter1value1 = ""; // · Filter1value1
  input string _Filter1value2 = ""; // · Filter1value2
  
  sinput string A = "————— Aggregator —————"; // · A ·
  input AGGREGATORS _AggregatorType = DEFAULT_AGGREGATOR_TYPE; // · AggregatorType
  input ENUM_FIELDS _AggregatorField = DEFAULT_AGGREGATOR_FIELD; // · AggregatorField
  input SORT_BY _SortBy = SORT_BY_NONE; // · SortBy

所有选择器及其字段都以数组形式实现,并可轻松地传递给引擎:

  SELECTORS _selectorArray[4];
  ENUM_FIELDS _selectorField[4];
  
  int OnInit()
  {
    _selectorArray[0] = SelectorX;
    _selectorArray[1] = SelectorY;
    _selectorArray[2] = SelectorZ;
    _selectorArray[3] = _Filter1;
    _selectorField[0] = FieldX;
    _selectorField[1] = FieldY;
    _selectorField[2] = FieldZ;
    _selectorField[3] = _Filter1Field;
    
    _defaultEngine.setAdapter(&_defaultQuotesAdapter);
  
    EventSetTimer(1);
    return INIT_SUCCEEDED;
   }

如我们所见,EA 用到了引擎和报价适配器的默认实例。 根据应用的详规,EA 应针对输入参数进行一次数据处理。 为此目的,以及在没有即时报价的周末也能启用操作,会在 OnInit 处理程序中启动计时器。

OnTimer 中的处理如下展开:

  LogDisplay _display(11, _Digits);
  
  void OnTimer()
  {
    EventKillTimer();
    
    double Filter1value1 = 0, Filter1value2 = 0;
    if(CustomQuotesRecord::datatype(_Filter1Field) == 't')
    {
      Filter1value1 = (double)StringToTime(_Filter1value1);
      Filter1value2 = (double)StringToTime(_Filter1value2);
     }
    else
    {
      Filter1value1 = StringToDouble(_Filter1value1);
      Filter1value2 = StringToDouble(_Filter1value2);
     }
    
    _defaultQuotesAdapter.reset();
    _defaultEngine.process(_selectorArray, _selectorField,
          _AggregatorType, _AggregatorField,
          _display,
          _SortBy,
          Filter1value1, Filter1value2);
   }

分析报价时,我们需要按日期进行过滤。 因此,要在输入参数中以字符串形式设置过滤器的值。 根据所需过滤器的字段类型,字符串将解释为数字或日期(采用通用格式 YYYY.MM.DD)。 在第一篇文章中,我们总是要输入数字值,这对于需要日期型的最终用户而言不太方便。

所有准备好的输入参数都会传递给 OLAP 引擎的 “process” 方法。 无需用户干预即可完成进一步的操作,然后通过 LogDisplay 实例将结果显示在智能系统日志当中。

测试 OLAP 报价分析

我们利用上述功能进行简单的报价研究。

打开 EURUSD D1 图表,并将 OLAPQTS EA 加载到该图表。 所有参数保留为默认值。 这意味着:“类型”选择器取 X 轴,和 COUNT 聚合器。 应更改以下过滤器设置:在 Filter1 参数中,设为 "filter(field)",而 Filter1Field — datetime,在 “Filter1Value1” 和 “Filter1Value2” 中分别设为 — “2019.01.01” 和 “2020.01.01”。 因此,计算范围限于 2019 年。

EA 执行结果如下:

  OLAPQTS (EURUSD,D1)	Bars read: 12626
  OLAPQTS (EURUSD,D1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [3]
  OLAPQTS (EURUSD,D1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,D1)	Selectors: 1
  OLAPQTS (EURUSD,D1)	X: ShapeSelector(FIELD_SHAPE) [3]
  OLAPQTS (EURUSD,D1)	Processed records: 259
  OLAPQTS (EURUSD,D1)	  134.00000: bearish
  OLAPQTS (EURUSD,D1)	    0.00000: flat
  OLAPQTS (EURUSD,D1)	  125.00000: bullish

从日志中可以看出,EA 分析了 12626 根柱线(EURUSD D1 的全部可用历史记录),但是只有 259 根柱线符合过滤条件。 它们中有 134 根看跌,125 根 — 看涨。

将时间帧切换为 H1,我们可以获得一小时柱线的评估:

  OLAPQTS (EURUSD,H1)	Bars read: 137574
  OLAPQTS (EURUSD,H1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [3]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 1
  OLAPQTS (EURUSD,H1)	X: ShapeSelector(FIELD_SHAPE) [3]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	 3051.00000: bearish
  OLAPQTS (EURUSD,H1)	   55.00000: flat
  OLAPQTS (EURUSD,H1)	 3090.00000: bullish

接下来,我们尝试分析点差。 MetaTrader 的特征之一是 MqlRates 结构仅存储最小的点差。 在测试交易策略时,这种方法可能很危险,因为这也许会给出乐观但错误的利润估算。 更好的选择是最小和最大点差的历史记录两者都有。 当然,如有必要,您可以采用即时报价历史记录,但是柱线模式更节省资源。 我们尝试按日内的小时数评估实际点差。

我们采用相同的 EURUSD H1 图表,同样限于 2019 年的过滤器,并添加以下 EA 设置。 选择器 X — "hour-of-day", 聚合器 — "AVERAGE", 聚合器字段 — "spread"。 结果如下:

  OLAPQTS (EURUSD,H1)	Bars read: 137574
  OLAPQTS (EURUSD,H1)	AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 1
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	    4.71984: 00
  OLAPQTS (EURUSD,H1)	    3.19066: 01
  OLAPQTS (EURUSD,H1)	    3.72763: 02
  OLAPQTS (EURUSD,H1)	    4.19455: 03
  OLAPQTS (EURUSD,H1)	    4.38132: 04
  OLAPQTS (EURUSD,H1)	    4.28794: 05
  OLAPQTS (EURUSD,H1)	    3.93050: 06
  OLAPQTS (EURUSD,H1)	    4.01158: 07
  OLAPQTS (EURUSD,H1)	    4.39768: 08
  OLAPQTS (EURUSD,H1)	    4.68340: 09
  OLAPQTS (EURUSD,H1)	    4.68340: 10
  OLAPQTS (EURUSD,H1)	    4.64479: 11
  OLAPQTS (EURUSD,H1)	    4.57915: 12
  OLAPQTS (EURUSD,H1)	    4.62934: 13
  OLAPQTS (EURUSD,H1)	    4.64865: 14
  OLAPQTS (EURUSD,H1)	    4.61390: 15
  OLAPQTS (EURUSD,H1)	    4.62162: 16
  OLAPQTS (EURUSD,H1)	    4.50579: 17
  OLAPQTS (EURUSD,H1)	    4.56757: 18
  OLAPQTS (EURUSD,H1)	    4.61004: 19
  OLAPQTS (EURUSD,H1)	    4.59459: 20
  OLAPQTS (EURUSD,H1)	    4.67054: 21
  OLAPQTS (EURUSD,H1)	    4.50775: 22
  OLAPQTS (EURUSD,H1)	    3.57312: 23

为一天中的每个小时指定平均点差值。 但这只是最小点差的平均值,因此不是真正的点差。 要获得更真实的图像,我们切换到 M1 时间帧。 因此,我们将分析所有可用的详细历史信息(无需即时报价便可获得)。

  OLAPQTS (EURUSD,M1)	Bars read: 1000000
  OLAPQTS (EURUSD,M1)	AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,M1)	Selectors: 1
  OLAPQTS (EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,M1)	Processed records: 371475
  OLAPQTS (EURUSD,M1)	   14.05653: 00
  OLAPQTS (EURUSD,M1)	    6.63397: 01
  OLAPQTS (EURUSD,M1)	    6.00707: 02
  OLAPQTS (EURUSD,M1)	    5.72516: 03
  OLAPQTS (EURUSD,M1)	    5.72575: 04
  OLAPQTS (EURUSD,M1)	    5.77588: 05
  OLAPQTS (EURUSD,M1)	    5.82541: 06
  OLAPQTS (EURUSD,M1)	    5.82560: 07
  OLAPQTS (EURUSD,M1)	    5.77979: 08
  OLAPQTS (EURUSD,M1)	    5.44876: 09
  OLAPQTS (EURUSD,M1)	    5.32619: 10
  OLAPQTS (EURUSD,M1)	    5.32966: 11
  OLAPQTS (EURUSD,M1)	    5.32096: 12
  OLAPQTS (EURUSD,M1)	    5.32117: 13
  OLAPQTS (EURUSD,M1)	    5.29633: 14
  OLAPQTS (EURUSD,M1)	    5.21140: 15
  OLAPQTS (EURUSD,M1)	    5.17084: 16
  OLAPQTS (EURUSD,M1)	    5.12794: 17
  OLAPQTS (EURUSD,M1)	    5.27576: 18
  OLAPQTS (EURUSD,M1)	    5.48078: 19
  OLAPQTS (EURUSD,M1)	    5.60175: 20
  OLAPQTS (EURUSD,M1)	    5.70999: 21
  OLAPQTS (EURUSD,M1)	    5.87404: 22
  OLAPQTS (EURUSD,M1)	    6.94555: 23

结果更接近现实:在一些小时内,平均最小点差增加了 2-3 倍。 为了令分析更加严格,我们可以利用 “MAX” 聚合器创建最高数值来替代平均值。 尽管结果值将是最小值中的最高值,但请不要忘记它们是基于每个小时内的一分钟柱线,因此可以完美地描述短线交易的入场和离场条件。

  OLAPQTS (EURUSD,M1)	Bars read: 1000000
  OLAPQTS (EURUSD,M1)	MaxAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,M1)	Selectors: 1
  OLAPQTS (EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,M1)	Processed records: 371475
  OLAPQTS (EURUSD,M1)	  157.00000: 00
  OLAPQTS (EURUSD,M1)	   31.00000: 01
  OLAPQTS (EURUSD,M1)	   12.00000: 02
  OLAPQTS (EURUSD,M1)	   12.00000: 03
  OLAPQTS (EURUSD,M1)	   13.00000: 04
  OLAPQTS (EURUSD,M1)	   11.00000: 05
  OLAPQTS (EURUSD,M1)	   12.00000: 06
  OLAPQTS (EURUSD,M1)	   12.00000: 07
  OLAPQTS (EURUSD,M1)	   11.00000: 08
  OLAPQTS (EURUSD,M1)	   11.00000: 09
  OLAPQTS (EURUSD,M1)	   12.00000: 10
  OLAPQTS (EURUSD,M1)	   13.00000: 11
  OLAPQTS (EURUSD,M1)	   12.00000: 12
  OLAPQTS (EURUSD,M1)	   13.00000: 13
  OLAPQTS (EURUSD,M1)	   12.00000: 14
  OLAPQTS (EURUSD,M1)	   14.00000: 15
  OLAPQTS (EURUSD,M1)	   16.00000: 16
  OLAPQTS (EURUSD,M1)	   14.00000: 17
  OLAPQTS (EURUSD,M1)	   15.00000: 18
  OLAPQTS (EURUSD,M1)	   21.00000: 19
  OLAPQTS (EURUSD,M1)	   17.00000: 20
  OLAPQTS (EURUSD,M1)	   25.00000: 21
  OLAPQTS (EURUSD,M1)	   31.00000: 22
  OLAPQTS (EURUSD,M1)	   70.00000: 23

看一下区别:一开始我们有 4 个点的点差; 而在午夜则为数十甚至数百。

我们来评估点差的方差,并检查新聚合器如何工作。 我们选择 “DEVIATION” 来完成它。

  OLAPQTS (EURUSD,M1)	Bars read: 1000000
  OLAPQTS (EURUSD,M1)	VarianceAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,M1)	Selectors: 1
  OLAPQTS (EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,M1)	Processed records: 371475
  OLAPQTS (EURUSD,M1)	    9.13767: 00
  OLAPQTS (EURUSD,M1)	    3.12974: 01
  OLAPQTS (EURUSD,M1)	    2.72293: 02
  OLAPQTS (EURUSD,M1)	    2.70965: 03
  OLAPQTS (EURUSD,M1)	    2.68758: 04
  OLAPQTS (EURUSD,M1)	    2.64350: 05
  OLAPQTS (EURUSD,M1)	    2.64158: 06
  OLAPQTS (EURUSD,M1)	    2.64934: 07
  OLAPQTS (EURUSD,M1)	    2.62854: 08
  OLAPQTS (EURUSD,M1)	    2.72117: 09
  OLAPQTS (EURUSD,M1)	    2.80259: 10
  OLAPQTS (EURUSD,M1)	    2.79681: 11
  OLAPQTS (EURUSD,M1)	    2.80850: 12
  OLAPQTS (EURUSD,M1)	    2.81435: 13
  OLAPQTS (EURUSD,M1)	    2.83489: 14
  OLAPQTS (EURUSD,M1)	    2.90745: 15
  OLAPQTS (EURUSD,M1)	    2.95804: 16
  OLAPQTS (EURUSD,M1)	    2.96799: 17
  OLAPQTS (EURUSD,M1)	    2.88021: 18
  OLAPQTS (EURUSD,M1)	    2.76605: 19
  OLAPQTS (EURUSD,M1)	    2.72036: 20
  OLAPQTS (EURUSD,M1)	    2.85615: 21
  OLAPQTS (EURUSD,M1)	    2.94224: 22
  OLAPQTS (EURUSD,M1)	    4.60560: 23

这些数值代表单个标准偏差,在剥头皮策略或跟踪波动性的机器人中,可以依据该标准偏差配置过滤器。

我们来检查用一个范围或一根柱线的价格走势来填充字段,量化操作采用指定单元大小,并排序。

为此目的,切换回 EURUSD D1,并采用同样限于 2019 年的过滤器。 另外,设置以下参数:

  • QuantGranularity=100 (5-digit points)
  • SelectorX=quants
  • FieldX=price range (OC)
  • Aggregator=COUNT
  • SortBy=value (descending)

得到以下结果:

  OLAPQTS (EURUSD,D1)	Bars read: 12627
  OLAPQTS (EURUSD,D1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [20]
  OLAPQTS (EURUSD,D1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,D1)	Selectors: 1
  OLAPQTS (EURUSD,D1)	X: QuantizationSelector<QUOTE_RECORD_FIELDS>(FIELD_PRICE_RANGE_OC) [20]
  OLAPQTS (EURUSD,D1)	Processed records: 259
  OLAPQTS (EURUSD,D1)	      [value]   [title]
  OLAPQTS (EURUSD,D1) [ 0] 72.00000 "0.0"    
  OLAPQTS (EURUSD,D1) [ 1] 27.00000 "100.0"  
  OLAPQTS (EURUSD,D1) [ 2] 24.00000 "-100.0" 
  OLAPQTS (EURUSD,D1) [ 3] 24.00000 "-200.0" 
  OLAPQTS (EURUSD,D1) [ 4] 21.00000 "200.0"  
  OLAPQTS (EURUSD,D1) [ 5] 17.00000 "-300.0" 
  OLAPQTS (EURUSD,D1) [ 6] 16.00000 "300.0"  
  OLAPQTS (EURUSD,D1) [ 7] 12.00000 "-400.0" 
  OLAPQTS (EURUSD,D1) [ 8]  8.00000 "500.0"  
  OLAPQTS (EURUSD,D1) [ 9]  8.00000 "400.0"  
  OLAPQTS (EURUSD,D1) [10]  6.00000 "-700.0" 
  OLAPQTS (EURUSD,D1) [11]  6.00000 "-500.0" 
  OLAPQTS (EURUSD,D1) [12]  6.00000 "700.0"  
  OLAPQTS (EURUSD,D1) [13]  4.00000 "-600.0" 
  OLAPQTS (EURUSD,D1) [14]  2.00000 "600.0"  
  OLAPQTS (EURUSD,D1) [15]  2.00000 "1000.0" 
  OLAPQTS (EURUSD,D1) [16]  1.00000 "-800.0" 
  OLAPQTS (EURUSD,D1) [17]  1.00000 "-1100.0"
  OLAPQTS (EURUSD,D1) [18]  1.00000 "900.0"  
  OLAPQTS (EURUSD,D1) [19]  1.00000 "-1000.0"

正如预期的那样,大多数柱线(72)都落在零之下的范围,即这些柱线的价格变化不超过 100 点。 变化在 ±100 和 ±200 点的更遥远,依此类推。

不过,这只是运用 OLAP 分析报价的可能性演示。 现在是时候进行下一步,并利用 OLAP 创建交易策略。

基于 OLAP 报价分析构建交易策略。 第 1 部分

我们尝试找出报价是否拥有与日内和周内周期相关联的任何形态。 如果当前的价格走势在某些时辰或周内的某些天里不是对称的,我们就可以利用它来开单。 为了检测这种周期形态,我们需要利用日内时辰和周内日辰选择器。 选择器可以顺序逐个调用,也可以同时使用,每个选择器都位于自己的数轴上。 第二个选项更可取,因为它能够一次性参考两个因素(周期)来构建更精准的数据样本。 选择器设置在 X 轴上,亦或 Y 轴上,对于程序来说没有区别。然而这会影响到用户的显示结果。

这些选择器的范围是 24(一天中的时辰数)和 5(日辰数),因此数据块尺寸为 120。 若选择沿 Z 轴的年内月份选择器,还有可能连接一年内的季节性周期形态。 为简单起见,我们现在将操控二维数据块。

柱线内的价格变化在两个字段中表示:FIELD_PRICE_RANGE_OC 和 FIELD_PRICE_RANGE_HL。 第一个提供开盘价和收盘价之间的点差,第二个显示最高价和最低价之间的范围。 我们用第一个作为潜在成交的统计数据源。 现在应该决定要计算哪些统计信息,即该应用哪个聚合器。

奇怪的是,ProfitFactorAggregator 聚合器也许会派上用场。 在之前的文章中曾对其进行过探讨。 该聚合器分别将指定字段的正数值和负数值累加,然后返回它们的商:除以正数值和负数值取模。 因此,如果在某些超数据块单元中出现正数值价格增量,则利润因子将高于1。 如果出现负数值,则利润因子将大大低于 1。 换言之,所有与 1 差距很大的数值代表开立多头或空头成交的良好条件。 当利润因子大于 1 时,多头成交可以获利,而利润因子小于 1 则空头可以获利更多。 空头的实际利润因子是计算值的倒数。

我们以 EURUSD H1 进行分析。 选择输入参数:

  • SelectorX=hour-of-day
  • SelectorY=day-of-week
  • Filter1=field
  • Filter1Field=datetime
  • Filter1Value1=2019.01.01
  • Filter1Value2=2020.01.01
  • AggregatorType=Profit Factor
  • AggregatorField=price range (OC)
  • SortBy=value (descending)

120 行的完整结果列表对我们来说毫无亮点。 此处代表最可盈利的买卖选项的初始值和最终值(由于启用了排序,它们会出现在最开始和最后)。

  OLAPQTS (EURUSD,H1)	Bars read: 137597
  OLAPQTS (EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_PRICE_RANGE_OC [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 5.85417 "00; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  1] 5.79204 "00; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  2] 5.25194 "00; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  3] 4.10104 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  4] 4.00463 "01; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [  5] 2.93725 "01; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 2.50000 "00; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  7] 2.44557 "15; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  8] 2.43496 "04; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  9] 2.36278 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [ 10] 2.33917 "04; 4`Thursday" 
  ...
  OLAPQTS (EURUSD,H1) [110] 0.49096 "09; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [111] 0.48241 "13; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [112] 0.45891 "19; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [113] 0.45807 "19; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [114] 0.44993 "14; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [115] 0.44513 "23; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [116] 0.42693 "23; 1`Monday"   
  OLAPQTS (EURUSD,H1) [117] 0.37026 "10; 1`Monday"   
  OLAPQTS (EURUSD,H1) [118] 0.34662 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [119] 0.19705 "23; 5`Friday"   

请注意,显示的每个数值包含两个维度 X 和 Y 的坐标(分别用于时辰和日辰)。

得到的数值并不完全正确,因为它们忽略了点差。 此处的自定义字段可用于解决问题。 例如,要评估点差的潜在影响,我们要把柱线范围减去点差保存在第一个自定义字段当中。 对于第二个字段,计算柱线方向减去点差。

  virtual void fillCustomFields() override
  {
    const double newBarRange = get(FIELD_PRICE_RANGE_OC);
    const double spread = get(FIELD_SPREAD);

    set(FIELD_CUSTOM1, MathSign(newBarRange) * (MathAbs(newBarRange) - spread));
    set(FIELD_CUSTOM2, MathSign(newBarRange) * MathSign(MathAbs(newBarRange) - spread));
    
    // ...
   }

选择自定义字段 1 作为聚合器。 此为结果:

  OLAPQTS (EURUSD,H1)	Bars read: 137598
  OLAPQTS (EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM1 [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 6.34239 "00; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  1] 5.63981 "00; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  2] 5.15044 "00; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  3] 4.41176 "01; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [  4] 4.18052 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  5] 3.04167 "01; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 2.60000 "00; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  7] 2.53118 "15; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  8] 2.50118 "04; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  9] 2.47716 "04; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [ 10] 2.46208 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [ 11] 2.20858 "03; 5`Friday"   
  OLAPQTS (EURUSD,H1) [ 12] 2.11964 "03; 1`Monday"   
  OLAPQTS (EURUSD,H1) [ 13] 2.11123 "19; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [ 14] 2.10998 "01; 1`Monday"   
  OLAPQTS (EURUSD,H1) [ 15] 2.07638 "10; 4`Thursday"
  OLAPQTS (EURUSD,H1) [ 16] 1.95498 "09; 5`Friday"    
  ...
  OLAPQTS (EURUSD,H1) [105] 0.59029 "11; 5`Friday"   
  OLAPQTS (EURUSD,H1) [106] 0.55008 "14; 5`Friday"   
  OLAPQTS (EURUSD,H1) [107] 0.54643 "13; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [108] 0.50484 "09; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [109] 0.50000 "22; 1`Monday"   
  OLAPQTS (EURUSD,H1) [110] 0.49744 "06; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [111] 0.46686 "13; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [112] 0.44753 "19; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [113] 0.44499 "19; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [114] 0.43838 "14; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [115] 0.41290 "23; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [116] 0.39770 "23; 1`Monday"   
  OLAPQTS (EURUSD,H1) [117] 0.35586 "10; 1`Monday"   
  OLAPQTS (EURUSD,H1) [118] 0.34721 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [119] 0.18769 "23; 5`Friday"   

该值表示应在周四执行交易操作并可从中获利:在凌晨 0、1 和 4 时做多,做空时间则在下午 7 和 11 时。 (19 和 23)。 在星期五,建议在早上 0、3、4、9 时做多,在 11、14 和 23 时做空。 不过,由于交易时段即将结束,且有潜在的不可盈利缺口,周五 23 时的做空可能会带来风险(顺便说一句,此处也可以利用自定义字段轻松实现缺口分析自动化)。 在本项目中,可接受的利润因子级别设置为 2 或更高(对于做空,分别为 0.5 或更低)。 实际上,该值通常比理论结果差,因此应提供一定的安全松紧度。

此外,不仅应按柱线范围来计算利润因子,而且还应该按看涨和看跌烛条的数量来计算利润因子。 为此目的,请选择柱线类型(窗体)作为聚合器。 有时,一根或两根超尺寸烛条能够产生一定的获利额度。 如果我们比较两类获利因子:按烛条尺寸与按不同方向的柱线数量;则此类尖峰将变得更加引人注目。

一般说来,按日期字段选择的低层选择器,若时间帧相同,则我们不必分析其数据。 这次,我们在 H1 时间帧内使用 “hour-of-day(日内时辰)”。 若数据所处时间帧小于或等于按日期字段选择的低层选择器的时间帧,那么就能够分析这些数据。 例如,我们可以在 M15 上执行类似的分析,并利用 "hour-of-day" 选择器按小时保留分组。 如此,我们即可判定 15 分钟柱线的获利因子。 然而,对于当前策略,我们将需要额外指定小时内的入场时刻。 这可以通过分析每小时的最可能烛条形成方式来完成(即,形成与主要柱线实体逆向走势之后)。 OLAPQTS 源代码的注释中提供了柱线尾部“数字化”的示例。

在逐小时和逐日分析中,更直观地识别稳定的 “买入” 和 “卖出” 柱线的方法是利用 ProgressiveTotalAggregator。 在此情况下,应为 X 轴设置 “ordinal number(序号)”选择器(连续分析所有柱线),并为 Y 轴和 Z 轴设置 “hour-of-day” 和 "day-of-week" 选择器,还要用到之前的聚合字段 “custom 1”。 如此将为每个特定的小时柱线生成实际的交易余额曲线。 但是记录和分析此类数据并不方便,故此该方法更适合与图形显示结合。 这会令实现变得更加复杂,此即为何我们要使用日志。

我们来创建 SingleBar 智能交易系统,它会根据运用 OLAP 分析找到的周期执行成交。 主要参数允许配置交易时间表:

  input string BuyHours = "";
  input string SellHours = "";
  input uint ActiveDayOfWeek = 0;

字符串参数 BuyHours 和 SellHours 接收时辰列表,分别设定开多头和开空头成交。 每个列表中的时辰用逗号分隔。 在 ActiveDayOfWeek 中设置日辰(数值从 1 到 5,对应星期一到星期五)。 在测试阶段,会检查指定的某天。 不过,在将来,智能交易系统应支持每周全日时间表。 如果 ActiveDayOfWeek 设置为 0,则 EA 将采用相同的时间表全日交易。 不过,这需要按配置 “hour-of-day” 进行 OLAP 初步分析,同时重置 Y 轴为 “day-of-week”。如您愿意,您可自行测试该策略。

在 OnInit 中读取并检查设置:

  int buyHours[], sellHours[];
  
  int parseHours(const string &data, int &result[])
  {
    string str[];
    const int n = StringSplit(data, ',', str);
    ArrayResize(result, n);
    for(int i = 0; i < n; i++)
    {
      result[i] = (int)StringToInteger(str[i]);
     }
    return n;
   }
  
  int OnInit()
  {
    const int trend = parseHours(BuyHours, buyHours);
    const int reverse = parseHours(SellHours, sellHours);
    
    return trend > 0 || reverse > 0 ? INIT_SUCCEEDED : INIT_PARAMETERS_INCORRECT;
   }

在 OnTick 处理程序中,将检查交易时间列表,如果在其中匹配到当前时间,则特殊的 “mode” 变量将被设置为 +1 或 -1。 如果未找到该时辰,则 “mode” 将等于 0,这意味着所有持仓全部平仓,且不开新仓。 如果此刻没有订单,且 “mode” 不等于零,则应开一笔新仓。 如果已有与时间表建议的同向持仓,则持仓予以保留。 如果信号方向与打仓相反,则应逆转持仓。 同一时刻只能开立一笔持仓。

  template<typename T>
  int ArrayFind(const T &array[], const T value)
  {
    const int n = ArraySize(array);
    for(int i = 0; i < n; i++)
    {
      if(array[i] == value) return i;
     }
    return -1;
   }
  
  void OnTick()
  {
    MqlTick tick;
    if(!SymbolInfoTick(_Symbol, tick)) return;
    
    const int h = TimeHour(TimeCurrent());
  
    int mode = 0;
    
    if(ArrayFind(buyHours, h) > -1)
    {
      mode = +1;
     }
    else
    if(ArrayFind(sellHours, h) > -1)
    {
      mode = -1;
     }
  
    if(ActiveDayOfWeek != 0 && ActiveDayOfWeek != _TimeDayOfWeek()) mode = 0; // skip all days except specified
  
    // pick up existing orders (if any)
    const int direction = CurrentOrderDirection();
    
    if(mode == 0)
    {
      if(direction != 0)
      {
        OrdersCloseAll();
       }
      return;
     }
    
    if(direction != 0) // there exist open orders
    {
      if(mode == direction) // keep direction
      {
        return; // existing trade goes on
       }
      OrdersCloseAll();
     }
    
    
    const int type = mode > 0 ? OP_BUY : OP_SELL;
    
    const double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
      
    OrderSend(_Symbol, type, Lot, p, 100, 0, 0);
   }

成交仅在交易策略设置的时辰,柱线初创的时刻执行。 其他函数 ArrayFind、CurrentOrderDirection 和 OrdersCloseAll 如下所示。 所有这些函数,以及 EA 均使用 MT4Orders 函数库,从而更轻松地利用交易 API。 此外,随附的 MT4Bridge/MT4Time.mqh 代码用于处理日期。

  int CurrentOrderDirection(const string symbol = NULL)
  {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
      if(OrderSelect(i, SELECT_BY_POS))
      {
        if(OrderType() <= OP_SELL && (symbol == NULL || symbol == OrderSymbol()))
        {
          return OrderType() == OP_BUY ? +1 : -1;
         }
       }
     }
    return 0;
   }
  
  void OrdersCloseAll(const string symbol = NULL, const int type = -1) // OP_BUY or OP_SELL
  {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
      if(OrderSelect(i, SELECT_BY_POS))
      {
        if(OrderType() <= OP_SELL && (type == -1 || OrderType() == type) && (symbol == NULL || symbol == OrderSymbol()))
        {
          OrderClose(OrderTicket(), OrderLots(), OrderType() == OP_BUY ? SymbolInfoDouble(OrderSymbol(), SYMBOL_BID) : SymbolInfoDouble(OrderSymbol(), SYMBOL_ASK), 100);
         }
       }
     }
   }

完整的源代码随附如下。 在本文里跳过了一件事,就是遵循 OLAP 引擎中运用的逻辑对利润因子进行理论计算。 这可以将理论值与测试结果中的实际利润因子进行比较。 这两个数值通常近似,但不完全匹配。 当然,若 EA 设置为仅在一个方向买入(BuyHours)或卖出(SellHours)时,理论利润因子才有意义。 否则,这两种模式会重叠,且理论利润因子(PF)趋于 1。 Also, the theoretical profitable profit factor for sell deals is indicated by values less than 1, since it is the inverse of the normal profit factor. 例如,理论做空 PF 为 0.5,相当于测试器中的实际 PF 等于 2。 对于做多方向,理论和实际 PF 相似:值大于 1 表示盈利,值小于 1 表示亏损。

我们利用 2019 年的 EURUSD H1 数据测试 SingleBar EA。 设置交易时辰为星期五(Friday):

  • BuyHours=0,4,3,9
  • SellHours=23,14,11
  • ActiveDayOfWeek=5

指定时辰的顺序并不重要。 在此,按预期获利能力以降序指定它们。 测试结果如下:

图例 4 在 EURUSD H1 图表,2019 年,时间表为星期五,SingleBar EA 的测试报告

图例 4 在 EURUSD H1 图表,2019 年,时间表为星期五,SingleBar EA 的测试报告

结果很棒。 但这并不奇怪,因为于今年也进行了初步分析。 我们将测试开始日期更改为 2018 年初,以便观察所发现形态的性能。

图例 5 在 EURUSD H1,2018-2019 年之间,时间表为星期五,SingleBar EA 的交易报告,

图例 5 在 EURUSD H1,2018-2019 年之间,时间表为星期五,SingleBar EA 的交易报告,

尽管结果有些糟,但您可以看到这些形态自 2018 年中期以来运行良好,故此利用 OLAP 分析,可以在“in the present future(从今往后)”交易时更早地发现它们。 然而,搜索最佳分析周期,并判定所发现形态的持续时间是另一个大话题。 从某种意义上说,OLAP 分析也需要如同智能交易系统那样进行优化。 从理论上讲,将 OLAP 内置到 EA 中的方法是有可能实现的,该 EA 在测试器中以不同的长度,和不同的开始日期,在不同历史记录区间运行;然后针对它们当中的每一段都执行正向验证。 这就是 Cluster Walk-Forward (集簇正向验证)技术,不过在 MetaTrader 中不完全支持该技术(在撰写本文时,只能自动启动正向验证测试,而不能更改开始日期,或调整周期大小,因此不得不利用 MQL5 或其他工具,例如 Shell 脚本自行实现)。

通常,应将 OLAP 视为一种研究工具,它有助您运用其他方法分辨需要进行更全面分析的区域,譬如传统的智能交易系统优化等等。 进而,我们将看到如何将 OLAP 引擎内置到智能交易系统之中,并可同时支持测试器和在线交易。

我们再用一些其他日辰验证当前的交易策略。 无论好坏都特意彰显于此。

图例 6.在 EURUSD H1,2018-2019 年,星期二,SingleBar EA 的交易报告

图例 6.在 EURUSD H1,2018-2019 年,星期二,SingleBar EA 的交易报告

图例 6.b.在 EURUSD H1,2018-2019 年,星期三,SingleBar EA 的交易报告

图例 6.b.在 EURUSD H1,2018-2019 年,星期三,SingleBar EA 的交易报告

图例 6.c.在 EURUSD H1,2018-2019 年,星期四,SingleBar EA 的交易报告

图例 6.c.在 EURUSD H1,2018-2019 年,星期四,SingleBar EA 的交易报告

不出所料,一周中不同日辰的交易行为模棱两可,这表明不存在通用的解决方案,这一方案需要进一步改进。

我们看看如果我们分析更长时间段的报价(例如从 2015 年到 2019 年),然后在 2019 年交易转为正向验证模式,能否发现什么样的交易时间表。

  OLAPQTS (EURUSD,H1)	Bars read: 137606
  OLAPQTS (EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM3 [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1420070400.0 ... 1546300800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 24832
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 2.04053 "01; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  1] 1.78702 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  2] 1.75055 "15; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  3] 1.71793 "00; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  4] 1.69210 "00; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  5] 1.64361 "04; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 1.63956 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  7] 1.62157 "05; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  8] 1.53032 "00; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  9] 1.49733 "16; 1`Monday"   
  OLAPQTS (EURUSD,H1) [ 10] 1.48539 "01; 5`Friday"   
  ...
  OLAPQTS (EURUSD,H1) [109] 0.74241 "16; 5`Friday"   
  OLAPQTS (EURUSD,H1) [110] 0.70346 "13; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [111] 0.68990 "23; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [112] 0.66238 "23; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [113] 0.66176 "14; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [114] 0.62968 "13; 1`Monday"   
  OLAPQTS (EURUSD,H1) [115] 0.62585 "23; 5`Friday"   
  OLAPQTS (EURUSD,H1) [116] 0.60150 "14; 5`Friday"   
  OLAPQTS (EURUSD,H1) [117] 0.55621 "11; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [118] 0.54919 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [119] 0.49804 "11; 3`Wednesday"

如您所见,时间段的递增会导致每个独立时辰的盈利能力递减。 在某些点,实情与形态搜索的冲突开始泛滥。 星期三似乎是最有可能盈利的一天。 不过,在正向验证期内它的行为并不十分稳定。 例如,考虑以下设置:

  • BuyHours=1,4,20,5,0
  • SellHours=11,23,13
  • ActiveDayOfWeek=3

结果报告如下:

图例 7.在 EURUSD H1,2015-2020 年,排除 2019 年,星期三,SingleBar EA 的交易报告

图例 7.在 EURUSD H1,2015-2020 年,排除 2019 年,星期三,SingleBar EA 的交易报告

需要更多能的技术来解决此问题,而 OLAP 只是多种所需工具中的一种。 甚或,寻找更复杂的(多因子)形态才是更有意义的。 我们尝试创建一种不旦考虑时间周期,而且考虑先前柱线方向的交易策略。

基于 OLAP 报价分析构建交易策略。 第 2 部分

可以假设,每根柱线的方向在某种程度上可以取决于前一根的方向。 这种依赖性很可能类似于前一章节中检测到的,与日内和周内波动有关联的周期性特征。 换言之,除了在 OLAP 分析中按时辰和周内日辰来累积柱线尺寸和方向之外,还必须以某种方式考虑前一根柱线的特性。 我们为此利用其余的自定义字段。

在第三个自定义字段中,将计算两根相邻柱线的“不对称”协方差。 普通协方差的计算公式是柱线内价格走势范围的乘积,考虑其方向(加号代表增长,减号代表降低),没有特殊的预测值,因为前一根和下一根柱线所得到的协方差数值相等。 但是,交易决策仅对下一根柱线有效,尽管它们是基于前一根柱线制定的。 换句话说,由于前一根柱线已成为历史,故前一根柱线的较大走势导致的高协方差业已产生。 这就是为什么我们尝试运用“不对称”协方差公式的原因,其中仅考虑下一根柱线的范围,伴同前一根柱线乘积的符号。

该字段允许测试两种策略:趋势和反转。 例如,如果我们在此字段中使用利润因子聚合器,则值大于 1 表示沿前一根柱线方向进行的交易是能盈利的;小于 1 的数值则表示逆向可有盈利。 如同以前的计算,极值(远大于 1 或远小于 1)分别意味着趋势或反转操作将有更高盈利。

在第四个自定义字段中,我们将保存相邻柱线是否同向(+1)或是异向(-1)的符号。 因此,我们能够使用聚合器确定相邻柱线反转的数量,以及趋势和反转策略的入场成效。

由于柱线始终按时间顺序进行分析(此顺序由适配器提供),因此我们可以将先前计算所需的柱线大小和跨度保存在静态变量中。 当然,只需用到一个报价适配器单例即可完成此操作(默认情况下,该实例在头文件中创建)。 这很适合我们的示例,并且更易于理解。 只是,通常适配器只应传递自定义记录构造函数(例如 CustomQuotesBaseRecord),再传递给 fillCustomFields 方法某个容器,该容器允许保存和还原状态,例如,数组的引用:fillCustomFields(double &bundle[])。

  class CustomQuotesRecord: public QuotesRecord
  {
    private:
      static double previousBarRange;
      static double previousSpread;
      
    public:
      // ...
      
      virtual void fillCustomFields() override
      {
        const double newBarRange = get(FIELD_PRICE_RANGE_OC);
        const double spread = get(FIELD_SPREAD);
  
        // ...
  
        if(MathAbs(previousBarRange) > previousSpread)
        {
          double mult = newBarRange * previousBarRange;
          double value = MathSign(mult) * MathAbs(newBarRange);
  
          // this is an attempt to approximate average losses due to spreads
          value += MathSignNonZero(value) * -1 * MathMax(spread, previousSpread);
          
          set(FIELD_CUSTOM3, value);
          set(FIELD_CUSTOM4, MathSign(mult));
         }
        else
        {
          set(FIELD_CUSTOM3, 0);
          set(FIELD_CUSTOM4, 0);
         }
  
        previousBarRange = newBarRange;
        previousSpread = spread;
       }
      
  };

应修改 OLAPQTS 的输入值。 主要修改涉及 AggregatorField 中 “custom 3” 的选择。 以下参数保持不变:按 X 和 Y 选择器,聚合器类型(PF)和排序。 此外,日期过滤器也被修改。

  • SelectorX=hour-of-day
  • SelectorY=day-of-week
  • Filter1=field
  • Filter1Field=datetime
  • Filter1Value1=2018.01.01
  • Filter1Value2=2019.01.01
  • AggregatorType=Profit Factor
  • AggregatorField=custom 3
  • SortBy=value (descending)

正如我们分析 2015 年开始的报价时已经看到的那样,选择更长的时间段更适合旨在确定周期性的系统 — 它对应于 month-of-year(年内月份)选择器。 在我们的示例中,我们使用时辰和日辰选择器,我们的分析仅限于 2018 年,然后从 2019 年进行正向验证测试。

  OLAPQTS (EURUSD,H1)	Bars read: 137642
  OLAPQTS (EURUSD,H1)	Aggregator: ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM3 [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1514764800.0 ... 1546300800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 6203
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 2.65010 "23; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  1] 2.37966 "03; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  2] 2.33875 "04; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  3] 1.96317 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  4] 1.91188 "18; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [  5] 1.89293 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 1.87159 "12; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  7] 1.78903 "15; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  8] 1.74461 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  9] 1.73821 "13; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [ 10] 1.73244 "14; 2`Tuesday"
  ...  
  OLAPQTS (EURUSD,H1) [110] 0.57331 "22; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [111] 0.51515 "07; 5`Friday"   
  OLAPQTS (EURUSD,H1) [112] 0.50202 "05; 5`Friday"   
  OLAPQTS (EURUSD,H1) [113] 0.48557 "04; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [114] 0.46313 "23; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [115] 0.44182 "00; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [116] 0.40907 "13; 1`Monday"   
  OLAPQTS (EURUSD,H1) [117] 0.38230 "10; 1`Monday"   
  OLAPQTS (EURUSD,H1) [118] 0.36296 "22; 5`Friday"   
  OLAPQTS (EURUSD,H1) [119] 0.29462 "17; 5`Friday"   

我们来创建另一个智能交易系统 NextBar,以便测试在 “custom 3” 字段中实现的策略。 利用该 EA,我们可以在策略测试器中验证发现的交易机会。 一般的智能交易系统结构类似于 SingleBar:使用相同的参数、函数和代码片段。 交易逻辑越发复杂,您可以在附代的源文件中查看它。

我们选择最有吸引力的时辰组合(PF 为 2 和更高,或 0.5 和更低),例如星期一:

  • PositiveHours=23,3
  • NegativeHours=10,13
  • ActiveDayOfWeek=1

在 2018.01.01-2019.05.01 范围内运行测试:

图例 8.在 EURUSD H1,01.01.2018-01.05.2019 时间段,OLAP 分析 2018 年之后,NextBar EA 的交易报告

图例 8.在 EURUSD H1,01.01.2018-01.05.2019 时间段,OLAP 分析 2018 年之后,NextBar EA 的交易报告

该策略在 2019 年 1 月仍然成功运作,之后则开始了一连串亏损。 我们需要以某种方式找出形态的存活跨度,并学习如何随时随地更改它们。

基于 OLAP 报价分析的自适应交易

迄今为止,我们一直在利用特殊的非交易 EA OLAPQTS 进行 OLAP 分析,同时利用单独开发的 EA 测试单独的假想。 更加合理和便捷的解决方案是在智能交易系统中内置 OLAP 引擎。 故此,机器人能够以给定的周期自动分析报价,并调整交易时间表。 另外,通过在 EA 中实现的主要参数,我们可用一种方法来优化它们,该方法可以模拟上述的正向验证技术。 EA 称为 OLAPQRWF,它是 OLAP Quotes with Rolling Walk-Forward 的缩写。

智能交易系统的主要输入:

  input int BarNumberLookBack = 2880; // BarNumberLookBack (week: 120 H1, month: 480 H1, year: 5760 H1)
  input double Threshold = 2.0; // Threshold (PF >= Threshold && PF <= 1/Threshold)
  input int Strategy = 0; // Strategy (0 - single bar, 1 - adjacent bars)

  • BarNumberLookBack 设置历史柱线数量,将据其执行 OLAP 分析(此处假定为 H1 时间帧)。
  • Threshold 是启动成交的利润因子阈值。
  • Strategy 是已测试策略的编号(当前,我们有两种策略:0 — 独立柱线的方向统计,1 — 两根相邻柱线的方向统计)。

另外,我们需要指定重新计算 OLAP 数据块的频率。

  enum UPDATEPERIOD
  {
    monthly,
    weekly
  };
  
  input UPDATEPERIOD Update = monthly;

除了策略之外,我们还可以选择用于计算聚合器的自定义字段。 计算字段 1 和 3 时要考虑到柱线范围(分别针对策略 0 和 1),而字段 2 和 4 仅需考虑每个方向上的柱线数。

  enum CUSTOMFIELD
  {
    range,
    count
  };
  
  input CUSTOMFIELD CustomField = range;

CustomQuotesRecord 类是从 OLAPQTS 继承而来。 先前用于配置选择器、过滤器和聚合器的所有参数均设置为常量或全局变量(它们需要根据策略进行更改),而无需更改其名称。

  const SELECTORS SelectorX = SELECTOR_DAYHOUR;
  const ENUM_FIELDS FieldX = FIELD_DATETIME;
  
  const SELECTORS SelectorY = SELECTOR_WEEKDAY;
  const ENUM_FIELDS FieldY = FIELD_DATETIME;
  
  const SELECTORS SelectorZ = SELECTOR_NONE;
  const ENUM_FIELDS FieldZ = FIELD_NONE;
  
  const SELECTORS _Filter1 = SELECTOR_FILTER;
  const ENUM_FIELDS _Filter1Field = FIELD_INDEX;
        int _Filter1value1 = -1; // to be filled with index of first bar to process
  const int _Filter1value2 = -1;
  
  const AGGREGATORS _AggregatorType = AGGREGATOR_PROFITFACTOR;
        ENUM_FIELDS _AggregatorField = FIELD_CUSTOM1;
  const SORT_BY _SortBy = SORT_BY_NONE;

请注意,柱线将用 FIELD_INDEX 按数量进行过滤,而不是按时间过滤。 _Filter1value1 的实际值将计算为柱线总数与指定的 BarNumberLookBack 之间的差值。 因此,EA 将始终计算最后的 BarNumberLookBack 柱线数。

EA 将在 OnTick 处理程序以柱线模式进行交易。

  bool freshStart = true;
  
  void OnTick()
  {
    if(!isNewBar()) return;
    
    if(Bars(_Symbol, _Period) < BarNumberLookBack) return;
    
    const int m0 = TimeMonth(iTime(_Symbol, _Period, 0));
    const int w0 = _TimeDayOfWeek();
    const int m1 = TimeMonth(iTime(_Symbol, _Period, 1));
    const int w1 = _TimeDayOfWeek();
    
    static bool success = false;
    
    if((Update == monthly && m0 != m1)
    || (Update == weekly && w0 < w1)
    || freshStart)
    {
      success = calcolap();
      freshStart = !success;
     }
  
    //...
   }

根据分析频率,等待月份或周份变化,并在 “calcolap” 函数中运行 OLAP。

  bool calcolap()
  {
    _Filter1value1 = Bars(_Symbol, _Period) - BarNumberLookBack;
    _AggregatorField = Strategy == 0 ? (ENUM_FIELDS)(FIELD_CUSTOM1 + CustomField) : (ENUM_FIELDS)(FIELD_CUSTOM3 + CustomField);
  
    _defaultQuotesAdapter.reset();
    const int processed =
    _defaultEngine.process(_selectorArray, _selectorField,
          _AggregatorType, _AggregatorField,
          stats,                              // custom display object
          _SortBy,
          _Filter1value1, _Filter1value2);
    
    return processed == BarNumberLookBack;
   }

这部分代码已经很熟悉了。 有几处修改涉及根据输入参数选择聚合字段,以及设置第一根分析柱线的索引。

另一个重要的变化意味着利用特殊显示对象(stats),OLAP 引擎将在执行分析后调用它显示对象。

  class MyOLAPStats: public Display
  {
    // ...
    public:
      virtual void display(MetaCube *cube, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
      {
        // ...
       }
  
      void trade(const double threshold, const double lots, const int strategy = 0)
      {
        // ...
       }
  };
  
  MyOLAPStats stats;

鉴于此对象将从获得的统计信息中确定最佳交易时间,因此通过保留的 “trade” 方法将交易委托给同一对象很方便。 所以,将以下内容添加到 OnTick:

  void OnTick()
  {
    // ...

    if(success)
    {
      stats.trade(Threshold, Lot, Strategy);
     }
    else
    {
      OrdersCloseAll();
     }
   }

现在,我们㡱 MyOLAPStats 类的细节。 OLAP 分析结果由 “display” 方法处理(display 的主要虚拟方法)和 saveVector(辅助)。

  #define N_HOURS   24
  #define N_DAYS     5
  #define AXIS_HOURS 0
  #define AXIS_DAYS  1
  
  class MyOLAPStats: public Display
  {
    private:
      bool filled;
      double index[][3]; // value, hour, day
      int cursor;
  
    protected:
      bool saveVector(MetaCube *cube, const int &consts[], const SORT_BY sortby = SORT_BY_NONE)
      {
        PairArray *result = NULL;
        cube.getVector(0, consts, result, sortby);
        if(CheckPointer(result) == POINTER_DYNAMIC)
        {
          const int n = ArraySize(result.array);
          
          if(n == N_HOURS)
          {
            for(int i = 0; i < n; i++)
            {
              index[cursor][0] = result.array[i].value;
              index[cursor][1] = i;
              index[cursor][2] = consts[AXIS_DAYS];
              cursor++;
             }
           }
          
          delete result;
          return n == N_HOURS;
         }
        return false;
       }
  
    public:
      virtual void display(MetaCube *cube, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
      {
        int consts[];
        const int n = cube.getDimension();
        ArrayResize(consts, n);
        ArrayInitialize(consts, 0);
  
        filled = false;
        
        ArrayResize(index, N_HOURS * N_DAYS);
        ArrayInitialize(index, 1);
        cursor = 0;
  
        if(n == 2)
        {
          const int i = AXIS_DAYS;
          int m = cube.getDimensionRange(i); // should be 5 work days
          for(int j = 0; j < m; j++)
          {
            consts[i] = j;
            
            if(!saveVector(cube, consts, sortby)) // 24 hours (values) per current day
            {
              Print("Bad data format");
              return;
             }
            
            consts[i] = 0;
           }
          filled = true;
          ArraySort(index);
          ArrayPrint(index);
         }
        else
        {
          Print("Incorrect cube structure");
         }
       }
      
      //...
  };

此类描述的是二维数组 “index”。 它存储与时间表相关的性能数值。 在 “display” 方法中,此数组顺序填充来自 OLAP 数据块=的向量。 辅助 saveVector 函数可复制特定交易日所有 24 小时的数字。 数值、时辰和日辰依次写在 “index” 的第二维中。 数值位于第一个(0)元素中,该元素允许按利润因子对数组进行排序。 基本上,在日志中这些可提供一个方便的视图。

根据 “index” 数组的值选择交易模式。 相应地,若 PF 高于日内时辰和周内日辰相应的阈值,交易订单才会被发送。

    void trade(const double threshold, const double lots, const int strategy = 0)
    {
      const int h = TimeHour(lastBar);
      const int w = _TimeDayOfWeek() - 1;
    
      int mode = 0;
      
      for(int i = 0; i < N_HOURS * N_DAYS; i++)
      {
        if(index[i][1] == h && index[i][2] == w)
        {
          if(index[i][0] >= threshold)
          {
            mode = +1;
            Print("+ Rule ", i);
            break;
           }
          
          if(index[i][0] <= 1.0 / threshold)
          {
            mode = -1;
            Print("- Rule ", i);
            break;
           }
         }
       }
      
      // pick up existing orders (if any)
      const int direction = CurrentOrderDirection();
      
      if(mode == 0)
      {
        if(direction != 0)
        {
          OrdersCloseAll();
         }
        return;
       }
      
      if(strategy == 0)
      {
        if(direction != 0) // there exist open orders
        {
          if(mode == direction) // keep direction
          {
            return; // existing trade goes on
           }
          OrdersCloseAll();
         }
        
        const int type = mode > 0 ? OP_BUY : OP_SELL;
        
        const double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
        const double sl = StopLoss > 0 ? (type == OP_BUY ? p - StopLoss * _Point : p + StopLoss * _Point) : 0;
          
        OrderSend(_Symbol, type, Lot, p, 100, sl, 0);
       }
      // ...
     }

在此,我仅展示了一种交易策略,其代码已在第一个测试智能交易系统里用到。 完整的源代码随附如下。

我们在 2015 年至 2019 年的时间段内优化 OLAPQRWF,然后从 2019 年进行正向验证测试。 请注意,优化的思路是找到可交易的元参数:OLAP 分析的持续时间,OLAP 数据块重建的频率,策略的选择和自定义聚合字段。 在每次优化运行中,EA 均基于 _historical data_ 构建 OLAP 数据块,并采用 _past_ 中的设置进行虚拟 _future_ 交易。 为什么在这种情况下我们需要进行正向验证测试? 在此,交易效率直接取决于指定的元参数,这就是为什么超出样本间隔之外验证所选设置的适用性之所以重要的原因。

我们来优化所有影响分析的参数,Update 周期除外(保留按月份):

  • BarNumberLookBack=720||720||480||5760||Y
  • Threshold=2.0||2.0||0.5||5.0||Y
  • Strategy=0||0||1||1||Y
  • Update=0||0||0||1||N
  • CustomField=0||0||0||1||Y

EA 计算合成的自定义优化值,该值等于锋锐(Sharpe)比率与交易数量的乘积。 根据此数值,采用以下输入参数生成最佳预测:

  • BarNumberLookBack=2160
  • Threshold=3.0
  • Strategy=0
  • Update=monthly
  • CustomField=count

我们从 2015 年到 2020 年进行单独的测试,并标记其在正向验证周期的行为。

图例 9 在 EURUSD H1,OLAP 分析优化窗口 2018 年(含)之后,从 01.01.2015 至 01.01.2020 的 OLAPQRWF EA 报告

图例 9 在 EURUSD H1,OLAP 分析优化窗口 2018 年(含)之后,从 01.01.2015 至 01.01.2020 的 OLAPQRWF EA 报告

可以得出结论,利用前几年发现的聚合窗口尺寸,智能交易系统自动确定了盈利时间,并在 2019 年交易成功。 当然,这个系统需要进一步的研究和分析。 无论如何,该工具已被确认能够运行。

结束语

在本文中,我们改进和扩展了 OLAP 函数库的功能(用于在线数据处理),并以特殊的适配器和含有报价领域的记录处理类实现了将其捆绑。 利用所研讨的程序,可以分析历史,并判断可盈利交易的形态。 在第一阶段,当您熟悉 OLAP 分析后,使用单独的非交易智能交易系统更为方便,该智能交易系统仅处理源数据,并提供宽泛的统计信息。 而且,此类 EA 允许开发并调试算法来计算包含交易策略(假设)基本元素的自定义字段。 在进一步的 OLAP 研究步骤中,该引擎会与现有的、或新的交易机器人集成在一起。 在这种情况下,EA 优化不仅应考虑公用操作参数,而且还应考虑与 OLAP 有关联、并影响其统计集合的全新元参数。

当然,OLAP 工具不是万能灵药,特别是对于难以预测的行情状况。 因此,它们不能作为“开箱即用”的圣杯。 尽管如此,内置的报价分析无疑扩展了可能性,令交易者可以搜索新策略,并创建新的智能交易系统。

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

附加的文件 |
MQLOLAP3.zip (66.21 KB)
轻松快捷开发 MetaTrader 程序的函数库 (第 三十二部分) :延后交易请求 - 在特定条件下挂单 轻松快捷开发 MetaTrader 程序的函数库 (第 三十二部分) :延后交易请求 - 在特定条件下挂单

我们继续功能开发,允许用户利用延后请求进行交易。 在本文中,我们将实现在特定条件下挂单的功能。

轻松快捷开发 MetaTrader 程序的函数库 (第 三十一部分) :延后交易请求 - 在特定条件下开仓 轻松快捷开发 MetaTrader 程序的函数库 (第 三十一部分) :延后交易请求 - 在特定条件下开仓

从本文开始,我们将开发一种功能,允许用户在特定条件下利用延后请求进行交易,举例来说,当达到特定时间限制、超出指定利润或由止损平仓时。

轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓 轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓

我们继续开发利用延后请求进行交易的函数库功能。 我们已实现了发送开仓和下挂单的条件交易请求。 在本文中,我们将实现条件平仓 – 全部、部分和由逆向仓位平仓。

如何在 MetaTrader 5 中利用 DirectX 创建 3D 图形 如何在 MetaTrader 5 中利用 DirectX 创建 3D 图形

3D 图形为大数据分析提供了完美的方案,它可以直观透视隐藏的形态。 这些任务能以 MQL5 直接解决,而 DireсtX 函数允许创建三维物体。 故其能够为 MetaTrader 5 创建任意复杂度的程序,甚至 3D 游戏。 学习 3D 图形,从绘制简单的三维形状开始。