MetaTrader 5 – Blaupause für maschinelles Lernen (Teil 7): Von verstreuten Experimenten zu reproduzierbaren Ergebnissen
Einführung
In dieser Reihe haben wir wichtige Komponenten des maschinellen Lernens für den Handel behandelt: Datenstrukturen, Labeling und Meta-Labeling, Stichprobengewichtung und zeitlich bereinigte Kreuzvalidierung (Purged Cross-Validation). Doch diese Methoden, so leistungsfähig sie auch einzeln sein mögen, entfalten ihr volles Potenzial erst, wenn sie in ein einheitliches Forschungssystem eingebunden sind. In diesem Artikel werde ich zeigen, wie man diese Bausteine zu einer produktionsreifen Pipeline zusammenfügt, die Ad-hoc-Experimente in nachvollziehbare und auditierbare Forschungsergebnisse umwandelt, und dabei auf der in meinem vorherigen Artikel entwickelten Caching-Architektur aufbaut.
Der Code, den wir uns ansehen werden, ist nicht nur ein weiteres Beispiel, sondern ein wesentlicher Bestandteil des Systems, das ich zur Entwicklung von Handelsmodellen nutze. Es verarbeitet alles – von Roh-Tickdaten bis zu ONNX-Modellen für den Einsatz in MetaTrader 5, wobei dabei automatisch umfassende Protokollierungs-, Caching- und Analyseberichte erstellt werden. Der Übersichtlichkeit halber werde ich einige wichtige Themen nur am Rande behandeln, wie beispielsweise die Analyse der Merkmalsbedeutung und die Auswahl optimaler Schwellenwerte für die Triple-Barrier-Methode, um nur einige zu nennen. Dieser Artikel geht davon aus, dass die vorangegangenen Forschungsschritte bereits durchgeführt wurden, und konzentriert sich daher auf die Erstellung reproduzierbarer Pipelines. Was macht ein Forschungssystem „produktionsreif“?
- Reproduzierbarkeit: Führen Sie denselben Code zweimal aus, um identische Ergebnisse zu erhalten.
- Rückverfolgbarkeit: Für jedes Modell exakt nachvollziehen können, mit welchen Daten jedes Modell trainiert wurde.
- Effizienz: Rechenintensive Berechnungen werden zwischengespeichert, sodass keine Arbeit doppelt ausgeführt werden muss
- Validierung: Fehler erkennen, bevor sie im Live-Handel wirksam werden
- Dokumentation: Automatische Berichte, die jede Entscheidung erläutern

Das Problem: Forschungschaos
Bevor wir uns mit der Lösung befassen, sollten wir uns zunächst das Problem vor Augen führen. Die meisten Forschungsarbeiten zum Thema maschinelles Lernen im Handel sehen folgendermaßen aus:
# Somewhere in notebook cell 47... df = load_data("EURUSD", "2020-01-01", "2023-12-31") features = calculate_indicators(df) X_train, y_train = features[:-1000], labels[:-1000] model = RandomForestClassifier(n_estimators=100) model.fit(X_train, y_train) # Score: 0.67 - is that good? What parameters did I use? # When did I run this? Which data version?
Drei Monate später lassen sich diese Ergebnisse nicht mehr reproduzieren. Die Daten haben sich geändert. Sie wissen nicht mehr, welche Merkmale Sie genutzt haben. Die Modelldatei enthält keine Metadaten. Sie stehen wieder ganz am Anfang.
Das ist nicht nur lästig, sondern auch gefährlich. Im Live-Handel bedeutet eine nicht reproduzierbare Analyse, dass Sie Ihrem Backtest nicht vertrauen können. Fehler lassen sich nicht debuggen. Man kann nicht sicher iterieren.
Die Lösung: Eine methodisch fundierte Pipeline
Unser Forschungssystem geht diese Probleme anhand von vier Leitprinzipien an:
- Zeitabhängiges Caching: Lassen Sie niemals zukünftige Daten in das Training einfließen, sondern speichern Sie alles sicher im Cache.
- Automatische Dokumentation: Erstellen Sie in jeder Phase umfassende Berichte
- Strukturierte Speicherung: Navigieren Sie durch Ihre Experimente nach Strategie, Symbol und Parametern
- Validierung zuerst: Fehler in der Forschung aufspüren, nicht in der Produktion
Hier ist die allgemeine Architektur:
Jede Phase wird zwischengespeichert, jede Entscheidung protokolliert, jede Ausgabe überprüft.
Ich möchte noch einmal betonen, dass es sich hierbei um eine übergeordnete Architektur handelt, die keineswegs darauf ausgelegt ist, alle Schritte des Entwicklungsprozesses abzudecken.
Intelligente Datenarchitektur: Warum ein Tick-RAM-Cache plus Parquet-Bars alles verändert.
Bevor wir uns mit den einzelnen Phasen der Pipeline befassen, wollen wir uns zunächst mit einer entscheidenden architektonischen Entscheidung beschäftigen, die das gesamte System erst praktikabel macht: der Strategie zur Datenverarbeitung.
Das Dilemma mit den Tick-Daten
Bei der Entwicklung von Handelsstrategien stehen Sie vor einem klassischen Optimierungsproblem:
| Option 1: Bars direkt vom Broker laden (MetaTrader 5) | Option 2: Alles auf der Festplatte zwischenspeichern | Option 3: Hybridansatz (die Lösung) |
|---|---|---|
|
|
|
Ich möchte Ihnen zeigen, warum dieser dritte Ansatz genial ist.
Der TickDataLoader: RAM-Caching richtig umgesetzt
Hier ist die entscheidende Erkenntnis: Tick-Daten sollten im Arbeitsspeicher (RAM) und nicht auf der Festplatte gespeichert werden, da es sich dabei um das Ausgangsmaterial handelt, das Sie auf vielfältige Weise verarbeiten. Die Klasse TickDataLoader kontrolliert alle Daten, die bereits in der aktuellen Sitzung geladen wurden. Wenn wir unsere Modellentwicklung mit EURUSD-Daten aus dem Jahr 2023 beginnen und später beschließen, Daten aus den Jahren 2021–2022 hinzuzufügen, wird nur der fehlende Teil geladen – der Cache wird nahtlos erweitert. Stattdessen ruft TickDataLoader nur die Daten ab, die sich nicht im Cache befanden, und fügt sie den bereits geladenen Daten hinzu. Der folgende Code ist ein Ausschnitt aus der Klasse TickDataLoader. Den vollständigen Code finden Sie in der beigefügten Datei model_development.py.
class TickDataLoader: """ Loader for tick-level bid/ask data with intelligent local caching. Features: 1. Smart caching that checks if requested date range is within cached ranges 2. Handles partial overlaps by reusing available cached data 3. Memory management with cache size limits 4. Cache statistics tracking Notes ----- - Typical performance: ~0.5s for cached retrieval - Memory usage: ~100MB per 1M ticks """ def __init__(self, max_cache_size_mb: int = 3000, max_cached_symbols: int = 20): """ Initialize the tick data loader. Parameters ---------- max_cache_size_mb : int, optional Maximum cache size in MB (default: 5000MB) max_cached_symbols : int, optional Maximum number of symbols to keep in cache (default: 20) """ self._cache: Dict[Tuple[str, str], pd.DataFrame] = {} # (symbol, account_name) -> DataFrame self._cache_metadata: Dict[Tuple[str, str], Dict] = {} # (symbol, account_name) -> metadata self.max_cache_size_mb = max_cache_size_mb self.max_cached_symbols = max_cached_symbols self.cache_stats = { "hits": 0, "misses": 0, "partial_hits": 0, "total_loaded": 0, } def get_tick_data( self, symbol: str, start_date: str, end_date: str, account_name: str ) -> pd.DataFrame: """ Retrieve tick-level bid/ask data with intelligent caching. Parameters ---------- symbol : str Trading instrument symbol (e.g., 'EURUSD') start_date : str Start date in 'YYYY-MM-DD' format end_date : str End date in 'YYYY-MM-DD' format account_name : str MT5 account identifier for data retrieval Returns ------- pd.DataFrame Tick data with columns ['bid', 'ask'] indexed by timestamp Notes ----- - Checks if cached data fully covers requested date range - If partial coverage exists, loads only missing data - Merges cached and newly loaded data seamlessly """ cache_key = (symbol, account_name) start_dt, end_dt = date_conversion(start_date, end_date) # Check if we have cached data for this symbol/account if cache_key in self._cache: cached_df = self._cache[cache_key] metadata = self._cache_metadata[cache_key] cached_start, cached_end = date_conversion(metadata["start_date"], metadata["end_date"]) # Check if cached data fully covers requested range if cached_start <= start_dt and cached_end >= end_dt: self.cache_stats["hits"] += 1 logger.debug(f"Cache hit for {symbol} {start_date} to {end_date}") # Return subset of cached data mask = (cached_df.index >= start_dt) & (cached_df.index <= end_dt) return cached_df[mask].copy() # Check if there's partial overlap if cached_end >= start_dt and cached_start <= end_dt: self.cache_stats["partial_hits"] += 1 logger.debug(f"Partial cache hit for {symbol}") return self._load_with_partial_cache( symbol, start_date, end_date, account_name, cache_key ) # No cache hit, load all data self.cache_stats["misses"] += 1 logger.debug(f"Cache miss for {symbol} {start_date} to {end_date}") return self._load_and_cache_data(symbol, start_date, end_date, account_name, cache_key)
Das System geht intelligent mit drei Szenarien um.

