MetaTrader 5 Machine Learning Blueprint (Teil 1): Datenlecks und Zeitstempelfehler
Einführung
Willkommen zum ersten Teil unserer Serie MetaTrader 5 Machine Learning Blueprint. Dieser Artikel befasst sich mit einem kritischen, aber oft übersehenen Problem bei der Erstellung robuster maschineller Lernmodelle für Finanzmärkte unter Verwendung von MetaTrader 5-Daten: der „Zeitstempel-Falle“. Wir werden untersuchen, wie eine falsche Handhabung von Zeitstempeln zu heimtückischen Datenlecks führen kann, die die Integrität Ihrer Modelle gefährden und unzuverlässige Handelssignale erzeugen. Noch wichtiger ist, dass wir konkrete Lösungen und bewährte Verfahren anbieten, die sich auf etablierte Branchenforschung stützen, um sicherzustellen, dass Ihre Daten sauber, unverfälscht und bereit für fortgeschrittene quantitative Analysen sind.
Zielpublikum & Voraussetzungen
Dieser Artikel richtet sich speziell an quantitative Händler, Datenwissenschaftler und Entwickler, die über ein grundlegendes Verständnis von Python, Pandas und der MetaTrader 5 API verfügen. Die Vertrautheit mit grundlegenden Konzepten des maschinellen Lernens ist ebenfalls von Vorteil. Unser Ziel ist es, Sie mit dem praktischen Wissen und den Werkzeugen auszustatten, die Sie benötigen, um hochintegrierte Datensätze für die Entwicklung vertrauenswürdiger maschineller Lernmodelle im algorithmischen Handel zu erstellen.
Vorgehensplan der Serie
Dieser Artikel markiert den Beginn einer umfassenden Serie, die sich mit dem Aufbau eines vollständigen, maschinellen Lernkonzepts für MetaTrader 5 beschäftigt. In diesem Teil legen wir die wesentlichen Grundlagen, indem wir die Datenintegrität sicherstellen. Künftige Themen werden sich mit fortgeschritteneren Stadien der maschinellen Lernpipeline befassen, darunter:
- Teil 2 - Fortgeschrittene Merkmalstechnik und Kennzeichnung: Techniken zur Definition von Zielvariablen, die die tatsächliche Marktdynamik erfassen.
- Teil 3 - Modelltraining und Validierung: Bewährte Verfahren für das Training, die Validierung und die Auswahl von maschinellen Lernmodellen, die auf Finanzzeitreihen zugeschnitten sind.
- Teil 4 - Strenge Rücktests und Einsatz: Methoden zur Bewertung der Modellleistung in realistischen Handelsumgebungen und Strategien für den Einsatz von Modellen in der Praxis.
Die Zeitstempel-Falle des MetaTrader 5: Verständnis und Prävention
Das Ausspähen von Daten oder Datenlecks mag unauffällig erscheinen, aber die Auswirkungen auf maschinelle Lernmodelle können gewaltig sein - und verheerend. Stellen Sie sich vor, Sie lernen für einen Test und schauen sich unwissentlich die Antworten vorher an. Ihr perfektes Ergebnis fühlt sich verdient an, ist aber eigentlich Betrug. Genau das passiert, wenn wir die Standard-Zeitstempel von MetaTrader 5 für das maschinelle Lernen verwenden - ein Datenleck untergräbt unerwartet die Integrität Ihres Modells.
Wie die Zeitstempel des MetaTrader 5 Sie austricksen

