以经济方式计算指标的原则

Nikolay Kositsin | 18 十一月, 2013

简介

在人类的实践活动的一个或多个领域中保存资源的想法或许是人类发展与进步的过程中最重要和最紧迫的主题。在这一点上,用 MQL5 语言编程也不例外。当然,如果任务的范围仅仅局限于可视化交易,则编程的很多缺陷仍然能够处于未被发现的状况。

但是与自动交易有关的所有一切,开始都需要以最大的经济性编写代码,否则交易机器人的测试和优化过程可能延长到几乎不可能等待它们完成的时间。在此类情形中创建某些有价值的东西的想法似乎很不现实。

因此,在着手实施交易策略之前,最好更好地熟悉对 EA 交易程序的优化和测试时间有影响的编程细节。因为大部分的 EA 交易程序在它们的代码中包含对用户指标的调用,因此我认为我们应该从它们开始。

一般而言,在构建指标时并没有很多要必须记住的相关要点,因此按顺序简单地回顾一下它们是最符合逻辑的。

于经典指标中在尚未计算的新指标柱的每一次价格变动时重新计算指标

RSI、ADX、ATR、CCI 等经典指标的本质是在已收盘的指标柱上,这些指标的计算只能进行一次,之后只能在新出现的指标柱上进行计算。唯一例外是当前尚未收盘的指标柱,在其上每一次价格变动时都会进行计算,直到该指标柱收盘为止。

找出在尚未计算的指标柱上计算指标是否合理的最简单的方式是在策略测试程序中将此类(经过优化的)指标的运行结果与在所有指标柱上计算的所有时间的指标(未优化)进行比较。

这很简单。使用空函数 OnInit () 和 OnTick () 创建了一个 EA 交易程序。您要做的只是在 EA 交易程序中调用经过优化的或未经优化的指标的所需版本,并在两种情形下赞美此类 EA 交易程序在测试程序中的运行结果。我将采用在本人的 "User Indicators in MQL5 for Beginners"(面向初学者的 MQL5 用户指标)一文中所写的指标 SMA.mq5 作为一个例子,在该指标中,我会替换一行代码。  

   if (prev_calculated == 0) // 如果是第一次计算,重新计算所有已存在的柱
    first = MAPeriod - 1 + begin;
   else first = prev_calculated - 1; // 在之后的计算中, 只计算新出现的柱

替换为 

   first = MAPeriod -  1  + Begin;  / / 有订单时重新计算全部柱 

结果,我得到一个未经优化的编程代码版本 (SMA !!!!!!. mq5),该版本与原来的不同,将在每一次价格变动时重新计算所有的值。严格地讲,在两种情况下,EA 交易程序的代码在实际上都是相同的,因此我仅提供其中一个 (SMA_Test.mq5)

//+------------------------------------------------------------------+
//|                                                     SMA_Test.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
int Handle;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//----+
   //----+ 取得指标句柄
   Handle = iCustom(Symbol(), 0, "SMA");
   if (Handle == INVALID_HANDLE)
     Print(" 无法获得SMA指标句柄");
//----+
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//----+
    //--- 释放指标句柄
    IndicatorRelease(Handle);
//----+   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//----+
   double SMA[1];
   //----+ 使用指标句柄把指标 
                   // 缓冲区的值复制到特别准备好的静态数组中
   CopyBuffer(Handle, 0, 0, 1, SMA);
//----+
  }
//+------------------------------------------------------------------+

现在我们可以开始测试了。应该注意,在本文的所有测试中,我们将使用与实际情况非常接近的指标柱改变的模拟模式 -"Every tick"(每一价格变动)!

图 1 EA 交易程序 SMA_Test 的测试配置 

以下是在测试程序中运行经过优化的指标的结果:

图 2 EA 交易程序 SMA_Test 的测试结果 

红色表示测试所用的时间。无法说这太长了!但是对于指标 SMA !!!!!!. mq5 的测试完成,我们不得不等待很长的时间!

图 3 EA 交易程序 SMA !!!!!!_ Test 的测试结果 

基本上,在此情形中的测试处理时间与上一个测试所用的时间相差 500 多倍。尽管选择了足够短的测试周期。但是在测试 EA 交易程序期间,我们能够造成如此之大的计算成本,我们最好忘记它们的参数的优化!

因此,这是最有力的证据,证明以经济的方式编写代码对于编程领域内的专业人士而言不仅仅是一项娱乐,也是编写您自己的代码的非常有针对性的方法。

在互联网上有一个专门加速个人计算机以最大程度提高其性能的网站 Overclockers.ru。此实践的基本方式是使用较为昂贵的计算机组件来提高 CPU 和 RAM 内存的时钟速度。

