Pemodelan dan Prediksi Bid-Ask Spread dengan Machine Learning
Bid-ask spread adalah satu-satunya variabel terpenting yang dikendalikan oleh seorang market maker. Atur terlalu lebar dan Anda kehilangan arus order ke kompetitor. Atur terlalu sempit dan adverse selection akan melahap inventory Anda hidup-hidup. Teori microstructure tradisional memberi kita dekomposisi spread yang elegan ke dalam komponen-komponen ekonominya. Machine learning memberi kita alat untuk memprediksi bagaimana komponen-komponen itu bergeser secara real time. Tulisan ini menjembatani kedua dunia tersebut: kita mulai dari teori klasik, membangun hingga estimator spread implisit Roll, lalu beralih ke gradient boosting dan model deep learning yang memprediksi spread dari fitur order book. Sepanjang jalan kita menandai jebakan satuan, kebocoran data (leakage), dan benchmarking yang diam-diam membuat model spread tidak valid dalam praktik.
Mengapa Spread Penting bagi Market Maker
Seorang market maker terus-menerus memberikan kuotasi harga bid dan harga ask . Quoted spread adalah:
Setiap round-trip (membeli pada bid si maker, menjual pada ask si maker, keduanya diisi oleh taker) mentransfer hingga dari para taker ke si maker — secara teori. Dalam praktik, si maker mendapat kurang dari karena adverse selection: sebagian taker memiliki informasi dan melakukan trading tepat sebelum harga bergerak melawan si maker. Profit terealisasi per round-trip adalah realized spread, yang sama dengan effective spread dikurangi price impact:
Kita mengukur ketiga besaran tersebut atas dasar full-spread yang sama (bukan setengah), sehingga identitasnya konsisten secara dimensional. Effective spread untuk satu trade tunggal adalah:
Di sini adalah arah trade (taker membeli atau menjual), adalah harga transaksi, dan adalah titik tengah dari best bid dan ask pada saat trade terjadi. Suku price-impact didefinisikan secara simetris atas horizon pasca-trade :
di mana adalah mid pada horizon setelah trade. Horizon harus dinyatakan secara eksplisit — pilihan umum adalah 5 menit pada ekuitas dan 30 detik pada kripto, di mana harga melakukan repricing lebih cepat. Mengurangkan impact dari effective spread menyisakan realized spread: apa yang dipertahankan si maker setelah pasar bergerak.
Seorang market maker yang dapat memprediksi spread — beserta komponen-komponennya — dalam 1, 5, atau 60 detik berikutnya dapat menyesuaikan kuotasi secara dinamis untuk memaksimalkan realized spread sambil mempertahankan fill rate.
Tiga Komponen Spread

