← Torna agli articoli
July 20, 2026
5 min di lettura

Implementation shortfall e TCA fai-da-te: misurare quanto costa davvero l'esecuzione

Implementation shortfall e TCA fai-da-te: misurare quanto costa davvero l'esecuzione
#esecuzione
#tca
#implementation-shortfall
#markouts
#costi-di-transazione
#algotrading
#backtest
#microstruttura-di-mercato

Ogni desk istituzionale ha una pipeline di transaction cost analysis. Quasi nessuno di chi gestisce un bot crypto ce l'ha. La configurazione tipica registra i fill, somma le commissioni e chiama "slippage" la differenza tra PnL di backtest e PnL live — un unico residuo inspiegato che assorbe latenza, spread, impatto, adverse selection e ogni ordine non eseguito che è scappato via. Non puoi correggere un costo che misuri come un numero singolo. Il meccanismo per misurarlo correttamente esiste dal 1988, non è proprietario, e sui tuoi log di fill sono circa 200 righe di Python. Questo articolo lo costruisce.

Il ritorno non è una dashboard più bella. Il tuo backtest contiene un cost model — una costante di slippage, una probabilità di fill, un coefficiente di impatto — e ogni parametro al suo interno è oggi una supposizione. La TCA sui fill live è l'unica ground truth su cui questi parametri possono essere calibrati. Abbiamo costruito il lato simulazione di questo loop in Simulazione dei fill: la scala dalla fantasia del prezzo di chiusura alla realtà consapevole della coda; questo articolo costruisce il lato misurazione.

Carta contro realtà: cosa misurava davvero Perold

Il trucco fondamentale si deve ad André Perold (1988, "The Implementation Shortfall: Paper Versus Reality," Journal of Portfolio Management 14(3), 4–9). Fai girare due portafogli in parallelo. Il portafoglio di carta esegue ogni decisione istantaneamente, in dimensione illimitata, a costo zero, al prezzo vigente nel momento della decisione. Il portafoglio reale è ciò che il tuo bot ha effettivamente fatto: fill parziali, quote inseguite, resti cancellati, commissioni. L'implementation shortfall è la differenza tra i loro rendimenti.

La definizione conta per ciò che rifiuta di nascondere. Un estratto conto commissioni mostra le commissioni. Un report fill-contro-prezzo-limite non mostra nulla (non riempi mai peggio del tuo limite — per costruzione). Il portafoglio di carta ti addebita tutto: il drift tra decisione e arrivo, lo spread attraversato, l'impatto causato e — punto cruciale — gli ordini che non sono mai stati eseguiti mentre il prezzo scappava via. Wagner ed Edwards (1993, "Best Execution," Financial Analysts Journal 49(1), 65–71) chiamavano le commissioni visibili la punta dell'iceberg; per qualsiasi cosa con turnover, la parte sommersa domina.

Fissiamo la notazione. Un ordine parent: lato s{+1,1}s \in \{+1, -1\} (buy/sell), dimensione XX. Il prezzo di decisione PdP_d è il mid che la strategia ha visto quando è scattato il segnale. I fill arrivano come (pj,xj)(p_j, x_j) con quantità totale eseguita x=jxjXx = \sum_j x_j \le X. All'orizzonte TT (parent completato, cancellato o scaduto), il mid è PTP_T. Le commissioni esplicite sono FF. L'implementation shortfall in termini monetari:

IS=sjxj(pjPd)costo di esecuzione  +  s(Xx)(PTPd)costo opportunitaˋ  +  F,\mathrm{IS} = \underbrace{s \sum_j x_j\,(p_j - P_d)}_{\text{costo di esecuzione}} \;+\; \underbrace{s\,(X - x)(P_T - P_d)}_{\text{costo opportunità}} \;+\; F,

normalizzato in basis point dividendo per il nozionale di carta XPdX \cdot P_d. Positivo significa che hai pagato. Il primo termine è quanto sono costati i tuoi fill rispetto al portafoglio di carta; il secondo è il costo opportunità di Perold — il residuo non eseguito valorizzato al prezzo a cui è sfuggito; il terzo è l'unica parte che il tuo estratto conto sull'exchange ammette.

