
MQL5 中的事件处理:快速更改 MA 周期
简介
这篇简短的文章致力于说明 MetaQuotes Software Corp 开发的 MetaTrader 5 平台的一个新的 MQL5 功能。也许本文发表的时间稍微有点晚(它应该在 2009 年 9-10 月间发布,这样就算得上及时了),但尚未有该主题的相似文章面世。此外,当时也不具备在指标中处理事件的可能性。
想象一下,我们将一些简单的价格指标应用至图表(本例中是移动平均线,即 MA),而我们想更改其平滑周期。在 MT4 平台中,我们有以下选项:
- 在 MetaEditor 中,您可以编辑负责 MA 周期的“EA 交易”的输入参数 (extern),然后编译源文件。
- 无需切换至 MetaEditor,您可以在终端窗口中打开指标的“属性”对话框,然后在其中编辑相应的输入参数。
- 您可以打开 Win32 API 库,找到消息捕获函数,然后微调指标代码,使其从键盘响应事件。
众所周知,事半功倍的愿望是促使我们进步的最大动力。现在,由于全新的 MT5 平台允许用户发起指标事件处理,我们可以略过上述可能性,按一下按键即可更改指标参数。本文介绍了问题解决方案的技术实施。
任务指派和问题
在我们的试验中使用的指标源代码随客户端一起提供。未更改的源代码文件 (Custom Moving Average.mq5) 已附于本文的末尾。
我们暂时不会分析源代码,尤其是相较其 MQL4 原型发生的变化。是的,有些地方发生了显著的变化,这并不总是显而易见的。有关重新构建计算的基本部分的相应说明可在论坛或在线帮助中找到。
然而,MQL4 中指标的主要部分仍保持未变。至少有 80% 的旨在解决问题的代码更改是基于指标的计算函数理念而作为“黑盒子”做出。
我们目标的一个示例如下所示。假设我们已将该指标应用至图表,且它在指定时刻显示零平移周期为 10 的指数 MA (EMA)。我们的目标是将简单 MA (SMA) 的平滑周期增加 3(达到 13),并将其向右平移 5 个柱。假定的操作序列如下所示:
- 按 TAB 键数次以将显示的 MA 从指数更改为简单(更改 MA 类型)。
- 按键盘主体部分的向上箭头键三次,将简单 MA 的周期增大 3。
- 按数字小键盘上的向上 (8) 箭头键 5 次,将 MA 向右移动 5 个柱。
基本的和最明显的解决方案是将 OnChartEvent() 函数插入指标代码并编写键击事件处理程序。根据 MetaTrader 4 客户端版本 245 和 246 中的更改列表 https://www.mql5.com/en/forum/53/page1/#comment_2655,
MetaTrader 5 客户端版本 245 和 246
…
MQL5:与通过“EA 交易”类似,通过自定义指标增加了事件处理的可能性。
因此,对于在指标中添加新的事件处理程序,我们已没有任何问题。但由于下述原因,我们仍需对代码稍作修改。
首先,在 MQL5 中指标外部参数的状态已改变:您无法在代码中修改参数。修改参数的唯一方法是在客户端中通过“属性”对话框进行。一般情况下,在更改外部参数的紧急需求中我们可轻易规避这一限制:将外部参数的值复制到指标的新的全局变量,然后执行所有计算,就好像这些新的变量实际就是指标的外部参数一样。另一方面,在此情况下外部参数的可行性消失,其值只能误导用户。现在,这些参数只不过不是必要的。
因此,指标中没有外部(输入)参数。现在,担任外部参数角色的变量是终端全局变量,或简称为 TGV。如果您想要查看作为指标前外部参数的 TGV,您只需在终端中按下 F3 键。我找不到第二种简单方法来控制指标参数。
其次(这很重要),指标的外部参数如果有任何变化,我们都必须重新计算它贯穿整个历史数据的值,从零再次开始。换言之,我们将不得不执行计算,而这通常仅发生在指标的首次启动时。指标的计算优化仍然存在,但现在它变得更加微妙了。
修改指标的第一个版本的几个代码部分如下所示。完整的代码已附于本文末尾。
“标准”版本:标准指标源代码的更改描述
外部参数不再是外部的,它们只不过是全局变量
指标的所有外部参数都失去了它们的 input 修饰符。一般情况下,我甚至无法将它们全局化,但我决定按照传统方法来实现这一点:
int MA_Period = 13; int MA_Shift = 0; ENUM_MA_METHOD MA_Method = 0; int Updated = 0; /// 指示指标在其值改变后是否有更新
前三个选项 - 分别是 MA 的周期、偏移和类型,而第四个选项 - Updated - 在更改 MA 参数时负责计算优化。说明请见下面几行。
虚拟键代码
输入虚拟键的代码:
#define KEY_UP 38 #define KEY_DOWN 40 #define KEY_NUMLOCK_DOWN 98 #define KEY_NUMLOCK_UP 104 #define KEY_TAB 9
这些代码用于“向上箭头”键,“向下箭头”键,数字小键盘上的类似箭头键(按键 "8" 和 "2")以及 TAB 键。<MT5dir>\MQL5\Include\VirtualKeys.mqh 文件中实际上存在相同的代码(具有 VK_XXX 常量的其他名称),但在此情况下我决定保留其原样。
对函数代码稍作修改,计算线性加权移动平均线 (LWMA)
我已对 CalculateLWMA() 函数稍作调整:在原始版本中,weightsum 变量使用静态修饰符声明。显然,开发人员这样做的唯一原因是需要在首次调用函数时对其进行预计算。此外,此变量在代码中保持不变。以下是该函数的原始代码,与计算和 weightsum 使用相关的部分均采用注释标出:
void CalculateLWMA(int rates_total,int prev_calculated,int begin,const double &price[]) { int i,limit; static int weightsum; // <-- 使用weightsum double sum; //--- 第一次计算或者柱的数量有变化 if(prev_calculated==0) // <-- 使用weightsum { weightsum=0; // <-- 使用 weightsum limit=InpMAPeriod+begin; // <-- 使用 weightsum //--- 设置首个limit个柱形的值为空 for(i=0;i<limit;i++) ExtLineBuffer[i]=0.0; //--- 计算首个可视值 double firstValue=0; for(i=begin;i<limit;i++) // <-- 使用 weightsum { int k=i-begin+1; // <-- 使用 weightsum weightsum+=k; // <-- 使用 weightsum firstValue+=k*price[i]; } firstValue/=(double)weightsum; ExtLineBuffer[limit-1]=firstValue; } else limit=prev_calculated-1; //--- 主循环 for(i=limit;i<rates_total;i++) { sum=0; for(int j=0;j<InpMAPeriod;j++) sum+=(InpMAPeriod-j)*price[i-j]; ExtLineBuffer[i]=sum/weightsum; // <-- 使用 weightsum } //--- }
此前,该变体运行良好,但当我在此类型的 MA 中运行“指标 + EA 交易”串联(这在本文末尾提及)时遇到了麻烦。主要问题由上述环境引起,即 weightsum 是静态变量:由于每次快速更改 MA 参数都有必要从头开始重新进行计算,该变量将不断增大。
我这是直接和间接计算 weightsum 值(它等于从 1 到 MA 周期的整数和 - 为此,有一个简单的公式可用于此算术级数的和)同时拒绝其静态状态的最简单的方法。现在,与之前使用静态修饰符声明 weightsum 不同,我们不使用该修饰符声明它,而仅使用“正确”的值对其进行初始化,因此去除了“变量累积”的初始循环。
int weightsum = MA_Period *( MA_Period + 1 ) / 2;
现在,一切正常。
作为处理程序的 OnCalculate() 函数
我对 OnCalculate() 函数进行了大量更改,因此,我将它的完整代码引用如下。
int OnCalculate(const int rates_total, const int prev_calculated, /// Mathemat:完整计算! const int begin, /// Mathemat:完整计算! const double &price[]) { //--- 检查柱形的数量 if(rates_total<MA_Period-1+begin) return(0);// not enough bars for calculation //--- 第一次计算或者柱的数量有变化 if(prev_calculated==0) ArrayInitialize(LineBuffer,0); //--- 设置绘图开始的第一个柱形的索引位置 PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,MA_Period-1+begin); //--- 计算 (由Mathemat优化) if( GlobalVariableGet( "Updated" ) == 1 ) { if(MA_Method==MODE_EMA) CalculateEMA( rates_total,prev_calculated,begin,price); if(MA_Method==MODE_LWMA) CalculateLWMA_Mthmt(rates_total,prev_calculated,begin,price); if(MA_Method==MODE_SMMA) CalculateSmoothedMA(rates_total,prev_calculated,begin,price); if(MA_Method==MODE_SMA) CalculateSimpleMA( rates_total,prev_calculated,begin,price); } else { OnInit( ); /// Mthmt if(MA_Method==MODE_EMA) CalculateEMA( rates_total,0,0,price); if(MA_Method==MODE_LWMA) CalculateLWMA_Mthmt(rates_total,0,0,price); if(MA_Method==MODE_SMMA) CalculateSmoothedMA(rates_total,0,0,price); if(MA_Method==MODE_SMA) CalculateSimpleMA( rates_total,0,0,price); GlobalVariableSet( "Updated", 1 ); Updated = 1; } //--- 返回prev_calculated的值用于下一次调用 return(rates_total); } //+------------------------------------------------------------------
与“从头开始”完整计算指标的感知需求相关的主要更改:显然,如果用户的键盘操作将 MA 周期从 13 更改为 14,其之前的所有计算优化均告无效,且我们必须再次计算 MA。这在 Updated 变量具有 0 值时发生(按下热键后,TGV 改变,而重绘指标的价格跳动还未到来)。
然而,之前我们不得不显式调用 OnInit() 函数,因为我们需要更改当光标在线上悬停时将显示的指标简称。在初始 MA 计算后,Updated TGV 被设为 1,从而为优化指标计算开辟了道路 - 直到您不愿再次快速更改指标的参数。
OnChartEvent() 处理程序
下面是 OnChartEvent() 处理程序的简单代码:
void OnChartEvent( const int id, const long &lparam, const double &dparam, const string &sparam ) { if( id == CHARTEVENT_KEYDOWN ) switch( lparam ) { case( KEY_TAB ): changeTerminalGlobalVar( "MA_Method", 1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; case( KEY_UP ): changeTerminalGlobalVar( "MA_Period", 1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; case( KEY_DOWN ): changeTerminalGlobalVar( "MA_Period", -1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; case( KEY_NUMLOCK_UP ): changeTerminalGlobalVar( "MA_Shift", 1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; case( KEY_NUMLOCK_DOWN ): changeTerminalGlobalVar( "MA_Shift", -1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; } return; }//+------------------------------------------------------------------+
处理程序工作如下:按下热键,其虚拟代码被定义,然后 changeTerminalGlobalVar() 辅助函数启动并正确修改所需 TGV。之后 Updated 标志被重置为零,等待将启动 OnCalculate() 并“从头开始”重绘指标的价格跳动。
“正确”更改 TGV 的辅助函数
最终,用于 OnChartEvent() 处理程序的 changeTerminalGlobalVar() 函数的代码如下所示:
void changeTerminalGlobalVar( string name, int dir = 0 ) { int var = GlobalVariableGet( name ); int newparam = var + dir; if( name == "MA_Period" ) { if( newparam > 0 ) /// 对MA有效的时间周期 { GlobalVariableSet( name, newparam ); MA_Period = newparam; /// 不要忘记改变全局变量 } else /// 我们不改变时间周期,因为MA周期已等于最小值1 { GlobalVariableSet( name, 1 ); MA_Period = 1; /// 不要忘记改变全局变量 } } if( name == "MA_Method" ) /// 这里当你调用“dir”,它总是等于1,dir 的值并不重要 { newparam = ( var + 1 ) % 4; GlobalVariableSet( name, newparam ); MA_Method = newparam; } if( name == "MA_Shift" ) { GlobalVariableSet( name, newparam ); MA_Shift = newparam; } ChartRedraw( ); return; }//+------------------------------------------------------------------+
该函数的主要目的 - 将“物理限制”考虑在内的新 MA 的正确计算。显然,我们无法令 MA 周期小于 1,MA 偏移可以是随机的,但 MA 类型是从 0 到 3 的数字,对应于 ENUM_MA_METHOD 枚举中的条件成员编号。
检查。它奏效了,但效果不尽如人意。我们该怎么办?
好了,让我们把我们的指标应用至图表,并零星地按下更改 MA 参数的热键。是的,一切正常,但也存在一个不那么令人愉快的现象:TGV 立即更改(您可以使用 F3 键调用 TGV 来进行检查),但 MA 只有在新的价格跳动到来时才会及时重绘。如果我们有具有活动价格跳动流的美国时段,我们就很难注意到延迟。但如果更改发生在夜间,在没有新的价格跳动到来的平静时段,我们要花费数分钟来等待重绘。这是怎么回事?
好吧,正如人们常说的,种瓜得瓜种豆得豆。在版本 245 之前,指标中只有一个“入口点”,即 OnCalculate() 函数。当然,我不是要讨论负责指标的初始计算、初始化和结束的 OnInit() 和 OnDeinit() 函数。现在,指标有了好几个入口点,并且这些入口点与新 Timer 及 ChartEvent 事件有关。
然而,新处理程序仅处理它们的分内之事,也没有正式与 OnCalculate() 处理程序关联。因此,我们能对我们“相异的”OnChartEvent() 处理程序做些什么,以使其“正常”工作,也就是及时重绘 MA?
一般来说,有几种方法可实现此目标:
- “嵌套”(在 OnChartEvent() 内部调用 OnCalculate()):将 OnCalculate() 函数调用插入此处理程序,预填充所有参数。随着 OnChartEvent() 处理程序表明至少更改了一个 MA 参数,则它将影响其所有历史数据,即,我们必须“从头开始”对其进行重新计算,且没有计算优化。
- 通过将控制转移至 OnCalculate() 函数起始处的“人工价格跳动”修改图形缓冲区。显然,MT5 文档中并没有提供什么“正统”方法(虽然我的搜索可能并不彻底)。如果您感兴趣的话,您可以搜索一下 «API»、«PostMessageA» 等。因此,我们在此不考虑这种变体,因为它并不能保证未载入文献的功能在未来不会改变。我肯定这一定会发生。
“嵌套”起作用了!
事实证明,我们已完成了最重要的事情。下面是函数的代码,十分的简单。您可简单地将其调用直接插入 OnChartEvent() 处理程序中 return 运算符的前面。
int OnCalculate_Void() { const int rates_total = Bars( _Symbol, PERIOD_CURRENT ); CopyClose( _Symbol, PERIOD_CURRENT, 0, rates_total, _price ); OnCalculate( rates_total, 0, 0, _price ); return( 1 ); }//+------------------------------------------------------------------+
编译指标并将其应用至图表后,我们看到在一般情况下代码运行快速且与价格跳动的到来无关。
此实施的缺点在于收盘价被复制到 price[] 数组中。若需要,我们可通过在指标属性对话框的 "Settings"(设置)选项卡上设置字段 "Apply to"(应用至),将我们想要的取代 CopyClose() 函数。如果当前价格为基本价格(开盘价、最高价、最低价、收盘价),那么我们已经具有对应的 CopyXXXX() 函数。在更复杂价格(中间价格、典型价格或加权价格)的情况下,我们必须以不同的方法计算数组。
我不确定我们是否需要 CopyClose() 函数,该函数用于复制数组的整个历史数据。另一方面,在未加载过多历史数据时该函数足够快速。对 EURUSD H1 指标(含 1999 年以前的历史数据,约 70 万个柱)的检查表明,指标涉及计算且未显示任何减速。在这样的历史数据负载下,也许可能的减速不是由 CopyXXXX() 函数引起,而是由更复杂的指标从头开始重新计算历史数据的需要所引起(这是强制的)。
若干发现和结论
哪个更好 - 一个指标文件或“指标 + EA 交易”串联?
事实上,要回答这个问题不是那么容易。一方面,我们只有一个指标文件是有好处的,因为所有函数,包括事件处理程序都集中在一处。
另一方面,让我们想象一下有 3 个或 4 个指标以及一个“EA 交易”被应用至图表 - 这种情形并不少见。此外,假设每个指标在标准 OnCalculate() 之外还具有自己的事件处理程序。为了避免在该“大杂侩”中混淆事件处理,将所有事件处理程序集中起来是更合理的,现在可以集中在指标中的一处 -“EA 交易”中。
软件开发人员经过较长的时间才决定提供我们在指标中处理事件的能力:从非发布的测试版 09.09.09 开始(当时指标被视为“纯粹的计算与数学实体”,不能有任何阻碍计算速度的功能)刚好历时 5 个月。“纯粹的理念”很可能受到了影响 - 现在,编程人员幻象中的一些真正混乱的东西将被释放出来。但平衡点始终处于这两点之间:纯粹但功能有限,以及不那么纯粹但具有更强大的功能。
2009 年的 9-10 月,当时 MT5 测试版的版本号甚至还不到 200,我已编写并调试了“EA 交易 + 指标”串联,它允许快速管理 MA 参数,但性能只有“C 级”:它只在价格跳动到来时更新,并不及时。当时,该串联是唯一可能的解决方案,而现在,我想没有人会对它感兴趣了。
那时候我没有想过将指标的功能提升到“B 级”,即它在最新版本中展现出来的那样。现在,我很乐意为对此有兴趣的读者提供更方便的解决方案。
“嵌套”视频演示
随附有我的一段简短视频,展示我们的工作创意。MA 曲线的平滑变化(只有周期在改变 - 先是增大,然后减小)在某种程度上仍使人眼花缭乱。这就是嵌套(类似于俄罗斯著名的嵌套娃娃)。
当然,这类把戏只在从头开始计算指标本身不会花费太多时间时有用。包含于该指标中的简单 MA 正满足这一要求。
一个不稳定因素
请记住,指标原来的外部参数现已变成终端全局变量 (TGV),您可以通过按 F3 键查看。假设您打开“全局变量”对话框并更改了一个 TGV(例如,MA 周期)。您期望这一更改立即在图表中的指标上反映出来。
当前在终端中没有对应于由用户执行的 TGV 编辑的事件存在(例如,CHARTEVENT_GLOBALVAR_ENDEDIT)。同样地,我认为我们无法在“全局变量”对话框中禁用 TGV 修改。因此,在此我们无法指望任何事件,除了价格跳动。现实中会发生什么情况?
如果您不触按键盘,即使下一个价格跳动到来,更新也将是“错误”的:Updated 变量没有设为零,因此只会执行“优化的”指标计算(对应于改变的 TGV 之前的值)。在此情况下,为重回正轨,我们建议如下:在编辑 TGV 后,您至少应当按一次热键,以修改 TGV,将 Updated 变量设为 0,并执行指标的完整重新计算。
所有可能的用户和开发人员应将此事实牢记于心。
随附源代码和视频文件
最后,我附上源代码文件。说明:
- Custom Moving Average.mq5 - MA 源代码文件,与 MT5 一起提供。
- MyMA_ind_with_ChartEvent.mq5 - 初始(“C 级”)实施:指标仅在价格跳动到来时更新。
- MyMA_ind_with_ChartEvent_Matryoshka.mq5 - 第二个(也许达到“B 级”)变体:指标及时更新,无需等待价格跳动到来。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/39
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



简而言之,它没有最终确定。
摘自文章:
如果所需的价格是 "基本 "价格(开盘价、最高价、最低价、收盘价), 我们已经有了相应的 CopyXXXX() 函数 ,但如果是 "复杂 "价格(中位数、平均值或典型值),我们必须以另一种方式计算该数组。
虽然我很想看到 MA 线在我的命令下偷偷移动,但任何了解 MQL4 的人都会感到难过,因为在 MQL5 中,我们真的无法调用和即时更改任何指标参数。
在 MQL5 中,一旦初始化了句柄,指标就固定了 - 死死的固定在它的参数上。我不能再用不同的周期扫描价格走势,因为指标的周期已经固定。
在 MQL4 中,我们可以在 start() 内直接调用指标,并随意更改参数。
难怪Integer 在代码库中写了那么多 ...OnArray库。
:(
来自文章:
回来了,谁有兴趣的方法,oninit 不能被调用第二次,显示缓冲区滚动到零(大小= = 0)。
价格已更正,通过 par-ry 实现
简而言之,作为备忘录。