Literatur market microstructure (Stoll 1978, Glosten dan Milgrom 1985, Huang dan Stoll 1997) mendekomposisi bid-ask spread menjadi tiga komponen ekonomi.
1. Order Processing Cost ()
Ini adalah biaya menyediakan layanan market-making per sisi yang terisi: fee yang benar-benar dibayar si maker ditambah infrastruktur teknologi, kepatuhan regulasi, dan opportunity cost dari modal yang dikerahkan. Demsetz (1968) dan Tinic (1972) adalah yang pertama memformalkan komponen ini.
Perbedaan kuncinya adalah siapa membayar fee yang mana. Seorang maker yang memberikan kuotasi secara pasif membayar maker fee atas fill-nya sendiri — dan di banyak venue adalah rebate, yaitu negatif. Ia tidak membayar taker fee atas fill pasif tersebut; pihak lawan yang melintasi spread itulah yang membayar . Jadi order-processing cost per sisi si maker adalah:
di mana bertanda (sebuah rebate menurunkan dan dapat membuatnya negatif) dan mencakup konektivitas, colocation, dan compute. Di pasar elektronik modern komponen ini telah menyusut drastis — sub-penny pada ekuitas, beberapa basis point atau rebate bersih pada kripto.
Taker fee penting karena alasan yang berbeda: ia menetapkan batas bawah seberapa sempit spread penuh bisa menguntungkan, karena seorang taker yang melintas membayar di atas spread. Jika Anda menginginkan batas bawah spread yang menjaga kuotasi Anda tetap menarik secara ekonomi relatif terhadap biaya taker tersebut, motivasikan secara terpisah alih-alih melipat ke dalam biaya si maker sendiri. Mencampuradukkan keduanya menghitung-ganda fee round-trip di dalam satu setengah-spread.
2. Inventory Holding Cost ()
Ketika seorang market maker mengakumulasi posisi terarah (long atau short), ia menanggung risiko harga. Komponen inventory mengompensasi risiko ini. Stoll (1978) dan Amihud dan Mendelson (1980) memodelkannya sebagai fungsi dari volatilitas dan inventory si maker saat ini:
di mana adalah volatilitas aset dan adalah inventory si maker saat ini. Seiring bertumbuhnya inventory, si maker melebarkan spread pada sisi di mana ia terekspos dan menyempitkannya pada sisi lain, sebuah teknik yang disebut inventory skewing.
3. Adverse Selection Cost ()
Ini adalah komponen yang paling berbahaya. Trader berinformasi — mereka yang memiliki informasi superior tentang pergerakan harga yang akan segera terjadi — secara sistematis mengambil keuntungan dari kuotasi usang. Adverse selection cost sama dengan ekspektasi kerugian per trade kepada pihak lawan yang berinformasi. Copeland dan Galai (1983) memodelkannya sebagai nilai opsi gratis yang diberikan si maker kepada trader berinformasi. Glosten dan Milgrom (1985) memformalkannya sebagai revisi Bayesian dalam keyakinan si maker setelah mengamati sebuah trade:
di mana adalah nilai fundamental sebenarnya. Di pasar likuid, adverse selection dapat menyumbang 30-60% dari total spread.
Dekomposisi Penuh
Quoted half-spread dapat ditulis sebagai:
dengan , , dan semuanya dinyatakan sebagai biaya per-sisi (half-spread) — itulah yang menjaga akuntansinya tetap konsisten. Huang dan Stoll (1997) mengusulkan metode ekonometrik untuk mengestimasi komponen-komponen ini dari data trade dan quote. Wawasan kuncinya: order processing cost menciptakan batas bawah spread yang tetap, inventory cost menciptakan spread yang bervariasi terhadap posisi dan volatilitas, dan adverse selection cost menciptakan spread yang bervariasi terhadap asimetri informasi.
Model Spread Implisit Roll

Sebelum data frekuensi tinggi tersedia secara luas, Richard Roll (1984) mengusulkan metode elegan untuk mengestimasi effective spread hanya menggunakan harga transaksi. Wawasannya: di pasar yang efisien, bid-ask bounce menginduksi serial covariance negatif pada perubahan harga, bahkan ketika tidak ada informasi baru.
Model
Asumsikan nilai fundamental mengikuti random walk:
Harga transaksi yang teramati memantul antara bid dan ask:
di mana dengan probabilitas sama (yaitu, beli dan jual sama-sama mungkin). Perubahan harga adalah:
Menghitung autokovarians orde-pertama:
Model ini diturunkan dalam satuan harga: keluar dari autokovarians perubahan harga, bukan return. Perbedaan ini adalah kesalahan implementasi yang paling umum, dan kita menjaga kode di bawah agar setia terhadapnya.
Estimator Roll
Menyelesaikan untuk :
Ketika autokovarians sampel positif (yang sering terjadi dalam praktik karena noise atau momentum), estimator menjadi tak terdefinisi. Perbaikan umum adalah menetapkan estimasi ke nol atau menggunakan akar bertanda:
di mana adalah autokovarians orde-pertama sampel.
Implementasi dalam Python
Estimator mengembalikan spread dalam satuan harga. Untuk menyatakannya dalam basis point kita membaginya dengan midprice satu kali — karena, tidak seperti estimator ruang-return, ia belum dibagi 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 kewajaran cepat pada deret simulasi — random walk fundamental di dekat harga 100 dengan spread sebenarnya — memulihkan dari perubahan harga. Varian berbasis return akan mengembalikan , meleset sebesar price level, dan kemudian membagi itu dengan midprice lagi untuk mendapatkan bps memperparah kesalahan tersebut. Jika Anda lebih menyukai estimator ruang-return, turunkan model dalam ruang log-price dan hilangkan pembagian kedua dengan midprice; pilih satu konvensi dan buat kode cocok dengan matematikanya.
Keterbatasan Model Roll
Model Roll mengasumsikan: (1) efisiensi pasar, (2) tidak ada asimetri informasi, (3) arah trade i.i.d., dan (4) spread konstan. Semua ini dilanggar dalam praktik. Harris (1990) menunjukkan bahwa estimator ini bias berat akibat ketidaksamaan Jensen ketika diterapkan pada data yang berisik (noisy). Terlepas dari keterbatasan ini, estimator Roll tetap berguna sebagai baseline cepat dan banyak digunakan dalam riset empiris keuangan.
Fitur ML untuk Prediksi Spread

