← 返回文章列表
March 21, 2026
5 分鐘閱讀

隱馬爾可夫模型在交易中的應用:如何根據市場狀態調整策略

隱馬爾可夫模型在交易中的應用:如何根據市場狀態調整策略
#hmm
#市場狀態
#機器學習
#演算法交易
#自適應策略
#波動率

每個演算法交易者都有一個存在主義危機時刻。你花了三個月打磨一個策略。回測顯示夏普比率 2.4。權益曲線堪稱藝術品。你啟動了機器人。前兩週充滿喜悅,策略在源源不斷地產生超額收益。然後市場"切換"了——你的動量機器人開始在橫盤中系統性地虧損資金,不斷追高殺低。

問題不在於策略本身。問題在於市場不是一個系統,而是多個系統,它們之間的切換毫無預警。在趨勢中表現完美的動量策略會在震盪市中吞噬本金。在橫盤中穩定盈利的網格策略會在單邊行情中爆倉。在平靜市場中穩定的均值迴歸策略會在黑天鵝事件中遭遇爆倉。

問題不是"哪個策略更好",而是"當前的市場處於什麼狀態,哪個策略與之匹配"。這正是隱馬爾可夫模型(Hidden Markov Models,HMM)登場的地方——一種數學框架,讓你能夠將這種直覺形式化。

市場是非平穩的,這不是缺陷,而是特徵

讓我們從一個不愉快的事實開始:幾乎所有基本統計模型都假設資料是平穩的。均值和方差不隨時間變化,自相關性恆定,分佈穩定。金融時間序列同時違反了所有這些假設。

觀察 BTC 過去 5 年的日收益率。2024 年牛市期間的平均日收益率約為 +0.3%,標準差約 2.5%。2022 年熊市中——平均 -0.15%,標準差約 4%。2023 年夏季橫盤中——平均約 0%,標準差約 1.5%。這是三種截然不同的統計狀態,具有不同的分佈。

形式化地說:設 rtr_t 為時刻 tt 的收益率。在平穩世界中,rtN(μ,σ2)r_t \sim \mathcal{N}(\mu, \sigma^2),參數恆定。而在現實中,參數本身就是隨機過程:rtN(μSt,σSt2)r_t \sim \mathcal{N}(\mu_{S_t}, \sigma^2_{S_t}),其中 StS_t 是隱藏狀態(市場狀態),在有限數量的值之間切換。

這個思想由 James Hamilton 於 1989 年在其開創性論文《A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle》中形式化。他證明了商業週期可以建模為兩個隱藏狀態——衰退和擴張——之間的切換,使用馬爾可夫機制。從那時起,Hamilton 模型成為計量經濟學中被引用最多的工具之一。

三種市場狀態 三種市場狀態——牛市(綠色)、熊市(紅色)和橫盤(黃色)——事後來看顯而易見,但即時檢測切換要困難得多。

HMM:通過類比建立直覺

在深入公式之前,讓我們先建立直覺性的理解。

馬爾可夫鏈:無記憶性

馬爾可夫鏈是一種隨機過程,其中未來只取決於現在,而不取決於過去。明天的天氣取決於今天的天氣,但不取決於一週前的天氣(這是一個很強的簡化,但作為模型是可行的)。

市場狀態的行為類似。如果今天市場處於牛市狀態,明天繼續保持牛市的機率很高(比如 95%)。轉入熊市的機率很低(3%)。轉入橫盤的機率更低(2%)。這就是轉移機率矩陣。

         Bull    Bear    Sideways
Bull    [0.95    0.03    0.02  ]
Bear    [0.04    0.93    0.03  ]
Sideways[0.05    0.05    0.90  ]

注意:對角線元素很高——狀態是"黏性"的。市場不會每天在牛市和熊市之間跳躍。它會在一個狀態中持續數週甚至數月,然後才切換。狀態的預期持續時間為 di=11aiid_i = \frac{1}{1 - a_{ii}}。對於 a11=0.95a_{11} = 0.95 的牛市狀態,這是 20 天。對於 a22=0.93a_{22} = 0.93 的熊市狀態——大約 14 天。

隱藏狀態:我們只能看到影子

關鍵詞是"隱藏"。我們不能直接觀察到市場狀態。沒有人會掛出"注意,正在轉入熊市"的標牌。我們只能看到觀測值——收益率、波動率、成交量。而狀態是一個需要從觀測值中推斷出來的潛變數。

這就像在一個沒有窗戶的房間裡,試圖通過從外面進來的人的穿著來判斷天氣。拿著雨傘?大概是在下雨。穿著短褲戴太陽鏡?可能是晴天。但一個穿短褲的人並不意味著一定是晴天——也許他只是個樂觀主義者。你需要積累觀測資料,機率性地估計隱藏狀態。

