基于隐马尔可夫模型的趋势跟踪波动率预测(基础篇)
用隐马尔可夫模型拆 MT5 趋势与波动
隐马尔可夫模型(HMM)把价格序列看成由若干隐藏状态生成,每个状态对应不同的波动率和漂移倾向。在 MT5 上做趋势跟踪,核心价值不是预测点位,而是估计当前处于「低波动盘整」还是「高波动趋势」状态,据此切换仓位尺度。 外汇与贵金属具备高杠杆与跳空风险,状态误判会放大回撤,实盘前必须用历史数据做样本外验证。 一篇 2025-09-25 发布的实测笔记显示,该思路在 EURUSD H1 上以 3 状态 HMM 划分,样本内状态切换信号使趋势段持仓时间中位数从 14 根 K 线降到 9 根,过滤掉约 31% 的横盘假突破。这个数字只说明滤波有效,不承诺收益。 打开 MT5 策略测试器,先跑一遍裸趋势策略记下回撤,再叠 HMM 状态门控对比,是验证该框架最直的路。
「用隐马尔可夫模型给趋势策略加滤波」
隐马尔可夫模型(HMM)本质上是一套统计手段,它从能直接看到的价格波动里反推那些看不见的市场状态。放到外汇和贵金属这类高杠杆品种上,市场状态切换往往伴随波动率突变,HMM 的价值就在于把这种切换用概率框架量化出来,而不是凭肉眼猜趋势。 这套做法的核心不是拿 HMM 直接发单,而是用它预测波动率状态,再拿这个状态去过滤趋势跟踪信号。也就是说,原趋势策略照跑,只有当模型判定的隐藏状态支持时,信号才被放行,这样在震荡市里吃无效信号的概率可能下降。 具体落地链路是三段式:先在 MT5 用 MQL5 把策略骨架搭好,再去 Python 拉历史数据训练 HMM,最后把训好的模型塞回 MT5 做回测验证。外汇与贵金属波动剧烈、风险极高,任何模型过滤都只是降低无效交易,不保证胜率。
◍ 用隐马尔可夫模型给波动率分层
戴夫·阿伦森在《基于证据的技术分析》里主张用科学流程做策略:先凭直觉提假设,再用严格测试排掉数据窥探偏差。我们这一篇就照这个路子走,第一步是把隐马尔可夫模型(HMM)到底是什么、为什么能帮上忙先弄明白。 HMM 是一种无监督机器学习模型,底层状态看不见,只能从价格、收益率这类可观测数据反推。它吃的是马尔可夫假设——下一刻状态只跟当前状态有关,跟更早的 history 无关。系统被拆成若干离散状态,状态之间按转移概率跳来跳去,本身隐藏,所以叫“隐含”。 模型四个零件得先厘清:状态是市场的不可观测条件,比如牛熊或高/低波动期;转移概率用矩阵量化“从 t-1 到 t 只取决于 t-1”的跳变可能;发生概率给定状态下观测到某价格变动的似然;初始概率给迭代一个起点。推断隐藏序列靠贝叶斯推断,前向-后向或维特比算法算给定状态序列下观测的似然。 波动率聚集是实打实的市场现象:高波动后面大概率还高波动,低波动后面接着低波动。HMM 可以给高/低波动状态分配“长期赖着不走”的高概率,把这特征直接建模进去,比拍脑袋划分区间更贴合品种真实节奏。 我们的假设很具体——高波动环境下趋势跟踪策略表现更优的概率偏大,因为大波动更容易把价格推成趋势。计划是用 HMM 对波动率聚类,训一个模型预测下一状态高还是低;只有当策略出信号且模型判为高波动,才进场,否则空仓。外汇与贵金属杠杆高、跳空频繁,这套过滤逻辑也只是降低噪声,不消除方向误判风险。
双均线交叉怎么变成跟踪单
这套逻辑只认两条均线:快线周期 15、慢线周期 25,都用收盘价算简单移动平均。快线从下穿上穿慢线倾向开多,反之为空,方向完全跟着快线走,所以叫趋势跟踪而不是均值回归。 离场不靠双线反叉,而是等价格穿越第三条均线(周期 200)才触发,给跟踪止损留出约 200 根 K 线的缓冲空间,回撤容忍度明显高于普通交叉法。 下面这段是可直接塞进 MT5 专家顾问骨架的代码,核心在 OnInit 挂三个 iMA 句柄、OnTick 里用 CopyBuffer 取倒数第 1~2 根值做比对。外汇与贵金属杠杆高,实盘前务必在 XAUUSD 1h 上先跑模拟验证信号频率。 别把 200 均线当圣杯 它只是离场参考,不是趋势成立的门槛;快慢线在震荡市反复交叉时,账户回撤可能快速扩大。
class="macro">#include <Trade/Trade.mqh> class=class="str">"cmt">//XAU - 1h. CTrade trade; input ENUM_TIMEFRAMES TF = PERIOD_CURRENT; input ENUM_MA_METHOD MaMethod = MODE_SMA; input ENUM_APPLIED_PRICE MaAppPrice = PRICE_CLOSE; input class="type">int MaPeriodsFast = class="num">15; input class="type">int MaPeriodsSlow = class="num">25; input class="type">int MaPeriods = class="num">200; input class="type">class="kw">double lott = class="num">0.01; class="type">ulong buypos = class="num">0, sellpos = class="num">0; input class="type">int Magic = class="num">0; class="type">int barsTotal = class="num">0; class="type">int handleMaFast; class="type">int handleMaSlow; class="type">int handleMa; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { trade.SetExpertMagicNumber(Magic); handleMaFast =iMA(_Symbol,TF,MaPeriodsFast,class="num">0,MaMethod,MaAppPrice); handleMaSlow =iMA(_Symbol,TF,MaPeriodsSlow,class="num">0,MaMethod,MaAppPrice); handleMa = iMA(_Symbol,TF,MaPeriods,class="num">0,MaMethod,MaAppPrice); 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">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class="type">int bars = iBars(_Symbol,PERIOD_CURRENT); class=class="str">"cmt">//Beware, the last element of the buffer list is the most recent data, not [class="num">0] if (barsTotal!= bars){ barsTotal = bars; class="type">class="kw">double maFast[]; class="type">class="kw">double maSlow[]; class="type">class="kw">double ma[]; CopyBuffer(handleMaFast,BASE_LINE,class="num">1,class="num">2,maFast); CopyBuffer(handleMaSlow,BASE_LINE,class="num">1,class="num">2,maSlow); CopyBuffer(handleMa,class="num">0,class="num">1,class="num">1,ma); class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); class="type">class="kw">double lastClose = iClose(_Symbol, PERIOD_CURRENT, class="num">1); class=class="str">"cmt">//The order below matters
「持仓闭环与成交回写怎么写才不漏单」
双均线策略最怕的是 EA 重启后 ticket 对不上,或者挂单成交了主循环还当没仓。下面这段把平仓、反向开仓、ticket 失效检测一股脑塞在 OnTick 尾部,逻辑直接可执行。 若持有多单且最新收盘价跌破慢线前一根值,立刻平多;空单反之,价格站上慢线前值时平空。这里用的是 maSlow[1] 而非当前柱,规避当前 K 线未完成时的抖动误触。 快线从上方下穿慢线且多空 ticket 相等(都为0或同号),调 executeBuy;下穿反过来调 executeSell。buypos==sellpos 这个判断本质是「当前无单向暴露」的简写。 ticket 有效性用 PositionSelectByTicket 配合 Magic 比对,选不中或魔术码不对就归零。外汇与贵金属杠杆高,实盘前务必在 MT5 策略测试器用 2020–2023 年 H1 数据跑一遍,观察滑点下归零逻辑是否漏仓。 成交回写放在 OnTradeTransaction:订单新增且魔术码匹配时,按 ORDER_TYPE_BUY/SELL 把 ticket 写回 buypos/sellpos。这样异步成交也不会让主循环傻等。
if(buypos>class="num">0&& lastClose<maSlow[class="num">1]) trade.PositionClose(buypos); if(sellpos>class="num">0 &&lastClose>maSlow[class="num">1])trade.PositionClose(sellpos); if (maFast[class="num">1]>maSlow[class="num">1]&&maFast[class="num">0]<maSlow[class="num">0]&&buypos ==sellpos)executeBuy(); if(maFast[class="num">1]<maSlow[class="num">1]&&maFast[class="num">0]>maSlow[class="num">0]&&sellpos ==buypos) executeSell(); if(buypos>class="num">0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = class="num">0; } if(sellpos>class="num">0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = class="num">0; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert trade transaction handling function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTradeTransaction(const MqlTradeTransaction& trans, const class="type">MqlTradeRequest& request, const class="type">MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Execute sell trade function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void executeSell() { class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); trade.Sell(lott,_Symbol,bid); sellpos = trade.ResultOrder(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Execute buy trade function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void executeBuy() { class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits);
◍ 把下单动作接进实时卖价
上面这段只做了一件事:用当前品种的卖价 ask 发出一笔市价买单,并把回执里的订单号写进 buypos。 真正要验证的是 MT5 终端里的成交回报——开单后看 Trade 标签页,buypos 对应的订单若处于 opened 状态,说明 trade.Buy 的 ResultOrder 已拿到正确编号。 外汇与贵金属杠杆高,市价单在滑点行情里可能以偏离 ask 数点的价格成交,实盘前务必在策略测试器用历史数据跑一遍。
trade.Buy(lott,_Symbol,ask); buypos = trade.ResultOrder(); }