Wie gewichtet man ein Multi-Faktor-Modell sicher neu? Regime, Shadow und Weight Drift
Sobald ein Faktor einen positiven IC zeigt, kann der gefährlichste nächste Schritt darin bestehen, sein Gewicht sofort zu erhöhen.
Im vorherigen Experiment ging es um die Validierung des Faktors selbst: signal-time snapshot einfrieren, danach Bucket Spread, Rank IC und Outcomes über mehrere Horizonte vergleichen. Das beantwortet die Frage: Enthält der Faktor tatsächlich Ranking-Information?
Bei automatischer Neugewichtung entsteht eine andere Frage: Warum darf ein Faktor, der in den letzten 20 Perioden stärker aussah, sofort das active model überschreiben?
Beides wird leicht miteinander verwechselt.
Ein Faktor kann echte Information enthalten und im jüngsten Fenster trotzdem überschätzt sein. Umgekehrt kann ein Regime wirklich gewechselt haben, während zu starke Schutzmechanismen das Modell daran hindern, schnell genug zu reagieren. Adaptive Gewichtung ist nicht fertig, sobald aus Research-ICs ein neuer Weight Vector entsteht. Das schwierigere Problem ist der Modellwechsel selbst.
Ich habe dafür weder alte Produktionsgewichte noch Schwellenwerte aus meinen Notion-Notizen als Beweis verwendet. Stattdessen habe ich ein kontrolliertes synthetisches Regime-Switching-Experiment gebaut. Weil die Ground Truth bekannt ist, lässt sich genau erkennen, wann sich die Faktorbeziehungen wirklich ändern. Verglichen wurden drei Activation Policies:
- jedes neue Gewicht sofort aktivieren;
- jedes neue Gewicht aktivieren, aber die maximale Änderung pro Faktor begrenzen;
- den Candidate zuerst im Shadow laufen lassen, ihn erst nach mehreren Siegen gegen Active promoten und auch beim Promotion einen Delta Cap erzwingen.
Das Experiment verwendet 12 feste Random Seeds. Pro Seed gibt es 240 fortlaufende Perioden, 200 synthetische Assets pro Periode und 4 Faktoren. Regime-Wechsel liegen explizit bei Periode 80 und 160. Alle 5 Perioden wird ein Candidate erzeugt. Die Schätzung sieht ausschließlich die vorherigen 20 Perioden; bewertet wird nur auf den folgenden 5 Perioden.
Diese zeitliche Grenze ist bewusst streng. Die Dokumentation zu TimeSeriesSplit von scikit-learn weist darauf hin, dass gewöhnliche Cross-Validation bei zeitlich geordneten Daten zu ungültigen Splits führen kann, wenn zukünftige Beobachtungen zum Training verwendet werden und anschließend die Vergangenheit bewertet wird.
Wenn Candidate Weights bereits die Zukunft gesehen haben, sind Shadow, Regime und Drift Budget danach nur noch Dekoration.
Wie groß ist der Unterschied zwischen schneller und stabiler Neugewichtung?
Die Schätzung der Candidate Weights bleibt absichtlich einfach. Im trailing window wird für jeden Faktor der cross-sectionale Rank IC berechnet. Negative Werte werden auf null gesetzt; die verbleibende positive Evidenz wird zu Candidate Weights normalisiert.
Geändert wird nur die Activation Policy.
Gemessen wurden:
- Combined-Score Rank IC im Future Block;
- kumulativer L1 Weight Turnover;
- größter einzelner Weight Jump;
- Zahl der Promotions;
- Anteil der Promotions, bei denen das neue Profil im nächsten Evaluation Block schlechter als das alte Active Profile ist;
- Zeit nach einem echten Regime-Wechsel, bis das größte Active-Gewicht auf den stärksten Faktor des neuen Regimes wechselt.
Den letzten Wert nenne ich Adaptation Lag. Das ist kein Finanzmarktstandard, sondern eine experimentelle Definition dafür, wie langsam das Modell einen bekannten Regime-Wechsel anerkennt.
Mittelwerte über 12 Seeds:
| Policy | Cap | Shadow confirmations | Mean OOS Rank IC | L1 turnover | Max weight jump | False promotion rate | Adaptation lag |
|---|---|---|---|---|---|---|---|
| Immediate | — | — | 0.2199 | 7.305 | 0.333 | 39.0% | 11.67 |
| Delta capped | 5% | — | 0.2092 | 5.591 | 0.050 | 27.8% | 22.08 |
| Shadow first | 5% | 1 | 0.2001 | 4.197 | 0.050 | 20.6% | 29.79 |
| Shadow first | 5% | 2 | 0.1882 | 2.409 | 0.050 | 15.0% | 52.29 |
| Shadow first | 5% | 3 | 0.1850 | 1.659 | 0.050 | 10.2% | 75.83 |
| Delta capped | 10% | — | 0.2172 | 6.884 | 0.100 | 36.2% | 14.58 |
| Shadow first | 10% | 1 | 0.2077 | 5.015 | 0.100 | 27.6% | 22.71 |
| Shadow first | 10% | 2 | 0.1937 | 3.472 | 0.100 | 13.7% | 37.08 |
| Shadow first | 10% | 3 | 0.1869 | 2.411 | 0.100 | 11.2% | 47.71 |
Die Tabelle liefert keinen einzelnen „sichersten“ Parameter.
Immediate erzielt den höchsten Mean OOS Rank IC und reagiert am schnellsten auf Regime-Wechsel. Der durchschnittliche Adaptation Lag liegt nur bei 11.67 Perioden. Dafür ist der kumulative Turnover am höchsten, an praktisch jedem der 44 Checkpoints ändern sich Gewichte, und die False-Promotion-Rate liegt bei rund 39%.
Ein echter 5%-Delta-Cap senkt den Mean OOS Rank IC von 0.2199 auf 0.2092 und verlangsamt die Anpassung. Gleichzeitig fällt der L1 Turnover von 7.305 auf 5.591 und die False-Promotion-Rate von 39.0% auf 27.8%.
Mit Shadow wird der Effekt noch stärker. Bei 5% Cap und drei erforderlichen Shadow-Siegen sinkt die False-Promotion-Rate auf etwa 10.2% und der Turnover auf 1.659. Das wirkt sehr stabil – bis man den Adaptation Lag betrachtet: 75.83 Perioden.
Wer nur „möglichst wenige falsche Promotions“ optimiert, könnte die letzte Einstellung wählen. Nach einem echten Regime-Wechsel ist das Modell dann möglicherweise so stark geschützt, dass es kaum noch reagieren kann.
Für mich zeigt das Experiment vor allem: Ein Guardrail ist keine kostenlose Sicherheitsschicht. Er wird Teil des Modells.
AWS SageMaker nutzt bei Shadow Tests ein ähnliches Produktionsmuster. Eine neue Variant erhält gespiegelte Production Requests, ihre Response wird jedoch nicht an den Caller zurückgegeben. Erst nach dem Vergleich mit der Production Variant wird über eine Promotion entschieden. Dieses Muster lässt sich gut auf Quant-Systeme übertragen. Die SageMaker-Dokumentation sagt aber nicht, nach wie vielen Siegen ein Candidate live gehen sollte, und sie kann auch keinen passenden Weight Cap für ein Trading-Modell bestimmen.
Diese Schwellen müssen aus den eigenen Outcomes, Turnover-Kosten und der tolerierbaren Reaktionszeit auf Regime-Wechsel abgeleitet werden.
Ein Regime ist kein Etikett – es verändert, welcher Vergangenheit man vertrauen sollte
Bei dynamischen Gewichten entsteht schnell der Eindruck, mehr historische Samples machten jede Schätzung automatisch robuster.
Bei stabilen Beziehungen stimmt das oft. Wenn sich Faktorverhalten aber je nach Regime ändert, kann ein langes Fenster mit einer ganz anderen Vergangenheit zwar die Stichprobe vergrößern, gleichzeitig aber die aktuell relevante Beziehung verwässern.
Die 2025 im Journal of Portfolio Management veröffentlichte Arbeit von Shu und Mulvey, Dynamic Factor Allocation Leveraging Regime-Switching Signals, untersucht Dynamic Allocation mit factor-spezifischen Regime-Signalen. Mein Experiment reproduziert dieses Modell nicht und die Publikation beweist nichts über mein System. Sie zeigt aber, dass regime-abhängige Faktorallokation ein reales Forschungsfeld ist.
In einem automatischen Reweighting-System würde ich Market / Regime deshalb als Evaluation Context behandeln, nicht als spätes Label.
Für denselben Momentum-Faktor kann der Kontext so aussehen:
momentum evidence
+ market
+ regime
+ session / universe
+ data quality
↓
candidate evidence
Man muss nicht alles in viele kleine Buckets zerlegen. Auch Hierarchical Shrinkage oder andere Pooling-Ansätze sind möglich. Entscheidend ist, „historisch insgesamt wirksam“ nicht direkt in „jetzt Gewicht erhöhen“ zu übersetzen.
Die aktuelle Model-Risk-Management-Guidance der US Federal Reserve behandelt Bankmodelle, nicht Trading-Strategien. Ein Prinzip ist trotzdem übertragbar: Ongoing Monitoring soll prüfen, ob sich Data Relevance, Exposures, Activities oder Market Conditions so verändert haben, dass das Modell nicht mehr wie erwartet funktioniert. Outcomes Analysis kann dann Anpassung oder Redevelopment begründen.
Für Quant-Systeme ist dieser Blick hilfreich. Ein Weight Vector ist weder eine ewige Konstante nach einem Training noch eine Wegwerfvariable, die in jedem Fenster spontan neu berechnet wird. Er braucht Version, Context und explizite Change Conditions.
Deshalb steht der vorherige Artikel zur Faktorvalidierung vor diesem Thema. Erst klären, ob Factor Evidence vertrauenswürdig ist; danach entscheiden, wie Candidate Weights verändert werden dürfen. In umgekehrter Reihenfolge reagiert das Modell womöglich sehr präzise auf Rauschen.
Ein 5%-Cap wurde nach dem Normalisieren fast zu 9.1%
Der nützlichste Teil des Experiments war nicht die Ergebnistabelle, sondern ein Implementierungsfehler.
Die erste Version des Delta Caps sah völlig plausibel aus:
target weight
↓
jeden Faktor auf old ± 5% clippen
↓
normalisieren, damit Summe = 1
Intuitiv scheint alles sicher. Nach dem Clipping auf ±5% sollte das Normalisieren doch nur die Summe korrigieren.
Im tatsächlichen Lauf erreichte der beobachtete maximale Weight Jump bei einem nominellen 5%-Cap im Mittel jedoch rund 9.1%.
Der Grund: Das Clipping begrenzt die Koordinaten nur vor der Normalisierung. Werden alle Gewichte danach gemeinsam skaliert, können einzelne Dimensionen wieder weiter vom alten Wert wegwandern.
Dieser scheinbar harmlose Code:
clipped = np.clip(target, old - cap, old + cap)
weights = clipped / clipped.sum()
garantiert daher nicht:
abs(weights[i] - old[i]) <= cap
Solche Fehler sind gefährlicher als eine leicht falsche IC-Nachkommastelle. Im Monitoring kann weiter cap=0.05 stehen, während das Modell faktisch größere Sprünge macht.
Die finale Implementierung verwendet deshalb eine bounded simplex projection. Der neue Weight Vector muss gleichzeitig nichtnegativ sein, sich zu 1 summieren und in jeder Dimension innerhalb von old ± cap bleiben.
sum(w) = 1
0 <= w_i <= 1
old_i - cap <= w_i <= old_i + cap
Die Projektion nutzt eine kleine binäre Suche:
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
Nach der Korrektur liegt der beobachtete maximale Sprung im finalen Experiment beim 5%-Cap bei 0.05 und beim 10%-Cap bei 0.10, abgesehen von Floating-Point-Epsilon.
Dadurch sehe ich ein Drift Budget inzwischen anders: Es sollte nicht nur als Konfiguration existieren.
Steht im System:
max_weight_delta: 0.05
sollte zur Laufzeit auch geprüft werden:
max(abs(new_weight - old_weight)) <= max_weight_delta + epsilon
Sonst hat man einen Parameter, aber keine echte Constraint.
Shadow soll nicht besser vorhersagen, sondern Research und Production State trennen
Ich würde den Lifecycle eines adaptiven Faktors heute in vier Ebenen aufteilen:
observe
↓
candidate
↓
shadow
↓
active
Observe sammelt Evidenz, beeinflusst aber kein Live Scoring.
Candidate bedeutet, dass Research ein vergleichbares neues Weight Profile erzeugt hat. Es ist noch kein Production State.
Shadow vergleicht Candidate und Active parallel auf denselben zukünftigen Outcomes. Nur weil der Candidate mit neueren Daten geschätzt wurde, bekommt er kein automatisches Recht auf Deployment.
Promotion ist erst der eigentliche State Change.
Mit dieser Trennung werden mehrere Unterschiede klar:
Factor IC verbessert sich
≠ Candidate ist sicher besser
Candidate gewinnt ein Fenster
≠ Promotion ist gerechtfertigt
Promotion ist erlaubt
≠ Gewichte dürfen unbegrenzt springen
False Promotion habe ich in diesem Experiment bewusst eng definiert: Wird ein neues Profil promoted und verliert es im nächsten Future Evaluation Block gegen das vorherige Active Profile, zählt das als False Promotion.
Das ist nicht die einzig mögliche Definition, reicht aber aus, um das Problem sichtbar zu machen. Ein Candidate kann im jüngsten Estimation Window gut aussehen und direkt nach dem Wechsel verlieren.
Mehr Shadow Confirmations reduzieren solche Fälle, verzögern aber auch die Reaktion auf einen echten Regime-Wechsel.
Deshalb würde ich in einer Production Pipeline nicht sofort festschreiben:
3 Siege in Folge -> deploy
Stattdessen würde ich zuerst speichern:
candidate_version
active_version
market / regime
evidence_window
future_eval_window
weight_delta
turnover
shadow_outcome
promotion_decision
und mit echten settled outcomes untersuchen, welchen Tradeoff Confirmation Count und Drift Budget tatsächlich erzeugen.
In alten Notion-Notizen standen zwei oder drei Bestätigungen sowie Drift-Grenzen wie 5%, 8% oder 12–15%. Dieses Experiment beweist keinen dieser Werte als richtig. Es zeigt nur, dass solche Controls das Modellverhalten deutlich verändern und der Tradeoff messbar ist.
Automatisches Reweighting würde ich als begrenzten Model-Change-Prozess behandeln
Auf Systemebene lässt sich der minimale Loop recht knapp formulieren:
1. aktuelle active weights einfrieren
2. factor evidence nur aus Vergangenheitsdaten schätzen
3. market / regime context anhängen
4. candidate weights erzeugen
5. hard constraint / drift check
6. candidate in shadow schicken
7. future outcomes abwarten
8. active und shadow vergleichen
9. nur bei erfüllter promotion rule eine neue active version erzeugen
10. vollständigen decision trail speichern
Schritt 9 sollte nicht einfach if ic > threshold heißen.
Mindestens möchte ich wissen:
- wie lange der Candidate bereits gewinnt;
- ob die Gewinne nur aus demselben Regime kommen;
- wie groß der Weight Jump bei Promotion wäre;
- wie hoch der jüngste kumulative Turnover bereits ist;
- ob der Unterschied zwischen Candidate und Active aus einem Faktor oder dem gesamten Profile kommt;
- wie viel echte Regime-Adaptation verloren geht, wenn noch nicht promoted wird.
„Recent Rank IC = 0.08“ beantwortet keine dieser Fragen.
Ich würde sogar die Promotion Decision selbst als Outcome-Tracking-Objekt behandeln. Jedes Mal, wenn das System sagt „jetzt Gewichte wechseln“, kann später geprüft werden: Was wäre passiert, wenn wir nicht gewechselt hätten? Was wäre passiert, wenn wir noch ein Fenster gewartet hätten?
Erst mit genügend solchen Records lässt sich aus realer Evidenz entscheiden, ob Shadow Confirmation 1, 2 oder 3 sein sollte und ob ein Delta Cap eher 5% oder 10% betragen darf.
Dann ist es nicht mehr bloß Parameter Tuning.
Ein adaptives Weighting-System muss nicht nur lernen, welcher Faktor zuletzt funktioniert hat, sondern auch, ob seine eigenen Model-Change-Regeln historisch geholfen haben.
Genau dort endet dieses synthetische Experiment. Immediate reagiert schnell, erzeugt aber viel Churn. Shadow ist vorsichtiger, kann aber so langsam werden, dass es ein Regime verpasst. Ein Delta Cap begrenzt einzelne Bewegungen, ersetzt jedoch keine Future-Outcome-Validierung.
Ich werde deshalb keine Zahl aus dieser Tabelle direkt in eine Production Config übernehmen.
Der sinnvollere nächste Schritt ist, diese Guardrails eine Weile im Shadow gegen echte settled outcomes laufen zu lassen: jeden Candidate speichern, den das System promoten wollte, und später prüfen, ob er tatsächlich weiter gewonnen hat. Erst dann kann das System aus realen Daten lernen, wie viele False Promotions tolerierbar sind und wie viel Adaptation Lag es für Stabilität bezahlen will.