| Time | Open | High | Low | Close |
|---|---|---|---|---|
|
|
|
|
|
Durch den Zeitstempel am Anfang impliziert MetaTrader 5, dass die Daten dieses Balkens um 18:55:00 Uhr zur Verfügung standen - ganze 5 Minuten vor seinem tatsächlichen Ende! Wenn Ihr Modell dies beim Training verwendet, ist es so, als würde man einem Schüler 5 Minuten vor Beginn der Prüfung die Antworten geben. Um dem entgegenzuwirken, sollten wir es vermeiden, die vorkompilierten Zeitbalken von MetaTrader 5 zu verwenden, sondern stattdessen Tick-Daten nutzen, um die Balken zu erstellen, die wir in unseren Modellen verwenden.
Warum Datenlecks wichtig sind
Ein Datenleck kann Ihr gesamtes maschinelles Lernprojekt stillschweigend ruinieren. Das passiert, wenn Ihr Modell versehentlich aus Informationen lernt, die es während des Trainings nicht haben sollte - wie ein Blick in die Zukunft. Infolgedessen sähe das Modell beim Training unglaublich genau aus, aber in Wirklichkeit wurde es nur mit Antworten gefüttert, die es in der realen Welt nie bekommen würde.
Anstatt echte Muster zu lernen, fängt das Modell an, Geräusche auswendig zu lernen und wird wie ein Schüler, der den Lösungsschlüssel paukt, ohne den Stoff zu verstehen. Dies führt zu einer schlechten Leistung, wenn es an der Zeit ist, echte Vorhersagen über neue Daten zu treffen.
Noch schlimmer ist, dass ein Modell, das mit durchgesickerten Daten trainiert wurde, zwar vertrauenswürdig erscheint, aber beim Einsatz nicht funktioniert. Das kann zu falschem Selbstvertrauen und schlechten Entscheidungen führen - was besonders gefährlich ist, wenn viel auf dem Spiel steht, wie beim Handel, wo selbst kleine Fehler teuer werden können.
Die nachträgliche Behebung eines Datenlecks ist frustrierend. Das bedeutet in der Regel, dass Sie große Teile Ihrer Pipeline neu erstellen müssen, was Zeit, Rechenressourcen und manchmal sogar Geld kostet. Deshalb ist das frühzeitige Erkennen und Verhindern von Datenlecks so wichtig.
Warum Tick-Bars wichtig sind: Eine quantitative Perspektive
Finanzdaten treffen oft in unregelmäßigen Abständen ein, und damit wir sie für das maschinelle Lernen (ML) nutzen können, müssen wir sie regularisieren, da die meisten ML-Algorithmen Daten in einem Tabellenformat erwarten. Die Zeilen dieser Tabellen werden im Allgemeinen als „Balken“ bezeichnet. Die Charts, die wir auf MetaTrader 5 und praktisch allen anderen Chart-Plattformen sehen, stellen Zeitbalken dar, die Tick-Daten in die Spalten Open, High, Low, Close und Volume umwandeln, indem sie Ticks über einen festen Zeithorizont, z. B. einmal pro Minute, abfragen.
Obwohl Zeitleisten bei Praktikern und Wissenschaftlern vielleicht am beliebtesten sind, sollten sie aus zwei Gründen vermieden werden. Erstens verarbeiten die Märkte Informationen nicht in einem konstanten Zeitintervall. Die Stunde nach der Eröffnung ist viel aktiver als die Stunde um die Mittagszeit (oder die Stunde um Mitternacht im Falle von Futures). Als biologische Wesen ist es für den Menschen sinnvoll, seinen Tag nach dem Sonnenzyklus zu gestalten.
Die heutigen Märkte werden jedoch von Algorithmen betrieben, die unter lockerer menschlicher Aufsicht handeln, für die CPU-Verarbeitungszyklen viel wichtiger sind als zeitliche Abstände. Das bedeutet, dass die Zeitbalken in Zeiten geringer Aktivität zu viele Informationen enthalten und in Zeiten hoher Aktivität zu wenige Informationen enthalten. Zweitens weisen Zeitreihen oft schlechte statistische Eigenschaften auf, wie serielle Korrelation, Heteroskedastizität und Nichtnormalität der Erträge.
(López de Prado, 2018, S.26)
- Die Intention des Praktikers verstehen: Erfahrene Praktiker versehen ihre Zeitbalken zu Recht mit einem Zeitstempel am Ende des Intervalls (z. B. 09:01:00 für den Zeitraum 09:00:00-09:00:59.999). Dieser entscheidende Schritt stellt sicher, dass alle Informationen für einen abgeschlossenen Balken zu seinem aufgezeichneten Zeitstempel wirklich bekannt sind, wodurch die klassische Vorausschau auf künftige Balken verhindert wird.
- Das subtile Intra-Bar Leakage: Eine subtilere Form des Datenverlusts kann jedoch auch bei der Bildung dieses Zeitstrahls auftreten. Wenn ein bedeutendes Ereignis in der Mitte eines 1-Minuten-Balkens eintritt (z. B. um 09:00:35 Uhr), werden alle von diesem Balken abgeleiteten Merkmale (wie der Höchstpreis oder eine Kennzeichnung des Ereignisses) diese Information zwangsläufig bis zum Ende des Balkens berücksichtigen.
- Das Dilemma der Vorhersage: Wenn also ein maschinelles Lernmodell eine Vorhersage macht oder ein Signal auf dem Anfangszeitpunkt des Balkens (z. B. 09:00:00) basiert und diese Merkmale verwendet, die spätere Ereignisse innerhalb derselben Minute widerspiegeln, erhält es implizit einen unfairen Vorteil. Beim Echtzeithandel um 09:00:00 Uhr ist das Ereignis um 09:00:35 Uhr tatsächlich unbekannt.
- Aktivitätsgesteuerte Bars als Lösung: Aktivitätsgesteuerte Balken, wie z. B. Tick-Balken, umgehen dieses Problem grundsätzlich, indem sie erst nach einem vorher festgelegten Umfang der Marktaktivität (z. B. einer bestimmten Anzahl von Transaktionen oder einem bestimmten Wert des gehandelten Volumens/Dollars) abgeschlossen werden. Diese inhärente Struktur garantiert, dass alle Merkmale eines solchen Balkens aus Informationen konstruiert werden, die genau zu dem Zeitpunkt verfügbar waren , als die Bildung des Balkens abgeschlossen wurde, was auf natürliche Weise mit dem Echtzeit-Informationsfluss übereinstimmt und eine Verzerrung innerhalb des Balkens verhindert.
Aus den oben genannten Gründen sollten Zeitbalken beim Training von ML-Modellen vermieden werden. Stattdessen sollten wir Balken verwenden, deren Erstellung von der Handelsaktivität abhängt, wie z. B. Tick-, Volumen- oder Dollar-Balken. Diese werden erstellt, indem Informationen gesammelt werden, sobald eine bestimmte Anzahl von Ticks eingetroffen ist, ein bestimmtes Volumen gehandelt wurde oder ein bestimmter Dollarbetrag ausgetauscht wurde. Diese Balken erzielen Renditen, die näher an einer identischen (I), unabhängigen (I) Normalverteilung (D) liegen, was sie für ML-Modelle geeigneter macht, von denen viele davon ausgehen, dass die Beobachtungen aus einem I.I.D-Gauß-Prozess gezogen werden.
Nachfolgend werden die Verteilungen der Log-Renditen für M5-, M15- und M30-Zeit- und Tick-Balken verglichen. Die Größe der Tick-Balken wird anhand der mittleren Anzahl von Ticks über den Zeitrahmen für den Stichprobenzeitraum berechnet, und für EURUSD zwischen 2023 und 2024 erhalten wir Tick-200, 500 und 1000 Balken für die Zeitrahmen M5, M15 bzw. M30. Dies geschieht mit der Funktion calculate_ticks_per_period, die im nächsten Abschnitt beschrieben wird.

