BIST'te Anomali Tespiti: Isolation Forest ile Anormal Fiyat Hareketleri
Makine öğrenmesiyle BIST hisselerinde anormal fiyat ve hacim hareketlerini tespit ediyoruz. Isolation Forest algoritmasını sıfırdan kuruyor, ClarQuant'ta bu sinyali nasıl kullandığımızı anlatıyoruz.
ClarQuant'ta bir noktada şunu fark ettim: bazı hareketler hiçbir teknik indikatör tarafından önceden yakalanmıyordu. Ne RSI, ne MACD, ne Bollinger. Fiyat aniden %8 açılıyor ya da hacim birkaç katına çıkıyor — ama sinyal yok. O zaman soruyu değiştirdim: "Bu hareket ne zaman geliyor?" yerine "Bu hareket normalin ne kadar dışında?" diye sordum. Isolation Forest bu soruyu yanıtlamanın en temiz yollarından biri.
Isolation Forest Nedir?
Isolation Forest gözetimsiz (unsupervised) bir anomali tespit algoritmasıdır. Diğer anomali yöntemlerinin aksine "normal veriyi profillemez" — doğrudan anomaliyi izole etmeye çalışır.
Mantığı şu: anormal bir veri noktası az sayıda rastgele bölünmeyle kolayca izole edilir. Normal bir nokta ise çok sayıda bölünme gerektirir.
Finansal veri için neden uygun?
- Etiketleme gerektirmez — hangi günün "anomali" olduğunu önceden bilmeniz şart değil
- Çok boyutlu: fiyat, hacim, volatilite, getiri gibi birden fazla özelliği aynı anda analiz eder
- Hızlı ve ölçeklenebilir — binlerce günlük veri saniyeler içinde işlenir
- BIST gibi gelişmekte olan piyasalarda sık görülen "beklenmedik sıçramalar" bu algoritma için biçilmiş kaftan
Adım 1: Kurulum ve Veri Hazırlığı
!pip install yfinance --quiet
import yfinance as yf
import pandas as pd
import numpy as np
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
import plotly.graph_objects as go
from plotly.subplots import make_subplots
ticker = "THYAO.IS"
df = yf.download(ticker, start="2021-01-01", end="2025-01-01", progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.droplevel(1)
df = df[df["Volume"] > 0].ffill().dropna()
df.index = pd.to_datetime(df.index)
print(f"Veri hazır: {len(df)} gün | {df.index[0].date()} → {df.index[-1].date()}")
Adım 2: Özellik Mühendisliği — Feature Engineering
Ham fiyat verisini doğrudan modele vermek yerine, anormalliği yansıtan türetilmiş özellikler oluşturuyoruz.
def ozellik_uret(df):
feat = pd.DataFrame(index=df.index)
# Getiri özellikleri
feat["getiri"] = df["Close"].pct_change()
feat["getiri_abs"] = feat["getiri"].abs()
feat["gap"] = (df["Open"] - df["Close"].shift(1)) / df["Close"].shift(1)
# Volatilite özellikleri
feat["gunluk_aralik"] = (df["High"] - df["Low"]) / df["Close"]
feat["ust_golge"] = (df["High"] - df[["Open","Close"]].max(axis=1)) / df["Close"]
feat["alt_golge"] = (df[["Open","Close"]].min(axis=1) - df["Low"]) / df["Close"]
# Hacim özellikleri
feat["hacim_oran"] = df["Volume"] / df["Volume"].rolling(20).mean()
feat["hacim_getiri"] = feat["hacim_oran"] * feat["getiri_abs"]
# Momentum özellikleri
feat["rsi_sapma"] = df["Close"].pct_change(5)
feat["vol_sapma"] = feat["gunluk_aralik"] / feat["gunluk_aralik"].rolling(20).mean()
return feat.dropna()
ozellikler = ozellik_uret(df)
print(f"\nÖzellik sayısı: {ozellikler.shape[1]}")
print(f"Veri noktası : {ozellikler.shape[0]}")
Adım 3: Model Kurma ve Anomali Tespiti
def isolation_forest_uygula(ozellikler, contamination=0.04, n_estimators=200):
scaler = StandardScaler()
X = scaler.fit_transform(ozellikler)
model = IsolationForest(
n_estimators=n_estimators,
contamination=contamination,
random_state=42,
n_jobs=-1
)
model.fit(X)
skorlar = model.score_samples(X)
etiketler = model.predict(X)
return skorlar, etiketler, scaler, model
skorlar, etiketler, scaler, model = isolation_forest_uygula(ozellikler)
sonuclar = ozellikler.copy()
sonuclar["anomali_skor"] = skorlar
sonuclar["anomali"] = etiketler
sonuclar["fiyat"] = df.loc[sonuclar.index, "Close"]
sonuclar["hacim"] = df.loc[sonuclar.index, "Volume"]
n_anomali = (sonuclar["anomali"] == -1).sum()
print(f"\nToplam gün : {len(sonuclar)}")
print(f"Anomali tespit: {n_anomali} gün (%{n_anomali/len(sonuclar)*100:.1f})")
print(f"\nEn anormal 10 gün:")
en_anormal = sonuclar.nsmallest(10, "anomali_skor")[
["fiyat", "getiri", "hacim_oran", "gap", "anomali_skor"]
].round(4)
print(en_anormal.to_string())
Adım 4: Anomalileri Görselleştirme
anomaliler = sonuclar[sonuclar["anomali"] == -1].copy()
boyut = (anomaliler["anomali_skor"].min() - anomaliler["anomali_skor"]) * 50 + 8
renk = ["#43a047" if g > 0 else "#e53935" for g in anomaliler["getiri"]]
fig = make_subplots(
rows=3, cols=1, shared_xaxes=True,
row_heights=[3, 1, 1], vertical_spacing=0.03,
subplot_titles=("Fiyat + Anomaliler", "Hacim", "Anomali Skoru")
)
fig.add_trace(go.Scatter(
x=sonuclar.index, y=sonuclar["fiyat"],
mode="lines", name="Kapanış",
line=dict(color="#1565c0", width=1)
), row=1, col=1)
fig.add_trace(go.Scatter(
x=anomaliler.index, y=anomaliler["fiyat"],
mode="markers", name="Anomali",
marker=dict(
color=renk, size=boyut.clip(8, 25),
symbol="circle",
line=dict(color="black", width=0.5), opacity=0.85
)
), row=1, col=1)
hacim_renk = ["#e53935" if e == -1 else "#90a4ae" for e in sonuclar["anomali"]]
fig.add_trace(go.Bar(
x=sonuclar.index, y=sonuclar["hacim"],
marker_color=hacim_renk, opacity=0.7, name="Hacim"
), row=2, col=1)
fig.add_trace(go.Scatter(
x=sonuclar.index, y=sonuclar["anomali_skor"],
mode="lines", name="Anomali Skoru",
line=dict(color="#7b1fa2", width=0.8),
fill="tozeroy", fillcolor="rgba(123,31,162,0.05)"
), row=3, col=1)
esik = sonuclar[sonuclar["anomali"] == -1]["anomali_skor"].max()
fig.add_hline(y=esik, line_dash="dash", line_color="red", line_width=1, row=3, col=1)
fig.update_layout(
title=f"{ticker} — Isolation Forest Anomali Tespiti (2021-2025)",
height=700, plot_bgcolor="#fff", paper_bgcolor="#f8f9fa",
hovermode="x unified"
)
fig.show()
Adım 5: Anomali Sonrası Fiyat Davranışı
def anomali_sonrasi_getiri(sonuclar, df, ileriki_gunler=[1, 3, 5, 10]):
anomali_idx = sonuclar[sonuclar["anomali"] == -1].index
fiyat = df.loc[sonuclar.index, "Close"]
pozitif_anomali = sonuclar[
(sonuclar["anomali"] == -1) & (sonuclar["getiri"] > 0)
].index
negatif_anomali = sonuclar[
(sonuclar["anomali"] == -1) & (sonuclar["getiri"] < 0)
].index
print("Anomali Sonrası Ortalama Getiri:")
for gun in ileriki_gunler:
tum_g = []
for tarih in anomali_idx:
try:
iloc_pos = fiyat.index.get_loc(tarih)
if iloc_pos + gun < len(fiyat):
gelecek = fiyat.iloc[iloc_pos + gun] / fiyat.iloc[iloc_pos] - 1
tum_g.append(gelecek)
except:
pass
t = np.mean(tum_g)*100 if tum_g else 0
print(f" +{gun} gün: %{t:.2f}")
anomali_sonrasi_getiri(sonuclar, df)
Adım 6: Contamination Parametre Duyarlılığı
print("Contamination Parametre Analizi:")
for cont in [0.01, 0.02, 0.03, 0.05, 0.07, 0.10]:
s, e, _, _ = isolation_forest_uygula(ozellikler, contamination=cont)
n_an = (e == -1).sum()
print(f" Contamination: {cont:.2f} → {n_an} anomali (%{n_an/len(e)*100:.1f})")
Sonuç
- Isolation Forest etiket gerektirmeyen yapısıyla BIST gibi "ne olacağı belirsiz" piyasalar için güçlü bir araç.
- Feature engineering bu modelde her şey — hangi özellikleri seçtiğiniz modelin neyi "anormal" sayacağını belirliyor.
- Bu analizin sınırı: model geçmişe bakıyor. Yapısal değişiklikler sonrasında model yeniden eğitilmeli.
- Bir sonraki adım: bu anomali sinyallerini strateji filtresi olarak kullanmak.
Bu yazıdaki kodlar eğitim amaçlıdır; yatırım tavsiyesi değildir.