Untuk melangkah melampaui model statis, kita membutuhkan fitur yang menangkap pendorong dinamis dari variasi spread. Berikut adalah taksonomi fitur yang ditata berdasarkan komponen spread yang diwakilinya.
Fitur Order Book (Inventory & Adverse Selection)
| Fitur | Formula | Mewakili |
|---|---|---|
| Book imbalance | Tekanan terarah | |
| Weighted mid-price | Nilai wajar jangka pendek | |
| Depth ratio (level 1-5) | Pasokan/permintaan multi-level | |
| Book pressure | Tekanan terbobot jarak | |
| Spread / tick ratio | Kerapatan relatif terhadap minimum |
Book pressure di sini menggunakan peluruhan jarak-ke-mid absolut, , sehingga volume yang berada dekat touch dihitung lebih besar daripada volume dalam, dan kedua sisi dibobot oleh fungsi positif yang menurun. Ini menghindari bias tanda struktural akibat membagi volume dengan jarak bertanda (yang negatif pada sisi bid, positif pada sisi ask, dan meledak saat sebuah level mendekati mid). Pilih dari kedalaman book yang tipikal, atau ganti eksponensial dengan bobot positif apa pun yang menurun terhadap jarak.
Fitur Trade Flow (Adverse Selection)
| Fitur | Formula | Mewakili |
|---|---|---|
| Trade imbalance | Arus berinformasi bersih | |
| VPIN | Volume-synchronized probability of informed trading | Toksisitas |
| Kyle's lambda | Regresi terhadap signed volume | Price impact per unit |
| Frekuensi trade besar | Jumlah trade dalam window | Aktivitas institusional |
Fitur Volatilitas (Inventory Cost)
| Fitur | Formula | Mewakili |
|---|---|---|
| Realized volatility | Risiko jangka pendek | |
| Garman-Klass vol | Vol berbasis range | |
| Vol-of-vol | Rolling std dari | Ketidakpastian rezim |
| Autokorelasi return | Momentum / mean-reversion |
Fitur Rezim Pasar
| Fitur | Deskripsi | Mewakili |
|---|---|---|
| Pengkodean waktu-dalam-hari | Musiman intraday | |
| Detik sejak trade terakhir | Jeda waktu | Tingkat aktivitas |
| Korelasi lintas-aset | Rolling corr dengan indeks/BTC | Risiko sistematik |
| Funding rate (kripto) | Funding rate perp | Penempatan dengan leverage |
Gradient Boosting untuk Prediksi Spread
Gradient boosted tree (XGBoost, LightGBM, CatBoost) adalah tulang punggung prediksi tabular dalam keuangan kuantitatif. Mereka menangani tipe fitur campuran, menangkap interaksi nonlinier, membutuhkan preprocessing minimal, dan berlatih cepat pada jutaan baris — asalkan pembangunan fitur itu sendiri tervektorisasi (lihat catatan autokorelasi di bawah).
Perumusan Masalah
Kita merumuskan prediksi spread sebagai tugas regresi. Targetnya adalah rata-rata quoted spread terbobot-waktu selama detik berikutnya:
Dalam praktik, kita mendekatinya dengan rata-rata spread terbobot-volume selama snapshot berikutnya:
Target ini adalah forward window sepanjang (atau horizon), yang berarti baris-baris yang bersebelahan berbagi window masa depan yang tumpang tindih. Tumpang tindih itu membocorkan informasi melintasi split train/validation yang naif — kita menanganinya secara eksplisit dalam kode pelatihan di bawah.
Pipeline Lengkap
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
Detail yang krusial adalah gap purge/embargo. Target forward-mean berarti baris-baris yang bersebelahan tumpang tindih hingga horizon snapshot, sehingga TimeSeriesSplit polos membiarkan baris validation berbagi window masa depan dengan baris pelatihan — membocorkan jawabannya dan menggelembungkan R² validation. Membuang gap minimal horizon baris di kedua sisi setiap batas fold (purged k-fold gaya Lopez de Prado) menghilangkan bias tersebut. Ini berlaku sama untuk pipeline gradient-boosting maupun deep learning, meskipun kebocoran ini lebih sering dibahas untuk model sekuens.
Analisis Feature Importance
Salah satu keunggulan utama model berbasis tree adalah interpretabilitas. Setelah pelatihan, periksa nilai SHAP untuk memahami fitur mana yang mendorong prediksi spread:
import shap
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_val)
shap.summary_plot(shap_values, X_val, max_display=15)
Temuan tipikal lintas kelas aset:
- Lagged spread () hampir selalu merupakan fitur terpenting — spread sangat berautokorelasi. Inilah juga sebabnya R² utama tampak tinggi: sebagian besar skor hanyalah persistensi, jadi selalu benchmark terhadap baseline AR/EWMA (lebih lanjut di bawah).
- Realized volatility adalah yang kedua terpenting — volatilitas intraday dan spread berkorelasi positif kuat, baik secara serentak maupun dinamis.
- Book imbalance paling penting selama periode bergejolak — ia memberi sinyal pergerakan terarah yang akan segera terjadi.
- Trade flow imbalance menangkap adverse selection jangka pendek — ledakan arus satu sisi memprediksi pelebaran spread.
- Waktu-dalam-hari menangkap pola intraday berbentuk U (lebih lebar pada pembukaan/penutupan, lebih sempit di tengah hari).
Pertimbangan Hyperparameter
Khusus untuk prediksi spread:
- Gunakan MAE atau Huber loss alih-alih MSE. Distribusi spread condong ke kanan dengan outlier ekstrem sesekali (selama peristiwa berita). MAE lebih robust.
- Atur
min_child_samplestinggi (100+) untuk mencegah model menyesuaikan diri dengan noise microstructure pada masing-masing snapshot. - Gunakan
subsample < 1.0untuk mendekorelasi tree dan meningkatkan generalisasi lintas rezim volatilitas yang berbeda.
Pendekatan Deep Learning
Sementara gradient boosting unggul pada fitur tabular, deep learning dapat mempelajari representasi langsung dari data order book mentah. Dua arsitektur telah terbukti efektif untuk tugas prediksi terkait spread.
Arsitektur 1: CNN-LSTM untuk Snapshot Order Book
Arsitektur DeepLOB (Zhang dkk. 2019) menggunakan konvolusi kernel-kecil bertumpuk — dan sebuah modul Inception — untuk mengekstrak pola spasial lintas level order book sambil mempertahankan struktur spasial tersebut, diikuti oleh lapisan LSTM untuk memodelkan dependensi temporal. Pilihan desain yang penting adalah tidak melakukan global-pool pada sumbu level sebelum lapisan rekuren: melakukannya meruntuhkan persis struktur lintas-level yang seharusnya ditangkap oleh konvolusi.
Untuk prediksi spread, masukannya adalah tensor berbentuk :
- = jumlah langkah waktu (mis., 100 snapshot)
- = jumlah level harga (mis., 10 bid + 10 ask = 20)
- = fitur per level (harga, volume, jumlah order)
Model di bawah mempertahankan feature map konvolusional atas level dan meratakannya (flatten) ke dalam masukan LSTM (input_size = 16 * L), alih-alih merata-ratakan dimensi level menjadi 16 channel mean:
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 menginginkan pooling pada sumbu level untuk mengendalikan jumlah parameter, gunakan pooling ber-stride atau pooling yang dipelajari yang mempertahankan lebih dari satu posisi — bukan AdaptiveAvgPool1d(1), yang merata-ratakan setiap level menjadi satu angka dan membuang sinyal spasial.
Arsitektur 2: Transformer Encoder
Transformer dapat menangkap dependensi jarak-jauh dalam sekuens order book tanpa hambatan sekuensial LSTM. Untuk prediksi spread, sebuah transformer encoder ringan bekerja 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 Pelatihan
-
Normalisasi: Normalisasikan harga sebagai offset dari midprice (dalam tick atau bps). Normalisasikan volume dengan rolling mean-nya. Harga dan volume mentah menyebabkan ketidakstabilan pelatihan.
-
Fungsi loss: Gunakan Huber loss () untuk menangani lonjakan spread:
-
Pengambilan sampel window dan kebocoran: Gunakan window yang tidak tumpang tindih untuk pelatihan, dan — persis seperti pada pipeline gradient-boosting — purge/embargo gap minimal
horizonsnapshot antara train dan validation. Baik target forward-mean maupun window masukan yang tumpang tindih membocorkan informasi masa depan melintasi batas split dan menggelembungkan performa yang tampak. -
Adaptasi online: Di produksi, secara berkala fine-tune model pada data terkini (1-2 jam terakhir) dengan learning rate kecil. Market microstructure berubah dalam sehari, dan model yang dilatih pada data pagi mungkin berkinerja buruk pada sore hari.
Kapan Menggunakan Deep Learning vs. Gradient Boosting
| Kriteria | Gradient Boosting | Deep Learning |
|---|---|---|
| Tipe masukan | Fitur tabular | Sekuens order book mentah |
| Ukuran data pelatihan | Bekerja dengan 100K+ baris | Butuh 1M+ baris |
| Feature engineering | Manual (upaya tinggi, kendali tinggi) | Dipelajari (upaya lebih rendah, kurang dapat diinterpretasi) |
| Latensi inferensi | µs satu digit dengan prediktor terkompilasi; puluhan µs dari Python | Ratusan µs pada GPU |
| Interpretabilitas | Tinggi (SHAP) | Rendah (attention map) |
| Adaptasi rezim | Latih ulang / pembaruan online | Fine-tune pada data terkini |
| Keterampilan spread horizon-pendek | Secara luas setara dengan DL | Keunggulan tumbuh pada horizon lebih panjang / data lebih besar |
Kita sengaja menghindari mengutip angka R² spesifik: akurasi peramalan spread sangat bergantung pada horizon, aset, dan seberapa banyak skornya semata-mata adalah autokorelasi spread. Sebuah model dapat mencetak R² mentah yang mengesankan sambil menambahkan hampir tidak ada apa pun di atas EWMA satu baris. Laporkan keterampilan di atas baseline AR/EWMA pada data yang sama, dengan horizon yang dinyatakan, alih-alih R² utama. Demikian pula, perlakukan angka latensi sebagai bergantung pada implementasi: model LightGBM 2000 putaran, 63 leaf memprediksi satu baris dalam puluhan mikrodetik dari Python dan hanya mencapai beberapa mikrodetik dengan prediktor terkompilasi/C++.
Dalam praktik, banyak sistem produksi menggunakan pendekatan dua tahap: model gradient boosting cepat untuk kuotasi real-time (kritis-latensi), dan model deep learning yang berjalan secara asinkron untuk menyesuaikan parameter model boosting atau memberikan sinyal sekunder.
Dari Prediksi ke Kuotasi