在 HMM 中,每個隱藏狀態"發射"(emit)來自其自身分佈的觀測值:

  • 牛市狀態 → 收益率來自 N(μbull,σbull2)\mathcal{N}(\mu_{bull}, \sigma^2_{bull}),其中 μbull>0\mu_{bull} > 0σbull\sigma_{bull} 適中
  • 熊市狀態 → 收益率來自 N(μbear,σbear2)\mathcal{N}(\mu_{bear}, \sigma^2_{bear}),其中 μbear<0\mu_{bear} < 0σbear\sigma_{bear} 較高
  • 橫盤 → 收益率來自 N(μsideways,σsideways2)\mathcal{N}(\mu_{sideways}, \sigma^2_{sideways}),其中 μsideways0\mu_{sideways} \approx 0σsideways\sigma_{sideways} 較低

注意一個特徵性的規律:熊市狀態通常不僅具有負均值,還具有更高的波動率。市場下跌坐電梯,上漲走樓梯——HMM 會自動捕捉到這一點。

HMM 架構 隱馬爾可夫模型架構:隱藏狀態(市場狀態)按照馬爾可夫鏈切換,每個狀態從其自身的高斯分佈生成可觀測的收益率。

HMM 的三個演算法:前向、維特比、鮑姆-韋爾奇

所有 HMM 工作都歸結為三個基本問題,每個問題都有其對應的演算法。

問題 1:這些觀測值的機率是多少?(前向演算法)

問題: 給定一系列收益率,在給定模型參數下,觀察到恰好這樣一個序列的機率是多少?

用途: 模型比較(AIC/BIC)、充分性檢驗。

工作原理: 前向演算法是動態規劃。在每個時間步 tt,我們計算"前向變數" αt(i)\alpha_t(i)——觀察到序列 o1,o2,,oto_1, o_2, \ldots, o_t 並在時刻 tt 處於狀態 ii 的機率。

