自定义品种(符号):实践基础

Stanislav Korotky | 22 十二月, 2020

MetaTrader 5 具有创建自有报价和即时报价的自定义品种(符号)的功能。 它们可以通过 MQL API 终端接口和程序级别对其进行访问。 自定义品种可显示于标准图表上,允许应用指标,标记对象,甚至基于这些品种创建交易策略。

自定义品种可采用经纪商提供的真实品种报价,或外部资源作为数据源。 在本文中,我们将研究几种流行的转换操作品种的方法,这为交易者提供了其他分析工具:

此外,我们将开发一种自适应智能交易系统机制,可以交易真实品种,其与运行 EA 的图表上的衍生自定义品种相关。

在本文中,源(标准)品种图采用黑色背景,而自定义品种图采用白色背景。

等量/范围图表

等量图表是基于相邻柱线交易量相等原理的柱线图表。 在常规图表上,每根新柱线均以指定的时间间隔形成,其与时间帧大小相匹配。 在等量图表上,当价格变动或实际交易量的总和达到预设值时,每根柱线均视为已形成。 之后,程序开始计算下一根柱线的额度。 当然,在计算交易量时价格走势依然受控,因此您会在图表上得到通常的四个价格值:开盘价、最高价、最低价和收盘价。

尽管等量图表上的水平轴仍然表示时序,但每根柱线的时间戳是任意的,且取决于每个时间区间的波动性(交易的数量或大小)。 许多交易者认为,与固定时间帧相比,这种柱形法方法能更准确地描述不断变化的行情。

不幸的是,MetaTrader 4/5 都未提供开箱即用的等量图表。 它们应以特殊方式生成。

在 MetaTrader 4 当中,这可以利用离线图表来达成。 等量图表复查中介绍了该方法。

而在 MetaTrader 5 当中,相同的算法可利用自定义品种来实现。 为了简化任务,我们借用指定文章中的非交易型智能交易系统,并令其与 MetaTrader 5 MQL API 适配。

原始文件 EqualVolumeBars.mq4 已重命名为 EqualVolumeBars.mq5,并对其进行了少许修改。 特别是,描述输入参数的 'extern' 关键字已替换为 'input'。 用一个 StartDate 参数取代了 StartYear 和 StartMonth 两个参数。 在 MetaTrader 4 中设置非标准时间帧的 CustomPeriod 参数现在不需要了,故将其删除。

请注意,MetaTrader 4 的交易量是全部即时报价的交易量,即,它们代表柱线内即时报价的数量(价格变动次数)。 原本的想法是处理 M1 柱线(带有其即时报价量),或含有其他经纪商提供的即时报价的外部 csv 文件,为了计数每次输入的即时报价,以及一旦达到指定的即时报价次数后,立即形成新的等量柱线。 柱线被写入一个 hst 文件,该文件可以在 MetaTrader 4 中作为脱机图表打开。

读取 csv 文件并写入 hst 文件的相关代码在 MetaTrader 5 中已不再需要。 取而代之,我们可用自定义品种 API 读取真实的即时报价历史记录,并形成柱线。 此外,MetaTrader 5 允许经纪商提供真实的交易量和即时报价(针对交易所金融产品,但它们通常不适用于外汇金融产品)。 如果启用此模式,则构建等量柱线时可以不依据即时报价次数,而是依据实际交易量。

FromM1 输入参数判断 EA 是否需要处理 M1 柱线(默认为 “true”),亦或是即时报价历史记录(“false”)。 开始处理即时报价时,请勿选择距离太远的起点,因为这可能需要大量时间和磁盘空间。 如果您已采用即时报价记录操作,那么您要了解您的 PC 能力和可用资源。

以相同的方式绘制等范围柱线。 然而,当价格覆盖了指定的点数时,将在此处开立一根新的柱线。 请注意,这些柱线仅在即时报价模式下可用(FromM1 == false)。

Chart type — EqualTickVolumes, EqualRealVolumes, RangeBars — 由 WorkMode 输入参数设置。

最方便的方法是利用 Symbol 函数(由 fxsaber 开发)来操控自定义品种。 可用 #include 指令将其连接到智能交易系统:

  #include <Symbol.mqh>

现在,我们可以基于当前图表品种创建自定义品种。 如下完成:

  if(!SymbolSelect(symbolName, true))
  {
    const SYMBOL Symb(symbolName);
    Symb.CloneProperties(_Symbol);
    
    if(!SymbolSelect(symbolName, true))
    {
      Alert("Can't select symbol:", symbolName, " err:", GetLastError());
      return INIT_FAILED;
    }
  }

其中 symbolName 是含有自定义品种名称的字符串。

与所有自定义品种管理相关的初始化片段,和许多其他辅助任务(特别是重置现有历史记录,使用新的自定义品种打开图表)均会以类似方式在所有程序中执行。 您可以在下面的附件中查看相关的源代码。 我将在本文中忽略它们,因为它们只是次要内容。

当出现一根新的等量柱线,或当前的等量柱线变化时,将调用 WriteToFile 函数。 经调用 MetaTrader 5 中的 CustomRatesUpdate 来实现此函数:

  void WriteToFile(datetime t, double o, double l, double h, double c, long v, long m = 0)
  {
    MqlRates r[1];
    
    r[0].time = t;
    r[0].open = o;
    r[0].low = l;
    r[0].high = h;
    r[0].close = c;
    r[0].tick_volume = v;
    r[0].spread = 0;
    r[0].real_volume = m;
    
    int code = CustomRatesUpdate(symbolName, r);
    if(code < 1)
    {
      Print("CustomRatesUpdate failed: ", GetLastError());
    }
  }

令人惊讶的是,M1 柱线周期(FromM1 = true 模式)与 MQL4 版本几乎相同,这意味着只需修改 WriteToFile 函数,我们就可以得到 MQL5 版本的 M1 柱线函数代码。 唯一需要更改的部分是 RefreshWindow 中即时报价的生成。 在 MetaTrader 4 之中,这是通过发送 Windows 消息来模拟脱机图表上的即时报价柱线来完成的。 MetaTrader 5 则利用 CustomTicksAdd 函数:

  void RefreshWindow(const datetime t)
  {
    MqlTick ta[1];
    SymbolInfoTick(_Symbol, ta[0]);
    ta[0].time = t;
    ta[0].time_msc = ta[0].time * 1000;
    if(CustomTicksAdd(symbolName, ta) == -1)
    {
      Print("CustomTicksAdd failed:", GetLastError(), " ", (long) ta[0].time);
      ArrayPrint(ta);
    }
  }

即时报价生成会在自定义品种图表上调用 OnTick 事件,其允许在此类图表上运行智能交易系统进行交易。 不过,这项技术需要采取一些额外的措施,我们稍后再加以研究。