Im Folgenden finden Sie ein Beispiel für die Verwendung der Klasse TickDataLoader:
# Initialize the loader once per session from ..production.model_development import TickDataLoader loader = TickDataLoader( max_cache_size_mb=3000, # 3GB RAM limit max_cached_symbols=20, # Cache up to 20 symbols ) # First request - loads from disk/MT5 and caches ticks = loader.get_tick_data( symbol="EURUSD", start_date="2023-01-01", end_date="2023-12-31", account_name="FUNDEDNEXT_STLR2_6K" ) # Takes ~30 seconds (first load) # Second request - same data, instant from RAM ticks = loader.get_tick_data( symbol="EURUSD", start_date="2023-01-01", end_date="2023-12-31", account_name="FUNDEDNEXT_STLR2_6K" ) # Takes ~0.5 seconds (cache hit) # Partial request - subset is instant ticks = loader.get_tick_data( symbol="EURUSD", start_date="2023-01-01", end_date="2023-03-31", account_name="FUNDEDNEXT_STLR2_6K" ) # Takes ~0.5 seconds (subset from cache) ticks = loader.get_tick_data( symbol="EURUSD", start_date="2022-01-01", end_date="2023-12-31", account_name="FUNDEDNEXT_STLR2_6K" ) # Takes ~15 seconds (only 2022 data)
Im Folgenden sind Merkmale aufgeführt, die Tick-Daten verwenden:
# Example of features that use tick data
from ..util.volatility import two_time_scale_realized_vol
def two_time_scale_realized_vol(
tick_prices: pd.Series, slow_freq: str = "5min"
) -> float:
"""
Two-Time-Scale Realized Volatility Estimator
Advanced estimator for tick data that removes microstructure noise while
preserving information. Combines high-frequency and low-frequency sampling
to extract true volatility signal.
**What it measures:**
- True underlying volatility from noisy tick data
- Removes bid-ask bounce and other microstructure effects
- Preserves information lost in simple low-frequency sampling
**Mathematical foundation:**
- TSRV = RV_slow - (n_slow/n_fast) * (RV_fast - RV_slow)
- Uses ratio of observation counts to properly scale noise estimate
- Asymptotically consistent under jump-diffusion models
**Best used for:**
- High-frequency trading strategies
- When you have access to tick data
- Precision-critical applications (research, risk management)
- Markets with significant microstructure noise
**Advantages:**
- Most accurate volatility estimate for tick data
- Removes upward bias from microstructure noise
- Retains more information than sparse sampling
- Theoretically well-founded
**Computational considerations:**
- More intensive than simple realized volatility
- Requires choice of slow sampling frequency
- Benefits increase with data quality and frequency
**Typical slow frequencies:**
- 1 minute: Very liquid assets, high precision needed
- 5 minutes: Most common, good noise reduction
- 15-30 minutes: Less liquid assets
:param tick_prices: (pd.Series) Tick-level price data, datetime indexed
:param slow_freq: (str) Slow sampling frequency ('5min', '1min', etc.)
:return: (float) Two-time-scale realized volatility estimate
"""
# Fast scale (tick-by-tick)
tick_returns = np.log(tick_prices / tick_prices.shift(1)).dropna()
rv_fast = (tick_returns**2).sum()
n_fast = len(tick_returns)
# Slow scale (e.g., 5-minute)
slow_prices = tick_prices.resample(slow_freq).last().dropna()
slow_returns = np.log(slow_prices / slow_prices.shift(1)).dropna()
rv_slow = (slow_returns**2).sum()
n_slow = len(slow_returns)
# Two-time-scale estimator with proper scaling
if n_fast > 0 and n_slow > 0:
tsrv = rv_slow - (n_slow / n_fast) * (rv_fast - rv_slow)
return max(tsrv, 0) # Ensure non-negative result
else:
return rv_slow
two_time_scale_vol = two_time_scale_realized_vol(tick_prices=ticks["bid"], slow_freq="5min") Warum RAM statt Festplatte?
Denken Sie einmal über Ihren typischen Forschungsablauf nach:
# You might experiment with: tick_bars_M1 = make_bars(ticks, "tick", "M1", "mid_price") tick_bars_M5 = make_bars(ticks, "tick", "M5", "mid_price") volume_bars = make_bars(ticks, "volume", 1000, "mid_price") dollar_bars = make_bars(ticks, "dollar", 5000, "mid_price") # And different price types: tick_bars_bid = make_bars(ticks, "tick", "M1", "bid") tick_bars_ask = make_bars(ticks, "tick", "M1", "ask")Sie erstellen 6 verschiedene Bar-Typen aus denselben Tick-Daten. Wenn Sie Ticks und Bars auf der Festplatte zwischenspeichern würden, würden Sie dieselben Basisdaten mehrfach in leicht unterschiedlichen Aggregationen speichern. Das ist Verschwendung. Möglicherweise entscheiden Sie sich auch dafür, mikrostrukturelle Merkmale zu erstellen (wofür Tick-Daten erforderlich sind). Aus diesen Gründen ist es am besten, die Tick-Daten im RAM zu speichern, falls Sie sie später einmal benötigen sollten.
Stattdessen:
- Ticks werden im RAM gespeichert (schneller Zugriff, werden zwischen den Sitzungen gelöscht)
- Jede einzelne Barkonfiguration wird im Parquet-Format zwischengespeichert (persistent, kompakt)
- Die Tick-Daten dienen als verbindliche Referenzquelle für die Sitzung.
- Parquet-Dateien dienen als kompilierte Artefakte für die Langzeitarchivierung
Die Architektur aus TickDataLoader + Parquet funktioniert, weil sie einer grundlegenden Erkenntnis der Forschung im Bereich des maschinellen Lernens Rechnung trägt:
Rohdaten sollten schnell verfügbar sein (RAM), verarbeitete Daten sollten dauerhaft gespeichert werden (Parquet), und man sollte niemals etwas duplizieren, was man neu generieren kann.
Diese Datenarchitektur lässt sich von der individuellen Forschung bis hin zur Zusammenarbeit im Team skalieren. Wenn alle mit Parquet-Dateien arbeiten, ergibt sich Folgendes:
- Einheitliche Datentypen (kein „Auf meinem Rechner funktioniert es“)
- Effiziente Speicherung (kompatibel mit Versionskontrollsystemen)
- Schnelle Iteration (die Kosten der Experimente nähern sich Null)
- Reproduzierbarkeit (Parquet-Dateien sind binärstabil)
Vergleichen Sie dies mit CSV-basierten Arbeitsabläufen, bei denen Sie ständig mit folgenden Problemen zu kämpfen haben:
- Inkonsistente Datumsauswertung
- Probleme mit der Gleitkommagenauigkeit
- Kodierungsprobleme
- Lange Ladezeiten
- Große Dateigrößen
- Probleme bei der Schemavalidierung
Die Anfangsinvestition in die Einrichtung eines Parquet-basierten Speichers zahlt sich jeden Tag aus. Und wenn man dies mit einem intelligenten RAM-Caching der Quelldaten (Ticks) kombiniert, erhält man ein System, das sowohl schnell als auch effizient ist – ganz ohne Kompromisse.
Der Arbeitsablauf
Lassen Sie mich Ihnen ein konkretes Beispiel erläutern: die Entwicklung einer Bollinger-Band-Umkehrstrategie für den EURUSD. Wir werden genau sehen, was in jeder Phase passiert und welche Artefakte dabei entstehen.
Phase 1: Konfiguration
Alles beginnt mit der Konfiguration. Keine über mehrere Dateien verstreuten Parameter, sondern eine einzige maßgebliche Konfigurationsquelle:
from model_development import ModelDevelopmentPipeline from strategies import BollingerBandStrategy # Data configuration data_config = { "symbol": "EURUSD", "train_start": "2023-01-01", "train_end": "2023-12-31", "account_name": "FUNDEDNEXT_STLR2_6K", "bar_type": "tick", "bar_size": "M1", # Will be converted to tick count "price": "mid_price" } # Feature configuration feature_config = { "func": calculate_bollinger_features, "params": { "window": 20, "num_std": 2.0 } } # Triple-barrier labeling target_config = { "func": get_daily_vol, "params": {"lookback": 20} } label_config = { "profit_target": 1.0, "stop_loss": 2.0, "max_holding_period": {"days": 1}, "min_ret": 0.0, "vertical_barrier_zero": True, "filter_as_series": False } # Model training parameters model_params = { "pipe_clf": RandomForestClassifier( criterion="entropy", class_weight="balanced_subsample", min_weight_fraction_leaf=0.05 ), "param_grid": { "n_estimators": [100, 200, 300], "max_depth": [3, 5, 7, 10], "min_samples_split": [2, 5, 10] }, "cv_splits": 5, "bagging_n_estimators": 10, "n_jobs": -1, "random_state": 42 } # Initialize strategy strategy = BollingerBandStrategy( window=20, std=2.0, )
Beachten Sie, dass jeder Parameter explizit angegeben und dokumentiert ist. In drei Monaten werden Sie genau wissen, was Sie getan haben.
Stufe 2: Initialisierung der Pipeline
Nun erstellen wir die Pipeline mit intelligenter Dateiverwaltung:
pipeline = ModelDevelopmentPipeline(
strategy=strategy,
data_config=data_config,
feature_config=feature_config,
target_config=target_config,
label_config=label_config,
model_params=model_params,
base_dir="Models"
)
Im Hintergrund wird dadurch eine Verzeichnisstruktur erstellt: Models/ └── BollingerBandStrategy/ └── EURUSD/ └── FUNDEDNEXT_STLR2_6K/ └── tick/ └── M1/ └── 20230101_20231231/ └── a3f7b2e9/ # Config hash ├── config.json ├── logs/ ├── plots/ └── reports/Jede Konfiguration erhält ein eigenes Verzeichnis. Kein „model_v2_final_WIRKLICH_FINAL.pkl“ mehr.
Phase 3: Ausführung
Nachdem die Konfiguration abgeschlossen ist, führen wir die gesamte Pipeline aus:
model, features, metrics, config = pipeline.run( generate_reports=True, save=True, export_onnx=True, verbose=True )Jetzt beginnt der Zauber. Schauen wir uns einmal an, was in den einzelnen Phasen geschieht:
Schritt 1: Laden der Daten (mit Smart Caching)
[Schritt 1/7] Trainingsdaten werden geladen...
✓ Cache-Treffer für EURUSD vom 01.01.2023 bis zum 31.12.2023
✓ 15.847.392 Ticks in 0,4 s abgerufen
✓ 262.800 M1-Bars erzeugt
Das System prüft, ob wir diese Daten bereits geladen haben. In diesem Fall werden sie innerhalb von Millisekunden statt Minuten aus dem Cache abgerufen. Aber hier kommt der entscheidende Punkt: Das Caching ist zeitabhängig. Es protokolliert, auf welche Daten wann zugegriffen wurde, und verhindert so, dass zukünftige Daten in Ihren Trainingsdatensatz gelangen.
# From model_development.py @cacheable(time_aware=True) def load_and_prepare_training_data(symbol, start_date, end_date, ...): tick_df = loader.get_tick_data(symbol, start_date, end_date, account_name) data = make_bars(tick_df, bar_type, bar_size, price) # Log data access for contamination tracking log_data_access( dataset_name=f"{symbol}_{bar_type}_{bar_size}_{price}".lower(), start_date=data.index[0], end_date=data.index[-1], purpose="train" ) return dataSchritt 2: Feature-Engineering
✓ 47 Merkmale generiert
- 12 Merkmale des Bollinger-Bands
- 15 Momentum-Merkmale
- 8 volumenbasierte Merkmale
- 12 zeitbasierte Merkmale
@cacheable(time_aware=True) def create_feature_engineering_pipeline(data, feature_config, data_config): func = feature_config["func"] features = func(data, **feature_config["params"]) time_feat = get_time_features(data, timeframe=data_config["bar_size"]) return features.join(time_feat).dropna()Wenn sich Ihre Feature-Funktion ändert, wird der Cache automatisch ungültig gemacht. Wenn sich nur die Daten ändern, werden die Merkmale effizient neu berechnet.
Schritt 3: Label-Erstellung
[Schritt 3/7] Ereignisse werden generiert...
✓ 89.247 Ereignisse generiert
✓ Verteilung der Labels:
- Long (1): 38.562 (43,2 %)
- None (0): 24.891 (27,9 %)
- Short (-1): 25.794 (28,9 %)
@cacheable() def generate_events_triple_barrier(data, strategy, target_config, profit_target, stop_loss, ...): # Calculate dynamic barriers based on volatility close = data["close"] fn = target_config["func"] params = target_config["params"] target = fn(close, **params) # Get entry signals from strategy side, t_events = get_entries(strategy, data, target.mean()) # Apply triple-barrier method vb = add_vertical_barrier(t_events, close, **max_holding_period) events = triple_barrier_labels(close, target, t_events, vertical_barrier_times=vb, side_prediction=side, pt_sl=[profit_target, stop_loss], ...) # Add sample weights events = get_event_weights(events, close) return events
Schritt 4: Optimierung der Stichprobengewichtung
Jetzt wird es erst richtig spannend. Anstatt die Stichprobengewichte zu schätzen, suchen wir nach dem optimalen Gewichtungsschema:
Testen verschiedener Gewichtungsschemata:
- ungewichtet + linearer Abklingvorgang
- ungewichtet + exponentieller Abklingfaktor
- ertragsbasiert + linearer Abklingfaktor
- ertragsbasiert + exponentieller Abklingfaktor
- Einzigartigkeit + linearer Abklingvorgang
- Einzigartigkeit + exponentieller Abklingvorgang
✓ Bestes Schema: uniqueness_linear_0.7428 (CV-Wert: 0,6847)
Das System probiert verschiedene Gewichtungsansätze aus und wählt denjenigen aus, der bei der bereinigten Kreuzvalidierung die beste Leistung erbringt:
@cacheable() def get_optimal_sample_weight( data_index: pd.DatetimeIndex, events: pd.DataFrame, features: pd.DataFrame, cv_splits: int = 5, n_iter: int = 10, ) -> pd.Series: """ Compute best sample weight with time decay. Parameters ---------- data_index: pd.DatetimeIndex Price data index. events : pd.DataFrame Event labels with uniqueness weights. features: pd.DataFrame Training features cv_splits : int, optional Number of cross-validation splits (default: 5). n_iter : int, optional Number of random search iterations (default: 10). Returns ------- weights : pd.Series Computed sample weights. cv_results : dict Cross-validation results. """ valid_index = features.index.intersection(events.index) cont = events.loc[valid_index] X = features.loc[valid_index] y = cont["bin"] classifier = RandomForestClassifier( criterion="entropy", class_weight="balanced_subsample", max_samples=cont["tW"].mean(), max_depth=4, min_weight_fraction_leaf=0.05, ) scoring = "f1" if set(y.unique()) == {0, 1} else "neg_log_loss" cv_gen = PurgedKFold(n_splits=cv_splits, t1=cont["t1"], pct_embargo=0.02) weights = [ ("return", cont["w"]), ("unweighted", pd.Series(1.0, index=cont.index)), ("uniqueness", cont["tW"]), ] best_score = 0 cv_results = pd.DataFrame() for scheme, weight in tqdm(weights, desc="Analyzing weighting schemes", total=len(weights)): scores = ml_cross_val_score( classifier, X, y, cv_gen, sample_weight_train=weight, sample_weight_score=weight, scoring=scoring, ) score = scores.mean() cv_results[scheme] = scores if not np.isinf(score) and score > best_score: best_score = score best_weight = weight best_scheme = scheme est = weighted_estimator(classifier, cont, data_index) param_distributions = { "scheme": [best_scheme], "decay": uniform(0, 1), # decay factor between 0 and 1 inclusive "linear": [True, False], } gs = RandomizedSearchCV( estimator=est, param_distributions=param_distributions, n_iter=n_iter, cv=cv_gen, scoring=scoring, n_jobs=-1, random_state=42, refit=False, ) gs.fit(X, y) scheme, decay, linear = [gs.best_params_[k] for k in ["scheme", "decay", "linear"]] best_scheme = f"{scheme}_{'linear' if linear else 'exp'}_{decay}" logger.info(f"Best sample weight scheme: {best_scheme}") decay_vec = get_weights_by_time_decay_optimized( triple_barrier_events=cont, close_index=data_index, last_weight=decay, linear=linear, av_uniqueness=cont["tW"], ) best_weight *= decay_vec cv_results = { "best_score": best_score, "cv_results_scheme": cv_results, "cv_results": pd.DataFrame(gs.cv_results_), "scoring": scoring, "best_scheme": best_scheme, } return best_weight, cv_results
Auf diese Weise finden Sie heraus, welches Gewichtungsschema Ihrer spezifischen Strategie tatsächlich am meisten nützt.
Schritt 5: Meta-Merkmale
✓ 8 Meta-Merkmale hinzugefügt
- rolling_accuracy_20, rolling_accuracy_50
- rolling_precision_20, rolling_precision_50
- rolling_recall_20, rolling_recall_50
- rolling_f1_20, rolling_f1_50
Anhand dieser Merkmale lässt sich feststellen, wie gut das Modell in letzter Zeit abgeschnitten hat, was die Leistung beim Meta-Labeling verbessern kann. Die Berechnung wurde mit Numba auf Geschwindigkeit optimiert:
@njit(parallel=True, fastmath=True, cache=True) def _rolling_metrics_numba(y_true, y_pred, weights, window): """Numba-accelerated rolling metrics calculation.""" n = len(y_true) accuracy = np.full(n, np.nan) precision = np.full(n, np.nan) recall = np.full(n, np.nan) f1 = np.full(n, np.nan) for i in prange(window - 1, n): start = i - window + 1 tp = fp = tn = fn = 0.0 # Inner loop for window for j in range(start, i + 1): if y_true[j] == 1 and y_pred[j] == 1: tp += weights[j] elif y_true[j] == 0 and y_pred[j] == 1: fp += weights[j] elif y_true[j] == 0 and y_pred[j] == 0: tn += weights[j] elif y_true[j] == 1 and y_pred[j] == 0: fn += weights[j] total = tp + fp + tn + fn if total > 0: accuracy[i] = (tp + tn) / total denom_prec = tp + fp if denom_prec > 0: precision[i] = tp / denom_prec denom_rec = tp + fn if denom_rec > 0: recall[i] = tp / denom_rec if not np.isnan(precision[i]) and not np.isnan(recall[i]): denom_f1 = precision[i] + recall[i] if denom_f1 > 0: f1[i] = 2 * (precision[i] * recall[i]) / denom_f1 return accuracy, precision, recall, f1
Schritt 6: Modelltraining mit Hyperparameter-Suche
Dies ist das Herzstück des Systems – intelligente Hyperparameter-Optimierung mit ordnungsgemäßer Kreuzvalidierung:
[Schritt 6/7] Modell mit Kreuzvalidierung trainieren...
Fortschritt der Rastersuche: Testen von 81 Parameterkombinationen
Verwendung einer 5-fachen, bereinigten Kreuzvalidierung
Embargo: 1 % zwischen den Folds
[████████████████████████████████████] 81/81 (100 %)
✓ Optimale Parameter ermittelt:
- n_estimators: 200
- max_depth: 7
- min_samples_split: 5
✓ Kreuzvalidierungswerte:
- Mittelwert F1: 0,6847 ± 0,0234
- Fold 0: 0,6923
- Fold 1: 0,6801
- Fold 2: 0,6889
- Fold 3: 0,6712
- Fold 4: 0,6912
✓ Bagging-Ensemble aus trainierten Modellen (10 Schätzer)...
✓ Modelltraining abgeschlossen
Bei der Hyperparameter-Suche wird eine purged k-fold-Kreuzvalidierung verwendet, um Datenleckagen zu verhindern:
def train_model_with_cv( features: pd.DataFrame, events: pd.DataFrame, sample_weight: np.ndarray, pipe_clf: Union[ClassifierMixin, Pipeline], param_grid: Dict, cv_splits: int = 5, bagging_n_estimators: int = 0, bagging_max_samples: float = 1.0, bagging_max_features: float = 1.0, rnd_search_iter: int = 0, n_jobs: int = -1, pct_embargo: float = 0.02, random_state: int = None, verbose: int = 0, ) -> Tuple[RandomForestClassifier, Dict]: """ Train model with cross-validation using cached hyperparameter search. Parameters ---------- features : pd.DataFrame Feature matrix. events : pd.DataFrame Event labels. sample_weight : np.ndarray Sample weights aligned with events. pipe_clf : sklearn.Pipeline Pipeline including classifier. param_grid : dict Hyperparameter grid for search. cv_splits : int, default=5 Number of CV splits. bagging_n_estimators : int, default=0 Number of bagging estimators. bagging_max_samples : float, default=1.0 Max samples for bagging. bagging_max_features : float, default=1.0 Max features for bagging. rnd_search_iter : int, default=0 Randomized search iterations. n_jobs : int, default=-1 Parallel jobs. pct_embargo : float, default=0.02 Embargo percentage for purging CV splits. random_state : int, optional Random seed. verbose : int, default=0 Verbosity flag. Returns ------- best_model : RandomForestClassifier Trained best model. cv_results : dict Cross-validation results. """ valid_index = features.index.intersection(events.index) cont = events.loc[valid_index] X = features.loc[valid_index] y = cont["bin"] t1 = cont["t1"] w = sample_weight.loc[valid_index] best_model, cv_results = clf_hyper_fit( features=X, labels=y, t1=t1, pipe_clf=pipe_clf, param_grid=param_grid, cv=cv_splits, bagging_n_estimators=bagging_n_estimators, bagging_max_samples=bagging_max_samples, bagging_max_features=bagging_max_features, rnd_search_iter=rnd_search_iter, n_jobs=n_jobs, pct_embargo=pct_embargo, random_state=random_state, verbose=verbose, sample_weight=w, ) return best_model, cv_results
- Leistungsverteilung über alle Parameterkombinationen hinweg
- Streudiagramm: Punktzahl vs. Stabilität
- Analyse des Verhältnisses von Punktzahl zu Trainingszeit
- Rangliste der Parameter nach ihrer Bedeutung
- Konsistenz der Kreuzvalidierungsfolds
Schritt 7: Analyse der Merkmalsbedeutung
[Step 7/7] Analyzing feature importance... 2025-12-16 11:18:21 [info ] pipeline_step duration_seconds=0.1509995460510254 metrics={'top_feature': 'rolling_accuracy_20'} status=completed step=feature_analysis Top 10 Features: feature importance rolling_accuracy_20 0.166330 rolling_f1_20 0.132372 rolling_accuracy_50 0.131260 rolling_f1_50 0.109463 spread 0.029035 d1_vol 0.025773 hour_cos_h2 0.022493 ret_1_lag_1 0.022391 parkinson_vol_10 0.019278 london_session_vol 0.018018
Die Analyse der Merkmalsbedeutung hilft uns zu verstehen, was das Modell gelernt hat, und potenzielle Probleme zu erkennen.
Phase 4: Automatisierte Berichterstellung
Nach Abschluss des Trainings erstellt das System automatisch umfassende Analyseberichte. So sieht die Ausgabe nach Abschluss des Vorgangs aus:
2025-12-16 11:14:08 | afml.production.model_development | INFO | Starting pipeline for EURUSD 2025-12-16 11:14:08 | afml.production.model_development | INFO | Training period: 2022-01-01 to 2023-12-31 2025-12-16 11:14:08 | afml.production.model_development | INFO | Output directory: Models\Bollinger_w10_std1_5\EURUSD\FUNDEDNEXT_STLR2_6K\time\M1\20220101_20231231\bdad004f ====================================================================== PRODUCTION MODEL DEVELOPMENT PIPELINE ====================================================================== Configuration -------------------------------------------------- strategy Bollinger_w10_std1.5 symbol EURUSD training_start 2022-01-01 training_end 2023-12-31 account_name FUNDEDNEXT_STLR2_6K bar_type time bar_size M1 price mid_price target_lookback 10 profit_target 1 stop_loss 2 max_holding_period {'days': 1} min_ret 0 vertical_barrier_zero True filter_as_series False 2025-12-16 11:14:09 [info ] pipeline_step status=started step=pipeline_start strategy=Bollinger_w10_std1.5 symbol=EURUSD train_period=2022-01-01_2023-12-31 [Step 1/7] Loading training data... 2025-12-16 11:14:09 | afml.cache.unified_cache_system | DEBUG | Cache HIT: afml.production.model_development.load_and_prepare_training_data (key: d890725d...) 2025-12-16 11:14:09 [info ] pipeline_step duration_seconds=0.10399937629699707 metrics={'samples': 738824} status=completed step=data_loading ✓ Loaded 738,824 samples [Step 2/7] Computing features... 2025-12-16 11:14:09 | afml.cache.unified_cache_system | DEBUG | Cache HIT: afml.production.model_development.create_feature_engineering_pipeline (key: 4e32ea77...) 2025-12-16 11:14:09 [info ] pipeline_step duration_seconds=0.699995756149292 metrics={'features_generated': 68} status=completed step=feature_engineering ✓ Generated 68 features [Step 3/7] Generating events... 2025-12-16 11:14:09 | afml.cache.unified_cache_system | DEBUG | Cache HIT: afml.production.model_development.generate_events_triple_barrier (key: 5a7b1eb0...) 2025-12-16 11:14:09 [info ] pipeline_step duration_seconds=0.14899849891662598 metrics={'events_generated': 75975, 'label_distribution': count proportion bin 1 49,791 0.65536 0 26,184 0.34464, 'average_uniqueness': 0.4667911271632548} status=completed step=label_generation ✓ Generated events: count proportion bin 1 49,791 0.65536 0 26,184 0.34464 Average Uniqueness: 0.4668 [Step 4/7] Computing sample weights... 2025-12-16 11:18:04 | afml.production.model_development | INFO | Best Weighting Scheme: Uniqueness Exp 0.05808 2025-12-16 11:18:07 | afml.cache.unified_cache_system | DEBUG | Cache MISS: afml.production.model_development.find_optimal_sample_weight (key: 9d10ce42...) (00:03:57) 2025-12-16 11:18:07 [info ] pipeline_step duration_seconds=237.31548690795898 metrics={'weighting_scheme': 'uniqueness_exp_0.05808361216819946', 'weight_cv_score': 0.7457131539000748} status=completed step=weight_computation ✓ Best weighting scheme: uniqueness_exp_0.05808361216819946 [Step 5/7] Computing rolling meta-label features... 2025-12-16 11:18:07 | afml.cache.unified_cache_system | DEBUG | Cache HIT: afml.production.model_development.calculate_rolling_metrics (key: ae8cdce2...) 2025-12-16 11:18:20 [info ] pipeline_step duration_seconds=13.350001573562622 metrics={'meta_features_added': 8, 'features_after_preprocessing': 72, 'features_removed': -4} status=completed step=meta_features_preprocessing ✓ Computed rolling meta-label features ✓ Preprocessed features: 72 features retained [Step 6/7] Training model with cross-validation... 2025-12-16 11:18:21 | afml.cache.unified_cache_system | DEBUG | Cache HIT: afml.cross_validation.hyper_fit.clf_hyper_fit_internal (key: 907bd2af...) 2025-12-16 11:18:21 [info ] pipeline_step duration_seconds=0.4719998836517334 metrics={'cv_score': 0.6915768921130778, 'best_params': {'clf__max_depth': 6, 'clf__max_features': 0.12075618253727419, 'clf__min_samples_leaf': 3, 'clf__min_samples_split': 13, 'clf__min_weight_fraction_leaf': 0.28140549728612524, 'clf__n_estimators': 369}, 'n_splits': 5} status=completed step=model_training ✓ Best CV score: 0.6916 ✓ Best params: {'clf__max_depth': 6, 'clf__max_features': 0.12075618253727419, 'clf__min_samples_leaf': 3, 'clf__min_samples_split': 13, 'clf__min_weight_fraction_leaf': 0.28140549728612524, 'clf__n_estimators': 369} [Step 7/7] Analyzing feature importance... 2025-12-16 11:18:21 [info ] pipeline_step duration_seconds=0.1509995460510254 metrics={'top_feature': 'rolling_accuracy_20'} status=completed step=feature_analysis Top 10 Features: feature importance rolling_accuracy_20 0.166330 rolling_f1_20 0.132372 rolling_accuracy_50 0.131260 rolling_f1_50 0.109463 spread 0.029035 d1_vol 0.025773 hour_cos_h2 0.022493 ret_1_lag_1 0.022391 parkinson_vol_10 0.019278 london_session_vol 0.018018 [Generating Reports] Creating analysis reports... 🔍 Running hyperparameter analysis... ================================================================================ HYPERPARAMETER ANALYSIS REPORT ================================================================================ 1. TOP PERFORMING MODELS (sorted by mean_test_score): -------------------------------------------------- params mean_test_score std_test_score mean_fit_time 4 {'clf__max_depth': 6, 'clf__max_features': 0.12075618253727419, 'clf__min_samples_leaf': 3, 'clf__min_samples_split': 13, 'clf__min_weight_fraction_leaf': 0.28140549728612524, 'clf__n_estimators': 369} 0.691577 0.011027 28.989994 2 {'clf__max_depth': 4, 'clf__max_features': 0.15077042112439024, 'clf__min_samples_leaf': 8, 'clf__min_samples_split': 13, 'clf__min_weight_fraction_leaf': 0.4723487190570876, 'clf__n_estimators': 435} 0.683852 0.011899 28.105996 6 {'clf__max_depth': 4, 'clf__max_features': 0.9539969835279999, 'clf__min_samples_leaf': 2, 'clf__min_samples_split': 10, 'clf__min_weight_fraction_leaf': 0.05718481349909639, 'clf__n_estimators': 435} 0.677322 0.006976 614.569299 17 {'clf__max_depth': 4, 'clf__max_features': 0.8984914683186939, 'clf__min_samples_leaf': 1, 'clf__min_samples_split': 4, 'clf__min_weight_fraction_leaf': 0.10381741067223577, 'clf__n_estimators': 180} 0.675696 0.006911 175.406800 1 {'clf__max_depth': 5, 'clf__max_features': 0.5133240027692805, 'clf__min_samples_leaf': 5, 'clf__min_samples_split': 5, 'clf__min_weight_fraction_leaf': 0.11429006806487335, 'clf__n_estimators': 180} 0.670490 0.007110 95.500397 5 {'clf__max_depth': 3, 'clf__max_features': 0.14180537144799796, 'clf__min_samples_leaf': 3, 'clf__min_samples_split': 8, 'clf__min_weight_fraction_leaf': 0.1267358556592812, 'clf__n_estimators': 216} 0.669676 0.009235 33.474998 10 {'clf__max_depth': 4, 'clf__max_features': 0.905344615384884, 'clf__min_samples_leaf': 8, 'clf__min_samples_split': 15, 'clf__min_weight_fraction_leaf': 0.13819228808861533, 'clf__n_estimators': 239} 0.669449 0.008087 196.870216 16 {'clf__max_depth': 4, 'clf__max_features': 0.1858691048413702, 'clf__min_samples_leaf': 7, 'clf__min_samples_split': 6, 'clf__min_weight_fraction_leaf': 0.37832278025212884, 'clf__n_estimators': 409} 0.667428 0.007923 36.593196 13 {'clf__max_depth': 5, 'clf__max_features': 0.4810613326357327, 'clf__min_samples_leaf': 1, 'clf__min_samples_split': 13, 'clf__min_weight_fraction_leaf': 0.18206967862311718, 'clf__n_estimators': 112} 0.664284 0.002792 40.329997 12 {'clf__max_depth': 3, 'clf__max_features': 0.3528410587186427, 'clf__min_samples_leaf': 9, 'clf__min_samples_split': 16, 'clf__min_weight_fraction_leaf': 0.12437012257835113, 'clf__n_estimators': 394} 0.664078 0.007689 133.290791 2. PERFORMANCE SUMMARY: -------------------------------------------------- Average mean_test_score: 0.6656 ± 0.0106 Best mean_test_score: 0.6916 Worst mean_test_score: 0.6482 Performance Range: 0.0434 3. STABILITY ANALYSIS: -------------------------------------------------- Models with stable performance (std ≤ 0.03): 20 Best stable model: 0.6916 ± 0.0110 4. TIME-EFFICIENCY ANALYSIS: -------------------------------------------------- 5. HYPERPARAMETER TRENDS: -------------------------------------------------- Parameter: clf__max_depth Optimal value: 4 (score: 0.6716) Performance by value: score_mean score_std count fold_std_mean time_mean param_clf__max_depth 4 0.6716 0.0098 6 0.0088 184.0137 6 0.6659 0.0177 4 0.0104 99.7223 3 0.6638 0.0048 4 0.0088 108.2067 5 0.6607 0.0073 6 0.0076 60.1069 Parameter: clf__max_features Optimal value: 0.12075618253727419 (score: 0.6916) Performance by value: score_mean score_std count fold_std_mean time_mean param_clf__max_features 0.120756 0.6916 NaN 1 0.0110 28.9900 0.150770 0.6839 NaN 1 0.0119 28.1060 0.953997 0.6773 NaN 1 0.0070 614.5693 0.898491 0.6757 NaN 1 0.0069 175.4068 0.513324 0.6705 NaN 1 0.0071 95.5004 0.141805 0.6697 NaN 1 0.0092 33.4750 0.905345 0.6694 NaN 1 0.0081 196.8702 0.185869 0.6674 NaN 1 0.0079 36.5932 0.481061 0.6643 NaN 1 0.0028 40.3300 0.352841 0.6641 NaN 1 0.0077 133.2908 0.749557 0.6636 NaN 1 0.0093 166.1914 0.316923 0.6623 NaN 1 0.0069 119.1400 0.484787 0.6620 NaN 1 0.0052 37.7866 0.816889 0.6600 NaN 1 0.0078 117.2920 0.278958 0.6595 NaN 1 0.0110 46.2450 0.860080 0.6581 NaN 1 0.0081 132.6173 0.933671 0.6579 NaN 1 0.0089 99.8698 0.568061 0.6556 NaN 1 0.0112 52.5364 0.992990 0.6515 NaN 1 0.0156 118.1418 0.918388 0.6482 NaN 1 0.0115 23.4877 Parameter: clf__min_samples_leaf Optimal value: 3 (score: 0.6744) Performance by value: score_mean score_std count fold_std_mean time_mean param_clf__min_samples_leaf 3 0.6744 0.0153 3 0.0085 33.4172 5 0.6705 NaN 1 0.0071 95.5004 8 0.6682 0.0114 4 0.0097 97.1283 2 0.6677 0.0136 2 0.0076 373.5933 7 0.6644 0.0027 3 0.0081 107.3082 1 0.6638 0.0121 3 0.0084 111.2929 9 0.6610 0.0044 2 0.0083 116.5803 4 0.6519 0.0052 2 0.0113 38.0120 Parameter: clf__min_samples_split Optimal value: 10 (score: 0.6773) Performance by value: score_mean score_std count fold_std_mean time_mean param_clf__min_samples_split 10 0.6773 NaN 1 0.0070 614.5693 13 0.6707 0.0165 5 0.0097 66.9416 5 0.6705 NaN 1 0.0071 95.5004 4 0.6676 0.0115 2 0.0090 110.8259 6 0.6674 NaN 1 0.0079 36.5932 15 0.6665 0.0041 2 0.0087 181.5308 8 0.6624 0.0051 4 0.0078 72.1058 16 0.6611 0.0042 2 0.0079 132.9540 19 0.6519 0.0052 2 0.0113 38.0120 Parameter: clf__min_weight_fraction_leaf Optimal value: 0.28140549728612524 (score: 0.6916) Performance by value: score_mean score_std count fold_std_mean time_mean param_clf__min_weight_fraction_leaf 0.281405 0.6916 NaN 1 0.0110 28.9900 0.472349 0.6839 NaN 1 0.0119 28.1060 0.057185 0.6773 NaN 1 0.0070 614.5693 0.103817 0.6757 NaN 1 0.0069 175.4068 0.114290 0.6705 NaN 1 0.0071 95.5004 0.126736 0.6697 NaN 1 0.0092 33.4750 0.138192 0.6694 NaN 1 0.0081 196.8702 0.378323 0.6674 NaN 1 0.0079 36.5932 0.182070 0.6643 NaN 1 0.0028 40.3300 0.124370 0.6641 NaN 1 0.0077 133.2908 0.272208 0.6636 NaN 1 0.0093 166.1914 0.272830 0.6623 NaN 1 0.0069 119.1400 0.263917 0.6620 NaN 1 0.0052 37.7866 0.250625 0.6600 NaN 1 0.0078 117.2920 0.405579 0.6595 NaN 1 0.0110 46.2450 0.422932 0.6581 NaN 1 0.0081 132.6173 0.432517 0.6579 NaN 1 0.0089 99.8698 0.398810 0.6556 NaN 1 0.0112 52.5364 0.325244 0.6515 NaN 1 0.0156 118.1418 0.348135 0.6482 NaN 1 0.0115 23.4877 6. CROSS-VALIDATION CONSISTENCY: -------------------------------------------------- Fold performance consistency: Fold 0: 0.6758 ± 0.0113 Fold 1: 0.6734 ± 0.0104 Fold 2: 0.6607 ± 0.0091 Fold 3: 0.6612 ± 0.0127 Fold 4: 0.6570 ± 0.0149 7. MODEL SELECTION RECOMMENDATION: -------------------------------------------------- ✅ RECOMMENDATION: Choose stable model Score: 0.6916 (vs best: 0.6916) Stability: 0.0110 (vs best: 0.0110) Performance difference: 0.0000 (insignificant) 🎯 RECOMMENDED HYPERPARAMETERS: clf__max_depth: 6 clf__max_features: 0.12075618253727419 clf__min_samples_leaf: 3 clf__min_samples_split: 13 clf__min_weight_fraction_leaf: 0.28140549728612524 clf__n_estimators: 369 8. GENERATING VISUALIZATIONS... 9. PRACTICAL INTERPRETATION FOR TRADING: -------------------------------------------------- Expected Strategy Performance: • Best mean_test_score: 0.6916 • Cross-validation Consistency: Good ✅ LOW RISK: Excellent consistency across CV folds Strategy likely to perform similarly in live trading ================================================================================ ANALYSIS COMPLETE ================================================================================ SPECIFIC INSIGHTS FROM YOUR RESULTS: ================================================================================ 1. KEY OBSERVATIONS: -------------------------------------------------- max_depth: 6 min_samples_leaf: 3 min_samples_split: 13 n_estimators: 369 Best Model mean_test_score: 0.6916 Standard Deviation: 0.0110 Training Time: 28.99s Best Simple Model (param_clf__max_depth ≤ 4): param_clf__max_depth=4 mean_test_score: 0.6839 (vs best: 0.6916) 2. PERFORMANCE SATURATION: -------------------------------------------------- Maximum performance by max_depth: depth=3: 0.6697 depth=4: 0.6839 depth=5: 0.6705 depth=6: 0.6916 3. ACTIONABLE RECOMMENDATIONS: -------------------------------------------------- ✅ Excellent performance achieved! Consider testing with additional features or ensemble methods 4. PRODUCTION CONSIDERATIONS: -------------------------------------------------- Expected Inference Speed: ~199.1ms per prediction Training Time Range: 23.49s to 614.57s Average Model Size: ~303 trees ✅ Markdown report generated: Models\Bollinger_w10_std1_5\EURUSD\FUNDEDNEXT_STLR2_6K\time\M1\20220101_20231231\bdad004f\reports\hyperparameter_analysis_report.md 2025-12-16 11:18:24 | afml.production.model_development | INFO | Generated hyperparameter report: Models\Bollinger_w10_std1_5\EURUSD\FUNDEDNEXT_STLR2_6K\time\M1\20220101_20231231\bdad004f\reports\hyperparameter_analysis_report.md 2025-12-16 11:18:25 | afml.production.model_development | INFO | Generated feature importance plot: Models\Bollinger_w10_std1_5\EURUSD\FUNDEDNEXT_STLR2_6K\time\M1\20220101_20231231\bdad004f\plots\feature_importance.png 2025-12-16 11:18:25 | afml.production.model_development | WARNING | HTML summary generation failed: 'numpy.ndarray' object is not callable ✓ Reports generated in 4.15s [Saving] Writing artifacts to disk... 2025-12-16 11:18:29 | afml.production.model_development | INFO | Saved all artifacts to Models\Bollinger_w10_std1_5\EURUSD\FUNDEDNEXT_STLR2_6K\time\M1\20220101_20231231\bdad004f ✓ Saved to Models\Bollinger_w10_std1_5\EURUSD\FUNDEDNEXT_STLR2_6K\time\M1\20220101_20231231\bdad004f 2025-12-16 11:18:29 [info ] pipeline_step duration_seconds=260.72733187675476 metrics={'total_duration': 260.72733187675476, 'model_trained': True, 'reports_generated': True} status=completed step=pipeline_complete ✓ Pipeline completed in 00:04:21 ======================================================================
Phase 5: Modellexport und Validierung
Im letzten Schritt wird das Modell nach einer umfassenden Validierung in das ONNX-Format exportiert:
[Saving] Writing artifacts to disk... ONNX EXPORT PIPELINE
======================================================================
ONNX EXPORT PIPELINE
======================================================================
[Step 1/5] Preparing metadata...
✓ Model type: RandomForestClassifier
✓ Features: 74
✓ Version: 1.0
[Step 2/5] Converting to ONNX format...
✓ Conversion successful
✓ ONNX opset: 12 (MQL5 compatible)
[Step 3/5] Saving ONNX model...
✓ Saved to: C:\Users\JoeN\Documents\GitHub\Machine-Learning-Blueprint\models\bollinger_meta_model_eurusd_tick_m1.onxx
✓ File size: 0.16 MB
[Step 4/5] Validating ONNX model...
✓ ONNX model structure valid
[Step 5/5] Comparing Python vs ONNX predictions...
Generating test data...
Computing Python predictions...
Computing ONNX predictions...
✓ ONNX returned 2 output(s)
Output 0: shape=(1000,), dtype=int64
Sample values: 1
Output 1: shape=(1000, 2), dtype=float32
Sample values: [0.48720157 0.5127984 ]
✓ Using output 1 (probabilities)
✓ Extracted positive class probabilities from shape (1000, 2)
Prediction Comparison (1000 samples):
• Max difference: 1.52e-07
• Mean difference: 4.91e-08
• Std difference: 3.30e-08
✅ VALIDATION PASSED - Predictions match within tolerance (1.00e-05)
Sample Predictions (first 5):
Index Python ONNX Diff
--------------------------------------------------
0 0.512798 0.512798 8.19e-08
1 0.459688 0.459688 5.50e-08
2 0.451748 0.451748 7.06e-08
3 0.478857 0.478857 6.04e-08
4 0.460223 0.460223 6.36e-08
======================================================================
✅ EXPORT SUCCESSFUL - Model ready for MQL5 deployment
======================================================================
Dieser Validierungsschritt ist von entscheidender Bedeutung. Das System generiert zufällige Testdaten und überprüft, ob das ONNX-Modell bitweise identische Vorhersagen wie das Python-Modell liefert. Sollte es zu Abweichungen kommen, schlägt der Export sofort fehl, anstatt im Produktivbetrieb unbemerkt falsche Vorhersagen zu liefern.
Die Ergebnisse verstehen
Sobald der Prozess abgeschlossen ist, verfügen Sie über eine vollständige Nachvollziehbarkeit aller Schritte. Jede Datei ist mit einem Zeitstempel versehen, versioniert und verlinkt. Sie können Ihre Forschungshistorie wie eine gut organisierte Bibliothek durchstöbern.

| Dateiformate (Farbe als Schlüssel) | Funktion |
|---|---|
| Konfigurationsdateien | Alle Einstellungen, die zur Wiederholung dieses Experiments erforderlich sind. Der Konfigurations-Hash gewährleistet die Eindeutigkeit. |
| Modelldateien | Das trainierte Modell in verschiedenen Formaten: Python (joblib) für die Forschung, ONNX für die Bereitstellung auf MQL5. |
| Datenartefakte | Verarbeitete Merkmale, Labels und Gewichtungen. Ermöglicht die Modellanalyse ohne Neuberechnung. |
| Berichte und Protokolle | Umfassende Dokumentation: Ausführungsprotokolle, Leistungsdiagramme, Analyseberichte. |
Was Ihnen diese Organisation bietet
- Einfache Navigation: Jedes Experiment lässt sich nach Strategie, Symbol und Datumsbereich auffinden.
- Perfekte Reproduzierbarkeit: Der Konfigurations-Hash gewährleistet Eindeutigkeit und Rückverfolgbarkeit
- Experimente vergleichen: Direkter Vergleich verschiedener Konfigurationen
- Schnelle Bereitstellung: ONNX-Datei, die direkt in einen MQL5-EA eingefügt werden kann
- Vollständige Nachvollziehbarkeit aller Schritte: Alles wird automatisch dokumentiert
Um trainierte Modelle zu finden, die bestimmten Kriterien entsprechen, können Sie den folgenden Code verwenden:
from ..production.utils import ModelFileManager file_manager = ModelFileManager() base_dir = "Models" # base_dir is path to where trained models are saved search_criteria = {"bar_size": "M1", "bar_type": "time"} file_manager.find_models(search_criteria, base_dir)
Die Vorteile des Cachings
Lassen Sie uns darüber sprechen, was dieses System so schnell macht. Der Kern dieser Effizienz liegt in der intelligenten Logik zur Cache-Invalidierung, die sicherstellt, dass bei Änderungen nur die notwendigen Komponenten der Pipeline neu berechnet werden. Indem der Workflow des maschinellen Lernens als Abhängigkeitsgraf behandelt wird, kann das System zwischen umfangreichen Änderungen – wie beispielsweise der Änderung von Datensymbolen, die eine vollständige Neuberechnung aller Stufen erfordern – und geringfügigeren Änderungen – wie der Optimierung von Modell-Hyperparametern – unterscheiden. Im letzteren Fall verwendet das System zwischengespeicherte Daten für die Bars, Merkmale, Labels und Gewichte wieder und führt lediglich den abschließenden Schritt des Modelltrainings durch. Diese detaillierte Steuerung verhindert redundante Berechnungen und ermöglicht es Forschern, bestimmte Teile der Strategie – wie beispielsweise das Feature-Engineering oder die Label-Erzeugung – zu optimieren, ohne Zeit mit der erneuten Verarbeitung des gesamten Datensatzes zu verschwenden.
Wenn Sie mehr über mein Caching-System erfahren möchten, lesen Sie den letzten Artikel meiner Reihe.

Nach dem ersten Durchlauf laufen die nachfolgenden Experimente deutlich schneller ab.
Cache-Sicherheit und Datenkontamination
Das wichtigste Merkmal des Caching-Systems ist der Schutz vor Datenkontamination. Jeder Datenzugriff wird protokolliert:
@cacheable(time_aware=True) def load_and_prepare_training_data(...): # ... load data ... log_data_access( dataset_name=f"{symbol}_{bar_type}_{bar_size}".lower(), start_date=data.index[0], end_date=data.index[-1], purpose="train", data_shape=data.shape) return data
Sie können jederzeit prüfen, ob eine Datenkontamination vorliegt:
from ..cache import print_contamination_report print_contamination_report()

Praktische Anwendungsbeispiele
Ich möchte Ihnen einige gängige Forschungsabläufe vorstellen:
1. Strategievergleich
strategies = [ BollingerBandStrategy(window=20, num_std=2.0), BollingerBandStrategy(window=20, num_std=2.5), BollingerBandStrategy(window=30, num_std=2.0), ] results = [] for strategy in strategies: pipeline = ModelDevelopmentPipeline( symbol="EURUSD", train_start="2023-01-01", train_end="2023-12-31", strategy=strategy, # ... config ... ) model, features, metrics, config = pipeline.run(verbose=False) results.append({ "strategy": strategy.get_strategy_name(), "cv_score": metrics["cv_results"]["best_score"], "features": len(features), "model": model }) comparison = pd.DataFrame(results).sort_values("cv_score", ascending=False) print(comparison)
Dank des Cachings lässt sich der Vergleich der Strategien schnell durchführen – lediglich das Modelltraining ändert sich.
2. Experimente zur Merkmalsentwicklung
feature_configs = [
{"func": calculate_basic_features, "params": {}},
{"func": calculate_advanced_features, "params": {"lookback": 20}},
{"func": calculate_advanced_features, "params": {"lookback": 50}},
]
for i, feat_config in enumerate(feature_configs):
pipeline = ModelDevelopmentPipeline(
# ... same data config ... feature_config=feat_config,
# ... rest of config ...
)
model, features, metrics, _ = pipeline.run()
print(f"Feature set {i+1}: {metrics['cv_results']['best_score']:.4f}") 3. Walk-Forward-Analyse
train_periods = [ ("2022-01-01", "2022-12-31"), ("2022-07-01", "2023-06-30"), ("2023-01-01", "2023-12-31"), ] models = [] for train_start, train_end in train_periods: pipeline = ModelDevelopmentPipeline( symbol="EURUSD", train_start=train_start, train_end=train_end, # ... config ... ) model, _, metrics, _ = pipeline.run() models.append({ "period": f"{train_start} to {train_end}", "score": metrics["cv_results"]["best_score"], "model_path": pipeline.file_paths["model"] })
Die übersichtliche Verzeichnisstruktur erleichtert den Vergleich von Modellen über verschiedene Zeiträume hinweg.
Leistungsüberwachung
Das System erfasst seine eigene Leistung:
from ..cache import print_health_report print_health_report()

Integration mit MQL5
Der ONNX-Export vereinfacht die Bereitstellung erheblich. Hier ist ein minimalistisches MQL5-EA-Grundgerüst:
//+------------------------------------------------------------------+ //| BollingerBandStrategy_EA.mq5 | //+------------------------------------------------------------------+ #property version "1.00" #property strict //--- Include Resources #include <Trade\Trade.mqh> //--- Load the ONNX model from resources #resource "\\Models\\BollingerBandStrategy\\EURUSD\\model.onnx" as uchar model_data[] //--- Input Parameters input double InpLotSize = 0.01; // Trade Lot Size input double InpThresholdBuy = 0.55; // Probability to Buy input double InpThresholdExit = 0.45; // Probability to Exit/Sell //--- Global Variables long model_handle = INVALID_HANDLE; CTrade trade; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Load ONNX model from the resource buffer model_handle = OnnxCreateFromBuffer(model_data, ONNX_DEFAULT); if(model_handle == INVALID_HANDLE) { Print("Error: Failed to load ONNX model. Code: ", GetLastError()); return(INIT_FAILED); } // Set output shape if necessary (depends on your model export) // OnnxSetOutputShape(model_handle, 0, [1, 1]); Print("Model loaded successfully. Handle: ", model_handle); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(model_handle != INVALID_HANDLE) { OnnxRelease(model_handle); model_handle = INVALID_HANDLE; } } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // 1. Prepare Features double features[47]; if(!CalculateFeatures(features)) return; // 2. Prepare ONNX Vectors vector v_features; v_features.Assign(features); vector v_output; // 3. Run Inference if(!OnnxRun(model_handle, ONNX_NO_CONVERSION, v_features, v_output)) { Print("Inference failed. Error: ", GetLastError()); return; } // 4. Trade Logic double probability = v_output[0]; CheckTradeLogic(probability); } //+------------------------------------------------------------------+ //| Logic for Entry and Exit | //+------------------------------------------------------------------+ void CheckTradeLogic(double prob) { bool hasPosition = (PositionsTotal() > 0); // Buy Logic if(prob > InpThresholdBuy && !hasPosition) { trade.Buy(InpLotSize, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_ASK), 0, 0, "ONNX Signal"); } // Exit Logic else if(prob < InpThresholdExit && hasPosition) { trade.PositionClose(_Symbol); } } //+------------------------------------------------------------------+ //| Feature Calculation - MUST MATCH PYTHON PRE-PROCESSING | //+------------------------------------------------------------------+ bool CalculateFeatures(double &features[]) { ArrayInitialize(features, 0.0); // Example: Getting Bollinger Bands data for the current bar double bb_upper = iBands(_Symbol, PERIOD_CURRENT, 20, 2.0, 0, PRICE_CLOSE, MODE_UPPER, 0); double bb_lower = iBands(_Symbol, PERIOD_CURRENT, 20, 2.0, 0, PRICE_CLOSE, MODE_LOWER, 0); double close = iClose(_Symbol, PERIOD_CURRENT, 0); // Fill your 47 features here exactly as your model expects // features[0] = (close - bb_lower) / (bb_upper - bb_lower); // Normalized example // ... return true; }
Entscheidend ist, dass Ihre MQL5-Merkmale exakt mit den in Python berechneten Merkmalen übereinstimmen. Dabei hilft die im Trainingsbericht enthaltene Merkmalsliste.
Debugging und Fehlerbehebung
Wenn ein Fehler auftritt, liefert das System detaillierte Diagnoseinformationen:
Beispiel: Leistungseinbußen des Modells
# Get all experiments for a symbol file_manager = ModelFileManager("Models") models = file_manager.find_models({ "symbol": "EURUSD", "strategy": "BollingerBandStrategy" }) # Compare performance over time for model_info in models: metrics = json.load(open(model_info["file_path"].replace(".joblib", "_metrics.json"))) print(f"{model_info['date_range']}: {metrics['cv_results']['best_score']:.4f}")
Sie können schnell feststellen, wann sich die Leistung verändert hat, und die entsprechende Konfiguration laden, um zu sehen, was anders war.
Beispiel: Cache-Probleme
Wenn Sie den Verdacht haben, dass der Cache beschädigt ist:
from cache import clear_cache # Clear specific function cache clear_cache("load_and_prepare_training_data") # Or clear everything clear_cache()
Das System wird von Grund auf neu aufgebaut, wodurch die Korrektheit gewährleistet wird.
Bewährte Verfahren
Nachdem ich dieses System nun seit Monaten nutze, sind hier meine Empfehlungen:
1. Verwenden Sie stets aussagekräftige Strategienamen
# Good strategy name BollingerBandStrategy(window=20, num_std=2.0) strategy.get_strategy_name() # "BollingerBand_w20_std2.0" # Better - includes your hypothesis strategy BollingerBandStrategy(window=20, num_std=2.0, name="BB_MeanReversion_v1")
Ihr zukünftiges Ich wird es Ihnen danken, wenn Sie später durch 50 Experimente navigieren müssen.
2. Beginnen Sie mit kleinen Parametergittern
# First iteration - quick feedback param_grid = { "n_estimators": [100, 200], "max_depth": [3, 5, 7] } # After you understand the landscape param_grid = { "n_estimators": [100, 200, 300, 500], "max_depth": [3, 5, 7, 10, 15], "min_samples_split": [2, 5, 10, 20] }
Zuerst schnell iterieren, dann verfeinern.
3. Alles speichern
Speicherplatz ist günstig, Reproduzierbarkeit ist unbezahlbar:
pipeline.run( generate_reports=True, # Always save=True, # Always export_onnx=True, # If deploying verbose=True # Until you're confident )
4. Berichte vor der Bereitstellung prüfen
Treffen Sie Ihre Bereitstellungsentscheidungen nicht allein auf der Grundlage des CV-Scores. Überprüfen:
- Konsistenz der CV-Folds (hohe Varianz = unzuverlässig)
- Bedeutung der Merkmale (sinnvolle Merkmale?)
- Verteilung der Labels (ausgewogen?)
- ONNX-Validierung (bestanden?)
Die Berichte machen das ganz einfach.
5. Versionieren Sie Ihre Merkmalsfunktionen
def calculate_features_v1(data, **params): """Initial feature set - baseline.""" # ... features ... def calculate_features_v2(data, **params): """Added momentum indicators.""" # ... features ... feature_config = { "func": calculate_features_v2, # Track version in function name "params": {} }
Wenn Sie Merkmale verbessern, erstellen Sie eine neue Funktion. Der Cache übernimmt die Versionsverwaltung automatisch.
Schlussfolgerung
Der Aufbau eines produktionsreifen Forschungssystems erfordert zwar Anfangsinvestitionen, doch der Nutzen ist enorm:
- Geschwindigkeit: 10-mal mehr Experimente in derselben Zeit
- Zuverlässigkeit: Reproduzierbare Ergebnisse, auf die Sie sich verlassen können
- Einblick: Eine automatische Analyse deckt Muster auf, die man sonst übersehen würde
- Bereitstellung: Export nach MQL5 mit nur einem Klick
- Wartung: Einfach zu debuggen, einfach zu erweitern
Der Code, den wir uns angesehen haben, veranschaulicht einige der wichtigsten Aspekte einer Forschungsmethodik im Bereich des maschinellen Lernens für den Finanzsektor. Es sorgt für die Einhaltung bewährter Verfahren: ordnungsgemäße Kreuzvalidierung, Stichprobengewichtung, Merkmalsanalyse (wenn auch in sehr begrenztem Umfang) und Validierung. So wird der richtige Weg zum einfachen Weg.
Vor allem aber können Sie sich so auf das Wesentliche konzentrieren: die Entwicklung besserer Handelsstrategien. Die technische Infrastruktur funktioniert einfach.
Nächste Schritte
So richten Sie dieses System ein:
- Fangen Sie klein an: Beginnen Sie mit einer einzigen Strategie und einem Symbol
- Überprüfen des Caching: Führen Sie die Pipeline zweimal aus und überprüfen Sie die Beschleunigung
- Prüfen der Berichte: Überprüfen Sie alle erstellten Berichte
- ONNX validieren: Stellen Sie sicher, dass die Vorhersagen exakt übereinstimmen
- Vorsichtig vorgehen: Beginnen Sie mit dem Papierhandel
- Iterieren: Nutzen Sie das System, um systematische Experimente durchzuführen
Der vollständige Code ist in den beigefügten Dateien verfügbar. Beginnen Sie mit der Datei model_development.py im Verzeichnis „production“ und arbeiten Sie sich durch das Beispiel-Notebook.
Denken Sie daran: Trading ist schon schwer genug, ohne dass Sie sich zusätzlich mit Ihren Forschungswerkzeugen herumschlagen müssen. Entwickeln Sie Systeme, die Ihnen zum Erfolg verhelfen.
Datei-Referenzhandbuch
| Datei | Hauptzweck | Wichtige Klassen/Funktionen | Abhängigkeiten | Wann zu verwenden |
|---|---|---|---|---|
| __init__.py | Modul-Exporte | complete_export_workflow() export_model_to_onnx() extract_onnx_metadata() validate_onnx_predictions() | model_export.py | Stellt die ONNX-Exportfunktion auf Paketebene bereit |
| dual_model_development.py | Bid/Ask-sensitive separate Long/Short-Modelle | BidAskLongShortPipeline train_bidask_longshort_models() | model_development.py, Strategieklassen | Wenn Sie separate Modelle für Long- und Short-Positionen unter Verwendung realistischer Ausführungskurse benötigen (Long → Ask, Short → Bid) |
| model_development.py | Kern-Produktionspipeline | ModelDevelopmentPipeline TickDataLoader load_and_prepare_training_data() generate_events_triple_barrier() train_model_with_cv() | feature_engine, sklearn, numba, Cache-System | Der zentrale Arbeitsablauf für das Training und die Bewertung jedes einzelnen Modells mit vollständiger Reproduzierbarkeit |
| model_export.py | ONNX-Konvertierung und -Validierung | export_model_to_onnx() validate_onnx_predictions() complete_export_workflow() | skl2onnx, onnxruntime | Letzter Schritt vor der MQL5-Bereitstellung – stellt sicher, dass das Produktionsmodell exakt mit Python übereinstimmt |
| utils.py | Dateiverwaltung und -organisation | ConfigPathGenerator ModelFileManager find_models() load_artifacts() | hashlib, json, pickle, pathlib | Verwaltung der Modellverzeichnisstruktur, Speichern und Laden von Artefakten sowie Auffinden früherer Experimente |
Dateibeziehungen:
- __init__.py → model_export.py (stellt Exportfunktionen bereit)
- dual_model_development.py → model_development.py (erweitert die Basis-Pipeline)
- model_development.py → utils.py (nutzt Dateiverwaltung)
- model_export.py → Eigenständig (kann unabhängig laufen)
- Alle Dateien → System-Cache (für die Leistung)
Wichtige Integrationspunkte:
- dual_model_development.py erstellt zwei ModelDevelopmentPipeline-Instanzen (lang/kurz)
- Der ModelFileManager in utils.py wird von ModelDevelopmentPipeline zur strukturierten Speicherung verwendet
- export_model_to_onnx() wird von ModelDevelopmentPipeline aufgerufen, wenn export_onnx=True ist
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20451
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.
Verwendung des MQL5-Wirtschaftskalenders zur Nachrichtenfilterung (Teil 1): Implementierung von Zeitfenstern vor und nach Nachrichtenereignissen in MQL5
Bivariate Copulas in MQL5 (Teil 3): Implementierung und Optimierung von Mixed-Copula-Modellen in MQL5
MQL5-Trading Tools (Teil 19): Entwicklung einer interaktiven Werkzeugpalette zum Zeichnen in Charts
Workshop zu benutzerdefinierten Indikatoren (Teil 2): Entwicklung eines praxistauglichen Supertrend-Expert-Advisors in MQL5
- 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.