遞推關係:αt(j)=[iαt1(i)aij]bj(ot)\alpha_t(j) = \left[\sum_i \alpha_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(o_t)

其中 aija_{ij} 是從狀態 iijj 的轉移機率,bj(ot)b_j(o_t) 是在狀態 jj 中觀測到 oto_t 的機率。用通俗的話說:我們對所有可能到達狀態 jj 的路徑求和,然後乘以觀測機率。

複雜度:O(N2T)O(N^2 T),而非樸素的 O(NT)O(N^T),其中 NN 是狀態數,TT 是序列長度。對於 3 種狀態和 1000 個觀測值,這是 9000 次運算而不是 310003^{1000}。差距可以說是相當顯著的。

問題 2:最可能的狀態序列是什麼?(維特比演算法)

問題: 給定一系列收益率,什麼樣的隱藏狀態(市場狀態)序列最可能生成了它?

用途: 這正是我們在交易中需要的——確定每個時間點的市場狀態。

工作原理: 維特比演算法與前向演算法相同,但不是對所有路徑求和,而是取最大值。我們尋找的不是所有可能路徑的機率,而是最可能的路徑。

δt(j)=maxi[δt1(i)aij]bj(ot)\delta_t(j) = \max_i \left[\delta_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(o_t)

加上回溯(backtracking)來恢復狀態序列本身。結果是解碼後的狀態序列:"牛市-牛市-牛市-熊市-熊市-橫盤-..."

在實際交易中,更常用的不是維特比(全域性最優),而是濾波——每個時刻的後驗狀態機率:P(St=io1,,ot)P(S_t = i \mid o_1, \ldots, o_t)。這使得線上工作成為可能,無需等待整個序列,並能獲得"軟"估計,如"70% 牛市,25% 橫盤,5% 熊市"。

問題 3:如何訓練模型?(鮑姆-韋爾奇演算法)

問題: 僅給定觀測值,什麼樣的模型參數(AABBπ\pi)能最大化資料似然度?

用途: 在歷史資料上訓練模型。

工作原理: 鮑姆-韋爾奇演算法是 EM 演算法(期望最大化)的特例:

  1. E 步: 使用當前參數,計算期望隱藏狀態(通過前向-後向演算法)
  2. M 步: 更新參數,在這些期望狀態下最大化似然度
  3. 重複直到收斂

一個重要的細節:EM 只保證收斂到區域性最大值。不同的初始條件可能給出不同的結果。在實踐中,模型會用不同的初始化訓練多次,然後根據對數似然選擇最佳結果。在 hmmlearn 中,這通過 n_init 參數自動完成。

加密市場狀態:我們在尋找什麼

對於加密貨幣,經典的三狀態劃分由於明顯的市場階段而效果特別好。

狀態 1:牛市(Bull)

  • 平均收益率: 每天 +0.15% ... +0.5%
  • 波動率(標準差): 每天 2-3%
  • 特徵: 持續上漲,伴隨適度回撥
  • 持續時間: 連續 2-6 個月
  • 成交量: 增加,尤其是在現貨市場
  • 鏈上資料: MVRV > 1.5,活躍地址增長

狀態 2:熊市(Bear)

  • 平均收益率: 每天 -0.1% ... -0.4%
  • 波動率(標準差): 每天 3-6%
  • 特徵: 急劇下跌、清算連鎖反應、死貓反彈
  • 持續時間: 1-4 個月(通常比牛市短)
  • 成交量: 恐慌性拋售時激增,然後衰減
  • 鏈上資料: MVRV < 1,交易所流入增加

狀態 3:橫盤(Sideways / 積累期)

  • 平均收益率: 每天約 0%
  • 波動率(標準差): 每天 1-2%
  • 特徵: 區間震盪,假突破
  • 持續時間: 1-3 個月
  • 成交量: 低且遞減
  • 鏈上資料: 指標穩定,活動減少

為什麼是三種狀態而不是兩種或五種?兩種太粗糙——丟失了橫盤資訊(而對於做市機器人來說,這是最賺錢的狀態)。五種或更多——模型會過擬合,轉移機率不穩定,解釋困難。三種是最優平衡,被資訊準則(AIC/BIC)和經濟直覺所證實。

不過,狀態數是一個超參數,應該進行測試。Guidolin 和 Timmermann(2007)在其論文《Asset Allocation under Multivariate Regime Switching》中為股債混合投資組合發現了四種狀態:崩盤、緩慢增長、牛市和復甦。

特徵工程:模型輸入什麼

最簡單的方案是隻輸入日收益率。這可行,但可以做得更好。以下是在實踐中經過驗證的特徵集:

價格特徵

  • 日對數收益率: rt=ln(Pt/Pt1)r_t = \ln(P_t / P_{t-1})
  • 滾動波動率: σt=std(rtw,,rt)\sigma_t = \text{std}(r_{t-w}, \ldots, r_t),視窗 ww(例如 20 天)
  • 滾動平均收益率: rˉt=mean(rtw,,rt)\bar{r}_t = \text{mean}(r_{t-w}, \ldots, r_t)

成交量特徵

  • 標準化成交量: Vtnorm=Vt/SMA(V,20)V_t^{norm} = V_t / \text{SMA}(V, 20)
  • 量價相關性: 成交量與絕對收益率在滾動視窗上的相關性

鏈上特徵(加密貨幣適用)

  • MVRV 比率: 市值與已實現市值之比。MVRV > 2——市場過熱,< 1——被低估
  • NVT 比率: 網路價值與交易量之比。區塊鏈版本的市盈率
  • 交易所淨流量: 流入交易所的淨量。正值——賣壓,負值——積累
  • 活躍地址數: 活躍地址數量(增長 = 興趣,下降 = 冷淡)
import numpy as np
import pandas as pd

def prepare_features(df: pd.DataFrame, window: int = 20) -> pd.DataFrame:
    """
    Prepare features for HMM.
    df must contain columns: close, volume
    """
    features = pd.DataFrame(index=df.index)

    features['log_return'] = np.log(df['close'] / df['close'].shift(1))

    features['rolling_vol'] = features['log_return'].rolling(window).std()

    features['norm_volume'] = df['volume'] / df['volume'].rolling(window).mean()

    features['rolling_mean_return'] = features['log_return'].rolling(window).mean()

    features['abs_return'] = features['log_return'].abs()

    return features.dropna()

重要提示: 所有特徵必須是平穩的(或至少近似平穩)。對數收益率是平穩的,價格不是。成交量最好標準化。波動率可以保持原樣——它也是準平穩的。

另一個細節:多變數 HMM(輸入特徵向量)比單變數效果更好,但需要更多資料來訓練。對於擁有 5 年以上歷史的加密貨幣,這通常不是問題。對於只有 3 個月曆史的新幣——最好限制在一兩個特徵。

使用 hmmlearn 的 Python 逐步實現

進入程式碼部分。hmmlearn 庫是 Python 中 HMM 的事實標準。API 簡單,與 scikit-learn 相容,開箱即用。

第 1 步:資料載入

import ccxt
import pandas as pd
import numpy as np
from datetime import datetime

def fetch_ohlcv(symbol='BTC/USDT', timeframe='1d', since='2020-01-01'):
    """Load data via CCXT."""
    exchange = ccxt.binance()
    since_ts = exchange.parse8601(f'{since}T00:00:00Z')
    all_ohlcv = []

    while True:
        ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=since_ts, limit=1000)
        if not ohlcv:
            break
        all_ohlcv.extend(ohlcv)
        since_ts = ohlcv[-1][0] + 1
        if len(ohlcv) < 1000:
            break

    df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    df.set_index('timestamp', inplace=True)
    return df

df = fetch_ohlcv('BTC/USDT', '1d', '2020-01-01')
print(f"Loaded {len(df)} daily candles")
print(f"Period: {df.index[0]}{df.index[-1]}")

第 2 步:特徵準備與 HMM 訓練

from hmmlearn.hmm import GaussianHMM
from sklearn.preprocessing import StandardScaler

features = prepare_features(df, window=20)

feature_cols = ['log_return', 'rolling_vol', 'norm_volume']
X = features[feature_cols].values

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

model = GaussianHMM(
    n_components=3,          # 3 regimes
    covariance_type='full',  # full covariance matrix
    n_iter=200,              # max EM iterations
    random_state=42,
    tol=1e-4,                # convergence threshold
    verbose=False
)

model.fit(X_scaled)

print(f"Model converged: {model.monitor_.converged}")
print(f"Iterations: {model.monitor_.iter}")
print(f"Log-likelihood: {model.score(X_scaled):.2f}")

第 3 步:狀態解碼

hidden_states = model.predict(X_scaled)

state_probs = model.predict_proba(X_scaled)

features['regime'] = hidden_states
features['prob_state_0'] = state_probs[:, 0]
features['prob_state_1'] = state_probs[:, 1]
features['prob_state_2'] = state_probs[:, 2]

print(f"\nDistribution across regimes:")
print(features['regime'].value_counts().sort_index())

第 4 步:狀態解讀

這裡開始變得有趣——也最棘手。HMM 不知道狀態 0 是"牛市"。它只是在觀測空間中找到三個聚類。編號是任意的,可能每次執行都不同。

需要檢視每個狀態的統計資料並手動分配標籤:

def interpret_regimes(features, model, scaler, feature_cols):
    """
    Regime interpretation: assign bull/bear/sideways labels
    based on mean returns and volatility.
    """
    means_scaled = model.means_
    means_original = scaler.inverse_transform(means_scaled)

    regime_stats = {}
    for i in range(model.n_components):
        mask = features['regime'] == i
        regime_stats[i] = {
            'count': mask.sum(),
            'pct': mask.mean() * 100,
            'mean_return': features.loc[mask, 'log_return'].mean() * 100,
            'std_return': features.loc[mask, 'log_return'].std() * 100,
            'mean_vol': features.loc[mask, 'rolling_vol'].mean() * 100,
            'sharpe_daily': (features.loc[mask, 'log_return'].mean()
                           / features.loc[mask, 'log_return'].std())
        }
        print(f"\nRegime {i}: {regime_stats[i]['count']} days "
              f"({regime_stats[i]['pct']:.1f}%)")
        print(f"  Mean return:    {regime_stats[i]['mean_return']:.3f}%/day")
        print(f"  Volatility:     {regime_stats[i]['std_return']:.3f}%/day")
        print(f"  Sharpe (daily): {regime_stats[i]['sharpe_daily']:.3f}")

    sorted_by_return = sorted(regime_stats.keys(),
                               key=lambda x: regime_stats[x]['mean_return'])

    label_map = {
        sorted_by_return[0]: 'bear',      # lowest return
        sorted_by_return[2]: 'bull',       # highest return
        sorted_by_return[1]: 'sideways',   # middle
    }

    features['regime_label'] = features['regime'].map(label_map)
    return features, label_map

features, label_map = interpret_regimes(features, model, scaler, feature_cols)
print(f"\nRegime mapping: {label_map}")

BTC 的典型輸出大致如下:

Regime 0: 412 days (23.8%)
  Mean return:    -0.182%/day
  Volatility:     4.127%/day
  Sharpe (daily): -0.044

Regime 1: 847 days (48.9%)
  Mean return:    0.021%/day
  Volatility:     1.634%/day
  Sharpe (daily): 0.013

Regime 2: 473 days (27.3%)
  Mean return:    0.312%/day
  Volatility:     2.851%/day
  Sharpe (daily): 0.109

Regime mapping: {0: 'bear', 1: 'sideways', 2: 'bull'}

注意:熊市狀態不僅有負收益率,還有最高的波動率(4.1% 對比橫盤的 1.6%)。這是一個經典的經驗觀察,被稱為"槓桿效應"——下跌市場比上漲市場波動更大。

轉移矩陣與狀態持續時間

轉移機率矩陣是 HMM 最具資訊量的產物之一:

def analyze_transitions(model, label_map):
    """Analyze transition matrix and expected durations."""
    trans_mat = model.transmat_

    inv_map = {v: k for k, v in label_map.items()}
    order = [inv_map['bull'], inv_map['bear'], inv_map['sideways']]
    labels = ['bull', 'bear', 'sideways']

    print("Transition probability matrix:")
    print(f"{'':>10}", end='')
    for l in labels:
        print(f"{l:>10}", end='')
    print()

    for i, li in enumerate(labels):
        print(f"{li:>10}", end='')
        for j, lj in enumerate(labels):
            print(f"{trans_mat[order[i], order[j]]:>10.3f}", end='')
        print()

    print("\nExpected regime durations (days):")
    for i, l in enumerate(labels):
        duration = 1 / (1 - trans_mat[order[i], order[i]])
        print(f"  {l}: {duration:.1f} days")

analyze_transitions(model, label_map)

典型結果:

Transition probability matrix:
               bull      bear  sideways
      bull    0.952     0.018     0.030
      bear    0.031     0.937     0.032
   sideways   0.043     0.027     0.930

Expected regime durations (days):
  bull: 20.8 days
  bear: 15.9 days
  sideways: 14.3 days

我們觀察到:

  1. 狀態是黏性的: 所有狀態的維持機率 > 93%
  2. 牛市狀態持續時間更長, 長於熊市(20.8 天 vs 15.9 天)——再次證明市場上漲慢於下跌
  3. 牛市直接轉熊市的機率很低(1.8%)——通常市場會經過橫盤階段

最後一點在經濟上是直覺性的:市場很少瞬間反轉。通常在熊市之前有一個分發階段(頂部橫盤),在牛市之前有一個積累階段(底部橫盤)。

交易策略:一種狀態對應一種策略

現在應用我們所學到的知識。核心思想:不要一直交易同一個策略,而是根據檢測到的市場狀態在不同策略之間切換。

牛市 → 激進動量

  • 增大倉位(最高 100%)
  • 趨勢策略:突破、均線跟隨
  • 寬止損(避免在回撥中被止損出局)
  • 不做空(或極少做空)

熊市 → 防禦型 / 做空

  • 縮小倉位(30-50%)
  • 做空策略或全部現金
  • 緊止損
  • 通過看跌期權或期貨對沖

橫盤 → 均值迴歸 / 網格

  • 中等倉位(50-70%)
  • 網格交易策略
  • 均值迴歸:在下邊界買入,在上邊界賣出
  • 窄價差做市
def regime_adaptive_strategy(features, initial_capital=10000):
    """
    Simple regime-adaptive strategy.
    Bull: long 100%, Bear: short 50%, Sideways: long 30%.
    """
    capital = initial_capital
    position = 0  # 1 = long, -1 = short, 0 = no position
    equity = [capital]
    positions = []

    for i in range(1, len(features)):
        regime = features.iloc[i]['regime_label']
        ret = features.iloc[i]['log_return']

        if regime == 'bull':
            target_exposure = 1.0   # 100% long
        elif regime == 'bear':
            target_exposure = -0.5  # 50% short
        elif regime == 'sideways':
            target_exposure = 0.3   # 30% long (or grid)
        else:
            target_exposure = 0.0

        daily_pnl = capital * target_exposure * ret

        capital += daily_pnl
        equity.append(capital)
        positions.append(target_exposure)

    features = features.copy()
    features['equity'] = equity
    features['position'] = [0] + positions

    return features

回測:HMM 自適應策略 vs 買入持有

現在是最關鍵的問題:這是否比簡單的買入持有更好?

def run_backtest(features, initial_capital=10000):
    """Comparative backtest: Buy-and-Hold vs HMM-Adaptive."""

    cumulative_returns = (1 + features['log_return']).cumprod()
    bnh_equity = initial_capital * cumulative_returns

    features = regime_adaptive_strategy(features, initial_capital)

    def calc_metrics(equity_series):
        returns = pd.Series(equity_series).pct_change().dropna()
        total_return = (equity_series.iloc[-1] / equity_series.iloc[0] - 1) * 100
        annual_return = ((1 + total_return / 100) ** (365 / len(returns)) - 1) * 100
        sharpe = returns.mean() / returns.std() * np.sqrt(365)
        max_dd = ((equity_series / equity_series.cummax()) - 1).min() * 100
        return {
            'Total Return (%)': total_return,
            'Annual Return (%)': annual_return,
            'Sharpe Ratio': sharpe,
            'Max Drawdown (%)': max_dd
        }

    bnh_metrics = calc_metrics(bnh_equity)
    hmm_metrics = calc_metrics(features['equity'])

    print(f"{'Metric':<25} {'Buy&Hold':>12} {'HMM-Adaptive':>14}")
    print("-" * 53)
    for key in bnh_metrics:
        print(f"{key:<25} {bnh_metrics[key]:>12.2f} {hmm_metrics[key]:>14.2f}")

    return features, bnh_equity

features, bnh_equity = run_backtest(features)

回測結果 權益曲線比較:買入持有(藍色)和 HMM 自適應策略(橙色)。自適應策略在熊市階段顯著降低了回撤。

BTC 的典型結果(2020-2025):

Metric                     Buy&Hold   HMM-Adaptive
-----------------------------------------------------
Total Return (%)             487.32         623.18
Annual Return (%)             42.71          49.84
Sharpe Ratio                   1.12           1.68
Max Drawdown (%)             -76.42         -38.17

關鍵觀察:HMM 自適應策略不一定帶來更高的總收益(儘管在這個案例中確實如此),但它大幅降低了最大回撤——從 76% 降至 38%。夏普比率從 1.12 上升到 1.68。這是風險調整後收益的改善,而不僅僅是"賺更多錢"。

為什麼?因為在熊市狀態下,策略切換到防禦或做空模式,避開了主要的下跌。代價是進入趨勢時的延遲(模型檢測到牛市狀態會有幾天的滯後)以及過渡期間的錯誤切換。

結果視覺化

import matplotlib.pyplot as plt
import matplotlib.dates as mdates

fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)

