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

Lo spazio dei parametri a due assi: perché gran parte del tuo sweep dovrebbe essere quasi gratis

Lo spazio dei parametri a due assi: perché gran parte del tuo sweep dovrebbe essere quasi gratis
#algotrading
#backtest
#ricerca dei parametri
#ottimizzazione
#caching
#maledizione della dimensionalità
Part 4 of 10 · Collection
High-Performance Backtest Engines

Fa parte della serie "Backtest Senza Illusioni".

La maledizione della dimensionalità viene di solito raccontata come un monito: ogni parametro aggiunto moltiplica lo spazio di ricerca, quindi una strategia a 18 dimensioni è impossibile da scandagliare. Questa impostazione assume tacitamente che ogni dimensione costi lo stesso da valutare. Non è così. Nel nostro motore a doppio/triplo timeframe, due terzi dei parametri sono quasi gratis da esplorare, mentre un terzo si porta addosso praticamente l'intero conto computazionale. Una volta vista questa spaccatura, "lo spazio di ricerca è troppo grande" smette di essere la lamentela giusta. La domanda giusta è: quali dimensioni stai davvero pagando?

La dimensione che non serviva ricalcolare

Il nostro benchmark di ricerca dei parametri esegue una strategia momentum multi-timeframe: una media mobile di Hull e la sua variante a triplo smoothing (HMA3) su due o tre timeframe, con un gate di separazione direzionale che decide quando un incrocio è abbastanza "pulito" da giustificare un'azione. Lo spazio di ricerca completo per la variante a tre timeframe ha diciotto dimensioni: periodo e lunghezza della HMA per ciascun timeframe, più un insieme di soglie di separazione per ingresso e uscita su ogni timeframe.

Il modo ingenuo di esplorare questo spazio è un unico ciclo: scegli un vettore di parametri, costruisci ogni indicatore da zero, esegui la simulazione, assegna un punteggio, ripeti. Siamo partiti da lì. Era lento, e lo era per una ragione che, una volta messa a fuoco, si è rivelata imbarazzante: per la stragrande maggioranza delle prove adiacenti stavamo ricalcolando indicatori che non erano cambiati.

Sposta la soglia di separazione in ingresso da 0.03 a 0.035 e rilancia. La media di Hull su un anno di barre a 1 minuto è identica bit per bit alla prova precedente — la soglia non compare da nessuna parte nella sua definizione. Eppure il ciclo ingenuo la ricostruisce comunque, su mezzo milione di barre, per tre timeframe, ogni singola volta. Stavamo spendendo quasi tutto il budget di calcolo per ricavare di nuovo quantità invarianti rispetto al parametro che stavamo effettivamente variando.

Quell'osservazione è l'intero articolo. Nel momento in cui noti che alcuni parametri cambiano gli indicatori e altri cambiano solo la regola decisionale applicata a indicatori fissi, lo spazio dei parametri smette di essere una nuvola piatta di dimensioni e diventa due strati annidati con cartellini del prezzo enormemente diversi.

Due assi, non uno

Due assi di uno spazio dei parametri con cartellini del prezzo diversi: una griglia rada e costosa di indicatori le cui celle sbloccano ciascuna un foglio denso e quasi gratuito di configurazioni di soglie

Dividi i parametri in base a cosa la loro rivalutazione ti costringe a ricalcolare:

  • L'asse costoso — i parametri degli indicatori. Periodo del timeframe e lunghezza della HMA. Cambia uno dei due e devi ricostruire l'indicatore sull'intera serie dei prezzi: ricampionare il timeframe superiore, applicare la pesatura di Hull, ricalcolare la serie degli incroci e la separazione a ogni incrocio. È una passata O(n) per ogni barra di storico, e la paghi per intero per ogni combinazione distinta. Nella strategia a tre timeframe questo asse conta sei parametri: (period, hma_length) per ciascuno dei timeframe alto, medio e basso.

  • L'asse economico — le soglie decisionali. I gate di separazione direzionale che decidono, date le serie di incroci e separazione già calcolate, se entrare o uscire. Cambia una soglia e nessun indicatore si muove. Rilanci soltanto una singola passata O(n) sui segnali precalcolati, verificando i gate e registrando le esecuzioni. Nella strategia a tre timeframe questo asse conta dodici parametri (quattro soglie di separazione — ingresso-buy, ingresso-sell, uscita-buy, uscita-sell — su ciascuno dei tre timeframe). La variante a due timeframe ne ha otto (quattro soglie su ciascuno dei due timeframe).