Prediksi spread hanya berharga jika ia diterjemahkan menjadi kuotasi yang lebih baik. Berikut adalah aturan kuotasi yang disederhanakan yang menggunakan spread yang diprediksi:
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
Ketika inventory long (), skew menurunkan baik bid maupun ask. Dari satu perspektif yang konsisten: ask yang lebih rendah membuat para taker lebih murah membeli dari kita, yang melepaskan inventory long kita; bid yang lebih rendah membuat kita lebih kecil kemungkinannya dihantam oleh penjual, memperlambat akumulasi lebih lanjut. Spread yang diprediksi mengendalikan lebar keseluruhan — melebar ketika model mengantisipasi volatilitas atau adverse selection, menyempit ketika kondisi tenang.
Evaluasi dan Backtesting

Metrik Prediksi Spread
Melampaui metrik regresi standar (MAE, ), evaluasi prediksi spread dengan metrik yang penting bagi market making:
- Keterampilan di atas baseline: Selalu laporkan MAE/R² relatif terhadap peramalan AR(1) atau EWMA dari spread terkini. Karena spread sangat persisten, skor absolut didominasi oleh autokorelasi; hanya peningkatan di atas baseline trivial yang mencerminkan kandungan prediktif sebenarnya.
- Akurasi terarah: Apakah model dengan benar memprediksi apakah spread akan melebar atau menyempit? Sebuah model dengan MAE biasa-biasa saja tetapi akurasi terarah tinggi tetap dapat menguntungkan.
- Cakupan ekor (tail): Apakah model memprediksi lonjakan spread? Hitung MAE secara terpisah untuk 5% nilai spread teratas — di sinilah kerugian adverse selection terkonsentrasi.
- Kalibrasi: Plot kuantil spread prediksi vs. realisasi. Prediksi persentil ke-90 dari model yang terkalibrasi baik seharusnya cocok dengan persentil ke-90 dari spread yang terealisasi.
Evaluasi Berbasis PnL
Pada akhirnya, satu-satunya metrik yang penting adalah PnL terealisasi. Backtest seluruh loop:
- Pada setiap timestamp, prediksi spread
- Hitung kuotasi menggunakan spread yang diprediksi + inventory skew
- Simulasikan fill terhadap trade historis
- Lacak inventory, PnL terealisasi, dan rasio Sharpe
Bandingkan terhadap baseline: (a) spread konstan (median deret waktu), (b) EWMA dari spread terkini, dan (c) estimator Roll.
Kesimpulan
Pemodelan spread berada di persimpangan teori keuangan dan ML terapan. Dekomposisi klasik ke dalam biaya order processing, inventory, dan adverse selection memberikan intuisi ekonomi tentang mengapa spread bervariasi. Model Roll memberikan estimator baseline yang elegan dari data minimal — selama Anda menghitungnya dalam satuan harga. Model gradient boosting mengubah fitur microstructure menjadi peramalan spread horizon-pendek yang akurat dengan inferensi latensi-rendah. Arsitektur deep learning mempelajari langsung dari data order book mentah, menangkap pola yang mungkin terlewat oleh fitur buatan tangan — asalkan arsitekturnya mempertahankan struktur lintas-level alih-alih menggumpalkannya pergi melalui pooling.
Untuk sistem market-making di produksi, rekomendasi praktis bersifat berlapis:
- Gunakan dekomposisi Huang-Stoll secara offline untuk memahami komponen spread Anda dan mengalibrasi limit risiko
- Gunakan estimator Roll sebagai pemeriksaan kewajaran dan untuk instrumen di mana Anda tidak memiliki data order book
- Terapkan model LightGBM untuk prediksi spread real-time — ia cepat, dapat diinterpretasi, dan robust — dengan validasi purged walk-forward dan benchmark AR/EWMA
- Jalankan model CNN-LSTM atau Transformer dalam loop sekunder untuk mendeteksi perubahan rezim dan menyesuaikan model utama
Spread bukanlah sebuah angka — ia adalah sebuah sinyal. Semakin baik Anda memodelkannya (dan semakin jujur Anda mengukur model itu), semakin presisi Anda dapat memberi harga pada penyediaan likuiditas.
Tulisan ini adalah bagian dari seri marketmaker.cc tentang algorithmic market making dan microstructure.
Penulis
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.