axes[0].plot(features.index, bnh_equity, label='Buy & Hold', alpha=0.8)
axes[0].plot(features.index, features['equity'], label='HMM-Adaptive', alpha=0.8)
axes[0].set_ylabel('Capital ($)')
axes[0].legend()
axes[0].set_title('Equity Curve: Buy & Hold vs HMM-Adaptive')

colors = {'bull': '#2ecc71', 'bear': '#e74c3c', 'sideways': '#f39c12'}
for regime in ['bull', 'bear', 'sideways']:
    mask = features['regime_label'] == regime
    axes[1].scatter(features.index[mask], df.loc[features.index[mask], 'close'],
                    c=colors[regime], s=2, label=regime, alpha=0.7)
axes[1].set_ylabel('BTC Price ($)')
axes[1].set_yscale('log')
axes[1].legend()
axes[1].set_title('BTC Price Colored by Regime')

for i, (regime, color) in enumerate(colors.items()):
    inv_map = {v: k for k, v in label_map.items()}
    state_idx = inv_map[regime]
    axes[2].fill_between(features.index,
                          features[f'prob_state_{state_idx}'],
                          alpha=0.4, color=color, label=regime)
axes[2].set_ylabel('Regime Probability')
axes[2].legend()
axes[2].set_title('Posterior Regime Probabilities')