完成此项工作之后,对于此超频 CPU,使用更加昂贵的水冷却系统,甚至浸入液氮处理器。此类操作的结果是 PC 性能增加两倍甚至三倍。

以经济方式编写的胜任代码通常能够帮助我们事半功倍。当然,此方法不能将 Celleron300A 转换为 Core 2 Quad Q6600,但是它确实允许我们使一台常规的标准预算 PC 以顶配计算机才具备的性能工作!

在某些不是很经典的指标中反复重新计算当前已收盘的指标柱

如果程序代码优化的这一方法无差别地适合所有指标,则一切事情都会很美好了!但是,唉,这不是真的。有一组指标,如果采用此类方法,在将指标加载到已经存在的历史数据期间仅开始正常计算一次。

在加载指标后出现的所有指标柱上,其值变为完全不正确。发生这种情况的主要原因在于来自指标的某些变量取决于在前一指标柱上计算指标之后具有相同变量的那些指标。正式地,它看起来如下所示:

                                                                                                                                                                                                                           

SomeVariable(bar) = Function(SomeVariable(bar - 1))

 

其中:

出于显而易见的原因,在实际代码中,此类依存关系具有很少的清晰函数形式。但是本质并没有改变,例如,对于移动 T3(未经优化的指标 - T3 !!!!!!. mq5),与我们相关的代码部分看起来如下所示:

   e1 = w1 * series + w2 * e1;
   e2 = w1 * e1 + w2 * e2;
   e3 = w1 * e2 + w2 * e3;
   e4 = w1 * e3 + w2 * e4;
   e5 = w1 * e4 + w2 * e5;
   e6 = w1 * e5 + w2 * e6;
   //----  
   T3 = c1 * e6 + c2 * e5 + c3 * e4 + c4 * e3;

变量 e1、e2、e3、e4、e5、e6 正好有此类函数依存关系,涉及使用此代码来计算新的指标柱一次!但是当前指标柱,通过类似的计算,将反复被跳过,直到其收盘为止。

这些变量在当前指标柱上的值将一直变化,尽管对于当前指标柱,在改变之前,它们应该在计算上一指标柱之后保持不变!

因此,这些变量在上一指标柱(相对于当前指标柱)的值应保存在静态变量中,并且转移它们以便在下一次指标柱改变时重复使用,在新的指标柱上,变量的倒数第二个值应再次保存为 e1、e2、e3、e4、e5、e6。

对值进行类似处理的其他代码也很简单。首先,您应在函数 OnCalculate () 内声明用于存储值的局部静态变量

   //---- 声明用于存储系数有效值的静态变量
   static double e1_, e2_, e3_, e4_, e5_, e6_;

之后,在新出现的指标柱的编号大于零时,在当前柱上进行任何计算之前,在循环中记住变量的值:

     //---- 在运行至当前柱之前记录变量值
     if (rates_total != prev_calculated && bar == rates_total - 1)
      {
       e1_ = e1;
       e2_ = e2;
       e3_ = e3;
       e4_ = e4;
       e5_ = e5;
       e6_ = e6;
      }

在循环运算符块之前,通过反转恢复变量的值:

   //---- 恢复变量值
   e1 = e1_;
   e2 = e2_;
   e3 = e3_;
   e4 = e4_;
   e5 = e5_;
   e6 = e6_;

很自然地,现在,计算系数的开始初始化在函数 OnCalculate () 第一次启动时只进行一次,并且现在并不是用系数本身进行计算,而是用对应的静态变量进行计算。

//---- 首先计算用于重新计算柱的起始编号
   if (prev_calculated == 0) // 确认这是指标的第一次计算
    {
     first = begin; // 计算所有柱的起始编号
     //---- 开始所计算系数的初始化
     e1_ = price[first];
     e2_ = price[first];
     e3_ = price[first];
     e4_ = price[first];
     e5_ = price[first];
     e6_ = price[first];
    }

结果,最终的指标 T3.mq5 开始以最符合成本效益的方式进行计算。所有一切都不重要,但并不是始终都能轻松识别类似的函数依存关系。在这个例子中,所有指标变量的值都可以保存在静态变量中,并且以相同的方式恢复。

并且我们只能在后来开始指出哪些变量真的需要恢复,哪些变量没有此需要。为此,我们需要将未经优化的指标和经过优化的指标挂在图表上,并且检查它们的工作,逐渐地从恢复列表中一次移除一个变量。最后,我们仅剩下那些真正需要恢复的变量。

