← Makalelere geri dön
June 3, 2026
5 dakikalık okuma

Makine Öğrenmesiyle Alış-Satış Spread'i Modelleme ve Tahmini

Makine Öğrenmesiyle Alış-Satış Spread'i Modelleme ve Tahmini
#microstructure
#spread
#market-making
#prediction
#machine-learning
#gradient-boosting
#deep-learning

Alış-satış spread'i, bir piyasa yapıcısının kontrol ettiği en önemli tek değişkendir. Çok geniş ayarlarsanız akışı rakiplerinize kaptırırsınız. Çok dar ayarlarsanız ters seçim envanterinizi diri diri yer. Geleneksel mikroyapı teorisi, spread'i ekonomik bileşenlerine ayrıştıran zarif çözümlemeler sunar. Makine öğrenmesi ise bu bileşenlerin gerçek zamanlı olarak nasıl kaydığını tahmin edecek araçları verir. Bu yazı her iki dünyayı birbirine bağlıyor: klasik teoriyle başlıyoruz, Roll'un örtük spread tahmincisine kadar ilerliyoruz, ardından emir defteri özelliklerinden spread tahmin eden gradyan artırma ve derin öğrenme modellerine geçiyoruz. Bu süreçte, pratikte spread modellerini sessizce geçersiz kılan birim, sızıntı ve kıyaslama tuzaklarına işaret ediyoruz.

Spread'ler Piyasa Yapıcıları İçin Neden Önemli

Bir piyasa yapıcı sürekli olarak bir alış fiyatı PbP_b ve bir satış fiyatı PaP_a kote eder. Kote edilen spread şudur:

S=PaPbS = P_a - P_b

