动量弹球交易策略(基础篇)
动量弹球策略的出处与样本量
动量弹球(Momentum Pinball)是一套在 MetaTrader 5 上跑的示例策略,作者 Alexander Puzanov 于 2018 年 1 月 15 日 08:40 发布。 该帖在源平台累计获得 9 981 次查看、5 条评论,说明它作为 MT5 内置示例有一定的受众基础,但并非经过大规模实盘验证的成熟系统。 外汇与贵金属交易本身高风险,这类示例策略仅适合在 MT5 策略测试器中用历史数据复现,不建议直接上真实账户。
◍ 动量弹球策略要怎么用结构重写
这一节先把要落地的东西说清楚:照着 L. Raschke 和 L. Connors 在《华尔街智慧》里写的动量弹球(Momentum Pinball)思路,用 MQL5 把价格范围边界的测试策略写成代码。它的形态只跨两根日线柱——第一根定第二天方向,第二根开盘后的价变直接给出进场和退场价位,逻辑不复杂但很吃执行精度。 面向已经会 MQL5 的人,这一版刻意不用类,全用 struct 代替。对从过程化编程转过来的人,结构和类的差别最小,又能覆盖信号计算这种轻量任务,没必要上完整 OOP。 落地路径分三步:先写信号模块,再写调用它的人工标记指标,最后是挂这个模块自动跑的 EA。作者原书用的是 20 年前报价,我们会在最新 tick 上重测一遍,看信号在当下外汇与贵金属行情里还灵不灵——这类品种杠杆高、滑点跳空频繁,历史胜率不能直接当现在概率。
「用 LBR/RSI 和 ROC 框定次日多空」
Raschke 与 Connors 发现 Taylor 的日内方向预判法在实际交易中常被自己打破,导致规则失去一致性。他们改用 ROC(变化率)作为方向过滤器,再把 RSI 套在 ROC 数值上,由此得到 LBR/RSI 指标,用来在超买超卖边界内识别更适合的买入日与卖出日。 买入侧的核心条件很硬:D1 周期里,LBR/RSI 在最近一根已收盘日线必须低于 30(超卖区)。新交易日第一根 H1 收盘后,在高于该小时线最高价处挂 buy stop;单子触发后,止损放在这根 H1 的最低价。若被打损,就在同一价位重新挂买。 若持仓到当日结束仍盈利,则留仓到次交易日,且必须在第二天平仓,不可再延。实测中 2017 年 10 月 30 日出现过 D1 上 LBR/RSI 落入超卖、随后第一小时范围给出挂单水平的典型形态,可在 MT5 历史数据里复核对齐。 出场原文未写死,作者仅提到跟踪止损、次晨平仓、或突破首交易日最高价时离场三种倾向。卖出规则对称:LBR/RSI 需高于 70,挂单置于首小时线最低价下方。外汇与贵金属杠杆高,信号失效时回撤可能快速扩大,仅作概率参考。
把 ROC 和 RSI 揉进一个独立指标
人工盯盘时,若信号模块只吐数值不画区域,超买超卖形态全靠脑补。单独挂一个 LBR/RSI 指标,用颜色块把 70 以上、30 以下区域填出来,肉眼扫一下就知道价格处在哪一侧。它不在每根分时上反复算,只在日柱收盘后取数,资源占用低,对 MT5 终端几乎无负担。 这个指标把 ROC(变动率)和 RSI 算法合并:先按 Linda Raschke 的改法算 ROC——不是相邻柱比价,而是当前柱和往前数第 3 个交易日的柱比价,再用这个结果跑标准 RSI。显示上用了 9 个缓冲区,其中 5 个画图、4 个做辅助计算,超买填粉红、超卖填浅蓝。 原交易系统没给的参数这里都开放了:RSI 周期默认 3,边界 70/30,价格源可从 ENUM_APPLIED_PRICE 的 7 种里挑,默认用收盘价,也可换典型价或加权价。文件必须命名 LBR_RSI.mq5 丢进标准指标目录,因为信号模块里 iCustom 调用的就是这名,改了就接不上。 下面这段是缓冲区与输入的声明骨架,逐行看:#property 那几行定义独立窗口、9 缓冲、3 绘图(两填充一曲线),坐标锁 0–100;input 块暴露价格类型、RSI 周期、超买超卖阈值;double 数组里 buff_Overbought_High/Low 管超买背景,buff_Oversold_High/Low 管超卖背景,buff_ROC 存变动率,buff_RSI 存最终曲线,buff_Positive/Negative 供 RSI 递推。OnInit 里 SetIndexBuffer 把 0 号缓冲绑给超买上沿。
class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_buffers class="num">9 class="macro">#class="kw">property indicator_plots class="num">3 class="macro">#class="kw">property indicator_label1 “超买区域" class="macro">#class="kw">property indicator_type1 DRAW_FILLING class="macro">#class="kw">property indicator_color1 C&class="macro">#x27;class="num">255,class="num">208,class="num">234&class="macro">#x27; class="macro">#class="kw">property indicator_width1 class="num">1 class="macro">#class="kw">property indicator_label2 “超卖区域" class="macro">#class="kw">property indicator_type2 DRAW_FILLING class="macro">#class="kw">property indicator_color2 C&class="macro">#x27;class="num">179,class="num">217,class="num">255&class="macro">#x27; class="macro">#class="kw">property indicator_width2 class="num">1 class="macro">#class="kw">property indicator_label3 "RSI от ROC" class="macro">#class="kw">property indicator_type3 DRAW_LINE class="macro">#class="kw">property indicator_style3 STYLE_SOLID class="macro">#class="kw">property indicator_color3 clrTeal class="macro">#class="kw">property indicator_width3 class="num">2 class="macro">#class="kw">property indicator_minimum class="num">0 class="macro">#class="kw">property indicator_maximum class="num">100 class="kw">input ENUM_APPLIED_PRICE TS_MomPin_Applied_Price = PRICE_CLOSE; class=class="str">"cmt">// 用于 ROC 计算的价格 class="kw">input class="type">uint TS_MomPin_RSI_Period = class="num">3; class=class="str">"cmt">// RSI 周期数 class="kw">input class="type">class="kw">double TS_MomPin_RSI_Overbought = class="num">70; class=class="str">"cmt">// RSI 超卖水平 class="kw">input class="type">class="kw">double TS_MomPin_RSI_Oversold = class="num">30; class=class="str">"cmt">// RSI 超买水平 class="type">class="kw">double buff_Overbought_High[], buff_Overbought_Low[], class=class="str">"cmt">// 超买区域背景 buff_Oversold_High[], buff_Oversold_Low[], class=class="str">"cmt">// 超卖区域背景 buff_Price[], class=class="str">"cmt">// 计算价格的数组 buff_ROC[], class=class="str">"cmt">// 来自计算价格的 ROC 数组 buff_RSI[], class=class="str">"cmt">// ROC 的 RSI buff_Positive[], buff_Negative[] class=class="str">"cmt">// RSI 计算的辅助数组 ; class="type">int OnInit() { class=class="str">"cmt">// 指标缓冲区设计: class=class="str">"cmt">// 超买区域 SetIndexBuffer(class="num">0, buff_Overbought_High, INDICATOR_DATA);
◍ 缓冲区绑定与价格源切换的实现细节
指标初始化阶段把 9 个缓冲区依次挂到对应插槽:0~3 号存超买/超卖区域的上下边界,4 号存 RSI 主线,5~8 号是计算用的中间量(价格、ROC、正负变化)。其中超买与超卖区域缓冲都设了 PLOT_SHOW_DATA=false,意味着它们只参与填充色块、不单独在数据窗口露脸。 PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE) 这类调用把空值定义为 EMPTY_VALUE,避免图表上画出 0 轴误判。IndicatorSetInteger(INDICATOR_DIGITS, 2) 锁定两位小数显示,短名直接写成 "LBR/RSI",在 MT5 导航树里一眼能认。 计算循环从第二根柱(i_Period_Bar=1)起步,用 switch 按 TS_MomPin_Applied_Price 把收盘价、开盘价、最高价、最低价或几种加权价写进 buff_Price。中位数价乘 0.5、典型价乘 0.33333、加权价乘 0.25,这些系数是写死在代码里的,改价格源不用动循环逻辑,只切 case 即可。 在 MT5 里把这段粘进 OnInit 和 OnCalculate 头部,先确认导航栏出现 "LBR/RSI" 且子窗口无多余线型,再调 TS_MomPin_Applied_Price 从 PRICE_CLOSE 切到 PRICE_TYPICAL,观察 buff_Price 数值是否按 0.33333 倍 High+Low+Open 变化。外汇与贵金属波动剧烈,这类自定义指标仅作概率参考,实盘前务必用历史数据验证。
PlotIndexSetDouble(class="num">0, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(class="num">0, PLOT_SHOW_DATA, false); SetIndexBuffer(class="num">1, buff_Overbought_Low, INDICATOR_DATA); class=class="str">"cmt">// 超卖区域 SetIndexBuffer(class="num">2, buff_Oversold_High, INDICATOR_DATA); PlotIndexSetDouble(class="num">1, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(class="num">1, PLOT_SHOW_DATA, false); SetIndexBuffer(class="num">3, buff_Oversold_Low, INDICATOR_DATA); class=class="str">"cmt">// RSI 曲线 SetIndexBuffer(class="num">4, buff_RSI, INDICATOR_DATA); PlotIndexSetDouble(class="num">2, PLOT_EMPTY_VALUE, EMPTY_VALUE); class=class="str">"cmt">// RSI 计算使用的缓冲区 SetIndexBuffer(class="num">5, buff_Price, INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">6, buff_ROC, INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">7, buff_Negative, INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">8, buff_Positive, INDICATOR_CALCULATIONS); IndicatorSetInteger(INDICATOR_DIGITS, class="num">2); IndicatorSetString(INDICATOR_SHORTNAME, "LBR/RSI"); class="kw">return(INIT_SUCCEEDED); } class="type">int i_RSI_Period = class="type">int(TS_MomPin_RSI_Period), class=class="str">"cmt">// 把 RSI 周期数转换为整数类型 i_Bar, i_Period_Bar class=class="str">"cmt">// 用于同时访问的两个柱的索引 ; class="type">class="kw">double d_Sum_Negative, d_Sum_Positive, class=class="str">"cmt">// 用于 RSI 计算的辅助变量 d_Change class=class="str">"cmt">// 用于 ROC 计算的辅助变量 ; class=class="str">"cmt">// Fill in ROC buffer and fill areas: i_Period_Bar = class="num">1; class="kw">while(++i_Period_Bar < rates_total && !IsStopped()) { class=class="str">"cmt">// 计算的柱的价格: class="kw">switch(TS_MomPin_Applied_Price) { case PRICE_CLOSE: buff_Price[i_Period_Bar] = Close[i_Period_Bar]; class="kw">break; case PRICE_OPEN: buff_Price[i_Period_Bar] = Open[i_Period_Bar]; class="kw">break; case PRICE_HIGH: buff_Price[i_Period_Bar] = High[i_Period_Bar]; class="kw">break; case PRICE_LOW: buff_Price[i_Period_Bar] = Low[i_Period_Bar]; class="kw">break; case PRICE_MEDIAN: buff_Price[i_Period_Bar] = class="num">0.50000 * (High[i_Period_Bar] + Low[i_Period_Bar]); class="kw">break; case PRICE_TYPICAL: buff_Price[i_Period_Bar] = class="num">0.33333 * (High[i_Period_Bar] + Low[i_Period_Bar] + Open[i_Period_Bar]); class="kw">break; case PRICE_WEIGHTED: buff_Price[i_Period_Bar] = class="num">0.25000 * (High[i_Period_Bar] + Low[i_Period_Bar] + Open[i_Period_Bar] + Open[i_Period_Bar]); class="kw">break; }
「RSI 平滑递推的尾段实现」
这段逻辑承接前面 ROC 的计算,把动量变化转成类 RSI 的摆动值。初始化阶段只跑一次:当已计算柱数不足 RSI 周期时,用 while 循环把前 i_RSI_Period 根 BAR 的 d_Change 正负分别累加,再除以周期得到首值 buff_Positive / buff_Negative。 递推部分从 i_RSI_Period+1 开始,每根新 BAR 用「前一值×(周期-1)+当前增量」除以周期做滑动平均。若负向和为 0 且正向和非 0,RSI 直接给 100;两者皆 0 则给 50,这是边界保护。 外汇与贵金属杠杆高,这类指标仅描述动量饱和概率,金叉死叉都可能假突破,实盘前请在 MT5 用不同 i_RSI_Period(如 14 与 21)对比缓冲区的尖刺密度。
class=class="str">"cmt">// 计算价格的差 (ROC 数值): if(i_Period_Bar > class="num">1) buff_ROC[i_Period_Bar] = buff_Price[i_Period_Bar] - buff_Price[i_Period_Bar - class="num">2]; class=class="str">"cmt">// 填充背景: buff_Overbought_High[i_Period_Bar] = class="num">100; buff_Overbought_Low[i_Period_Bar] = TS_MomPin_RSI_Overbought; buff_Oversold_High[i_Period_Bar] = TS_MomPin_RSI_Oversold; buff_Oversold_Low[i_Period_Bar] = class="num">0; } i_Period_Bar = prev_calculated - class="num">1; if(i_Period_Bar <= i_RSI_Period) { buff_RSI[class="num">0] = buff_Positive[class="num">0] = buff_Negative[class="num">0] = d_Sum_Positive = d_Sum_Negative = class="num">0; i_Bar = class="num">0; class="kw">while(i_Bar++ < i_RSI_Period) { buff_RSI[class="num">0] = buff_Positive[class="num">0] = buff_Negative[class="num">0] = class="num">0; d_Change = buff_ROC[i_Bar] - buff_ROC[i_Bar - class="num">1]; d_Sum_Positive += (d_Change > class="num">0 ? d_Change : class="num">0); d_Sum_Negative += (d_Change < class="num">0 ? -d_Change : class="num">0); } buff_Positive[i_RSI_Period] = d_Sum_Positive / i_RSI_Period; buff_Negative[i_RSI_Period] = d_Sum_Negative / i_RSI_Period; if(buff_Negative[i_RSI_Period] != class="num">0) buff_RSI[i_RSI_Period] = class="num">100 - (class="num">100 / (class="num">1. + buff_Positive[i_RSI_Period] / buff_Negative[i_RSI_Period])); else buff_RSI[i_RSI_Period] = buff_Positive[i_RSI_Period] != class="num">0 ? class="num">100 : class="num">50; i_Period_Bar = i_RSI_Period + class="num">1; } i_Bar = i_Period_Bar - class="num">1; class="kw">while(++i_Bar < rates_total && !IsStopped()) { d_Change = buff_ROC[i_Bar] - buff_ROC[i_Bar - class="num">1]; buff_Positive[i_Bar] = (buff_Positive[i_Bar - class="num">1] * (i_RSI_Period - class="num">1) + (d_Change> class="num">0 ? d_Change : class="num">0)) / i_RSI_Period; buff_Negative[i_Bar] = (buff_Negative[i_Bar - class="num">1] * (i_RSI_Period - class="num">1) + (d_Change <class="num">0 ? -d_Change : class="num">0)) / i_RSI_Period; if(buff_Negative[i_Bar] != class="num">0) buff_RSI[i_Bar] = class="num">100 - class="num">100. / (class="num">1. + buff_Positive[i_Bar] / buff_Negative[i_Bar]); else buff_RSI[i_Bar] = buff_Positive[i_Bar] != class="num">0 ? class="num">100 : class="num">50; }