← Kembali ke artikel
June 3, 2026
Bacaan 5 minit

Pemodelan dan Ramalan Spread Bid-Ask dengan Pembelajaran Mesin

Pemodelan dan Ramalan Spread Bid-Ask dengan Pembelajaran Mesin
#microstructure
#spread
#market-making
#prediction
#machine-learning
#gradient-boosting
#deep-learning

Spread bid-ask ialah pemboleh ubah tunggal paling penting yang dikawal oleh seorang market maker. Tetapkan ia terlalu lebar dan anda kehilangan aliran kepada pesaing. Tetapkan ia terlalu sempit dan adverse selection memakan inventori anda hidup-hidup. Teori mikrostruktur tradisional memberi kita penguraian spread yang elegan kepada komponen ekonominya. Pembelajaran mesin memberi kita alat untuk meramalkan bagaimana komponen tersebut beralih dalam masa nyata. Catatan ini menghubungkan kedua-dua dunia: kita bermula dengan teori klasik, membina menuju penganggar spread tersirat Roll, kemudian beralih ke model gradient boosting dan deep learning yang meramalkan spread daripada ciri order book. Sepanjang perjalanan kita menandai unit, kebocoran, dan perangkap penanda aras yang secara senyap-senyap membatalkan model spread dalam amalan.

Mengapa Spread Penting bagi Market Maker

Seorang market maker secara berterusan menyebut harga bid PbP_b dan harga ask PaP_a. Spread yang disebut ialah:

S=PaPbS = P_a - P_b

Setiap perjalanan pergi-balik (beli pada bid maker, jual pada ask maker, kedua-duanya dipenuhi oleh taker) memindahkan sehingga SS daripada taker kepada maker — pada teorinya. Dalam amalan, maker memperoleh kurang daripada SS kerana adverse selection: sesetengah taker bermaklumat dan berdagang tepat sebelum harga bergerak menentang maker. Keuntungan terealisasi setiap perjalanan pergi-balik ialah realized spread, yang sama dengan effective spread tolak price impact:

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

Kita mengukur ketiga-tiga kuantiti pada asas full-spread yang sama (bukan separuh), supaya identiti itu konsisten dari segi dimensi. Effective spread untuk satu dagangan tunggal ialah:

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

Di sini dt{+1,1}d_t \in \{+1, -1\} ialah arah dagangan (taker beli atau jual), PtP_t ialah harga transaksi, dan MtM_t ialah titik tengah bagi best bid dan ask pada masa dagangan. Terma price-impact ditakrifkan secara simetri sepanjang ufuk pasca-dagangan τ\tau:

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

di mana Mt+τM_{t+\tau} ialah mid satu ufuk τ\tau selepas dagangan. Ufuk itu mesti dinyatakan secara eksplisit — pilihan biasa ialah 5 minit dalam ekuiti dan 30 saat dalam crypto, di mana harga menetapkan semula harga dengan lebih pantas. Menolak impak daripada effective spread meninggalkan realized spread: apa yang maker simpan selepas pasaran bergerak.

Seorang market maker yang boleh meramalkan spread — dan komponennya — dalam 1, 5, atau 60 saat seterusnya boleh melaraskan sebut harga secara dinamik untuk memaksimumkan realized spread sambil mengekalkan kadar pengisian.

Tiga Komponen Spread

Spread bid-ask diuraikan kepada lapisan kos pemprosesan pesanan, inventori, dan adverse selection

Literatur mikrostruktur pasaran (Stoll 1978, Glosten dan Milgrom 1985, Huang dan Stoll 1997) menguraikan spread bid-ask kepada tiga komponen ekonomi.

1. Kos Pemprosesan Pesanan (α\alpha)

Ini ialah kos menyediakan perkhidmatan market-making bagi setiap sisi yang dipenuhi: yuran yang sebenarnya dibayar oleh maker ditambah infrastruktur teknologi, pematuhan kawal selia, dan kos peluang modal yang digunakan. Demsetz (1968) dan Tinic (1972) ialah yang pertama memformalkan komponen ini.