Her gidiş-dönüş (piyasa yapıcının alışından satın alma, piyasa yapıcının satışına satma, ikisi de taker'lar tarafından gerçekleştirilir) taker'lardan piyasa yapıcıya en fazla SS kadar transfer eder — teoride. Pratikte piyasa yapıcı, ters seçim nedeniyle SS'den daha azını kazanır: bazı taker'lar bilgilidir ve fiyat piyasa yapıcının aleyhine hareket etmeden hemen önce işlem yaparlar. Gidiş-dönüş başına gerçekleşen kâr, etkin spread eksi fiyat etkisine eşit olan gerçekleşen spread'dir:

Srealized=SeffectivePriceImpactS_{\text{realized}} = S_{\text{effective}} - \text{PriceImpact}

Üç büyüklüğün de aynı tam-spread temelinde (yarım değil) ölçeriz, böylece özdeşlik boyutsal olarak tutarlı olur. Tek bir işlem için etkin spread şöyledir:

Seffective=2dt(PtMt)S_{\text{effective}} = 2 \cdot d_t \cdot (P_t - M_t)

Burada dt{+1,1}d_t \in \{+1, -1\} işlem yönüdür (taker alışı veya satışı), PtP_t işlem fiyatıdır ve MtM_t işlem anındaki en iyi alış ve satışın orta noktasıdır. Fiyat etkisi terimi, işlem sonrası bir τ\tau ufku üzerinde simetrik olarak tanımlanır:

PriceImpact=2dt(Mt+τMt)\text{PriceImpact} = 2 \cdot d_t \cdot (M_{t+\tau} - M_t)

burada Mt+τM_{t+\tau} işlemden τ\tau ufku sonraki orta noktadır. Ufuk açıkça belirtilmelidir — yaygın seçimler hisse senetlerinde 5 dakika ve fiyatların daha hızlı yeniden fiyatlandığı kriptoda 30 saniyedir. Etkin spread'den etkiyi çıkarmak gerçekleşen spread'i bırakır: piyasa hareket ettikten sonra piyasa yapıcının elinde kalan.

Sonraki 1, 5 veya 60 saniyedeki spread'i — ve bileşenlerini — tahmin edebilen bir piyasa yapıcı, gerçekleşme oranlarını korurken gerçekleşen spread'i en üst düzeye çıkarmak için kotaları dinamik olarak ayarlayabilir.

Spread'in Üç Bileşeni

Alış-satış spread'inin emir işleme, envanter ve ters seçim maliyet katmanlarına ayrıştırılması

Piyasa mikroyapısı literatürü (Stoll 1978, Glosten ve Milgrom 1985, Huang ve Stoll 1997) alış-satış spread'ini üç ekonomik bileşene ayrıştırır.

1. Emir İşleme Maliyeti (α\alpha)

Bu, gerçekleştirilen taraf başına piyasa yapma hizmetini sunmanın maliyetidir: piyasa yapıcının fiilen ödediği ücret artı teknoloji altyapısı, düzenleyici uyum ve konuşlandırılan sermayenin fırsat maliyeti. Demsetz (1968) ve Tinic (1972) bu bileşeni ilk biçimlendirenler oldu.

Temel ayrım hangi ücreti kimin ödediğidir. Pasif olarak kote eden bir piyasa yapıcı, kendi gerçekleşmelerinde maker ücreti fmf_m öder — ve birçok borsada fmf_m bir iadedir, yani negatiftir. Bu pasif gerçekleşmelerde taker ücreti ftf_t'yi ödemez; spread'i geçen karşı taraf ftf_t'yi öder. Yani piyasa yapıcının taraf başına emir işleme maliyeti şudur:

αfm+cinfra\alpha \approx f_m + c_{\text{infra}}

burada fmf_m işaretlidir (bir iade α\alpha'yı düşürür ve negatif yapabilir) ve cinfrac_{\text{infra}} bağlantı, ortak yerleşim ve hesaplamayı kapsar. Modern elektronik piyasalarda bu bileşen dramatik biçimde küçüldü — hisse senetlerinde kuruşun altında, kriptoda birkaç baz puan veya net bir iade.

Taker ücreti ftf_t farklı bir nedenle önemlidir: tam spread'in kârlı biçimde ne kadar dar olabileceğine bir taban koyar, çünkü geçen bir taker spread'in üzerine ftf_t öder. Kotalarınızı bu taker maliyetine göre ekonomik olarak çekici tutan bir spread tabanı istiyorsanız, bunu ftf_t'yi piyasa yapıcının kendi maliyetine katlamak yerine ayrıca gerekçelendirin. İkisini birbirine karıştırmak, tek bir yarım spread içinde bir gidiş-dönüş ücretini çift saymaktır.

2. Envanter Tutma Maliyeti (β\beta)

Bir piyasa yapıcı yönlü bir pozisyon (uzun veya kısa) biriktirdiğinde, fiyat riskine maruz kalır. Envanter bileşeni bu riski telafi eder. Stoll (1978) ile Amihud ve Mendelson (1980) bunu volatilite ve piyasa yapıcının mevcut envanterinin bir fonksiyonu olarak modelledi:

βσQ\beta \propto \sigma \cdot |Q|

burada σ\sigma varlığın volatilitesi ve QQ piyasa yapıcının mevcut envanteridir. Envanter büyüdükçe piyasa yapıcı maruz kaldığı tarafta spread'i genişletir ve diğer tarafta daraltır; bu envanter eğme (inventory skewing) adı verilen bir tekniktir.

3. Ters Seçim Maliyeti (γ\gamma)

Bu en tehlikeli bileşendir. Bilgili tüccarlar — yaklaşan fiyat hareketleri hakkında üstün bilgiye sahip olanlar — sistematik olarak bayatlamış kotaları toplarlar. Ters seçim maliyeti, bilgili karşı taraflara işlem başına beklenen kayba eşittir. Copeland ve Galai (1983) bunu piyasa yapıcının bilgili tüccarlara verdiği bedava bir opsiyonun değeri olarak modelledi. Glosten ve Milgrom (1985) bunu, bir işlem gözlemledikten sonra piyasa yapıcının inançlarındaki Bayesçi revizyon olarak biçimlendirdi:

γ=E[Vtrade]Mt\gamma = E[V | \text{trade}] - M_t

burada VV gerçek temel değerdir. Likit piyasalarda ters seçim, toplam spread'in %30-60'ını oluşturabilir.

Tam Ayrıştırma

Kote edilen yarım spread şöyle yazılabilir:

S2=α+β+γ\frac{S}{2} = \alpha + \beta + \gamma

burada α\alpha, β\beta ve γ\gamma hepsi taraf başına (yarım spread) maliyetler olarak ifade edilir — muhasebeyi tutarlı tutan budur. Huang ve Stoll (1997) bu bileşenleri işlem ve kotasyon verilerinden tahmin etmek için bir ekonometrik yöntem önerdi. Temel kavrayış: emir işleme maliyetleri sabit bir spread tabanı yaratır, envanter maliyetleri pozisyon ve volatiliteyle değişen bir spread yaratır ve ters seçim maliyetleri bilgi asimetrisiyle değişen bir spread yaratır.

Roll'un Örtük Spread Modeli

Roll'un örtük spread modeli: alış ve satış arasında zıplayan işlem fiyatları, negatif seri-kovaryans imzası bırakır

Yüksek frekanslı veriler yaygın olarak mevcut olmadan önce, Richard Roll (1984) yalnızca işlem fiyatlarını kullanarak etkin spread'i tahmin etmek için zarif bir yöntem önerdi. Kavrayışı şuydu: etkin bir piyasada, alış-satış sıçraması, yeni bir bilgi olmasa bile fiyat değişimlerinde negatif seri kovaryansı tetikler.

Model

Temel değer VtV_t'nin rastgele yürüyüş izlediğini varsayalım:

Vt=Vt1+ut,uti.i.d.(0,σu2)V_t = V_{t-1} + u_t, \quad u_t \sim \text{i.i.d.}(0, \sigma_u^2)

Gözlemlenen işlem fiyatı alış ve satış arasında zıplar:

Pt=Vt+S2dtP_t = V_t + \frac{S}{2} \cdot d_t

burada dt{1,+1}d_t \in \{-1, +1\} eşit olasılıkla (yani alışlar ve satışlar eşit derecede olasıdır). Fiyat değişimi şöyledir:

ΔPt=ut+S2(dtdt1)\Delta P_t = u_t + \frac{S}{2}(d_t - d_{t-1})

Birinci dereceden otokovaryansı hesaplayarak:

Cov(ΔPt,ΔPt1)=S24\text{Cov}(\Delta P_t, \Delta P_{t-1}) = -\frac{S^2}{4}

Model fiyat birimleri cinsinden türetilir: SS, getirilerin değil, fiyat değişimlerinin otokovaryansından çıkar. Bu ayrım, en yaygın tek uygulama hatasıdır ve aşağıdaki kodu buna sadık tutuyoruz.

Roll Tahmincisi

SS için çözerek:

S^Roll=2Cov(ΔPt,ΔPt1)\hat{S}_{\text{Roll}} = 2\sqrt{-\text{Cov}(\Delta P_t, \Delta P_{t-1})}

Örnek otokovaryans pozitif olduğunda (gürültü veya momentum nedeniyle pratikte sık gerçekleşir), tahminci tanımsızdır. Yaygın bir düzeltme, tahmini sıfıra ayarlamak veya işaretli kökü kullanmaktır:

S^Roll=2sign(γ^1)γ^1\hat{S}_{\text{Roll}}^{*} = 2 \cdot \text{sign}(-\hat{\gamma}_1) \cdot \sqrt{|\hat{\gamma}_1|}

burada γ^1\hat{\gamma}_1 örnek birinci dereceden otokovaryanstır.

Python'da Uygulama

Tahminci, spread'i fiyat birimleri cinsinden döndürür. Baz puan olarak ifade etmek için bir kez orta fiyata böleriz — çünkü, getiri uzayı tahmincisinin aksine, henüz fiyata bölünmemiştir:

import numpy as np
import pandas as pd

def roll_spread(prices: pd.Series, window: int = 200) -> pd.Series:
    """
    Rolling Roll (1984) spread estimator, in PRICE units.

    The model is P_t = V_t + (S/2) d_t with Cov(ΔP_t, ΔP_{t-1}) = -S^2/4,
    so S is recovered from the autocovariance of price CHANGES (diff),
    not returns (pct_change). Using returns rescales the estimate by the
    price level and is wrong by roughly that factor.

    Parameters
    ----------
    prices : pd.Series
        Transaction prices.
    window : int
        Rolling window size (number of price changes).

    Returns
    -------
    pd.Series
        Estimated spread per window, in price units.
    """
    dprice = prices.diff().dropna()
    autocov = dprice.rolling(window).apply(
        lambda x: np.cov(x[:-1], x[1:])[0, 1], raw=True
    )
    return 2.0 * np.sqrt(np.maximum(-autocov, 0.0))


trades = pd.read_parquet("trades.parquet")
trades["roll_spread"] = roll_spread(trades["price"], window=200)

trades["quoted_spread"] = trades["ask"] - trades["bid"]
trades["midprice"] = 0.5 * (trades["ask"] + trades["bid"])
trades["quoted_spread_bps"] = trades["quoted_spread"] / trades["midprice"] * 1e4
trades["roll_spread_bps"] = trades["roll_spread"] / trades["midprice"] * 1e4

Simüle edilmiş bir seri üzerinde hızlı bir mantık kontrolü — 100 fiyatı civarında bir temel rastgele yürüyüş ve S=0.10S = 0.10 gerçek spread'i ile — fiyat değişimlerinden 0.0999\approx 0.0999 elde eder. Getiri tabanlı varyant, fiyat seviyesi kadar sapmayla 0.001\approx 0.001 döndürür ve ardından bunu tekrar orta fiyata bölerek baz puan elde etmek hatayı katlar. Getiri uzayı tahmincisini tercih ederseniz, modeli log-fiyat uzayında türetin ve orta fiyata ikinci bölmeyi atın; bir konvansiyon seçin ve kodu matematikle eşleştirin.

Roll Modelinin Sınırlamaları

Roll'un modeli şunları varsayar: (1) piyasa etkinliği, (2) bilgi asimetrisinin olmaması, (3) i.i.d. işlem yönü ve (4) sabit spread. Bunların hepsi pratikte ihlal edilir. Harris (1990), gürültülü verilere uygulandığında tahmincinin Jensen eşitsizliği nedeniyle ciddi biçimde yanlı olduğunu gösterdi. Bu sınırlamalara rağmen, Roll tahmincisi hızlı bir temel çizgi olarak yararlı olmaya devam ediyor ve ampirik finans araştırmalarında yaygın olarak kullanılıyor.

Spread Tahmini İçin ML Özellikleri

Bir spread tahmincisini besleyen tasarlanmış mikroyapı özellikleri — volatilite, emir akışı dengesizliği, derinlik, işlem yoğunluğu

Statik modellerin ötesine geçmek için, spread varyasyonunun dinamik itici güçlerini yakalayan özelliklere ihtiyacımız var. İşte temsil ettikleri spread bileşenine göre düzenlenmiş bir özellik taksonomisi.

Emir Defteri Özellikleri (Envanter ve Ters Seçim)

Özellik Formül Temsil Ettiği
Defter dengesizliği BI=Vb1Va1Vb1+Va1\text{BI} = \frac{V_b^1 - V_a^1}{V_b^1 + V_a^1} Yönlü baskı
Ağırlıklı orta fiyat Pw=PaVbVb+Va+PbVaVb+VaP_w = P_a \cdot \frac{V_b}{V_b + V_a} + P_b \cdot \frac{V_a}{V_b + V_a} Kısa vadeli adil değer
Derinlik oranı (seviye 1-5) DR5=i=15Vbii=15Vai\text{DR}_5 = \frac{\sum_{i=1}^{5} V_b^i}{\sum_{i=1}^{5} V_a^i} Çok seviyeli arz/talep
Defter baskısı BP=i=15Vbiwibi=15Vaiwia\text{BP} = \sum_{i=1}^{5} V_b^i\, w_i^b - \sum_{i=1}^{5} V_a^i\, w_i^a Mesafe ağırlıklı baskı
Spread / tik oranı S/tickS / \text{tick} Minimuma göre darlık

Buradaki defter baskısı, orta noktaya mutlak mesafe sönümü kullanır, wi=eλPiMw_i = e^{-\lambda |P_i - M|}, böylece dokunuşun yakınında duran hacim derin hacimden daha fazla sayılır ve her iki taraf da pozitif, azalan bir fonksiyonla ağırlıklandırılır. Bu, hacmi işaretli mesafe PiMP_i - M'e bölmenin yapısal işaret yanlılığından kaçınır (bu, alış tarafında negatif, satış tarafında pozitiftir ve bir seviye orta noktaya yaklaştıkça patlar). λ\lambda'yı tipik defter derinliğinden seçin veya üstel ifadeyi mesafede azalan herhangi bir pozitif ağırlık w(PiM)w(|P_i - M|) ile değiştirin.

İşlem Akışı Özellikleri (Ters Seçim)

Özellik Formül Temsil Ettiği
İşlem dengesizliği TIn=i=1ndivii=1nvi\text{TI}_{n} = \frac{\sum_{i=1}^{n} d_i \cdot v_i}{\sum_{i=1}^{n} v_i} Net bilgili akış
VPIN Hacim senkronize bilgili işlem olasılığı Toksisite
Kyle's lambda ΔM\Delta M'in işaretli hacme regresyonu Birim başına fiyat etkisi
Büyük işlem sıklığı Pencerede >kmedyan> k \cdot \text{medyan} işlemlerin sayısı Kurumsal faaliyet

Volatilite Özellikleri (Envanter Maliyeti)

Özellik Formül Temsil Ettiği
Gerçekleşen volatilite σrv=(ΔlogM)2\sigma_{\text{rv}} = \sqrt{\sum (\Delta \log M)^2} Kısa vadeli risk
Garman-Klass volatilitesi 12(logH/L)2(2ln21)(logC/O)2\frac{1}{2}(\log H/L)^2 - (2\ln 2 - 1)(\log C/O)^2 Aralık tabanlı volatilite
Volatilitenin volatilitesi σrv\sigma_{\text{rv}}'nin yuvarlanan std'si Rejim belirsizliği
Getiri otokorelasyonu ρ1(ΔM)\rho_1(\Delta M) Momentum / ortalamaya dönüş

Piyasa Rejimi Özellikleri

Özellik Açıklama Temsil Ettiği
Günün saati kodlaması sin(2πt/T),cos(2πt/T)\sin(2\pi t / T), \cos(2\pi t / T) Gün içi mevsimsellik
Son işlemden bu yana saniye Zaman boşluğu Faaliyet seviyesi
Varlıklar arası korelasyon Endeks/BTC ile yuvarlanan korelasyon Sistematik risk
Fonlama oranı (kripto) Perp fonlama oranı Kaldıraçlı konumlanma

Spread Tahmini İçin Gradyan Artırma

Gradyan artırmalı ağaçlar (XGBoost, LightGBM, CatBoost) kantitatif finansta tablo verili tahminin beygiridir. Karışık özellik türlerini işler, doğrusal olmayan etkileşimleri yakalar, minimum ön işleme gerektirir ve milyonlarca satır üzerinde hızlı eğitilir — özellik oluşturmanın kendisi vektörleştirildiği sürece (aşağıdaki otokorelasyon notuna bakın).

Problemin Formülasyonu

Spread tahminini bir regresyon görevi olarak çerçeveliyoruz. Hedef, sonraki τ\tau saniyedeki zaman ağırlıklı ortalama kote edilen spread'dir:

yt=1τtt+τS(u)duy_t = \frac{1}{\tau} \int_{t}^{t+\tau} S(u) \, du

Pratikte bunu, sonraki NN anlık görüntüdeki hacim ağırlıklı ortalama spread ile yaklaşık olarak hesaplarız:

yt=i=1NSt+iVt+ii=1NVt+iy_t = \frac{\sum_{i=1}^{N} S_{t+i} \cdot V_{t+i}}{\sum_{i=1}^{N} V_{t+i}}

Bu hedef, NN (veya horizon) uzunluğunda bir ileri penceredir, bu da bitişik satırların örtüşen gelecek pencereleri paylaştığı anlamına gelir. Bu örtüşme, naif bir eğitim/doğrulama bölmesi boyunca bilgi sızdırır — aşağıdaki eğitim kodunda bunu açıkça ele alıyoruz.

Tam Boru Hattı

import lightgbm as lgb
import numpy as np
import pandas as pd
from sklearn.metrics import mean_absolute_error, r2_score

def build_features(df: pd.DataFrame) -> pd.DataFrame:
    """Build spread-prediction features from L2 order book snapshots."""
    f = pd.DataFrame(index=df.index)

    f["imb1"] = (df["bid_vol_1"] - df["ask_vol_1"]) / (
        df["bid_vol_1"] + df["ask_vol_1"] + 1e-9
    )

    bid_depth = df[[f"bid_vol_{i}" for i in range(1, 6)]].sum(axis=1)
    ask_depth = df[[f"ask_vol_{i}" for i in range(1, 6)]].sum(axis=1)
    f["depth_imb5"] = (bid_depth - ask_depth) / (bid_depth + ask_depth + 1e-9)

    mid = 0.5 * (df["ask_1"] + df["bid_1"])
    f["spread_bps"] = (df["ask_1"] - df["bid_1"]) / mid * 1e4
    f["log_spread"] = np.log1p(df["ask_1"] - df["bid_1"])

    log_ret = np.log(mid / mid.shift(1))
    f["rvol_50"] = log_ret.rolling(50).std()
    f["rvol_200"] = log_ret.rolling(200).std()

    if "trade_sign" in df.columns and "trade_vol" in df.columns:
        signed_vol = df["trade_sign"] * df["trade_vol"]
        total_vol = df["trade_vol"].rolling(50).sum()
        f["tfi_50"] = signed_vol.rolling(50).sum() / (total_vol + 1e-9)

    lag1 = log_ret.shift(1)
    f["ret_autocorr"] = log_ret.rolling(100).corr(lag1)

    if isinstance(df.index, pd.DatetimeIndex):
        seconds = df.index.hour * 3600 + df.index.minute * 60 + df.index.second
        f["tod_sin"] = np.sin(2 * np.pi * seconds / 86400)
        f["tod_cos"] = np.cos(2 * np.pi * seconds / 86400)

    for lag in [1, 5, 10, 50]:
        f[f"spread_lag_{lag}"] = f["spread_bps"].shift(lag)

    return f.dropna()


def build_target(df: pd.DataFrame, horizon: int = 10) -> pd.Series:
    """Forward mean spread over the next `horizon` snapshots (in bps).

    target[t] = mean(spread_bps[t+1 .. t+horizon]). Note that consecutive
    targets share an overlapping forward window of length `horizon`, which
    is why the CV below purges a gap of `horizon` rows around each fold.
    """
    mid = 0.5 * (df["ask_1"] + df["bid_1"])
    spread_bps = (df["ask_1"] - df["bid_1"]) / mid * 1e4
    fwd = spread_bps.shift(-1).rolling(horizon).mean().shift(-(horizon - 1))
    return fwd


def purged_walk_forward(n: int, n_splits: int, horizon: int):
    """Expanding-window splits with a purge/embargo gap of `horizon` rows.

    Because each target spans `horizon` future snapshots, rows straddling a
    train/val boundary share overlapping target windows. Dropping a gap of
    `horizon` rows between train and validation removes that leakage
    (Lopez de Prado-style purging). Without it, validation R²/MAE are
    optimistically biased by the target overlap.
    """
    fold_size = n // (n_splits + 1)
    for k in range(1, n_splits + 1):
        train_end = fold_size * k
        val_start = train_end + horizon       # embargo gap
        val_end = val_start + fold_size
        if val_end > n:
            break
        train_idx = np.arange(0, train_end - horizon)   # purge gap
        val_idx = np.arange(val_start, val_end)
        yield train_idx, val_idx


def train_spread_model(features: pd.DataFrame, target: pd.Series, horizon: int = 10):
    """Train LightGBM with purged, embargoed walk-forward validation."""
    common = features.index.intersection(target.dropna().index)
    X = features.loc[common].reset_index(drop=True)
    y = target.loc[common].reset_index(drop=True)

    models, scores = [], []
    params = {
        "objective": "mae",
        "learning_rate": 0.05,
        "num_leaves": 63,
        "min_child_samples": 100,
        "subsample": 0.8,
        "colsample_bytree": 0.8,
        "reg_alpha": 0.1,
        "reg_lambda": 1.0,
        "verbose": -1,
    }

    for fold, (train_idx, val_idx) in enumerate(
        purged_walk_forward(len(X), n_splits=5, horizon=horizon)
    ):
        X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx]
        y_tr, y_val = y.iloc[train_idx], y.iloc[val_idx]

        ds_tr = lgb.Dataset(X_tr, y_tr)
        ds_val = lgb.Dataset(X_val, y_val, reference=ds_tr)

        model = lgb.train(
            params,
            ds_tr,
            num_boost_round=2000,
            valid_sets=[ds_val],
            callbacks=[lgb.early_stopping(50), lgb.log_evaluation(200)],
        )
        preds = model.predict(X_val)
        mae = mean_absolute_error(y_val, preds)
        r2 = r2_score(y_val, preds)
        print(f"Fold {fold}: MAE={mae:.4f} bps, R²={r2:.4f}")
        models.append(model)
        scores.append({"mae": mae, "r2": r2})

    return models[-1], scores

Kritik ayrıntı temizleme/ambargo boşluğudur. İleri-ortalama hedefi, ardışık satırların horizon kadar anlık görüntüye kadar örtüştüğü anlamına gelir, bu nedenle düz bir TimeSeriesSplit, doğrulama satırlarının gelecek pencerelerini eğitim satırlarıyla paylaşmasına izin verir — cevabı sızdırır ve doğrulama R²'sini şişirir. Her kat sınırının her iki tarafında en az horizon kadar satırlık bir boşluk düşürmek (Lopez de Prado tarzı temizlenmiş k-fold) bu yanlılığı ortadan kaldırır. Sızıntı daha çok dizi modelleri için tartışılsa da, bu gradyan artırma boru hattı için de derin öğrenme kadar geçerlidir.

Özellik Önemi Analizi

Ağaç tabanlı modellerin temel avantajlarından biri yorumlanabilirliktir. Eğitimden sonra, hangi özelliklerin spread tahminlerini yönlendirdiğini anlamak için SHAP değerlerini inceleyin:

import shap

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_val)
shap.summary_plot(shap_values, X_val, max_display=15)

