Quando un fattore mostra un IC positivo, la mossa successiva più pericolosa potrebbe essere aumentargli subito il peso.

Nel test precedente avevo separato il problema della validazione: congelare il signal-time snapshot, poi osservare Bucket Spread, Rank IC e outcome su più orizzonti. Quella fase risponde a una domanda: questo fattore contiene davvero informazione di ranking?

Con il reweighting automatico la domanda cambia: perché un fattore che sembra più forte negli ultimi 20 period dovrebbe poter riscrivere subito l’active model?

È facile confondere le due cose.

Un fattore candidato può avere informazione reale e, allo stesso tempo, essere sovrastimato nella finestra più recente. Al contrario, il regime può essere cambiato davvero mentre troppe protezioni rendono il modello troppo lento nel seguirlo. Il reweighting adattivo non finisce quando trasformiamo un IC di ricerca in un nuovo vettore di weights. Il problema più difficile è il cambio di stato del modello.

Per questo non ho usato vecchi pesi di produzione o soglie salvate nelle note Notion come prova. Ho costruito un esperimento sintetico e controllato di regime switching. Conoscendo la ground truth posso sapere esattamente quando la relazione tra fattori cambia davvero e confrontare tre activation policy:

  • attivare immediatamente ogni nuova stima dei pesi;
  • attivare ogni stima, ma limitare la variazione massima di ciascun fattore;
  • lasciare prima il candidate in shadow, promuoverlo solo dopo vittorie consecutive contro active e applicare comunque un delta cap al promotion.

L’esperimento usa 12 random seed fissi. Ogni seed contiene 240 period consecutivi, 200 synthetic assets per period e 4 factors. Il regime cambia esplicitamente ai period 80 e 160. Ogni 5 period viene creato un candidate usando soltanto i 20 period precedenti; la valutazione usa esclusivamente i 5 period successivi.

Questo confine temporale è volutamente rigido. La documentazione di scikit-learn su TimeSeriesSplit spiega perché la cross-validation ordinaria può essere inadatta ai dati ordinati nel tempo quando finisce per allenare sul futuro e valutare il passato.

Se il candidate weight ha già visto il futuro, shadow, regime e drift budget aggiunti dopo non possono correggere il problema.

Quanto cambia davvero tra ripesare velocemente e ripesare in modo stabile?

Ho tenuto volutamente semplice lo stimatore dei candidate weights. Nella trailing window calcolo il Rank IC cross-sectional di ciascun factor, porto a zero i valori negativi e normalizzo l’evidenza positiva rimanente per ottenere i candidate weights.

A cambiare è soltanto l’activation policy.

Ho misurato:

  • combined-score Rank IC nel future block;
  • L1 weight turnover cumulato;
  • massimo weight jump singolo;
  • numero di promotions;
  • quota di promotions in cui il nuovo profile perde contro il vecchio active profile nel successivo evaluation block;
  • quanto tempo, dopo un vero regime switch, serve perché il peso maggiore dell’active profile passi al fattore più forte del nuovo regime.

Chiamo quest’ultima misura adaptation lag. Non è una metrica standard della finanza: è una definizione dell’esperimento per quantificare quanto lentamente il modello riconosce un cambio di regime noto.

Medie sui 12 seed:

PolicyCapShadow confirmationsMean OOS Rank ICL1 turnoverMax weight jumpFalse promotion rateAdaptation lag
Immediate——0.21997.3050.33339.0%11.67
Delta capped5%—0.20925.5910.05027.8%22.08
Shadow first5%10.20014.1970.05020.6%29.79
Shadow first5%20.18822.4090.05015.0%52.29
Shadow first5%30.18501.6590.05010.2%75.83
Delta capped10%—0.21726.8840.10036.2%14.58
Shadow first10%10.20775.0150.10027.6%22.71
Shadow first10%20.19373.4720.10013.7%37.08
Shadow first10%30.18692.4110.10011.2%47.71

Questa tabella non produce un unico “parametro più sicuro”.

Immediate ottiene il mean OOS Rank IC più alto e segue il cambio di regime più rapidamente, con adaptation lag medio di appena 11.67 period. Il costo è altrettanto evidente: turnover cumulato massimo, cambi di peso praticamente a tutti i 44 checkpoint e false promotion rate vicino al 39%.

Con un vero delta cap del 5%, il mean OOS Rank IC scende da 0.2199 a 0.2092 e l’adattamento rallenta. In compenso il L1 turnover scende da 7.305 a 5.591 e il false promotion rate dal 39.0% al 27.8%.

Shadow aggiunge un ulteriore filtro. Con cap del 5% e tre vittorie shadow consecutive, il false promotion rate scende a circa 10.2% e il turnover a 1.659. Sembra molto stabile, finché non si guarda l’adaptation lag: 75.83 period.

Se ottimizzassi soltanto “meno promotion sbagliate”, l’ultima configurazione sarebbe attraente. Ma dopo un vero regime switch il modello potrebbe essere così protetto da quasi non reagire.

