基于隐马尔可夫模型的趋势跟踪波动率预测(基础篇)
📘

基于隐马尔可夫模型的趋势跟踪波动率预测(基础篇)

第 1/3 篇

用隐马尔可夫模型拆 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 均线当圣杯 它只是离场参考,不是趋势成立的门槛;快慢线在震荡市反复交叉时,账户回撤可能快速扩大。

MQL5 / C++
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。这样异步成交也不会让主循环傻等。

MQL5 / C++
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 数点的价格成交,实盘前务必在策略测试器用历史数据跑一遍。

MQL5 / C++
      trade.Buy(lott,_Symbol,ask);
      buypos = trade.ResultOrder();
}

常见问题

HMM把行情自动分成几种隐藏状态,能给出当前处于趋势或震荡的概率,比凭感觉判断更少受主观干扰,可直接用作过滤条件。
只在HMM输出高概率趋势状态时允许双均线交叉下单,震荡状态直接跳过信号,回测中这类滤波能明显降低无效交易次数。
小布盯盘的AIGC分析已内置状态识别,打开对应品种页就能看到当前趋势/波动分层结论,不用自己跑模型。
高波动状态放宽止损并降仓位,低波动状态可略加仓但止损收窄,具体倍数按品种历史分位回测确定。
成交后必须立即把ticket和状态写回持仓表,并用定时校验补漏,避免信号重发或平仓事件丢失导致敞口异常。