Varlık sınıfları genelinde tipik bulgular:

  1. Gecikmeli spread (spread_lag_1\text{spread\_lag\_1}) neredeyse her zaman en önemli özelliktir — spread'ler yüksek derecede otokorelelidir. Başlıktaki R²'nin yüksek görünmesinin nedeni de budur: skorun çoğu sadece kalıcılıktır, bu yüzden her zaman bir AR/EWMA temel çizgisine karşı kıyaslayın (bunun hakkında daha fazlası aşağıda).
  2. Gerçekleşen volatilite ikinci en önemlisidir — gün içi volatilite ve spread'ler hem eşzamanlı hem de dinamik olarak güçlü biçimde pozitif korelelidir.
  3. Defter dengesizliği en çok volatil dönemlerde önemlidir — yaklaşan yönlü hareketlere işaret eder.
  4. İşlem akışı dengesizliği kısa vadeli ters seçimi yakalar — tek taraflı bir akış patlaması spread genişlemesini öngörür.
  5. Günün saati, U-şeklindeki gün içi deseni yakalar (açılış/kapanışta daha geniş, gün ortasında daha dar).

Hiperparametre Hususları

Özellikle spread tahmini için:

  • MSE yerine MAE veya Huber kaybı kullanın. Spread dağılımları, ara sıra aşırı uç değerlerle (haber olayları sırasında) sağa çarpıktır. MAE daha sağlamdır.
  • Modelin tek tek anlık görüntülerdeki mikroyapı gürültüsüne uymasını önlemek için min_child_samples değerini yüksek (100+) ayarlayın.
  • Ağaçları korelasyonsuzlaştırmak ve farklı volatilite rejimleri genelinde genellemeyi iyileştirmek için subsample < 1.0 kullanın.

