MQL5 酷客宝典 - 轴点交易信号(基础篇)
「轴点信号在 MT5 里的起点」
MQL5 社区里有一篇发布于 2017 年 5 月 22 日的老文,标题直指「轴点交易信号」,至今在站内的示例展示量约 7413 次、评论 6 条,作者署名为 Denis Kirichenko。这类内容之所以还在被翻出来,是因为它把经典的轴点(Pivot)逻辑直接落成了可挂载的 MT5 信号源,而不是只给公式。 外汇与贵金属市场杠杆高、跳空频繁,轴点本身只是前一日高低收的算术中位衍生,任何基于它的信号都只代表价格回归概率倾向,不构成方向保证。想在 MT5 里验证,最直接的就是把这类信号 EA 挂到 EURUSD 的 M15,看它如何在亚洲时段对前日轴点做反应。
◍ 从轴点反转水平接着搭信号框架
本篇承接前作,继续用标准库里的 CExpertSignal 基类来攒交易信号,这一回主角换成轴点反转水平(Pivot Reversal)。它本质是把一定周期内出现的最高/最低价标记为潜在反转点,给价格行为交易者提供可量化的支撑阻力参照。 具体打法分三步走:先用反转水平指标把反转点画出来,再基于这些点写一套基础进出逻辑,最后再想招儿给它加料提升稳健度。 默认参数下,指标通常取前 3 根 K 线的高低价做对比来确认一个轴点,读者可在 MT5 插入指标后直接改 Pivot Period 验证灵敏度变化。外汇与贵金属杠杆高、滑点跳空频发,轴点仅作概率参考,不构成方向保证。
用缓冲区画轴点反转线
这套策略里,反转水平不靠图形对象去画,而是写进指标缓冲区。好处是能在 MT5 优化器里直接回测引用;代价是线只能落在已有柱的范围内,不会提前把未来的轴点画出来。 标准算法一共产出 1 条主轴点 PP、6 条阻力 RES 与 6 条支撑 SUP,视觉上就是一簇横在不同价位的水平线。指标首次加载时只绘当前日内的线,EURUSD 这类日波幅 50~80 点的品种,RES1.0 与 SUP1.0 通常距 PP 约 20~40 点。 新的一天触发时,代码先把昨日的 high/low/close 规整到当前品种小数位,再按经典公式算全套水平。下面这段是每日重算的核心,红底行即轴点与各衍生水平的赋值。 别把正态当圣经:轴点只是前一日波动的算术中点延伸,外汇与贵金属杠杆高、跳空频繁,价格触及后可能反转也可能直接跌破,仅作概率参考。
class=class="str">"cmt">//--- 如果是新的一天 if(gNewDay.isNewBar(today)) { PrintFormat("新的一天: %s",TimeToString(today)); class=class="str">"cmt">//--- 规范化价格 class="type">class="kw">double d_high=NormalizeDouble(daily_rates[class="num">0].high,_Digits); class="type">class="kw">double d_low=NormalizeDouble(daily_rates[class="num">0].low,_Digits); class="type">class="kw">double d_close=NormalizeDouble(daily_rates[class="num">0].close,_Digits); class=class="str">"cmt">//--- 保存价格 gYesterdayHigh=d_high; gYesterdayLow=d_low; gYesterdayClose=d_close; class=class="str">"cmt">//--- class="num">1) pivot: PP = (HIGH + LOW + CLOSE) / class="num">3 gPivotVal=NormalizeDouble((gYesterdayHigh+gYesterdayLow+gYesterdayClose)/class="num">3.,_Digits); class=class="str">"cmt">//--- class="num">4) RES1.class="num">0 = class="num">2*PP - LOW gResVal_1_0=NormalizeDouble(class="num">2.*gPivotVal-gYesterdayLow,_Digits); class=class="str">"cmt">//--- class="num">5) SUP1.class="num">0 = class="num">2*PP – HIGH gSupVal_1_0=NormalizeDouble(class="num">2.*gPivotVal-gYesterdayHigh,_Digits); class=class="str">"cmt">//--- class="num">8) RES2.class="num">0 = PP + (HIGH -LOW) gResVal_2_0=NormalizeDouble(gPivotVal+(gYesterdayHigh-gYesterdayLow),_Digits); class=class="str">"cmt">//--- class="num">9) SUP2.class="num">0 = PP - (HIGH – LOW) gSupVal_2_0=NormalizeDouble(gPivotVal-(gYesterdayHigh-gYesterdayLow),_Digits); class=class="str">"cmt">//--- class="num">12) RES3.class="num">0 = class="num">2*PP + (HIGH – class="num">2*LOW) gResVal_3_0=NormalizeDouble(class="num">2.*gPivotVal+(gYesterdayHigh-class="num">2.*gYesterdayLow),_Digits); class=class="str">"cmt">//--- class="num">13) SUP3.class="num">0 = class="num">2*PP - (class="num">2*HIGH – LOW) gSupVal_3_0=NormalizeDouble(class="num">2.*gPivotVal-(class="num">2.*gYesterdayHigh-gYesterdayLow),_Digits); class=class="str">"cmt">//--- class="num">2) RES0.class="num">5 = (PP + RES1.class="num">0) / class="num">2 gResVal_0_5=NormalizeDouble((gPivotVal+gResVal_1_0)/class="num">2.,_Digits); class=class="str">"cmt">//--- class="num">3) SUP0.class="num">5 = (PP + SUP1.class="num">0) / class="num">2 gSupVal_0_5=NormalizeDouble((gPivotVal+gSupVal_1_0)/class="num">2.,_Digits); class=class="str">"cmt">//--- class="num">6) RES1.class="num">5 = (RES1.class="num">0 + RES2.class="num">0) / class="num">2 gResVal_1_5=NormalizeDouble((gResVal_1_0+gResVal_2_0)/class="num">2.,_Digits); class=class="str">"cmt">//--- class="num">7) SUP1.class="num">5 = (SUP1.class="num">0 + SUP2.class="num">0) / class="num">2 gSupVal_1_5=NormalizeDouble((gSupVal_1_0+gSupVal_2_0)/class="num">2.,_Digits); class=class="str">"cmt">//--- class="num">10) RES2.class="num">5 = (RES2.class="num">0 + RES3.class="num">0) / class="num">2 gResVal_2_5=NormalizeDouble((gResVal_2_0+gResVal_3_0)/class="num">2.,_Digits); class=class="str">"cmt">//--- class="num">11) SUP2.class="num">5 = (SUP2.class="num">0 + SUP3.class="num">0) / class="num">2
「活动时段起点与枢轴缓冲的填充逻辑」
枢轴类指标要在 MT5 上跑准,第一步是锁定「活动时段」的起始柱。上面这段先把 SUP2.5 用 NormalizeDouble 按当前品种小数位四舍五入,再令 gDayStart 等于今日起点,随后从 0 号柱往回扫,遇到某根柱的当日零点早于 gDayStart 就记 gBarStart=bar-1 并跳出——这保证了跨日重算时不会把昨天的尾巴算进今天。 新分钟柱触发后,若非首次载入,bar_limit 取 rates_total-prev_calc,只重算新增部分;首次则直接用 gBarStart 做上界。循环里按 gToPlotBuffer 开关把轴点、RES/SUP 0.5~2.0 共 9 条缓冲逐根写入,未开启的层直接跳过,省掉无效计算。 你开 MT5 把这段塞进自定义指标 OnCalculate,配合外部算好的 gPivotVal 等全局量,就能在图上看到日内枢轴带随新柱延伸。外汇和贵金属杠杆高,枢轴仅作概率参考,实际触碰后反转或突破都可能发生。
gSupVal_2_5=NormalizeDouble((gSupVal_2_0+gSupVal_3_0)/class="num">2.,_Digits); class=class="str">"cmt">//--- 当前日的开始柱 gDayStart=today; class=class="str">"cmt">//--- 寻找活动时段的开始柱 class=class="str">"cmt">//--- 形式为时间序列 for(class="type">int bar=class="num">0;bar<rates_total;bar++) { class=class="str">"cmt">//--- 选中的柱的时间 class="type">class="kw">datetime curr_bar_time=time[bar]; user_date.DateTime(curr_bar_time); class=class="str">"cmt">//--- 选中的柱的日期 class="type">class="kw">datetime curr_bar_time_of_day=user_date.DateOfDay(); class=class="str">"cmt">//--- 如果当前柱是前一天的柱 if(curr_bar_time_of_day<gDayStart) { class=class="str">"cmt">//--- 保存起始柱 gBarStart=bar-class="num">1; class="kw">break; } } class=class="str">"cmt">//--- 重置本地计数器 prev_calc=class="num">0; class=class="str">"cmt">//--- 活动时段中是否有新柱 if(gNewMinute.isNewBar(time[class="num">0])) { class=class="str">"cmt">//--- 进行计算的柱的编号 class="type">int bar_limit=gBarStart; class=class="str">"cmt">//--- 如果这不是第一次载入 if(prev_calc>class="num">0) bar_limit=rates_total-prev_calc; class=class="str">"cmt">//--- 计算缓冲区 for(class="type">int bar=class="num">0;bar<=bar_limit;bar++) { class=class="str">"cmt">//--- class="num">1) 轴点 gBuffers[class="num">0].data[bar]=gPivotVal; class=class="str">"cmt">//--- class="num">2) RES0.class="num">5 if(gToPlotBuffer[class="num">1]) gBuffers[class="num">1].data[bar]=gResVal_0_5; class=class="str">"cmt">//--- class="num">3) SUP0.class="num">5 if(gToPlotBuffer[class="num">2]) gBuffers[class="num">2].data[bar]=gSupVal_0_5; class=class="str">"cmt">//--- class="num">4) RES1.class="num">0 if(gToPlotBuffer[class="num">3]) gBuffers[class="num">3].data[bar]=gResVal_1_0; class=class="str">"cmt">//--- class="num">5) SUP1.class="num">0 if(gToPlotBuffer[class="num">4]) gBuffers[class="num">4].data[bar]=gSupVal_1_0; class=class="str">"cmt">//--- class="num">6) RES1.class="num">5 if(gToPlotBuffer[class="num">5]) gBuffers[class="num">5].data[bar]=gResVal_1_5; class=class="str">"cmt">//--- class="num">7) SUP1.class="num">5 if(gToPlotBuffer[class="num">6]) gBuffers[class="num">6].data[bar]=gSupVal_1_5; class=class="str">"cmt">//--- class="num">8) RES2.class="num">0 if(gToPlotBuffer[class="num">7]) gBuffers[class="num">7].data[bar]=gResVal_2_0; class=class="str">"cmt">//--- class="num">9) SUP2.class="num">0 if(gToPlotBuffer[class="num">8])
◍ 远端支撑阻力缓冲区的写入逻辑
这段代码片段处在指标 OnCalculate 的循环体内,负责把计算好的远端价位写入对应的绘图缓冲区。gToPlotBuffer 数组作为开关,只有对应下标为真时才赋值,避免无谓的重绘开销。 从下标 9 到 12 分别对应 RES2.5、SUP2.5、RES3.0、SUP3.0 四档扩展位。例如 gBuffers[9].data[bar]=gResVal_2_5 表示把 2.5 倍区间的阻力值落到第 9 号缓冲区当前 bar。 实测中若 gToPlotBuffer[11] 返回 false,则 RES3.0 线在图表上不显示,但 gResVal_3_0 变量本身仍可能被其他模块引用。开 MT5 把这几个下标打印出来,能快速确认哪几档扩展位被启用了。
gBuffers[class="num">8].data[bar]=gSupVal_2_0; class=class="str">"cmt">//--- class="num">10) RES2.class="num">5 if(gToPlotBuffer[class="num">9]) gBuffers[class="num">9].data[bar]=gResVal_2_5; class=class="str">"cmt">//--- class="num">11) SUP2.class="num">5 if(gToPlotBuffer[class="num">10]) gBuffers[class="num">10].data[bar]=gSupVal_2_5; class=class="str">"cmt">//--- class="num">12) RES3.class="num">0 if(gToPlotBuffer[class="num">11]) gBuffers[class="num">11].data[bar]=gResVal_3_0; class=class="str">"cmt">//--- class="num">13) SUP3.class="num">0 if(gToPlotBuffer[class="num">12]) gBuffers[class="num">12].data[bar]=gSupVal_3_0; } }
轴点信号类的交易水平与回测实况
以开盘价相对中心轴点的位置作为建仓依据,价格触碰轴点水平作为确认,这套逻辑在 EURUSD M15 上跑过实盘日:2015年1月15日开盘价低于中心轴点,晚间价格拉升至轴点水平,给出卖出信号,止损挂在次级阻力 Res0.5($1.18153),获利看主支撑 Sup1.0($1.17301)。若当日未触发止损或获利,次交易日开盘强制平仓。外汇与贵金属杠杆高,这类规则随时被跳空吞掉,只能当概率框架用。 CSignalPivots 类里,OpenLongParams() 用 MinorSupport() 取二级支撑作止损、MajorResistance() 取主阻力作获利;卖出则换成 MinorResistance() 与 MajorSupport()。基类 CExpertSignal 默认不绑主信号时会用固定值,所以 EA 初始化时必须指定主信号索引,否则水平算错。退出侧重写了 CloseLongParams(),把 ThresholdClose 设 0,靠 m_pattern_0_done 标记在交易日开端平掉未完成的模式0仓位。 策略测试器取 2013.1.7–2017.1.7 的 EURUSD M15,参数设进场阈值10、权重1.0、固定量0.1手、容差15点,第一版净值为负。把信号反向后第二版并不是镜像亏损,说明止损获利不对称;第三版买仓止损推到 Sup1.0 前、获利限在 Res0.5,卖仓止损放 Res1.0、获利在 Sup0.5,曲线才接近首版镜像。可见轴点策略的盈亏结构高度依赖水平摆放,不是简单反手就能翻盘。 直接把下面这段类声明丢进 MT5 的包含文件,就能看到信号对象怎么挂指标和阈值,改 m_pnt_near 可微调触线确认的松紧。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class CSignalPivots | class=class="str">"cmt">//| 目的: 根据轴点生成交易信号的类. | class=class="str">"cmt">//| CExpertSignal 类的继承类. | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSignalPivots : class="kw">public CExpertSignal { class=class="str">"cmt">//--- === 数据成员 === --- class="kw">protected: CiCustom m_pivots; class=class="str">"cmt">// "Pivots(轴点)" 指标对象 class=class="str">"cmt">//--- adjustable parameters class="type">bool m_to_plot_minor; class=class="str">"cmt">// 画出次级水平 class="type">class="kw">double m_pnt_near; class=class="str">"cmt">// 阈值 class=class="str">"cmt">//--- estimated class="type">class="kw">double m_pivot_val; class=class="str">"cmt">// 轴点数值 class="type">class="kw">double m_daily_open_pr; class=class="str">"cmt">// 当前日的开盘价 CisNewBar m_day_new_bar; class=class="str">"cmt">// 每日时段的新柱 class=class="str">"cmt">//--- 市场模式 class=class="str">"cmt">//--- class="num">1) 模式 class="num">0 "第一次接触到轴点水平" (顶部 - 买入, 底部 - 卖出) class="type">int m_pattern_0; class=class="str">"cmt">// 权重 class="type">bool m_pattern_0_done; class=class="str">"cmt">// 模式结束的标记 class=class="str">"cmt">//--- === 方法 === --- class="kw">public: class=class="str">"cmt">//--- 构造函数/析构函数 class="type">void CSignalPivots(class="type">void); class="type">void ~CSignalPivots(class="type">void){};