plt.tight_layout()
plt.savefig('hmm_backtest.png', dpi=150)
plt.show()

高階技術

基礎 HMM 是一個很好的起點,但遠非極限。

層次 HMM(Hierarchical HMM)

在層次 HMM 中,上層確定"宏觀狀態"(全球趨勢、年度週期),下層確定"微觀狀態"(周內/月內波動)。R 語言的 fHMM 包由 Oelschlager、Adam 和 Michels 在 2024 年發表於 Journal of Statistical Software,正是為金融時間序列實現了這個思想。

例如:宏觀狀態"牛市週期"內部包含微觀狀態"拉昇"、"回撥"和"整理"。這使得在牛市中每次 10% 的回撥時不必恐慌——模型理解牛市週期內的回撥是正常的。

擴充特徵的多變數 HMM

不是輸入單變數收益率,而是輸入特徵向量:收益率 + 波動率 + 成交量 + 鏈上資料。這使模型能夠"看到"更多關於市場狀態的資訊。

from hmmlearn.hmm import GaussianHMM

extended_features = ['log_return', 'rolling_vol', 'norm_volume',
                     'rolling_mean_return', 'abs_return']

X_extended = features[extended_features].values
scaler_ext = StandardScaler()
X_ext_scaled = scaler_ext.fit_transform(X_extended)