自然地,我提供这一版本的逻辑来处理普通指标的程序代码,在代码中会重新统计当前指标柱和新出现的指标柱。对于重绘并着眼将来的指标,由于这些指标的特点,我们不能创建一个类似的、非常标准和简单的代码优化方法。并且大多数经验丰富的 EA 交易程序编写者并不认为有这种需要。因此,这是我们可以考虑详细分析这些指标完成的地方。

可能导致 MQL5 代码异常缓慢的指标调用的特点 

似乎任务已经完成,我们得到了经过优化的指标,该指标以最经济的方式统计指标柱,并且现在足以编写几行代码,在 EA 交易程序或指标的代码中调用此指标,以从指标缓存中获得计算值。

但是如果到了正式阶段,都没有指出这几行代码包含什么类型的操作,这就并不像看起来的那么简单。

正如在 MQL5 中从时间序列获取值一样,从用户指标和技术指标获取值的特点是通过将数据复制到用户数组变量来进行。对于当前帐户,这可能导致建立完全不必要的数据。

最容易的方式是从某些技术指标在某个数据接收关系上验证所有这一切。作为一个例子,我们可以采用移动 iAMA,并依据此技术指标建立一个自定义指标 AMkA。

为了复制数据,我们将使用函数调用 CopyBuffer () 的第一版本,并为复制请求起始位置和需要的元素数量。在 AMkA 指标中,使用技术指标标准方差在当前指标柱上处理移动增量,之后,为了获取交易信号,将此增量与已经处理的总标准方差值进行比较。

因此,在针对 AMkA 指标实施的最简单的情况中,您应首先创建一个指标,在该指标中,指标缓存包含移动增量的值(指标 dAMA)。接着,在另一指标中,我们使用指标句柄及 AMA 增量获得经过标准方差指标处理而得到的值。

创建类似指标的过程已经在有关这些主题的各种文章中得到详细解释,因此我将不会暂停于此,并且将仅分析在其他指标的代码中存取所调用指标的指标缓存的细节。

在广阔的互联网资源中,我们已经看到 MQL5 例子的出现,它们的作者照字面将指标缓存的整个内容复制到中间动态数组。之后,使用循环运算符,从这些中间数组将所有的值逐个传输到最终的指标缓存。

要解决我们的问题,此方法看起来非常简单

   if (CopyBuffer(AMA_Handle, 0, 0, rates_total, Array) <= 0) return(0);
   ArrayCopy(AMA_Buffer, Array, 0, 0, WHOLE_ARRAY);

(指标 dAMA !!!!!!. mq5)或如下所示 

   if (CopyBuffer(AMA_Handle, 0, 0, rates_total, Array) <= 0) return(0);
   
   for(bar = 0; bar < rates_total; bar++)
    {
     AMA_Buffer[bar] = Array[bar];
     /*
      这里是指标计算的代码
    */     
    }

但是此类自然的解决方案价值如何呢?首先,最好进一步了解操作的最佳合理过程是什么。第一,使用中间数组 Array [] 并不是必须的,数据应直接复制到指标缓存 AMA []。

第二,在每一次指标价格变动时,只需要复制以下三种情形下中的值:

指标缓存中剩余的值已经存在,并没有多次重写它们的必要。 

//--- 计算所需复制数据的数量
   int to_copy;
   if(prev_calculated > rates_total || prev_calculated <= 0)// 确认第一次进行指标计算
        to_copy = rates_total - begin; // 计算全部柱的数量
   else to_copy = rates_total - prev_calculated + 1; // 仅计算新柱的数量

//--- 把重新出现的数据复制到指标缓冲区 AMA_Buffer[]
   if (CopyBuffer(AMA_Handle, 0, 0, to_copy, AMA_Buffer) <= 0)
    return(0); 

 很自然,在这种情形中的最终代码将有点复杂(指标 dAMA.mq5),但是现在我们能够使用我在本文开头提出的方法在两种情形中进行测试,并得出相应的结论。这一次,让我们将测试周期增加到一年。

图 4 EA 交易程序 dAMA_Test 的测试配置
 

最后,在通过测试之后,在策略测试程序的日志中,我们获得了必要的 EA 交易程序 dAMA_Test 的测试时间。

图 5 EA 交易程序 dAMA_Test 的测试结果

测试所用时间 43,937 ms 在合理范围以内。很不幸,对 EA 交易程序 dAMA !!!!!!_ Test 的类似测试所用时间就无话可说了。

图 6 EA 交易程序 dAMA !!!!!!_ Test 的测试结果
  

