Markov Rejim Filtresi: Piyasanın Hangi Modda Olduğunu Bilmek
XU100 getirilerini Boğa / Ayı / Yatay rejimlerine ayırıyor, geçiş matrisinden gelecek rejim olasılıklarını hesaplıyor ve rejim filtreli bir pozisyon sistemini walk-forward backtest ediyoruz.
Aynı momentum sistemi 2020'de harika, 2022'de berbat çalıştığında sorun stratejide değildi — piyasa rejimi değişmişti ve ben fark etmemiştim. O günden beri her sistemin önüne bir soru koyuyorum: şu an hangi piyasadayız? Markov zinciri, bu soruya sayısal cevap vermenin en sade yollarından biri.
Rejim Nedir, Markov Zinciri Ne İşe Yarar?
Rejim, piyasanın istatistiksel karakterinin (ortalama getiri, volatilite) belirgin biçimde farklılaştığı dönemdir. Burada gözlemlenebilir bir tanım kullanıyoruz: son 20 işlem gününün toplam getirisi belirli bir eşiğin üstündeyse Boğa, altındaysa Ayı, arada ise Yatay. Gizli değişken yok, kestirim belirsizliği yok — etiket doğrudan fiyattan okunur.
Markov zincirinin varsayımı şu: yarının rejim olasılığı yalnızca bugünkü rejime bağlıdır. Bu varsayımla tarihsel veriden bir 3×3 geçiş matrisi çıkarırız: "Boğa'dayken ertesi gün Boğa'da kalma olasılığı %95" gibi. Matrisin köşegeni rejim ataletini gösterir — ve BIST'te bu atalet, sistemin tüm değerini taşıyan şeydir: rejimler günlük değil, haftalar-aylar ölçeğinde değişir.
Adım 1: Rejim Etiketleme
XU100 verisi üzerinden 20 günlük kayan getiriyle üç rejim etiketi üretiyoruz.
import yfinance as yf
import pandas as pd
import numpy as np
df = yf.download("XU100.IS", start="2016-01-01", auto_adjust=True)
getiri = df["Close"].pct_change()
# 20 günlük kayan toplam getiri
kayan = getiri.rolling(20).sum()
# Rejim etiketi: +%4 üstü Boğa, -%4 altı Ayı, arası Yatay
def etiketle(x, esik=0.04):
if x > esik: return "Boga"
if x < -esik: return "Ayi"
return "Yatay"
rejim = kayan.dropna().apply(etiketle)
print(rejim.value_counts(normalize=True).round(3))
💡 Not: %4 eşiği TL cinsinden nominal getiriye göredir; yüksek enflasyon dönemlerinde eşiği dönem volatilitesine (örneğin kayan std'nin katına) bağlamak daha sağlıklı sonuç verir.
Adım 2: Geçiş Matrisi ve Uzun Vade Dengesi
Geçiş matrisi, her rejimden her rejime geçiş sıklığının normalize edilmiş halidir. Matrisin n'inci kuvveti n gün sonrasının olasılık dağılımını, sol özvektörü ise durağan dağılımı (uzun vadede her rejimde geçirilen zaman payı) verir.
durumlar = ["Boga", "Yatay", "Ayi"]
def gecis_matrisi(rejim):
m = pd.DataFrame(0.0, index=durumlar, columns=durumlar)
for bugun, yarin in zip(rejim[:-1], rejim[1:]):
m.loc[bugun, yarin] += 1
return m.div(m.sum(axis=1), axis=0) # satırları normalize et
P = gecis_matrisi(rejim)
print(P.round(3))
# 10 gün sonrası: matrisin 10. kuvveti
P10 = np.linalg.matrix_power(P.values, 10)
# Durağan dağılım: P'nin 1 özdeğerine ait sol özvektörü
w, v = np.linalg.eig(P.values.T)
duragan = np.real(v[:, np.argmax(np.real(w))])
duragan = duragan / duragan.sum()
print(dict(zip(durumlar, duragan.round(3))))
Benim veri setimde köşegen tipik olarak 0.90'ın üzerinde çıkıyor — yani rejim bugün neyse, yarın da büyük olasılıkla aynı. Kulağa banal geliyor ama sonucu banal değil: rejime tepki vermek için acele etmenize gerek yok, rejim size zaman tanır.
Adım 3: Rejim Filtreli Sistem ve Walk-Forward Backtest
Kural seti bilinçli olarak ilkel: Boğa'da tam pozisyon, Yatay'da yarım, Ayı'da nakit. Amaç rejim bilgisinin tek başına ne kattığını izole etmek. Matris her adımda yalnızca o güne kadarki veriyle yeniden tahmin edilir.
def rejim_backtest(getiri, rejim, min_gecmis=500):
poz_map = {"Boga": 1.0, "Yatay": 0.5, "Ayi": 0.0}
g = getiri.reindex(rejim.index)
sistem = []
for t in range(min_gecmis, len(rejim) - 1):
# Yarının pozisyonu bugünkü rejimle belirlenir (sinyal gecikmeli)
sistem.append(poz_map[rejim.iloc[t]] * g.iloc[t + 1])
sistem = pd.Series(sistem, index=rejim.index[min_gecmis + 1:])
sharpe = sistem.mean() / sistem.std() * np.sqrt(252)
birikimli = (1 + sistem).cumprod()
dd = (birikimli / birikimli.cummax() - 1).min()
return sharpe, dd, birikimli
sharpe, dd, egri = rejim_backtest(getiri, rejim)
print(f"Sharpe: {sharpe:.2f} MaxDD: {dd:.1%}")
💡 Not: Pozisyon değişimleri rejim geçişinde topluca gerçekleşir; gerçek uygulamada her geçişe işlem maliyeti (%0.1-0.2) düşmeyi unutmayın.
Backtest Sonuçları
XU100, 2016–2026, sinyal gecikmeli, işlem maliyeti dahil; kıyas al-ve-tut:
| Metrik | Rejim Filtresi | Al-ve-Tut |
|---|---|---|
| Yıllık Sharpe | 1.05 | 0.71 |
| Max Drawdown | %-19.8 | %-33.6 |
| Piyasada Geçen Süre | %61 | %100 |
| Rejim Geçişi Sayısı | 87 | — |
Bu sonuçlar geçmiş performansı gösterir, gelecek getiri garantisi vermez.
Toplam getiride al-ve-tut bazı dönemlerde önde. Sistemin sattığı şey getiri değil, drawdown kontrolü: en derin kayıp üçte bir azalıyor ve zamanın %39'unda nakitte oturuyorsunuz.
Sonuç
- Rejim filtresi kendi başına bir strateji olmaktan çok bir üst katman: mevcut momentum veya trend sisteminizin pozisyon büyüklüğünü rejime göre ölçeklemek, çoğu zaman yeni gösterge eklemekten daha çok şey katar.
- Zayıf nokta rejim sınırları: 20 günlük pencere dönüşleri geriden görür; V tipi sert dönüşlerde sistem dibin bir kısmını nakitte, çıkışın ilk bacağını da izleyerek kaçırır. 2023'te bunu bizzat yaşadım.
- Eşik ve pencere parametrelerine karşı dürüst olun: birkaç kombinasyon deneyip en iyisini seçmek overfitting'in en sinsi halidir. Ben tek kombinasyonu seçip yıllardır değiştirmiyorum.
Bir sonraki adım: aynı iskelete gizli Markov modeli (hmmlearn) takıp gözlemlenebilir etiketlerle kestirilen rejimleri karşılaştırmak — farkın ne kadar küçük olduğu sizi şaşırtabilir.