La vera forma dello spazio, quindi, è: 6 costosi + 12 economici = 18 per la versione a tre timeframe, e 4 costosi + 8 economici = 12 per quella a due. Due terzi della dimensionalità vivono sull'asse economico. E i due assi non differiscono solo nel numero: differiscono nel costo unitario di oltre tre ordini di grandezza, ed è proprio questo il numero di cui parla il resto dell'articolo.

La struttura è rigorosamente annidata. Ogni punto sull'asse costoso — una (period, hma_length) concreta per timeframe — definisce un insieme di array di segnali fissi. Sopra quella fondazione fissa poggia un intero foglio dell'asse economico: migliaia di vettori di soglie, ciascuno una passata veloce sugli stessi array. Paghi la fondazione una volta sola e la ammortizzi sull'intero foglio.

Perché gli indicatori sono invarianti rispetto alle soglie

Il caching funziona solo grazie a un fatto matematico, non a un fortunato dettaglio implementativo, e vale la pena enunciarlo con precisione perché è l'assunzione portante. Gli array degli indicatori sono funzione soltanto di (period, hma_length). Le soglie di separazione non compaiono da nessuna parte nella loro definizione.

Concretamente, per ogni timeframe il motore precalcola quattro array allineati sull'indice base: la media di Hull hma, la sua versione a triplo smoothing hma3, la serie degli incroci cross (+1 buy / −1 sell / 0) e la percentuale di separation a ogni incrocio. Ognuno di questi deriva dai prezzi e dai due parametri dell'indicatore. La soglia — il numero con cui confronti separation — viene applicata dopo, al momento della decisione. Non tocca mai gli array.

Non è un riordino banalmente sicuro. È sicuro solo perché siamo stati attenti a quando l'informazione diventa disponibile, che è il tema del lavoro sul look-ahead bias altrove in questa serie. L'indicatore del timeframe superiore alla barra base i è calcolato dalle candele chiuse del timeframe superiore più una candela in formazione la cui chiusura corrente coincide con la chiusura base attuale close[i] — un valore noto alla barra i. Nulla filtra dal futuro. Quindi il segnale precalcolato alla barra i è esattamente ciò che un bot live avrebbe visto alla barra i, e resta valido qualunque sia la soglia con cui lo testerai in seguito. Mettere in cache un segnale che perde informazione dal futuro significherebbe solo mettere in cache la falla; mettere in cache un segnale causale significa mettere in cache qualcosa su cui puoi davvero fare trading.

L'invarianza ci regala una fattorizzazione. Scrivi la valutazione di un vettore di parametri completo θ = (indicator_params, threshold_params) come:

signals   = build_indicators(indicator_params)      # EXPENSIVE, depends only on indicator_params
score     = simulate(signals, threshold_params)     # CHEAP, reuses signals across all thresholds

build_indicators non legge threshold_params. È questo singolo fatto ad autorizzare la cache: tieni fisso indicator_params, varia liberamente threshold_params, e signals è una costante che calcoli una volta sola.

L'architettura: calcola una volta, esplora in massa

Una cache di segnali indicizzata dai parametri degli indicatori che alimenta uno sweep batch veloce: una singola costruzione di indicatori si dirama in un'ampia fila di valutazioni di soglie che condividono gli stessi array in cache

Il motore (scripts/engine_multitf.py, commit bfc8aaa nel nostro backtester) implementa la fattorizzazione con due componenti i cui nomi dicono esattamente cosa fanno.

SignalCache — l'asse costoso, memoizzato. È un dizionario indicizzato da (period_bars, hma_length). Chiedigli i segnali di un timeframe e restituisce i TFSignals in cache se quella combinazione di indicatori è già stata costruita, altrimenti li costruisce una volta e li salva. Poiché la chiave è composta dai soli parametri degli indicatori, ogni configurazione di soglie che condivide una combinazione di indicatori — e in uno sweep denso di soglie parliamo di migliaia — colpisce la stessa voce di cache. I timeframe superiori premiano particolarmente questo schema: una griglia grossolana di, diciamo, quattro periodi candidati per una manciata di lunghezze HMA è un numero ridotto di costruzioni costose distinte, e ciascuna viene riutilizzata sull'intero foglio di soglie impilato sopra di essa.

