Curve di slippage, non costanti di slippage: modelli di costo che sopravvivono al contatto con il trading live
La riga più costosa nella maggior parte dei backtest è slippage_bps = 5. Non perché 5 sia il numero sbagliato — mediato su un anno di fill potrebbe anche essere corretto — ma perché una costante è sbagliata condizionalmente, e il tuo processo di selezione delle strategie è una macchina fatta apposta per trovare e sfruttare errori condizionali. Lo slippage è una funzione della dimensione dell'ordine, dello spread, della volatilità e della liquidità disponibile. Una costante è quella funzione valutata in un punto ed estrapolata ovunque, incluso l'orario delle 3 di notte durante una cascata di liquidazioni, proprio quando il tuo segnale di mean reversion scatta più forte e il book ha un decimo della sua profondità abituale. Il backtest addebita 5 bps anche lì. Il trading live no.
Questo articolo riguarda il modello di costo dentro il loop del backtest: la funzione cost(size, market_state) che viene sottratta da ogni trade simulato. Deliberatamente non tratta la previsione ex ante dell'impatto per pianificare un'esecuzione — quello è il problema complementare, coperto dal lato del modello in price impact modeling e dal lato della pianificazione ottimale in Almgren-Chriss. Qui la domanda è più circoscritta e, per chi seleziona strategie tramite backtest, più urgente: quale funzione dovrebbe stare al posto della tua costante, come la stimi a partire dai dati che hai realmente a disposizione, e quanto cambia la risposta la strategia che finisci per eseguire. Quest'ultimo punto ha una risposta empirica netta: riclassifica 20 varianti di una stessa famiglia di strategie sotto quattro modelli di costo e osserva la classifica ribaltarsi.
La scala dei modelli di costo

