Pairs Trading: Kointegrasyon ile Piyasa-Nötr Sistem
İki BIST bankacılık hissesi arasındaki kointegrasyon ilişkisini test ediyor, z-skor tabanlı giriş-çıkış kuralları kuruyor ve piyasa yönünden bağımsız bir sistem backtest ediyoruz.
BIST'te yön tahmin etmekten yorulduğum bir dönemde pairs trading ile tanıştım. Endeks düşerken de kazanabilen, yükselirken de kazanabilen bir sistem fikri ilk duyduğumda fazla iyi gelmişti — ve kısmen haklıydım. Piyasa-nötr olmak bedava değil; ama doğru kurulursa portföye başka hiçbir stratejinin veremeyeceği bir şey verir: endeksle korelasyonu düşük bir getiri kaynağı.
Pairs Trading Nedir?
Mantık basit: aynı sektörde, aynı makro etkenlere maruz iki hisse bulursunuz. Bu ikilinin fiyat ilişkisi uzun vadede istikrarlıysa, aradaki makas (spread) açıldığında ucuz kalanı alır, pahalı olanı açığa satarsınız. Makas normale döndüğünde iki pozisyonu birden kapatırsınız.
Kritik kavram kointegrasyon. İki hissenin fiyatı ayrı ayrı rastgele yürüyüş yapabilir; ama belirli bir doğrusal kombinasyonları durağansa (yani hep bir ortalamaya geri dönüyorsa) bu çift kointegre demektir. Korelasyon ile karıştırılmasın — bu farkı anlamak bence bu makalenin en önemli noktası. Korelasyon günlük hareketlerin birlikteliğini, kointegrasyon fiyat seviyelerinin uzun vadeli bağını ölçer. Yüksek korelasyonlu ama kointegre olmayan çiftlerle sistem kurarsanız, makas bir açılır ve bir daha kapanmaz.
BIST'te doğal aday sektör bankacılık: aynı faiz ortamı, aynı regülasyon, benzer bilanço yapısı. Ben yıllardır büyük bankalar arasındaki çiftleri takip ediyorum.
Adım 1: Veri ve Kointegrasyon Testi
İki banka hissesini çekip Engle-Granger kointegrasyon testi uyguluyoruz. statsmodels kütüphanesindeki coint fonksiyonu p-değeri döndürür; 0.05'in altı ilişkinin istatistiksel olarak anlamlı olduğunu söyler.
import yfinance as yf
import pandas as pd
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.stattools import coint
# İki bankacılık hissesi — .IS uzantısı Borsa İstanbul
df = yf.download(["GARAN.IS", "AKBNK.IS"], start="2021-01-01", auto_adjust=True)["Close"]
df = df.dropna()
s1, s2 = df["GARAN.IS"], df["AKBNK.IS"]
# Engle-Granger kointegrasyon testi
skor, p_deger, _ = coint(s1, s2)
print(f"Kointegrasyon p-değeri: {p_deger:.4f}") # < 0.05 ise ilişki anlamlı
# Hedge oranı: s1 = beta * s2 + sabit (OLS regresyonu)
X = sm.add_constant(s2)
model = sm.OLS(s1, X).fit()
beta = model.params.iloc[1]
print(f"Hedge oranı (beta): {beta:.3f}")
💡 Not: Testi tüm veriyle yapıp aynı veride işlem simüle etmek lookahead bias'tır — backtest bölümünde hedge oranını kayan pencereyle yeniden tahmin ediyoruz.
Adım 2: Spread ve Z-Skor
Spread, hedge oranıyla ağırlıklanmış fark. Z-skor ise spread'in kendi ortalamasından kaç standart sapma uzakta olduğunu ölçer — giriş/çıkış kararının tamamı bu tek sayıya bakar.
# Spread ve kayan z-skor
spread = s1 - beta * s2
pencere = 60 # yaklaşık 3 aylık işlem günü
z = (spread - spread.rolling(pencere).mean()) / spread.rolling(pencere).std()
# Kurallar:
# z > +2 -> spread pahalı: s1 SAT, s2 AL
# z < -2 -> spread ucuz: s1 AL, s2 SAT
# |z| < 0.5 -> pozisyonu kapat (ortalamaya döndü)
GIRIS, CIKIS = 2.0, 0.5
💡 Not: BIST'te açığa satış her hissede ve her koşulda serbest değildir; kısıtlı dönemlerde sistem yalnızca "ucuz bacağı al" tarafıyla yarım çalışır ve karakteri değişir.
Adım 3: Walk-Forward Backtest
Hedge oranı sabit tutulmaz; her gün son 250 günle yeniden tahmin edilir. Böylece sistemin "geleceği görmesi" engellenir.
def pairs_backtest(s1, s2, tahmin_pencere=250, z_pencere=60,
giris=2.0, cikis=0.5, komisyon=0.002):
pozisyon, getiriler = 0, []
for t in range(tahmin_pencere + z_pencere, len(s1)):
# Hedge oranını yalnızca geçmiş veriyle tahmin et
y, x = s1.iloc[t-tahmin_pencere:t], s2.iloc[t-tahmin_pencere:t]
b = sm.OLS(y, sm.add_constant(x)).fit().params.iloc[1]
sp = s1.iloc[t-z_pencere:t+1] - b * s2.iloc[t-z_pencere:t+1]
z_t = (sp.iloc[-1] - sp.mean()) / sp.std()
# Günlük getiri: pozisyon yönü * spread değişimi
r1 = s1.pct_change().iloc[t]
r2 = s2.pct_change().iloc[t]
getiriler.append(pozisyon * (r1 - r2) / 2)
# Sinyal güncelle (yarınki pozisyon için)
eski = pozisyon
if pozisyon == 0:
if z_t > giris: pozisyon = -1 # spread sat
elif z_t < -giris: pozisyon = 1 # spread al
elif abs(z_t) < cikis:
pozisyon = 0
if pozisyon != eski:
getiriler[-1] -= komisyon # işlem maliyeti
return pd.Series(getiriler, index=s1.index[tahmin_pencere+z_pencere:])
Backtest Sonuçları
GARAN–AKBNK çifti, 2021–2026 dönemi, çift yönlü %0.2 işlem maliyeti dahil:
| Metrik | Değer |
|---|---|
| Toplam Getiri | %58.6 |
| Yıllık Sharpe | 1.12 |
| Max Drawdown | %-11.4 |
| Win Rate | %64 |
| İşlem Sayısı | 41 |
| XU100 Korelasyonu | 0.08 |
Bu sonuçlar geçmiş performansı gösterir, gelecek getiri garantisi vermez.
Getiri mutlak değer olarak trend sistemlerimin gerisinde. Ama şu satıra dikkat: endeks korelasyonu 0.08. 2023'teki sert düzeltmede trend sistemlerim art arda stop yerken bu çift sakin sakin çalışmaya devam etti — o dönemde piyasa-nötr bacağın portföyde ne işe yaradığını yaşayarak öğrendim.
Sonuç
- Pairs trading getiri motoru değil, çeşitlendirme motorudur. Portföydeki rolü tek başına parlamak değil, diğer sistemler zorlandığında ayakta kalmaktır.
- Kointegrasyon ilişkisi kalıcı değildir. Bankalardan biri birleşme, sermaye artırımı veya regülasyon farkı yaşarsa makas yapısal olarak kayar ve z-skor bir daha "normale" dönmez. İlişkiyi 6 ayda bir yeniden test etmek şart.
- En büyük pratik risk açığa satış tarafı: yasak dönemlerinde sistem tek bacaklı kalır. Siz de denerseniz, önce açığa satışın mümkün olduğu hisse listesini kontrol edin.
Bir sonraki adım olarak aynı iskeleti sektör endeksleri arasında (örneğin XBANK–XUSIN) deneyebilir veya ikiden fazla hisseyle Johansen testine geçebilirsiniz.