sweep_separations — l'asse economico, in batch. Prende gli array di segnali in cache e una matrice di vettori di soglie (sps, di forma [m, 12]) e li fa passare tutti attraverso un unico kernel compilato. Ogni riga è una passata O(n): scorri le barre, applica i gate, registra esecuzioni senza leak a open[i+1], somma PnL e tempo in posizione. Nessun indicatore viene ricostruito dentro questo ciclo — legge cross e separation direttamente dalla cache. La simulazione interna è compilata JIT (Numba), quindi a regime il costo per configurazione è dominato dalla singola scansione lineare delle barre, non dall'overhead di Python.

I due componenti si compongono nella naturale ricerca annidata: il ciclo esterno percorre l'asse costoso (ogni iterazione costruisce e mette in cache una combinazione di indicatori), mentre il ciclo interno lancia un ampio batch sull'asse economico usando quegli array in cache. Nel codice la forma è esattamente questa — un piccolo ciclo costoso che avvolge un ampio batch economico:

cache = SignalCache(base_close, base_ts)           # keyed by (period, hma_length)

for htf_p, htf_h, mtf_p, mtf_h, ltf_p, ltf_h in indicator_grid:   # EXPENSIVE axis (coarse)
    htf = cache.get(htf_p, htf_h)                  # built once, then a cache hit forever
    mtf = cache.get(mtf_p, mtf_h)
    ltf = cache.get(ltf_p, ltf_h)

    sps = sample_thresholds(m=4000)                # CHEAP axis: [m, 12] threshold vectors
    pnl, n_trades, bars_in_pos = sweep_separations(  # one compiled batch, no indicator work
        base_close, base_open, htf, ltf, sps, mtf=mtf)

Tocchi il costruttore costoso il minimo numero di volte che la griglia degli indicatori consente, e lasci che sia lo sweep economico a fare il volume — migliaia di righe di sps contro array che non si muovono mai. Questa è l'intera ottimizzazione — nessuna approssimazione, nessuna perdita di fedeltà, solo il rifiuto di ricalcolare ciò che non è cambiato.

Quanto costa davvero il "gratis": i numeri

Un confronto a barre del costo per configurazione: una colonna alta per il ricalcolo a ogni configurazione accanto a una scheggia per la passata sulle soglie in cache, annotato con il divario di circa 1,600x

Abbiamo misurato i due assi sul carico di lavoro dimostrativo: un anno intero di barre a 1 minuto di ETHUSDT (~527k barre), tre timeframe, con indicatori già caldi e compilazione JIT esclusa dal cronometraggio.

Sull'asse economico, sweep_separations sostiene ~5,600 configurazioni di soglie al secondo. Si tratta di una simulazione completa — gate, esecuzioni, PnL, esposizione — per ogni configurazione, su mezzo milione di barre, a circa 180 microsecondi per configurazione. Può essere così veloce perché non fa alcun lavoro sugli indicatori: ogni configurazione legge gli stessi array cross e separation dalla cache.

Ora dai un prezzo all'alternativa. Costruire una volta il set di indicatori a tre timeframe richiede nell'ordine di qualche centinaio di millisecondi (~0.3 s) — ricampionamento di tre timeframe, pesatura di Hull, estrazione di incroci e separazione sull'intero anno. Se ricalcolassi gli indicatori dentro il ciclo delle configurazioni — il design ingenuo a ciclo singolo — ognuna di quelle 5,600 configurazioni al secondo pagherebbe invece l'intera costruzione degli indicatori. Il costo per configurazione lievita da ~180 microsecondi a ~0.3 secondi:

Costo per configurazione Configurazioni/sec
Asse economico (segnali in cache) ~180 µs ~5,600
Ricalcolo degli indicatori a ogni configurazione ~0.3 s ~3.4

Il rapporto è ~1,600x. Esplorare l'asse economico su segnali in cache è circa tre ordini di grandezza più economico del design ingenuo che ricostruisce gli indicatori per ogni vettore di soglie. In concreto: un batch di qualche migliaio di configurazioni di soglie che si conclude in meno di un secondo sul percorso con cache richiederebbe quasi un'ora se ogni configurazione ricostruisse i propri indicatori. Stessi risultati, stessa fedeltà, nessuna scorciatoia nella matematica — l'unica differenza è che uno dei due ricalcola un invariante e l'altro no.

Non è una micro-ottimizzazione da spolverare alla fine. Cambia quali ricerche sono fattibili. A 5,600 cfg/s l'asse delle soglie è abbastanza denso da poter essere esplorato come si deve — puoi permetterti una griglia fine o un lungo campionamento random/QMC per ogni combinazione di indicatori — mentre l'asse costoso resta una manciata deliberatamente grossolana di costruzioni. Il budget di calcolo fluisce dove i parametri costano davvero qualcosa.