Perbezaan utama ialah siapa membayar yuran yang mana. Seorang maker yang menyebut secara pasif membayar maker fee fmf_m pada pengisiannya sendiri — dan pada banyak venue fmf_m ialah rebat, iaitu negatif. Ia tidak membayar taker fee ftf_t pada pengisian pasif tersebut; pihak lawan yang menyeberangi spread membayar ftf_t. Jadi kos pemprosesan pesanan setiap sisi bagi maker ialah:

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

di mana fmf_m bertanda (rebat menurunkan α\alpha dan boleh menjadikannya negatif) dan cinfrac_{\text{infra}} meliputi kesalinghubungan, kolokasi, dan komputasi. Dalam pasaran elektronik moden komponen ini telah mengecut secara dramatik — sub-sen dalam ekuiti, beberapa basis point atau rebat bersih dalam crypto.

Taker fee ftf_t penting atas sebab yang berbeza: ia menetapkan lantai bagi sejauh mana spread penuh boleh menguntungkan apabila sempit, kerana taker yang menyeberangi membayar ftf_t di atas spread. Jika anda mahu lantai spread yang mengekalkan sebut harga anda menarik secara ekonomi berbanding kos taker tersebut, motivasikannya secara berasingan dan bukannya melipat ftf_t ke dalam kos maker sendiri. Mengelirukan kedua-duanya akan mengira dua kali yuran pergi-balik di dalam satu separuh-spread.

2. Kos Memegang Inventori (β\beta)

Apabila seorang market maker mengumpul kedudukan berarah (long atau short), mereka menanggung risiko harga. Komponen inventori mengimbangi risiko ini. Stoll (1978) dan Amihud dan Mendelson (1980) memodelkan ini sebagai fungsi volatiliti dan inventori semasa maker:

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

di mana σ\sigma ialah volatiliti aset dan QQ ialah inventori semasa maker. Apabila inventori membesar, maker melebarkan spread pada sisi di mana mereka terdedah dan menyempitkannya pada sisi yang lain, satu teknik dipanggil inventory skewing.

3. Kos Adverse Selection (γ\gamma)

Ini ialah komponen paling berbahaya. Pedagang bermaklumat — mereka yang mempunyai maklumat unggul tentang pergerakan harga yang bakal berlaku — secara sistematik mengutip sebut harga yang basi. Kos adverse selection sama dengan jangkaan kerugian setiap dagangan kepada pihak lawan yang bermaklumat. Copeland dan Galai (1983) memodelkan ini sebagai nilai opsyen percuma yang maker berikan kepada pedagang bermaklumat. Glosten dan Milgrom (1985) memformalkannya sebagai semakan Bayesian dalam kepercayaan maker selepas memerhati satu dagangan:

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

di mana VV ialah nilai asas sebenar. Dalam pasaran cair, adverse selection boleh menyumbang 30-60% daripada jumlah spread.

Penguraian Penuh

Separuh-spread yang disebut boleh ditulis sebagai:

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

dengan α\alpha, β\beta, dan γ\gamma kesemuanya dinyatakan sebagai kos setiap sisi (separuh-spread) — itulah yang mengekalkan perakaunan konsisten. Huang dan Stoll (1997) mencadangkan kaedah ekonometrik untuk menganggar komponen ini daripada data dagangan dan sebut harga. Pandangan utama: kos pemprosesan pesanan mencipta lantai spread tetap, kos inventori mencipta spread yang berubah dengan kedudukan dan volatiliti, dan kos adverse selection mencipta spread yang berubah dengan asimetri maklumat.

Model Spread Tersirat Roll

Model spread tersirat Roll: harga transaksi melantun antara bid dan ask, meninggalkan tandatangan kovarians bersiri negatif

Sebelum data frekuensi tinggi tersedia secara meluas, Richard Roll (1984) mencadangkan kaedah elegan untuk menganggar effective spread menggunakan hanya harga transaksi. Pandangannya: dalam pasaran cekap, lantunan bid-ask mendorong kovarians bersiri negatif dalam perubahan harga, walaupun tiada maklumat baharu.

Model

Andaikan nilai asas VtV_t mengikut random walk:

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)