Una nota sulle convenzioni: nel settore si dice "arrival price", e nella maggior parte della TCA azionaria arrival indica il mid nel momento in cui l'ordine ha raggiunto il mercato. Per un bot, il momento della decisione e il momento di arrivo differiscono per la tua latenza interna più l'accodamento dovuto ai rate limit — un costo reale e misurabile. Perciò teniamo entrambi i timestamp ed entrambi i prezzi, e lasciamo che sia la decomposizione a separarli.

La decomposizione: delay, impatto, timing, opportunità, commissioni

Un singolo numero di IS ti dice che l'esecuzione è costosa. Non ti dice perché, e le correzioni per componenti diverse sono completamente diverse — non risolvi il costo di delay e il costo di impatto con lo stesso intervento. L'implementation shortfall espanso di Robert Kissell (Kissell, 2006, "The Expanded Implementation Shortfall: Understanding Transaction Cost Components," Journal of Trading 1(3), 6–16; sviluppato in forma estesa in The Science of Algorithmic Trading and Portfolio Management, Academic Press, 2013) scompone il totale in componenti, ciascuna attribuibile a una fase distinta del ciclo di vita dell'ordine. La versione pratica, usando l'arrival mid P0P_0 (mid al primo acknowledgment dell'exchange):

IS=sX(P0Pd)delay  +  sjxj(pjP0)costo di trading  +  s(Xx)(PTP0)opportunitaˋ  +  F.\mathrm{IS} = \underbrace{s\,X\,(P_0 - P_d)}_{\text{delay}} \;+\; \underbrace{s \sum_j x_j\,(p_j - P_0)}_{\text{costo di trading}} \;+\; \underbrace{s\,(X - x)(P_T - P_0)}_{\text{opportunità}} \;+\; F.

L'identità si ricompone esattamente nella definizione di Perold — espandi i termini e P0P_0 si semplifica. Ogni pezzo ha un responsabile distinto:

Costo di delay sX(P0Pd)sX(P_0 - P_d): drift di prezzo tra lo scatto del segnale e il momento in cui il primo child order è live sull'exchange. Questa è la tua infrastruttura — serializzazione, rete, code da rate limit, controlli di rischio. Se il tuo segnale ha una vera alpha a breve orizzonte, il costo di delay è dove trapela per primo; una media costantemente positiva indica che il mercato si muove a tuo favore prima che tu arrivi — alpha momentum in decadimento, oppure qualcuno più veloce che tradinga lo stesso segnale.

Costo di trading sjxj(pjP0)s\sum_j x_j(p_j - P_0): quanto sono costati i tuoi fill rispetto all'arrivo — spread attraversato più impatto di mercato più drift intra-schedule. Questo è il pagellone dell'algoritmo di esecuzione, ed è la grandezza che la ricerca sull'esecuzione modella davvero. Almgren, Thum, Hauptmann e Li (2005, "Direct Estimation of Equity Market Impact," Risk 18(7), 58–62) l'hanno misurata su circa 700.000 ordini azionari USA dai desk di Citigroup (dicembre 2001–giugno 2003) e hanno trovato che il costo di trading scala con la volatilità giornaliera e il tasso di partecipazione — impatto temporaneo che segue una power law nel tasso di trading con esponente vicino a 3/5, impatto permanente vicino alla linearità nella dimensione. Riutilizzeremo la loro forma funzionale quando calibreremo.

Rischio di timing: non un termine nella decomposizione della media ma la varianza attorno ad essa. Distribuire un parent nel tempo riduce l'impatto atteso e ti espone alla volatilità; per uno schedule con posizione residua q(t)q(t) la deviazione standard del costo scala come σ0Tq(t)2dt\sigma \sqrt{\int_0^T q(t)^2\,dt}. Questo è esattamente il trade-off che il framework di Almgren–Chriss ottimizza. Nel tuo report TCA compare come la dispersione dell'IS tra i parent — riporta la deviazione standard accanto a ogni media, altrimenti la media catturerà tutta l'attenzione e le code catturano tutti i tuoi soldi.

Costo opportunità s(Xx)(PTP0)s(X - x)(P_T - P_0): la quantità non eseguita valorizzata al prezzo terminale. Per le strategie passive questa è di norma la componente più grande e meno esaminata, ed è il termine che rende onesto l'intero framework — ne parleremo meglio nella sezione sulle trappole, perché ometterlo è il modo più comune con cui ci si mente da soli con la TCA.

Commissioni FF: la parte esplicita. In crypto, registra le commissioni dopo gli sconti (livelli VIP, rebate in token convertiti al prezzo del momento del fill) e mantieni segnati i rebate maker — una commissione negativa è un dato, non rumore.

Un esempio pratico

Compra X=10X = 10 BTC. Il segnale scatta con Pd=60.000P_d = 60.000; nozionale di carta $600.000. Il primo child order viene confermato con mid P0=60.012P_0 = 60.012. Nei due minuti successivi, 8 BTC vengono eseguiti a VWAP 60.072; il prezzo è in trend contrario, l'algo rispetta il proprio limite, e i restanti 2 BTC vengono cancellati con mid a PT=60.240P_T = 60.240. Commissioni miste 2,5 bps sul nozionale eseguito.

Componente Formula USD bps del nozionale di carta
Delay 10×(60.01260.000)10 \times (60.012 - 60.000) 120 2,0
Costo di trading 8×(60.07260.012)8 \times (60.072 - 60.012) 480 8,0
Opportunità 2×(60.24060.012)2 \times (60.240 - 60.012) 456 7,6
Commissioni 8×60.072×2,5e-48 \times 60.072 \times 2,5\text{e-}4 120 2,0
IS totale 1.176 19,6

L'estratto conto dell'exchange mostra $120. Il costo reale nell'implementare la decisione è stato di $1.176 — un fattore dieci, con le due componenti maggiori invisibili alla contabilità basata sulle commissioni. Circa il 40% deriva da quantità mai negoziata. Un report TCA che analizzasse solo i fill valuterebbe questo parent a 8 bps e lo definirebbe accettabile.

Decomposizione a cascata dell'implementation shortfall dal portafoglio di carta al portafoglio reale

Markouts: quotare l'adverse selection

L'implementation shortfall valuta l'ordine parent. Non dice nulla sulla qualità dei singoli fill — nello specifico, se stai sistematicamente tradando con controparti che sanno qualcosa che tu non sai. Questo si misura con i markouts: marca ogni fill al mid a un orizzonte fisso dopo l'evento.

πτ=s(mtf+τpf)mtf×104 bps,\pi_\tau = \frac{s\,(m_{t_f + \tau} - p_f)}{m_{t_f}} \times 10^4 \ \text{bps},

dove pfp_f è il prezzo di fill al tempo tft_f e mtm_t è il mid al tempo tt. Questo è il PnL mark-to-market per unità del fill all'orizzonte τ\tau, e ha un'anatomia pulita a τ=0\tau = 0: un fill maker parte a ++mezzo spread (hai comprato al bid, il mid è sopra di te); un fill taker parte a -mezzo spread. Ciò che accade al crescere di τ\tau è il contenuto informativo del trade:

  • τ=1\tau = 1s isola lo sniping e il pickoff su quote stale. Se i tuoi fill maker sono già in perdita un secondo dopo l'esecuzione, partecipanti più veloci stanno colpendo le tue quote nel momento esatto in cui diventano mal prezzate — il tuo ciclo di aggiornamento quote è più lento del loro ciclo di trigger. Questo markout è una diagnostica di latenza, non una diagnostica di strategia.
  • τ=10\tau = 10s misura l'adverse selection classica: fill seguiti da movimento continuato attraverso il tuo prezzo. Per un market maker questo è il costo che la cattura dello spread deve battere; la microstruttura azionaria chiama la grandezza correlata realized spread, istituzionalizzata nel reporting della SEC Rule 605 a un orizzonte di 5 minuti. Il crypto si muove più veloce; 10–60s è la banda equivalente.
  • τ=60\tau = 60s ti dice se i fill portano momentum contro di te oltre l'orizzonte di microstruttura — per le strategie taker, se l'alpha del tuo segnale a 60s supera lo spread più impatto che hai pagato per entrare. Una curva di markout taker che parte a 4-4 bps e non attraversa mai lo zero è una strategia che paga per entrate che la sua alpha non riesce a finanziare.

L'asimmetria maker/taker è l'intera economia del trading passivo racchiusa in due curve. Una curva maker che parte a +2+2 bps (mezzo spread) e decade a 1-1 bps entro 60s dice: catturi lo spread e ne restituisci di più, e nessun tier di rebate lo corregge. La stessa curva che si assesta a +0,7+0,7 bps dice che il motore di quotazione si guadagna il proprio mantenimento. Questo è anche esattamente ciò che i backtest naive a fill-al-tocco danno per scontato — un simulatore che ti riempie ogni volta che il prezzo tocca il tuo limite ignora che essere eseguiti è correlato con l'avere torto, motivo per cui il gradino consapevole della coda della scala di simulazione dei fill ha bisogno di markout misurati come input piuttosto che come assunzione.

Curve di markout rispetto all'orizzonte per fill maker e taker che mostrano il decadimento dell'adverse selection

Il calcolo è un merge_asof sul tuo stream di mid:

import pandas as pd

def markouts(fills: pd.DataFrame, mids: pd.DataFrame,
             horizons=("1s", "10s", "60s")) -> pd.DataFrame:
    """fills: [ts, price, qty, side, liquidity]; mids: [ts, mid].
    Both UTC-indexed and sorted. Mid stream must be from YOUR captured
    feed, not candles reconstructed later."""
    fills = fills.sort_values("ts").reset_index(drop=True)
    mids  = mids.sort_values("ts")
    out = fills.copy()
    for h in horizons:
        probe = fills[["ts"]].copy()
        probe["ts"] = probe["ts"] + pd.Timedelta(h)
        m = pd.merge_asof(probe, mids, on="ts", direction="backward")
        out[f"mo_{h}"] = (fills["side"] * (m["mid"].values - fills["price"])
                          / fills["price"] * 1e4)
    return out

def markout_report(mo: pd.DataFrame) -> pd.DataFrame:
    """Qty-weighted markouts by liquidity flag. Weighting matters:
    a 0.001 BTC fill and a 2 BTC fill are not equal evidence."""
    cols = [c for c in mo.columns if c.startswith("mo_")]
    def agg(g):
        w = g["qty"] / g["qty"].sum()
        return pd.Series({c: (g[c] * w).sum() for c in cols}
                         | {"n": len(g), "qty": g["qty"].sum()})
    return mo.groupby("liquidity").apply(agg)

Suddividi ulteriormente il report per simbolo, ora del giorno e distanza della quota dal mid. Il taglio più actionable per una strategia maker è il markout per posizione-in-coda-al-momento-del-fill: i fill in testa a una coda fresca hanno un pricing molto diverso dai fill in cui il livello è stato spazzato via attraverso di te.

La pipeline: cosa registrare

La TCA muore al livello di logging, non al livello matematico. La matematica sopra richiede numeri che la maggior parte dei bot butta via, e nessuno di essi può essere ricostruito a posteriori dalla cronologia dell'exchange. I punti non negoziabili:

  • Mid di decisione, dal tuo feed, al momento del segnale. Non la candela di chiusura dell'exchange, non una ricostruzione successiva. Il benchmark è "il prezzo che la mia strategia credeva vero quando ha deciso" — solo il tuo processo in quel momento lo conosce.
  • Entrambi i timestamp: momento della decisione e momento del primo ack, altrimenti il costo di delay è immisurabile e si fonde silenziosamente nel costo di trading.
  • Ogni fill con il timestamp dell'exchange, la commissione e il flag maker/taker — il tuo tempo di ricezione locale è inquinato dalla tua stessa latenza in ingresso.
  • Parent cancellati e scaduti, registrati come tutto il resto. I parent con zero fill sono le righe più costose della tabella.
  • Uno stream di mid persistente (o stream L1) a granularità 100–250ms, conservato abbastanza a lungo da calcolare i markout. Se già registri gli order book per il simulatore di fill, questo è gratuito.

Due tabelle piatte bastano:

PARENTS = {
    "parent_id":    "str",
    "strategy":     "str",
    "symbol":       "str",
    "venue":        "str",
    "algo":         "str",        # twap | pov | sniper | quote | ...
    "side":         "int8",       # +1 buy, -1 sell
    "qty":          "float64",    # parent size, base units
    "limit_px":     "float64",    # NaN for unconstrained
    "decision_ts":  "datetime64[ns, UTC]",
    "decision_mid": "float64",    # mid your feed showed at decision_ts
    "arrival_ts":   "datetime64[ns, UTC]",   # first exchange ack
    "arrival_mid":  "float64",
    "end_ts":       "datetime64[ns, UTC]",   # filled / cancelled / expired
    "terminal_mid": "float64",
    "mkt_volume":   "float64",    # market volume over [arrival_ts, end_ts]
    "sigma_bps":    "float64",    # realized vol estimate at decision time
}

FILLS = {
    "parent_id": "str",
    "ts":        "datetime64[ns, UTC]",  # exchange timestamp
    "price":     "float64",
    "qty":       "float64",
    "fee":       "float64",      # quote ccy, post-discount, signed
    "liquidity": "str",          # maker | taker
    "venue":     "str",
}

La funzione di attribuzione è una trascrizione diretta della decomposizione:

import numpy as np

def is_decomposition(p: pd.Series, fills: pd.DataFrame) -> dict:
    """Expanded implementation shortfall for one parent, bps of paper
    notional. Sign convention: positive = cost."""
    s, X = p["side"], p["qty"]
    paper = X * p["decision_mid"]
    x = fills["qty"].sum()
    delay = s * X * (p["arrival_mid"] - p["decision_mid"])
    trade = s * ((fills["price"] - p["arrival_mid"]) * fills["qty"]).sum()
    oppty = s * (X - x) * (p["terminal_mid"] - p["arrival_mid"])
    fees  = fills["fee"].sum()
    bps   = lambda v: 1e4 * v / paper
    return {
        "parent_id": p["parent_id"],
        "delay_bps": bps(delay), "trade_bps": bps(trade),
        "oppty_bps": bps(oppty), "fees_bps":  bps(fees),
        "is_bps":    bps(delay + trade + oppty + fees),
        "fill_ratio": x / X,
        "participation": x / max(p["mkt_volume"], x),
    }

def tca_table(parents: pd.DataFrame, fills: pd.DataFrame) -> pd.DataFrame:
    fg = dict(tuple(fills.groupby("parent_id")))
    empty = fills.iloc[0:0]
    rows = [is_decomposition(p, fg.get(p["parent_id"], empty))
            for _, p in parents.iterrows()]
    return parents.merge(pd.DataFrame(rows), on="parent_id")

E le query di attribuzione, dove la TCA smette di essere contabilità e diventa ricerca:

tca = tca_table(parents, fills)

def wavg(g: pd.DataFrame, col: str) -> float:
    w = g["qty"] * g["decision_mid"]          # notional weights
    return (g[col] * w).sum() / w.sum()

comp = ["delay_bps", "trade_bps", "oppty_bps", "fees_bps", "is_bps"]
by_strat = tca.groupby("strategy").apply(
    lambda g: pd.Series({c: wavg(g, c) for c in comp}
                        | {"is_std": g["is_bps"].std(), "n": len(g)}))

done = tca[tca["fill_ratio"] > 0.99]
done["pov_bin"] = pd.qcut(done["participation"], 6)
impact_curve = done.groupby("pov_bin").apply(lambda g: wavg(g, "trade_bps"))

tca["hour"] = tca["arrival_ts"].dt.hour
by_hour = tca.groupby("hour").apply(lambda g: wavg(g, "is_bps"))

by_venue = tca.groupby("venue").apply(
    lambda g: pd.Series({"is_bps": wavg(g, "is_bps"),
                         "is_std": g["is_bps"].std(),
                         "fill_ratio": g["fill_ratio"].mean(), "n": len(g)}))

La query 4 è la tabella di input su cui ottimizza uno smart order router — instradare senza TCA per venue è instradare in base al listino commissioni, cioè in base alla componente di costo più piccola (Smart order routing in crypto prende questa tabella come punto di partenza). Insieme al modulo di markout, queste sono le promesse ~200 righe.

Chiudere il loop: calibrare il cost model del backtest

Ecco dove la pipeline si ripaga da sola. Il tuo backtest afferma dei numeri: slippage_bps = 5, una curva di probabilità di fill, un coefficiente di impatto. Ognuno di essi è un'affermazione sull'esecuzione live, e la tabella TCA è esecuzione live. Il loop: misura con la TCA, adatta il cost model, esegui il backtest col modello calibrato, tradinga, rimisura.

Loop di calibrazione chiuso dai fill live attraverso l'attribuzione TCA fino al cost model del backtest

Per il costo taker, prendi in prestito la forma funzionale da Almgren et al. (2005) invece di inventarne una. Il loro risultato — costo proporzionale alla volatilità giornaliera moltiplicata per una potenza del tasso di partecipazione — dà un modello a due parametri:

trade_bpsσbps=a(xV)b,\frac{\text{trade\_bps}}{\sigma_{\text{bps}}} = a \cdot \left(\frac{x}{V}\right)^{b},

con la stima azionaria b0,6b \approx 0,6 come prior. Adattalo su medie binnate, non su parent grezzi — i costi dei singoli parent sono dominati dal rumore (quello è il termine di rischio di timing), e una regressione log-log su dati grezzi adatterà allegramente il rumore:

done = tca[(tca["fill_ratio"] > 0.99) & (tca["participation"] > 0)]
done["norm_cost"] = done["trade_bps"] / done["sigma_bps"]
bins = done.groupby(pd.qcut(done["participation"], 8)).agg(
    pov=("participation", "mean"), cost=("norm_cost", "mean"))
bins = bins[bins["cost"] > 0]                # can't log a negative bin
b, log_a = np.polyfit(np.log(bins["pov"]), np.log(bins["cost"]), 1)

def taker_cost_bps(participation, sigma_bps, a=np.exp(log_a), b=b):
    return a * sigma_bps * participation ** b

Poi sii onesto riguardo ai margini di errore. Il team di Almgren aveva 700.000 ordini e ha comunque riportato l'esponente di impatto temporaneo come 0,600±0,0380,600 \pm 0,038; un bot con 2.000 parent non può permettersi di stimare bb liberamente per ogni simbolo. Il regime pratico: raggruppa tra simboli dopo la normalizzazione per σ\sigma, comprimi bb con decisione verso 0,6 (oppure fissalo e adatta solo aa), e ricalibra mensilmente. Se il tuo aa calibrato deriva verso l'alto, il tuo footprint è cresciuto oppure il mercato si è assottigliato — in entrambi i casi il backtest doveva saperlo.

Per le strategie maker gli obiettivi di calibrazione sono diversi e provengono dagli altri due moduli:

  • Probabilità di fill: il modello di coda del simulatore di fill predice P(fill)P(\text{fill}) per cella (distanza-dallo-spread, posizione-in-coda); il tuo log dei parent fornisce i rapporti di fill realizzati per cella. Il disaccordo è un bug del simulatore con un riferimento a griglia allegato.
  • Adverse selection: sostituisci l'assunzione implicita del simulatore "i fill sono intercambiabili" con la tabella di markout misurata — un fill maker simulato a distanza dd nell'ora hh porta il π10s(d,h)\pi_{10s}(d, h) misurato come haircut mark-to-market immediato. Questa singola modifica è la differenza tra un backtest maker che allucina e uno che segue la realtà; era l'input di calibrazione mancante segnalato nell'articolo sulla simulazione dei fill.
  • Dispersione dei costi: alimenta il backtest con la distribuzione dell'IS, non con la sua media. Un cost model che sposta solo la media non può riprodurre i drawdown che il rischio di timing crea; anche una lognormale adattata all'IS per-parent batte una costante.

Il trattamento completo delle curve di slippage — forme funzionali, condizionamento per regime, quando la power law si rompe — è un articolo a sé: Curve di slippage e cost model per i backtest. Il punto qui è architetturale: i modelli di quell'articolo sono incalibrabili senza le tabelle di questo articolo.

Trappole

La letteratura TCA ha decenni, e altrettanti ne hanno i modi per aggirarla. Tre modalità di fallimento spiegano la maggior parte dell'autoinganno.

Gaming del benchmark. Qualsiasi benchmark diverso dall'arrival price può essere "abbracciato" (tracciato artificialmente). Il classico è il VWAP: un algoritmo valutato contro il VWAP dell'intervallo può tracciarlo entro un basis point mentre la posizione perde venti punti rispetto all'arrivo, perché il benchmark deriva insieme al prezzo che tu stesso stai spingendo — e a livelli di partecipazione significativi le tue stesse stampe sono il VWAP, quindi tracciarlo è un compito a casa auto-corretto. Abbiamo sezionato la politica dei benchmark in TWAP vs VWAP vs POV; la regola sul lato TCA è più semplice: i benchmark si scelgono prima di tradinga, e l'IS-contro-arrivo va sempre calcolato anche quando uno scheduler viene valutato contro il proprio benchmark di scheduling. La variante più sottile è il gaming dell'arrivo: se il componente che imposta decision_ts può vedere momentum a breve termine, può temporizzare le "decisioni" per abbellire il termine di delay. I timestamp di decisione appartengono al livello del segnale, registrati prima che qualsiasi logica di esecuzione entri in funzione.

Survivorship bias nell'analisi dei soli fill eseguiti. Condiziona la tua analisi dei costi ai soli fill e l'esecuzione passiva sembrerà gratuita. Concretamente: 100 parent buy passivi, un tick sotto il mid. Sessanta vengono eseguiti e — essendo passivi — a prezzi in media 3 bps migliori dell'arrivo: "costo" misurato 3-3 bps, un report di cui essere orgogliosi. I quaranta mai eseguiti erano esattamente quelli in cui il prezzo è scappato via; segnali a 25 bps avversi alla cancellazione e il numero onesto è 0,6×(3)+0,4×25=+8,20,6 \times (-3) + 0,4 \times 25 = +8,2 bps. Il report dei soli fill eseguiti e il report vero differiscono di 11 bps e di segno. Questo non è un caso limite — è il meccanismo stesso del trading passivo: essere eseguiti è correlato col fatto che il prezzo passi attraverso di te, ed è lo stesso condizionamento che rende fantasiosi i backtest a fill-al-tocco. Il costo opportunità non è un raffinamento opzionale dell'IS; è il termine che difende l'intera misurazione dalla selezione. Lo stesso bias ha una variante taker: ordini IOC mancati, ordini respinti da rate limit o controlli di rischio — se i mancati non vengono registrati, il costo del mancare l'esecuzione è immisurato, ed è massimo esattamente nei mercati veloci in cui la tua strategia voleva di più quel trade.

Autolesionismi vari, in breve: mid di arrivo ricostruiti dalle candele dell'exchange (il tuo feed e la candela sono in disaccordo esattamente quando conta); media dei bps per-parent senza pesi nozionali (mille fill polvere che sovrastano un ordine vero); commissioni registrate pre-sconto; mid di markout presi da un venue diverso da quello del fill (base cross-venue mascherata da adverse selection); e sui perpetual, lasciare che l'accumulo di funding contamini la finestra di esecuzione — il funding è un costo, ma non un costo di esecuzione, e mescolarli avvelena entrambe le analisi.

Cosa fare questa settimana

Aggiungi le due tabelle di log al tuo bot — lo schema sopra è un elenco di colonne, non un progetto. Non fare backfill di nulla: due settimane di log onesti battono un anno di ricostruzioni. Esegui la decomposizione e i tre orizzonti di markout. Scoprirai quale componente domina (quasi mai le commissioni), se i tuoi fill passivi subiscono adverse selection oltre la loro cattura di spread, e quanto la costante di costo del tuo backtest si discosta dalla curva misurata. Poi ricollega la curva calibrata nel simulatore e riesegui il backtest che ti ha detto per primo di lanciare questa strategia. Il portafoglio di carta di Perold, trentotto anni dopo, resta l'unico avversario onesto che quello reale abbia mai avuto.

Disclaimer: le informazioni fornite in questo articolo hanno solo scopo didattico e informativo e non costituiscono consulenza finanziaria, di investimento o di trading. Il trading di criptovalute comporta un rischio significativo di perdita.

Autori

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

Resta un Passo Avanti al Mercato

Iscriviti alla nostra newsletter per approfondimenti esclusivi sul trading con IA, analisi di mercato e aggiornamenti sulla piattaforma.

Rispettiamo la tua privacy. Annulla l'iscrizione in qualsiasi momento.