测试所用时间为 960 625 ms,是前一情形的 20 多倍。结论似乎很显然。应该以最经济的方式编写代码,从而不必执行任何不必要的计算!
按上述原则建立的 AMkA 指标并没有证明任何新的东西,因此我将仅仅暂停于在这种情形中复制数据的细节。

//---- 声明局部数组
   double dAMA_Array[], StdDev_Array[];
//---- 数组的元素指数类似时间序列
   ArraySetAsSeries(dAMA_Array, true);
   ArraySetAsSeries(StdDev_Array, true);

//--- 计算所需复制的数据数量
   int to_copy;
   if(prev_calculated > rates_total || prev_calculated <= 0)// 确认进行第一次指标计算
        to_copy = rates_total - begin; // 计算全部柱的数量
   else to_copy = rates_total - prev_calculated + 1; // 仅计算新柱的数量
   
//--- 把新出现的数据复制到指标缓冲区和局部动态数组中
   if(CopyBuffer(dAMAHandle,   1, 0, to_copy, AMABuffer   ) <= 0) return(0);
   if(CopyBuffer(dAMAHandle,   0, 0, to_copy, dAMA_Array  ) <= 0) return(0);
   if(CopyBuffer(StdDevHandle, 0, 0, to_copy, StdDev_Array) <= 0) return(0);

除了现在数据被复制到一个指标缓存并且两个局部声明用于中间计算的动态数组以外,其他一切都以一种完全类似的方式进行。 

作为优化方式之一,在指标内实施所有指标计算 

所有这一切都非常有趣,但是连续调用用户指标和技术指标的结构是如此复杂,看起来有点可疑。应通过某些方式对其进行测试。但是要这样做,不应伤害 AMkA 指标的代码,该代码应在用户指标内,并且不能调用其他指标。

