如何在 MetaTrader 5 里快速开发并调试交易策略

MetaQuotes | 26 九月, 2016

"没有人可以信任, 除了自己"  (с)调试器

自动剥头皮系统理所当然地被认为是算法交易的巅峰, 但同时它们的代码也最难编写。在本文中, 我们将介绍如何使用内置调试工具并基于接收的瞬时报价分析来构建策略, 以及可视测试。开发入场和离场规则, 往往需要经历多年的手工交易。但借助 MetaTrader 5, 您可以在真实历史数据的基础上快速测试任何策略。


依据瞬时报价进行交易的理念

首先, 必须创建一个绘制瞬时报价图表的指标, 即图表每处的价格变化都能看到。这些指标的首选之一可在代码库里找到— https://www.mql5.com/zh/code/89。与通常的那些不同, 必要时, 它可在新的瞬时报价来临时将整个图表向后移位。


测试的理念将基于价格变化系列之间的两个连续瞬时报价。近似的点数顺序如下:

+1, 0, +2, -1, 0, +1, -2, -1, +1, -5, -1, +1, 0, -1, +1, 0, +2, -1, +1, +6, -1, +1,...

正态分布规律为, 两个瞬时报价之间 99% 的价格变化在 3 个标准差之内。我们将尝试在每笔瞬时报价到来时, 实时计算标准差, 并用红色和蓝色图标标注价格尖峰。因此, 有可能直观地选择一个策略来发挥这种锐利喷发的优势 — 在变换方向或 "均值回归" 时进行交易。如您所见, 思路很简单, 很多数学爱好者肯定已经沿着这条道路走下去了。


创建瞬时报价指标

在 MetaEditor 里运行 MQL 向导, 设置名称和两个输入参数:

接下来, 勾选 "指标位于单独窗口" 并指定 2 块图形作图板, 它们将在子窗口里显示信息: 一条瞬时报价线和有关价格尖峰出现的彩色信号箭头。

在结果草案里用黄色标记变化

//+------------------------------------------------------------------+
//|                                              TickSpikeHunter.mq5 |
//|                                 版权所有 2016, MetaQuotes 软件公司|
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "版权所有 2016, MetaQuotes 软件公司"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 3
#property indicator_plots   2
//--- 绘制瞬时报价
#property indicator_label1  "瞬时报价"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrGreen
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- 绘制信号
#property indicator_label2  "信号"
#property indicator_type2   DRAW_COLOR_ARROW
#property indicator_color2  clrRed,clrBlue,C'0,0,0',C'0,0,0',C'0,0,0',C'0,0,0',C'0,0,0',C'0,0,0'
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- 输入参数
input int      ticks=50;         // 计算所用瞬时报价数量
input double   gap=3.0;          // 通道宽度的标准差
//--- 指标缓存区
double         TickPriceBuffer[];
double         SignalBuffer[];
double         SignalColors[];
//--- 价格变化计数器
int ticks_counter;
//--- 第一次指标调用
bool first;
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                              |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 指标缓存区映射
   SetIndexBuffer(0,TickPriceBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,SignalBuffer,INDICATOR_DATA);
   SetIndexBuffer(2,SignalColors,INDICATOR_COLOR_INDEX);
//--- 设置空值, 在绘图时应被忽略  
   PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0);
   PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0);
//--- 信号将作为图标输出
   PlotIndexSetInteger(1,PLOT_ARROW,159);
//--- 初始化全局变量
   ticks_counter=0;
   first=true;
//--- 程序初始化成功
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- 返回 prev_calculated 的数值用于下次调用
   return(rates_total);
  }
//+------------------------------------------------------------------+

现在, 将其它部分代码加入接收瞬时报价的 OnCalculate() 预定义处理器。在首次调用函数期间要明确将指标缓存区清零, 并且出于便利, 将它们标记为序列 — 因此, 它们将会从右至左索引。这样就可以使用索引 0 来调用最近的指标缓存区数值, 即最后的瞬时报价值将会保存在 TickPriceBuffer[0]。