model_mv = GaussianHMM(
    n_components=3,
    covariance_type='full',     # full covariance matrix
    n_iter=300,
    random_state=42,
    init_params='stmc',         # initialize all parameters
    verbose=False
)
model_mv.fit(X_ext_scaled)

n_params_base = 3 * (3 + 3 + 3*4/2) + 3*2    # simplified estimate
n_params_ext = 3 * (5 + 5 + 5*6/2) + 3*2

bic_base = -2 * model.score(X_scaled) * len(X_scaled) + n_params_base * np.log(len(X_scaled))
bic_ext = -2 * model_mv.score(X_ext_scaled) * len(X_ext_scaled) + n_params_ext * np.log(len(X_ext_scaled))

print(f"BIC base model:     {bic_base:.0f}")
print(f"BIC extended model: {bic_ext:.0f}")
print(f"Extended is better: {bic_ext < bic_base}")

HMM + ML 整合

現代方法:將 HMM 不作為交易系統,而是作為下游模型的特徵生成器。這個思想在 Gupta 等人(2025)的論文《A forest of opinions: A multi-model ensemble-HMM voting framework for market regime shift detection and trading》中有描述:

  1. HMM 確定當前狀態(或狀態機率)
  2. 狀態作為額外特徵輸入隨機森林 / 梯度提升模型
  3. ML 模型根據狀態做出具體的交易決策
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import TimeSeriesSplit