L’esperimento mi ha chiarito una cosa: un guardrail non è uno strato di sicurezza gratuito. Diventa parte del modello.

I shadow tests di AWS SageMaker usano un pattern di produzione simile: una nuova variant può ricevere richieste replicate dalla produzione senza restituire la response al caller, così da essere confrontata con la production variant prima di decidere il promotion. È un pattern utile anche per un sistema quant, ma la documentazione di SageMaker non dice quante vittorie consecutive giustificano il promotion e non può scegliere il weight cap di un modello di trading.

Quelle soglie devono derivare dai propri outcome, dal costo del turnover e dalla tolleranza a reagire lentamente a un cambio di regime.

Regime non è un’etichetta: cambia quale parte del passato conviene credere

Un errore comune nel dynamic weighting è pensare che più campioni storici rendano sempre la stima più sicura.

Se la relazione sottostante è stabile, più osservazioni possono aiutare. Ma il comportamento di un factor può dipendere dal regime. Mescolare in una sola trailing window dati molto vecchi di un ambiente diverso può aumentare il campione e allo stesso tempo diluire la relazione che conta oggi.

Il paper di Shu e Mulvey del 2025 su Journal of Portfolio Management, Dynamic Factor Allocation Leveraging Regime-Switching Signals, studia dynamic allocation con inferenza del regime specifica per fattore. Il mio esperimento non replica quel modello e quei risultati non validano il mio sistema, ma mostrano che la factor allocation condizionata al regime è un vero filone di ricerca.

In un sistema di reweighting automatico preferisco trattare market / regime come contesto di valutazione, non come label aggiunta alla fine.

Per lo stesso momentum factor, il contesto può essere pensato così:

momentum evidence
    + market
    + regime
    + session / universe
    + data quality
        ↓
candidate evidence

Non è necessario dividere tutto in tanti bucket indipendenti. Si possono usare hierarchical shrinkage o altri approcci di pooling. Il punto è non tradurre direttamente “ha funzionato storicamente” in “adesso bisogna aumentare il peso”.

L’attuale model risk management guidance della Federal Reserve statunitense riguarda la governance dei modelli bancari, non le strategie di trading. Ma contiene un principio trasferibile: ongoing monitoring dovrebbe verificare se variazioni nella data relevance, nelle exposures, nelle activities o nelle market conditions fanno sì che il modello non si comporti più come previsto; l’outcomes analysis può poi giustificare adjustment o redevelopment.

Questo modo di ragionare è utile anche nei sistemi quant. Un weight vector non è né una costante eterna dopo un training, né una variabile temporanea da ricalcolare liberamente a ogni window. Ha bisogno di version, context e condizioni esplicite di cambiamento.

Per questo il precedente articolo sulla validazione dei fattori viene prima di questo. Prima si decide se la factor evidence merita fiducia, poi si decide come il candidate weight può cambiare. Invertendo l’ordine, il modello rischia di reagire con grande precisione al rumore.

Un cap del 5% è quasi diventato 9.1% dopo normalize

La parte più utile dell’esperimento non è stata la tabella dei risultati, ma un errore di implementazione.

La prima versione del delta cap era molto naturale:

target weight
    ↓
clip di ogni factor a old ± 5%
    ↓
normalize affinché la somma torni a 1

A prima vista sembra sicuro. Se i valori sono già stati limitati a ±5%, normalize dovrebbe soltanto correggere la somma.

Nell’esecuzione reale, però, un cap nominale del 5% ha prodotto un observed max weight jump medio di circa 9.1%.

Il motivo è semplice: clip limita le coordinate prima della normalizzazione. Riscalando tutti i pesi dopo, alcune dimensioni possono allontanarsi di nuovo dal valore precedente.

Quindi questo codice apparentemente corretto:

clipped = np.clip(target, old - cap, old + cap)
weights = clipped / clipped.sum()

non garantisce:

abs(weights[i] - old[i]) <= cap

Un bug del genere può essere più pericoloso di un IC con una cifra decimale sbagliata. Il monitor può continuare a mostrare cap=0.05, mentre il modello sta muovendo i pesi molto di più.

La versione finale usa una bounded simplex projection. Il nuovo vettore di pesi deve rispettare contemporaneamente tre vincoli: pesi non negativi, somma uguale a 1 e ogni coordinata dentro old ± cap.

sum(w) = 1
0 <= w_i <= 1
old_i - cap <= w_i <= old_i + cap

La proiezione usa una piccola ricerca binaria:

lower = np.maximum(0.0, old - cap)
upper = np.minimum(1.0, old + cap)

lo = np.min(target - upper) - 1.0
hi = np.max(target - lower) + 1.0

for _ in range(80):
    lam = (lo + hi) / 2.0
    w = np.clip(target - lam, lower, upper)

    if w.sum() > 1.0:
        lo = lam
    else:
        hi = lam