Harga transaksi yang diperhati melantun antara bid dan ask:

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

di mana dt{1,+1}d_t \in \{-1, +1\} dengan kebarangkalian sama (iaitu, belian dan jualan adalah sama berkemungkinan). Perubahan harga ialah:

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

Mengira autokovarians tertib pertama:

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

Model ini diterbitkan dalam unit harga: SS terhasil daripada autokovarians perubahan harga, bukan pulangan. Perbezaan ini ialah ralat pelaksanaan tunggal yang paling biasa, dan kita mengekalkan kod setia kepadanya di bawah.

Penganggar Roll

Menyelesaikan untuk SS:

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

Apabila autokovarians sampel positif (yang kerap berlaku dalam amalan akibat bunyi atau momentum), penganggar tidak tertakrif. Pembetulan biasa ialah menetapkan anggaran kepada sifar atau menggunakan punca bertanda:

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

di mana γ^1\hat{\gamma}_1 ialah autokovarians tertib pertama sampel.

Pelaksanaan dalam Python

Penganggar mengembalikan spread dalam unit harga. Untuk menyatakannya dalam basis point kita bahagikan dengan midprice sekali — kerana, tidak seperti penganggar ruang-pulangan, ia belum dibahagikan dengan harga:

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

Pemeriksaan kewarasan pantas pada siri tersimulasi — random walk asas berhampiran harga 100 dengan spread sebenar S=0.10S = 0.10 — memperoleh semula 0.0999\approx 0.0999 daripada perubahan harga. Varian berasaskan pulangan akan mengembalikan 0.001\approx 0.001, tersasar mengikut paras harga, dan kemudian membahagikan itu dengan midprice sekali lagi untuk mendapatkan bps akan menggandakan ralat. Jika anda lebih suka penganggar ruang-pulangan, terbitkan model dalam ruang log-harga dan gugurkan pembahagian kedua dengan midprice; pilih satu konvensyen dan jadikan kod sepadan dengan matematik.

Batasan Model Roll

Model Roll menganggap: (1) kecekapan pasaran, (2) tiada asimetri maklumat, (3) arah dagangan i.i.d., dan (4) spread malar. Kesemua ini dilanggar dalam amalan. Harris (1990) menunjukkan bahawa penganggar sangat berat sebelah akibat ketaksamaan Jensen apabila digunakan pada data berbunyi. Walaupun terdapat batasan ini, penganggar Roll kekal berguna sebagai garis dasar pantas dan digunakan secara meluas dalam penyelidikan kewangan empirikal.

Ciri ML untuk Ramalan Spread

Ciri mikrostruktur terkejuruterakan — volatiliti, ketidakseimbangan order-flow, kedalaman, keamatan dagangan — menyuap peramal spread

Untuk bergerak melangkaui model statik, kita memerlukan ciri yang menangkap pemacu dinamik variasi spread. Berikut ialah taksonomi ciri yang disusun mengikut komponen spread yang diwakilinya.

Ciri Order Book (Inventori & Adverse Selection)

Ciri Formula Mewakili
Ketidakseimbangan buku BI=Vb1Va1Vb1+Va1\text{BI} = \frac{V_b^1 - V_a^1}{V_b^1 + V_a^1} Tekanan berarah
Mid-price berwajaran 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} Nilai saksama jangka pendek
Nisbah kedalaman (paras 1-5) DR5=i=15Vbii=15Vai\text{DR}_5 = \frac{\sum_{i=1}^{5} V_b^i}{\sum_{i=1}^{5} V_a^i} Bekalan/permintaan pelbagai paras
Tekanan buku 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 Tekanan berwajaran jarak
Nisbah spread / tick S/tickS / \text{tick} Keketatan relatif kepada minimum