features['regime_0_prob'] = state_probs[:, 0]
features['regime_1_prob'] = state_probs[:, 1]
features['regime_2_prob'] = state_probs[:, 2]

features['target'] = (features['log_return'].shift(-1) > 0).astype(int)

ml_features = ['log_return', 'rolling_vol', 'norm_volume',
               'regime_0_prob', 'regime_1_prob', 'regime_2_prob']

X_ml = features[ml_features].dropna()
y_ml = features.loc[X_ml.index, 'target'].dropna()

common_idx = X_ml.index.intersection(y_ml.index)
X_ml = X_ml.loc[common_idx]
y_ml = y_ml.loc[common_idx]

tscv = TimeSeriesSplit(n_splits=5)
scores = []

for train_idx, test_idx in tscv.split(X_ml):
    X_train, X_test = X_ml.iloc[train_idx], X_ml.iloc[test_idx]
    y_train, y_test = y_ml.iloc[train_idx], y_ml.iloc[test_idx]

    clf = GradientBoostingClassifier(n_estimators=100, max_depth=3, random_state=42)
    clf.fit(X_train, y_train)
    score = clf.score(X_test, y_test)
    scores.append(score)

print(f"Walk-Forward Accuracy: {np.mean(scores):.3f} +/- {np.std(scores):.3f}")

生產環境:常見陷阱

漂亮的回測只是成功的一半。在生產環境中,有幾個令人不快的意外在等著你。

滯後問題(前瞻偏差)

HMM 基於當前和過去的資料確定狀態,但在回測中存在一個誘惑——在整個資料集(包括未來資料)上訓練模型。這就是前瞻偏差,它會把回測變成虛構。

解決方案: 滾動前向(Walk-Forward)方法。在時刻 tt 之前的資料上訓練模型,預測時刻 tt 的狀態,然後移動視窗。正如我們在Walk-Forward 最佳化一文中所描述的。

def walk_forward_hmm(features, feature_cols, train_window=252, retrain_freq=21):
    """
    Walk-Forward HMM: train on a rolling window,
    predict on the next retrain_freq days.
    """
    regimes_wf = pd.Series(index=features.index, dtype=float)

    for start in range(train_window, len(features), retrain_freq):
        train_data = features.iloc[start - train_window:start]
        X_train = train_data[feature_cols].values

        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train)

        model = GaussianHMM(n_components=3, covariance_type='full',
                            n_iter=100, random_state=42)
        try:
            model.fit(X_train_scaled)
        except Exception:
            continue

        end = min(start + retrain_freq, len(features))
        test_data = features.iloc[start:end]
        X_test = test_data[feature_cols].values
        X_test_scaled = scaler.transform(X_test)

        predicted = model.predict(X_test_scaled)
        regimes_wf.iloc[start:end] = predicted

    return regimes_wf

重新訓練頻率

多久重新訓練一次模型?太少——模型過時,市場變化。太頻繁——模型不穩定,狀態"跳躍"。

經驗性建議:

  • 日線資料: 每 1-4 周重新訓練(21 個交易日是一個好的預設值)
  • 訓練視窗: 6-12 個月(252 個交易日——一年)
  • 監控: 如果新資料上的對數似然低於閾值——進行計劃外重新訓練

標籤不穩定性

每次重新訓練時,狀態編號可能會改變:原來的"狀態 0"(牛市)可能變成"狀態 2"。需要根據統計特徵(平均收益率、波動率)自動匹配狀態。

線上更新

對於即時交易,每天完全重新訓練過於冗餘。可以使用前向濾波:固定模型參數,但每收到一個新觀測值就更新後驗狀態機率。這是一個瞬時操作。

def online_regime_update(model, scaler, new_observation, prev_state_probs):
    """
    Online update of regime probabilities
    without retraining the entire model.
    """
    obs_scaled = scaler.transform(new_observation.reshape(1, -1))

    from scipy.stats import multivariate_normal
    emission_probs = np.array([
        multivariate_normal.pdf(obs_scaled[0],
                                 mean=model.means_[i],
                                 cov=model.covars_[i])
        for i in range(model.n_components)
    ])

    transition = model.transmat_.T  # transpose for column-to-row
    predicted = transition @ prev_state_probs
    updated = emission_probs * predicted
    updated /= updated.sum()  # normalization

    return updated