Derin Öğrenme Yaklaşımları

Gradyan artırma, tablo verili özelliklerde mükemmel olsa da, derin öğrenme temsilleri doğrudan ham emir defteri verisinden öğrenebilir. İki mimari, spread ile ilgili tahmin görevleri için etkili olduğunu kanıtladı.

Mimari 1: Emir Defteri Anlık Görüntüleri İçin CNN-LSTM

DeepLOB mimarisi (Zhang ve diğerleri 2019), bu uzamsal yapıyı korurken emir defteri seviyeleri boyunca uzamsal desenleri çıkarmak için yığılmış küçük çekirdekli evrişimleri — ve bir Inception modülünü — kullanır, ardından zamansal bağımlılıkları modellemek için LSTM katmanları gelir. Önemli tasarım seçimi, seviye eksenini yinelemeli katmandan önce küresel olarak havuzlamak (global-pool) değildir: bunu yapmak, evrişimlerin yakalaması amaçlanan seviyeler arası yapıyı tam olarak çökertir.

Spread tahmini için girdi, (T,L,F)(T, L, F) şeklinde bir tensördür:

  • TT = zaman adımı sayısı (örneğin 100 anlık görüntü)
  • LL = fiyat seviyesi sayısı (örneğin 10 alış + 10 satış = 20)
  • FF = seviye başına özellikler (fiyat, hacim, emir sayısı)