另外, 主要的瞬时报价处理将移至单独的 ApplyTick() 函数:

//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//--- 在首次调用期间将指标缓存区清零并标记为序列
   if(first)
     {
      ZeroMemory(TickPriceBuffer);
      ZeroMemory(SignalBuffer);
      ZeroMemory(SignalColors);
      //--- 序列数组向后定位, 在这种情况下最方便
      ArraySetAsSeries(SignalBuffer,true);
      ArraySetAsSeries(TickPriceBuffer,true);
      ArraySetAsSeries(SignalColors,true);
      first=false;
     }
//--- 使用当前的收盘价作为价格
   double lastprice=close[rates_total-1];
//--- 瞬时报价计数
   ticks_counter++;
   ApplyTick(lastprice); // 执行计算和缓存区移位
//--- 返回 prev_calculated 的数值用于下次调用
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| 应用瞬时报价进行计算                                              |
//+------------------------------------------------------------------+
void ApplyTick(double price)
  {
   int size=ArraySize(TickPriceBuffer);
   ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,size-1);
   ArrayCopy(SignalBuffer,SignalBuffer,1,0,size-1);
   ArrayCopy(SignalColors,SignalColors,1,0,size-1);
//--- 保存最后的价格值
   TickPriceBuffer[0]=price;
//---
  }

目前, ApplyTick() 执行最简单地操作 — 将所有缓存区数值向后挪一位并将最后的瞬时报价写入 TickPriceBuffer[0]。在调试模式里运行指标并观察一段时间。

正如可以看到的那样, 供给价用作当前蜡烛的基准收盘价一般保持不变, 所以图表上绘出了一片 "高地"。稍微调整代码, 以便仅得到 "锯齿" - 这更加直观。

//--- 只在价格变化时计算
   if(lastprice!=TickPriceBuffer[0])
     {
      ticks_counter++;      // 瞬时报价计数
      ApplyTick(lastprice); // 执行计算和缓存区移位
     }

如此, 指标的第一个版本已经创建, 现在价格没有零增量。


添加一个辅助缓存区并计算标准差

需要额外的数组来计算差值。这个数组将会保存每笔瞬时报价的价格增量。作为这样的数组, 在需要的地方添加另一个指标缓冲区和相应的代码:

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   2
...
//--- 指标缓存区
double         TickPriceBuffer[];
double         SignalBuffer[];
double         DeltaTickBuffer[];
double         ColorsBuffers[];
...
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                              |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 指标缓存区映射
   SetIndexBuffer(0,TickPriceBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,SignalBuffer,INDICATOR_DATA);
   SetIndexBuffer(2,SignalColors,INDICATOR_COLOR_INDEX);
   SetIndexBuffer(3,DeltaTickBuffer,INDICATOR_CALCULATIONS);
...
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                |
//+------------------------------------------------------------------+
int OnCalculate(const ...)

//--- 在首次调用期间将指标缓存区清零并标记为序列
   if(first)
     {
      ZeroMemory(TickPriceBuffer);
      ZeroMemory(SignalBuffer);
      ZeroMemory(SignalColors);
     ZeroMemory(DeltaTickBuffer);
      //--- 序列数组向后定位, 在这种情况下最方便
      ArraySetAsSeries(TickPriceBuffer,true);
      ArraySetAsSeries(SignalBuffer,true);
      ArraySetAsSeries(SignalColors,true);
      ArraySetAsSeries(DeltaTickBuffer,true);
      first=false;
     }
...
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| 应用瞬时报价进行计算                                              |
//+------------------------------------------------------------------+
void ApplyTick(double price)
  {
   int size=ArraySize(TickPriceBuffer);
   ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,size-1);
   ArrayCopy(SignalBuffer,SignalBuffer,1,0,size-1);
   ArrayCopy(SignalColors,SignalColors,1,0,size-1);  
   ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,1,0,size-1);
//--- 保存最后的价格值
   TickPriceBuffer[0]=price;
