Pemodelan dan Ramalan Spread Bid-Ask dengan Pembelajaran Mesin
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 dan harga ask . Spread yang disebut ialah:
Setiap perjalanan pergi-balik (beli pada bid maker, jual pada ask maker, kedua-duanya dipenuhi oleh taker) memindahkan sehingga daripada taker kepada maker — pada teorinya. Dalam amalan, maker memperoleh kurang daripada 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:
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:
Di sini ialah arah dagangan (taker beli atau jual), ialah harga transaksi, dan ialah titik tengah bagi best bid dan ask pada masa dagangan. Terma price-impact ditakrifkan secara simetri sepanjang ufuk pasca-dagangan :
di mana ialah mid satu ufuk 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

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 ()
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 pada pengisiannya sendiri — dan pada banyak venue ialah rebat, iaitu negatif. Ia tidak membayar taker fee pada pengisian pasif tersebut; pihak lawan yang menyeberangi spread membayar . Jadi kos pemprosesan pesanan setiap sisi bagi maker ialah:
di mana bertanda (rebat menurunkan dan boleh menjadikannya negatif) dan 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 penting atas sebab yang berbeza: ia menetapkan lantai bagi sejauh mana spread penuh boleh menguntungkan apabila sempit, kerana taker yang menyeberangi membayar 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 ke dalam kos maker sendiri. Mengelirukan kedua-duanya akan mengira dua kali yuran pergi-balik di dalam satu separuh-spread.
2. Kos Memegang Inventori ()
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:
di mana ialah volatiliti aset dan 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 ()
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:
di mana 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:
dengan , , dan 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

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 mengikut random walk:
Harga transaksi yang diperhati melantun antara bid dan ask:
di mana dengan kebarangkalian sama (iaitu, belian dan jualan adalah sama berkemungkinan). Perubahan harga ialah:
Mengira autokovarians tertib pertama:
Model ini diterbitkan dalam unit harga: 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 :
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:
di mana 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 — memperoleh semula daripada perubahan harga. Varian berasaskan pulangan akan mengembalikan , 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

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 | Tekanan berarah | |
| Mid-price berwajaran | Nilai saksama jangka pendek | |
| Nisbah kedalaman (paras 1-5) | Bekalan/permintaan pelbagai paras | |
| Tekanan buku | Tekanan berwajaran jarak | |
| Nisbah spread / tick | Keketatan relatif kepada minimum |
Tekanan buku di sini menggunakan pereputan jarak-mutlak-ke-mid, , 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 (yang negatif pada sisi bid, positif pada sisi ask, dan meletup apabila satu paras menghampiri mid). Pilih daripada kedalaman buku tipikal, atau gantikan eksponen dengan sebarang wajaran positif yang menurun dalam jarak.
Ciri Trade Flow (Adverse Selection)
| Ciri | Formula | Mewakili |
|---|---|---|
| Ketidakseimbangan dagangan | Aliran bermaklumat bersih | |
| VPIN | Kebarangkalian disegerakkan-volum bagi dagangan bermaklumat | Ketoksikan |
| Kyle's lambda | Regresi pada volum bertanda | Price impact per unit |
| Kekerapan dagangan besar | Bilangan dagangan dalam tetingkap | Aktiviti institusi |
Ciri Volatiliti (Kos Inventori)
| Ciri | Formula | Mewakili |
|---|---|---|
| Realized volatility | Risiko jangka pendek | |
| Vol Garman-Klass | Vol berasaskan julat | |
| Vol-of-vol | Sisihan piawai rolling bagi | Ketidakpastian rejim |
| Autokorelasi pulangan | Momentum / mean-reversion |
Ciri Rejim Pasaran
| Ciri | Penerangan | Mewakili |
|---|---|---|
| Pengekodan masa-dalam-hari | 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 saat seterusnya:
Dalam amalan, kita menganggarkan ini dengan purata spread berwajaran volum sepanjang snapshot seterusnya:
Sasaran ini ialah forward window berpanjang (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:
- Spread tertangguh () 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).
- Realized volatility ialah yang kedua paling penting — volatiliti intrahari dan spread berkorelasi positif kuat, baik secara sezaman mahupun dinamik.
- Ketidakseimbangan buku paling penting semasa tempoh meruap — ia menandakan pergerakan berarah yang bakal berlaku.
- Ketidakseimbangan trade flow menangkap adverse selection jangka pendek — letusan aliran satu sisi meramalkan pelebaran spread.
- 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_samplestinggi (100+) untuk menghalang model daripada memuatkan kepada bunyi mikrostruktur dalam snapshot individu. - Guna
subsample < 1.0untuk 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 :
- = bilangan langkah masa (cth., 100 snapshot)
- = bilangan paras harga (cth., 10 bid + 10 ask = 20)
- = 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
-
Penormalan: Normalkan harga sebagai ofset daripada midprice (dalam tick atau bps). Normalkan volum mengikut min rolling mereka. Harga dan volum mentah menyebabkan ketidakstabilan latihan.
-
Fungsi kerugian: Guna Huber loss () untuk mengendalikan lonjakan spread:
-
Pensampelan tetingkap dan kebocoran: Guna tetingkap tidak bertindih untuk latihan, dan — tepat seperti dalam saluran paip gradient-boosting — purge/embargo jurang sekurang-kurangnya
horizonsnapshot 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. -
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

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 (), 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

Metrik Ramalan Spread
Melangkaui metrik regresi standard (MAE, ), 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:
- Pada setiap cap masa, ramalkan spread
- Kira sebut harga menggunakan spread yang diramalkan + inventory skew
- Simulasikan pengisian terhadap dagangan sejarah
- 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:
- Guna penguraian Huang-Stoll luar talian untuk memahami komponen spread anda dan menentukur had risiko
- Guna penganggar Roll sebagai pemeriksaan kewarasan dan untuk instrumen di mana anda kekurangan data order book
- Deploy model LightGBM untuk ramalan spread masa nyata — ia pantas, boleh ditafsir, dan teguh — dengan pengesahan purged walk-forward dan penanda aras AR/EWMA
- 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.
Pengarang
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.