Obwohl keine der Log-Return-Verteilungen normal ist, was zu erwarten ist, sind die durch Tick-Bars erzeugten Verteilungen über alle Zeitrahmen hinweg normaler als die durch Zeit-Bars erzeugten.
Lassen Sie uns die statistischen Eigenschaften von Zeit- und Tick-Balken anhand der nachstehenden Charts genauer analysieren.


Aus den obigen Charts geht hervor, dass etwa 20 % der Zeitbalken etwa 51 % der gesamten Kursänderung erklären, während 20 % der Tick-Balken etwa 46 % der gesamten Kursänderung erklären. Bemerkenswert ist, dass praktisch alle Anteile von Tick-Bars weniger von der Gesamtpreisänderung erklären als der gleiche Anteil von Zeit-Bars, was darauf hindeutet, dass Tick-Bars besser geeignet sind, Informationen zu sammeln als Zeit-Bars. Ein Blick auf das Histogramm bestätigt dies, indem es uns zeigt, dass die absolute Preisveränderung von Tick-Bars einer viel angenehmeren statistischen Verteilung folgt (monoton abnehmend) als die von Zeit-Bars, die zufällige Varianzen aufweist.
In diesem und den folgenden Artikeln werden wir uns auf die Anwendung von ML auf Deviseninstrumente konzentrieren. Da diese nicht an einer zentralen Börse gehandelt werden, sind keine Angaben zum Volumen verfügbar, weshalb ich mich in dieser Serie auf Zeit- und Tick-Bars beschränken werde. Der Leser sollte beachten, dass ich oben nur Standard-Bar-Formationen beschrieben habe. Für weitere Informationen zu fortgeschrittenen Kerzen-Balken empfehle ich diesen Artikel, da er die Arbeit von Marcos López de Prado in Advances in Financial Machine Learning erläutert, zu dem Seminar-Anmerkungen online gefunden werden können.
Die Lösung: Neuschreiben der zeitlichen Realität durch Erstellen von Balken aus Tickdaten
Implementierung des Codes
Beginnen wir damit, Daten von unserem Terminal abzurufen und sie zu bereinigen, um zu verhindern, dass fehlerhafte Ticks zur Erstellung unserer Balkendaten verwendet werden. Ich werde Ihnen zeigen, wie Sie Zeit- und Tick-Balken erstellen. Wir werden Python verwenden, weil Pandas zeitbasierte Manipulationen ermöglicht und für ML einfach zu verwenden ist.
Schritt 0: Imports
Dies sind die Importe, die wir für die Codefragmente in diesem Artikel verwenden werden.
import numpy as np import pandas as pd import MetaTrader5 as mt5 import logging from datetime import datetime as dt logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
Schritt 1: Datenextraktion
def get_ticks(symbol, start_date, end_date): """ Downloads tick data from the MT5 terminal. Args: symbol (str): Financial instrument (e.g., currency pair or stock). start_date, end_date (str or datetime): Time range for data (YYYY-MM-DD). Returns: pd.DataFrame: Tick data with a datetime index. """ if not mt5.initialize(): logging.error("MT5 connection not established.") raise RuntimeError("MT5 connection error.") start_date = pd.Timestamp(start_date, tz='UTC') if isinstance(start_date, str) else ( start_date if start_date.tzinfo is not None else pd.Timestamp(start_date, tz='UTC') ) end_date = pd.Timestamp(end_date, tz='UTC') if isinstance(end_date, str) else ( end_date if end_date.tzinfo is not None else pd.Timestamp(end_date, tz='UTC') ) try: ticks = mt5.copy_ticks_range(symbol, start_date, end_date, mt5.COPY_TICKS_ALL) df = pd.DataFrame(ticks) df['time'] = pd.to_datetime(df['time_msc'], unit='ms') df.set_index('time', inplace=True) df.drop('time_msc', axis=1, inplace=True) df = df[df.columns[df.any()]] df.info() except Exception as e: logging.error(f"Error while downloading ticks: {e}") return None return df
Schritt 2: Datenbereinigung
def clean_tick_data(df: pd.DataFrame, n_digits: int, timezone: str = 'UTC' ) -> Optional[pd.DataFrame]: """ Clean and validate Forex tick data with comprehensive quality checks. Args: df: DataFrame containing tick data with bid/ask prices and timestamp index n_digits: Number of decimal places in instrument price. timezone: Timezone to localize/convert timestamps to (default: UTC) Returns: Cleaned DataFrame or None if empty after cleaning """ if df.empty: return None df = df.copy(deep=False) # Work on a copy to avoid modifying the original DataFrame n_initial = df.shape[0] # Store initial row count for reporting # 1. Ensure proper datetime index # Use errors='coerce' to turn unparseable dates into NaT and then drop them. if not isinstance(df.index, pd.DatetimeIndex): original_index_name = df.index.name df.index = pd.to_datetime(df.index, errors='coerce') nan_idx_count = df.index.isnull().sum() if nan_idx_count > 0: logging.info(f"Dropped {nan_idx_count:,} rows with unparseable timestamps.") df = df[~df.index.isnull()] if original_index_name: df.index.name = original_index_name if df.empty: # Check if empty after index cleaning logging.warning("Warning: DataFrame empty after initial index cleaning") return None # 2. Timezone handling if df.index.tz is None: df = df.tz_localize(timezone) elif str(df.index.tz) != timezone.upper(): df = df.tz_convert(timezone) # 3. Price validity checks # Apply rounding and then filtering df['bid'] = df['bid'].round(n_digits) df['ask'] = df['ask'].round(n_digits) # Validate prices price_filter = ( (df['bid'] > 0) & (df['ask'] > 0) & (df['ask'] > df['bid']) ) n_before_price_filter = df.shape[0] df = df[price_filter] n_filtered_prices = n_before_price_filter - df.shape[0] if n_filtered_prices > 0: logging.info(f"Filtered {n_filtered_prices:,} ({n_filtered_prices / n_before_price_filter:.2%}) invalid prices.") if df.empty: # Check if empty after price cleaning logging.warning("Warning: DataFrame empty after price cleaning") return None # Dropping NA values initial_rows_before_na = df.shape[0] if df.isna().any().any(): # Use .any().any() to check if any NA exists in the whole DF na_counts = df.isna().sum() na_cols = na_counts[na_counts > 0] if not na_cols.empty: logging.info(f'Dropped NA values from columns: \n{na_cols}') df.dropna(inplace=True) n_dropped_na = initial_rows_before_na - df.shape[0] if n_dropped_na > 0: logging.info(f"Dropped {n_dropped_na:,} ({n_dropped_na / n_before_price_filter:.2%}) rows due to NA values.") if df.empty: # Check if empty after NA cleaning logging.warning("Warning: DataFrame empty after NA cleaning") return None # 4. Microsecond handling if not df.index.microsecond.any(): logging.warning("Warning: No timestamps with microsecond precision found") # 5. Duplicate handling duplicate_mask = df.index.duplicated(keep='last') dup_count = duplicate_mask.sum() if dup_count > 0: logging.info(f"Removed {dup_count:,} ({dup_count / n_before_price_filter:.2%}) duplicate timestamps.") df = df[~duplicate_mask] if df.empty: # Check if empty after duplicate cleaning logging.warning("Warning: DataFrame empty after duplicate cleaning") return None # 6. Chronological order if not df.index.is_monotonic_increasing: logging.info("Sorting DataFrame by index to ensure chronological order.") df.sort_index(inplace=True) # 7. Final validation and reporting if df.empty: logging.warning("Warning: DataFrame empty after all cleaning steps.") return None n_final = df.shape[0] n_cleaned = n_initial - n_final percentage_cleaned = (n_cleaned / n_initial) if n_initial > 0 else 0 logging.info(f"Cleaned {n_cleaned:,} of {n_initial:,} ({percentage_cleaned:.2%}) datapoints.") return df
Schritt 3: Balken erstellen und in Endzeit konvertieren
Zunächst werden wir einige Hilfsfunktionen erstellen.
set_resampling_freq
def set_resampling_freq(timeframe: str) -> str: """ Converts an MT5 timeframe to a pandas resampling frequency. Args: timeframe (str): MT5 timeframe (e.g., 'M1', 'H1', 'D1', 'W1'). Returns: str: Pandas frequency string. """ timeframe = timeframe.upper() nums = [x for x in timeframe if x.isnumeric()] if not nums: raise ValueError("Timeframe must include numeric values (e.g., 'M1').") x = int(''.join(nums)) if timeframe == 'W1': freq = 'W-FRI' elif timeframe == 'D1': freq = 'B' elif timeframe.startswith('H'): freq = f'{x}H' elif timeframe.startswith('M'): freq = f'{x}min' elif timeframe.startswith('S'): freq = f'{x}S' else: raise ValueError("Valid timeframes include W1, D1, Hx, Mx, Sx.") return freq
calculate_ticks_per_period
def calculate_ticks_per_period(df: pd.DataFrame, timeframe: str = "M1", method: str = 'median', verbose: bool = True) -> int: """ Dynamically calculates the average number of ticks per given timeframe. Args: df (pd.DataFrame): Tick data. timeframe (str): MT5 timeframe. method (str): 'median' or 'mean' for the calculation. verbose (bool): Whether to print the result. Returns: int: Rounded average ticks per period. """ freq = set_resampling_freq(timeframe) resampled = df.resample(freq).size() fn = getattr(np, method) num_ticks = fn(resampled.values) num_rounded = int(np.round(num_ticks)) num_digits = len(str(num_rounded)) - 1 rounded_ticks = int(round(num_rounded, -num_digits)) rounded_ticks = max(1, rounded_ticks) if verbose: t0 = df.index[0].date() t1 = df.index[-1].date() logging.info(f"From {t0} to {t1}, {method} ticks per {timeframe}: {num_ticks:,} rounded to {rounded_ticks:,}") return rounded_ticks
flatten_column_names
def flatten_column_names(df): ''' Joins tuples created by dataframe aggregation with a list of functions into a unified name. ''' return ["_".join(col).strip() for col in df.columns.values]
Nun zu den wichtigsten Funktionen, die zur Erstellung von Balken verwendet werden.
make_bar_type_grouper
def make_bar_type_grouper( df: pd.DataFrame, bar_type: str = 'tick', bar_size: int = 100, timeframe: str = 'M1' ) -> tuple[pd.core.groupby.generic.DataFrameGroupBy, int]: """ Create a grouped object for aggregating tick data into time/tick/dollar/volume bars. Args: df: DataFrame with tick data (index should be datetime for time bars). bar_type: Type of bar ('time', 'tick', 'dollar', 'volume'). bar_size: Number of ticks/dollars/volume per bar (ignored for time bars). timeframe: Timeframe for resampling (e.g., 'H1', 'D1', 'W1'). Returns: - GroupBy object for aggregation - Calculated bar_size (for tick/dollar/volume bars) """ # Create working copy (shallow is sufficient) df = df.copy(deep=False) # OPTIMIZATION: Shallow copy here only once # Ensure DatetimeIndex if not isinstance(df.index, pd.DatetimeIndex): try: df = df.set_index('time') except KeyError: raise TypeError("Could not set 'time' as index") # Sort if needed if not df.index.is_monotonic_increasing: df = df.sort_index() # Time bars if bar_type == 'time': freq = set_resampling_freq(timeframe) bar_group = (df.resample(freq, closed='left', label='right') # includes data upto, but not including, the end of the period if not freq.startswith(('B', 'W')) else df.resample(freq)) return bar_group, 0 # bar_size not used # Dynamic bar sizing if bar_size == 0: if bar_type == 'tick': bar_size = calculate_ticks_per_period(df, timeframe) else: raise NotImplementedError(f"{bar_type} bars require non-zero bar_size") # Non-time bars df['time'] = df.index # Add without copying if bar_type == 'tick': bar_id = np.arange(len(df)) // bar_size elif bar_type in ('volume', 'dollar'): if 'volume' not in df.columns: raise KeyError(f"'volume' column required for {bar_type} bars") # Optimized cumulative sum cum_metric = (df['volume'] * df['bid'] if bar_type == 'dollar' else df['volume']) cumsum = cum_metric.cumsum() bar_id = (cumsum // bar_size).astype(int) else: raise NotImplementedError(f"{bar_type} bars not implemented") return df.groupby(bar_id), bar_size
make_bars
def make_bars(tick_df: pd.DataFrame, bar_type: str = 'tick', bar_size: int = 0, timeframe: str = 'M1', price: str = 'midprice', verbose=True): ''' Create OHLC data by sampling ticks using timeframe or a threshold. Parameters ---------- tick_df: pd.DataFrame tick data bar_type: str type of bars to create from ['tick', 'time', 'volume', 'dollar'] bar_size: int default 0. bar_size when bar_type != 'time' timeframe: str MT5 timeframe (e.g., 'M5', 'H1', 'D1', 'W1'). Used for time bars, or for tick bars if bar_size = 0. price: str default midprice. If 'bid_ask', columns (bid_open, ..., bid_close), (ask_open, ..., ask_close) are included. verbose: bool print information about the data Returns ------- pd.DataFrame with columns [open, high, low, close, median_price, tick_volume, volume] ''' if 'midprice' not in tick_df: tick_df['midprice'] = (tick_df['bid'] + tick_df['ask']) / 2 bar_group, bar_size_ = make_bar_type_grouper(tick_df, bar_type, bar_size, timeframe) ohlc_df = bar_group['midprice'].ohlc().astype('float64') ohlc_df['tick_volume'] = bar_group['bid'].count() if bar_type != 'tick' else bar_size_ if price == 'bid_ask': # Aggregate OHLC data for every bar_size rows bid_ask_df = bar_group.agg({k: 'ohlc' for k in ('bid', 'ask')}) # Flatten MultiIndex columns col_names = flatten_column_names(bid_ask_df) bid_ask_df.columns = col_names ohlc_df = ohlc_df.join(bid_ask_df) if 'volume' in tick_df: ohlc_df['volume'] = bar_group['volume'].sum() if bar_type == 'time': ohlc_df.ffill(inplace=True) else: end_time = bar_group['time'].last() ohlc_df.index = end_time + pd.Timedelta(microseconds=1) # ensure end time is after event df.drop('time', axis=1, inplace=True) # Remove 'time' column # drop last bar due to insufficient ticks if len(tick_df) % bar_size_ > 0: ohlc_df = ohlc_df.iloc[:-1] if verbose: if bar_type != 'time': tm = f'{bar_size_:,}' if bar_type == 'tick' and bar_size == 0: tm = f'{timeframe} - {bar_size_:,} ticks' timeframe = tm print(f'\nTick data - {tick_df.shape[0]:,} rows') print(f'{bar_type}_bar {timeframe}') ohlc_df.info() # Remove timezone info from DatetimeIndex try: ohlc_df = ohlc_df.tz_convert(None) except: pass return ohlc_df
Die Darstellung der Volatilitätsanalyse, die wir oben verwendet haben, wurden mit folgendem Code erstellt:
import plotly.graph_objects as go import numpy as np import pandas as pd def plot_volatility_analysis_of_bars(df, symbol, start, end, freq, thres=.01, bins=100): """ Plot the volatility analysis of bars using Plotly. df: DataFrame containing the data with 'open' and 'close' columns. symbol: Symbol of the asset. start: Start date of the data. end: End date of the data. freq: Frequency of the data. thres: Threshold for filtering large values, e.g., 1-.01 for 99th quantile. bins: Number of bins for the histogram. """ abs_price_changes = (df['close'] / df['open'] - 1).mul(100).abs() thres = abs_price_changes.quantile(1 - thres) abs_price_changes = abs_price_changes[abs_price_changes < thres] # filter out large values for visualization # Calculate Histogram counts, bins = np.histogram(abs_price_changes, bins=bins) bins = bins[:-1] # remove the last bin edge # Calculate Proportions total_counts = len(abs_price_changes) proportion_candles_right = [] proportion_price_change_right = [] for i in range(len(bins)): candles_right = abs_price_changes[abs_price_changes >= bins[i]] count_right = len(candles_right) proportion_candles_right.append(count_right / total_counts) proportion_price_change_right.append(np.sum(candles_right) / np.sum(abs_price_changes)) fig = go.Figure() # Histogram with Hover Template fig.add_trace( go.Bar(x=bins, y=counts, name='Histogram absolute price change (%)', marker=dict(color='#1f77b4'), hovertemplate='<b>Bin: %{x:.2f}</b><br>Frequency: %{y}', # Custom hover text yaxis='y1', opacity=.65)) ms = 3 # marker size lw = .5 # line width # Proportion of Candles at the Right with Hover Text fig.add_trace( go.Scatter(x=bins, y=proportion_candles_right, name='Proportion of candles at the right', mode='lines+markers', marker=dict(color='red', size=ms), line=dict(width=lw), hovertext=[f"Bin: {x:.2f}, Proportion: {y:.4f}" for x, y in zip(bins, proportion_candles_right)], # Hover text list hoverinfo='text', # Show only the 'text' from hovertext yaxis='y2')) # Proportion Price Change Produced by Candles at the Right with Hover Text fig.add_trace( go.Scatter(x=bins, y=proportion_price_change_right, name='Proportion price change produced by candles at the right', mode='lines+markers', marker=dict(color='green', size=ms), line=dict(width=lw), hovertext=[f"Bin: {x:.2f}, Proportion: {y:.4f}" for x, y in zip(bins, proportion_price_change_right)], # Hover text list hoverinfo='text', # Show only the 'text' from hovertext yaxis='y2')) # Indices of proportion_price_change_right at 10% intervals search_idx = [.01, .05] + np.linspace(.1, 1., 10).tolist() price_idxs = np.searchsorted(sorted(proportion_candles_right), search_idx, side='right') for ix in price_idxs: # Add annotations for every step-th data point as an example x = bins[-ix] y = proportion_candles_right[-ix] fig.add_annotation( x=x, y=y, text=f"{y:.4f}", # Display the proportion value with 4 decimal points showarrow=True, arrowhead=1, ax=0, ay=-15, # Offset for the annotation text font=dict(color="salmon"), arrowcolor="red", yref='y2' ) y = proportion_price_change_right[-ix] fig.add_annotation( x=x, y=y, text=f"{y:.4f}", # Display the proportion value with 4 decimal points showarrow=True, arrowhead=1, ax=0, ay=-25, # Offset for the annotation text font=dict(color="lightgreen"), arrowcolor="green", yref='y2' ) # Layout Configuration with Legend Inside fig.update_layout( title=f'Volatility Analysis of {symbol} {freq} from {start} to {end}', xaxis_title='Absolute price change (%)', yaxis_title='Frequency', yaxis2=dict( title='Proportion', overlaying='y', side='right', gridcolor='#444' # Set grid color for the secondary y-axis ), plot_bgcolor='#222', # Dark gray background paper_bgcolor='#222', font=dict(color='white'), xaxis=dict(gridcolor='#444'), # Set grid color for the primary x-axis yaxis=dict(gridcolor='#444'), # Set grid color for the primary y-axis legend=dict( x=0.3, # Adjust x coordinate (0 to 1) y=0.95, # Adjust y coordinate (0 to 1) traceorder="normal", # Optional: maintain trace order font=dict(color="white") # Optional: set legend text color ), # width=750, # Set width of the figure # height=480, # Set height of the figure ) return fig
Nachdem wir nun gesehen haben, wie wir aus unseren Tick-Daten strukturierte Daten in Form von Zeit- oder Tick-Balken erstellen können, wollen wir sehen, ob einige der Behauptungen, die wir über die statistischen Eigenschaften von Tick-Balken aufgestellt haben, denen von Zeit-Balken überlegen sind, zutreffen. Wir werden die EURUSD 2023 Tick-Daten verwenden, die in den Dateien unten beigefügt sind.

