自定义交易品种交易细节

自定义交易品种仅为客户端终端所知,在交易服务器上不可用。因此,如果自定义交易品种是基于某个真实交易品种构建的,那么放置在此类自定义交易品种图表上的任何 EA 交易都应为原始交易品种生成交易订单。

作为此问题的最简单解决方案,你可以将 EA 交易放置在原始交易品种的图表上,但从自定义交易品种接收信号(例如,从指标)。另一种显而易见的方法是在执行交易操作时替换交易品种的名称。为了测试这两种方法,我们需要一个自定义交易品种和一个 EA 交易。

作为自定义交易品种的一个有趣的实际示例,我们采用几种不同的等量图。

等量图是一种基于其中包含的交易量相等的原则构建的柱线图。在常规图表上,每个新柱线都以指定的频率形成,与时间范围大小一致。在等量图上,当分时报价或实际交易量的总和达到预设值时,每个柱线被视为已形成。此时,程序开始计算下一个柱线的数量。当然,在计算交易量的过程中,会控制价格变动,我们在图表上得到通常的价格集:OpenHighLowClose

等距柱线的构建方式类似:当价格向任一方向移动给定点数时,会打开一个新的柱线。

因此,EqualVolumeBars.mq5 EA 交易将支持三种模式,即三种图表类型:

  • EqualTickVolumes – 按分时报价计数的等量柱线
  • EqualRealVolumes – 按实际交易量计数的等量柱线(如果它们被广播)
  • RangeBars – 等距柱线

它们通过输入参数 WorkMode 进行选择。

柱线大小和用于计算的历史深度在 TicksInBarStartDate 参数中指定。

input int TicksInBar = 1000;
input datetime StartDate = 0;

根据模式,自定义交易品种将分别获得后缀 "_Eqv"、"_Qrv" 或 "_Rng",并附加柱线大小。

尽管等量/等距图上的水平轴仍然表示时间顺序,但每个柱线的时间戳是任意的,并且取决于每个时间范围内的波动性(交易笔数或交易规模)。在这方面,选择自定义交易品种图表的时间范围时应使其等于最小的 M1。

平台的限制是所有柱线都具有相同的名义持续时间,但在我们的“人工”图表的情况下,应该记住每个柱线的实际持续时间并不相同,并且可能远远超过 1 分钟,或者相反,远远少于 1 分钟。因此,如果一个柱线的给定交易量足够小,则可能会出现如下情况:新柱线的形成频率远高于每分钟一次,此时自定义交易品种柱线的虚拟时间将领先于现实时间,进入未来。为防止这种情况发生,你应该增加柱线的交易量(TicksInBar 参数)或将旧柱线向左移动。

用于管理自定义交易品种的初始化和其他辅助任务(特别是重置现有历史记录以及打开带有新交易品种的图表)的执行方式与其他示例类似,我们将在此省略它们。我们转向具有应用性质的细节。

我们将使用内置函数 CopyTicks/CopyTicksRange 读取实际分时报价的历史记录:第一个函数用于以 10,000 个分时报价为一批次交换历史记录,第二个函数用于请求自上次处理以来的新分时报价。所有这些功能都打包在 TicksBuffer 类中(随附了完整的源代码)。

class TicksBuffer
{
private:
   MqlTick array[]; // internal array of ticks
   int tick;        // incremental index of the next tick for reading
public:
   bool fill(ulong &cursorconst bool history = false);
   bool read(MqlTick &t);
};

公共方法 fill 用于从 cursor 时间(以毫秒为单位)开始,用下一部分分时报价填充内部数组。同时,每次调用时 cursor 中的时间都会根据读入缓冲区的最后一个分时报价的时间向前移动(请注意,该参数是通过引用传递的)。

参数 history 确定是使用 CopyTicks 还是 CopyTicksRange。通常,如果是在线,我们将从 OnTick 处理程序中读取一个或多个新的分时报价。

read 方法从内部数组中返回一个分时报价,并将内部指针 (tick) 移至下一个分时报价。如果在读取时到达数组末尾,该方法将返回 false,这意味着是时候调用 fill 方法了。

使用这些方法,分时报价历史旁路算法的实现如下(此代码间接从 OnInit 通过计时器调用)。

   ulong cursor = StartDate * 1000;
   TicksBuffer tb;
    
   while(tb.fill(cursortrue) && !IsStopped())
   {
      MqlTick t;
      while(tb.read(t))
      {
         HandleTick(ttrue);
      }
   }