对于彻底掌握了用 MQL5 编写指标的过程的程序员而言,这个问题并不需要太多工作。首先,用户指标 AMA.mq5 的代码已经写好,其次,已经向代码添加了实施 AMkA_.mq5 指标所需的必要元素。我们收到指标计算的第二个大型循环,在该循环中,将 AMA 指标的增量加载到用户数组,

   //---- 计算 AMkA 指标的主循环
   for(bar = first; bar < rates_total; bar++)
    {
     //---- 把AMA指标的增量载入数组用于中间计算
     for(iii = 0; iii < ama_period; iii++)
      dAMA[iii] = AMABuffer[bar - iii - 0] - AMABuffer[bar - iii - 1]; 

然后用此数组进行运算,类似于依据 dAMA.mq5 指标计算技术指标 StDev。

     //---- 计算AMA增量的简单平均数
     Sum = 0.0;
     for(iii = 0; iii < ama_period; iii++)
      Sum += dAMA[iii];
     SMAdif = Sum / ama_period;
     
     //---- 计算增量的平方差的和以及平均数
     Sum = 0.0;
     for(iii = 0; iii < ama_period; iii++)
      Sum += MathPow(dAMA[iii] - SMAdif, 2);
     
     //---- 从 AMA 增量计算 StDev 均方差的最终值
     StDev = MathSqrt(Sum / ama_period);

余下的代码与 AMkA.mq5 指标绝对类似,不会让我们有任何特别兴趣。现在,我们可以在 EA 交易程序 AMkA__Test.mq5 和 AMkA_Test.mq5 的帮助下开始测试指标 AMkA_.mq5 和 AMkA.mq5。

测试 AMkA_.mq5 指标时不会出现难度,并且测试时间也在可充分接受的范围内。

图 7 使用 EA 交易程序 AMkA__Test 进行测试的结果
 

但是指标 AMkA.mq5 非常慢

图 8 使用 EA 交易程序 AMkA_Test 进行测试的结果
 

其结果比其“兄弟”慢了七倍多。能有其他评价吗?结论很显然:建立如此复杂的结构,包括几个对指标的连续调用,从一个到另一个,并不是非常谨慎,仅适合初步测试!

显然,这些结果是在客户端的测试版本上获得的,现在还难以判断将来会变成什么样子。但是对于即将出现的交易机器人冠军,可以非常明确地认为这是一个相关且有效的主题。

从 EA 交易程序调用指标的某些特点

适合对在指标的编程代码中存取用户数据和技术指标进行优化的一切方法也同样适合对在 EA 交易程序的编程代码中存取用户数据和技术指标进行优化。除了已经介绍的情形以后,EA 交易程序的代码还有另一因素,该因素可能显著影响交易系统的测试和优化。

相当多的 EA 交易程序通常仅在指标柱改变时才处理指标数据,因此,在这些 EA 交易程序中,无需在每次价格变动时都调用函数 CopyBuffer ()

在这种情形下,EA 交易程序仅需要在指标柱改变期间从指标缓存获取数据。因此,如果来自指标缓存的所有必要数据都被成功复制到用于中间计算的数组,指标的调用应位于括号后面的代码块内,对于指标柱的每次改变,仅允许对其访问一次。

担当此类过滤器的最好东西是一个用户函数,在当前指标柱改变时,该函数返回时间的逻辑单位。文件 IsNewBar.mqh 包含我编写的这个函数的一个非常通用的版本:

bool IsNewBar
            (
             int Number, // 在EA交易程序代码中调用 IsNewBar 函数的编号
             string symbol, // 进行数据计算的图表交易品种
             ENUM_TIMEFRAMES timeframe // 进行数据计算的图表时间框架
            )

在 EA 交易程序中使用此函数时,它看起来可能如下所示:

    //---- 声明静态变量 - 用于储存 AMA 指标值的数组
    static double AMA_Array[3];

    //---- 调用AMA指标,复制其数据到AMA_Array数组
    if (IsNewBar(0, Symbol(), 0))
     {
      CopyBuffer(AMA_Handle, 0, 1, 3, AMA_Array); 
     }

但是在这个例子中以不同的方式进行更为合理。事实在于,当您调用 CopyBuffer () 时,数组 AMA_Array [] 中的数据可能未被复制,在这种情况下,您将需要在每次价格变动时调用此函数,直到成功复制了数据,这通过向过滤器增加一定的复杂程度来实施。

   //---- 声明静态变量- 用于储存AMA指标值的数组
   static double AMA_Array[3];
    
   //---- 声明静态变量用于保存从AMA指标复制数据的结果
   static bool Recount;

   //---- 调用AMA指标,复制其数据到AMA_Array数组
   if (IsNewBar(0, Symbol(), 0) || Recount)
     {
      if (CopyBuffer(AMA_Handle, 0, 1, 3, AMA_Array) < 0)
       {
        Recount = true; // 复制数据的尝试没有成功 
        return; // 退出 OnTick() 函数
       }
      
      //---- 所有从指标缓冲区复制数据的操作都成功完成
           // 直到下一次柱改变都无需回到此区块
      Recount = false;
     }

现在,在 EA 交易程序中合理调用指标值的复制函数的细节已经变得清楚了,您可以测试在 EA 交易程序中应用函数 IsNewBar () 所带来的好处。

现在,我们有了两个可在策略测试程序中进行测试的 EA 交易程序选择,第一个为 AMA_Test.ex5。它在每次价格变动时从指标缓存复制数据。

图 9 使用 EA 交易程序 AMA_Test 进行测试的结果

第二个为 IsNewBar_AMA_Test.mq5,仅在指标柱改变期间复制数据。

图 10 使用 EA 交易程序 IsNewBar_AMA_Test 进行测试的结果

是的!测试结果有点令人失望。结果证明在每次价格变动时调用函数 IsNewBar () 比将数据复制到用户数组的三个单元格要花费很多! 

在这里,我愿意提醒您注意指标的另一个重要但似乎不显眼的部分。事实在于,如果我们在 OnInit () 函数中获得指标的句柄,则无论我们是否在函数 OnTick () 内从这个指标复制数据,其在一根尚未计算的当前指标柱上的计算,仍然会在每次价格变动时进行。

因此,如果我们的 EA 交易程序不需要来自当前未收盘的指标柱上的统计指标值,则从节省时间的观点而言,最好禁止这些值的计算。这很简单 - 在改变之前,将指标内重新统计指标柱的主循环的右边界减 1,AMA.mq5 指标中的这个循环看起来如下所示

   //---- 指标计算主循环
   for(bar = first; bar < rates_total; bar++)

在改变之后,它看起来将如下所示  

   //---- 指标计算主循环
   for(bar = first; bar < rates_total - 1; bar++)

指标 AMA_Ex.mq5。现在您可以测试这个指标(EA 交易程序 AMA_Ex_Test.mq5)

图 11 使用 EA 交易程序 AMA_Ex_Test 进行测试的结果 

当然,此结果比 AMA 指标的测试好 21%,该指标的结果不算太坏,但是如果我们思考一下,此结果会好很多。

总结

最终而言,程序代码的效率是一个非常客观的参数。效果可以测量,进行逻辑分析,在某些情形下还能显著提升。实现这些任务的方法并不是非常复杂。需要的只是一些耐心,并且除了进行一些直接影响自动交易系统的盈利能力的实践以外,还要做得更多。