Come la scala di simulazione dei fill, i modelli di costo formano una scala di fedeltà in cui ogni gradino richiede più dati e rimuove un bias sistematico specifico. I gradini sono cumulativi: ognuno mantiene i termini di quello sottostante e ne aggiunge uno.
M0: bps costanti
Un numero, applicato a ogni trade. I tre modi di fallire, in ordine crescente di danno:
- Cieco alla dimensione. Un clip da 10.000$ e un parent order da 5 milioni di dollari pagano lo stesso. Qualsiasi confronto tra strategie a livelli di capacità diversi diventa privo di significato.
- Cieco al regime. Un tranquillo martedì pomeriggio e il crollo di FTX pagano lo stesso. L'errore non è rumore; è anti-correlato esattamente con gli stati in cui le strategie event-driven concentrano il proprio trading.
- Tossico per la selezione. Quando esplori 200 combinazioni di parametri, l'ottimizzatore scopre che sotto un costo piatto il turnover è economico rispetto alla realtà e sposta l'intera ricerca verso varianti ad alta frequenza. L'errore di costo diventa una forza direzionale sulla selezione del modello, non un rumore simmetrico sul PnL.
M1: proporzionale allo spread
Addebita l'half-spread misurato al timestamp della decisione (più la fee, che è deterministica e dovrebbe sempre essere una voce a parte). Questo è il primo modello che risponde in qualche modo al mercato: gli spread si allargano alle notizie, alle aperture di sessione, sulle altcoin sottili, quindi il costo ora si muove insieme ad almeno una variabile di liquidità. Ciò che ancora manca: lo spread prezza il fill di un solo clip a top-of-book. Qualunque size più grande "cammina" nel book, e quel percorso è invisibile a .
M2: scalato per la volatilità
Aggiungi un termine proporzionale alla volatilità a breve orizzonte. La giustificazione è sia empirica che strutturale: spread e profondità sono fissati dai market maker il cui rischio di adverse selection e di inventario scala con (la logica di quotazione di Avellaneda-Stoikov letta al contrario), quindi il costo della domanda di liquidità è naturalmente denominato in unità di volatilità. Quando non hai affatto dati di book — solo barre giornaliere, storico lungo — con intorno a 0,05–0,1 della vol giornaliera è già un modello nettamente migliore di qualsiasi costante, perché almeno respira insieme al regime.
M3: square-root nella partecipazione
Il termine di dimensione. è la size del tuo ordine, il volume di mercato nella finestra rilevante, la volatilità nella stessa finestra. Questa forma funzionale non è una comodità; è uno dei risultati empirici più replicati nella microstruttura di mercato, e vale la pena essere precisi su dove nasce.
Il punto di partenza teorico è Kyle (1985), "Continuous auctions and insider trading" (Econometrica 53(6), 1315–1335). Nel modello di Kyle un market maker fissa il prezzo come funzione lineare del flusso d'ordini netto, , con
— impatto per unità di flusso proporzionale all'incertezza sul valore rispetto al volume di noise trading. Il lambda di Kyle ha dato al campo la sua unità di liquidità (la profondità è ) e la sua prima affermazione verificabile: il costo per dollaro scambiato dipende dal rapporto tra informazione e liquidità, non da una costante. Ma il modello è lineare nella dimensione, e i dati dicono altro.
I dati dicono concavo. Almgren, Thum, Hauptmann and Li (2005), "Direct estimation of equity market impact" (Risk, luglio 2005), hanno stimato circa 700.000 ordini azionari statunitensi dai desk di Citigroup e hanno trovato l'impatto temporaneo crescere come la partecipazione elevata alla potenza — hanno esplicitamente testato e respinto sia la forma lineare sia la square-root pura a favore di — mentre l'impatto permanente è risultato pari a , statisticamente indistinguibile dal lineare (il che è rassicurante, dato che Huberman and Stanzl (2004) hanno mostrato che un impatto permanente non lineare ammette manipolazione). Tóth et al. (2011), "Anomalous price impact and the critical nature of liquidity in financial markets" (Physical Review X 1, 021006), usando quasi 500.000 metaorder di Capital Fund Management sui mercati future, hanno trovato con dell'ordine di uno — e hanno fornito la spiegazione ormai standard: il book visibile è una frazione evanescente della liquidità latente, che è localmente lineare nel prezzo intorno al mid, il che fa sì che il costo di consumarla scali come radice quadrata. Lavori successivi rifiniscono i bordi: Zarinelli, Treccani, Farmer and Lillo (2015), "Beyond the square root" (Market Microstructure and Liquidity 1(2)), hanno mostrato su sette milioni di metaorder istituzionali ANcerno che la radice quadrata si adatta bene su circa due decadi di dimensione dell'ordine ma una forma logaritmica si estende su cinque; Bucci et al. (2019, Physical Review Letters 122, 108302) hanno documentato il passaggio da un impatto lineare (alla Kyle) a bassissima partecipazione a uno square-root al di sopra. E Frazzini, Israel and Moskowitz (2018), "Trading Costs" (SSRN 3229719), usando 1.700 miliardi di dollari di esecuzioni live di AQR su 19 anni, hanno trovato costi realizzati di un ordine di grandezza inferiori rispetto alle stime accademiche precedenti — un avvertimento sul fatto che coefficienti stimati sul flusso di altri, con urgenza e contenuto informativo diversi, non si trasferiscono automaticamente al tuo.
Per il loop di backtest, il riassunto pratico si riduce a tre affermazioni. L'impatto scala con la volatilità. L'impatto è concavo nella dimensione, con un esponente da trattare come finché i tuoi dati non suggeriscono . Il prefattore è dell'ordine di uno ma varia per venue, asset e — in modo cruciale — per il tuo stesso stile di trading, motivo per cui la prossima sezione riguarda il fitting piuttosto che la copia. La calibrazione approfondita (kernel di decadimento, scomposizione tra impatto transitorio e permanente, cross-impact) appartiene all'articolo sul modeling dell'impatto; il backtest ha bisogno solo della curva.
Un numero concreto, per fissare gli ordini di grandezza. BTC perp, vol giornaliera bps, volume giornaliero della venue miliardi di dollari, , :
| Dimensione ordine | Termine di impatto | + half-spread (0,5 bp) | ||
|---|---|---|---|---|
| $50k | 0.0008% | 0.0029 | 0.5 bp | 1.0 bp |
| $500k | 0.008% | 0.0091 | 1.7 bp | 2.2 bp |
| $5M | 0.083% | 0.0289 | 5.4 bp | 5.9 bp |
| $50M | 0.83% | 0.0913 | 17.1 bp | 17.6 bp |
Tre ordini di grandezza nella dimensione coprono un fattore di ~18 nel costo. Nessuna costante copre quella tabella, e ogni confronto tra strategie che fa variare la position size mantenendo slippage_bps fisso sta silenziosamente paragonando mele da 50k dollari ad arance da 5 milioni.
Il fitting della curva: i tuoi fill, o quelli di nessuno
Dai tuoi stessi fill
I dati giusti sono il tuo stesso record TCA: una riga per parent order con mid all'arrivo, VWAP di fill realizzato, dimensione dell'ordine e stato di mercato al momento della decisione — esattamente il dataset che la misurazione dell'implementation shortfall produce come sottoprodotto. Lo shortfall in bps rispetto all'arrivo, normalizzato per la volatilità contemporanea, regredito sulla partecipazione, ti dà direttamente e .
La trappola è il rumore. Lo shortfall di un singolo ordine è impatto più rumore di prezzo lungo la finestra di esecuzione, e il rumore domina: per un'esecuzione di 10 minuti a 250 bps di vol giornaliera, il termine di rumore è bps contro un segnale di impatto di forse 3 bps. Un rapporto segnale-rumore di 0,15 per osservazione è il motivo per cui Almgren et al. avevano bisogno di 700k ordini e per cui devi fare il fit su mediane di bucket, mai su punti grezzi:
import numpy as np
import pandas as pd
def fit_sqrt_curve(orders: pd.DataFrame) -> tuple[float, float]:
"""orders: one row per parent order.
is_bps -- implementation shortfall vs arrival mid, bps, cost-positive
sigma_bps -- volatility at arrival (same window used at simulation time)
q_over_v -- parent size / market volume over the execution window
Fits I = Y * sigma * (Q/V)^delta on participation-bucket medians."""
df = orders.query("q_over_v > 0 and sigma_bps > 0").copy()
df["i_norm"] = df.is_bps / df.sigma_bps # impact in vol units
df["bucket"] = pd.qcut(np.log10(df.q_over_v), 12, duplicates="drop")
b = (df.groupby("bucket", observed=True)
.agg(i=("i_norm", "median"), qv=("q_over_v", "median")))
b = b[b.i > 0] # noisy buckets can go negative; drop, don't clip
delta, log_y = np.polyfit(np.log(b.qv), np.log(b.i), 1)
return float(np.exp(log_y)), float(delta)
Su qualche migliaio di parent order crypto questo fit è già stabile entro circa ±30% su e ±0,1 su — grossolano, ma la differenza tra una curva stimata e una costante indovinata non è del 30%, è l'intera forma. Rifitta trimestralmente; la deriva di è essa stessa una diagnostica (un crescente a parità di partecipazione significa o che il regime di liquidità della venue è cambiato, o che il tuo order flow è diventato più informato/più rilevabile).
Cold start: nessun fill ancora
Prima del primo ordine live hai solo dati pubblici, e due opzioni oneste.
Depth-walk del book. Media il costo di attraversare gli snapshot L2 a ogni dimensione, esattamente come fa il simulatore di fill per singolo trade, ma aggregato in una curva. Questo è un limite inferiore con un segno noto: lo snapshot mostra la liquidità sopravvissuta al flusso di tutti gli altri, la dinamica di rifornimento è invisibile, e i tuoi stessi child order precedenti avranno spostato il book prima che arrivino quelli successivi.
def curve_from_book(snapshots, sizes_usd, safety=1.75):
"""snapshots: iterable of (mid, asks), asks = [(price, qty), ...] best-first.
Depth-walk cost is a LOWER bound on realized cost -- scale it."""
rows = []
for q_usd in sizes_usd:
costs = []
for mid, asks in snapshots:
rem, paid, got = q_usd / mid, 0.0, 0.0
for p, q in asks:
x = min(q, rem)
paid += x * (p - mid); got += x; rem -= x
if rem <= 0:
break
if rem <= 0:
costs.append(paid / got / mid * 1e4)
rows.append((q_usd, safety * float(np.median(costs))))
return pd.DataFrame(rows, columns=["q_usd", "cost_bps"])
Footprint dei trade pubblici. Raggruppa i trade aggressori per dimensione, misura il movimento del mid alcuni secondi dopo ciascuno, e ricava una curva di impatto empirica. Questo recupera la forma concava e all'incirca il giusto scaling in , ma il livello è distorto in una direzione che non puoi determinare a priori: i print pubblici sono decisioni di altri trader, quindi i print grandi sono condizionatamente informati (distorcendo l'impatto misurato verso l'alto rispetto al tuo ribilanciamento non informato) mentre i trader più sensibili alla liquidità frazionano gli ordini proprio per nascondersi (distorcendolo verso il basso). Usala per la forma, non per il livello.
In entrambi i casi, il protocollo di cold start è: prendi la curva da dati pubblici, applica un fattore di sicurezza di 1,5–2, tratta in piccolo, e comincia a popolare la tabella TCA che la sostituirà. La curva pubblica è un'impalcatura, non un muro portante.
Dipendenza dal regime: lo stesso ordine, cinque volte il costo

