MetaTrader 5 – Leitfaden für maschinelles Lernen (Teil 8): Bayes’sche Hyperparameter-Optimierung mit Purged Cross-Validation und Trial Pruning
Inhaltsverzeichnis
- Das Problem mit Standard-HPO im finanziellen ML
- Optuna – Architektur und Kernkonzepte
- Der Datenkontrakt und _WeightedEstimator
- FinancialModelSuggester
- Zielfunktion – Purged K-Fold mit Pruning
- Finanzorientiertes Pruning
- Orchestrierung und Speicherung
- Von der Optuna-Studie zu Scikit-Learn cv_results_
- Visualisierung
- Praktische Überlegungen
- Schlussfolgerung
- Beigefügte Dateien
Einführung
Sie erstellen einen finanziellen ML-Klassifikator auf Basis von Triple-Barrier- oder Meta-Labels und müssen Hyperparameter fair auswählen – ohne die Suche selbst in eine Overfitting-Maschine zu verwandeln. Standard-Tools versagen bei diesem Anwendungsfall in drei konkreten Punkten. GridSearchCV und RandomizedSearchCV lernen nicht aus vergangenen Versuchen (Trials), sodass sie Budget verschwenden, indem sie Bereiche des Hyperparameter-Raums erneut prüfen, die sich bereits als schlecht erwiesen haben. Sie können eine wenig vielversprechende Konfiguration nicht nach dem ersten aufwendigen Fold PurgedKFold stoppen, sodass jeder Versuch den vollen Aufwand aller Folds trägt, selbst wenn der erste Fold bereits ein Scheitern signalisiert. Und sie lassen sich nicht natürlich in den Datenkontrakt für Finanzdaten integrieren – PurgedKFold als einziger gültiger Splitter, separate Gewichtungen für Anpassung und Bewertung sowie persistente Speicherung, damit eine lange Suche Abstürze übersteht und parallele Worker unterstützt. Die messbaren Symptome sind verschwendete Rechenleistung, verzerrte Out-of-Sample-Vergleiche aufgrund von Informationslecks über die Purge-Grenze hinweg und fragile Experimente, die nach jeder Unterbrechung von Grund auf neu gestartet werden müssen.
Dieser Artikel zeigt einen praktischen Ersatz: Optuna (TPE-Sampler + Pruning + SQLite-Speicherung), nativ verbunden mit den Finanz-Cross-Validation- und Gewichtungskonventionen, die in früheren Teilen dieser Serie etabliert wurden. Nach der Lektüre verfügen Sie über eine konkrete, ausführbare HPO-Struktur, die aus fünf Komponenten besteht.
- eine Zielfunktion, die PurgedKFold Cross-Validation mit renditeattributionsgewichteter Bewertung ausführt und Ergebnisse auf Fold-Ebene für das Pruning meldet;
- FinancialModelSuggester, eine Parameter-Übersetzungsschicht, die scikit-learn-Verteilungsspezifikationen in trial.suggest_*() Aufrufe umwandelt und gleichzeitig das Stichprobengewichtungsschema und den Zerfall optimiert;
- einen finanzspezifisch kalibrierten Pruner (TradingModelPruner), der eine entropiebasierte wirtschaftliche Baseline und eine regime-skalierte Volatilitätstoleranz erzwingt;
- einen Orchestrator (optimize_trading_model) mit SQLite-Speicherung für die Wiederaufnahme und parallele Worker;
- ein Konverter von Studie zu cv_results_ für die Kompatibilität mit bestehenden scikit-learn-Analysen.
Die Ausgabe-Artefakte sind eine persistierte Study, ein neu angepasster best_estimator_ (_WeightedEstimator, der ein abgestimmtes Basismodell umschließt) und ein cv_results_ DataFrame mit Ergebnissen pro Fold – bereit für dieselben nachgelagerten Diagnosen, die im Rest der Pipeline verwendet werden.
Dieser Artikel ist Teil 8.1 der Reihe MetaTrader 5 Machine Learning Blueprint. Die vorangegangenen Artikel bauten die Komponenten auf, von denen dieses System abhängt: Teil 1 behob Datenlecks auf Bar-Ebene, Teil 2 führte Triple-Barrier- und Meta-Labels ein, Teil 3 fügte Trend-Scanning-Labels hinzu, Teil 4 befasste sich mit Label-Gleichzeitigkeit und führte durchschnittliche Eindeutigkeitsgewichtungen ein, Teil 5 führte sequentielles Bootstrapping ein und etablierte die Konvention für separate Trainings- und Bewertungsgewichtungen, Teil 6 baute die Caching-Infrastruktur auf, und Teil 7 fügte alles zu einer reproduzierbaren Produktionspipeline zusammen. Die Integration der Komponenten dieses Artikels in die Produktionspipeline – einschließlich des clf_hyper_fit-Wrappers und des Cachings – wird in Teil 8.2 behandelt.
1. Das Problem mit Standard-HPO im finanziellen ML
1.1 Warum Hyperparameter-Optimierung im Finanzwesen wichtiger ist
In den meisten ML-Bereichen liefert ein vernünftig abgestimmtes Modell mit guten Merkmalen akzeptable Ergebnisse. Im Finanzwesen ist das Signal-Rausch-Verhältnis niedrig, Nicht-Stationarität ist die Norm und die Kosten für Overfitting werden in Kapital gemessen. Wie López de Prado betont, ist jeder Freiheitsgrad in einer Pipeline eine Gelegenheit zum Overfitting. Hyperparameter sind Freiheitsgrade, und die Methode, mit der sie gesucht werden, bestimmt, ob die resultierende Konfiguration generalisiert oder memorisiert.
Das Problem wird durch die Struktur von Finanzdaten verschärft. Wie in Teil 4 dargelegt, überschneiden sich Triple-Barrier-Labels zeitlich. Die resultierende Label-Überlappung bedeutet, dass Standard-k-fold-CV Informationen über die Trainings-Test-Grenze hinweg preisgibt und jeder dadurch erzeugte Score optimistisch verzerrt ist. Das HPO-Tool muss nativ mit PurgedKFold zusammenarbeiten, um valide Vergleiche über Versuche hinweg zu erstellen.
1.2 Einschränkungen von Scikit-Learn
GridSearchCV skaliert exponentiell: Ein Random Forest mit fünf Werten für jeden der vier Hyperparameter erzeugt 625 Kombinationen, was 3.125 Modell-Fits bei fünffachem CV entspricht. RandomizedSearchCV reduziert die Kosten, bleibt aber uninformiert – jeder Versuch ist unabhängig. Keines unterstützt vorzeitigen Abbruch, Lernen über mehrere Versuche hinweg oder persistente Speicherung.
| Begrenzung | Auswirkungen auf Financial ML | |
|---|---|---|
| 1. | Kein Lernen über mehrere Versuche hinweg | Verschwendete Rechenleistung in schlechten Bereichen des Hyperparameter-Raums |
| 2. | Kein vorzeitiger Abbruch schlechter Versuche | Alle Folds werden ausgeführt, selbst wenn frühe Folds bereits schlechte Ergebnisse zeigen |
| 3. | Keine persistente Speicherung | Unterbrochene Suchen können nicht fortgesetzt werden; der Vergleich zwischen Experimenten erfordert manuelle Buchführung |
| 4. | Starre CV-Integration | Fold-basiertes Reporting für Pruning erfordert Workarounds, die das Standard-CV-Interface umgehen oder aufbrechen. |
1.3 Eine kritische Grenze: HPO vs. Strategieoptimierung
Bevor Sie fortfahren, muss eine Grenze klar gezogen werden. Intelligente Suche ist für HPO geeignet und schädlich für die Optimierung von Strategieparametern. Timothy Masters identifiziert dies präzise: Ein TPE-Sampler oder genetischer Algorithmus findet das globale Optimum der In-Sample-Fitnessoberfläche effizient – aber bei Finanzdaten besteht diese Oberfläche größtenteils aus Rauschen. Je effektiver Sie sie durchsuchen, desto stärker ist das Ergebnis überangepasst.
Die Regel ist einfach: Wenn Ihre Zielfunktion ein statistisches Maß für beschriftete Daten ist (kreuzvalidierte Genauigkeit, Log-Loss bei Triple-Barrier-Labels), verwenden Sie Optuna. Wenn es sich um ein finanzielles Maß für historische Equity-Kurven handelt (Sharpe-Ratio, Drawdown), tun Sie dies nicht. Optunas Vorteil bei HPO ist dieselbe Eigenschaft, die es für die Strategieoptimierung gefährlich macht – es findet das Optimum zu zuverlässig.
2. Optuna – Architektur und Kernkonzepte
2.1 Wie Optuna funktioniert
Optuna basiert auf drei Konzepten:
- Studie: Eine vollständige Optimierungssitzung mit einer Richtung (Maximierung oder Minimierung), die alle Ergebnisse speichert. Mit SQLite-Speicherung bleibt eine Studie über Python-Sitzungen hinweg bestehen – genau die Art von zeitbewusster Reproduzierbarkeit, die in Teil 6 etabliert wurde.
- Versuch: Eine einzelne Bewertung einer Hyperparameter-Konfiguration. Jeder Versuch schlägt Werte vor, bewertet die Zielfunktion und meldet das Ergebnis.
- Sampler: Der Algorithmus, der auswählt, welche Werte als Nächstes ausprobiert werden sollen. Der Standard ist TPE (Tree-structured Parzen Estimator), eine Bayes’sche Optimierungsmethode, die aus abgeschlossenen Versuchen lernt.
Der Hauptunterschied zu den Tools von scikit-learn: Optuna lernt aus abgeschlossenen Versuchen. Der TPE-Sampler modelliert die Beziehung zwischen Hyperparametern und der Zielfunktion und konzentriert sich zunehmend auf vielversprechende Bereiche. Jeder nachfolgende Versuch erkundet mit höherer Wahrscheinlichkeit Konfigurationen in der Nähe bekannter guter Ergebnisse.
2.2 Pruning: HyperbandPruner
Pruning stoppt einen Versuch vor Abschluss, wenn Zwischenergebnisse darauf hindeuten, dass er nicht konkurrenzfähig sein wird. In einem fünffach Purged-CV-Setup können die verbleibenden Folds übersprungen werden, wenn der erste Fold ein schlechtes Ergebnis liefert – was den gesamten Rechenaufwand direkt reduziert.
HyperbandPruner ist die richtige Wahl ohne weitere Voraussetzungen für finanzielles CV. Bei nur 5–10 Folds pro Versuch haben die meisten Pruner – die innerhalb eines Versuchs arbeiten – fast nichts, womit sie arbeiten könnten. Hyperband arbeitet über Versuche hinweg. Anstatt zu fragen „Schneidet dieser Versuch im Vergleich zu allen anderen in diesem Schritt schlecht ab?“, stellt Hyperband eine Frage zur Ressourcenallokation: „Welche Versuche verdienen bei einem festen Rechenbudget mehr Folds?“
Wie die Brackets funktionieren
Unter Verwendung der exakten Parameter aus dem Code – min_resource=1, max_resource=5 (für cv=5), reduction_factor=3 (η=3) – ergibt sich folgende Anzahl an Brackets (Ausscheidungsstufen):
s_max = ⌊log₃(5/1)⌋ = ⌊1.46⌋ = 1
Es gibt also zwei Brackets, s=0 und s=1, die gleichzeitig über das Versuchsbudget hinweg laufen. Stellen Sie sich diese wie zwei parallele Rennen vor.
Bracket s=1 – das aggressive Bracket: drei Versuche starten. Jeder wird nur bei Fold 1 ausgewertet. Der beste ⌈3/3⌉ = 1 Überlebende wird in den Folds 2–5 weitergeführt. Die anderen beiden werden nach einer einzigen Fold-Auswertung beschnitten. Gesamte Fold-Auswertungen: 3×1 + 1×4 = 7.
Bracket s=0 – das konservative Bracket: Zwei Versuche beginnen und beide laufen bedingungslos bis Fold 5. In diesem Bracket erfolgt kein Pruning, unabhängig von Zwischenergebnissen. Gesamte Fold-Auswertungen: 2×5 = 10.
Das konservative Bracket ist kein Fallback – es ist eine garantierte Absicherung, die parallel läuft. Eine Parameterkombination, die bei Fold 1 schlecht aussieht (vielleicht deckt dieser Fold einen Krisenzeitraum ab), aber tatsächlich die beste Konfiguration ist, erhält durch das konservative Bracket immer eine vollständige Auswertung. Dies unterscheidet Hyperband von SuccessiveHalvingPruner, was isoliert betrachtet einfach Bracket s=1 ist und diesen Versuch vollständig verwerfen würde.
| Bracket s=1 (aggressiv) | Bracket s=0 (konservativ) | |
|---|---|---|
| Versuche gestartet | 3 | 2 |
| Nach Fold 1 abgebrochen | 2 | 0 |
| Schließt alle Folds ab | 1 | 2 |
| Gesamte Fold-Auswertungen | 7 | 10 |
Abb. 1 – HyperbandPruner Bracket-Struktur
Sollten Sie einen benutzerdefinierten Pruner auf Basis von HyperbandPruner erstellen?
Nein – und ein Wrapping würde ihn aktiv beschädigen. HyperbandPruner verwaltet den internen Status durch Bracket-Zuweisungen, die zum Zeitpunkt der Versuchserstellung vorgenommen werden. Wenn trial.should_prune() aufgerufen wird, prüft Hyperband, zu welchem Bracket dieser Versuch gehört, auf welcher Stufe er sich befindet und ob er unter das oberste 1/η der Versuche auf dieser Stufe fällt. Dies ist ein zustandsbehaftetes Turnierverwaltungssystem, keine einfache Schwellenwertprüfung. Das Ableiten davon und das Hinzufügen von Finanzregeln in prune() erzeugt zwei Fehlermodi: Ihre Regeln könnten einen Versuch abbrechen, den Hyperband für seine konservative Bracket vorgesehen hatte (was die Garantie der vollständigen Auswertung verletzt), und die Stufenaufzeichnungen von Hyperband werden inkonsistent, da ein durch Ihre Regel abgebrochener Versuch nie auf seiner geplanten Stufe verglichen wurde, was die Medianvergleiche für alle nachfolgenden Versuche verfälscht.
TradingModelPruner funktioniert genau deshalb, weil er MedianPruner umschließt, der keinen Bracket-Zustand hat, der beschädigt werden könnte. Der korrekte Weg, eine finanzspezifische Logik bei der Verwendung von HyperbandPruner hinzuzufügen, besteht darin, die Prüfungen innerhalb der Zielfunktion vor dem Aufruf von trial.should_prune() zu platzieren:
for fold_idx, (train_idx, val_idx) in enumerate(cv.split(X, y)): # ... fit and score ... fold_scores.append(score) # 1. Financial domain check fires first — before Hyperband's bracket logic if score < min_score_threshold: trial.set_user_attr("pruned_reason", "below_baseline") raise TrialPruned(f"Below economic baseline at fold {fold_idx}") # 2. Hyperband bracket management — rung comparison across trials trial.report(score, step=fold_idx) if trial.should_prune(): raise TrialPruned()
Die finanzspezifische Logik wird vor der Meldung an Hyperband ausgelöst, sodass der Versuch auf dieser Stufe einfach nicht erscheint und der interne Zustand von Hyperband konsistent bleibt. Die beiden Mechanismen sind sauber voneinander getrennt.
from optuna.pruners import HyperbandPruner pruner = HyperbandPruner( min_resource=1, max_resource=n_splits, reduction_factor=3, )
2.3 Define-by-Run-API
Optuna spezifiziert den Suchraum innerhalb der Zielfunktion. Dies ermöglicht bedingte Hyperparameter-Räume und erlaubt es FinancialModelSuggester, weight_scheme, weight_decay und weight_linear neben den Basismodellparametern hinzuzufügen, wodurch die Gewichtungsstrategie gemeinsam mit dem Modell optimiert wird.
2.4 Persistente Speicherung und Wiederaufnehmbarkeit
Optuna speichert Studienergebnisse in SQLite (oder PostgreSQL, MySQL). Ein abgestürzter oder zeitlich abgelaufener Lauf wird über load_if_exists=True ab dem letzten abgeschlossenen Versuch fortgesetzt. Dies steht im Einklang mit dem Reproduzierbarkeitsprinzip, das in Teil 7 festgelegt wurde: Jedes Experiment muss wiederaufnehmbar und prüfbar sein und bei einer erneuten Ausführung identische Ergebnisse liefern. Mehrere Worker können Versuche parallel für dieselbe Studie ausführen, wobei das 30-sekündige SQLite-Timeout Sperrkonflikte verhindert.
3. Der Datenkontrakt und _WeightedEstimator
Alle Komponenten in diesem System teilen sich einen gemeinsamen Datenkontrakt, der von der Produktionspipeline übernommen wurde, die in Teil 7 festgelegt wurde:
- X: Feature-DataFrame mit einem DatetimeIndex, erstellt durch die Feature-Engineering-Pipeline.
- y: Zielreihe ausgerichtet mit X – Triple-Barrier- oder Trend-Scanning-Labels aus Teil 2 und Teil 3.
- events: DataFrame, indexiert nach Ereigniszeit, der mindestens Folgendes enthält: t1 (Zeitpunkt der Barrierenberührung), w (Rendite-Attributionsgewicht |Rendite|) und tW (auf Einzigartigkeit basierendes Gewicht aus Teil 4).
- data_index: Der DatetimeIndex des vollständigen Bar-Datensatzes – wird von _WeightedEstimator verwendet, um Gewichte über das korrekte Universum von Bars zu berechnen.
3.1 Zwei unterschiedliche Gewichtungsrollen
Bevor der Code beschrieben wird, erfordert die Gewichtungshandhabung eine explizite Behandlung, da Prados Beispiele zwei Größen vermischen, die unterschiedlichen Zwecken dienen. Diese Unterscheidung wurde bereits in Teil 5 angedeutet, in dem ml_cross_val_scores_all mit separaten Parametern für sample_weight_train und sample_weight_score eingeführt wurde.
Durchschnittliche Einzigartigkeitsgewichte (events['tW']) beantworten die Frage: „Wie viele unabhängige Informationen trägt diese Beobachtung bei?“ Sie gehören in estimator.fit(), um zu verhindern, dass das Modell redundante, überlappende Beobachtungen implizit übergewichtet – das in Teil 4 identifizierte das Problem sich überlappender gleichzeitiger Labels.
Rendite-Attributionsgewichte (events['w']) beantworten die Frage: „Wie stark wirkt sich eine korrekte oder inkorrekte Vorhersage für diese Beobachtung auf die GuV aus?“ Sie gehören in den Validierungs-Scorer. Ohne sie behandelt die CV-Metrik eine Vorhersage an einem flachen Tag als ebenso wichtig wie an einem Tag mit einer großen Kursbewegung, was Vergleiche über Versuche hinweg stillschweigend verzerrt.
# Fitting: _WeightedEstimator applies tW internally via scheme='uniqueness' fit = clone(model).fit(X_train, y_train) # Scoring: return-attribution weights from events['w'] w_val = events['w'].iloc[val_idx].to_numpy() score = -log_loss(y_val, y_prob, sample_weight=w_val)
Prado berechnet ein einzelnes kombiniertes Gewicht (Einzigartigkeit × |Rendite|) und verwendet es sowohl für das Fitting als auch für das Scoring. Das ist eine vernünftige Annäherung. Die Verwendung separater Gewichte wie oben ist eindeutig korrekter, da jedes Gewicht die Aufgabe erfüllt, für die es geeignet ist. Darüber hinaus ist das Gewichtungsschema selbst – ungewichtet, Einzigartigkeit oder Rendite – ein Hyperparameter, den FinancialModelSuggester gemeinsam mit den Modellparametern optimiert.
Ein wichtiger Hinweis zum Strategietyp: Trendfolgemodelle profitieren möglicherweise eher von Rendite-Attributionsgewichten für das Fitting als von Einzigartigkeitsgewichten. Die durchschnittliche Einzigartigkeit bestraft Label-Überlappungen. In Trendmärkten sind Labels konstruktionsbedingt lang und stark überlappend, sodass ein Trendmodell, das mit Einzigartigkeitsgewichten trainiert wurde, lernt, genau den Bedingungen zu misstrauen, die es eigentlich ausnutzen soll. Rendite-Attributionsgewichte haben keine Meinung zu Überschneidungen – sie gewichten große Bewegungen einfach stärker. Der Hyperparameter weight_scheme macht dies zu einer optimierbaren Suchdimension.
3.2 _WeightedEstimator
_WeightedEstimator ist ein sklearn-kompatibler Wrapper (aus afml.production.model_development), der die Gewichtungsberechnung vollständig kapselt. Er akzeptiert den Basis-Schätzer, das DataFrame events, die vollständige Bar data_index und die von Optuna abgetasteten Gewichtungsschema-Parameter. Wenn fit(X_train, y_train) ohne das explizite Argument sample_weight aufgerufen wird, berechnet _WeightedEstimator intern die geeigneten Gewichte unter Verwendung von data_index und events und übergibt sie dann an den Basis-Schätzer. Dieses Design hält die Gewichtungsberechnung aus der Schleife der Zielfunktion heraus und macht das Gewichtungsschema zu einem Hyperparameter erster Klasse.
4. FinancialModelSuggester
FinancialModelSuggester übersetzt Parameterverteilungen im Scikit-Learn-Stil in trial.suggest_*()-Aufrufe und konfiguriert einen _WeightedEstimator mit den gesampelten Parametern. Es stellt zwei Methoden bereit, die ein duales Paar bilden: (1) suggest_and_apply für die stochastische Verwendung innerhalb der Zielfunktion und (2) apply_from_params für die deterministische Rekonstruktion aus study.best_params. Das zentrale Registry WEIGHT_KEYS trennt in beiden Methoden Gewichtungs-Hyperparameter von Modell-Hyperparametern und stellt sicher, dass der Validierungsschritt in apply_from_params Gewichtungsschlüssel nur dann gegen die akzeptierten Parameter des Basismodells prüft, wenn dies angemessen ist.
class FinancialModelSuggester: """ Translates Scikit-Learn style distribution dictionaries into Optuna trial suggestions for rigorous statistical HPT. Two core methods form a dual pair: suggest_and_apply — Trial → params → model (stochastic, used in objective) apply_from_params — params → model (deterministic, used for refit) """ # Central registry: separates weight keys from model keys in both methods WEIGHT_KEYS = frozenset({"weight_scheme", "weight_decay", "weight_linear"}) @classmethod def suggest_and_apply( cls, trial: optuna.Trial, base_model, param_distributions: dict, events: pd.DataFrame, data_index: pd.DatetimeIndex, ): # 1. Suggest weight hyperparameters — optimised jointly with the model scheme = trial.suggest_categorical("weight_scheme", ["unweighted", "uniqueness", "return"]) decay = trial.suggest_float("weight_decay", 0.1, 1.0) linear = trial.suggest_categorical("weight_linear", [True, False]) # 2. Suggest base model hyperparameters from scikit-learn-style distributions sampled_params = {} for name, dist in param_distributions.items(): if isinstance(dist, list): sampled_params[name] = trial.suggest_categorical(name, dist) elif hasattr(dist, 'ppf'): # scipy.stats distribution low, high = dist.support() if dist.dist.name == "randint": sampled_params[name] = trial.suggest_int(name, int(low), int(high)) else: is_log = dist.dist.name in ['reciprocal', 'loguniform'] sampled_params[name] = trial.suggest_float(name, low, high, log=is_log) elif isinstance(dist, range): try: sampled_params[name] = trial.suggest_int(name, dist.start, dist.stop - 1) except AttributeError: low, high = dist.support() sampled_params[name] = trial.suggest_int(name, int(low), int(high)) else: sampled_params[name] = dist # 3. Clone and configure — _WeightedEstimator handles weight computation internally new_base = clone(base_model) new_base.set_params(**sampled_params) return _WeightedEstimator( base_estimator=new_base, events=events, data_index=data_index, scheme=scheme, decay=decay, linear=linear, ) @classmethod def apply_from_params( cls, params: dict, base_model, events: pd.DataFrame, data_index: pd.DatetimeIndex, ) -> "_WeightedEstimator": """ Reconstruct a WeightedEstimator from a flat params dict (e.g. study.best_params). Validates model params against the base model's accepted parameter set. """ weight_params = {k: params[k] for k in cls.WEIGHT_KEYS if k in params} model_params = {k: v for k, v in params.items() if k not in cls.WEIGHT_KEYS} # Defensive validation: catch invalid params before refit valid_keys = set(base_model.get_params().keys()) invalid = set(model_params) - valid_keys if invalid: raise ValueError( f"Parameters {invalid} are not valid for " f"{type(base_model).__name__}. Valid: {sorted(valid_keys)}" ) new_base = clone(base_model) new_base.set_params(**model_params) return _WeightedEstimator( base_estimator=new_base, events=events, data_index=data_index, scheme=weight_params.get("weight_scheme", "unweighted"), decay=weight_params.get("weight_decay", 1.0), linear=weight_params.get("weight_linear", False), ) @classmethod def get_search_space(cls, model_name: str): """ Returns curated parameter distributions for financial models. Note min_weight_fraction_leaf for random forests: requires each leaf to account for at least a fraction of total sample weight. This interacts directly with return-attribution weights and is a stronger regulariser than min_samples_leaf when weights vary widely across market regimes. """ spaces = { "random_forest": { "n_estimators": range(100, 1000), "max_depth": range(3, 7), "min_weight_fraction_leaf": stats.uniform(0.025, 0.1), "max_features": ["sqrt", "log2", 0.5, 1.0], "ccp_alpha": stats.loguniform(1e-5, 1e-2), }, "xgboost": { "n_estimators": range(100, 1000), "learning_rate": stats.loguniform(1e-3, 0.1), "max_depth": range(2, 8), "subsample": stats.uniform(0.6, 0.4), "colsample_bytree": stats.uniform(0.6, 0.4), "gamma": stats.uniform(0, 5), }, } return spaces.get(model_name.lower(), {})
5. Zielfunktion – Purged K-Fold mit Pruning
Die Zielfunktion ist der Integrationspunkt. Sie führt eine Kreuzvalidierung PurgedKFold durch (eingeführt in Teil 4), wendet Rendite-Attributionsgewichte auf die Validierungsmetrik an und meldet die Fold-Scores nach jedem Fold über trial.report() an HyperbandPruner. Diese direkte Meldung auf Fold-Ebene ist die Schlüsselfunktion, die GridSearchCV von scikit-learn ohne erhebliche Workarounds nicht bieten kann.
Zwei Details sind erwähnenswert. Erstens ist clone(model).fit(X_train, y_train) ohne das explizite Argument sample_weight korrekt – _WeightedEstimator berechnet und wendet die Trainingsgewichte intern basierend auf dem für diesen Versuch ausgewählten scheme an. Zweitens ist w_val aus events['w'] immer das Rendite-Attributionsgewicht für die Bewertung, unabhängig davon, welches Trainingsschema ausgewählt wurde, da wir Vorhersagen immer nach ökonomischer Signifikanz bewerten. Diese Trennung spiegelt die Konvention sample_weight_train / sample_weight_score aus ml_cross_val_scores_all in Teil 5 wider.
def optimize_trading_model_with_pruning( trial: optuna.Trial, X, y, events, data_index, classifier, param_distributions: dict, n_splits: int = 5, metric: str = "neg_log_loss", ): """ Objective function for tuning models using Purged K-Fold cross-validation. Uses separate weights for fitting (_WeightedEstimator) and scoring (events['w']), mirroring the sample_weight_train / sample_weight_score convention from Part 5. """ suggester = FinancialModelSuggester() model = suggester.suggest_and_apply( trial, classifier, param_distributions, events, data_index ) t1 = events['t1'] cv = PurgedKFold(n_splits=n_splits, t1=t1, pct_embargo=0.01) # Convert once before the loop — avoids repeated pandas overhead per fold # and ensures val_idx (numpy integer array from PurgedKFold) indexes correctly w_score = events['w'].to_numpy() fold_scores = [] for fold_idx, (train_idx, val_idx) in enumerate(cv.split(X, y)): X_train, X_val = X.iloc[train_idx], X.iloc[val_idx] y_train, y_val = y.iloc[train_idx], y.iloc[val_idx] # _WeightedEstimator applies training weights (uniqueness or return) internally fit = clone(model).fit(X_train, y_train) # Slice pre-converted numpy array — no pandas overhead inside the hot loop w_val = w_score[val_idx] if metric == "neg_log_loss": y_prob = fit.predict_proba(X_val) score = -log_loss(y_val, y_prob, sample_weight=w_val) else: y_pred = fit.predict(X_val) score = f1_score(y_val, y_pred, sample_weight=w_val) fold_scores.append(score) # Financial baseline check fires before Hyperband's bracket logic # (only active when TradingModelPruner is not the pruner) trial.report(score, step=fold_idx) if trial.should_prune(): avg_score_so_far = np.mean(fold_scores) trial.set_user_attr("pruned_at_fold", fold_idx) trial.set_user_attr("score_when_pruned", avg_score_so_far) trial.set_user_attr("total_folds_attempted", len(fold_scores)) raise TrialPruned(f"Pruned at fold {fold_idx}. Avg: {avg_score_so_far:.4f}") final_score = np.mean(fold_scores) trial.set_user_attr("fold_scores", fold_scores) trial.set_user_attr("score_std", np.std(fold_scores)) return final_score
Die in trial.user_attrs["fold_scores"] gespeicherten Fold-Scores sind nicht nur diagnostische Metadaten. Sie steuern den check_for_overfitting-Callback und die Spalten auf Fold-Ebene im cv_results_-DataFrame. Ein hoher score_std über einen 5-Fold-Purged-Split deutet oft darauf hin, dass ein Modell eher regimespezifische Muster erfasst als ein wirklich verallgemeinerbares Signal – genau die Art von Regimeempfindlichkeit, die das in Teil 7 beschriebene Bar-Sampling über mehrere Marktregime hinweg.
6. Finanzorientiertes Pruning
TradingModelPruner erweitert MedianPruner um drei finanzwirtschaftlich fundierte Regeln. Er ist kein universeller Ersatz für HyperbandPruner – er hat einen spezifischen sinnvollen Einsatzbereich. Da er MedianPruner umschließt, ist er für die ersten n_startup_trials=10 abgeschlossenen Versuche komplett stumm und liefert erst nach 20–30 weiteren zuverlässige Vergleiche. Bei einer kurzen Studie mit weniger als 30 Versuchen bietet er weniger Einsparungen als HyperbandPruner. Die vier Bedingungen, unter denen er die bessere Wahl ist, sind wie folgt:
Großes Versuchsbudget für ein gut verstandenes Instrument. Die Durchführung von 100 oder mehr Versuchen für ein Instrument mit bekannten Eigenschaften gibt Ihnen eine kalibrierte Erwartung darüber, wie eine plausible Untergrenze für den Log-Loss aussieht. Ein Versuch, das nach Fold 1 weit unter diesem Mindestwert liegt, ist kein regimeempfindlicher Underperformer – es ist eine fehlerhafte Konfiguration. Die Entropie-Baseline beendet es sofort und bedingungslos. HyperbandPruner kann diese Entscheidung nicht treffen, da er den relativen Rang über Versuche hinweg vergleicht und nicht mit einem absoluten wirtschaftlichen Mindestwert. Beispiel: Bei EURUSD-Triple-Barrier-Labels, die auf ein 3:1-Barrier-Verhältnis kalibriert sind, liegt der Log-Loss konsistent bei etwa −0.65. Ein Versuch, das nach Fold 1 −0.92 liefert, rechtfertigt eine sofortige Beendigung, unabhängig davon, was andere Versuche tun.
Trendstarkes Instrument mit hoher Varianz der Renditegewichtung. Bei einem stark trendenden Instrument weist events['w'] einen hohen Variationskoeffizienten auf – einige wenige Beobachtungen großer Bewegungen haben ein Gewicht, das um eine Größenordnung über dem Rest liegt. TradingModelPruner skaliert volatility_tolerance proportional zu diesem VK, was bedeutet, dass er große Schwankungen der Fold-Scores akzeptiert, ohne ein Trendmodell zu beschneiden, das zufällig auf einem Mean-Reverting-Fold schlecht aussieht. HyperbandPruner beschneidet nach relativem Rang, unabhängig davon, ob die Schwankung die Modellqualität oder die Marktstruktur widerspiegelt. Beispiel: Ein GBPJPY-Modell, das mit wöchentlichen Triple-Barrier-Labels während einer Trendphase trainiert wurde, weist eine hohe Varianz der Fold-Scores auf, einfach weil jeder Fold eine unterschiedliche Momentum-Phase abdeckt. TradingModelPruner berücksichtigt dies; HyperbandPruner verwirft es möglicherweise.
CV-Fenster, das einen bekannten Regimewechsel umfasst. Wenn das Trainingsfenster einen strukturellen Bruch abdeckt – eine Änderung der Zentralbankpolitik, einen Volatilitätsregimewechsel, eine Änderung der Mikrostruktur –, sieht ein Fold kategorisch anders aus als die anderen. Das Modell, das in beiden Regimen am besten abschneidet, sieht nach Fold 1 nicht unbedingt am besten aus. TradingModelPruners n_warmup_steps=2 gewährt eine zweifache Schonfrist, bevor das varianzbasierte Pruning ausgelöst wird. Beispiel: Eine Studie, deren CV-Fenster den Zeitraum 2019–2022 umfasst, hat einen Fold, der die Volatilität der COVID-Ära abdeckt. Das Zulassen von zwei Folds vor dem Auslösen des varianzbasierten Prunings verhindert, dass dieser Fold Konfigurationen eliminiert, die über Regime hinweg gut generalisieren.
Folgestudie nach einem ersten HyperbandPruner-Durchlauf. Ein praktischer Arbeitsablauf besteht darin, eine erste Studie mit 50 Versuchen mit HyperbandPruner durchzuführen, um die Zielfunktionslandschaft zu ermitteln, und dann eine fokussierte Folgestudie mit 150 Versuchen mit TradingModelPruner auszuführen. Die erste Studie liefert Ihnen einen kalibrierten Score-Bereich, um multiplier präzise einzustellen. Die zweite Studie verhindert, dass der TPE-Sampler – der nun über einen Prior aus dem ersten Durchlauf verfügt – Konfigurationen erneut prüft, die bereits als nahe am Rauschpegel liegend identifiziert wurden.
Der Basis-Entropieschwellenwert verwendet die nach Renditebeitrag gewichtete Label-Verteilung – nicht die Rohverteilung. Dies ist die korrekte Basis, da ein naiver Klassifikator, der immer die dominante Klasse vorhersagt, diese Entropie erreicht, wenn die Klassen nach ihrer wirtschaftlichen Bedeutung gewichtet werden. Die Volatilitätstoleranz ist proportional zum Variationskoeffizienten der Renditebeitragsgewichte: Trendmärkte (hoher VK) erhalten eine breitere Toleranz; Märkte mit Tendenz zur Rückkehr zum Mittelwert (niedriger VK) erhalten eine engere.
class TradingModelPruner(MedianPruner): """ Financial-aware pruner that adjusts thresholds based on label entropy and return-attribution weighted volatility. """ def __init__( self, y, sample_weight, # Return-attribution weights: events['w'] n_startup_trials: int = 10, n_warmup_steps: int = 2, multiplier: float = 1.15, ): super().__init__(n_startup_trials=n_startup_trials, n_warmup_steps=n_warmup_steps) # Baseline entropy from return-attribution weighted label distribution weighted_counts = pd.Series(sample_weight).groupby(y.values).sum() probs = weighted_counts / weighted_counts.sum() if set(y.unique()) != {0, 1}: self.baseline_entropy = -np.sum(probs * np.log(probs)) self.min_score_threshold = -self.baseline_entropy * multiplier else: majority_ratio = probs.max() self.min_score_threshold = majority_ratio / multiplier # Volatility tolerance scales with CV of weights: # trending regimes (high CV) → higher fold-score variance is acceptable weight_cv = np.std(sample_weight) / np.mean(sample_weight) self.volatility_tolerance = 0.1 * (1 + weight_cv) def prune(self, study, trial) -> bool: step = trial.last_step if step is None: return False if trial.number >= 5 and len(trial.intermediate_values) >= 3: # Rule 1: Worse than economically-weighted naive baseline? current_score = trial.intermediate_values.get(step) if trial.number > 1 and current_score < self.min_score_threshold: return True # Rule 2: Unstable across recent folds? recent_scores = list(trial.intermediate_values.values())[-3:] if np.std(recent_scores) > self.volatility_tolerance: return True # Rule 3: Standard median pruning return super().prune(study, trial)
Zusammenfassende Entscheidungsregel: n_trials < 30 → HyperbandPruner verwenden; n_trials ≥ 100 und mindestens eine der oben genannten Bedingungen trifft zu → TradingModelPruner verwenden; erste Studie zu einem unbekannten Instrument → HyperbandPruner verwenden.
7. Orchestrierung und Speicherung
optimize_trading_model erstellt die Studie, bindet den Pruner und Sampler ein, stellt die Verbindung zum SQLite-Speicher her und übernimmt das Refit. HyperbandPruner ist der standardmäßige pruner_type. Das Einstellen von n_jobs=1 für den Klassifikator vor der Studie und das Wiederherstellen von -1 nach dem Refit verhindert eine Überzeichnung von Ressourcen: Da study.optimize parallele Worker ausführt, belegt jeder Worker bereits einen Kern – der im Worker laufende Klassifikator, der ebenfalls mehrere Kerne anfordert, würde eine verschachtelte Parallelität verursachen, die den Durchsatz eher verschlechtert als verbessert. Dasselbe Prinzip gilt für den Wrapper clf_hyper_fit aus Teil 7.
load_if_exists=True bedeutet, dass ein abgebrochener Lauf bei Aufruf mit demselben study_name und db_path ab dem letzten abgeschlossenen Versuch fortgesetzt wird. Kombiniert mit der Caching-Architektur aus Teil 6 bedeutet dies, dass ein vollständiger Pipeline-Neustart nach einer Unterbrechung alle zwischengespeicherten Vorverarbeitungsergebnisse neu lädt und genau dort fortfährt, wo die Optuna-Studie aufgehört hat – ohne verschwendete Rechenleistung.
def optimize_trading_model( classifier, X: pd.DataFrame, y: pd.Series, events: pd.DataFrame, data_index: pd.DatetimeIndex, param_distributions: dict, n_trials: int = 100, timeout: int = 3600, n_splits: int = 5, pruner_type: str = "hyperband", metric: str = "neg_log_loss", study_name: str = None, db_path: str = None, random_state: int = 42, refit: bool = True, ): if pruner_type == "median": pruner = TradingModelPruner(y, sample_weight=events.loc[X.index, 'w']) elif pruner_type == "hyperband": pruner = HyperbandPruner(min_resource=1, max_resource=n_splits, reduction_factor=3) else: pruner = SuccessiveHalvingPruner() sampler = TPESampler(seed=random_state) storage_url = f"sqlite:///{db_path}.db?timeout=30" # 30s timeout for parallel workers try: study = optuna.create_study( direction="maximize", sampler=sampler, pruner=pruner, study_name=study_name, storage=storage_url, load_if_exists=True, # resume from last completed trial ) if hasattr(classifier, 'n_jobs'): classifier.set_params(n_jobs=1) # prevent oversubscription if hasattr(classifier, 'random_state'): classifier.set_params(random_state=random_state) def objective(trial): return optimize_trading_model_with_pruning( trial, X, y, events, data_index, classifier, param_distributions, n_splits, metric ) study.optimize( objective, n_trials=n_trials, timeout=timeout, callbacks=[print_best_trial, save_intermediate_results, check_for_overfitting], ) if refit: best_model = FinancialModelSuggester.apply_from_params( study.best_trial.params, classifier, events, data_index ) if hasattr(best_model, 'n_jobs'): best_model.set_params(n_jobs=-1) best_model.fit(X, y) study.best_estimator_ = best_model cv_results = optuna_to_cv_results(study) return study, cv_results except StorageInternalError as e: logger.error(f"Storage Error: {db_path} is locked or unreachable. {e}") except Exception as e: raise e
Die drei Callbacks bieten Einblick in den Optimierungslauf, ohne das Ziel zu verändern. print_best_trial protokolliert Ergebnisverbesserungen in der Konsole. save_intermediate_results schreibt jeden abgeschlossenen Versuch als JSON auf die Festplatte in das Verzeichnis optuna_results/ – ein leichtgewichtiger Audit-Trail, analog zur Protokollierungsinfrastruktur aus Teil 7. check_for_overfitting gibt eine Warnung aus, wenn score_std 0,3 überschreitet, und markiert Modelle, die empfindlich darauf reagieren, welches Marktregime ein Fold abdeckt.
8. Von der Optuna-Studie zu Scikit-Learn cv_results_
Vorhandener Analysecode in der Pipeline – einschließlich der Hyperparameter-Analysefunktionen in afml.cross_validation.hyper_fit_analysis – erwartet den scikit-learn DataFrame cv_results_. Bedingte Suchräume erzeugen Zeilen mit vielen fehlenden Werten: Der Suchraum xgboost enthält learning_rate und gamma, die in Random-Forest-Versuchen nicht existieren. optuna_to_cv_results handhabt dies, indem ein DataFrame mit einer Zeile pro abgeschlossenem Versuch erstellt wird, wobei fehlende Parameter mit NaN gefüllt werden. Die pro Fold gespeicherten Ergebnisse in trial.user_attrs["fold_scores"] werden in split0_test_score, split1_test_score usw. aufgefächert, was die gleiche Analyse der Fold-Ebene ermöglicht, die in den nativen CV-Ergebnissen von scikit-learn verfügbar ist.
def optuna_to_cv_results(study): """Converts an Optuna study into a Scikit-Learn style cv_results_ DataFrame.""" rows = [] for trial in study.trials: if trial.state != optuna.trial.TrialState.COMPLETE: continue # exclude pruned trials from ranking res = { "mean_test_score": trial.value, "std_test_score": trial.user_attrs.get("score_std", 0), "mean_fit_time": (trial.datetime_complete - trial.datetime_start).total_seconds(), "params": trial.params, } for k, v in trial.params.items(): res[f"param_{k}"] = v fold_scores = trial.user_attrs.get("fold_scores", []) for i, score in enumerate(fold_scores): res[f"split{i}_test_score"] = score rows.append(res) return pd.DataFrame(rows)
9. Visualisierung
Nach Abschluss einer Studie bietet die Visualisierungssuite von Optuna diagnostische Einblicke in den Suchprozess. Alle Funktionen geben Plotly-Abbildungen zurück, die über optuna.visualization verfügbar sind. Die folgenden Diagramme sind repräsentative Beispiele, die aus einer synthetischen Finanz-ML-Studie generiert wurden. Diese Diagramme sind besonders wertvoll, da sie Informationen über die Hyperparameter-Landschaft aufzeigen, die der DataFrame cv_results_ allein nicht vermitteln kann.
plot_optimization_history(study)
Zeigt den Zielfunktionswert pro Versuch (Punkte) und den laufenden Bestwert (Linie). Die Lücke zwischen den einzelnen Versuchsergebnissen und dem laufenden Bestwert verringert sich, während TPE sein Modell der Zielfunktionslandschaft aufbaut. Ein flach laufender Bestwert deutet auf Konvergenz hin; ein sich weiter verbessernder Bestwert legt nahe, dass weitere Versuche gerechtfertigt sind. In der Praxis konvergiert TPE typischerweise innerhalb von 30–60 Versuchen für einen Random-Forest-Raum mit 4–5 Parametern.

Abb. 2 – Optimierungshistorie
plot_intermediate_values(study)
Zeigt den laufenden Mittelwert pro Fold für jeden Versuch an, wobei abgebrochene Versuche als kurze Linien dargestellt werden, die vorzeitig enden. Dies ist die primäre Diagnose für HyperbandPruner. Grün gefärbte Linien schließen alle Folds ab und neigen dazu, sich am oberen Ende des Wertebereichs zu gruppieren; rot gefärbte Linien werden früh beschnitten und enden bei Fold 1. Wenn keine Linien beschnitten werden, muss reduction_factor möglicherweise angepasst werden, oder die Zielfunktionslandschaft ist zu flach, als dass die Werte von Fold 1 informativ wären.

Abb. 3 – Zwischenwerte (Pruning-Diagnose)
plot_param_importances(study)
Verwendet fANOVA, um den Beitrag jedes Hyperparameters zur Zielvarianz zu schätzen. Im Bereich Financial ML dominieren Regularisierungsparameter (min_weight_fraction_leaf, ccp_alpha, max_depth) typischerweise die Kapazitätsparameter (n_estimators), was widerspiegelt, dass Overfitting auf das Trainingsregime die primäre Fehlerquelle darstellt. Die Wichtigkeit von weight_scheme ist besonders aufschlussreich: Eine hohe Wichtigkeit deutet darauf hin, dass die Wahl zwischen Uniqueness- und Return-Attribution-Training einen messbaren Unterschied für das spezifische Instrument und den Strategietyp macht.

Abb. 4 – Parameterwichtigkeit (fANOVA)
plot_parallel_coordinate(study)
Stellt jeden Versuch als Linie über parallelen Achsen dar, die nach dem Zielwert eingefärbt sind. Leistungsstarke Versuche (grün) gruppieren sich in sichtbaren Bändern und offenbaren gemeinsame Parameterinteraktionen – zum Beispiel, dass ein tiefer Baum die Leistung nur verbessert, wenn er mit starkem Cost-Complexity-Pruning über ccp_alpha kombiniert wird. Dies ist das beste Diagramm, um zu identifizieren, welche Parameterkombinationen zusammenarbeiten, anstatt dies einzeln zu betrachten.

Abb. 5 Parallele Koordinatendiagramme
plot_edf([study_optuna, study_random])
Stellt die empirische Verteilungsfunktion (CDF) der Zielwerte über alle Versuche für mehrere Studien dar. Eine Studie, deren EDF nach rechts verschoben ist, dominiert die andere stochastisch über die gesamte Versuchsverteilung hinweg. Dies ist der korrekte Weg, um zu demonstrieren, dass Optuna RandomizedSearchCV bei gleicher Versuchsanzahl übertrifft. Ein einzelner Vergleich von best_score ist anfällig für Zufallstreffer und sollte niemals als alleiniger Benchmark verwendet werden – die EDF zeigt das vollständige Bild.
vis.plot_edf erfordert eine Liste von optuna.Study-Objekten. Da RandomizedSearchCV den DataFrame cv_results_ anstelle einer Studie erzeugt, ist ein Konverter erforderlich. randomized_search_to_study verpackt jede Zeile von cv_results_ in einen abgeschlossenen optuna.Trial unter Verwendung von CategoricalDistribution-Stubs – plot_edf liest nur trial.value, daher spielt der Verteilungstyp keine Rolle. Die Komfortfunktion plot_edf_comparison übernimmt die Konvertierung und die Umbenennung der Traces in einem Aufruf.

Abb. 6 – EDF-Vergleich: Optuna vs. RandomizedSearchCV
import optuna.visualization as vis from optuna.distributions import CategoricalDistribution fig = vis.plot_optimization_history(study) fig.show() fig = vis.plot_intermediate_values(study) # primary HyperbandPruner diagnostic fig.show() fig = vis.plot_param_importances(study) # weight_scheme importance is informative fig.show() fig = vis.plot_parallel_coordinate(study) fig.show() def randomized_search_to_study( gs, study_name: str = "randomized_search_baseline", ) -> optuna.Study: """ Convert a fitted RandomizedSearchCV into an Optuna Study for use with vis.plot_edf. vis.plot_edf requires optuna.Study objects and reads only trial.value. CategoricalDistribution stubs satisfy the API without affecting the plot. NaN mean_test_score rows (failed fits) are skipped. """ study = optuna.create_study(direction="maximize", study_name=study_name) cv_df = pd.DataFrame(gs.cv_results_) param_cols = [c for c in cv_df.columns if c.startswith("param_")] for _, row in cv_df.iterrows(): score = row["mean_test_score"] if pd.isna(score): continue params = {} for col in param_cols: val = row[col] # Skip NaN params from conditional search spaces if not (pd.isna(val) if not isinstance(val, str) else False): params[col.replace("param_", "")] = val distributions = { k: CategoricalDistribution([v]) for k, v in params.items() } trial = optuna.trial.create_trial( params=params, distributions=distributions, value=float(score), ) study.add_trial(trial) return study def plot_edf_comparison(study_optuna, gs_random): """ Compare an Optuna study against a fitted RandomizedSearchCV via EDF. Handles the conversion and trace renaming in one call. """ study_random = randomized_search_to_study(gs_random) fig = vis.plot_edf([study_optuna, study_random]) for trace in fig.data: if "random" in trace.name.lower(): trace.name = "RandomizedSearchCV" else: trace.name = f"Optuna ({study_optuna.study_name})" fig.update_layout( title="EDF Comparison — Optuna (TPE + Hyperband) vs RandomizedSearchCV" ) return fig # Usage: # study, cv_results = optimize_trading_model(...) # gs = RandomizedSearchCV(...); gs.fit(X, y, sample_weight=events['w']) # fig = plot_edf_comparison(study, gs) # fig.show()
10. Praktische Überlegungen
Setzen Sie den Zufalls-Seed im Basisschätzer fest. Ohne diesen spiegeln Leistungsunterschiede zwischen den Versuchen Zufälligkeit wider und nicht die Qualität der Hyperparameter. optimize_trading_model setzt random_state im Klassifikator, bevor die Studie beginnt. Dies ist besonders wichtig für den Vergleich zwischen Versuchen, die sich nur im weight_scheme unterscheiden – der Unterschied muss auf das Gewichtungsschema zurückzuführen sein und nicht auf die Stochastik des Random Forest.
Verwenden Sie informierte Suchraumbegrenzungen. Bereiche wie n_estimators in [1, 10000] verschwenden frühe Versuche an Extremwerte. Die Factory get_search_space bietet kuratierte Verteilungen als Ausgangspunkt. Erweitern Sie den Bereich nur, wenn plot_contour zeigt, dass der beste Bereich an der Grenze des aktuellen Bereichs liegt.
Verwenden Sie eine logarithmische Skala für multiplikative Parameter. learning_rate und ccp_alpha erstrecken sich über Größenordnungen. FinancialModelSuggester handhabt dies automatisch, indem dist.dist.name auf die Verteilungen loguniform und reciprocal geprüft und log=True an trial.suggest_float übergeben wird.
Lassen Sie mindestens eine Schonfrist vor dem Pruning zu. Financial-CV-Folds decken verschiedene Marktregime ab. TradingModelPruner erzwingt n_warmup_steps=2, bevor varianzbasierte Regeln angewendet werden. Das konservative Bracket von HyperbandPruner bietet strukturell denselben Schutz – pro Bracket wird immer mindestens ein Versuch bis zum Abschluss ausgeführt.
Interpretieren Sie die Wichtigkeit von weight_scheme vorsichtig. Wenn plot_param_importances weight_scheme als wichtigsten Parameter anzeigt, bedeutet das nicht zwangsläufig, dass das Schema für die Modellqualität am wichtigsten ist – es kann bedeuten, dass die anderen Hyperparameter bereits durch Vorwissen gut eingeschränkt waren und der Suchraum verfeinert werden muss.
Schlussfolgerung
Das Ausführen von optimize_trading_model liefert eine reproduzierbare Optuna-Studie und drei unmittelbare, prüfbare Artefakte: die Studie selbst (persistiert in SQLite, fortsetzbar über load_if_exists=True und kompatibel mit parallelen Workern); einen refitteten best_estimator_ (_WeightedEstimator, der ein optimiertes Basismodell umschließt und bereit für Vorhersagen ist); sowie einen scikit-learn-kompatiblen cv_results_-DataFrame mit Scores pro Fold und Fold-Stabilitätsmetadaten, der für dieselben nachgelagerten Diagnosen geeignet ist, die in der gesamten Pipeline verwendet werden. Die praktischen Vorteile schließen die in der Einleitung identifizierten finanziellen Engpässe: Frühes Pruning (HyperbandPruner oder, für große Studien mit kalibrierten Baselines, TradingModelPruner) reduziert den gesamten Rechenaufwand erheblich, indem aussichtslose Versuche nach einem oder wenigen Folds beendet werden; persistente SQLite-Speicherung und die Caching-Architektur aus Teil 6 ermöglichen es, lange Experimente fortzusetzen, ohne teure Vorverarbeitung erneut durchführen zu müssen; und die getrennte Handhabung von Trainings- gegenüber Scoring-Gewichtungen stellt sicher, dass die ökonomische Signifikanz sowohl bei der Modellanpassung als auch bei der Evaluierung respektiert wird.
Wichtige Design-Erkenntnisse, nach denen Sie sofort handeln können:
- Verwenden Sie PurgedKFold als einzigen inneren CV für Triple-Barrier-Labels und berichten Sie immer die Fold-Level-Scores für das Pruning – CPCV ist ein Backtesting-Tool und erzeugt keinen skalaren Output, der für das Ranking von Hyperparameter-Kombinationen geeignet ist.
- Bevorzugen Sie HyperbandPruner als Standard für kleine bis mittlere Studien (weniger als ~30 Versuche) oder erste Studien zu unbekannten Instrumenten; verwenden Sie TradingModelPruner, wenn Sie ein großes Versuchsbudget (100+) und eine kalibrierte wirtschaftliche Baseline haben.
- Behandeln Sie das Trainingsgewichtungsschema (unweighted, uniqueness, return) als Hyperparameter und verwenden Sie für das Scoring immer Return-Attributionsgewichtungen (events['w']), gemäß der Dual-Weight-Konvention aus Teil 5.
- Fixieren Sie Zufalls-Seeds, verwenden Sie informierte Suchgrenzen (logarithmische Skala für multiplikative Parameter wie ccp_alpha und learning_rate) und lassen Sie ein Schonfenster vor dem Pruning zu, um die Regime-Heterogenität über die Folds hinweg zu berücksichtigen.
Respektieren Sie schließlich die Grenze: Optunas gezielte Suche ist das richtige Werkzeug für statistisches HPO auf gelabelten Daten – sie findet das Optimum zuverlässig, da die Zielfunktion (kreuzvalidierter Log-Loss auf Triple-Barrier-Labels) ein valides Maß für die Generalisierung ist. Es ist kein sicherer Ersatz für die Strategieoptimierung auf Basis historischer GuV, bei der dieselbe Zuverlässigkeit sie gefährlicher macht als eine Zufallssuche, nicht weniger. In Teil 8.2 wird die Integration dieser HPO-Kontur in die Produktionspipeline über den Wrapper clf_hyper_fit demonstriert, sie wird mit der bestehenden Caching-Infrastruktur verbunden und zeigt den ONNX-Exportpfad für den Einsatz in MetaTrader 5 auf.
Beigefügte Dateien
Die folgende Tabelle beschreibt jede an diesen Artikel angehängte Datei und ihre Rolle im HPO-System. Dateien aus dem Produktionsmodul und dem Kreuzvalidierungsmodul werden mit Teil 7 geteilt; Dateien mit dem Präfix optuna_hyper_fit werden hier eingeführt.
| Datei | Modul | Rolle in diesem Artikel | Wichtige Abhängigkeiten | |
|---|---|---|---|---|
| 1. | optuna_hyper_fit.py | afml.cross_validation | Haupt-HPO-Modul. Enthält FinancialModelSuggester, optimize_trading_model_with_pruning, TradingModelPruner, optimize_trading_model, optuna_to_cv_results sowie die in diesem Artikel vorgestellten Hilfsfunktionen für den EDF-Vergleich | cross_validation.py (PurgedKFold), model_development.py (_WeightedEstimator), optuna ≥ 3.0, scikit-learn ≥ 1.3, loguru |
| 2. | model_development.py | afml.production | Stellt _WeightedEstimator bereit – den sklearn-kompatiblen Wrapper, der Trainingsstichprobengewichtungen (Eindeutigkeit oder Renditezuordnung) intern berechnet und anwendet, sodass die Zielfunktionsschleife diese nicht explizit verarbeiten muss. Stellt außerdem TickDataLoader und die umfassendere Pipeline-Infrastruktur aus Teil 7 bereit. | utils.py (Datums-Konvertierung), MetaTrader5 Python-Paket, scikit-learn, pandas, numpy |
| 3. | utils.py | afml.production | Hilfsfunktionen, die im gesamten Produktionsmodul verwendet werden, einschließlich Datums-Konvertierungs-Helfern, die von TickDataLoader genutzt werden, sowie Datenvalidierungsroutinen, die an Pipeline-Einstiegspunkten aufgerufen werden. | pandas, numpy, MetaTrader5 Python-Paket |
| 4. | cross_validation.py | afml.cross_validation | Stellt PurgedKFold bereit – den einzigen gültigen inneren CV-Splitter für finanzielles HPO. Wird direkt innerhalb von optimize_trading_model_with_pruning mit t1 aus dem Ereignis-DataFrame und einem 1% Embargo aufgerufen. Stellt außerdem CombinatorialPurgedCV für die äußere Backtesting-Schleife bereit, die sich von HPO unterscheidet und nicht innerhalb der Funktionen in diesem Artikel verwendet wird. | scikit-learn (BaseCrossValidator), pandas, numpy |
| 5. | hyper_fit_analysis.py | afml.cross_validation | Post-Study-Analysefunktionen, die den von optuna_to_cv_results erzeugten DataFrame cv_results_ verwenden. Beinhaltet Konsistenzdiagramme auf Fold-Ebene, Zusammenfassungen von Parameterinteraktionen sowie die Funktion ml_cross_val_scores_all aus Teil 5, welche die Konvention für die duale Gewichtungsbewertung etablierte, auf der dieser Artikel aufbaut. | cross_validation.py (PurgedKFold), pandas, numpy, scikit-learn, matplotlib |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20117
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
Graphentheorie: Tiefensuche (DFS) zur Traversierung von Marktstrukturen im Handel
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Paketbasierter Ansatz mit KnitPkg für die MQL5-Entwicklung
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.