//--- 计算与前一数值的差值
   DeltaTickBuffer[0]=TickPriceBuffer[0]-TickPriceBuffer[1];
//--- 得到标准差
   double stddev=getStdDev(ticks);  


现在计算标准差的一切都已经准备好了。首先, 编写 getStdDev() 函数来执行所有的 "暴力" 计算, 根据需要使用尽可能的循环, 迭代数组里的所有元素。

//+------------------------------------------------------------------+
//| "暴力" 计算标准差                                                 |
//+------------------------------------------------------------------+
double getStdDev(int number)
  {
   double summ=0,sum2=0,average,stddev;
//--- 统计变化之和, 并计算期望收益
   for(int i=0;i<ticks;i++)
      summ+=DeltaTickBuffer[i];
   average=summ/ticks;
//--- 现在计算标准差
   sum2=0;
   for(int i=0;i<ticks;i++)
      sum2+=(DeltaTickBuffer[i]-average)*(DeltaTickBuffer[i]-average);
   stddev=MathSqrt(sum2/(number-1));
   return (stddev);
  }

此后, 添加负责在瞬时报价图表上放置信号的模块 - 红色和蓝色圆圈

//+------------------------------------------------------------------+
//| 应用瞬时报价进行计算                                              |
//+------------------------------------------------------------------+
void ApplyTick(double price)
  {
   int size=ArraySize(TickPriceBuffer);
   ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,size-1);
   ArrayCopy(SignalBuffer,SignalBuffer,1,0,size-1);
   ArrayCopy(SignalColors,SignalColors,1,0,size-1);
   ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,1,0,size-1);   
//--- 保存最后的价格值
   TickPriceBuffer[0]=price;
//--- 计算与前一数值的差值
   DeltaTickBuffer[0]=TickPriceBuffer[0]-TickPriceBuffer[1];   
//--- 得到标准差
   double stddev=getStdDev(ticks);   
//--- 如果价格变化超出指定的阀值
   if(MathAbs(DeltaTickBuffer[0])>gap*stddev) // 将会在第一个瞬时报价上显示一个信号, 保留它作为一个 "特征"
     {
      SignalBuffer[0]=price;     // 放置一个圆点
      string col="Red";          // 圆点省缺为红色
      if(DeltaTickBuffer[0]>0)   // 价格暴涨
        {
         SignalColors[0]=1;      // 则圆点为蓝色
         col="Blue";             // 保存日志
        }
      else                       // 价格暴跌
      SignalColors[0]=0;         // 圆点为红色
      //--- 输出消息至智能程序日志
      PrintFormat("瞬时报价=%G 变化=%.1f pts, 触发=%.3f pts,  标准差=%.3f pts %s",
                  TickPriceBuffer[0],DeltaTickBuffer[0]/_Point,gap*stddev/_Point,stddev/_Point,col);
     }
   else SignalBuffer[0]=0;       // 无信号      
//---
  }

按下 F5 键 (开始/继续 调试) 并在 MetaTrader 5 终端里观察指标的操作。

现在是代码 调试, 可以识别错误, 并改善程序运行速度。


代码剖析: 操作提速

执行速度对于实时运行的程序是至关重要的。MetaEditor 的开发框架可以轻松、快速地评估每一部分代码消耗的时间。为此, 就必须运行代码分析器, 让程序工作一段时间。对于剖析这个指标, 一分钟就足够了。

如您所见, 大部分时间 (59.29%) 花费在 ApplyTick() 函数处理上, 它在 OnCalculate() 函数里被调用了 41 次。而 OnCalculate() 自己被调用了 143 次, 但接收的瞬时报价与前一次有差别的仅有 41 次。与此同时, 在 ApplyTick() 函数本身之内, 大部分时间消耗在调用 ArrayCopy() 函数, 它仅执行辅助动作, 且不会执行指标涉及的计算。在 138 行执行的标准差计算仅占用总程序执行时间的 2.58%。 