Ogni termine in dipende dallo stato, e in uno spike di volatilità si muovono tutti contro di te contemporaneamente. Gli spread sono proporzionali alla vol a breve orizzonte, quindi si allarga di 5–10 volte. entra direttamente nel termine di impatto. Anche il volume sale — il che ingenuamente riduce la partecipazione — ma sale meno di al numeratore, e la compensazione nasconde un effetto più insidioso: la profondità quotata vicino al mid crolla molto più rapidamente del volume scambiato, quindi la curva realizzata si irripidisce oltre quanto la formula stimata prevede dai soli input stressati.
Concretamente, con la curva stimata sopra, un ordine BTC da 3 milioni di dollari:
- Giornata calma: bps, spread 1 bp, miliardi di dollari. Costo bps.
- Giornata di crisi (livello LUNA, FTX): bps, spread 6 bps, miliardi di dollari. Costo bps.
La formula dice 3,3x. Il TCA realizzato nei giorni di stress si posiziona sistematicamente sopra la formula — il crollo di profondità che il termine sqrt non vede — ed è lì che entra in gioco un moltiplicatore di stress misurato. Nei nostri fill il residuo del bucket di crisi va da 1,4 a 1,8 volte sopra la formula con input stressati, portando il costo totale di stress a 4,5–6 volte il costo in condizioni calme per lo stesso ordine. Questa è l'origine onesta di "lo stesso ordine costa 5 volte tanto in uno spike di volatilità": circa 2x da al netto del volume, 2x dallo spread, e il resto dalla profondità che evapora più rapidamente di quanto le statistiche di volume ammettano.
L'implementazione è un modello di costo condizionale: fitta la curva per bucket di regime, o equivalentemente fitta una curva e una tabella di moltiplicatori indicizzata su un quantile di vol realizzata calcolato al momento della decisione (solo finestra a ritroso — un'etichetta di regime calcolata sull'intero campione è look-ahead contrabbandato attraverso il modello di costo):
| Regime (quantile vol 1h trailing) | Frequenza | Molt. spread | Molt. curva (misurato) |
|---|---|---|---|
| Quieto (< p25) | 25% | 0.6x | 0.7x |
| Normale (p25–p75) | 50% | 1.0x | 1.0x |
| Elevato (p75–p95) | 20% | 1.8x | 1.6x |
| Stress (> p95) | 5% | 5–10x | 3–6x |
Frazzini, Israel and Moskowitz (2018) documentano lo stesso fenomeno nei loro dati istituzionali: i costi realizzati si muovono fortemente insieme alla volatilità contemporanea, e un modello di costo senza un termine di vol variabile nel tempo prezza male esattamente le code. Il motivo per cui questa tabella conta più di quanto la sua riga di stress al 5% suggerisca è la correlazione: le strategie non campionano queste righe in modo uniforme. Un sistema di vol-breakout fa la maggior parte del suo trading nelle due righe in basso. Un market maker in mercato calmo guadagna nella riga in alto e viene stoppato attraverso quella in basso. Pesare i trade di ogni strategia per il proprio istogramma di regime — anziché addebitare a tutti la media incondizionata — è la singola correzione di PnL più grande di questa intera pagina per i sistemi event-driven, più significativa della differenza tra due gradini adiacenti della scala.
L'esperimento di sensibilità: quattro modelli di costo, una classifica ribaltata
Se il modello di costo cambiasse il PnL di un offset costante, nulla di tutto questo conterebbe per la selezione — ogni variante si sposterebbe ugualmente e l'argmax sopravvivrebbe. Non è così, perché la sensibilità al costo varia tra le varianti di un ordine di grandezza. L'esperimento che rende questo concreto:
Setup. Una famiglia di strategie — mean reversion su z-score e breakout Donchian su perpetual BTC ed ETH, barre da 1 minuto, 18 mesi — con una griglia sull'orizzonte di detenzione (15m / 1h / 4h / 24h) e sulla soglia di ingresso: 20 varianti, turnover unidirezionale da 0,4x a 11x il book al giorno, clip da 250.000$ eseguiti in 5 minuti. Ogni variante è testata una volta per ciascun modello di costo:
- M0: solo fee;
- M1: fee + 5 bps costanti;
- M2: fee + half-spread + ;
- M3: fee + curva stimata , con la tabella di moltiplicatori di regime sopra.
Stessi fill, stessi segnali, stesso percorso di codice — il modello di costo viene passato al backtester come argomento del costruttore, nello stesso modo in cui lo è qualsiasi parametro di prima classe:
from dataclasses import dataclass
@dataclass(frozen=True)
class SqrtCost:
y: float = 0.75 # fitted impact coefficient
delta: float = 0.5 # fitted exponent
fee_bps: float = 2.5 # taker fee, always separate
def cost_bps(self, q_usd: float, st) -> float:
part = q_usd / st.window_volume_usd
return (self.fee_bps + st.stress_mult *
(st.spread_bps / 2 + self.y * st.sigma_bps * part ** self.delta))
for model in [ZeroCost(), ConstCost(5.0), VolCost(0.08), SqrtCost()]:
for variant in grid:
results[(model, variant)] = backtest(variant, cost_model=model)
Risultati (run rappresentativo; rendimenti netti annualizzati):
| Variante | Turnover/giorno | M0 | M1 (5 bp) | M2 | M3 (curva) | Rank M1 → M3 |
|---|---|---|---|---|---|---|
| MR-15m, z=1.0 | 11.4x | +187% | +61% | +9% | −14% | 1 → 17 |
| MR-15m, z=1.5 | 7.9x | +141% | +52% | +15% | −2% | 2 → 13 |
| BO-1h, k=2 | 3.1x | +74% | +43% | +28% | +19% | 3 → 4 |
| MR-1h, z=2.0 | 2.4x | +58% | +38% | +25% | +21% | 4 → 2 |
| BO-4h, k=3 | 0.9x | +39% | +33% | +29% | +26% | 7 → 1 |

