构建自优化型MQL5智能交易系统(EA)(第3部分):动态趋势跟踪与均值回归策略·综合运用
(3/3)·52%到86%胜率跃升背后,是一套不靠AI、只用内置指标的自适应通道模板
区间策略的底盘参数与生命周期
这套基于通道的区间策略,先把两个全局常量钉死:止损宽度设为 1.5 倍 ATR 距离,单笔固定手数 0.1。外汇与贵金属波动剧烈,固定手数配合 ATR 动态止损,能在一定程度上约束单笔风险暴露,但无法消除爆仓可能。 代码里用 CTrade 类接管下单逻辑,并声明了 ma、atr、trading_ma 三个指标句柄数组,以及区间中轨、上下轨和当前 active_zone 状态变量。OnInit 只做一件事——调用 setup() 初始化指标;OnDeinit 则释放三个指标句柄,避免 MT5 终端内存泄漏。 OnTick 的结构很薄:每次报价先 update() 刷新数据,当且仅当无持仓(PositionsTotal()==0)时才跑 find_setup() 寻找信号。这意味着同一时刻只维护一个仓位,回测中若 bars_used=30、atr_multiple=1,通道由最近 30 根 K 线算得,信号频率直接受这两个输入约束。 开 MT5 新建 EA,把下面片段贴进源码,改 bars_used 从 30 调到 15,能直观看到通道变窄、触发次数增加,但假突破概率往往随之上升。
class="macro">#define SL_WIDTH class="num">1.5 class=class="str">"cmt">//How wide should the stop loss be? class="macro">#define CURRENT_VOL class="num">0.1 class=class="str">"cmt">//Trading volume class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Libraries | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Trade\Trade.mqh> CTrade Trade; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int ma_handler,trading_ma_handler,atr_handler; class="type">class="kw">double ma[],atr[],trading_ma[]; class="type">class="kw">double range_zone_mid,range_zone_ub,range_zone_lb; class="type">class="kw">double ask,bid; class="type">int active_zone; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| User inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ input group "Technical Analysis" input class="type">class="kw">double atr_multiple = class="num">1; class=class="str">"cmt">//ATR Multiple input class="type">int bars_used = class="num">30; class=class="str">"cmt">//How Many Bars should we use to calculate the channel? class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- setup(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- IndicatorRelease(ma_handler); IndicatorRelease(atr_handler); IndicatorRelease(trading_ma_handler); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- update(); if(PositionsTotal() == class="num">0) find_setup(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 顺着与逆着区间的入场触发逻辑
这段代码把「趋势区」和「震荡区」的触发拆成了 4 个 active_zone 状态。zone 1 和 4 是顺当前 trading_ma 方向做:zone 1 在现价大于均线时买回调,zone 4 在现价小于均线时卖反弹;zone 2 和 3 是逆着做,但同样要求价格先触到均线才动手。 具体看成交函数,zone 1 的买单位置是 ask 入场,止损放在 bid - atr[0]*1.5,止盈用 bid + atr[0]*SL_WIDTH;zone 4 的卖单则把 atr 倍数换成变量 atr_multiple,说明顺势单的止损倍数可能和外围参数联动,不是写死的 1.5。 setup() 里同时挂了 iATR 和两条 iMA,其中 ma_handler 用 TF_1 周期、trading_ma_handler 用 PERIOD_CURRENT,意味着判定用的均线和趋势过滤均线不在同一周期。你可以在 MT5 里把 TF_1 设成 H1、PERIOD_CURRENT 设成 M5,验证跨周期信号是否如预期延迟。 外汇与贵金属杠杆高,这类 zone 切换逻辑在滑点扩大时可能连续触发,实盘前务必用策略测试器跑至少 3 个月 Tick 数据。
class="type">void find_setup(class="type">void) { class=class="str">"cmt">// Follow the trend if(active_zone == class="num">1) { class=class="str">"cmt">//Buy on rallies if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) > trading_ma[class="num">0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[class="num">0] * class="num">1.5)),(bid + (atr[class="num">0] * SL_WIDTH)),""); } class=class="str">"cmt">// Go against the trend if(active_zone == class="num">2) { class=class="str">"cmt">//Sell on rallies if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) > trading_ma[class="num">0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[class="num">0] * class="num">1.5)),range_zone_mid); } class=class="str">"cmt">// Go against the trend if(active_zone == class="num">3) { class=class="str">"cmt">//Buy the dip if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < trading_ma[class="num">0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[class="num">0] * class="num">1.5)),range_zone_mid,""); } class=class="str">"cmt">// Follow the trend if(active_zone == class="num">4) { class=class="str">"cmt">//Sell the dip if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < trading_ma[class="num">0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[class="num">0] * atr_multiple)),(ask - (atr[class="num">0] * SL_WIDTH))); } } class="type">void setup(class="type">void) { atr_handler = iATR(Symbol(),TF_1,ATR_PERIOD); trading_ma_handler = iMA(Symbol(),PERIOD_CURRENT,TRADING_MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE); ma_handler = iMA(Symbol(),TF_1,MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE); } class="type">void update(class="type">void) { class="kw">static class="type">class="kw">datetime time_stamp; class="type">class="kw">datetime current_time = iTime(Symbol(),PERIOD_CURRENT,class="num">0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
「每根 K 线重算区间并定位价格分区」
这段逻辑只在时间戳跳变时跑一次,避免每 tick 重算指标造成无谓开销。先取当前 bid,确认 time_stamp 不等于 current_time 后,把 MA、ATR 和交易用 MA 的最新值通过 CopyBuffer 拉进数组。 随后用 vector 取 MA 缓冲区前 bars_used 根 K 线的均值作为区间中轴 range_zone_mid,上下边界则按中轴 ± ATR[0]*atr_multiple 得出。atr_multiple 是外部可调参数,设 2 即代表两倍 ATR 通道,外汇与贵金属波动大时这个值需结合周期重新标定,属于高风险品种参数。 画线是顺手做的:先删掉旧的 RANGE HIGH/LOW/MID 三条水平线,再按新算出的 ub、lb、mid 重建。这样图表上始终只有当前区间的三条参考线,不会堆叠。 get_active_zone 用当前收盘价 iClose 对四个区间做判断:高于上轨返回 1,上轨与中轴之间返回 2,中轴与下轨之间返回 3,跌破下轨返回 4。数字即状态机代号,下游开仓逻辑直接读 active_zone 即可,不用再算一遍边界。
bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); if(time_stamp != current_time) { time_stamp = current_time; CopyBuffer(ma_handler,class="num">0,class="num">0,class="num">1,ma); CopyBuffer(atr_handler,class="num">0,class="num">0,class="num">1,atr); CopyBuffer(trading_ma_handler,class="num">0,class="num">0,class="num">1,trading_ma); vector ma_average = vector::Zeros(class="num">1); ma_average.CopyIndicatorBuffer(ma_handler,class="num">0,class="num">1,bars_used); range_zone_mid = ma_average.Mean(); range_zone_ub = (range_zone_mid + (atr[class="num">0] * atr_multiple)); range_zone_lb = (range_zone_mid - (atr[class="num">0] * atr_multiple)); get_active_zone(); Comment("Zone: ",active_zone); ObjectDelete(class="num">0,"RANGE HIGH"); ObjectDelete(class="num">0,"RANGE LOW"); ObjectDelete(class="num">0,"RANGE MID"); ObjectCreate(class="num">0,"RANGE MID",OBJ_HLINE,class="num">0,class="num">0,range_zone_mid); ObjectCreate(class="num">0,"RANGE LOW",OBJ_HLINE,class="num">0,class="num">0,range_zone_lb); ObjectCreate(class="num">0,"RANGE HIGH",OBJ_HLINE,class="num">0,class="num">0,range_zone_ub); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get our current active zone | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void get_active_zone(class="type">void) { if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) > range_zone_ub) { active_zone = class="num">1; class="kw">return; } if((iClose(Symbol(),PERIOD_CURRENT,class="num">0) < range_zone_ub) && (iClose(Symbol(),PERIOD_CURRENT,class="num">0) > range_zone_mid)) { active_zone = class="num">2; class="kw">return; } if((iClose(Symbol(),PERIOD_CURRENT,class="num">0) < range_zone_mid) && (iClose(Symbol(),PERIOD_CURRENT,class="num">0) > range_zone_lb)) { active_zone = class="num">3; class="kw">return; } if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < range_zone_lb) { active_zone = class="num">4; class="kw">return; } }
动态均线策略的后续打磨方向
这一路我们从最基础的均线趋势跟踪起步,借助 MT5 里的历史数据把决策流程逐步自动化。理论上手算也能跑通,但 MQL5 的向量函数把统计指标计算和订单执行都包在了后台,省掉了大量重复劳动。 附件里两份代码就是分水岭:Benchmark_Moving_Average_Strategy.mq5 是 4.47 KB 的初始写死版本,Dynamic_Moving_Average_Strategy.mq5 是 7.24 KB 的动态优化版,文件体积差近 2.8 KB,主要多在那套自适应逻辑上。 外汇和贵金属杠杆高、滑点跳空频繁,直接挂实盘前建议先开 MT5 用策略测试器跑一轮历史回测,重点看亏损单的平均敞口有没有被压住。接下来值得做的,是继续调动态策略里的持仓权重参数,看能否在不放大回撤的前提下把盈利单占比再抬一点。