Strateji05.04.2026schedule4 min read

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.

Makine Öğrenmesi ve Anomali Tespiti

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]}")

Veri Analizi ve Feature Engineering

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.