Alle 10:07 di un martedì, il nostro agente di ricerca ha segnalato un setup rialzista di breve termine su un ETF su indici. Il segnale sul sottostante era ordinario: il prezzo aveva recuperato la sua media a 20 barre, la volatilità realizzata si stava attenuando e nella sessione successiva era previsto un dato macroeconomico. L’operazione proposta era un call spread con circa due settimane alla scadenza.
Al backtest piaceva. La strategia paper non ha inserito l’ordine. Confrontando gli ordini, abbiamo verificato che i timestamp dei segnali coincidevano e i prezzi del sottostante differivano di pochi centesimi. I contratti erano diversi: il backtest aveva acquistato la scadenza del venerdì che appariva migliore nella catena storica; il processo paper aveva selezionato la scadenza successiva in calendario, una settimana dopo.
La discrepanza dipendeva da un join di calendario. Avevamo trattato i “giorni alla scadenza” come una proprietà di una data, quindi selezionato il contratto la cui scadenza cadeva più vicino all’obiettivo. Nei dati storici, il calendario delle scadenze includeva una quotazione settimanale aggiunta dopo la data oggetto del test. Il backtest aveva usato un contratto del futuro, come se fosse una scorciatoia innocua.
Inizialmente abbiamo sospettato un bug di fuso orario. Le date di scadenza delle opzioni sono particolarmente insidiose sotto questo aspetto: la sessione di mercato termina a New York, i timestamp delle borse possono essere memorizzati in UTC e una data a mezzanotte può slittare se una pipeline la converte senza attenzione. Abbiamo controllato. I timestamp erano corretti. Semplicemente, quel martedì il contratto non esisteva nell’universo storico.
Prima lezione: una catena storica di opzioni richiede un universo di quotazioni point-in-time. La data di scadenza di un contratto indica quando termina. Non indica quando è stato quotato, quando è diventato negoziabile o se il sistema avrebbe potuto selezionarlo in quel momento.
Abbiamo ricostruito la fase di selezione usando le informazioni disponibili al timestamp della decisione. Per ogni contratto candidato, abbiamo richiesto un timestamp di quotazione non successivo alla decisione, una quotazione valida non più vecchia della nostra soglia di freschezza e una sessione di mercato in cui la strategia potesse inviare un ordine. Abbiamo anche registrato l’insieme dei candidati prima di ordinarli. Quest’ultimo passaggio era importante perché il vecchio backtest conservava solo il vincitore: non potevamo sapere se avesse confrontato cinque contratti idonei o se avesse scelto silenziosamente da un calendario non ancora disponibile.
Poi abbiamo esaminato la quotazione usata per prezzare lo spread. Il backtest aveva campionato separatamente l’ultima quotazione disponibile per ciascuna gamba. L’ask della call era delle 10:06:58; il bid della call venduta era delle 10:04:11. Entrambe rientravano tecnicamente nei limiti della politica generale sui dati, ma nel frattempo il mercato si era mosso. Un midpoint dello spread calcolato su gambe non sincronizzate può descrivere un mercato che non è mai esistito.
Abbiamo modificato la regola sulle quotazioni: i timestamp delle due gambe devono distare al massimo due secondi e i mercati incrociati o con size pari a zero vengono scartati. Questo ha ridotto il numero di osservazioni storiche utilizzabili, ma ha reso più credibili i prezzi rimasti. Quando non esisteva una quotazione sincronizzata, indicavamo il candidato come non disponibile, invece di colmare la lacuna con un prezzo obsoleto.
Poi è arrivata la regola di ordinamento. L’agente aveva scelto la scadenza con il costo dello spread quotato più basso. Sembra sensato, finché lo spread è largo un solo contratto e la quotazione è obsoleta. Abbiamo reso esplicito l’obiettivo: massimizzare il payoff atteso per unità di rischio definito, considerando solo i contratti che superano i requisiti minimi di size quotata, freschezza e quotazione. In questo esempio, la scadenza idonea più vicina aveva un bid-ask spread quotato di $0.42 sulle due gambe, mentre la scadenza successiva aveva uno spread di $0.31. L’opzione apparentemente più economica aveva anche un solo contratto esposto sulla gamba short.
| Candidato | Scadenza disponibile in quel momento? | Età della quotazione | Size esposta sulla gamba short | Decisione |
|---|---|---|---|---|
| Scadenza settimanale del venerdì | No | Non applicabile | Non applicabile | Escludere |
| Venerdì successivo | Sì | 1,4 secondi | 18 contratti | Idoneo |
| Terzo venerdì | Sì | 9,8 secondi | 1 contratto | Escludere: quotazione obsoleta e mercato poco profondo |
Abbiamo riprodotto l’episodio applicando queste regole. Ora il backtest ha scelto la stessa scadenza della strategia paper e ha registrato i motivi per cui gli altri contratti erano stati scartati. Il prezzo di ingresso risultante per lo spread era peggiore di $0.11 rispetto all’esecuzione storica originale. Questo non significa che la strategia fosse priva di valore; significa che il vecchio dato di performance includeva una selezione dell’operazione che il processo live non poteva riprodurre.
Un piccolo inconveniente: i formati dei simboli delle borse fanno sembrare i bug nelle scadenze problemi di qualità dei dati. Abbiamo trovato un codice settimanale di un fornitore che differiva dal codice mensile per un solo carattere. Nei log di audit continuo a conservare il simbolo grezzo accanto alla chiave normalizzata del contratto. Non è elegante, ma ci ha evitato di fidarci troppo in fretta di un join ben ordinato.
Se state costruendo un backtest sulle opzioni, salvate l’intero insieme dei candidati a ogni decisione, insieme allo stato di quotazione di ciascun contratto, ai timestamp e alle size delle quotazioni e al motivo dell’esclusione. Poi rendete la selezione dei contratti una funzione deterministica di quello snapshot. La scelta della scadenza fa parte della strategia. Se il backtest può vedere un contratto che la strategia paper non poteva vedere, il confronto dei segnali inizia quando la risposta è già cambiata.
← Tutti gli articoli