Dopo la correzione, nell’esperimento finale il maximum observed jump è 0.05 con cap 5% e 0.10 con cap 10%, a parte epsilon floating-point.

Questo ha cambiato il modo in cui penso al drift budget: non dovrebbe esistere solo nella configurazione.

Se il sistema contiene:

max_weight_delta: 0.05

a runtime dovrebbe anche verificare:

max(abs(new_weight - old_weight)) <= max_weight_delta + epsilon

Altrimenti abbiamo un parametro, non un vincolo reale.

Shadow non serve a prevedere meglio: separa la conclusione di research dal production state

Oggi preferisco dividere il lifecycle di un fattore adattivo in quattro livelli:

observe
   ↓
candidate
   ↓
shadow
   ↓
active

Observe raccoglie evidence senza modificare il live scoring.

Candidate significa che la ricerca ha prodotto un weight profile che vale la pena confrontare, ma non è ancora production state.

Shadow confronta candidate e active in parallelo sugli stessi future outcomes. Usare dati più recenti non dà automaticamente al candidate il diritto di deployment.

Promotion è il vero cambio di stato.

Separando questi livelli diventano più chiare alcune differenze:

factor IC migliora
≠ candidate è sicuramente migliore

candidate vince una finestra
≠ bisogna fare promotion

promotion è consentito
≠ i pesi possono muoversi senza limiti

La mia definizione di false promotion in questo esperimento è volutamente stretta: se un nuovo profile viene promoted e nel successivo future evaluation block perde contro il vecchio active profile, conto una false promotion.

Non è l’unica definizione possibile, ma basta per mostrare il problema. Un candidate che appare forte nell’ultimo estimation window può perdere subito dopo l’attivazione.

Più shadow confirmations riducono questi casi, ma ritardano anche la risposta a un vero cambio di regime.

Per questo non fisserei subito:

3 vittorie consecutive -> deploy

Salverei prima:

candidate_version
active_version
market / regime
evidence_window
future_eval_window
weight_delta
turnover
shadow_outcome
promotion_decision

e userei outcome reali già chiusi per misurare che cosa stanno davvero scambiando confirmation count e drift budget.

Nelle vecchie note Notion avevo ipotesi come due o tre conferme e limiti del 5%, 8% o 12–15%. Questo esperimento non dimostra che nessuno di quei numeri sia corretto. Dimostra soltanto che questi controlli cambiano in modo sostanziale il comportamento del modello e che il tradeoff è osservabile.

Tratterei il reweighting automatico come un processo di cambio modello vincolato

Riportato a un design di sistema, il loop minimo non è complicato:

1. congelare gli active weights correnti
2. stimare factor evidence solo dai dati passati
3. aggiungere market / regime context
4. generare candidate weights
5. applicare hard constraint / drift check
6. eseguire candidate in shadow
7. attendere che i future outcomes siano maturi
8. confrontare active e shadow
9. creare una nuova active version solo se la promotion rule è soddisfatta
10. salvare l'intero decision trail

Il punto 9 non dovrebbe essere soltanto if ic > threshold.

Come minimo voglio sapere:

  • da quanto tempo il candidate sta vincendo;
  • se tutte le vittorie arrivano dallo stesso regime;
  • quanto grande sarebbe il weight jump del promotion;
  • quanto turnover si è già accumulato di recente;
  • se la differenza tra candidate e active arriva da un solo factor o dall’intero profile;
  • quanto adaptation lag accetto se non promuovo ancora.

“Recent Rank IC = 0.08” non risponde a nessuna di queste domande.

Andrei oltre: tratterei la promotion decision stessa come un oggetto di outcome tracking. Ogni volta che il sistema dice “cambia i pesi adesso”, tornerei in seguito a chiedere: cosa sarebbe successo senza cambiare? E aspettando una window in più?

Solo con abbastanza record ha senso decidere sulla propria evidenza se shadow confirmation debba essere 1, 2 o 3, o se il delta cap debba essere più vicino a 5% o 10%.

A quel punto non è più semplice parameter tuning.

Un adaptive weighting system deve imparare non solo quale factor sta funzionando adesso, ma anche se le proprie regole di cambio modello hanno storicamente aiutato.

È qui che si ferma questo esperimento sintetico. Immediate reagisce rapidamente ma genera molto churn. Shadow è prudente, ma può diventare così lento da perdere il regime. Un delta cap limita la dimensione dei singoli movimenti, ma non sostituisce la validazione su future outcomes.

Non prenderò un numero da questa tabella per incollarlo direttamente nella production config.

Il passo successivo più utile è lasciare questi guardrail per un po’ in shadow contro outcome reali già chiusi: registrare ogni candidate che il sistema avrebbe voluto promuovere e verificare in seguito se ha continuato a vincere. Quando l’evidenza reale sarà sufficiente, il sistema potrà iniziare a decidere quante false promotion può tollerare e quanto adaptation lag è disposto a pagare per la stabilità.