La maledizione della dimensionalità, riprezzata

Una griglia piatta e uniforme di parametri rietichettata come mappa dei prezzi a due livelli: poche celle costose dal contorno marcato, il resto ombreggiato come quasi gratuito

Torna all'impostazione con cui si è aperto questo articolo. La maledizione della dimensionalità dice che lo spazio di ricerca cresce esponenzialmente nel numero di parametri, quindi più dimensioni è categoricamente peggio. Questo è vero riguardo alla dimensione della griglia. È fuorviante riguardo al costo di coprirla, perché assegna a ogni dimensione lo stesso prezzo.

Una volta separati gli assi, il conteggio si legge diversamente. La strategia a tre timeframe ha 18 dimensioni, ma solo 6 di esse sono costose. Le altre 12 sono dimensioni dell'asse economico che espandono la griglia senza espandere in modo significativo il conto computazionale — puoi lanciargli contro migliaia di configurazioni per pochi spiccioli. La strategia a due timeframe ha 12 dimensioni, di cui solo 4 costose e 8 economiche. In entrambi i casi la maggior parte della "maledizione" si concentra sull'asse che costa quasi nulla da esplorare.

Il modo onesto di ragionare sul costo di una ricerca non è quindi "quanti parametri" ma "quanti parametri costosi, e quanto grossolana può essere la loro griglia". L'asse costoso è dove l'esponenziale fa davvero male, ed è lì che vuoi una griglia piccola e ben scelta — pochi periodi candidati, un intervallo contenuto di lunghezze HMA — eventualmente raffinata dal grossolano al fine, nello spirito del drill-down a risoluzione adattiva che usiamo altrove. L'asse economico è dove puoi permetterti di abbondare, perché ogni configurazione di soglie in più costa 180 microsecondi.

Questa riprezzatura si generalizza ben oltre la nostra strategia HMA. Lo schema — alcuni parametri cambiano le feature, la maggior parte cambia la regola applicata a feature fisse — ricorre ovunque nel trading sistematico. Lunghezze degli indicatori, frequenze di ricampionamento e finestre di lookback sono costose; soglie di ingresso/uscita, distanze degli stop, moltiplicatori di sizing e gate di conferma sono economici. Ogni volta che un parametro rimodella soltanto il confine decisionale su segnali già calcolati, appartiene all'asse economico, ed è lì che va esplorato. Vittorie di caching della stessa famiglia compaiono nel caching parquet multi-timeframe e nella più ampia scala di velocità del motore.

Dove il pasto gratis presenta il conto

L'asse economico è economico in calcolo. Non lo è in statistica, e confondere le due cose è il modo in cui trasformi una vittoria di prestazioni in una macchina per l'overfitting.

Ogni configurazione di soglie che valuti è una prova, e quando lanci migliaia di prove su un unico dataset la migliore di esse sembra buona in parte per fortuna. Rendere quelle prove 1,600x più economiche non fa sparire la fortuna — la rende più facile da accumulare. Uno sweep denso di soglie è esattamente la situazione in cui l'inflazione da test multipli morde più forte: molti candidati, una sola storia, e una regola di selezione che riporta il massimo. Il motore più veloce del mondo ti consegnerà volentieri un vettore di soglie che si adatta magnificamente al rumore della tua finestra di test e fallisce out of sample.

La disciplina deve quindi scalare con la velocità. Nel momento in cui il costo computazionale di una prova scende verso lo zero, la contabilità statistica diventa il vincolo stringente, e devi pagarla esplicitamente:

  • Deflaziona per il numero di prove. Valuta il vincitore rispetto a quante configurazioni hai provato, non rispetto a zero. Il Deflated Sharpe Ratio e la probabilità di overfitting del backtest esistono esattamente per questo — trasformano "abbiamo provato 4,000 vettori di soglie" in un taglio sull'edge riportato.
  • Valida out of sample, fold per fold. Uno sweep economico va comunque eseguito dentro uno split walk-forward onesto; una soglia che vince solo in-sample non vale nulla, per quanto in fretta tu l'abbia trovata. Il nostro motore mantiene le esecuzioni prive di leak (open[i+1]) proprio perché l'asse economico non possa comprarsi prestazioni sbirciando nel futuro.
  • Preferisci i plateau ai picchi. Poiché l'asse delle soglie è denso e veloce, puoi mapparne la superficie di risposta, non solo l'argmax. Un'ampia regione di soglie che funzionano tutte è un edge reale; un singolo massimo appuntito è un artefatto di fitting — la distinzione plateau contro picco, resa abbordabile proprio dalla velocità che stiamo descrivendo.