从即时报价历史中生成等量柱线的模式(FromM1 = false)要复杂一些。 这需要调用标准 CopyTicks/CopyTicksRange 函数读取真实的即时报价。 所有这些功能都已在 TicksBuffer 类中实现。

  #define TICKS_ARRAY 10000
  
  class TicksBuffer
  {
    private:
      MqlTick array[];
      int tick;
    
    public:
      bool fill(ulong &cursor, const bool history = false)
      {
        int size = history ? CopyTicks(_Symbol, array, COPY_TICKS_ALL, cursor, TICKS_ARRAY) : CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, cursor);
        if(size == -1)
        {
          Print("CopyTicks failed: ", GetLastError());
          return false;
        }
        else if(size == 0)
        {
          if(history) Print("End of CopyTicks at ", (datetime)(cursor / 1000));
          return false;
        }
        
        cursor = array[size - 1].time_msc + 1;
        tick = 0;
      
        return true;
      }
      
      bool read(MqlTick &t)
      {
        if(tick < ArraySize(array))
        {
          t = array[tick++];
          return true;
        }
        return false;
      }
  };

在 TICKS_ARRAY 片段的 “fill” 方法中请求即时报价,然后将其添加到“数组”中,然后调用 “read” 方法逐一读取。 该方法所实现的操控即时报价历史记录的算法类似于 M1 历史记录柱线(附件中提供了完整的源代码)。

    TicksBuffer tb;
    
    while(tb.fill(cursor, true) && !IsStopped())
    {
      MqlTick t;
      while(tb.read(t))
      {
        ...
        // New or first bar
        if(IsNewBar() || now_volume < 1)
        {
          WriteToFile(...);
        }
      }
    }

每次启动智能交易系统时,它都会调用 “Reset” 函数清除指定自定义品种的现有历史记录(如果存在的话)。 如有必要,可改进此行为,即保存历史记录,并在前一此生成结束的位置继续生成柱线。

您可以比较 EqualVolumeBars.mq4 和生成的 EqualVolumeBars.mq5 的源代码。

我们来看看新的智能交易系统如何工作。 这是运行 EA 的 EURUSD H1 图表:

MetaTrader 5 中 EURUSD H1 图表上的 EqualVolumeBars 智能交易系统

MetaTrader 5 中 EURUSD H1 图表上的 EqualVolumeBars 智能交易系统

以下是由 EA 创建的等量柱线图表,其中每根柱线包含 1000 个即时报价。

由 MetaTrader 5 的 EqualVolumeBars EA 生成的 EURUSD 等量柱线图表,每根柱线内含 1000 个即时报价

由 MetaTrader 5 的 EqualVolumeBars EA 生成的 EURUSD 等量柱线图表,每根柱线内含 1000 个即时报价

请注意,除最后一根仍在形成中的柱线外(即时报价计数仍在继续),所有柱线的即时报价数量均相等。

我们来检查另一种操作模式 - 等范围图表。 以下图表由内含 100 点数波动的柱线组成。

由 MetaTrader 5 中 EqualVolumeBars EA 生成的 EURUSD 等范围图表,每根柱线含 100 点

由 MetaTrader 5 中 EqualVolumeBars EA 生成的 EURUSD 等范围图表,每根柱线含 100 点

此外,EA 允许针对交易所金融产品使用实际交易量模式:

LKOH 原始图表,其每根柱线实际交易量为 10000,由 MetaTrader 5 中 EqualVolumeBars EA 生成

LKOH 等量图表,其每根柱线实际交易量为 10000,由 MetaTrader 5 中 EqualVolumeBars EA 生成

LKOH 原始 (a) 和等量图表,其每根柱线实际交易量为 10000,由 MetaTrader 5 中 EqualVolumeBars EA 生成

运行 EA 时品种的时间帧并不重要,因为进行计算时,总是取 M1 柱线,或即时报价历史记录的数据。

自定义品种图表的时间帧必须等于 M1(终端中最小的可用时间帧)。 因此,柱线的时间通常与它们的形成时刻紧密对应。 然而,在强劲的行情变化中,当即时报价的次数或交易量的大小每分钟形成若干根柱线时,柱线的时间将领先于真实柱线。 当行情平静下来时,等量柱线的时间标记将恢复正常。 对于等量或等范围柱线,此平台限制可能并不是特别重要,因为此类图表的初衷就是将它们与绝对时间解除绑定。

即使报价图表

在 MetaTrader 5 中的“市场观察”窗口中提供了即时报价图表。 出于某些原因,其实现方式与常规图表不同。 它显示的即时报价次数有限(据我所知,最多 2000 个),它很小,且无法扩展到全屏,并缺少标准图表通常提供的所有功能,诸如加载指标、对象和智能交易系统。

在 MetaTrader 5 市场观察窗口中的即时报价图表

在 MetaTrader 5 市场观察窗口中的即时报价图表

那么为什么标准分析工具不支持即时报价,而 MetaTrader 5 却提供了对实际即时报价的本地支持,并提供了高频交易(HFT)功能? 一些交易员认为即时报价影响力太小,甚至是噪音。 另一些交易者试图由即时报价来赚取利润。 因此,在拥有缩放功能的标准图表中显示即时报价,从而可应用模板和事件来运用智能交易系统也许会很有益处。 可借助自定义品种功能来实现。

同样,我们可利用已知的 MQL AP I函数,如 CopyTicks 和 CustomRatesUpdate。 借助它们,我们可以轻松实现一个非交易型智能交易系统,其基于当前图表品种生成自定义品种。 此处,自定义品种历史记录中的每根 M1 柱线都是一个单独的即时报价。 源代码作为示例,Ticks2Bars.mq5 文件附于文后。 例如,如果您在 EURUSD 图表(任何时间帧)上运行智能交易系统,它将创建 EURUSD_ticks 品种。

EA 的输入如下:

主要操作由 “apply” 函数执行:

  bool apply(const datetime cursor, const MqlTick &t, MqlRates &r)
  {
    static MqlTick p;
    
    // eliminate strange things
    if(t.ask == 0 || t.bid == 0 || t.ask < t.bid) return false;
    
    r.high = t.ask;
    r.low = t.bid;
    
    if(t.last != 0)
    {
      if(RenderBars == OHLC)
      {
        if(t.last > p.last)
        {
          r.open = r.low;
          r.close = r.high;
        }
        else
        {
          r.open = r.high;
          r.close = r.low;
        }
      }
      else
      {
        r.open = r.close = (r.high + r.low) / 2;
      }
      
      if(t.last < t.bid) r.low = t.last;
      if(t.last > t.ask) r.high = t.last;
      r.close = t.last;
    }
    else
    {
      if(RenderBars == OHLC)
      {
        if((t.ask + t.bid) / 2 > (p.ask + p.bid) / 2)
        {
          r.open = r.low;
          r.close = r.high;
        }
        else
        {
          r.open = r.high;
          r.close = r.low;
        }
      }
      else
      {
        r.open = r.close = (r.high + r.low) / 2;
      }
    }
    
    r.time = cursor;
    r.spread = (int)((t.ask - t.bid)/_Point);
    r.tick_volume = 1;
    r.real_volume = (long)t.volume;
  
    p = t;
    return true;
  }

在此函数中,存储当前时刻“光标”的 MqlTick 结构字段,导入到 MqlRates 结构字段当中,然后将其写入历史记录。

下图示意自定义品种的即时报价柱线图表(作为比较,还显示了标准即时报价图表):