HandleTick 函数中,在某些用于控制分时报价数量、总交易量(实际交易量,如果有)以及价格移动距离的全局变量中,必须要考虑分时报价 t 的特性。根据操作模式,应以不同方式分析这些变量以确定新柱线形成的条件。因此,如果在等量模式下,分时报价数量超过 TicksInBar,我们应该通过将计数器重置为 1 来开始一个新的柱线。在这种情况下,新柱线的时间计为四舍五入到最接近分钟的分时报价时间。

这组全局变量用于存储自定义交易品种上最后(“当前”)柱线的虚拟时间 (now_time)、其 OHLC 价格和交易量。

datetime now_time;
double now_closenow_opennow_lownow_high;
long now_volumenow_real;

无论是在历史记录读取期间,还是稍后当 EA 交易开始实时处理在线分时报价时(我们稍后会回到这一点),这些变量都会不断更新。

以某种简化的形式,HandleTick 内部的算法如下所示:

void HandleTick(const MqlTick &tconst bool history = false)
{
   now_volume++;               // count the number of ticks
   now_real += (long)t.volume// sum up all real volumes
   
   if(!IsNewBar()) // continue the current bar
   {
      if(t.bid < now_lownow_low = t.bid;   // monitor price fluctuations downward
      if(t.bid > now_highnow_high = t.bid// and upwards
      now_close = t.bid;                     // update the closing price
    
      if(!history)
      {
         // update the current bar if we are not in the history
         WriteToChart(now_timenow_opennow_lownow_highnow_close,
            now_volume - !historynow_real);
      }
   }
   else // new bar
   {
      do
      {
         // save the closed bar with all attributes
         WriteToChart(now_timenow_opennow_lownow_highnow_close,
            WorkMode == EqualTickVolumes ? TicksInBar : now_volume,
            WorkMode == EqualRealVolumes ? TicksInBar : now_real);
   
         // round up the time to the minute for the new bar
         datetime time = t.time / 60 * 60;
   
         // prevent bars with old or same time
         // if gone to the "future", we should just take the next count M1
         if(time <= now_timetime = now_time + 60;
   
         // start a new bar from the current price
         now_time = time;
         now_open = t.bid;
         now_low = t.bid;
         now_high = t.bid;
         now_close = t.bid;
         now_volume = 1;             // first tick in the new bar
         if(WorkMode == EqualRealVolumesnow_real -= TicksInBar;
         now_real += (long)t.volume// initial real volume in the new bar
   
         // save new bar 0
         WriteToChart(now_timenow_opennow_lownow_highnow_close,
            now_volume - !historynow_real);
      }
      while(IsNewBar() && WorkMode == EqualRealVolumes);
   }
}

history 参数确定计算是基于历史记录还是已经实时进行(基于传入的在线分时报价)。如果基于历史记录,则每个柱线只需形成一次即可,而在线情况下,当前柱线会随着每个分时报价而更新。这可以加快历史记录的处理速度。

当满足根据模式关闭下一个柱线的条件时,辅助函数 IsNewBar 返回 true

bool IsNewBar()
{
   if(WorkMode == EqualTickVolumes)
   {
      if(now_volume > TicksInBarreturn true;
   }
   else if(WorkMode == EqualRealVolumes)
   {
      if(now_real > TicksInBarreturn true;
   }
   else if(WorkMode == RangeBars)
   {
      if((now_high - now_low) / _Point > TicksInBarreturn true;
   }
   
   return false;
}

WriteToChart 函数通过调用 CustomRatesUpdate 创建具有给定特征的柱线。

void WriteToChart(datetime tdouble odouble ldouble hdouble clong vlong 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;
   
   if(CustomRatesUpdate(SymbolNamer) < 1)
   {
      Print("CustomRatesUpdate failed: "_LastError);
   }
}

上述读取和处理分时报价的循环是在初始访问历史记录期间,在创建或完成重新计算已存在的用户交易品种之后执行的。当涉及到新的分时报价时,OnTick 函数使用类似的代码,但没有“历史性”标志。

void OnTick()
{
   static ulong cursor = 0;
   MqlTick t;
   
   if(cursor == 0)
   {
      if(SymbolInfoTick(_Symbolt))
      {
         HandleTick(t);
         cursor = t.time_msc + 1;
      }
   }
   else
   {
      TicksBuffer tb;
      while(tb.fill(cursor))
      {
         while(tb.read(t))
         {
            HandleTick(t);
         }
      }
   }
   
   RefreshWindow(now_time);
}

RefreshWindow 函数在Market Watch窗口中添加一个自定义交易品种的分时报价。

请注意,分时报价转发会使柱线中的分时报价计数器增加 1,因此,当将分时报价计数器写入第 0 个柱线时,我们之前减去了 1(参见调用 WriteToChart 时的表达式 now_volume - !history)。

生成分时报价很重要,因为它会触发自定义金融工具图表上的 OnTick 事件,这可能允许放置在此类图表上的 EA 交易进行交易。然而,这项技术需要一些额外的技巧,我们稍后会探讨。

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

我们需要强调的是,生成的自定义分时报价的时间始终设置为当前柱线的标签,因为我们不能保留真实的分时报价时间:如果它已经提前超过 1 分钟,并且我们将这样的分时报价发送到Market Watch,终端将创建下一个 M1 柱线,这将违反我们的“等量”结构,因为我们的柱线不是按时间形成的,而是按交易量填充形成的(并且我们自己控制这个过程)。

理论上,我们可以为每个分时报价增加一毫秒,但我们无法保证柱线不需要存储超过 60,000 个分时报价(例如,如果用户订购一个具有特定价格范围的图表,而在该特定价格范围内,我们无法预测此类波动具体需要多少个分时报价)。

在按交易量计算的模式中,理论上可以使用线性公式对分时报价时间的秒和毫秒分量进行插值:

  • EqualTickVolumes – (now_volume - 1) * 60000 / TicksInBar;
  • EqualRealVolumes – (now_real - 1) * 60000 / TicksInBar;

然而,这只不过是识别分时报价的一种手段,而不是试图使“人工”分时报价的时间更接近真实分时报价的时间。这不仅仅是真实分时报价流不均匀性的损失,这本身就会导致原始交易品种与据其生成的自定义交易品种之间的价格差异。

主要问题是需要在 M1 柱线的边界处对分时报价时间进行取整,并将其“打包”在一分钟内(请参见关于特殊类型图表的补充说明)。例如,下一个实际时间为 12:37:05'123 的分时报价成为第 1001 个分时报价,并且应该形成一个新的等量柱线。但是,M1 柱线只能标记到分钟,比如 12:37。结果,该金融工具在 12:37 的实际价格将与为等量柱线 12:37 提供Open的分时报价中的价格不匹配。此外,如果接下来的 1000 个分时报价持续几分钟,我们还是必须得压缩这些分时报价的时间,以免达到 12:38 标记。

当通过标准 M1 时间周期图表模拟特殊图表时,由于时间量化,该问题具有系统性。在这样的图表上无法完全解决此问题。但是,当以连续时间生成带有分时报价的自定义交易品种时(例如,使用合成行情或基于来自外部服务的流数据),则不会出现此问题。

务必要注意,分时报价转发仅在此版本的生成器中在线完成,而在历史记录中不会生成自定义分时报价!这样做是为了加快行情的创建速度。如果你需要在较慢的过程中生成分时报价历史记录,则应调整 EA 交易 EqualVolumeBars.mq5:排除 WriteToChart 函数,并使用 CustomTicksReplace/CustomTicksAdd 执行整个生成过程。同时,应该记住,应将分时报价的原始时间替换为另一个时间,在一分钟柱线内,以免扰乱形成的等量图的结构。

我们来看看 EqualVolumeBars.mq5 是如何运作的。这是 EURUSD M15 的工作图表,其中运行着 EA 交易。它具有等量图,其中每个柱线分配了 1000 个分时报价。

由 EqualVolumeBars 专家生成的等量 EURUSD 图表,每柱线 1000 个分时报价

由 EqualVolumeBars EA 交易生成的等量 EURUSD 图表,每柱线 1000 个分时报价

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

统计信息显示在日志中。

Creating "EURUSD.c_Eqv1000"
Processing tick history...
End of CopyTicks at 2022.06.15 12:47:51
Bar 0: 2022.06.15 12:40:00 866 0
2119 bars written in 10 sec
Open "EURUSD.c_Eqv1000" chart to view results

我们检查另一种操作模式:等距。下图是一个图表,其中每个柱线的距离是 250 点。

由 EqualVolumeBars 专家生成的每柱线 250 点的 EURUSD 等距图表

由 EqualVolumeBars 生成的每柱线 250 点的 EURUSD 等距图表

对于交易所金融工具,EA 交易允许使用实际交易量模式,例如,如下所示:

由 EqualVolumeBars 专家生成的每柱线实际交易量为 10000 的以太坊原始图表和等量图表

每柱线实际交易量为 10000 的以太坊原始图表和等量图表

放置 EA 交易生成器时工作交易品种的时间范围并不重要,因为计算始终使用分时报价历史记录。

同时,自定义交易品种图表的时间范围必须等于 M1(终端中可用的最小时间范围)。因此,柱线的时间通常尽可能地(在可能的范围内)匹配其形成时刻。然而,在市场剧烈波动期间,当分时报价数量或交易量大小每分钟形成几个柱线时,柱线的时间将早于实际时间。当市场波动幅度较小时,等量柱线的时间标记情况将恢复正常。这不会影响在线价格的流动,因此可能并不特别重要,因为使用等量或等距柱线的全部意义在于与绝对时间脱钩。

不幸的是,原始交易品种的名称和据其上创建的自定义交易品种的名称无法通过平台本身以任何方式联系起来。如果在自定义交易品种的特性中有一个字符串字段 "origin"(来源),我们可以在其中写入真实工作工具的名称,那将会很方便。该字段默认为空,但如果填写,平台可以在所有交易订单和历史请求中替换交易品种,并且对用户来说是自动且透明的。理论上,在用户定义交易品种的特性中,有一个 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(MQLInfoInteger(MQL_TESTER)
            && (request.type == ORDER_TYPE_BUY
            || request.type == ORDER_TYPE_SELL))
         {
            if(TU::Equal(request.priceSymbolInfoDouble(_SymbolSYMBOL_ASK)))
               request.price = SymbolInfoDouble(workSymbolSYMBOL_ASK);
            if(TU::Equal(request.priceSymbolInfoDouble(_SymbolSYMBOL_BID)))
               request.price = SymbolInfoDouble(workSymbolSYMBOL_BID);
         }
      }
   }
   
public:
   static void setReplacementSymbol(const string replacementSymbol)
   {
      workSymbol = replacementSymbol;
   }
   
   static bool OrderSend(MqlTradeRequest &requestMqlTradeResult &result)
   {
      replaceRequest(request);
      return ::OrderSend(requestresult);
   }
   ...

请注意,主要工作方法 replaceRequest 不仅会替换交易品种,还会替换当前的AskBid。这是因为许多自定义工具,例如我们的等量图,其虚拟时间与真实原型交易品种的时间不同。因此,由测试程序模拟的自定义金融工具的价格与真实金融工具的相应价格不同步。

这种伪影现象仅在测试程序中出现。在线交易时,自定义交易品种图表将与真实图表同步更新(价格上),尽管柱线标签会有所不同(一个“人工”M1 柱线的实际持续时间或多或少于一分钟,并且其倒计时时间不是一分钟的倍数)。因此,这种价格转换更多的是一种预防措施,以避免在测试程序中出现重新报价。但是,在测试程序中,我们通常不需要进行交易品种替换,因为测试程序可以与自定义交易品种进行交易(与经纪商的服务器不同)。此外,完全是出于兴趣,我们将比较在有和没有字符替换的情况下运行的测试结果。

为了最大限度地减少对客户端源代码的编辑,提供了以下形式的全局函数和宏(适用于所有 CustomOrder 方法):

  bool CustomOrderSend(const MqlTradeRequest &requestMqlTradeResult &result)
  {
    return CustomOrder::OrderSend((MqlTradeRequest)requestresult);
  }
  
  #define OrderSend CustomOrderSend

它们允许将所有标准 API 函数调用自动重定向到 CustomOrder 类方法。为此,只需将 CustomOrder.mqh 纳入 EA 交易中,并设置工作交易品种,例如,在 WorkSymbol 参数中:

  #include <CustomOrder.mqh>
  #include <Expert/Expert.mqh>
  ...
  input string WorkSymbol = "";
  
  int OnInit()
  {
    if(WorkSymbol != "")
    {
      CustomOrder::setReplacementSymbol(WorkSymbol);
      
      // initiate the opening of the chart tab of the working symbol (in the visual mode of the tester)
      MqlRates rates[1];
      CopyRates(WorkSymbolPERIOD_CURRENT01rates);
    }
    ...
  }

重要的是,指令 #include<CustomOrder.mqh> 是第一个,在其他指令之前。因此,它会影响所有源代码,包括 MetaTrader 5 发行版中的标准库。如果未指定替换交易品种,则连接的 CustomOrder.mqh 对 EA 交易没有影响,并“透明地”将控制权转移给标准 API 函数。

现在我们已经准备好测试在自定义交易品种上进行交易的想法,包括自定义交易品种本身。

应用上述技术,我们修改了已经熟悉的 EA 交易 BandOsMaPro,将其重命名为 BandOsMaCustom.mq5。我们在 EURUSD 等量图上对其进行测试,该图表的柱线大小为 1000 个分时报价,使用 EqualVolumeBars.mq5 获得。

优化或测试模式设置为 OHLC M1 价格(更准确的方法没有意义,因为我们没有生成分时报价,还因为此版本以形成的柱线价格进行交易)。日期范围是整个 2021 年和 2022 年上半年。附加了带有 BandOsMACustom.set 设置的文件。

在测试程序设置中,别忘了选择自定义交易品种 EURUSD_Eqv1000 和 M1 时间范围,因为等量柱线是基于该交易品种模拟的。

WorkSymbol 参数为空时,EA 交易可交易自定义交易品种。结果如下:

在 EURUSD_Eqv1000 等量图上进行交易时的测试程序报告

在 EURUSD_Eqv1000 等量图上进行交易时的测试程序报告

如果 WorkSymbol 参数等于 EURUSD,则 EA 交易交易 EURUSD 货币对,尽管它在 EURUSD_Eqv1000 图表上工作。结果有所不同,但差异不大。

从 EURUSD_Eqv1000 等量图交易 EURUSD 时的测试程序报告

从 EURUSD_Eqv1000 等量图交易 EURUSD 时的测试程序报告

然而,正如本节开头已经提到的,对于依赖指标信号进行交易的 EA 交易来说,有一种更简单的方法来支持自定义交易品种。为此,只需在自定义交易品种上创建指标,并将 EA 交易放置在工作交易品种的图表上即可。

我们可以轻松实现此选项。我们将其称为 BandOsMACustomSignal.mq5

不再需要头文件 CustomOrder.mqh。我们添加了两个新的输入参数来代替 WorkSymbol 输入参数:

input string SignalSymbol = "";
input ENUM_TIMEFRAMES SignalTimeframe = PERIOD_M1;

应将它们传递给管理指标的 BandOsMaSignal 类的构造函数。以前,到处都使用 _Symbol_Period

interface TradingSignal
{
   virtual int signal(void);
   virtual string symbol();
   virtual ENUM_TIMEFRAMES timeframe();
};
   
class BandOsMaSignalpublic TradingSignal
{
   int hOsMAhBandshMA;
   int direction;
   const string _symbol;
   const ENUM_TIMEFRAMES _timeframe;
public:
   BandOsMaSignal(const string sconst ENUM_TIMEFRAMES tf,
      const int fastconst int slowconst int signalconst ENUM_APPLIED_PRICE price,
      const int bandsconst int shiftconst double deviation,
      const int periodconst int xENUM_MA_METHOD method): _symbol(s), _timeframe(tf)
   {
      hOsMA = iOsMA(stffastslowsignalprice);
      hBands = iBands(stfbandsshiftdeviationhOsMA);
      hMA = iMA(stfperiodxmethodhOsMA);
      direction = 0;
   }
   ...
   virtual string symbol() override
   {
      return _symbol;
   }
   
   virtual ENUM_TIMEFRAMES timeframe() override
   {
      return _timeframe;
   }
}

由于信号的交易品种和时间周期现在可以与图表的交易品种和周期不同,因此我们通过添加读取方法扩展了 TradingSignal 接口。实际值在 OnInit 中传递给构造函数。

int OnInit()
{
   ...
   strategy = new SimpleStrategy(
      new BandOsMaSignal(SignalSymbol != "" ? SignalSymbol : _Symbol,
         SignalSymbol != "" ? SignalTimeframe : _Period,
         p.fastp.slowSignalOsMAPriceOsMA,
         BandsMABandsShiftBandsDeviation,
         PeriodMAShiftMAMethodMA),
         MagicStopLossLots);
   return INIT_SUCCEEDED;
}

SimpleStrategy 类中,trade 方法现在根据信号的特性而不是当前图表来检查新柱线的出现。

   virtual bool trade() override
   {
      // looking for a signal once at the opening of the bar of the desired symbol and timeframe
      if(lastBar == iTime(command[].symbol(), command[].timeframe(), 0)) return false;
      
      int s = command[].signal(); // get signal
      ...
   }

对于具有相同设置的比较实验,应在 EURUSD 上启动 EA 交易 BandOsMACustomSignal.mq5(你可以使用 M1 或其他时间范围),并且应在 SignalSymbol 参数中指定 EURUSD_Eqv1000。 SignalTimeframe 默认应等于 PERIOD_M1。结果,我们将得到一个类似的报告。

在 EURUSD 图表上基于来自 EURUSD_Eqv1000 等量交易品种的信号进行交易时的测试程序报告

在 EURUSD 图表上基于来自 EURUSD_Eqv1000 等量交易品种的信号进行交易时的测试程序报告

此处的柱线和分时报价数量不同,因为选择 EURUSD 作为测试金融工具,而不是自定义的 EURUSD_Eqv1000。

所有三个测试结果都略有不同。这是由于将行情“打包”到分钟柱线中以及原始金融工具和自定义金融工具价格变动的轻微不同步造成的。哪个结果更准确?这很可能取决于具体的交易系统及其实现特性。对于我们的带有柱线开盘控制的 EA 交易 BandOsMa,直接在 EURUSD_Eqv1000 上进行交易的版本应该具有最真实的结果。理论上,经验法则指出,在几个备选检查中,最可靠的是利润最低的那个,这几乎总是得到满足。

因此,我们分析了几种用于调整 EA 交易以在自定义交易品种上进行交易的技术,这些自定义交易品种在经纪商的工作交易品种中有原型。然而,这种情况并非强制性的。在许多情况下,自定义交易品种是基于来自外部系统(例如加密货币交易所)的数据生成的。对自定义交易品种进行交易必须将其公共 API 用于 MQL5 网络函数。

使用自定义交易品种模拟特殊类型的图表
 
许多交易者使用特殊类型的图表,其中不考虑连续的实时时间。这不仅包括等量和等距柱线,还包括砖形图 (Renko)、点数图 (Point-And-Figure, PAF)、卡吉图 (Kagi) 等。自定义交易品种允许在 MetaTrader 5 中使用 M1 时间范围图表模拟这些类型的图表,但如果不是进行技术分析,而是在测试交易系统,则应谨慎对待。
 
对于特殊类型的图表,实际的柱线开盘时间(精确到毫秒)几乎总是不与 M1 柱线标记的分钟完全一致。因此,自定义柱线的开盘价不同于标准交易品种 M1 柱线的开盘价。
 
此外,其他 OHLC 价格也会有所不同,因为特殊图表上 M1 柱线形成的实际持续时间不等于一分钟。例如,等量图的 1000 个分时报价可能会累积超过 5 分钟。
 
自定义柱线的收盘价也不对应于实际的收盘时间,因为自定义柱线在技术上是一个 M1 柱线,即其名义持续时间为 1 分钟。
 
在使用诸如经典砖形图或点数图之类的图表类型时应特别小心。事实是,它们的反转柱线的开盘价与前一个柱线的收盘价之间存在缺口。因此,开盘价成为未来价格变动的预测指标。
 
对此类图表的分析应该根据已形成的柱线进行,也就是说,其特征价格是收盘价,然而,当按柱线工作时,测试程序仅为当前(最后)柱线提供开盘价(没有按收盘价的模式)。即使我们从已关闭的柱线(通常是第一个柱线)获取指标信号,交易仍会在第 0 个柱线的当前价格进行。即使我们转向分时报价模式,测试程序也总是根据一般规则生成分时报价,以基于每根柱线配置的参考点为指导。测试程序不考虑我们试图用 M1 柱线进行可视化模拟的特殊图表的结构和行为。
 
在测试程序中使用此类交易品种以任何模式(按开盘价、M1 OHLC 或按分时报价)进行交易都会影响结果的准确性:它们过于乐观,可能导致过高的期望。在这方面,至关重要的是不要在单独的砖形图或点数图上检查交易系统,而应结合在真实交易品种上执行订单进行检查。
 
自定义交易品种也可用于秒级时间范围或分时报价图表。在这种情况下,也会为柱线和分时报价生成与真实时间脱钩的虚拟时间。因此,此类图表非常适合操作分析,但在开发和测试交易策略(尤其是多交易品种策略)时需要额外注意。
 
对于任何自定义交易品种,另一种选择是在 EA 交易或指标内部独立计算柱线和分时报价数组。然而,调试和可视化此类结构需要额外的工作量。