Tekanan buku di sini menggunakan pereputan jarak-mutlak-ke-mid, wi=eλPiMw_i = e^{-\lambda |P_i - M|}, supaya volum yang terletak berhampiran touch dikira lebih daripada volum dalam, dan kedua-dua sisi diwajarkan oleh fungsi positif yang menurun. Ini mengelakkan bias tanda struktur akibat membahagikan volum dengan jarak bertanda PiMP_i - M (yang negatif pada sisi bid, positif pada sisi ask, dan meletup apabila satu paras menghampiri mid). Pilih λ\lambda daripada kedalaman buku tipikal, atau gantikan eksponen dengan sebarang wajaran positif w(PiM)w(|P_i - M|) yang menurun dalam jarak.

Ciri Trade Flow (Adverse Selection)

Ciri Formula Mewakili
Ketidakseimbangan dagangan TIn=i=1ndivii=1nvi\text{TI}_{n} = \frac{\sum_{i=1}^{n} d_i \cdot v_i}{\sum_{i=1}^{n} v_i} Aliran bermaklumat bersih
VPIN Kebarangkalian disegerakkan-volum bagi dagangan bermaklumat Ketoksikan
Kyle's lambda Regresi ΔM\Delta M pada volum bertanda Price impact per unit
Kekerapan dagangan besar Bilangan dagangan >kmedian> k \cdot \text{median} dalam tetingkap Aktiviti institusi

Ciri Volatiliti (Kos Inventori)

Ciri Formula Mewakili
Realized volatility σrv=(ΔlogM)2\sigma_{\text{rv}} = \sqrt{\sum (\Delta \log M)^2} Risiko jangka pendek
Vol Garman-Klass 12(logH/L)2(2ln21)(logC/O)2\frac{1}{2}(\log H/L)^2 - (2\ln 2 - 1)(\log C/O)^2 Vol berasaskan julat
Vol-of-vol Sisihan piawai rolling bagi σrv\sigma_{\text{rv}} Ketidakpastian rejim
Autokorelasi pulangan ρ1(ΔM)\rho_1(\Delta M) Momentum / mean-reversion

Ciri Rejim Pasaran

Ciri Penerangan Mewakili
Pengekodan masa-dalam-hari sin(2πt/T),cos(2πt/T)\sin(2\pi t / T), \cos(2\pi t / T) Bermusim intrahari
Saat sejak dagangan terakhir Jurang masa Tahap aktiviti
Korelasi rentas-aset Korelasi rolling dengan indeks/BTC Risiko sistematik
Funding rate (crypto) Funding rate perp Kedudukan berleveraj

Gradient Boosting untuk Ramalan Spread

Pokok gradient boosted (XGBoost, LightGBM, CatBoost) ialah kuda pekerja ramalan jadual dalam kewangan kuantitatif. Ia mengendalikan jenis ciri bercampur, menangkap interaksi tak linear, memerlukan pemprosesan awal minimum, dan dilatih pantas pada jutaan baris — dengan syarat pembinaan ciri itu sendiri divektorkan (lihat nota autokorelasi di bawah).

Perumusan Masalah

Kita merangka ramalan spread sebagai tugasan regresi. Sasarannya ialah purata spread yang disebut berwajaran masa sepanjang τ\tau saat seterusnya:

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

Dalam amalan, kita menganggarkan ini dengan purata spread berwajaran volum sepanjang NN snapshot seterusnya:

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}}

Sasaran ini ialah forward window berpanjang NN (atau horizon), yang bermaksud baris bersebelahan berkongsi tetingkap masa hadapan yang bertindih. Pertindihan itu membocorkan maklumat merentasi pembahagian latihan/pengesahan yang naif — kita mengendalikannya secara eksplisit dalam kod latihan di bawah.

Saluran Paip Penuh

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

Butiran penting ialah jurang purge/embargo. Sasaran forward-mean bermaksud baris bersebelahan bertindih sehingga horizon snapshot, jadi TimeSeriesSplit biasa membenarkan baris pengesahan berkongsi tetingkap masa hadapan dengan baris latihan — membocorkan jawapan dan menggelembungkan R² pengesahan. Menggugurkan jurang sekurang-kurangnya horizon baris pada kedua-dua sisi setiap sempadan fold (purged k-fold gaya Lopez de Prado) membuang bias itu. Ini terpakai sama banyaknya pada saluran paip gradient-boosting seperti pada deep learning, walaupun kebocoran lebih lazim dibincangkan untuk model jujukan.