让我们来设法降低非生产性成本。为此, 尝试从数组里仅拷贝最近的 200 个元素, 而非全部 (TickPriceBuffer, 等)。毕竟, 200 个最新值已经足够, 此外, 在单笔交易会话中, 瞬时报价数量可能达到数十或数十万次。此刻没有必要查看它们。因此, 引入一个输入参数 - shift=200, 定义要进行数值移位的数量。将标记为黄色的行添加到代码:

//--- 输入参数
input int      ticks=50;         // 计算所用瞬时报价数量
input int      shift=200;        // 数值移位的数量
input double   gap=3.0;          // 通道宽度的标准差
...
void ApplyTick(double price)
  {
//--- 每笔瞬时报价时指标缓存区需要移位的元素数量
   int move=ArraySize(TickPriceBuffer)-1;
   if(shift!=0) move=shift;
   ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,move);
   ArrayCopy(SignalBuffer,SignalBuffer,1,0,move);
   ArrayCopy(SignalColors,SignalColors,1,0,move);
   ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,1,0,move);



再次运行剖析器并查看新结果 — 拷贝数组的时间已经降低了数百或数千倍, 现在大部分时间用来调用 StdDev(), 它负责计算标准差。

所以, ApplyTick() 的操作速度已经被提升了若干数量级, 在策略优化及实时工作时, 它可节省大量时间。毕竟, 计算资源永远不会太富余。


分析代码优化

有些时候, 优化编写的代码可以得到更快的速度运行。在这种情况下, 如果公式稍加修改, 即可加速标准差的计算。 


如此, 简单地计算价格增量的平方之与和的平方就成为可能。这可以在每笔瞬时报价之时执行更少的数学运算。每笔瞬时报价之时简单地减去数组的遗弃元素, 并将接收的数组元素加入包含合计的变量。

创建新的 getStdDevOptimized() 函数, 应用熟悉的方法, 移位自身的数组数值。

//+------------------------------------------------------------------+
//| 基于公式计算标准差                                                |
//+------------------------------------------------------------------+
double getStdDevOptimized(int number)
  {
//---
   static double X2[],X[],X2sum=0,Xsum=0;
   static bool firstcall=true;
//--- 首次调用
   if(firstcall)
     {
      //--- 设置动态数组大小, 大于瞬时报价数量加一
      ArrayResize(X2,ticks+1);
      ArrayResize(X,ticks+1);
      //--- 在计算初始, 确保非零数值
      ZeroMemory(X2);
      ZeroMemory(X);

      firstcall=false;
     }
//--- 数组移位
   ArrayCopy(X,X,1,0,ticks);
   ArrayCopy(X2,X2,1,0,ticks);
//--- 计算新的接收数值合计
   X[0]=DeltaTickBuffer[0];
   X2[0]=DeltaTickBuffer[0]*DeltaTickBuffer[0];
//--- 计算新合计
   Xsum=Xsum+X[0]-X[ticks];
   X2sum=X2sum+X2[0]-X2[ticks];
//--- 标准方差
   double S2=(1.0/(ticks-1))*(X2sum-Xsum*Xsum/ticks);
//--- 统计瞬时报价之和, 并计算期望收益
   double stddev=MathSqrt(S2);
//---
   return (stddev);
  } 

我们加入第二种计算标准差的方法, 通过 getStdDevOptimized() 函数调用 ApplyTick(), 并再次运行代码剖析。

//--- 计算与前一数值的差值
   DeltaTickBuffer[0]=TickPriceBuffer[0]-TickPriceBuffer[1];
//--- 得到标准差
   double stddev=getStdDev(ticks);
   double std_opt=getStdDevOptimized(ticks);

执行结果:

很明显, 新的 getStdDevOptimized() 函数只需要一半的时间 — 7.12%, 不像在 getStdDev() 中的暴力方法 — 15.50%。因此, 使用优化的计算方法, 为程序的操作速度赋予了更大增益。更多详情请阅读文章 以线性回归为例, 加速指标的 3 种方法

