Hidden Markov Models dalam Trading: Cara Mengadaptasi Strategi ke Regime Pasar
Setiap algo trader pernah mengalami krisis eksistensial. Anda menghabiskan tiga bulan untuk sebuah strategi. Backtest menunjukkan Sharpe 2.4. Kurva ekuitas seperti karya seni. Anda menjalankan bot. Dua minggu pertama penuh euforia — strategi menghasilkan alpha. Kemudian pasar "berpindah" — dan bot momentum Anda mulai secara metodis menguras modal dalam range, membeli setiap high lokal dan menjual setiap low lokal.
Masalahnya bukan pada strategi. Masalahnya adalah pasar bukan satu sistem, melainkan beberapa sistem yang saling berpindah tanpa peringatan. Strategi momentum yang sempurna untuk tren akan menghancurkan akun dalam range. Strategi grid yang menghasilkan uang di pasar sideways akan meledak saat terjadi pergerakan searah. Mean-reversion yang stabil di pasar tenang akan terkena margin call saat black swan.
Pertanyaannya bukan "strategi mana yang lebih baik," tapi "apa regime pasar saat ini dan strategi mana yang cocok dengannya." Dan di sinilah Hidden Markov Models (HMM) masuk ke panggung — kerangka matematis yang memungkinkan Anda memformalkan intuisi ini.
Pasar Bersifat Non-Stasioner, dan Itu Bukan Bug, Melainkan Fitur
Mari mulai dengan kebenaran yang tidak menyenangkan: hampir semua model statistik dasar mengasumsikan stasioneritas data. Mean dan varians tidak berubah seiring waktu, autokorelasi konstan, distribusi stabil. Deret waktu finansial melanggar semua asumsi ini secara bersamaan.
Lihat return harian BTC selama 5 tahun terakhir. Return harian rata-rata selama rally bull 2024 sekitar +0,3%, dengan deviasi standar ~2,5%. Di pasar bear 2022 — rata-rata -0,15%, deviasi standar ~4%. Di pasar sideways musim panas 2023 — rata-rata ~0%, deviasi standar ~1,5%. Ini adalah tiga regime statistik yang berbeda secara fundamental dengan distribusi yang berbeda.
Secara formal: misalkan adalah return pada waktu . Dalam dunia stasioner, dengan parameter konstan. Dalam kenyataan, parameter itu sendiri adalah proses acak: , di mana adalah state tersembunyi (regime pasar), yang berpindah di antara sejumlah nilai terbatas.
Ide ini diformalisasi pada tahun 1989 oleh James Hamilton dalam makalah fondationalnya "A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle." Ia menunjukkan bahwa siklus bisnis dapat dimodelkan sebagai perpindahan antara dua state tersembunyi — resesi dan ekspansi — menggunakan mekanisme Markov. Sejak saat itu, model Hamilton menjadi salah satu alat yang paling banyak dikutip dalam ekonometrika.
Tiga regime pasar — bull (hijau), bear (merah), dan sideways (kuning) — secara visual jelas dalam retrospeksi, tetapi mendeteksi perpindahan secara real-time jauh lebih sulit.
HMM: Intuisi Melalui Analogi
Sebelum menyelami formula, mari bangun intuisi terlebih dahulu.
Rantai Markov: Tanpa Memori
Rantai Markov adalah proses acak di mana masa depan hanya bergantung pada masa kini, bukan masa lalu. Cuaca besok bergantung pada cuaca hari ini, tetapi tidak pada cuaca seminggu yang lalu (penyederhanaan yang kuat, tapi berfungsi sebagai model).
Regime pasar berperilaku serupa. Jika hari ini pasar berada di regime bull, kemungkinan tetap di sana besok tinggi (misalnya, 95%). Kemungkinan transisi ke bearish rendah (3%). Ke sideways — bahkan lebih rendah (2%). Inilah matriks probabilitas transisi.
Bull Bear Sideways
Bull [0.95 0.03 0.02 ]
Bear [0.04 0.93 0.03 ]
Sideways[0.05 0.05 0.90 ]
Perhatikan: elemen diagonal tinggi — regime bersifat "lengket." Pasar tidak melompat dari bull ke bear setiap hari. Ia tetap dalam satu regime selama berminggu-minggu dan berbulan-bulan sebelum berpindah. Durasi yang diharapkan dari sebuah regime adalah . Untuk regime bull dengan , itu adalah 20 hari. Untuk regime bear dengan — sekitar 14 hari.
State Tersembunyi: Kita Hanya Melihat Bayangannya
Kata kuncinya adalah "tersembunyi." Kita tidak mengamati regime pasar secara langsung. Tidak ada tanda yang berbunyi "Perhatian, beralih ke regime bear." Kita hanya melihat observasi — return, volatilitas, volume. Regime adalah variabel laten yang harus disimpulkan dari observasi.
Ini seperti berada di dalam ruangan tanpa jendela dan mencoba menentukan cuaca dari cara orang yang masuk dari luar berpakaian. Payung? Kemungkinan hujan. Celana pendek dan kacamata hitam? Cerah. Tapi satu orang bercelana pendek bukan berarti pasti cerah — mungkin mereka hanya optimis. Anda perlu mengakumulasi observasi dan memperkirakan state tersembunyi secara probabilistik.
Dalam HMM, setiap regime tersembunyi "mengeluarkan" observasi dari distribusinya sendiri:
- Regime bull → return dari , di mana , moderat
- Regime bear → return dari , di mana , tinggi
- Sideways → return dari , di mana , rendah
Perhatikan pola karakteristik: regime bear biasanya tidak hanya memiliki mean negatif, tetapi juga volatilitas yang meningkat. Pasar turun dengan lift dan naik dengan tangga — dan HMM menangkap ini secara otomatis.
Arsitektur Hidden Markov Model: state tersembunyi (regime) berpindah sesuai rantai Markov, setiap state menghasilkan return yang dapat diamati dari distribusi Gaussian-nya sendiri.
Tiga Algoritma HMM: Forward, Viterbi, Baum-Welch
Semua pekerjaan dengan HMM berujung pada tiga masalah fundamental, masing-masing dengan algoritmanya sendiri.
Masalah 1: Berapa Probabilitas Observasi Ini? (Algoritma Forward)
Pertanyaan: Diberikan urutan return, berapa probabilitas mengamati urutan persis ini dengan parameter model yang diberikan?
Mengapa: Perbandingan model (AIC/BIC), pengecekan kecukupan.
Cara kerjanya: Algoritma Forward adalah pemrograman dinamis. Pada setiap langkah , kita menghitung "variabel forward" — probabilitas mengamati urutan dan berada di state pada waktu .
Rekursi:
Di mana adalah probabilitas transisi dari state ke , dan adalah probabilitas observasi di state . Dengan kata-kata: kita menjumlahkan semua jalur yang memungkinkan kita tiba di state , dan mengalikan dengan probabilitas observasi.
Kompleksitas: alih-alih yang naif, di mana adalah jumlah state, adalah panjang urutan. Untuk 3 regime dan 1000 observasi, itu adalah 9000 operasi alih-alih . Perbedaannya, bisa dikatakan, sangat besar.
Masalah 2: Apa Urutan Regime yang Paling Mungkin? (Algoritma Viterbi)
Pertanyaan: Diberikan urutan return, urutan state tersembunyi (regime) mana yang paling mungkin menghasilkannya?
Mengapa: Inilah yang kita butuhkan untuk trading — menentukan regime pada setiap titik waktu.
Cara kerjanya: Algoritma Viterbi sama dengan Forward, tetapi alih-alih menjumlahkan semua jalur, ia mengambil maksimum. Kita mencari bukan probabilitas semua jalur yang mungkin, tetapi jalur yang paling mungkin.
Ditambah backward pass (backtracking) untuk memulihkan urutan state itu sendiri. Hasilnya adalah urutan regime yang didekode: "bull-bull-bull-bear-bear-sideways-..."
Dalam praktik, untuk trading, yang lebih umum digunakan bukan Viterbi (optimum global) tetapi filtering — probabilitas state posterior pada setiap momen: . Ini memungkinkan bekerja secara online tanpa menunggu seluruh urutan, dan memperoleh estimasi "lunak" seperti "70% bull, 25% sideways, 5% bear."
Masalah 3: Bagaimana Melatih Model? (Algoritma Baum-Welch)
Pertanyaan: Diberikan hanya observasi, parameter model (, , ) mana yang memaksimalkan kemungkinan data?
Mengapa: Melatih model pada data historis.
Cara kerjanya: Algoritma Baum-Welch adalah kasus khusus dari algoritma EM (Expectation-Maximization):
- E-step: Menggunakan parameter saat ini, hitung state tersembunyi yang diharapkan (melalui Forward-Backward)
- M-step: Perbarui parameter dengan memaksimalkan kemungkinan berdasarkan state yang diharapkan ini
- Ulangi sampai konvergen
Nuansa penting: EM menjamin konvergensi hanya ke maksimum lokal. Kondisi awal yang berbeda dapat menghasilkan hasil yang berbeda. Dalam praktik, model dilatih beberapa kali dengan inisialisasi berbeda, dan hasil terbaik dipilih berdasarkan log-likelihood. Di hmmlearn, ini dilakukan secara otomatis melalui parameter n_init.
Regime Pasar Crypto: Apa yang Kita Cari
Untuk cryptocurrency, pembagian tiga regime klasik bekerja sangat baik karena fase pasar yang menonjol.
Regime 1: Bull
- Return rata-rata: +0,15% ... +0,5% per hari
- Volatilitas (std): 2-3% per hari
- Karakter: pertumbuhan berkelanjutan dengan pullback moderat
- Durasi: 2-6 bulan terus-menerus
- Volume: meningkat, terutama di pasar spot
- On-chain: MVRV > 1,5, pertumbuhan alamat aktif
Regime 2: Bear
- Return rata-rata: -0,1% ... -0,4% per hari
- Volatilitas (std): 3-6% per hari
- Karakter: crash tajam, kaskade likuidasi, dead cat bounce
- Durasi: 1-4 bulan (biasanya lebih pendek dari bull)
- Volume: lonjakan saat panic selling, kemudian memudar
- On-chain: MVRV < 1, inflow ke exchange meningkat
Regime 3: Sideways (akumulasi)
- Return rata-rata: ~0% per hari
- Volatilitas (std): 1-2% per hari
- Karakter: pergerakan dalam range, false breakout
- Durasi: 1-3 bulan
- Volume: rendah, menurun
- On-chain: metrik stabil, aktivitas menurun
Mengapa tepat tiga regime dan bukan dua atau lima? Dua terlalu kasar — Anda kehilangan informasi tentang fase sideways (dan untuk bot market-making, ini adalah regime yang paling menguntungkan). Lima atau lebih — model menjadi overfit, probabilitas transisi tidak stabil, interpretasi sulit. Tiga adalah keseimbangan optimal, dikonfirmasi oleh kriteria informasi (AIC/BIC) maupun intuisi ekonomi.
Meskipun demikian, jumlah state adalah hyperparameter dan harus diuji. Guidolin & Timmermann (2007) dalam makalah mereka "Asset Allocation under Multivariate Regime Switching" menemukan empat regime untuk portofolio campuran saham-dan-obligasi: crash, pertumbuhan lambat, bull, dan pemulihan.
Feature Engineering: Apa yang Diberikan ke Model
Opsi paling sederhana adalah hanya memasukkan return harian. Ini berhasil, tetapi bisa ditingkatkan. Berikut adalah set fitur yang telah terbukti baik dalam praktik:
Fitur Harga
- Log return harian:
- Volatilitas rolling: dalam window (misalnya, 20 hari)
- Return rata-rata rolling:
Fitur Volume
- Volume yang dinormalisasi:
- Korelasi volume-harga: korelasi antara volume dan return absolut dalam rolling window
Fitur On-Chain (untuk cryptocurrency)
- MVRV Ratio: kapitalisasi pasar terhadap kapitalisasi yang direalisasikan. MVRV > 2 — pasar terlalu panas, < 1 — undervalued
- NVT Ratio: nilai jaringan terhadap volume transaksi. Setara blockchain dari P/E
- Exchange Net Flow: net flow ke exchange. Positif — tekanan jual, negatif — akumulasi
- Active Addresses: jumlah alamat aktif (pertumbuhan = minat, penurunan = apatis)
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()
Penting: semua fitur harus stasioner (atau setidaknya kuasi-stasioner). Log return bersifat stasioner. Harga tidak. Volume lebih baik dinormalisasi. Volatilitas bisa dibiarkan apa adanya — itu juga kuasi-stasioner.
Nuansa lain: HMM multivariat (ketika vektor fitur dimasukkan sebagai input) bekerja lebih baik dari univariat, tetapi membutuhkan lebih banyak data untuk pelatihan. Untuk crypto dengan riwayat 5+ tahun, ini biasanya bukan masalah. Untuk altcoin baru dengan riwayat 3 bulan — lebih baik tetap gunakan satu atau dua fitur.
Implementasi Langkah demi Langkah di Python dengan hmmlearn
Mari masuk ke kode. Library hmmlearn adalah standar de facto untuk HMM di Python. API sederhana, kompatibel dengan scikit-learn, langsung bisa digunakan.
Langkah 1: Memuat Data
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]}")
Langkah 2: Persiapan Fitur dan Pelatihan 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}")
Langkah 3: Dekoding Regime
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())
Langkah 4: Interpretasi Regime
Di sinilah hal menjadi menarik — dan rumit. HMM tidak tahu bahwa regime 0 adalah "bull." Ia hanya menemukan tiga cluster dalam ruang observasi. Penomoran bersifat arbitrer dan dapat berubah dari satu run ke run lainnya.
Anda perlu melihat statistik setiap regime dan menetapkan label secara manual:
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}")
Output tipikal untuk BTC kurang lebih seperti ini:
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'}
Perhatikan: regime bear tidak hanya memiliki return negatif, tetapi juga volatilitas tertinggi (4,1% vs. 1,6% di sideways). Ini adalah observasi empiris klasik yang dikenal sebagai "efek leverage" — pasar yang jatuh lebih volatil daripada yang naik.
Matriks Transisi dan Durasi Regime
Matriks probabilitas transisi adalah salah satu artefak HMM yang paling informatif:
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)
Hasil tipikal:
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
Yang kita amati:
- Regime bersifat lengket: probabilitas tetap di regime saat ini > 93% untuk semua state
- Regime bull berlangsung lebih lama dari bear (20,8 vs 15,9 hari) — sekali lagi, pasar naik lebih lambat dari turunnya
- Transisi langsung dari bull ke bear tidak mungkin terjadi (1,8%) — biasanya pasar melewati fase sideways terlebih dahulu
Poin terakhir secara ekonomi intuitif: pasar jarang berbalik seketika. Biasanya ada fase distribusi (sideways di puncak) sebelum pasar bear, dan fase akumulasi (sideways di bawah) sebelum pasar bull.
Strategi Trading: Satu Regime — Satu Strategi
Sekarang kita terapkan apa yang telah kita pelajari. Idenya: jangan trading satu strategi sepanjang waktu, tetapi beralih antar strategi tergantung pada regime yang terdeteksi.
Bull → Momentum Agresif
- Ukuran posisi diperbesar (hingga 100% dari modal)
- Strategi tren: breakout, mengikuti moving average
- Stop-loss lebar (jangan terkena stop saat pullback)
- Jangan short (atau short secara minimal)
Bear → Defensif / Posisi Short
- Ukuran posisi dikurangi (30-50% dari modal)
- Strategi short atau full cash
- Stop-loss ketat
- Hedging melalui put option atau futures
Sideways → Mean-Reversion / Grid
- Ukuran posisi menengah (50-70% dari modal)
- Strategi grid trading
- Mean-reversion: beli di batas bawah, jual di batas atas
- Market-making dengan spread ketat
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
Backtest: Strategi HMM-Adaptive vs Buy-and-Hold
Sekarang pertanyaan utama: apakah ini lebih baik dari Buy-and-Hold biasa?
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)
Perbandingan kurva ekuitas: Buy-and-Hold (biru) dan strategi HMM-adaptive (oranye). Strategi adaptif secara signifikan mengurangi drawdown selama fase bear.
Hasil tipikal untuk 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
Observasi utama: strategi HMM-adaptive tidak harus memberikan total return yang lebih tinggi (meskipun dalam kasus ini memang demikian), tetapi secara dramatis mengurangi drawdown maksimum — dari 76% menjadi 38%. Sharpe naik dari 1,12 menjadi 1,68. Ini adalah peningkatan dalam return yang disesuaikan risiko, bukan sekadar "lebih banyak uang."
Mengapa? Karena di regime bear, strategi beralih ke mode defensif atau short, menghindari crash besar. Biayanya adalah keterlambatan masuk ke tren (model mendeteksi regime bull dengan lag beberapa hari) dan perpindahan palsu selama periode transisi.
Visualisasi Hasil
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()
Teknik Lanjutan
HMM dasar adalah titik awal yang baik, tetapi jauh dari batas kemampuan.
HMM Hierarkis
Dalam HMM hierarkis, level atas menentukan "macro-regime" (tren global, siklus tahunan), dan level bawah menentukan "micro-regime" (fluktuasi intra-minggu/intra-bulan). Paket fHMM untuk R, yang diterbitkan di Journal of Statistical Software pada tahun 2024 (Oelschlager, Adam, Michels), mengimplementasikan ide ini persis untuk deret waktu finansial.
Contoh: macro-regime "siklus bull" mengandung di dalamnya micro-regime "rally," "koreksi," dan "konsolidasi." Ini mencegah kepanikan di setiap pullback 10% di pasar bull — model memahami bahwa koreksi dalam siklus bull adalah normal.
HMM Multivariat dengan Fitur yang Diperluas
Alih-alih return univariat, kita memasukkan vektor fitur: return + volatilitas + volume + data on-chain. Ini memungkinkan model "melihat" lebih banyak informasi tentang state pasar.
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 Ensemble
Pendekatan modern: gunakan HMM bukan sebagai sistem trading, tetapi sebagai generator fitur untuk model downstream. Ide ini, yang dijelaskan dalam Gupta et al. (2025) "A forest of opinions: A multi-model ensemble-HMM voting framework for market regime shift detection and trading":
- HMM menentukan regime saat ini (atau probabilitas regime)
- Regime dimasukkan sebagai fitur tambahan ke Random Forest / Gradient Boosting
- Model ML membuat keputusan trading spesifik dengan mempertimbangkan regime
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}")
Produksi: Jebakan-Jebakan
Backtest yang indah hanya setengah dari pertempuran. Dalam produksi, beberapa kejutan tidak menyenangkan menunggu.
Masalah Lag (Look-Ahead Bias)
HMM menentukan regime berdasarkan data saat ini dan masa lalu, tetapi dalam backtest ada godaan untuk melatih model pada seluruh dataset, termasuk data masa depan. Ini adalah look-ahead bias, dan ini mengubah backtest menjadi fiksi.
Solusi: Pendekatan Walk-Forward. Latih model pada data hingga waktu , prediksi regime pada waktu , kemudian geser window. Persis seperti yang dijelaskan dalam artikel kami tentang Walk-Forward Optimization.
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
Jadwal Pelatihan Ulang
Seberapa sering Anda harus melatih ulang model? Terlalu jarang — model menjadi usang, pasar berubah. Terlalu sering — model menjadi tidak stabil, regime "melompat."
Rekomendasi empiris:
- Untuk data harian: latih ulang setiap 1-4 minggu (21 hari trading adalah default yang baik)
- Window pelatihan: 6-12 bulan (252 hari trading — satu tahun)
- Monitoring: jika log-likelihood pada data baru turun di bawah ambang batas — pelatihan ulang tidak terjadwal
Ketidakstabilan Label
Dengan setiap pelatihan ulang, penomoran state dapat berubah: apa yang sebelumnya "regime 0" (bull) dapat menjadi "regime 2." Anda perlu mencocokkan state secara otomatis berdasarkan statistiknya (mean return, volatilitas).
Pembaruan Online
Untuk trading real-time, pelatihan ulang penuh harian berlebihan. Anda dapat menggunakan filtering Forward: tetapkan parameter model, tetapi perbarui probabilitas state posterior dengan setiap observasi baru. Ini adalah operasi instan.
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
Memilih Jumlah State
Meskipun tiga regime adalah default yang baik, alternatif harus diuji:
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)
Keterbatasan dan Peringatan
Tidak jujur untuk diam tentang masalah-masalah ini.
Asumsi Gaussian. GaussianHMM dasar mengasumsikan bahwa return dalam setiap regime terdistribusi normal. Distribusi nyata memiliki ekor tebal dan asimetri. Solusi parsial adalah menggunakan distribusi Student-t atau GMMHMM (Gaussian Mixture per state).
Jumlah state adalah pilihan Anda. BIC membantu, tetapi tidak selalu konklusif. Dua peneliti yang berbeda dapat sampai pada jumlah regime yang berbeda dan keduanya "benar."
Periode transisional. Model tidak pasti selama pergantian regime. Probabilitas terdistribusi hampir sama rata, dan strategi menerima sinyal yang "kabur." Solusinya adalah aturan ambang batas: beralih strategi hanya ketika probabilitas regime baru melebihi 70-80%.
Overfitting. Seperti model apa pun, HMM dapat overfit. Terutama dengan jumlah state atau fitur yang besar. Validasi Walk-Forward adalah wajib.
Masalah khusus crypto. Pasar cryptocurrency masih muda dan tidak stabil secara struktural. "Pasar bull" 2017 dan "pasar bull" 2024 secara statistik adalah fenomena yang berbeda. Model mungkin tidak tergeneralisasi lintas siklus.
Bacaan Lebih Lanjut
Bagi yang ingin mendalami lebih jauh:
Karya fondational:
- Hamilton, J.D. (1989). A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle. Econometrica, 57(2), 357-384. — Karya fondational tentang model Markov-switching
- Guidolin, M., & Timmermann, A. (2007). Asset Allocation under Multivariate Regime Switching. Journal of Economic Dynamics and Control, 31(11), 3503-3544. — Aplikasi praktis untuk alokasi aset
- Ang, A., & Bekaert, G. (2002). Regime Switches in Interest Rates. Journal of Business & Economic Statistics, 20(2), 163-182. — Regime dalam suku bunga
Penelitian modern:
- 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. — Pendekatan ensemble untuk deteksi regime
- Oelschlager, L., Adam, T., & Michels, R. (2024). fHMM: Hidden Markov Models for Financial Time Series in R. Journal of Statistical Software. — HMM hierarkis untuk keuangan
- Bitcoin Price Regime Shifts: A Bayesian MCMC and Hidden Markov Model Analysis of Macroeconomic Influence. Mathematics, 2025. — HMM untuk Bitcoin dengan pendekatan Bayesian
Panduan praktis:
- 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
Kesimpulan
Hidden Markov Models bukan peluru perak, melainkan sebuah alat. Alat yang berguna, berdasar secara matematis, dengan setengah abad sejarah dalam statistik dan tiga dekade dalam keuangan.
Nilai utama HMM untuk trading bukan bahwa ia "memprediksi pasar" (tidak ada yang bisa), tetapi bahwa ia memformalkan intuisi trader berpengalaman: pasar melewati fase-fase yang berbeda, dan strategi harus beradaptasi. Alih-alih "saya merasa pasar sedang bearish sekarang" yang subjektif, Anda mendapatkan "probabilitas regime bear adalah 82%, rata-rata durasi siklus bear adalah 16 hari, kita berada di hari ke-5."
Haruskah Anda mengintegrasikan HMM ke dalam stack trading Anda? Jika Anda memiliki beberapa strategi untuk kondisi pasar yang berbeda dan Anda lelah beralih secara manual — tentu saja ya. Jika Anda trading satu strategi dan tidak berencana untuk berkembang — simpan dulu, tapi ingatlah.
Dan ingat: model terbaik adalah yang bekerja dalam produksi, bukan yang menang dalam backtest.
Kutipan: Jika Anda menggunakan materi dari artikel ini dalam penelitian atau proyek Anda, harap kutip:
Hidden Markov Models dalam Trading: Cara Mengadaptasi Strategi ke Regime Pasar. marketmaker.cc, 2026. URL: https://marketmaker.cc/id/blog/post/regime-detection-hmm-adaptive-trading
Penulis
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.