每個演算法交易者都有一個存在主義危機時刻。你花了三個月打磨一個策略。回測顯示夏普比率 2.4。權益曲線堪稱藝術品。你啟動了機器人。前兩週充滿喜悅,策略在源源不斷地產生超額收益。然後市場"切換"了——你的動量機器人開始在橫盤中系統性地虧損資金,不斷追高殺低。
問題不在於策略本身。問題在於市場不是一個系統,而是多個系統,它們之間的切換毫無預警。在趨勢中表現完美的動量策略會在震盪市中吞噬本金。在橫盤中穩定盈利的網格策略會在單邊行情中爆倉。在平靜市場中穩定的均值迴歸策略會在黑天鵝事件中遭遇爆倉。
問題不是"哪個策略更好",而是"當前的市場處於什麼狀態,哪個策略與之匹配"。這正是隱馬爾可夫模型(Hidden Markov Models,HMM)登場的地方——一種數學框架,讓你能夠將這種直覺形式化。
市場是非平穩的,這不是缺陷,而是特徵
讓我們從一個不愉快的事實開始:幾乎所有基本統計模型都假設資料是平穩的。均值和方差不隨時間變化,自相關性恆定,分佈穩定。金融時間序列同時違反了所有這些假設。
觀察 BTC 過去 5 年的日收益率。2024 年牛市期間的平均日收益率約為 +0.3%,標準差約 2.5%。2022 年熊市中——平均 -0.15%,標準差約 4%。2023 年夏季橫盤中——平均約 0%,標準差約 1.5%。這是三種截然不同的統計狀態,具有不同的分佈。
形式化地說:設 為時刻 的收益率。在平穩世界中,,參數恆定。而在現實中,參數本身就是隨機過程:,其中 是隱藏狀態(市場狀態),在有限數量的值之間切換。
這個思想由 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 ]
注意:對角線元素很高——狀態是"黏性"的。市場不會每天在牛市和熊市之間跳躍。它會在一個狀態中持續數週甚至數月,然後才切換。狀態的預期持續時間為 。對於 的牛市狀態,這是 20 天。對於 的熊市狀態——大約 14 天。
隱藏狀態:我們只能看到影子
關鍵詞是"隱藏"。我們不能直接觀察到市場狀態。沒有人會掛出"注意,正在轉入熊市"的標牌。我們只能看到觀測值——收益率、波動率、成交量。而狀態是一個需要從觀測值中推斷出來的潛變數。
這就像在一個沒有窗戶的房間裡,試圖通過從外面進來的人的穿著來判斷天氣。拿著雨傘?大概是在下雨。穿著短褲戴太陽鏡?可能是晴天。但一個穿短褲的人並不意味著一定是晴天——也許他只是個樂觀主義者。你需要積累觀測資料,機率性地估計隱藏狀態。
在 HMM 中,每個隱藏狀態"發射"(emit)來自其自身分佈的觀測值:
- 牛市狀態 → 收益率來自 ,其中 , 適中
- 熊市狀態 → 收益率來自 ,其中 , 較高
- 橫盤 → 收益率來自 ,其中 , 較低
注意一個特徵性的規律:熊市狀態通常不僅具有負均值,還具有更高的波動率。市場下跌坐電梯,上漲走樓梯——HMM 會自動捕捉到這一點。
隱馬爾可夫模型架構:隱藏狀態(市場狀態)按照馬爾可夫鏈切換,每個狀態從其自身的高斯分佈生成可觀測的收益率。
HMM 的三個演算法:前向、維特比、鮑姆-韋爾奇
所有 HMM 工作都歸結為三個基本問題,每個問題都有其對應的演算法。
問題 1:這些觀測值的機率是多少?(前向演算法)
問題: 給定一系列收益率,在給定模型參數下,觀察到恰好這樣一個序列的機率是多少?
用途: 模型比較(AIC/BIC)、充分性檢驗。
工作原理: 前向演算法是動態規劃。在每個時間步 ,我們計算"前向變數" ——觀察到序列 並在時刻 處於狀態 的機率。
遞推關係:
其中 是從狀態 到 的轉移機率, 是在狀態 中觀測到 的機率。用通俗的話說:我們對所有可能到達狀態 的路徑求和,然後乘以觀測機率。
複雜度:,而非樸素的 ,其中 是狀態數, 是序列長度。對於 3 種狀態和 1000 個觀測值,這是 9000 次運算而不是 。差距可以說是相當顯著的。
問題 2:最可能的狀態序列是什麼?(維特比演算法)
問題: 給定一系列收益率,什麼樣的隱藏狀態(市場狀態)序列最可能生成了它?
用途: 這正是我們在交易中需要的——確定每個時間點的市場狀態。
工作原理: 維特比演算法與前向演算法相同,但不是對所有路徑求和,而是取最大值。我們尋找的不是所有可能路徑的機率,而是最可能的路徑。
加上回溯(backtracking)來恢復狀態序列本身。結果是解碼後的狀態序列:"牛市-牛市-牛市-熊市-熊市-橫盤-..."
在實際交易中,更常用的不是維特比(全域性最優),而是濾波——每個時刻的後驗狀態機率:。這使得線上工作成為可能,無需等待整個序列,並能獲得"軟"估計,如"70% 牛市,25% 橫盤,5% 熊市"。
問題 3:如何訓練模型?(鮑姆-韋爾奇演算法)
問題: 僅給定觀測值,什麼樣的模型參數(、、)能最大化資料似然度?
用途: 在歷史資料上訓練模型。
工作原理: 鮑姆-韋爾奇演算法是 EM 演算法(期望最大化)的特例:
- E 步: 使用當前參數,計算期望隱藏狀態(通過前向-後向演算法)
- M 步: 更新參數,在這些期望狀態下最大化似然度
- 重複直到收斂
一個重要的細節: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》中為股債混合投資組合發現了四種狀態:崩盤、緩慢增長、牛市和復甦。
特徵工程:模型輸入什麼
最簡單的方案是隻輸入日收益率。這可行,但可以做得更好。以下是在實踐中經過驗證的特徵集:
價格特徵
- 日對數收益率:
- 滾動波動率: ,視窗 (例如 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
我們觀察到:
- 狀態是黏性的: 所有狀態的維持機率 > 93%
- 牛市狀態持續時間更長, 長於熊市(20.8 天 vs 15.9 天)——再次證明市場上漲慢於下跌
- 牛市直接轉熊市的機率很低(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》中有描述:
- HMM 確定當前狀態(或狀態機率)
- 狀態作為額外特徵輸入隨機森林 / 梯度提升模型
- 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)方法。在時刻 之前的資料上訓練模型,預測時刻 的狀態,然後移動視窗。正如我們在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
實踐指南:
- QuantStart: Market Regime Detection using Hidden Markov Models in QSTrader
- QuantInsti: Step-by-Step Python Guide for Regime-Specific Trading Using HMM and Random Forest
- hmmlearn documentation
結論
隱馬爾可夫模型不是銀彈,而是一種工具。一種有用的、數學上嚴謹的工具,在統計學中有半個世紀的歷史,在金融領域有三十年的應用。
HMM 對交易的核心價值不在於它"預測市場"(沒有人能做到),而在於它將經驗豐富的交易者的直覺形式化:市場經歷不同的階段,策略必須適應。將主觀的"我感覺市場現在是熊市"替換為"熊市狀態的機率為 82%,熊市週期的平均持續時間為 16 天,我們已經處於第 5 天"。
是否應該將 HMM 整合到你的交易技術棧中?如果你有針對不同市場條件的多種策略,並且厭倦了手動切換——毫無疑問應該。如果你只交易一種策略且不打算擴充——暫時擱置,但記在心裡。
記住:最好的模型是在生產環境中有效的模型,而不是在回測中獲勝的模型。
引用: 如果您在研究或專案中使用了本文材料,請引用:
隱馬爾可夫模型在交易中的應用:如何根據市場狀態調整策略。 marketmaker.cc,2026。URL: https://marketmaker.cc/zh/blog/post/regime-detection-hmm-adaptive-trading
Authors
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.