MetaTrader 5 中功能齐全的 EURUSD 即时报价图表

MetaTrader 5 中功能齐全的 EURUSD 即时报价图表

这个图标我们可以运用指标、对象或智能系统来进行自动化报价分析和交易。

请注意,即时报价柱线图表上的柱线时间是虚构的。 如果启用了 “LoopBack” 模式,则最后一根柱线的当前时间始终会精确到一分钟,而前一根柱线则相距一分钟(这是 MetaTrader 5 中的最小时间帧)。 如果禁用 “LoopBack” 模式,则自智能系统启动时间开始,柱线时间将按一分钟递增,因此所有超出初始极限的柱线都处于虚拟的未来。

然而,最右边的 M1 柱线对应于最近的价格和当前的“收盘”(或“最后”)价格。 这样就可于图表上运行智能交易系统了,联线和测试器两者均可。 为了联线操作,EA 需要略微进行修改,因为它必须能够交易来自 “XY_ticks” 品种图表中的原始 XY 品种(自定义品种仅存在于终端中,而在服务器上不认识该品种)。 在上面的示例中,所有交易订单中的 “EURUSD_ticks” 应替换为 “EURUSD”。

如果智能交易系统从指标接收到交易信号,那么它也许足以在自定义品种图表上创建其实例,从而取代在当前品种上操作,并可在此正操作的品种图表上运行 EA。 但是这种方法并非一直适用。 为了令智能交易系统适配自定义品种,以后会讲述另一种方法。

操控即时报价图表时会出现某些困难,事实上这与它们更新非常迅速有关。 有因于此,几乎不可能手动分析和标记图表 - 必须借助指标或脚本自动进行所有操作。

提议的带有“即时报价”的方法可以测试剥头皮策略,而无需在内部缓冲区中积累特殊数据,也无需基于缓冲区计算信号,而我们可以简单地利用常规指标或对象。

时间平移和烛条变形

许多交易者在实践中利用烛太形态作为主要或附加信号。 该方法直观上具有启发性,但它也有重要的缺点。

烛条形态描述的是一系列柱线的预定义几何形状。 所有柱线都是价格随时间变化而形成的。 然而,时间本质上是连续的,尽管图表的时间表达人为地分为与柱线相对应的段,和与特定时区对齐的时间(由经纪商选择)。 例如,如果将 H1 柱线图表平移几分钟(譬如说 15 分钟),则柱线的几何形状很可能会发生变化。 结果就是,早前存在的形态可能会完全消失,而新的形态将会在其他地方形成。 不过,价格行为是相同的。

如果您查看一些流行的烛条形态,您可轻松发现它们是由相似的价格走势形成的,且它们的外观差异是由柱线计算开始时间引起的。 例如,如果将时间轴平移半根柱线,则“锤子”可以转换成“尖刺”,而“吊死鬼”可以转换成“乌云盖顶”。 取决于平移值和局部价格变化,该形态也可能变成看跌或看涨的“吞噬”。 如果您切换到较低的时间帧,上述所有形态都可能成为“晨十字星”或“昏十字星”。

换言之,每个形态都是从时间和尺度计算开始的函数。 特别是,通过更改起点,我们可以检测到一个图形,并截取所有与之等效的其他图形。

逆转形态(在交易者中很流行,因为它们能够判断走势的开始)可按以下简化形式表示:

价格逆转和等效的烛条结构

价格逆转和等效的烛条结构

该图示意向上逆转和向下逆转的规划图,它们每个都有两个变体柱线配置。 早前我们已经看到,可以有更多具有类似含义的形态。 跟踪所有这些形态是不合理的。 更为便利的解决方案是能够从任何时间开始,确定烛条形态。

甚至,对于其他某个时区,时间转换也许能相对于 GMT 表达一个可接受的偏移,这从理论上讲意味着这些新的烛条形态也应在该新区域中起作用,就像在我们所处时区中形成的那样。 由于交易服务器遍布世界各地的所有时区,因此交易者肯定可以看到完全不同的信号。 每个信号都是有价值的。

我们可以得出这样的结论:烛条形态的扇形应根据起点考虑其可变性。 这是自定义品种派上用场的地方。

基于所操作品种的自定义信号允许构建带有时间戳的柱线和即时报价,并按指定值将其偏移到将来或过去。 该偏移值可以被解释为任何选定时间帧柱线的一部分。 价格和走势没有变化,但这仍然可以提供有趣的结果。 首先,它提供了烛条形态的检测,如果没有这种偏移,烛条形态将不会被注意到。 其次,我们实际上可以看到一根不完整的柱线。

例如,如果您将报价前移 5 分钟,则 M15 图表上的柱线开立和收盘将比源图表提前(在一个小时内的第 10、25、40、55 分钟)三分之一。 如果偏移不明显,则原始图表和自定义图表中的形态几乎相同,但自定义图表的信号(由柱线图计算,包括指标信号)会出现更早。

在 TimeShift.mq5 EA 中实现了这种时移自定义品种的创建。

平移值在源 Shift 参数中指定(以秒为单位)。 EA 利用即时报价操作,从而允许从 Start 参数指定的日期开始计算转换报价的历史记录。 然后,如果启用了 OnTick 事件生成模式(由 EmulateTicks 参数指定,默认为 true),则会联线处理即时报价。

时间转换以简单的方式进行,其针对历史报价和联线即时报价:例如,在后一种情况下使用 “add” 函数:

  ulong lastTick;
  
  void add()
  {
    MqlTick array[];
    int size = CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, lastTick + 1, LONG_MAX);
    if(size > 0)
    {
      lastTick = array[size - 1].time_msc;
      for(int i = 0; i < size; i++)
      {
        array[i].time += Shift;
        array[i].time_msc += Shift * 1000;
      }
      if(CustomTicksAdd(symbolName, array) == -1)
      {
        Print("Tick error: ", GetLastError());
      }
    }
  }
  
  void OnTick(void)
  {
    ...
    if(EmulateTicks)
    {
      add();
    }
  }

原始和修改后的 EURUSD H1 图表如下所示。

含 TimeShift 智能交易系统的 EURUSD H1 图表

含 TimeShift 智能交易系统的 EURUSD H1 图表

柱线平移半小时(30 分钟)后,图像改变。

EURUSD H1 自定义图表,平移了半小时

EURUSD H1 自定义图表,平移了半小时

那些熟悉烛条形态的人一定会注意到很多差别,包括原始图表中未出现的新信号。 因此,通过在自定义品种图表上应用常规的烛条指表,我们可得到双倍的警报和交易机会。 甚至可利用智能交易系统将该过程自动化,但在此处,我们需要教会 EA 从自定义品种图表中提取真实品种并交易。 本文末尾将研究此任务。 现在,我们来研究另一种自定义图表类型,它可能是最受欢迎的一种 - Renko。

Renko

非交易智能交易系统 RenkoTicks.mq5 将用于实现 Renko 图表。 EA 在处理真实即时报价(可从您的经纪商的 MetaTrader 5 中获得)时生成 renko 作为自定义品种报价。 我们可用任何源品种的报价(柱线)和 RenkoTicks 运行所在图表的时间帧。