Wenn Sie sich die Bereiche mit hohem Tick-Volumen im M5-Chart genau ansehen, z. B. zwischen 14.00 und 16.00 Uhr, werden Sie feststellen, dass sich die im Tick-200-Chart gebildeten Balken aufgrund der verstärkten Probenahme in dieser Hochaktivitätsperiode überschneiden. Umgekehrt sind die Tick-Balken zwischen 06:00 und 08:00 Uhr spärlich und weisen große Lücken auf. Dies verdeutlicht, warum Tick-Balken als aktivitätsgesteuerte Balken bezeichnet werden, im Gegensatz zu Zeit-Balken, die gleichmäßig Daten über einen festen Zeithorizont abfragen.
Skalierbarkeit und Hardware-Empfehlungen
Die Arbeit mit hochfrequenten Tickdaten und die Konstruktion spezialisierter Balken kann rechenintensiv sein, insbesondere bei großen historischen Datensätzen. Für eine optimale Leistung und eine effiziente Verarbeitung empfehlen wir die folgenden Computereinstellungen:- RAM: Ein Minimum von 16 GB RAM ist ratsam, wobei 32 GB oder mehr für umfangreiches Backtests oder die Verarbeitung jahrelanger Tick-Daten vorzuziehen sind.
- CPU: Eine Multi-Core-CPU (z. B. Intel i7/i9 oder AMD Ryzen 7/9) wird dringend empfohlen. Die Möglichkeit, Datenverarbeitungsaufgaben auf mehrere Kerne zu verteilen, wird die Berechnungszeit erheblich verkürzen.
- Strategien der Parallelisierung: Erwägen Sie die Implementierung von Parallelverarbeitungstechniken in Ihrem Python-Code. Bibliotheken wie Dask für verteiltes Rechnen oder das in Python integrierte Multiprocessing-Modul können von unschätzbarem Wert sein, wenn es darum geht, die Datenaufbereitung, das Feature-Engineering und Backtest-Simulationen für große Datensätze zu beschleunigen.
Nächste Schritte
Um die in diesem Artikel besprochenen Konzepte effektiv anzuwenden und sich auf die folgenden Teile dieser Serie vorzubereiten, empfehlen wir die folgenden Schritte:
- Implementierung der Zeitstempelkorrektur: Integrieren Sie die bereitgestellten Codefragmente in Ihre Datenaufnahme-Pipeline, um sicherzustellen, dass alle MetaTrader 5-Daten korrekt mit einem Zeitstempel versehen und frei von Vorausschau-Bias sind.
- Experimentieren Sie mit Balken-Typen: Neben Tick-Bars können Sie auch andere spezialisierte Bar-Typen wie Volumen-Bars oder Dollar-Bars erkunden. Beobachten Sie, wie sich diese verschiedenen Stichprobenmethoden auf die Merkmale Ihres Datensatzes und ihre potenziellen Vorteile für Ihre spezifischen Handelsstrategien auswirken.
- Bereiten Sie Ihren Datensatz vor: Wenn Sie nun über saubere, unverzerrte Daten verfügen, beginnen Sie mit der Organisation und Vorbereitung Ihres Datensatzes für die nächsten Schritte der Pipeline für maschinelles Lernen. In Teil 2 werden wir uns mit fortgeschrittenem Feature Engineering und Kennzeichnungstechniken befassen.
Deshalb beschäftigen wir uns im nächsten Artikel mit zwei bahnbrechende Alternativen von Dr. Marcos López de Prados: die Triple-Barrier-Methode und die Trend-Scanning-Methode. Diese Techniken überdenken nicht nur die Kennzeichnungen - sie definieren sie neu.
Wenn Sie sich jemals gefragt haben, ob Ihre Kennzeichnungen wirklich mit dem Verhalten der Märkte übereinstimmen, ist dies die Erkenntnis, auf die Sie gewartet haben.
Schlussfolgerung
In diesem grundlegenden Artikel haben wir uns akribisch mit der kritischen „MetaTrader 5 Zeitstempel-Falle“ auseinandergesetzt und Lösungen dafür bereitgestellt. Wir haben gezeigt, wie eine unsachgemäße Handhabung von Zeitstempeln zu schwerwiegenden Datenverlusten führen kann, die fehlerhafte Modelle und unzuverlässige Handelssignale zur Folge haben. Durch die Implementierung robuster Zeitstempel-Korrekturmechanismen und die Nutzung der Leistungsfähigkeit der Tick-Balken-Konstruktion haben wir erfolgreich die Grundlage für den Aufbau hochintegrierter Datensätze gelegt. Diese grundlegende Korrektur ist von entscheidender Bedeutung, um die Gültigkeit Ihrer Forschung, die Genauigkeit Ihrer Backtests und letztlich die Zuverlässigkeit Ihrer maschinellen Lernmodelle im algorithmischen Handel zu gewährleisten. Dieser entscheidende erste Schritt ist für jeden ernsthaften quantitativen Praktiker, der wirklich effektive und vertrauenswürdige Handelssysteme entwickeln will, unerlässlich.
In den beigefügten Dokumenten finden Sie den oben verwendeten Code sowie einige Utility-Funktionen für die Anmeldung am MetaTrader 5-Terminal unter Verwendung der Python-API.
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17520
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.
Entwicklung des Price Action Analysis Toolkit (Teil 28): Werkzeug für den Ausbruch aus der Eröffnungsspanne
Meistern der Log-Einträge (Teil 7): Protokolle auf dem Chart anzeigen
Automatisieren von Handelsstrategien in MQL5 (Teil 20): Multi-Symbol-Strategie mit CCI und AO
Datenwissenschaft und ML (Teil 42): Forex-Zeitreihenvorhersage mit ARIMA in Python, alles was Sie wissen müssen
- 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.
Die tätigkeitsbezogenen Balken lösen nicht alle Probleme, die Sie für Zeitbalken genannt haben. Sie haben zum Beispiel geschrieben:
The Subtle Intra-Bar Leakage: However, a more subtle form of data leakage can still occur within the very formation of that time bar. If a significant event transpires midway through a 1-minute bar (e.g., at 09:00:35), any features derived from that bar (such as its high price or a flag for the event) will inevitably incorporate this information by the bar's end.
Wenn Sie Balken mit gleichem Volumen, gleichem Bereich oder andere Tick-basierte benutzerdefinierte Balken erstellen, werden Sie einen solchen Balken ohnehin mit einem einzigen Label markieren, und es werden Informationen über den Höchstpreis über den gesamten Balken hinweg durchsickern (oder genauer gesagt, verschwimmen).
Die einzige Möglichkeit, dieses Problem zu lösen, besteht darin, "Balken" mit den spezifischen Funktionen (die Sie verwenden werden) im Hinterkopf zu erstellen. Wenn z.B. Hochs oder Tiefs die Hauptmerkmale sind, sollten Sie versuchen, einen Zickzack-Balken" mit zeitlich exakt markierten Extermums zu erstellen.
Der Ansatz mit konstanten Zeitrahmen und insbesondere die Beschränkung auf M1 ist im Zusammenhang mit dem Datenleck im MT5 problematisch. Die Kennzeichnung von M1-Balken mit der Endzeit ist imho nicht viel besser als mit der Anfangszeit.
Für diejenigen, die daran interessiert sind, benutzerdefinierte Balken (Charts) nativ in MT5 zu erstellen, gibt es den Artikel mit der MQL5-Implementierung von Equal-Volume-, Equal-Range- und Renko-Balken. Natürlich können Sie die Balken mit Endzeit im Open Source Code markieren.
Die tätigkeitsbezogenen Balken lösen nicht alle Probleme, die Sie für Zeitbalken genannt haben. Sie haben zum Beispiel geschrieben:
Wenn Sie Balken mit gleichem Volumen, gleichem Bereich oder andere Tick-basierte benutzerdefinierte Balken erstellen, werden Sie einen solchen Balken ohnehin mit einem einzigen Etikett markieren, und es werden Informationen über den Höchstpreis über den gesamten Balken hinweg durchsickern (oder genauer gesagt, verschwimmen).
Die einzige Möglichkeit, dieses Problem zu lösen, besteht darin, "Balken" mit den spezifischen Funktionen (die Sie verwenden werden) im Hinterkopf zu erstellen. Wenn z.B. Hochs oder Tiefs die Hauptmerkmale sind, sollten Sie versuchen, einen Zickzack-Balken" mit zeitlich genau markierten Extermums zu erstellen.
Der Ansatz mit konstanten Zeitrahmen und insbesondere die Beschränkung auf M1 ist im Zusammenhang mit dem Datenleck im MT5 problematisch. Die Kennzeichnung von M1-Balken mit der Endzeit ist nicht viel besser als die mit der Anfangszeit, imho.
Für diejenigen, die daran interessiert sind, benutzerdefinierte Balken (Charts) nativ in MT5 zu erstellen, gibt es den Artikel mit der MQL5-Implementierung von Equal-Volume-, Equal-Range- und Renko-Balken. Natürlich können Sie die Balken mit Endzeit im Open Source Code markieren.
Die aktivitätsgesteuerten Balken zielen darauf ab, die in den Balken enthaltenen Informationen über statistische Eigenschaften zu verbessern, z. B. weniger Heteroskedastizität und verbesserte Normalität. Die von mir vorgeschlagene Lösung für das subtile Intra-Bar-Leakage besteht darin, die Balken mit ihren Endzeiten zu kennzeichnen, so dass alle Ereignisse, die innerhalb des Balkens auftreten, im Zeitstempel erfasst werden. Ein nützliches Beispiel ist die Verwendung von aus dem Zeitstempel abgeleiteten Merkmalen, wie z. B. Fourier-Transformationen, beim Training Ihres Modells. Wenn Sie die MetaTrader5-Konvention verwenden, bei der die Balken mit dem Beginn der Periode gekennzeichnet sind, dann geben Sie Ihrem Modell falsche Informationen. Diese Unterscheidung mag für einige Modelle keine große Rolle spielen, aber sie hat einen großen Einfluss auf Modelle, die die zyklische Natur der Märkte ausnutzen wollen. Ich hoffe, ich habe meine Absicht deutlich gemacht.
The activity-based bars don't solve all the problems you mentioned for time bars. For example, you wrote:
If you create bars of the same volume, range, or other tick-based custom bars, you'll be marking such a bar with a single label anyway, and information about the maximum price will leak (or more accurately, blur) across the entire bar.
The only way to solve this problem is to create "bars" with the specific features (you'll be using) in mind. For example, if highs or lows are the main characteristics, you should try to create a "zigzag bar" with extermums marked exactly in time.
The constant timeframe approach, and in particular the limitation to M1, is problematic in the context of the MT5 data leak. Marking M1 bars with the end time is imho not much better than with the start time.
For those interested in creating custom bars (charts) natively in MT5, there is the article with the MQL5 implementation of Equal Volume, Equal Range and Renko bars. Of course, you can mark the bars with end time in the open source code.
What do you mean when you state "If you create bars of the same volume, range, or other tick-based custom bars, you'll be marking such a bar with a single label anyway, and information about the maximum price will leak (or more accurately, blur) across the entire bar"?
Was meinen Sie, wenn Sie sagen: "Wenn Sie Balken mit dem gleichen Volumen, Bereich oder andere Tick-basierte benutzerdefinierte Balken erstellen, werden Sie einen solchen Balken sowieso mit einem einzigen Etikett markieren, und die Informationen über den Höchstpreis werden über den gesamten Balken auslaufen (oder genauer gesagt, verschwimmen)"?
Ich verstehe nicht, was unklar ist. Mein Satz ist eine direkte Antwort auf Ihren Satz, den ich im vorherigen Beitrag zitiert habe. Unabhängig davon, wie Sie den Balken formen, werden alle Eigenschaften des Balkens durch einen einzigen Zeitstempel zugeordnet, und die tatsächlichen "Ereignisse" der Eigenschaften stimmen nicht mit dieser Zeit überein. Zeit.
Jetzt verstehe ich die Bedeutung der Unschärfe.