MQL5 中的事件处理:快速更改 MA 周期·进阶篇
线性加权均值的权重累加与主循环
线性加权移动平均(LWMA)的核心在于权重系数随距离递减:离当前柱越近的价格乘的权重越大。代码里先用 weightsum 把总权重一次性算死,公式是 MA_Period*(MA_Period+1)/2,比如周期 14 时 weightsum=105,之后每根柱都复用这个值,避免重复运算。 首次计算时(prev_calculated==0),循环里逐根把 k*price[i] 累进 firstValue,同时 weightsum+=k 累加权重,最后 firstValue/(double)weightsum 得到缓冲首值赋给 ExtLineBuffer[limit-1]。 非首次走主循环时 limit=prev_calculated-1,只对新增柱计算:内层 j 从 0 到 InpMAPeriod-1,sum+=(InpMAPeriod-j)*price[i-j],再除以 weightsum 写回 ExtLineBuffer[i]。这套写法把外汇、贵金属 1 分钟到日线都适用,但杠杆品种跳空时 LWMA 可能滞后放大,属高风险信号,调参前建议在 MT5 策略测试器用历史数据验证权重分配。
weightsum+=k; class=class="str">"cmt">// <-- 使用 weightsum firstValue+=k*price[i]; } firstValue/=(class="type">class="kw">double)weightsum; ExtLineBuffer[limit-class="num">1]=firstValue; } else limit=prev_calculated-class="num">1; class=class="str">"cmt">//--- 主循环 for(i=limit;i<rates_total;i++) { sum=class="num">0; for(class="type">int j=class="num">0;j<InpMAPeriod;j++) sum+=(InpMAPeriod-j)*price[i-j]; ExtLineBuffer[i]=sum/weightsum; class=class="str">"cmt">// <-- 使用 weightsum } class=class="str">"cmt">//--- } class="type">int weightsum = MA_Period *( MA_Period + class="num">1 ) / class="num">2;
◍ 用键盘热键实时改均线参数
在 MT5 图表上做价格行为分析时,反复打开指标面板调均线周期很打断节奏。把按键事件接到 OnChartEvent 里,就能在盘面直接用键盘改 MA 的方法、周期和偏移。 下面这段逻辑只处理 CHARTEVENT_KEYDOWN:Tab 键切换平滑方式(MA_Method ±1),方向键上下改周期(MA_Period ±1),小键盘上下改偏移(MA_Shift ±1)。每次改动都同步把终端全局变量 Updated 置 0,通知主循环重算。 外汇与贵金属杠杆高、滑点大,这类热键脚本只解决交互效率,不预示任何方向;参数乱切可能让你在黄金 5 分钟图上画出完全失真的均线。 别把全局变量当摆设 Updated 置 0 是给指标或 EA 主循环发的“脏标记”。如果你只改了 MA_Period 却忘了 GlobalVariableSet("Updated",0),画面可能停留在上一个周期,造成误判。
class="type">void OnChartEvent( const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam ) { if( id == CHARTEVENT_KEYDOWN ) class="kw">switch( lparam ) { case( KEY_TAB ): changeTerminalGlobalVar( "MA_Method", class="num">1 ); GlobalVariableSet( "Updated", class="num">0 ); Updated = class="num">0; break; case( KEY_UP ): changeTerminalGlobalVar( "MA_Period", class="num">1 ); GlobalVariableSet( "Updated", class="num">0 ); Updated = class="num">0; break; case( KEY_DOWN ): changeTerminalGlobalVar( "MA_Period", -class="num">1 ); GlobalVariableSet( "Updated", class="num">0 ); Updated = class="num">0; break; case( KEY_NUMLOCK_UP ): changeTerminalGlobalVar( "MA_Shift", class="num">1 ); GlobalVariableSet( "Updated", class="num">0 ); Updated = class="num">0; break; case( KEY_NUMLOCK_DOWN ): changeTerminalGlobalVar( "MA_Shift", -class="num">1 );
「用全局变量热调均线参数」
在 MT5 里直接改 EA 或指标参数往往要重加载,借助终端全局变量可以把移动均线的周期、方法和偏移做成运行时热调。下面这段函数把参数名和步进方向传进去,先读 GlobalVariableGet 拿到旧值,再按规则写回并同步本地变量,最后 ChartRedraw 刷新图表。 对于 MA_Period,新值必须大于 0,否则强制落回 1——也就是说均线周期下限就是 1 根 K 线,再减就无意义。MA_Method 用 (var+1)%4 做循环,对应平台 0~3 四种平滑方式(SMA、EMA、SMMA、LWMA),每触发一次就切到下一种。 MA_Shift 则直接加减 dir,没有上下限保护,实盘里若被外部脚本误写为超大整数,均线会整体平移导致形态失真。外汇与贵金属杠杆高、滑点无常,这类热调只建议在模拟环境先验证再上真仓。 别把正态当圣经:全局变量是跨 EA 共享的,若同时挂了多个读取同名的脚本,一个改周期会牵连其他图表,排查时先看 GlobalVariableGet 的返回而非只看本地变量。
class="type">void changeTerminalGlobalVar( class="type">class="kw">string name, class="type">int dir = class="num">0 ) { class="type">int var = GlobalVariableGet( name ); class="type">int newparam = var + dir; if( name == "MA_Period" ) { if( newparam > class="num">0 ) class=class="str">"cmt">/// 对MA有效的时间周期 { GlobalVariableSet( name, newparam ); MA_Period = newparam; class=class="str">"cmt">/// 不要忘记改变全局变量 } else class=class="str">"cmt">/// 我们不改变时间周期,因为MA周期已等于最小值1 { GlobalVariableSet( name, class="num">1 ); MA_Period = class="num">1; class=class="str">"cmt">/// 不要忘记改变全局变量 } } if( name == "MA_Method" ) class=class="str">"cmt">/// 这里当你调用“dir”,它总是等于1,dir 的值并不重要 { newparam = ( var + class="num">1 ) % class="num">4; GlobalVariableSet( name, newparam ); MA_Method = newparam; } if( name == "MA_Shift" ) { GlobalVariableSet( name, newparam ); MA_Shift = newparam; } ChartRedraw( ); class="kw">return; }
图表静默时 MA 为何迟迟不重绘
把指标挂上图,随手按热键改 MA 参数,逻辑是通的:TGV 用 F3 调出来看会立刻变,但 MA 线要等下一笔价格跳动才重绘。美国时段报价流活跃,这点延迟基本看不见;可一到夜间平静段,没新 tick 进来,改完参数可能干等好几分钟线才动。 根子在 MT5 的事件模型。旧版(build 245 前)指标只有一个主入口 OnCalculate(),外加 OnInit/OnDeinit 管初始化和收尾。后来加了 Timer 和 ChartEvent 这类新事件入口,但 OnChartEvent() 只管自己那摊事,跟 OnCalculate() 没有正式关联,所以你在 OnChartEvent 里改了参数,它不会主动触发重算。 能想到的路子有两条。其一是在 OnChartEvent() 里直接“嵌套”调用 OnCalculate(),把参数都预填好,参数一变就从头重算整段历史——没有任何计算优化,数据量大的时候代价明显。 其二是往图形缓冲区塞一个“人工价格跳动”逼 OnCalculate 跑起来。文档里没正经写法,得去翻 PostMessageA、API 那类野路子。这种未载入官方文献的依赖以后很可能被改掉,不在实盘代码里赌它。
◍ 把价格更新塞进图表事件里
实测下来,最关键的改造已经跑通:把自定义更新函数直接插到 OnChartEvent() 的 return 之前,编译挂图后,报价跳动与指标刷新脱钩,肉眼看不出延迟。 下面这段就是核心函数,逻辑极简——先取当前品种当前周期的柱数,再把收盘价整段拷进 _price 数组,最后手动调一次 OnCalculate。注意它用的是 CopyClose,所以落进 price[] 的都是收盘价。 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 ); } 若你想用其他价格源,别死磕 CopyClose。在指标属性 Settings 里改 Apply to 字段,或把函数换成 CopyOpen/CopyHigh/CopyLow;中间价、典型价、加权价这类没有现成 CopyXXXX 的,就得自己先算数组再喂进去。 我对外汇贵金属这类高杠杆品种提醒一句:历史负载是隐性坑。EURUSD H1 灌进 1999 年以前的约 70 万根柱做检查,CopyClose 本身没拖慢指标;真要卡,大概率不是拷贝函数,而是复杂指标被强制从头重算历史。实盘前务必在 MT5 用满历史数据自测一次。
class="type">int OnCalculate_Void() { const class="type">int rates_total = Bars( _Symbol, PERIOD_CURRENT ); CopyClose( _Symbol, PERIOD_CURRENT, class="num">0, rates_total, _price ); OnCalculate( rates_total, class="num">0, class="num">0, _price ); class="kw">return( class="num">1 ); }
「画得少,看得清」
把 MA 周期塞进终端全局变量(F3 可查)后,指标外部参数就脱离了句柄固定的死锁,这点在 MT5 里比 MQL4 的即时调用更绕但也更稳。嵌套娃娃式变体(MyMA_ind_with_ChartEvent_Matryoshka.mq5)能在不等价格跳动时刷新,简单 MA 重算开销小,肉眼看曲线平滑伸缩还不卡。 一个坑得刻进肌肉记忆:全局变量对话框里手改 TGV 后,终端没有 CHARTEVENT_GLOBALVAR_ENDEDIT 这类事件,只能靠下一次按键把 Updated 置零触发全量重算;不然优化的局部计算会沿用旧值,图表 silently 跑偏。外汇和贵金属杠杆高,这类手动干预失误可能直接放大滑点。 作者早年写的 C 级串联只在 tick 到来时更新、性能垫底,如今早没人用;真要验证,下载那三个 mq5 挂上图,改周期按一下键,比读十遍文档来得实在。