动量弹球交易策略·进阶篇
(2/3)· 从信号模块到人工交易指标,MQL5 面向对象简化法如何落地弹球规则
◍ 把动量弹球信号做成可改参数的模块
原书里 LBR/RSI 用的是死数值:RSI 周期 3、超买 70、超卖 30。我们把它全部 input 化,方便在 MT5 里直接拖参数做实验,不用改代码重编译。 平仓逻辑给了三种让用户选:跟踪止损、次日第一个日柱收盘平、突破建仓日极值平。‘早晨’原文没定义,我们绑成 24 小时周期里每天第一个日柱,和多数策略规则一致,避免歧义。 fe_Get_Entry_Signal 的参数连接和返回值类型要和之前 Raschke/Connors 策略统一。EA 调用时用静态变量跨 tick 存水平,指标调用则把 t_Time 传负值来清零静态量,这是区分两种调用源的关键。 每隔 24 小时在已关闭 D1 柱上读 LBR/RSI,若落在 30–70 中性区就禁交易。H1 首柱偏移默认 10 点,同时用于挂单和止损距离。下面这段声明是模块骨架,复制进 MQ5 就能改。
enum ENUM_EXIT_MODE { class=class="str">"cmt">// 退场方法列表 CLOSE_ON_SL_TRAIL, class=class="str">"cmt">// 只通过跟踪止损 CLOSE_ON_NEW_1ST_CLOSE, class=class="str">"cmt">// 通过在后面一天的第一个柱平仓 CLOSE_ON_DAY_BREAK class=class="str">"cmt">// 通过突破建仓日价格极值平仓 }; class=class="str">"cmt">// user settings 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="kw">input class="type">uint TS_MomPin_Entry_Offset = class="num">10; class=class="str">"cmt">// 动量弹球: 入场水平距离 H1 边界的偏移(点数) class="kw">input class="type">uint TS_MomPin_Exit_Offset = class="num">10; class=class="str">"cmt">// 动量弹球: 出场水平距离 H1 边界的偏移(点数) class="kw">input ENUM_EXIT_MODE TS_MomPin_Exit_Mode = CLOSE_ON_SL_TRAIL; class=class="str">"cmt">// 动量弹球: 获利仓位的平仓方法 ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal( class=class="str">"cmt">// 两个柱形模式分析 (D1 + H1) class="type">class="kw">datetime t_Time, class=class="str">"cmt">// 当前时间 class="type">class="kw">double& d_Entry_Level, class=class="str">"cmt">// 进场水平 (与变量的连接) class="type">class="kw">double& d_SL, class=class="str">"cmt">// 止损水平 (与变量的连接) class="type">class="kw">double& d_TP, class=class="str">"cmt">// 获利水平 (与变量的连接) class="type">class="kw">double& d_Range_High, class=class="str">"cmt">// 范围内第一个小时柱的最高价 (与变量的连接) class="type">class="kw">double& d_Range_Low class=class="str">"cmt">// 范围内第一个小时柱的最低价 (与变量的连接) ) { class=class="str">"cmt">// function body } class="kw">static ENUM_ENTRY_SIGNAL se_Trade_Direction = ENTRY_UNKNOWN; class=class="str">"cmt">// 今天的交易方向 class="kw">static class="type">class="kw">double class=class="str">"cmt">// 用于在分时之间保存计算的水平的变量 sd_Entry_Level = class="num">0, sd_SL = class="num">0, sd_TP = class="num">0, sd_Range_High = class="num">0, sd_Range_Low = class="num">0 ; if(t_Time < class="num">0) { class=class="str">"cmt">// 只用于在指标中调用
日线 LBR_RSI 句柄与昨日柱的惰性初始化
这段逻辑把进场位、止损、止盈和区间高低全部先归零,方向标记为 ENTRY_UNKNOWN,等于每次新分析前把旧状态清空。随后用 static 句柄缓存 LBR_RSI 指标,只在第一次进函数时通过 iCustom 向日线周期(PERIOD_D1)要句柄,避免重复申请。 指标参数直接吃全局变量:TS_MomPin_Applied_Price、TS_MomPin_RSI_Period、TS_MomPin_RSI_Overbought、TS_MomPin_RSI_Oversold。若句柄仍是 INVALID_HANDLE,按 Log_Level 决定是否打印错误码并返回 ENTRY_INTERNAL_ERROR,这一步能帮你快速定位自定义指标路径写错或周期不支持。 真正取数靠 CopyTime 拉前两根日柱时间,要求返回值不小于 2;不足就报错退出。static 变量 st_Prev_Day 记录已分析过的日期,只有 ta_Bar_Time[0] 比它新才重算,所以同一根日线内反复调用不会重复跑 CopyBuffer。 CopyBuffer 取的是缓冲区编号 4、长度 1 的 LBR_RSI 值,若大于 100 或小于 0 视为脏数据返回 ENTRY_UNKNOWN。外汇与贵金属波动大,这类自定义动量指标在极端跳空时可能返回异常值,实盘前务必在 MT5 用历史日线手动验证返回值边界。
sd_Entry_Level = sd_SL = sd_TP = sd_Range_High = sd_Range_Low = class="num">0; se_Trade_Direction = ENTRY_UNKNOWN; } class=class="str">"cmt">// 默认使用之前保存的水平来做进场/出场: d_Entry_Level = sd_Entry_Level; d_SL = sd_SL; d_TP = sd_TP; d_Range_High = sd_Range_High; d_Range_Low = sd_Range_Low; class="kw">static class="type">int si_Indicator_Handle = INVALID_HANDLE; if(si_Indicator_Handle == INVALID_HANDLE) { class=class="str">"cmt">// 在第一次调用函数时取得指标句柄: si_Indicator_Handle = iCustom(_Symbol, PERIOD_D1, "LBR_RSI", TS_MomPin_Applied_Price, TS_MomPin_RSI_Period, TS_MomPin_RSI_Overbought, TS_MomPin_RSI_Oversold ); if(si_Indicator_Handle == INVALID_HANDLE) { class=class="str">"cmt">// 没有得到指标句柄 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: 收取 LBR_RSI 指标句柄出错代码 #%u", __FUNCTION__, _LastError); class="kw">return(ENTRY_INTERNAL_ERROR); } } class="kw">static class="type">int si_Indicator_Handle = INVALID_HANDLE; if(si_Indicator_Handle == INVALID_HANDLE) { class=class="str">"cmt">// receiving indicator handle at first function call: si_Indicator_Handle = iCustom(_Symbol, PERIOD_D1, "LBR_RSI", TS_MomPin_Applied_Price, TS_MomPin_RSI_Period, TS_MomPin_RSI_Overbought, TS_MomPin_RSI_Oversold ); if(si_Indicator_Handle == INVALID_HANDLE) { class=class="str">"cmt">// 没有取得指标句柄 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: 指标句柄接收错误 LBR_RSI #%u", __FUNCTION__, _LastError); class="kw">return(ENTRY_INTERNAL_ERROR); } } class=class="str">"cmt">// 查找前一天日柱的时间: class="type">class="kw">datetime ta_Bar_Time[]; if(CopyTime(_Symbol, PERIOD_D1, fabs(t_Time), class="num">2, ta_Bar_Time) < class="num">2) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyTime: 错误 #%u", __FUNCTION__, _LastError); class="kw">return(ENTRY_INTERNAL_ERROR); } class=class="str">"cmt">// 如果这是今天第一次调用,对前一天的分析: class="kw">static class="type">class="kw">datetime st_Prev_Day = class="num">0; if(t_Time < class="num">0) st_Prev_Day = class="num">0; class=class="str">"cmt">// only for call from indicator if(st_Prev_Day < ta_Bar_Time[class="num">0]) { class=class="str">"cmt">// 之前一天参数清零: se_Trade_Direction = ENTRY_UNKNOWN; d_Entry_Level = sd_Entry_Level = d_SL = sd_SL = d_TP = sd_TP = d_Range_High = sd_Range_High = d_Range_Low = sd_Range_Low = class="num">0; class=class="str">"cmt">// 取得前一天的 LBR/RSI 指标值: class="type">class="kw">double da_Indicator_Value[]; if(class="num">1 > CopyBuffer(si_Indicator_Handle, class="num">4, ta_Bar_Time[class="num">0], class="num">1, da_Indicator_Value)) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyBuffer: 错误 #%u", __FUNCTION__, _LastError); class="kw">return(ENTRY_INTERNAL_ERROR); } class=class="str">"cmt">// 如果 LBR/RSI 指标值有错误: if(da_Indicator_Value[class="num">0] > class="num">100. || da_Indicator_Value[class="num">0] < class="num">0.) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: 指标缓冲区值出错 (%f)", __FUNCTION__, da_Indicator_Value[class="num">0]); class="kw">return(ENTRY_UNKNOWN); } st_Prev_Day = ta_Bar_Time[class="num">0]; class=class="str">"cmt">// 重试次数计数
「用H1首根柱锁定当日进场与止损」
这段逻辑把「方向判定」和「价位计算」拆成了两步:先由 RSI 类指标值给出 ENTRY_BUY / ENTRY_SELL / ENTRY_NONE,再在 H1 周期上取最近 24 根小时柱,回找「今天第一根 H1 柱」的高、低点作为区间边界。若 H1 首根尚未走完(已成型柱数不足 3 根),函数直接返回 ENTRY_UNKNOWN,避免用不完整数据下单。 进场价与初始止损都靠偏移量算出来:买入时 sd_Entry_Level = 首根最高价 + _Point*TS_MomPin_Entry_Offset,卖出时取首根最低价减去该偏移;止损则对称地挂在区间另一侧。外汇与贵金属杠杆高,这种基于固定点差偏移的挂单在跳空时可能滑到更远价位,实盘前应在 MT5 用历史日跑一遍验证。 平仓侦测函数 fe_Get_Exit_Signal 用静态数组缓存前一天 D1 数据,并通过 t_Current_Time 正负号区分是指标调用还是 EA 调用——负数入参会被取反并重置计数器。复制下面这段核心片段,改 TS_MomPin_Entry_Offset 参数就能直接看不同偏移对触发频率的影响。
class=class="str">"cmt">// 记录今天的交易方向: if(da_Indicator_Value[class="num">0] > TS_MomPin_RSI_Overbought) se_Trade_Direction = ENTRY_SELL; else se_Trade_Direction = da_Indicator_Value[class="num">0] > TS_MomPin_RSI_Oversold ?ENTRY_NONE : ENTRY_BUY; class=class="str">"cmt">// 记到记录中: if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: 交易方向 %s: %s. LBR/RSI: (%.2f)", __FUNCTION__, TimeToString(ta_Bar_Time[class="num">1], TIME_DATE), StringSubstr(EnumToString(se_Trade_Direction), class="num">6), da_Indicator_Value[class="num">0] ); class=class="str">"cmt">// 今天没有搜索信号 if(se_Trade_Direction == ENTRY_NONE) class="kw">return(ENTRY_NONE); class=class="str">"cmt">// 分析今天第一个H1上的小时柱,除非已经做过了: if(sd_Entry_Level == class="num">0.) { class=class="str">"cmt">// to receive data of last class="num">24 bars H1: class="type">MqlRates oa_H1_Rates[]; class="type">int i_Price_Bars = CopyRates(_Symbol, PERIOD_H1, fabs(t_Time), class="num">24, oa_H1_Rates); if(i_Price_Bars == WRONG_VALUE) { class=class="str">"cmt">// 处理 CopyRates 函数错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: 错误 #%u", __FUNCTION__, _LastError); class="kw">return(ENTRY_INTERNAL_ERROR); } class=class="str">"cmt">// 在24个柱中找到今天的第一个柱,并且记录最高价和最低价: class="type">int i_Bar = i_Price_Bars; class="kw">while(i_Bar-- > class="num">0) { if(oa_H1_Rates[i_Bar].time < ta_Bar_Time[class="num">1]) class="kw">break; class=class="str">"cmt">// 前一天在 H1 上的最后一个柱 class=class="str">"cmt">// H1第一个柱的范围边界: sd_Range_High = d_Range_High = oa_H1_Rates[i_Bar].high; sd_Range_Low = d_Range_Low = oa_H1_Rates[i_Bar].low; } class=class="str">"cmt">// H1 第一个柱还没有关闭: if(i_Price_Bars - i_Bar < class="num">3) class="kw">return(ENTRY_UNKNOWN); class=class="str">"cmt">// 计算交易水平: class=class="str">"cmt">// 进入市场的水平: d_Entry_Level = _Point * TS_MomPin_Entry_Offset; class=class="str">"cmt">// 辅助计算 sd_Entry_Level = d_Entry_Level = se_Trade_Direction == ENTRY_SELL ?d_Range_Low - d_Entry_Level : d_Range_High + d_Entry_Level; class=class="str">"cmt">// 初始止损水平: d_SL = _Point * TS_MomPin_Exit_Offset; class=class="str">"cmt">// 辅助计算 sd_SL = d_SL = se_Trade_Direction == ENTRY_BUY ?d_Range_Low - d_SL : d_Range_High + d_SL; }
◍ 日内极值突破的离场判定
这段逻辑处理的是建仓后如何按日线级别规则平仓,核心在 CLOSE_ON_NEW_1ST_CLOSE 与 CLOSE_ON_DAY_BREAK 两种模式。前者要求在建仓日的后一天第一根柱收盘时检查信号,若 fe_Get_Entry_Signal 返回值小于 ENTRY_UNKNOWN 就全平,否则继续持有。 CLOSE_ON_DAY_BREAK 更依赖价格行为:先用 t_Current_Time % 86400 > 36000 过滤掉日内前 10 小时(36000 秒)的波动,再用 CopyRates 拉取两根 D1 柱。若持仓为多头且前一日高点低于当前日高点,视为无突破、不平仓;反向则倾向触发 EXIT_ALL。 注意 86400 是一天的秒数,36000 约等于 10 小时,这两个硬参数直接决定离场灵敏度。外汇与贵金属波动剧烈,这类突破平仓逻辑可能在大跳空时滑点放大,实盘前务必在 MT5 策略测试器用历史数据跑一遍。
d_SL, d_TP, d_Range_High, d_Range_Low ; if(e_Trade_Direction < class="num">1) { class=class="str">"cmt">// 没有仓位,一切清零 si_Price_Bars = class="num">0; } class="kw">switch(e_Exit_Mode) { case CLOSE_ON_SL_TRAIL: class=class="str">"cmt">// 只有跟踪止损 class="kw">return(EXIT_NONE); case CLOSE_ON_NEW_1ST_CLOSE: class=class="str">"cmt">// 在后面一天第一个柱时平仓 if((t_Current_Time - t_Current_Time % class="num">86400) == (t_Entry_Time - t_Current_Time % class="num">86400) ) class="kw">return(EXIT_NONE); class=class="str">"cmt">// 建仓日尚未结束 if(fe_Get_Entry_Signal(t_Current_Time, d_Curr_Entry_Level, d_SL, d_TP, d_Range_High, d_Range_Low) < ENTRY_UNKNOWN ) { if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: 后一天的第一个柱关闭了", __FUNCTION__); class="kw">return(EXIT_ALL); } class="kw">return(EXIT_NONE); class=class="str">"cmt">// 不做平仓 case CLOSE_ON_DAY_BREAK: class=class="str">"cmt">// 根据突破建仓日价格极值平仓 if((t_Current_Time - t_Current_Time % class="num">86400) == (t_Entry_Time - t_Current_Time % class="num">86400) ) class="kw">return(EXIT_NONE); class=class="str">"cmt">// 建仓日尚未结束 if(t_Current_Time % class="num">86400 > class="num">36000) class="kw">return(EXIT_ALL); class=class="str">"cmt">// 时间超出 if(si_Price_Bars < class="num">1) { si_Price_Bars = CopyRates(_Symbol, PERIOD_D1, t_Current_Time, class="num">2, soa_Prev_D1_Rate); if(si_Price_Bars == WRONG_VALUE) { class=class="str">"cmt">// 处理 CopyRates 函数错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: 错误 #%u", __FUNCTION__, _LastError); class="kw">return(EXIT_UNKNOWN); } if(e_Trade_Direction == ENTRY_BUY) { if(soa_Prev_D1_Rate[class="num">1].high < soa_Prev_D1_Rate[class="num">0].high) class="kw">return(EXIT_NONE); class=class="str">"cmt">// 没有突破
用前一日高低点卡死出场信号
这段逻辑把「昨天 D1 高低点是否被今日 D1 高低点突破」当成平仓触发器,而不是用指标金叉死叉。昨日 high 被今日 high 上破,返回 EXIT_BUY 平掉多仓;昨日 low 未被今日 low 下破时直接 EXIT_NONE 不动,只有真下破才返回 EXIT_SELL 平空。 枚举 ENUM_EXIT_SIGNAL 给了五个状态:UNKNOWN 是初始未标记,BUY/SELL 分别对应平多平空,ALL 是一刀切全平,NONE 是本次循环不操作。实盘里外汇和贵金属波动受消息面干扰大,突破可能是假突破,这种纯高低点出场在高波动时段可能频繁触发,建议拿 MT5 策略测试器跑 2023 年 XAUUSD 的 D1 数据看回撤。 逐行看代码:Log_Level 控制只打印高于错误级的日志,PrintFormat 把函数名和前后高低点都转成带 _Digits 精度的字符串;else 分支里先判断昨日 low 是否大于今日 low,若是说明没向下突破直接放行。最后函数兜底返回 EXIT_UNKNOWN,防止漏状态。
if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: 价格突破了昨天的最高价: %s > %s", __FUNCTION__, DoubleToString(soa_Prev_D1_Rate[class="num">1].high, _Digits), DoubleToString(soa_Prev_D1_Rate[class="num">0].high, _Digits)); class="kw">return(EXIT_BUY); } else { if(soa_Prev_D1_Rate[class="num">1].low > soa_Prev_D1_Rate[class="num">0].low) class="kw">return(EXIT_NONE); class=class="str">"cmt">// 没有突破 if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: 价格突破了昨天的最低价: %s < %s", __FUNCTION__, DoubleToString(soa_Prev_D1_Rate[class="num">1].low, _Digits), DoubleToString(soa_Prev_D1_Rate[class="num">0].low, _Digits)); class="kw">return(EXIT_SELL); } class="kw">return(EXIT_NONE); class=class="str">"cmt">// for each } class="kw">return(EXIT_UNKNOWN); } enum ENUM_EXIT_SIGNAL { class=class="str">"cmt">// 出场信号的列表 EXIT_UNKNOWN, class=class="str">"cmt">// 没有标记 EXIT_BUY, class=class="str">"cmt">// 关闭买入仓位 EXIT_SELL, class=class="str">"cmt">// 关闭卖出仓位 EXIT_ALL, class=class="str">"cmt">// 全部平仓 EXIT_NONE class=class="str">"cmt">// 不做平仓 };
「把信号模块塞进图表做人工交易指标」
把动量弹球信号模块直接做成指标,比写 EA 更轻:它不自动下单,只在图表上画出挂单位、止损位和固定获利位,并给手机推通知。指标额外把每天第一根小时柱用 DRAW_FILLING 填充,肉眼就能看出为什么这些水平这样摆,弥补策略测试器报告里看不到的视觉信息。 通知走三条通道:终端弹窗带声音、邮件、以及 MT5 推送。实操时建议只开推送,避免欧美盘重叠时弹窗刷屏。外汇和贵金属波动剧烈,这类水平仅作参考,触发概率不等于必胜。 缓冲区要开 6 个、绘图 4 类:1 个填充缓冲画首小时柱,3 个线缓冲画入场/获利/止损,其中入场线用 DRAW_COLOR_LINE 随方向变蓝或粉,另两条单色即可。初始化里把点数转成品种价格,能省掉主循环成千上万次重复换算。 主循环里先用 go_Brownie 结构判新柱,只在昨天和今天柱上算;切换周期要清残留缓冲,否则虚柱会被旧数据填实。模块给信号后先算获利:t_Curr_D1_Bar = Time[i_Current_TF_Bar] - Time[i_Current_TF_Bar] % 86400 拿到当天起点。若价格没触挂单就跳过后序,触单后才画止损获利直到一条被交叉。 挂单未明时 if (i_Period_Bar >= rates_total - 1) break 直接截断,避免在当前未结束日空转。完整源码在 TS_Momentum_Pinball.mq5,f_Do_Alert 推送函数与前篇一致未改,接上就能用。
class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_buffers class="num">6 class="macro">#class="kw">property indicator_plots class="num">4 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;, C&class="macro">#x27;class="num">179,class="num">217,class="num">255&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_COLOR_LINE class="macro">#class="kw">property indicator_style2 STYLE_DASHDOT class="macro">#class="kw">property indicator_color2 clrDodgerBlue, clrDeepPink class="macro">#class="kw">property indicator_width2 class="num">2 class="macro">#class="kw">property indicator_label3 "止损" class="macro">#class="kw">property indicator_type3 DRAW_LINE class="macro">#class="kw">property indicator_style3 STYLE_DASHDOTDOT class="macro">#class="kw">property indicator_color3 clrCrimson class="macro">#class="kw">property indicator_width3 class="num">1 class="macro">#class="kw">property indicator_label4 "获利" class="macro">#class="kw">property indicator_type4 DRAW_LINE class="macro">#class="kw">property indicator_color4 clrGreen class="macro">#class="kw">property indicator_width4 class="num">1 enum ENUM_LOG_LEVEL { class=class="str">"cmt">// 记录的级别 LOG_LEVEL_NONE, class=class="str">"cmt">// 禁止记录 LOG_LEVEL_ERR, class=class="str">"cmt">// 只记录错误 LOG_LEVEL_INFO, class=class="str">"cmt">// 记录错误和EA注释 LOG_LEVEL_DEBUG class=class="str">"cmt">// 全部记录,没有例外 }; enum ENUM_ENTRY_SIGNAL { class=class="str">"cmt">// 入场信号列表 ENTRY_BUY, class=class="str">"cmt">// 买入信号 ENTRY_SELL, class=class="str">"cmt">// 卖出信号 ENTRY_NONE, class=class="str">"cmt">// 没有信号 ENTRY_UNKNOWN, class=class="str">"cmt">// 状态不确定 ENTRY_INTERNAL_ERROR class=class="str">"cmt">// 内部函数错误 }; enum ENUM_EXIT_SIGNAL { class=class="str">"cmt">// 退出信号列表 EXIT_UNKNOWN, class=class="str">"cmt">// 没有识别