Analisis Kepentingan Ciri

Salah satu kelebihan utama model berasaskan pokok ialah kebolehtafsiran. Selepas latihan, periksa nilai SHAP untuk memahami ciri mana yang memacu ramalan spread:

import shap

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

Penemuan tipikal merentasi kelas aset:

  1. Spread tertangguh (spread_lag_1\text{spread\_lag\_1}) hampir selalu menjadi ciri paling penting — spread sangat autokorelasi. Inilah juga sebab R² utama kelihatan tinggi: kebanyakan skor hanyalah ketekalan, jadi sentiasa jadikan penanda aras berbanding garis dasar AR/EWMA (lebih lanjut tentang ini di bawah).
  2. Realized volatility ialah yang kedua paling penting — volatiliti intrahari dan spread berkorelasi positif kuat, baik secara sezaman mahupun dinamik.
  3. Ketidakseimbangan buku paling penting semasa tempoh meruap — ia menandakan pergerakan berarah yang bakal berlaku.
  4. Ketidakseimbangan trade flow menangkap adverse selection jangka pendek — letusan aliran satu sisi meramalkan pelebaran spread.
  5. Masa-dalam-hari menangkap corak intrahari berbentuk-U (lebih lebar pada buka/tutup, lebih ketat tengah hari).

Pertimbangan Hiperparameter

Khusus untuk ramalan spread:

  • Guna MAE atau Huber loss dan bukan MSE. Taburan spread terpencong-kanan dengan outlier ekstrem sekali-sekala (semasa peristiwa berita). MAE lebih teguh.
  • Tetapkan min_child_samples tinggi (100+) untuk menghalang model daripada memuatkan kepada bunyi mikrostruktur dalam snapshot individu.
  • Guna subsample < 1.0 untuk menyahkorelasikan pokok dan menambah baik generalisasi merentasi rejim volatiliti yang berbeza.

Pendekatan Deep Learning

Walaupun gradient boosting cemerlang pada ciri jadual, deep learning boleh mempelajari perwakilan terus daripada data order book mentah. Dua seni bina telah terbukti berkesan untuk tugasan ramalan berkaitan-spread.

Seni Bina 1: CNN-LSTM untuk Snapshot Order Book

Seni bina DeepLOB (Zhang et al. 2019) menggunakan konvolusi kernel-kecil bertindan — dan modul Inception — untuk mengekstrak corak ruang merentasi paras order book sambil memelihara struktur ruang itu, diikuti oleh lapisan LSTM untuk memodelkan kebergantungan temporal. Pilihan reka bentuk yang penting ialah tidak untuk global-pool paksi paras sebelum lapisan berulang: berbuat demikian meruntuhkan tepat struktur rentas-paras yang konvolusi sepatutnya tangkap.

Untuk ramalan spread, input ialah tensor berbentuk (T,L,F)(T, L, F):

  • TT = bilangan langkah masa (cth., 100 snapshot)
  • LL = bilangan paras harga (cth., 10 bid + 10 ask = 20)
  • FF = ciri setiap paras (harga, volum, kiraan pesanan)

Model di bawah mengekalkan peta ciri konvolusi merentasi paras dan meratakannya ke dalam input LSTM (input_size = 16 * L), dan bukannya mempuratakan dimensi paras kepada 16 min saluran:

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)

Jika anda memang mahu pooling pada paksi paras untuk mengawal kiraan parameter, guna pooling berlangkah atau yang dipelajari yang mengekalkan lebih daripada satu kedudukan — bukan AdaptiveAvgPool1d(1), yang mempuratakan setiap paras kepada satu nombor dan membuang isyarat ruang.

Seni Bina 2: Transformer Encoder

Transformer boleh menangkap kebergantungan jarak-jauh dalam jujukan order book tanpa kesesakan berjujukan LSTM. Untuk ramalan spread, transformer encoder ringan berfungsi dengan baik:

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, :])