Leggi la struttura a due assi nel modo giusto: non ti compra più fiducia, ti compra più prove allo stesso costo di fiducia per prova. È un valore autentico — la copertura densa dell'asse economico è ciò che ti permette di trovare plateau e caratterizzare la superficie — ma solo se tieni onesto il registro statistico. La velocità elimina la scusa computazionale per non cercare a fondo; non elimina l'obbligo di scontare ciò che una ricerca approfondita trova.

Come strutturare la tua ricerca

Per applicare tutto questo alla tua strategia, il lavoro è per lo più classificazione — smistare i parametri sull'asse giusto — seguita da un ciclo annidato:

  1. Etichetta ogni parametro in base a cosa ti costringe a ricalcolare. Se cambiarlo cambia un array di indicatori/feature, è costoso. Se cambia solo un confronto, una soglia o una regola di sizing applicata ad array fissi, è economico. In caso di dubbio, chiediti: questo parametro compare da qualche parte nella definizione dell'indicatore? Se no, è economico.
  2. Indicizza una cache solo sull'asse costoso. Memoizza la costruzione delle feature in base ai parametri degli indicatori (come fa SignalCache con (period, hma_length)). Le prove adiacenti che condividono una combinazione di indicatori riutilizzano così gli stessi array gratis.
  3. Esegui l'asse economico in batch sulle feature in cache. Lancia le configurazioni di soglie come un ciclo compilato e serrato sui segnali precalcolati, non come rivalutazioni complete. È da qui che arriva il throughput — nel nostro caso i ~5,600 cfg/s.
  4. Annida i cicli: costoso fuori, economico dentro. Mantieni la griglia costosa piccola e deliberata (grossolana, o dal grossolano al fine); lascia che lo sweep economico sia denso. Spendi il budget dove i parametri costano qualcosa.
  5. Dimensiona il numero di prove contro l'overfitting, non contro l'orologio. Ora che l'orologio non è più il limite, lascia che lo siano il deflated Sharpe / PBO. Decidi quante prove sull'asse economico puoi permetterti statisticamente, e valida il vincitore out of sample.

Il guadagno ingegneristico e il presidio statistico sono due facce della stessa idea: separare gli assi ti permette di esplorare esaustivamente le dimensioni economiche, ed è esattamente per questo che devi poi scontare quanto esaustivamente hai cercato.

Punti chiave

  1. Non tutte le dimensioni costano uguale. I parametri di una strategia si dividono in un asse costoso (gli indicatori — ricalcolati sull'intera serie) e un asse economico (le soglie — una passata O(n) su segnali precalcolati). Nel nostro motore sono 6 costosi + 12 economici per la versione a tre timeframe, 4 + 8 per quella a due.
  2. Gli indicatori sono invarianti rispetto alle soglie, e quell'invarianza è l'intera ottimizzazione. La costruzione delle feature dipende solo dai parametri degli indicatori, quindi costruisci una volta, metti in cache per (period, hma_length) e riutilizzi per ogni configurazione di soglie che condivide la combinazione.
  3. L'asse economico gira ~1,600x più a buon mercato. ~5,600 configurazioni di soglie al secondo (~180 µs ciascuna) su segnali in cache, contro ~0.3 s per configurazione se ricostruisci gli indicatori ogni volta. Stessa fedeltà — l'unica differenza è rifiutarsi di ricalcolare un invariante.
  4. La maledizione della dimensionalità è in realtà una maledizione della dimensionalità costosa. La maggior parte del conteggio dei parametri vive sull'asse che è quasi gratis da esplorare. Mantieni grossolana la griglia costosa; abbonda su quella economica.
  5. La velocità sposta il vincolo stringente dal calcolo alla statistica. Uno sweep denso e veloce è una macchina da test multipli. Deflaziona per il numero di prove, valida fold per fold e preferisci i plateau ai picchi — il pasto gratis è reale, ma il conto statistico non è facoltativo.

Il motore completo — SignalCache, sweep_separations, la simulazione multi-timeframe priva di leak e il test di parità che la inchioda alla semantica live della candela in formazione — vive in scripts/engine_multitf.py (commit bfc8aaa) nel nostro backtester. La prossima volta che qualcuno ti dice che una strategia a 18 parametri è troppo grande da esplorare, chiedigli quanti di quei parametri muovono davvero gli indicatori. Di solito è un terzo, e il resto è quasi gratis.

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.