生成 renko 时,针对自定义品种的备案可以是指标或绘图(使用对象,或在画布上),但是在两种情况下,都无法在生成的伪图表上使用指标或脚本。

所有的 renko 柱线都在 M1 时间帧内形成。 这是有意如此做的,因为有时可能会很快地(例如在高波动时间内)形成一个 renko 柱线,且两根柱线之间的时间应尽可能短。 一分钟是 MetaTrader 5 所支持的最小距离。 这就是为什么 renko 图表应始终具有 M1 时间帧的原因。 将 Renko 图表切换到另一个时间帧没有任何意义。 每根 1 分钟柱线开始的时间与相应形成的 renko 柱线开始时间匹配。 这种一分钟柱线的完成时间是假的,取而代之,您应该检查下一根一分钟柱线的开始时间。

不幸的是,有时必须在一分钟之内形成若干根 renko 柱线。 鉴于 MetaTrader 5 不允许这样做,因此 EA 会生成的柱线作为相邻 M1 柱线序列,从而每分钟人为地增加计数。 结果就是,renko 柱线的正式时间可能与实际时间不一致(可以提前)。 例如,如果 renko 大小为 100 个点,那么在 12:00:00 发生并持续 10 秒的 300 点波动将在 12:00:00、12:00:05、12:00:10 产生 renko 柱线。 取而代之,EA 将在 12:00、12:01、12:02 生成柱线。

在报价历史记录中发生这种情况时,可能会出现以下问题:从过去转移过来的此类 renko 柱线将与原始图表里最后形成的一根柱线重叠。 假设在 12:02 又发生了 100 点的走势,因此我们需要生成一个开立时间为 12:02 的 renko 柱线,但这个时间已经太忙了! 为了解决此类冲突,如果所需的计数已经很忙,则智能交易系统具有一种特殊模式,该模式会强制将下一根柱线的形成时间增加 1 分钟。 此模式由 SkipOverflows 参数设置,该参数默认情况下设置为 false(柱线不重叠,取而代之,在必要时移至将来)。 如果 SkipOverflows 为 true,则时间重叠的柱线会相互覆盖,且生成的 renko 不会完全正确。

请注意,这种状况可能会发生剧烈变化,并实时生成多个“超前”柱线 - 在这种情况下,柱线实际上会在将来某时刻形成! 在我们的示例中,开立时间为 12:00、12:01、12:02 的 renko 柱线将出现在12:00:10! 在分析和交易中应考虑到这一点。

有两种方式可以解决此问题,例如增加 renko 柱线的大小。 然而,它有一个明显的缺点 - 这会降低 renko 的精度, 即, 它记录的报价走势更粗糙,且产生更少的柱线。另一种可能的方式是打包(向左平移)老旧的柱线,但这其付出的代价是重绘指标或对象。

由于特定的平台功能,EA 会生成虚拟的即时报价,其时间等于最后一根 renko 柱线的开立时间。 它们的唯一目的是在智能交易系统中启动 OnTick 应答程序。 如果将即时报价从原始品种转换为自定义品种,且无任何更改,则会破坏 Renko 的结构。 同样,我们可以取强劲走势为例,尝试向 renko 图表发送一个实际时间为 12:00:00 的即时报价。 但此即时报价的时间并不对应于索引为零的最后一根(当前)柱线,而是对应于开立时间为 12:00,且索引为 2 的柱线。 结果就是,这样的即时报价将破坏 12:00 的 renko 柱线(这是历史记录),或产生错误。 当走势太慢时,Renko 可能会被逆反状况破坏。 如果报价长时间局限在一根柱线的范围内,则 renko 柱线保持时间不变,而新的即时报价可能会比第 0 根的 renko 高出一分钟以上。 如果将此类即时报价发送到 renko 图表,则会在“未来”形成幻像柱线。

请注意,历史 renko 即时报价以简约风格形成,每个盒则 1 个即时报价。 当联线操作时,所有的即时报价都会发送给 renko。

与其他自定义品种类似,此方法令我们可以在 renko 图表上运用任何指标、脚本和对象,以及运用智能交易系统。

主要参数

Renko 类处理即时报价流,并在此基础上创建新的 Renko 柱线。 以下伪代码示意其主要组件:

  class Renko
  {
    protected:
      bool incrementTime(const datetime time);
      void doWriteStruct(const datetime dtTime, const double dOpen, const double dHigh, const double dLow, const double dClose, const double dVol, const double dRealVol, const int spread);
      
    public:
      datetime checkEnding();
      void continueFrom(const datetime time);
      void doReset();
  
      void onTick(const MqlTick &t);
      void updateChartWindow(const double bid = 0, const double ask = 0);
  };

受保护方法 crementTime 和 doWriteStruct 分别切换到最接近指定时间的下一个空闲时间,即下一个 renko 盒子的 M1 样本,并调用 CustomRatesUpdate 输出柱线本身。 公开部分中的前三种方法负责在启动时初始化算法。 智能交易系统可以检查以前的 Renko 报价是否存在(这是由 checkEnding 方法完成的,该方法返回历史记录的结束日期和时间),并且根据是否存在,EA 可以调用 continueFrom 方法(恢复内部变量的值),或调用 doReset 重置即时报价“空”状态。

onTick 方法在每次即时报价(历史记录和联线记录)均被调用,并在必要时调用 doWriteStruct 形成一根 renko 柱线(我采用了著名的 RenkoLiveChart.mq4 EA 中的算法,并进行了一些修正)。 如果在 EA 设置中指定了即时报价模拟,则会另外调用 updateChartWindow。 完整的源代码附于文后。

TickProvider 类负责将即时报价“投递”到 Renko 对象:

  class TickProvider
  {
    public:
      virtual bool hasNext() = 0;
      virtual void getTick(MqlTick &t) = 0;
  
      bool read(Renko &r)
      {
        while(hasNext() && !IsStopped())
        {
          MqlTick t;
          getTick(t);
          r.onTick(t);
        }
        
        return IsStopped();
      }
  };

它是抽象的,因为它只声明了一个通用接口,用于从两个不同的来源读取/接收即时报价信息:在联线操作时,则由 EA 和 OnTick 事件应答程序处理基本品种的即时报价历史队列。 “read” 方法是通用的即时报价回环,它调用虚拟方法 hasNext() 和 getTick()。