Pertimbangan Latihan

  1. Penormalan: Normalkan harga sebagai ofset daripada midprice (dalam tick atau bps). Normalkan volum mengikut min rolling mereka. Harga dan volum mentah menyebabkan ketidakstabilan latihan.

  2. Fungsi kerugian: Guna Huber loss (δ=1.0\delta = 1.0) untuk mengendalikan lonjakan spread:

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. Pensampelan tetingkap dan kebocoran: Guna tetingkap tidak bertindih untuk latihan, dan — tepat seperti dalam saluran paip gradient-boosting — purge/embargo jurang sekurang-kurangnya horizon snapshot antara latihan dan pengesahan. Kedua-dua sasaran forward-mean dan tetingkap input bertindih membocorkan maklumat masa hadapan merentasi sempadan pembahagian dan menggelembungkan prestasi yang nampak.

  2. Penyesuaian dalam talian: Dalam produksi, fine-tune model secara berkala pada data terkini (1-2 jam terakhir) dengan kadar pembelajaran kecil. Mikrostruktur pasaran berubah intrahari, dan model yang dilatih pada data pagi mungkin berprestasi rendah pada sebelah petang.

Bila Menggunakan Deep Learning vs. Gradient Boosting

Kriteria Gradient Boosting Deep Learning
Jenis input Ciri jadual Jujukan order book mentah
Saiz data latihan Berfungsi dengan 100K+ baris Perlu 1J+ baris
Feature engineering Manual (usaha tinggi, kawalan tinggi) Dipelajari (usaha rendah, kurang boleh ditafsir)
Latensi inferens Mikrosaat satu digit dengan peramal terkompil; puluhan µs daripada Python Ratusan µs pada GPU
Kebolehtafsiran Tinggi (SHAP) Rendah (peta attention)
Penyesuaian rejim Latih semula / kemas kini dalam talian Fine-tune pada data terkini
Kemahiran spread ufuk-pendek Setanding secara luas dengan DL Kelebihan membesar pada ufuk lebih panjang / data lebih besar

Kita sengaja mengelak daripada memetik angka R² tertentu: ketepatan ramalan spread sangat bergantung pada ufuk, aset, dan berapa banyak skor itu hanyalah autokorelasi spread. Sesebuah model boleh mencatat R² mentah yang mengagumkan sambil menambah hampir tiada apa-apa melebihi EWMA satu baris. Laporkan kemahiran melebihi garis dasar AR/EWMA pada data yang sama, dengan ufuk dinyatakan, dan bukannya R² utama. Begitu juga, layan angka latensi sebagai bergantung-pelaksanaan: model LightGBM 2000-pusingan, 63-daun meramalkan satu baris dalam puluhan mikrosaat daripada Python dan hanya mencapai beberapa mikrosaat dengan peramal terkompil/C++.

Dalam amalan, banyak sistem produksi menggunakan pendekatan dua-peringkat: model gradient boosting pantas untuk sebut harga masa nyata (kritikal-latensi), dan model deep learning yang berjalan secara tak segerak untuk melaraskan parameter model boosting atau menyediakan isyarat sekunder.

Daripada Ramalan kepada Sebut Harga

Menukar spread yang diramalkan kepada sebut harga bid/ask langsung, dipencongkan oleh inventori sekitar mid-price saksama

Ramalan spread hanya bernilai jika ia diterjemahkan kepada sebut harga yang lebih baik. Berikut ialah peraturan sebut harga yang dipermudahkan yang menggunakan spread yang diramalkan:

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

Apabila inventori long (Q>0Q > 0), skew menurunkan kedua-dua bid dan ask. Daripada satu perspektif yang tunggal dan konsisten: ask yang lebih rendah menjadikannya lebih murah bagi taker untuk membeli daripada kita, yang melepaskan inventori long kita; bid yang lebih rendah menjadikan kita kurang berkemungkinan dikenakan oleh penjual, melambatkan pengumpulan selanjutnya. Spread yang diramalkan mengawal kelebaran keseluruhan — melebarkan apabila model menjangkakan volatiliti atau adverse selection, menyempitkan apabila keadaan tenang.

Penilaian dan Backtesting

Menilai model spread: penentukuran spread diramalkan-vs-terealisasi bersebelahan tolok kadar pengisian dan PnL

