量化交易软件:以经济方式计算指标的原则
简介
在人类的实践活动的一个或多个领域中保存资源的想法或许是人类发展与进步的过程中最重要和最紧迫的主题。在这一点上,用 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"(每一价格变动)!

编辑切换为居中
以下是在测试程序中运行经过优化的指标的结果:

编辑切换为居中
红色表示测试所用的时间。无法说这太长了!但是对于指标 SMA !!!!!!. mq5 的测试完成,赫兹量化不得不等待很长的时间!

编辑切换为居中
基本上,在此情形中的测试处理时间与上一个测试所用的时间相差 500 多倍。尽管选择了足够短的测试周期。但是在测试 EA 交易程序期间,赫兹量化能够造成如此之大的计算成本,我们最好忘记它们的参数的优化!
因此,这是最有力的证据,证明以经济的方式编写代码对于编程领域内的专业人士而言不仅仅是一项娱乐,也是编写您自己的代码的非常有针对性的方法。
在互联网上有一个专门加速个人计算机以最大程度提高其性能的网站 Overclockers.ru。此实践的基本方式是使用较为昂贵的计算机组件来提高 CPU 和 RAM 内存的时钟速度。
完成此项工作之后,对于此超频 CPU,使用更加昂贵的水冷却系统,甚至浸入液氮处理器。此类操作的结果是 PC 性能增加两倍甚至三倍。
以经济方式编写的胜任代码通常能够帮助我们事半功倍。当然,此方法不能将 Celleron300A 转换为 Core 2 Quad Q6600,但是它确实允许我们使一台常规的标准预算 PC 以顶配计算机才具备的性能工作!
在某些不是很经典的指标中反复重新计算当前已收盘的指标柱
如果程序代码优化的这一方法无差别地适合所有指标,则一切事情都会很美好了!但是,唉,这不是真的。有一组指标,如果采用此类方法,在将指标加载到已经存在的历史数据期间仅开始正常计算一次。
在加载指标后出现的所有指标柱上,其值变为完全不正确。发生这种情况的主要原因在于来自指标的某些变量取决于在前一指标柱上计算指标之后具有相同变量的那些指标。正式地,它看起来如下所示:
SomeVariable(bar) = Function(SomeVariable(bar - 1))
其中:
SomeVariable() — 某指标柱的某个变量的值;
bar - 在其上进行计算的指标柱的编号。
出于显而易见的原因,在实际代码中,此类依存关系具有很少的清晰函数形式。但是本质并没有改变,例如,对于移动 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 []。
第二,在每一次指标价格变动时,只需要复制以下三种情形下中的值:
从新出现的指标柱
从已收盘的指标柱
从当前未收盘的指标柱
指标缓存中剩余的值已经存在,并没有多次重写它们的必要。