Correlazioni di rank (Kendall ) tra le classifiche: — una costante riordina a malapena qualcosa, è una tassa, non un modello. — sei delle prime dieci di M1 escono dalla top ten sotto M3, e il campione di M1 passa da +61% a negativo. — il solo termine di vol recupera la maggior parte del riordinamento; il termine di dimensione completa il lavoro per le varianti che scambiano volumi grandi rispetto al volume di finestra. Il meccanismo è esattamente la tossicità di selezione del gradino M0: 5 bps piatti sussidiano i trade che in realtà costano 15 bps (grandi, veloci, in stress) e sovratassano quelli che costano 2 (piccoli, pazienti, in calma), quindi classificare secondo M1 promuove sistematicamente le varianti i cui costi veri sono più sottostimati. L'ottimizzatore non ha trovato alpha; ha trovato il termine di errore del tuo modello di costo.
Questo è il motivo per cui il modello di costo appartiene alla stessa categoria mentale dello split walk-forward o della funzione obiettivo: un parametro di backtest di prima classe che fai variare, non una costante ambientale che fissi una volta per tutte. Se il rank di una variante è stabile tra M2 e M3, il suo edge è reale rispetto ai costi. Se il suo rank crolla salendo la scala, hai scoperto che era un artefatto del modello di costo — lo stesso evento epistemico di una strategia che muore out-of-sample nella walk-forward optimization, e merita la stessa risposta. E se hai selezionato l'argmax di 200 varianti sotto M1, la logica dello Sharpe deflazionato si applica con una variante: hai condotto una ricerca a test multipli in cui la statistica del test stesso era distorta a favore delle voci ad alto turnover.
Limiti onesti: il PnL come banda
L'ultima disciplina discende dall'ammettere che il modello di costo è incerto anche dopo il fitting. ha un intervallo di confidenza, i moltiplicatori di stress sono stimati da una manciata di giorni di coda, e il futuro estrae i propri regimi. Quindi smetti di riportare il PnL netto come un numero. Riportalo come una banda su scenari di costo denominati:
- Ottimistico: limite inferiore da depth-walk, nessun moltiplicatore di stress. Da non riportare mai da solo; il suo unico compito è delimitare il caso migliore.
- Realistico: curva stimata, moltiplicatori di regime stimati — la tua stima centrale.
- Pessimistico: , spread , riga di stress sempre applicata a un valore il suo p75. In sostanza: la curva stimata valutata al bordo sfavorevole dell'intervallo di confidenza di ogni parametro.
Due regole decisionali rendono la banda operativa. Prima: alloca sul pessimistico, riporta il realistico. Una strategia che sopravvive solo sotto il modello realistico è una scommessa sul fatto che il tuo fit dei costi non abbia errore nella direzione costosa — una scommessa che non hai preso consapevolmente. Seconda: traccia il rapporto di fragilità — l'ampiezza della banda sul PnL lordo. Dall'esperimento sopra: BO-4h a turnover 0,9x mostra lordo +39%, banda [+22%, +29%], fragilità 0,44. MR-15m a 11,4x mostra lordo +187%, banda [−31%, +34%] — fragilità 2,2, e il segno della strategia si ribalta dentro la banda. Quella seconda strategia non ha un segno determinabile al tuo attuale stato di conoscenza dei costi; le affermazioni oneste sono "non lo sappiamo" e "riduci il turnover finché non lo sapremo". Un backtest che stampa +34% e omette il bordo −31% della banda non è ottimistico, è infalsificabile.
La banda non costa nulla da produrre — altre tre voci nello sweep del modello di costo che stai già eseguendo comunque — e cambia le conversazioni. "Questa strategia rende il 20%" invita alla fiducia cieca. "Questa strategia rende l'11–19% nei nostri scenari di costo, e resta positiva sotto quello pessimistico" invita all'allocazione.
Dove si inserisce tutto questo
Nel motore di backtest, il modello di costo si colloca esattamente dove il simulatore di fill restituisce una quantità eseguita: i fill determinano cosa hai ottenuto, la curva di costo determina quanto l'hai pagato, e i gradini sopra L2 di quella scala (depth-walk, modelli di coda) assorbono progressivamente il modello di costo nel modello di fill stesso — la curva è il surrogato parametrico che usi quando non stai simulando direttamente il book. I parametri provengono dal tuo stesso record TCA quando ne hai uno, e da dati pubblici scalati per sicurezza quando non ce l'hai. La forma funzionale proviene da quarant'anni di teoria e misurazione — Kyle (1985) per spiegare perché il costo per dollaro dipenda affatto dalla liquidità, Almgren et al. (2005) e Tóth et al. (2011) per la legge concava vera e propria, con Almgren-Chriss come controparte di scheduling e i modelli di impatto neurali come frontiera predittiva. Ciò che questo articolo aggiunge è il workflow: stima la curva, condizionala al regime, falla variare come un parametro, e riporta la banda. Niente di tutto ciò è glamour. Tutto è più economico che scoprire, dal vivo, da che parte di slippage_bps = 5 la tua strategia si trovasse realmente.
Autori
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.