Metrik Ramalan Spread

Melangkaui metrik regresi standard (MAE, R2R^2), nilai ramalan spread dengan metrik yang penting untuk market making:

  • Kemahiran melebihi garis dasar: Sentiasa laporkan MAE/R² relatif kepada ramalan AR(1) atau EWMA bagi spread terkini. Kerana spread sangat tekal, skor mutlak dikuasai oleh autokorelasi; hanya peningkatan melebihi garis dasar remeh mencerminkan kandungan ramalan sebenar.
  • Ketepatan berarah: Adakah model meramalkan dengan betul sama ada spread akan melebar atau menyempit? Sesebuah model dengan MAE sederhana tetapi ketepatan berarah tinggi masih boleh menguntungkan.
  • Liputan ekor: Adakah model meramalkan lonjakan spread? Kira MAE secara berasingan untuk 5% teratas nilai spread — di sinilah kerugian adverse selection tertumpu.
  • Penentukuran: Plot kuantil spread diramalkan vs. terealisasi. Ramalan persentil ke-90 model yang ditentukur dengan baik sepatutnya sepadan dengan persentil ke-90 spread terealisasi.

Penilaian Berasaskan PnL

Pada akhirnya, satu-satunya metrik yang penting ialah PnL terealisasi. Backtest gelung penuh:

  1. Pada setiap cap masa, ramalkan spread
  2. Kira sebut harga menggunakan spread yang diramalkan + inventory skew
  3. Simulasikan pengisian terhadap dagangan sejarah
  4. Jejaki inventori, PnL terealisasi, dan nisbah Sharpe

Bandingkan dengan garis dasar: (a) spread malar (median siri masa), (b) EWMA spread terkini, dan (c) penganggar Roll.

Kesimpulan

Pemodelan spread terletak di persimpangan antara teori kewangan dan ML gunaan. Penguraian klasik kepada kos pemprosesan pesanan, inventori, dan adverse selection menyediakan intuisi ekonomi bagi mengapa spread berubah-ubah. Model Roll memberikan penganggar garis dasar yang elegan daripada data minimum — selagi anda mengiranya dalam unit harga. Model gradient boosting menukar ciri mikrostruktur kepada ramalan spread ufuk-pendek yang tepat dengan inferens latensi-rendah. Seni bina deep learning belajar terus daripada data order book mentah, menangkap corak yang mungkin terlepas oleh ciri yang dibuat sendiri — dengan syarat seni bina memelihara struktur rentas-paras dan bukannya mempool-kannya keluar.

Untuk sistem market-making produksi, cadangan praktikal adalah berlapis:

  1. Guna penguraian Huang-Stoll luar talian untuk memahami komponen spread anda dan menentukur had risiko
  2. Guna penganggar Roll sebagai pemeriksaan kewarasan dan untuk instrumen di mana anda kekurangan data order book
  3. Deploy model LightGBM untuk ramalan spread masa nyata — ia pantas, boleh ditafsir, dan teguh — dengan pengesahan purged walk-forward dan penanda aras AR/EWMA
  4. Jalankan model CNN-LSTM atau Transformer dalam gelung sekunder untuk mengesan perubahan rejim dan melaraskan model utama

Spread bukanlah satu nombor — ia ialah satu isyarat. Semakin baik anda memodelkannya (dan semakin jujur anda mengukur model itu), semakin tepat anda boleh menetapkan harga penyediaan kecairan.


Catatan ini ialah sebahagian daripada siri marketmaker.cc mengenai market making algoritmik dan mikrostruktur.

Penafian: Maklumat yang disediakan dalam artikel ini adalah untuk tujuan pendidikan dan maklumat sahaja dan bukan merupakan nasihat kewangan, pelaburan, atau dagangan. Dagangan mata wang kripto melibatkan risiko kerugian yang ketara.

Pengarang

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

Kekal Mendahului Pasaran

Langgan surat berita kami untuk pandangan dagangan AI eksklusif, analisis pasaran, dan kemas kini platform.

Kami menghormati privasi anda. Berhenti melanggan pada bila-bila masa.