顺带说, 有关调用标准函数 - 在此指标中价格取自 close[] 时间序列, 它是基于供给价。有两种以上方式可获取此价格 — 使用 SymbolInfoDouble()SymbolInfoTick() 函数。我们在代码里添加这些调用, 并再次运行剖析器。

如您所见, 运算速度在此也有差异。这个也有道理, 因为与通用函数不同, 从 close[] 里读取就位价格不需要任何额外开销。

在测试器里基于实际的瞬时报价调试

当编写指标和交易机器人时, 不可能预见联线操作中所有可能出现的情况。幸运的是, MetaEditor 允许使用历史数据进行调试。在可视测试模式下简单地运行调试, 您能够在指定的历史时间间隔内测试程序。可以加速, 暂停以及跳过测试至期望的日期。

重要提示:调试窗口, 设置瞬时报价模型为 "每笔瞬时报价基于实际瞬时报价"。这将允许使用交易服务器存储的实际报价来调试。它们将在第一次测试时自动下载到您的计算机。

如果这些参数没有在 MetaEditor 中设置, 则可视测试将使用当前 测试设置。在其中指定"每笔瞬时报价基于实际瞬时报价" 模式。



在瞬时报价图表上可以看到奇怪的缺口。这意味着算法有错误。因为谁也不知道在实时测试时, 会取用多少来跟踪。在此情况下, 可视测试器的日志在新柱线出现之时显示发生的奇怪缺口。就是这样!— 我们忘了, 在过渡到新柱线时, 指标缓存区的大小按 1 递增。调整的代码:

void ApplyTick(double price)
  {
//--- 保存 TickPriceBuffer 数组大小 - 它等于图表上的柱线数量
   static int prev_size=0;
   int size=ArraySize(TickPriceBuffer);
//--- 如果指标缓存区的大小无变化, 向后移动元素 1 个位置
   if(size==prev_size)
     {
      //--- 每笔瞬时报价时指标缓存区需要移位的元素数量
      int move=ArraySize(TickPriceBuffer)-1;
      if(shift!=0) move=shift;
      ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,move);
      ArrayCopy(SignalBuffer,SignalBuffer,1,0,move);
      ArrayCopy(SignalColors,SignalColors,1,0,move);
      ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,1,0,move);
     }
   prev_size=size;
//--- 保存最后的价格值
   TickPriceBuffer[0]=price;
//--- 计算与前一数值的差值

运行可是测试并放置一个断点, 以便捕捉一根新柱线开盘的时刻。加入数值来查看, 并确保一切正确: 在图表上的柱线数又上升了一根, 当前柱线的瞬时报价交易量为 1 - 这是新柱线的第一笔瞬时报价。

如此, 代码优化已执行, 错误已修复, 不同函数的执行时间已测量。现在, 指标已经准备开工。我们现在可以运行 可视测试, 并在瞬时报价图表上观察信号出现后会发生什么。哪些方面可以进一步改进?编码完美主义者会说是的!尚未尝试使用 轮转缓存区 来改进操作速度。有兴趣者可以自行检查 — 它能否将性能提升?


MetaEditor 是开发交易策略的一个现成实验室

为了编写一个自动交易系统, 重要的不仅在于便利的开发环境和强力的编程语言, 还要额外的调试和程序校准工具。本文介绍了:

  1. 如何在一两分钟内创建瞬时报价图表;
  2. 如何在图表上通过按 F5 键使用实时模式调试;
  3. 如何运行代码剖析来确定低效的代码部分;
  4. 如何在可视l测试模式里基于历史数据进行快速调试;
  5. 如何在调试期间查看所需变量的数值。

开发显示交易信号的指标, 往往是创建交易机器人所需的第一步。可视化有助于制定交易规则, 或在项目开工之前驳回想法。

利用 MetaEditor 开发环境的所有功能, 创建高效的交易机器人!

相关文章:

  1. MQL5: 创建您自己的指标
  2. 利用 MQL5 创建瞬时报价指标
  3. 财务计算指标原理
  4. 无需使用其它缓冲区中间计算均价系列
  5. 调试 MQL5 程序