即时报价历史记录以一种熟悉的方式在 HistoryTickProvider 类中读取:它使用 CopyTicksRange 和 MqlTick array[] 中间缓冲区,其中所请求即时报价按日线读取:

  class HistoryTickProvider : public TickProvider
  {
    private:
      datetime start;
      datetime stop;
      ulong length;     // in seconds
      MqlTick array[];
      int size;
      int cursor;
      
      int numberOfDays;
      int daysCount;
      
    protected:
      void fillArray()
      {
        cursor = 0;
        do
        {
          size = CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, start * 1000, MathMin(start + length, stop) * 1000);
          Comment("Processing: ", DoubleToString(daysCount * 100.0 / (numberOfDays + 1), 0), "% ", TTSM(start));
          if(size == -1)
          {
            Print("CopyTicksRange failed: ", GetLastError());
          }
          else
          {
            if(size > 0 && array[0].time_msc < start * 1000) // prevent older than requested data returned
            {
              start = stop;
              size = 0;
            }
            else
            {
              start = (datetime)MathMin(start + length, stop);
              if(size > 0) daysCount++;
            }
          }
        }
        while(size == 0 && start < stop);
      }
    
    public:
      HistoryTickProvider(const datetime from, const long secs, const datetime to = 0): start(from), stop(to), length(secs), cursor(0), size(0)
      {
        if(stop == 0) stop = TimeCurrent();
        numberOfDays = (int)((stop - start) / DAY_LONG);
        daysCount = 0;
        fillArray();
      }
  
      bool hasNext() override
      {
        return cursor < size;
      }
  
      void getTick(MqlTick &t) override
      {
        if(cursor < size)
        {
          t = array[cursor++];
          if(cursor == size)
          {
            fillArray();
          }
        }
      }
  };

CurrentTickProvider 联线即时报价提供程序类:

  class CurrentTickProvider : public TickProvider
  {
    private:
      bool ready;
      
    public:
      bool hasNext() override
      {
        ready = !ready;
        return ready;
      }
      
      void getTick(MqlTick &t) override
      {
        SymbolInfoTick(_Symbol, t);
      }
  };

即时报价处理过程的主要部分简示如下:

  const long DAY_LONG = 60 * 60 * 24;
  bool _FirstRun = true;
  
  Renko renko;
  CurrentTickProvider online;
  
  void OnTick(void)
  {
    if(_FirstRun)
    {
      // find existing renko tail to supersede StartFrom
      const datetime trap = renko.checkEnding();
      if(trap > TimeCurrent())
      {
        Print("Symbol/Timeframe data not ready...");
        return;
      }
      if((trap == 0) || Reset) renko.doReset();
      else renko.continueFrom(trap);
  
      HistoryTickProvider htp((trap == 0 || Reset) ? StartFrom : trap, DAY_LONG, StopAt);
      
      const bool interrupted = htp.read(renko);
      _FirstRun = false;
      
      if(!interrupted)
      {
        Comment("RenkoChart (" + (string)RenkoBoxSize + "pt): open ", _SymbolName, " / ", renko.getBoxCount(), " bars");
      }
      else
      {
        Print("Interrupted. Custom symbol data is inconsistent - please, reset or delete");
      }
    }
    else if(StopAt == 0) // process online if not stopped explicitly
    {
      online.read(renko);
    }
  }

在首次开始时,将搜索 renko 历史记录的末尾,并用开始时间 StartFrom 或历史记录(如果找到)来创建 HistoryTickProvider 对象,然后读取所有即时报价。 所有未来的即时报价都通过 CurrentTickProvider 对象联线处理(它是在全局上下文中创建的,就像 Renko 对象一样)。

我们依据 EURUSD 生成一个 renko 图表,从 2019 年开始,柱线大小为 100 点。 为此,在 EURUSD H1 图表上运行 EA,并除 StartFrom 以外采用默认设置。 当重新启动 EA 时,仅在 renko 历史记录可用,时间帧才显重要 - 在这种情况下,重新计算 renko 将从最后一根柱线的时间开始,其虽为最后一根柱线,但也有一个 renko 砖形落在该柱线时间内。

例如,对于原始 EURUSD H1:

运行 RenkoTicks EA 的 EURUSD H1 图表

运行 RenkoTicks EA 的 EURUSD H1 图表

我们将收到以下图表:

EURUSD 的 renko 图表,砖形大小为 100 点

EURUSD 的 renko 图表,砖形大小为 100 点

为了清晰起见,我添加了两条均线。

现在我们已收到 renko 品种的报价,如今是开发测试交易 EA 的时候了。

一款基于两条均线交汇的智能交易系统

我们运用最简单的交易策略之一,即两条移动平均线交汇。 上一个屏幕截图演示了这个思路。 当快速移动平均线(红色)向上或向下穿过慢速移动平均线(蓝色)时,分别做多或做空。 这是一个逆势系统。

从草拟开始创建一个智能交易系统将很困难,但 MetaTrader 5 提供了一个 MQL 向导,该向导可以基于标准类库(随终端提供)生成智能交易系统。 对于不熟悉编程的交易者来说,这极其便利。 生成的代码结构对于大量机器人来说都是通用的,故此,将其应用于主要任务是一个好主意 - 令机器人适应于自定义品种的交易。 在没有标准库的情况下创建智能交易系统也可按照相同的方式进行调整,但鉴于其创建方式可能相差很大,因此程序员在需要时必须提供相应的修改(通常,经验丰富的程序员可以基于我们的示例改编成其他任意 EA)。

奇怪的是,尽管标准库是最流行的策略之一(至少在学习算法交易时它是最流行的),但它未提供两条均线交叉的信号。 因此,我们需要编写相应的信号模块。 我们将其命名为 Signal2MACross.mqh。 以下是符合 MQL 向导所需规则的信号代码。

它以 “header” 开头 — 信号说明已添加相应格式的特殊注释,可从 MetaEditor 进行访问:

  //--- wizard description start
  //+------------------------------------------------------------------+
  //| Description of the class                                         |
  //| Title=Signals of 2 MAs crosses                                   |
  //| Type=SignalAdvanced                                              |
  //| Name=2MA Cross                                                   |
  //| ShortName=2MACross                                               |
  //| Class=Signal2MACross                                             |
  //| Page=signal_2mac                                                 |
  //| Parameter=SlowPeriod,int,11,Slow MA period                       |
  //| Parameter=FastPeriod,int,7,Fast Ma period                        |
  //| Parameter=MAMethod,ENUM_MA_METHOD,MODE_LWMA,Method of averaging  |
  //| Parameter=MAPrice,ENUM_APPLIED_PRICE,PRICE_OPEN,Price type       |
  //| Parameter=Shift,int,0,Shift                                      |
  //+------------------------------------------------------------------+
  //--- wizard description end

类名称(line Class)必须与其他 MQL 代码中的真实类名称匹配。 该信号的两条均线拥有 5 个典型参数:2 个周期(快速和慢速),均化方法,价格类型和偏移。