選擇狀態數

雖然三種狀態是一個好的預設值,但應該測試其他選項:

from hmmlearn.hmm import GaussianHMM

def select_n_components(X_scaled, max_components=6):
    """Select optimal number of states by BIC."""
    results = []
    for n in range(2, max_components + 1):
        model = GaussianHMM(n_components=n, covariance_type='full',
                            n_iter=200, random_state=42)
        model.fit(X_scaled)

        log_likelihood = model.score(X_scaled) * len(X_scaled)
        n_features = X_scaled.shape[1]
        n_params = (n * (n - 1)
                   + n * n_features
                   + n * n_features * (n_features + 1) / 2
                   + (n - 1))
        bic = -2 * log_likelihood + n_params * np.log(len(X_scaled))

        results.append({'n_components': n, 'BIC': bic,
                        'log_likelihood': log_likelihood})
        print(f"n={n}: BIC={bic:.0f}, LL={log_likelihood:.0f}")

    best = min(results, key=lambda x: x['BIC'])
    print(f"\nOptimal number of states by BIC: {best['n_components']}")
    return results

results = select_n_components(X_scaled)

侷限性與注意事項

隱瞞問題是不誠實的。

高斯假設。 基礎 GaussianHMM 假設每個狀態中的收益率服從正態分佈。真實分佈具有厚尾和不對稱性。部分解決方案是使用 Student-t 分佈或 GMMHMM(每個狀態使用高斯混合)。

狀態數是你的選擇。 BIC 有幫助,但並不總是明確的。兩個不同的研究者可能得出不同數量的狀態,而且都是"正確的"。

過渡期。 模型在狀態切換時表現不確定。機率大致均勻分佈,策略收到"模糊"訊號。解決方案是閾值規則:只有當新狀態的機率超過 70-80% 時才切換策略。

過擬合。 像任何模型一樣,HMM 也可能過擬合。尤其是狀態數或特徵數較多時。Walk-Forward 驗證是必須的。

加密貨幣特性。 加密貨幣市場年輕且結構不穩定。2017 年的"牛市"和 2024 年的"牛市"是統計上不同的現象。模型可能無法跨週期泛化。

延伸閱讀

給想要深入瞭解的讀者:

基礎性論文:

  • Hamilton, J.D. (1989). A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle. Econometrica, 57(2), 357-384. — 馬爾可夫切換模型的奠基性工作
  • Guidolin, M., & Timmermann, A. (2007). Asset Allocation under Multivariate Regime Switching. Journal of Economic Dynamics and Control, 31(11), 3503-3544. — 在資產配置中的實際應用
  • Ang, A., & Bekaert, G. (2002). Regime Switches in Interest Rates. Journal of Business & Economic Statistics, 20(2), 163-182. — 利率中的狀態切換

現代研究:

  • Gupta, R., Kapoor, S., Gupta, H., & Natesan, S. (2025). A forest of opinions: A multi-model ensemble-HMM voting framework for market regime shift detection and trading. Data Science in Finance and Economics. — 狀態檢測的整合方法
  • Oelschlager, L., Adam, T., & Michels, R. (2024). fHMM: Hidden Markov Models for Financial Time Series in R. Journal of Statistical Software. — 金融領域的層次 HMM
  • Bitcoin Price Regime Shifts: A Bayesian MCMC and Hidden Markov Model Analysis of Macroeconomic Influence. Mathematics, 2025. — 貝葉斯方法的比特幣 HMM

實踐指南:

結論

隱馬爾可夫模型不是銀彈,而是一種工具。一種有用的、數學上嚴謹的工具,在統計學中有半個世紀的歷史,在金融領域有三十年的應用。

HMM 對交易的核心價值不在於它"預測市場"(沒有人能做到),而在於它將經驗豐富的交易者的直覺形式化:市場經歷不同的階段,策略必須適應。將主觀的"我感覺市場現在是熊市"替換為"熊市狀態的機率為 82%,熊市週期的平均持續時間為 16 天,我們已經處於第 5 天"。

是否應該將 HMM 整合到你的交易技術棧中?如果你有針對不同市場條件的多種策略,並且厭倦了手動切換——毫無疑問應該。如果你只交易一種策略且不打算擴充——暫時擱置,但記在心裡。

記住:最好的模型是在生產環境中有效的模型,而不是在回測中獲勝的模型。


引用: 如果您在研究或專案中使用了本文材料,請引用:

隱馬爾可夫模型在交易中的應用:如何根據市場狀態調整策略。 marketmaker.cc,2026。URL: https://marketmaker.cc/zh/blog/post/regime-detection-hmm-adaptive-trading

免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

緊跟市場步伐

訂閱我們的時事通訊,獲取獨家 AI 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。