Aşağıdaki model, seviyeler üzerindeki evrişimsel özellik haritasını korur ve seviye boyutunu 16 kanal ortalamasına ortalamak yerine onu LSTM girdisine düzleştirir (input_size = 16 * L):

import torch
import torch.nn as nn


class SpreadPredictor(nn.Module):
    """
    CNN-LSTM model for bid-ask spread prediction from L2 order book.

    Input: (batch, seq_len, n_levels, n_features)
    Output: (batch, 1) — predicted spread in bps
    """

    def __init__(
        self,
        n_levels: int = 20,
        n_features: int = 3,
        seq_len: int = 100,
        hidden_dim: int = 64,
        n_lstm_layers: int = 2,
        dropout: float = 0.2,
    ):
        super().__init__()
        self.seq_len = seq_len
        self.n_levels = n_levels

        self.conv = nn.Sequential(
            nn.Conv1d(n_features, 32, kernel_size=3, padding=1),
            nn.BatchNorm1d(32),
            nn.LeakyReLU(0.1),
            nn.Conv1d(32, 16, kernel_size=3, padding=1),
            nn.BatchNorm1d(16),
            nn.LeakyReLU(0.1),
        )
        conv_out_dim = 16 * n_levels  # flattened (channels × levels)

        self.lstm = nn.LSTM(
            input_size=conv_out_dim,
            hidden_size=hidden_dim,
            num_layers=n_lstm_layers,
            batch_first=True,
            dropout=dropout,
        )

        self.head = nn.Sequential(
            nn.Linear(hidden_dim, 32),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(32, 1),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Parameters
        ----------
        x : Tensor of shape (batch, seq_len, n_levels, n_features)

        Returns
        -------
        Tensor of shape (batch, 1) — predicted spread
        """
        batch, T, L, F = x.shape

        x = x.reshape(batch * T, L, F).permute(0, 2, 1)
        x = self.conv(x)                  # (batch * T, 16, L)
        x = x.reshape(batch, T, 16 * L)   # (batch, T, 16 * L) — keep levels

        lstm_out, _ = self.lstm(x)        # (batch, T, hidden_dim)
        last_hidden = lstm_out[:, -1, :]  # (batch, hidden_dim)

        return self.head(last_hidden)     # (batch, 1)

Parametre sayısını kontrol etmek için seviye ekseninde havuzlama isterseniz, tek bir konumdan fazlasını koruyan adımlı veya öğrenilmiş bir havuzlama kullanın — her seviyeyi tek bir sayıya ortalayan ve uzamsal sinyali atan AdaptiveAvgPool1d(1) değil.

Mimari 2: Transformer Kodlayıcı

Transformer'lar, LSTM'lerin sıralı darboğazı olmadan emir defteri dizilerindeki uzun menzilli bağımlılıkları yakalayabilir. Spread tahmini için hafif bir transformer kodlayıcı iyi çalışır:

class TransformerSpreadPredictor(nn.Module):
    """Transformer encoder for spread prediction from order book sequences."""

    def __init__(
        self,
        input_dim: int = 40,   # 20 levels * 2 features (price_offset, volume)
        d_model: int = 64,
        nhead: int = 4,
        n_layers: int = 3,
        seq_len: int = 100,
        dropout: float = 0.1,
    ):
        super().__init__()
        self.input_proj = nn.Linear(input_dim, d_model)
        self.pos_encoding = nn.Parameter(torch.randn(1, seq_len, d_model) * 0.02)

        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model,
            nhead=nhead,
            dim_feedforward=d_model * 4,
            dropout=dropout,
            batch_first=True,
            activation="gelu",
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=n_layers)
        self.head = nn.Sequential(
            nn.LayerNorm(d_model),
            nn.Linear(d_model, 1),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        x: (batch, seq_len, input_dim) — flattened order book snapshots
        """
        x = self.input_proj(x) + self.pos_encoding[:, : x.size(1), :]
        x = self.encoder(x)
        return self.head(x[:, -1, :])

Eğitim Hususları

  1. Normalizasyon: Fiyatları orta fiyattan ofsetler olarak normalize edin (tik veya baz puan cinsinden). Hacimleri yuvarlanan ortalamalarına göre normalize edin. Ham fiyatlar ve hacimler eğitim kararsızlığına neden olur.

  2. Kayıp fonksiyonu: Spread sıçramalarını ele almak için Huber kaybı (δ=1.0\delta = 1.0) kullanın:

Lδ(y,y^)={12(yy^)2if yy^δδyy^12δ2otherwiseL_\delta(y, \hat{y}) = \begin{cases} \frac{1}{2}(y - \hat{y})^2 & \text{if } |y - \hat{y}| \le \delta \\ \delta |y - \hat{y}| - \frac{1}{2}\delta^2 & \text{otherwise} \end{cases}

  1. Pencere örnekleme ve sızıntı: Eğitim için örtüşmeyen pencereler kullanın ve — tam olarak gradyan artırma boru hattında olduğu gibi — eğitim ile doğrulama arasında en az horizon kadar anlık görüntülük bir boşluğu temizleyin/ambargolayın. Hem ileri-ortalama hedefi hem de örtüşen girdi pencereleri, bölme sınırları boyunca gelecek bilgisini sızdırır ve görünür performansı şişirir.

  2. Çevrimiçi adaptasyon: Üretimde, modeli düşük bir öğrenme oranıyla periyodik olarak son veriler üzerinde (son 1-2 saat) ince ayarlayın. Piyasa mikroyapısı gün içinde değişir ve sabah verisiyle eğitilmiş bir model öğleden sonra daha düşük performans gösterebilir.

Derin Öğrenme ile Gradyan Artırma Ne Zaman Kullanılır

Kriter Gradyan Artırma Derin Öğrenme
Girdi türü Tablo verili özellikler Ham emir defteri dizileri
Eğitim veri boyutu 100K+ satırla çalışır 1M+ satıra ihtiyaç duyar
Özellik mühendisliği Manuel (yüksek çaba, yüksek kontrol) Öğrenilmiş (daha düşük çaba, daha az yorumlanabilir)
Çıkarım gecikmesi Derlenmiş bir tahminci ile tek haneli µs; Python'dan onlarca µs GPU'da yüzlerce µs
Yorumlanabilirlik Yüksek (SHAP) Düşük (dikkat haritaları)
Rejim adaptasyonu Yeniden eğitim / çevrimiçi güncelleme Son veriler üzerinde ince ayar
Kısa ufuk spread becerisi Genel olarak DL ile karşılaştırılabilir Daha uzun ufuklarda / daha büyük veride avantaj büyür

Belirli R² rakamlarını alıntılamaktan kasıtlı olarak kaçınıyoruz: spread tahmin doğruluğu büyük ölçüde ufka, varlığa ve skorun ne kadarının basitçe spread otokorelasyonu olduğuna bağlıdır. Bir model, tek satırlık bir EWMA üzerine neredeyse hiçbir şey eklemeden etkileyici bir ham R² gösterebilir. Başlıktaki bir R² yerine, ufuk belirtilmiş halde aynı veri üzerinde bir AR/EWMA temel çizgisinin üzerindeki beceriyi raporlayın. Benzer şekilde, gecikme rakamlarını uygulamaya bağlı olarak ele alın: 2000 turlu, 63 yapraklı bir LightGBM modeli, Python'dan tek bir satırı onlarca mikrosaniyede tahmin eder ve yalnızca derlenmiş/C++ bir tahminci ile birkaç mikrosaniyeye ulaşır.

Pratikte, birçok üretim sistemi iki aşamalı bir yaklaşım kullanır: gerçek zamanlı kotasyon için hızlı bir gradyan artırma modeli (gecikme açısından kritik) ve artırma modelinin parametrelerini ayarlamak veya ikincil bir sinyal sağlamak için asenkron çalışan bir derin öğrenme modeli.

Tahminden Kotasyona

Tahmin edilen bir spread'i, adil bir orta fiyat etrafında envantere göre eğilmiş canlı alış/satış kotalarına dönüştürmek

Bir spread tahmini ancak daha iyi kotalara dönüştüğünde değerlidir. İşte tahmin edilen spread'i kullanan basitleştirilmiş bir kotasyon kuralı:

def compute_quotes(
    mid: float,
    predicted_spread_bps: float,
    inventory: float,
    max_inventory: float,
    skew_factor: float = 0.5,
    min_spread_bps: float = 1.0,
) -> tuple[float, float]:
    """
    Compute bid/ask quotes from predicted spread and inventory.

    Parameters
    ----------
    mid : float
        Current midprice.
    predicted_spread_bps : float
        Model-predicted spread in basis points.
    inventory : float
        Current inventory (positive = long).
    max_inventory : float
        Maximum allowed inventory.
    skew_factor : float
        How aggressively to skew quotes toward inventory neutrality.
    min_spread_bps : float
        Minimum spread floor (covers order processing costs).

    Returns
    -------
    (bid, ask) : tuple[float, float]
    """
    spread_bps = max(predicted_spread_bps, min_spread_bps)
    half_spread = mid * spread_bps / 2e4

    inv_ratio = inventory / max_inventory  # in [-1, 1]
    skew = skew_factor * inv_ratio * half_spread

    bid = mid - half_spread - skew
    ask = mid + half_spread - skew

    return bid, ask

Envanter uzun olduğunda (Q>0Q > 0), eğme hem alışı hem de satışı düşürür. Tek, tutarlı bir perspektiften: daha düşük bir satış, taker'ların bizden satın almasını ucuzlatır, bu da uzun envanterimizi boşaltır; daha düşük bir alış, satıcılar tarafından vurulma olasılığımızı azaltır, daha fazla birikmeyi yavaşlatır. Tahmin edilen spread genel genişliği kontrol eder — model volatilite veya ters seçim beklediğinde genişler, koşullar sakin olduğunda daralır.

Değerlendirme ve Geriye Dönük Test

Bir spread modelini değerlendirme: bir gerçekleşme oranı ve PnL göstergesinin yanında tahmin edilen ve gerçekleşen spread kalibrasyonu

Spread Tahmin Metrikleri

Standart regresyon metriklerinin (MAE, R2R^2) ötesinde, spread tahminlerini piyasa yapma için önemli olan metriklerle değerlendirin:

  • Bir temel çizgi üzerindeki beceri: MAE/R²'yi her zaman son spread'lerin bir AR(1) veya EWMA tahminine göreli olarak raporlayın. Spread'ler güçlü biçimde kalıcı olduğundan, mutlak skor otokorelasyon tarafından domine edilir; yalnızca önemsiz bir temel çizgi üzerindeki iyileşme gerçek öngörü içeriğini yansıtır.
  • Yönlü doğruluk: Model, spread'in genişleyip genişlemeyeceğini veya daralacağını doğru tahmin ediyor mu? Vasat MAE ama yüksek yönlü doğruluğa sahip bir model yine de kârlı olabilir.
  • Kuyruk kapsamı: Model spread sıçramalarını tahmin ediyor mu? Spread değerlerinin en üst %5'i için MAE'yi ayrı hesaplayın — ters seçim kayıplarının yoğunlaştığı yer burasıdır.
  • Kalibrasyon: Tahmin edilen ve gerçekleşen spread kuantillerini çizin. İyi kalibre edilmiş bir modelin 90. yüzdelik dilim tahmini, gerçekleşen spread'lerin 90. yüzdelik dilimiyle eşleşmelidir.

PnL Tabanlı Değerlendirme

Nihayetinde, önemli olan tek metrik gerçekleşen PnL'dir. Tam döngüyü geriye dönük test edin:

  1. Her zaman damgasında spread'i tahmin edin
  2. Tahmin edilen spread + envanter eğmesini kullanarak kotaları hesaplayın
  3. Geçmiş işlemlere karşı gerçekleşmeleri simüle edin
  4. Envanteri, gerçekleşen PnL'yi ve Sharpe oranını takip edin

Temel çizgilere karşı karşılaştırın: (a) sabit spread (zaman serisi medyanı), (b) son spread'lerin EWMA'sı ve (c) Roll tahmincisi.

Sonuç

Spread modelleme, finansal teori ile uygulamalı ML'nin kesişiminde yer alır. Klasik ayrıştırma — emir işleme, envanter ve ters seçim maliyetlerine — spread'lerin neden değiştiğine dair ekonomik sezgiyi sağlar. Roll'un modeli, minimum veriden zarif bir temel tahminci verir — onu fiyat birimleri cinsinden hesapladığınız sürece. Gradyan artırma modelleri, mikroyapı özelliklerini düşük gecikmeli çıkarımla doğru kısa ufuklu spread tahminlerine dönüştürür. Derin öğrenme mimarileri, doğrudan ham emir defteri verisinden öğrenir, el yapımı özelliklerin kaçırabileceği desenleri yakalar — mimari, seviyeler arası yapıyı havuzlayıp atmak yerine koruduğu sürece.

Bir üretim piyasa yapma sistemi için pratik öneri katmanlıdır:

  1. Spread bileşenlerinizi anlamak ve risk limitlerini kalibre etmek için çevrimdışı olarak Huang-Stoll ayrıştırmasını kullanın
  2. Bir mantık kontrolü olarak ve emir defteri verisinden yoksun olduğunuz enstrümanlar için Roll'un tahmincisini kullanın
  3. Gerçek zamanlı spread tahmini için bir LightGBM modeli dağıtın — hızlı, yorumlanabilir ve sağlamdır — temizlenmiş ileriye dönük doğrulama ve bir AR/EWMA kıyaslamasıyla
  4. Rejim değişikliklerini tespit etmek ve birincil modeli ayarlamak için ikincil bir döngüde bir CNN-LSTM veya Transformer modeli çalıştırın

Spread bir sayı değildir — bir sinyaldir. Onu ne kadar iyi modellerseniz (ve o modeli ne kadar dürüst ölçerseniz), likidite sağlamayı o kadar kesin fiyatlandırabilirsiniz.


Bu yazı, algoritmik piyasa yapma ve mikroyapı üzerine marketmaker.cc serisinin bir parçasıdır.

Sorumluluk Reddi: Bu makalede sağlanan bilgiler yalnızca eğitim ve bilgilendirme amaçlıdır ve finansal, yatırım veya ticaret tavsiyesi niteliği taşımaz. Kripto para ticareti önemli bir kayıp riski içerir.

Yazarlar

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Piyasanın Önünde Olun

Özel yapay zeka ticaret içgörüleri, piyasa analizi ve platform güncellemeleri için bültenimize abone olun.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.