该类继承自 CExpertSignal。 它包含两个 CiMA 指标对象实例,带有操作参数的变量,参数设置器方法(方法名称必须与标题中的名称匹配)。 此外,该类还重新定义了虚拟方法,这些虚拟方法在指标初始化,以及检查设置和确定做多和做空信号时调用。

  class Signal2MACross : public CExpertSignal
  {
    protected:
      CiMA              m_maSlow;         // object-indicator
      CiMA              m_maFast;         // object-indicator
      
      // adjustable parameters
      int               m_slow;
      int               m_fast;
      ENUM_MA_METHOD    m_method;
      ENUM_APPLIED_PRICE m_type;
      int               m_shift;
      
      // "weights" of market models (0-100)
      int               m_pattern_0;      // model 0 "fast MA crosses slow MA"
  
    public:
                        Signal2MACross(void);
                       ~Signal2MACross(void);
                       
      // parameters setters
      void              SlowPeriod(int value) { m_slow = value; }
      void              FastPeriod(int value) { m_fast = value; }
      void              MAMethod(ENUM_MA_METHOD value) { m_method = value; }
      void              MAPrice(ENUM_APPLIED_PRICE value) { m_type = value; }
      void              Shift(int value) { m_shift = value; }
      
      // adjusting "weights" of market models
      void              Pattern_0(int value) { m_pattern_0 = value; }
      
      // verification of settings
      virtual bool      ValidationSettings(void);
      
      // creating the indicator and timeseries
      virtual bool      InitIndicators(CIndicators *indicators);
      
      // checking if the market models are formed
      virtual int       LongCondition(void);
      virtual int       ShortCondition(void);
  
    protected:
      // initialization of the indicators
      bool              InitMAs(CIndicators *indicators);
      
      // getting data
      double            FastMA(int ind) { return(m_maFast.Main(ind)); }
      double            SlowMA(int ind) { return(m_maSlow.Main(ind)); }
  };

该类描述了唯一的策略(形态或模型):当快速 MA 穿过慢速 MA 时,将初始化做多(向上交叉),或做空(向下交叉)。 默认情况下,模型权重等于 100。

  Signal2MACross::Signal2MACross(void) : m_slow(11), m_fast(7), m_method(MODE_LWMA), m_type(PRICE_OPEN), m_shift(0), m_pattern_0(100)
  {
  }

开仓条件是依据以下两种方法判定的(严格来说,代码不是检查交汇,而是检查一条均线相对于另一条均线的位置,而效果与总是在行情上的系统来说是相同的。 代码更简单 ):

  int Signal2MACross::LongCondition(void)
  {
    const int idx = StartIndex();
    
    if(FastMA(idx) > SlowMA(idx))
    {
      return m_pattern_0;
    }
    return 0;
  }
  
  int Signal2MACross::ShortCondition(void)
  {
    const int idx = StartIndex();
  
    if(FastMA(idx) < SlowMA(idx))
    {
      return m_pattern_0;
    }
    return 0;
  }

StartIndex 函数是在父类中定义。 正如您从代码中可见,索引是分析信号时的柱线编号。 如果在 EA 设置中选择了依据每次即时报价的操作(Expert_EveryTick = true),则起始索引等于 0;如果不是(即依据已收盘柱线操作),则索引为 1。

将 Signal2MACross.mqh 文件保存到 MQL5/Include/Expert/Signal/MySignals 文件夹,然后重新启动 MetaEditor(如果正在运行),以便在 MQL 向导中拾取新模块。

现在,我们可以根据信号生成一个智能交易系统。 在菜单中选择“文件”->“新建”,然后打开向导对话框。 Then follow the below steps:

  1. 选择“智能交易系统(生成)”
  2. 设置 EA 名称,例如 Experts\Examples\MA2Cross
  3. 添加信号 “2 条均线交叉的信号”
  4. 启用 "无尾随停止"
  5. 启用 "固定手数交易"

结果就是,您将得到以下 EA 代码:

  #include <Expert\Expert.mqh>
  #include <Expert\Signal\MySignals\Signal2MACross.mqh>
  #include <Expert\Trailing\TrailingNone.mqh>
  #include <Expert\Money\MoneyFixedLot.mqh>
  
  //+------------------------------------------------------------------+
  //| Inputs                                                           |
  //+------------------------------------------------------------------+
  // inputs for expert
  input string             Expert_Title              = "MA2Cross";  // Document name
  ulong                    Expert_MagicNumber        = 7623;
  bool                     Expert_EveryTick          = false;
  // inputs for main signal
  input int                Signal_ThresholdOpen      = 10;          // Signal threshold value to open [0...100]
  input int                Signal_ThresholdClose     = 10;          // Signal threshold value to close [0...100]
  input double             Signal_PriceLevel         = 0.0;         // Price level to execute a deal
  input double             Signal_StopLevel          = 0.0;         // Stop Loss level (in points)
  input double             Signal_TakeLevel          = 0.0;         // Take Profit level (in points)
  input int                Signal_Expiration         = 0;           // Expiration of pending orders (in bars)
  input int                Signal_2MACross_SlowPeriod = 11;         // 2MA Cross(11,7,MODE_LWMA,...) Slow MA period
  input int                Signal_2MACross_FastPeriod = 7;          // 2MA Cross(11,7,MODE_LWMA,...) Fast Ma period
  input ENUM_MA_METHOD     Signal_2MACross_MAMethod  = MODE_LWMA;   // 2MA Cross(11,7,MODE_LWMA,...) Method of averaging
  input ENUM_APPLIED_PRICE Signal_2MACross_MAPrice   = PRICE_OPEN;  // 2MA Cross(11,7,MODE_LWMA,...) Price type
  input int                Signal_2MACross_Shift     = 0;           // 2MA Cross(11,7,MODE_LWMA,...) Shift
  input double             Signal_2MACross_Weight    = 1.0;         // 2MA Cross(11,7,MODE_LWMA,...) Weight [0...1.0]
  // inputs for money
  input double             Money_FixLot_Percent      = 10.0;        // Percent
  input double             Money_FixLot_Lots         = 0.1;         // Fixed volume
  
  //+------------------------------------------------------------------+
  //| Global expert object                                             |
  //+------------------------------------------------------------------+
  CExpert ExtExpert;
  
  //+------------------------------------------------------------------+
  //| Initialization function of the expert                            |
  //+------------------------------------------------------------------+
  int OnInit()
  {
    // Initializing expert
    if(!ExtExpert.Init(Symbol(), Period(), Expert_EveryTick, Expert_MagicNumber))
    {
      printf(__FUNCTION__ + ": error initializing expert");
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
    // Creating signal
    CExpertSignal *signal = new CExpertSignal;
    if(signal == NULL)
    {
      printf(__FUNCTION__ + ": error creating signal");
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
    
    ExtExpert.InitSignal(signal);
    signal.ThresholdOpen(Signal_ThresholdOpen);
    signal.ThresholdClose(Signal_ThresholdClose);
    signal.PriceLevel(Signal_PriceLevel);
    signal.StopLevel(Signal_StopLevel);
    signal.TakeLevel(Signal_TakeLevel);
    signal.Expiration(Signal_Expiration);
    
    // Creating filter Signal2MACross
    Signal2MACross *filter0 = new Signal2MACross;
    if(filter0 == NULL)
    {
      printf(__FUNCTION__ + ": error creating filter0");
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
    signal.AddFilter(filter0);
    
    // Set filter parameters
    filter0.SlowPeriod(Signal_2MACross_SlowPeriod);
    filter0.FastPeriod(Signal_2MACross_FastPeriod);
    filter0.MAMethod(Signal_2MACross_MAMethod);
    filter0.MAPrice(Signal_2MACross_MAPrice);
    filter0.Shift(Signal_2MACross_Shift);
    filter0.Weight(Signal_2MACross_Weight);
  
    ...
    
    // Check all trading objects parameters
    if(!ExtExpert.ValidationSettings())
    {
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
    
    // Tuning of all necessary indicators
    if(!ExtExpert.InitIndicators())
    {
      printf(__FUNCTION__ + ": error initializing indicators");
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
  
    return(INIT_SUCCEEDED);
  }

文后附带了 MA2Cross.mq5 的完整代码。 一切准备就绪,可以编译了,并在策略测试器中进行测试,甚至可以针对任何品种进行优化,包括我们的自定义 renko。 由于我们对 Renko 确实很感兴趣,因此我们需要解释一下。

在价格走势完全形成之前,每个 renko 砖形得“矩形”形式都不存在。 当下一个砖形出现时,我们不仅知道它的收盘价,还知道它的开盘价,因为存在两个相反的方向:向上和向下。 当砖形最终收盘时,决定性和最具特色的是收盘价。 这就是为什么把 EA 中 Signal_2MACross_MAPrice 参数的默认值更改为 PRICE_CLOSE 的原因 - 不建议更改。 您可以尝试其他价格类型,但是 renko 的思路不仅为了摆脱时间限制,而且还可以剔除较小的价格波动,而这点可通过量化砖块大小来实现。

请注意,第 0 根 renko 柱线始终是不完整的(在大多数情况下,它是没有主体的烛条,不是矩形),这就是我们从第 1 根选用信号的原因。 为此目的,我们将 Expert_EveryTick 参数值设置为 false。

生成以 EURUSD 为基础的自定义 renko,其砖形大小为 100 点。 结果则为,我们得到品种 EURUSD_T_r100。 在测试器里选择它。 确保设置了 M1 时间帧。

我们来看看在 2019-2020 年期间(上半年)智能交易系统针对该品种的行为,例如,默认周期为 7 和 11(可用优化器单独验证其他组合)。

衍生自 EURUSD 的 100 点 renko 图表上的 MA2CrossCustom 策略结果

衍生自 EURUSD 的 100 点 renko 图表上的 MA2CrossCustom 策略结果

为了将自定义品种与真实品种进行比较,我在这里提供 MA2CrossCustom EA 的报告,该报告类似于带有空 WorkSymbol 参数的 MA2Cross。 在下一章节中,我们将研究如何从 MA2Cross 获取 MA2CrossCustom。

从成交表中可以看出,成交以砖形大小得倍数执行:卖出价格完全匹配,而买入价格因点差大小而不同(我们的 renko 生成器保存每根柱线形成时记录到的最大点差值),如果您愿意,可以在源代码中更改此行为)。 Renko 依据源图表中的价格类型构建。 在我们的例子中是出价(Bid)。 在交易所里金融产品使用最后价格。

基于 EURUSD 的自定义 renko 品种成交表

基于 EURUSD 的自定义 renko 品种成交表

现在,结果似乎太好了,难以置信。 确实,有许多隐藏的细微差别。

测试器中进行的 Renko 品种交易,在任何模式下都会影响结果的准确性:按开盘价、M1 OHLC、和即时报价。

标准 renko 柱线并非总能达到与标记时间匹配柱线同样的开盘价,但在很多情况下,它只是略微滞即可达到(因为价位在 renko 尺寸内向上或向下“漫步”一段时间,且最终也许会改变方向 ,形成逆转柱线)。 柱线标记时间是上一个柱线的完成时间。

收盘价并不对应于收盘时间,因为 Renko 柱线为 M1 柱线,即固定持续时间为一分钟。

可能会生成非标准的 renko,其柱线标记有完成时间,而非开始时间。 之后,收盘价对应于收盘时间。 然而,开盘时间比收盘时间早 1 分钟,因此它与实际开盘价不对应(即收盘价加/减 renko 大小)。

Renko 分析应该在已成形的柱线上执行,但其特征价格为收盘价,在逐柱线操作期间,测试器仅提供当前(最后一根)柱线的开盘价(没有提供 收盘价模式)。 此处,柱线开盘价格从定义上来说是预测因素。 如果我们用来自已收盘柱线的信号(通常从第一根柱线开始),则无论如何都将以第 0 根柱线的当前价格执行交易。 即使我们采用即时报价模式,测试器也会根据通用规则,按基于每根柱线配置的参考点为 Renko 生成即时报价。 测试器并未考虑 renko 报价的具体结构和行为(我们正在尝试采用 M1 柱线来直观地模拟它们)。 如果我们来推测,想象一次性形成整根柱线,它仍会只含有一个主体 - 对于此类柱线,测试器会从开盘价开始生成即时报价。 如果我们将柱线的即使报价成交量设置为等于 1,则柱线将丢失配置(将变成含有相等 OHLC 的价格标签)。

因此,在测试自定义 renko 品种时,所有的 renko 构造方法都将拥有订单执行效力。

换言之,由于 renko 结构本身,我们在测试器里针对 renko 品种得到了圣杯,因为它以等于 renko 柱线尺寸的步长渗透到未来。

故此,这就是为什么不能在单独的 renko 柱线上测试交易系统,而是必须结合真实品种来执行交易订单测试

Renko 提供分析和时序 - 何时入场。

截至目前为止,我们仅测试了自定义品种上 EA 进行交易的能力。 仅对测试器中针对 EA 应用设置了限制。 为了让 EA 变得通用,即能够运行在 renko 图表上时联线交易原始交易品种,我们需要添加一些内容。 这也有助于解决过度优化的问题。

自定义品种图表上智能交易系统适配

自定义品种仅对于客户终端是已知的,而在交易服务器上并不存在。 显然,在自定义品种图表中调整智能交易系统必须为原始品种(自定义品种所基于的)生成所有交易订单。 作为此问题的最简单解决方案,EA 可以在原始品种图表上运行,但可以接收自定义品种发来的信号(例如,来自指标)。 不过,许多交易者更喜欢看全面的图片。 甚或,选择性代码修改可能会产生错误。 希望针对源代码进行最小化的编辑。

不幸的是,原始品种的名称,以及在其基础上创建的 renko 的名称无法通过平台本身进行链接。 一个便捷的解决方案是在自定义品种属性中包含一个字符串字段 “origin” 或 “parent”,我们可以在其中写入真实品种的名称。 默认情况下为空。 但在填充后,平台将自动替换所有交易订单和历史记录请求中的交易品种。 鉴于在平台中不存在该机制,因此我们必须自行实现。 源和自定义品种的名称将利用参数进行设置。 自定义品种属性拥有一个含相应含义的字段 - SYMBOL_BASIS。 但是,由于我们不能保证任意的自定义品种生成器(任何 MQL 程序)都会正确填充参数,或将其完全用于此目的,因此我们需要提供另一种解决方案。

为此目的,我开发了 CustomOrder 类(请参阅下面的 CustomOrder.mqh)。 它包含的包装方法,与发送交易订单和历史记录的所有 MQL API 函数有关,其中的的字符串参数内含金融产品名称。 这些方法将自定义品种替换为当前的操作品种,反之亦然。 所有其他 API 函数均不需要“勾连”。 代码如下所示。

  class CustomOrder
  {
    private:
      static string workSymbol;
      
      static void replaceRequest(MqlTradeRequest &request)
      {
        if(request.symbol == _Symbol && workSymbol != NULL)
        {
          request.symbol = workSymbol;
          if(request.type == ORDER_TYPE_BUY
          || request.type == ORDER_TYPE_SELL)
          {
            if(request.price == SymbolInfoDouble(_Symbol, SYMBOL_ASK)) request.price = SymbolInfoDouble(workSymbol, SYMBOL_ASK);
            if(request.price == SymbolInfoDouble(_Symbol, SYMBOL_BID)) request.price = SymbolInfoDouble(workSymbol, SYMBOL_BID);
          }
        }
      }
      
    public:
      static void setReplacementSymbol(const string replacementSymbol)
      {
        workSymbol = replacementSymbol;
      }
      
      static bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result)
      {
        replaceRequest(request);
        return ::OrderSend(request, result);
      }
      
      static bool OrderCalcProfit(ENUM_ORDER_TYPE action, string symbol, double volume, double price_open, double price_close, double &profit)
      {
        if(symbol == _Symbol && workSymbol != NULL)
        {
          symbol = workSymbol;
        }
        return ::OrderCalcProfit(action, symbol, volume, price_open, price_close, profit);
      }
      
      static string PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id)
      {
        const string result = ::PositionGetString(property_id);
        if(property_id == POSITION_SYMBOL && result == workSymbol) return _Symbol;
        return result;
      }
      
      static string OrderGetString(ENUM_ORDER_PROPERTY_STRING property_id)
      {
        const string result = ::OrderGetString(property_id);
        if(property_id == ORDER_SYMBOL && result == workSymbol) return _Symbol;
        return result;
      }
      
      static string HistoryOrderGetString(ulong ticket_number, ENUM_ORDER_PROPERTY_STRING property_id)
      {
        const string result = ::HistoryOrderGetString(ticket_number, property_id);
        if(property_id == ORDER_SYMBOL && result == workSymbol) return _Symbol;
        return result;
      }
      
      static string HistoryDealGetString(ulong ticket_number, ENUM_DEAL_PROPERTY_STRING property_id)
      {
        const string result = ::HistoryDealGetString(ticket_number, property_id);
        if(property_id == DEAL_SYMBOL && result == workSymbol) return _Symbol;
        return result;
      }
      
      static bool PositionSelect(string symbol)
      {
        if(symbol == _Symbol && workSymbol != NULL) return ::PositionSelect(workSymbol);
        return ::PositionSelect(symbol);
      }
      
      static string PositionGetSymbol(int index)
      {
        const string result = ::PositionGetSymbol(index);
        if(result == workSymbol) return _Symbol;
        return result;
      }
      ...
  };
  
  static string CustomOrder::workSymbol = NULL;

为了尽量减少对源代码的编辑,请使用以下宏定义(针对所有方法):

  bool CustomOrderSend(const MqlTradeRequest &request, MqlTradeResult &result)
  {
    return CustomOrder::OrderSend((MqlTradeRequest)request, result);
  }
  
  #define OrderSend CustomOrderSend

它们允许将所有标准 API 函数调用自动重定向到 CustomOrder 类方法。 为此目的,请将 CustomOrder.mqh 包含到 EA,并设置操作品种:

  #include <CustomOrder.mqh>
  #include <Expert\Expert.mqh>
  ...
  input string WorkSymbol = "";
  
  int OnInit()
  {
    if(WorkSymbol != "")
    {
      CustomOrder::setReplacementSymbol(WorkSymbol);
      
      // force a chart for the work symbol to open (in visual mode only)
      MqlRates rates[1];
      CopyRates(WorkSymbol, PERIOD_H1, 0, 1, rates);
    }
    ...
  }

重要的是,#include <CustomOrder.mqh> 指令要先于其他所有指令。 因此,它将影响所有源代码,包括连接的标准库。 如果未指定通配符,则连接的 CustomOrder.mqh 对 EA 无效,它将控制权转移到标准 API 函数。

修改后的 MA2Cross EA 已重命名为 MA2CrossCustom.mq5。

现在,我们可以将 WorkSymbol 设置为 EURUSD,同时保持所有其他设置不变,然后开始测试。 现在,尽管 EA 在 renko 品种图表上运行,但它实际上交易 EURUSD。

交易真实的 EURUSD 品种时,MA2CrossCustom 策略在 100 点 renko 图表上的结果

交易真实的 EURUSD 品种时,MA2CrossCustom 策略在 100 点 renko 图表上的结果

这次的结果更接近现实。

在 EURUSD 交易中,价格与 renko 柱线收盘价的差异更大。 这是因为 Renko 柱线始终以分钟开始标记(这是平台中 M1 时间帧的限制),但一分钟内,价格可在任意时刻越过柱线边界。 由于智能交易系统是在柱线图表模式下操作的(不要与测试器模式混淆),因此价格通常不同,信号出现会“偏移”到 EURUSD 分钟柱线的开盘。 平均而言,误差是每笔交易的分钟柱线的数学期望。

智能交易系统从源自 EURUSD 的 Renko 图表上执行 EURUSD 交易

智能交易系统从源自 EURUSD 的 Renko 图表上执行 EURUSD 交易

为了消除差异,EA 必须处理所有的即时报价,但是我们已经发现,测试器中即时报价产生的逻辑与 renko 形成的逻辑不同:尤其是,逆转柱线的开盘价始终与前一根柱线的 renko 砖形收盘价存在一个缺口。

而在联线交易中不存在此问题。

我们用另一个为使用标准库编写的智能交易系统检查 CustomOrder 的功能。 为此,我们将利用有关数学表达式的计算的文章中的 ExprBot EA –它也采用了两条均线交叉策略,并利用 MT4Orders 函数库执行交易操作。 随附的是经过修改的智能交易系统 ExprBotCustom.mq5,以及所需的头文件(ExpresSParserS 文件夹)。

以下是 2019-2020 年(上半年)范围内的结果,设置相同(时间段 7/11,平均 LWMA 类型,第一条收盘价)。

源自 EURUSD 的 100 点 renko 图表上的 ExprBotCustom 策略的结果

源自 EURUSD 的 100 点 renko 图表上的 ExprBotCustom 策略的结果

ExprBotCustom 策略在 100 点 renko 图表上交易真实 EURUSD 品种的结果

ExprBotCustom 策略在 100 点 renko 图表上交易真实 EURUSD 品种的结果

这些结果与利用 MA2CrossCustom EA 获得的结果非常相似。

我们可以得出结论,所提议的方法解决了该问题。 不过,当前的 CustomOrder 实现只满足了基本的最低要求。 根据交易策略和操作品种的具体情况,也许需要进一步的改进。

结束语

我们研究了几种利用经纪商提供的有效品种报价生成自定义品种的方法。 特殊的数据归纳和累积算法,可从不同角度查看常用报价,并在此基础上构建高级交易系统。

当然,自定义品种提供了更多的可能性。 该技术的潜在应用范围广阔。 例如,我们可以利用合成品种,交易量增量和第三方数据源。 所讲述的程序转换,即可在策略测试器,也可在标准 MetaTrader 5 图表上联线模式下使用。