English 日本語
preview
Blaupause für maschinelles Lernen (Teil 4): Die versteckte Schwachstelle in Ihrer ML-Pipeline – Gleichzeitigkeit der Kennzeichnungen

Blaupause für maschinelles Lernen (Teil 4): Die versteckte Schwachstelle in Ihrer ML-Pipeline – Gleichzeitigkeit der Kennzeichnungen

MetaTrader 5Handel |
31 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

Einführung

In Teil 2 dieser Serie haben wir die Triple-Barrier-Kennzeichen-Methode zur Erstellung von Kennzeichen für maschinelles Lernen aus Finanzzeitreihendaten untersucht. Wir haben erörtert, wie dieser Ansatz die pfadabhängige Natur der Rückgaben berücksichtigt und realistischere Trainingskennzeichen für Klassifizierungsmodelle liefert. Dieser Artikel setzt voraus, dass Sie mit dem Triple-Barrier-Kennzeichnung und den überwachten ML-Methoden in Scikit-Learn vertraut sind.

Die Implementierung der Triple-Barrier-Methode bringt jedoch eine kritische Herausforderung mit sich, die von den meisten Praktikern des maschinellen Lernens übersehen wird: die Gleichzeitigkeit der Beschriftungen. Wenn wir Schranken auf Finanzdaten anwenden, überschneiden sich die daraus resultierenden Kennzeichnungen oft zeitlich. Mehrere Beobachtungen können gleichzeitig „aktiv“ sein, d. h., ihre Informationsmengen überschneiden sich. Dadurch entstehen zeitliche Abhängigkeiten, die die Grundannahme der meisten Algorithmen für maschinelles Lernen verletzen: dass die Trainingsstichproben unabhängig und identisch verteilt sind (IID).

Dieser Verstoß hat schwerwiegende Folgen. Modelle, die auf der Grundlage gleichzeitiger Beobachtungen trainiert werden, weisen eine überhöhte Leistung in der Stichprobe auf, da sie dieselben Muster mehrfach lernen. Ihre Leistung außerhalb der Stichprobe verschlechtert sich jedoch, weil die tatsächliche Häufigkeit dieser Muster viel geringer ist, als das Modell annimmt. Das Ergebnis sind überangepasste Modelle, die im realen Handel versagen.

Dieser Artikel befasst sich mit dieser Herausforderung durch die Gewichtung von Stichproben – ein prinzipieller Ansatz zur Korrektur der Gleichzeitigkeit von Kennzeichen. Wir zeigen Ihnen, wie das geht:

  • Quantifizierung des Überschneidungsgrads zwischen Beobachtungen anhand von Gleichzeitigkeitsmetriken
  • Berechnung von Stichprobengewichten, die den einzigartigen Informationsgehalt jeder Beobachtung widerspiegeln
  • Implementierung dieser Gewichte in Scikit-Learn-Klassifikatoren zur Verbesserung der Modellgeneralisierung
  • Bewertung von Leistungsverbesserungen über mehrere Strategien hinweg unter Verwendung geeigneter Kreuzvalidierungstechniken


Beispielgewichte – Berücksichtigung der Gleichzeitigkeit

Das Problem der Gleichzeitigkeit

Die meisten nicht-finanziellen ML-Forscher können davon ausgehen, dass die Beobachtungen aus IID-Prozessen (IID – Independent and Identically Distributed) stammen. Sie können zum Beispiel Blutproben von einer großen Zahl von Patienten nehmen und deren Cholesterinspiegel messen. Natürlich werden verschiedene zugrunde liegende gemeinsame Faktoren den Mittelwert und die Standardabweichung der Cholesterinverteilung verschieben, aber die Stichproben sind immer noch unabhängig: Es gibt eine Beobachtung pro Proband. Angenommen, Sie nehmen diese Blutproben und jemand in Ihrem Labor verschüttet Blut aus jedem Röhrchen in die folgenden neun Röhrchen zu ihrer Rechten. Das heißt, Röhrchen 10 enthält Blut für Patient 10, aber auch Blut von den Patienten 1 bis 9. Röhrchen 11 enthält Blut für Patient 11, aber auch Blut von Patient 2 bis 10 usw. Nun müssen Sie die Merkmale bestimmen, die für einen hohen Cholesterinspiegel prädiktiv sind (Ernährung, Bewegung, Alter usw.), ohne den Cholesterinspiegel des einzelnen Patienten genau zu kennen. Das ist die gleiche Herausforderung, der wir uns bei ML im Finanzbereich gegenübersehen, mit dem zusätzlichen Handicap, dass das Verschüttungsmuster nicht deterministisch und unbekannt ist.

Modelle, die auf der Grundlage gleichzeitiger Beobachtungen trainiert werden, zeigen oft eine überhöhte Leistung innerhalb der Stichprobe (weil sie dieselben Muster mehrfach lernen), aber eine schlechte Leistung außerhalb der Stichprobe (weil die tatsächliche Häufigkeit dieser Muster viel geringer ist, als das Modell glaubt).

Die Stichprobengewichtung bietet eine elegante Lösung. Anstatt alle Beobachtungen gleich zu behandeln, gewichten wir sie danach, wie viele einzigartige Informationen jede Beobachtung enthält. Beobachtungen, die sich stark mit anderen überschneiden, werden niedriger gewichtet, während wirklich unabhängige Beobachtungen höher gewichtet werden.

Mathematische Grundlage

Die mathematische Grundlage für die Stichprobengewichte ergibt sich aus dem Konzept der „durchschnittlichen Einzigartigkeit“. Für jede Beobachtung müssen wir quantifizieren, wie viel ihres Informationsgehalts einzigartig ist und wie viel sie mit anderen gleichzeitigen Beobachtungen gemeinsam hat.

Der Ansatz von López de Prado berechnet dies anhand einer Matrix von Kennzeichenüberschneidungen. Für zwei beliebige Beobachtungen i und j bestimmen wir, wie stark sich ihre jeweiligen „Informationsmengen“ zeitlich überschneiden. Wenn die Beobachtung i in ihrem Kennzeichen Informationen von t₁ bis t₂ verwendet und die Beobachtung j Informationen von t₃ bis t₄, dann ist ihre Überschneidung die Schnittmenge dieser Zeitintervalle.

Das Verfahren umfasst drei Schritte:

  1. Gleichzeitigkeit zählen: Wir zählen für jeden Balken in unseren Daten, wie viele Ereignisse zu diesem Zeitpunkt „aktiv“ sind. Wenn drei Handelsgeschäfte gleichzeitig geöffnet sind, hat jeder Balken während dieses Zeitraums eine Gleichzeitigkeit von 3.
  2. Einzigartigkeit: Wir berechnen für jedes Ereignis den Kehrwert der Gleichzeitigkeit (1/Gleichzeitigkeit) bei jedem Balken während der Lebensdauer des Ereignisses und bilden dann den Durchschnitt dieser Werte. Wenn sich ein Ereignis über Balken mit Gleichzeitigkeit [3, 4, 3, 2] erstreckt, ist seine durchschnittliche Eindeutigkeit (1/3 + 1/4 + 1/3 + 1/2)/4 ≈ 0,354.
  3. Gewicht der Probe: Dieser Einzigartigkeitswert wird während des Modelltrainings zum Gewicht für diese Beobachtung.

Die durchschnittliche Eindeutigkeit der Beobachtung i wird als Mittelwert der Kehrwerte der Gleichzeitigkeit über alle Balken in ihrer Lebensdauer berechnet. Eine Beobachtung, die sich mit keiner anderen überschneidet, hat eine durchschnittliche Eindeutigkeit von 1,0 (maximales Gewicht), während eine Beobachtung, die sich mit vielen anderen vollständig überschneidet, auf 0,0 zugeht (minimales Gewicht).

Dadurch entsteht ein natürliches Gewichtungsschema, bei dem:

  • Unabhängige Beobachtungen erhalten volles Gewicht (1,0)
  • Sich teilweise überschneidende Beobachtungen erhalten ein proportional verringertes Gewicht (0,3-0,7).
  • Stark überlappende Beobachtungen erhalten minimales Gewicht (< 0,3)

Das Schöne an diesem Ansatz ist, dass er überlappende Beobachtungen nicht vollständig eliminiert, sondern lediglich ihren Einfluss proportional zu ihrer Redundanz reduziert. Auf diese Weise bleiben die Informationen erhalten, während die künstliche Verstärkung, die durch die zeitliche Überlappung entsteht, korrigiert wird.

Umsetzung: Berechnung der Gleichzeitigkeit

Die Implementierung von Stichprobengewichten erfordert eine sorgfältige Abwägung, was „Gleichzeitigkeit“ in unserem spezifischen Kontext bedeutet. Bei der Triple-Barrier-Methode sind zwei Beobachtungen gleichzeitig, wenn sich ihre jeweiligen Zeiträume (vom Eintritt bis zum Austritt) in irgendeiner Weise überschneiden.

Die erste Funktion berechnet, wie viele Ereignisse zu den einzelnen Zeitpunkten aktiv sind:

def num_concurrent_events(close_series_index, label_endtime, molecule):
    """
    Advances in Financial Machine Learning, Snippet 4.1, page 60.

    Estimating the Uniqueness of a Label

    This function uses close series prices and label endtime (when the first barrier is touched) 
    to compute the number of concurrent events per bar.

    :param close_series_index: (pd.Series) Close prices index
    :param label_endtime: (pd.Series) Label endtime series (t1 for triple barrier events)
    :param molecule: (an array) A set of datetime index values for processing
    :return: (pd.Series) Number concurrent labels for each datetime index
    """
    # Find events that span the period [molecule[0], molecule[1]]
    label_endtime = label_endtime.fillna(
        close_series_index[-1]
    )  # Unclosed events still must impact other weights
    label_endtime = label_endtime[
        label_endtime >= molecule[0]
    ]  # Events that end at or after molecule[0]
    # Events that start at or before t1[molecule].max()
    label_endtime = label_endtime.loc[: label_endtime[molecule].max()]

    # Count events spanning a bar
    nearest_index = close_series_index.searchsorted(
        pd.DatetimeIndex([label_endtime.index[0], label_endtime.max()])
    )
    count = pd.Series(0, index=close_series_index[nearest_index[0] : nearest_index[1] + 1])
    for t_in, t_out in label_endtime.items():
        count.loc[t_in:t_out] += 1
    return count.loc[molecule[0] : label_endtime[molecule].max()]

Was dieser Code tatsächlich bewirkt: Angenommen, es gibt drei Handelsgeschäfte:

  • Handelsgeschäft A: Öffnet um 10:00 Uhr, schließt um 10:30 Uhr
  • Handelsgeschäft B: Öffnet um 10:15 Uhr, schließt um 10:45 Uhr
  • Handelsgeschäft C: Öffnet um 10:50 Uhr, schließt um 11:00 Uhr

Um 10:20 Uhr sind sowohl Handelsgeschäft A als auch Handelsgeschäft B geöffnet, also count[10:20] = 2. Um 10:55 Uhr ist nur Handelsgeschäft C geöffnet, also count[10:55] = 1. Mit dieser Funktion wird die gesamte Zeitleiste erstellt.

Die Wrapper-Funktion parallelisiert diese Berechnung, indem sie mp_pandas_obj, ein Multiprocessing-Dienstprogramm (siehe multiprocess.py), für Ihren Datensatz verwendet:

def get_num_conc_events(events, close, num_threads=4, verbose=True):
    num_conc_events = mp_pandas_obj(
        num_concurrent_events,
        ("molecule", events.index),
        num_threads,
        close_series_index=close.index,
        label_endtime=events["t1"],
        verbose=verbose,
    )
    return num_conc_events

Berechnung der durchschnittlichen Eindeutigkeit

Sobald wir die Gleichzeitigkeit an jeder Bar kennen, berechnen wir die durchschnittliche Einzigartigkeit für jedes Ereignis:

def _get_average_uniqueness(label_endtime, num_conc_events, molecule):
    """
    Advances in Financial Machine Learning, Snippet 4.2, page 62.

    Estimating the Average Uniqueness of a Label

    This function uses close series prices and label endtime (when the first barrier is touched)
    to compute the number of concurrent events per bar.

    :param label_endtime: (pd.Series) Label endtime series (t1 for triple barrier events)
    :param num_conc_events: (pd.Series) Number of concurrent labels (output from num_concurrent_events function).
    :param molecule: (an array) A set of datetime index values for processing.
    :return: (pd.Series) Average uniqueness over event's lifespan.
    """
    wght = {}
    for t_in, t_out in label_endtime.loc[molecule].items():
        wght[t_in] = (1.0 / num_conc_events.loc[t_in:t_out]).mean()

    wght = pd.Series(wght)
    return wght

Die Orchestrator-Funktion führt alles zusammen:

def get_av_uniqueness_from_triple_barrier(
    triple_barrier_events, close_series, num_threads, num_conc_events=None, verbose=True
):
    """
    This function is the orchestrator to derive average sample uniqueness from a dataset labeled by the triple barrier
    method.

    :param triple_barrier_events: (pd.DataFrame) Events from labeling.get_events()
    :param close_series: (pd.Series) Close prices.
    :param num_threads: (int) The number of threads concurrently used by the function.
    :param num_conc_events: (pd.Series) Number concurrent labels for each datetime index
    :param verbose: (bool) Flag to report progress on asynch jobs
    :return: (pd.Series) Average uniqueness over event's lifespan for each index in triple_barrier_events
    """
    out = pd.DataFrame()

    # Create processing pipeline for num_conc_events
    def process_concurrent_events(ce):
        """Process concurrent events to ensure proper format and indexing."""
        ce = ce.loc[~ce.index.duplicated(keep="last")]
        ce = ce.reindex(close_series.index).fillna(0)
        if isinstance(ce, pd.Series):
            ce = ce.to_frame()
        return ce

    # Handle num_conc_events (whether provided or computed)
    if num_conc_events is None:
        num_conc_events = get_num_conc_events(
            triple_barrier_events, close_series, num_threads, verbose
        )
        processed_ce = process_concurrent_events(num_conc_events)
    else:
        # Ensure precomputed value matches expected format
        processed_ce = process_concurrent_events(num_conc_events.copy())

    # Verify index compatibility
    missing_in_close = processed_ce.index.difference(close_series.index)
    assert missing_in_close.empty, (
        f"num_conc_events contains {len(missing_in_close)} " "indices not in close_series"
    )

    out["tW"] = mp_pandas_obj(
        _get_average_uniqueness,
        ("molecule", triple_barrier_events.index),
        num_threads,
        label_endtime=triple_barrier_events["t1"],
        num_conc_events=processed_ce,
        verbose=verbose,
    )
    return out

Rückgabe-Attribution

Während die durchschnittliche Einzigartigkeit zeitliche Überschneidungen berücksichtigt, werden alle Ereignisse unabhängig von ihrer Größe gleich behandelt. Die Methode der Renditeattribution kombiniert die Einzigartigkeit mit den absoluten Renditen, die während der Laufzeit eines jeden Ereignisses erzielt werden:

def _apply_weight_by_return(label_endtime, num_conc_events, close_series, molecule):
    """
    Advances in Financial Machine Learning, Snippet 4.10, page 69.

    Determination of Sample Weight by Absolute Return Attribution

    Derives sample weights based on concurrency and return. Works on a set of
    datetime index values (molecule). This allows the program to parallelize the processing.

    :param label_endtime: (pd.Series) Label endtime series (t1 for triple barrier events)
    :param num_conc_events: (pd.Series) Number of concurrent labels (output from num_concurrent_events function).
    :param close_series: (pd.Series) Close prices
    :param molecule: (an array) A set of datetime index values for processing.
    :return: (pd.Series) Sample weights based on number return and concurrency for molecule
    """

    ret = np.log(close_series).diff()  # Log-returns, so that they are additive

    weights = {}
    for t_in, t_out in label_endtime.loc[molecule].items():
        # Weights depend on returns and label concurrency
        weights[t_in] = (ret.loc[t_in:t_out] / num_conc_events.loc[t_in:t_out]).sum()

    weights = pd.Series(weights)
    return weights.abs()

Die vollständige Umsetzung mit ordnungsgemäßer Datenverarbeitung:

def get_weights_by_return(
    triple_barrier_events,
    close_series,
    num_threads=4,
    num_conc_events=None,
    verbose=True,
):
    """
    Determination of Sample Weight by Absolute Return Attribution
    Modified to ensure compatibility with precomputed num_conc_events

    :param triple_barrier_events: (pd.DataFrame) Events from labeling.get_events()
    :param close_series: (pd.Series) Close prices
    :param num_threads: (int) Number of threads
    :param num_conc_events: (pd.Series) Precomputed concurrent events count
    :param verbose: (bool) Report progress
    :return: (pd.Series) Sample weights
    """
    # Validate input
    assert not triple_barrier_events.isnull().values.any(), "NaN values in events"
    assert not triple_barrier_events.index.isnull().any(), "NaN values in index"

    # Create processing pipeline for num_conc_events
    def process_concurrent_events(ce):
        """Process concurrent events to ensure proper format and indexing."""
        ce = ce.loc[~ce.index.duplicated(keep="last")]
        ce = ce.reindex(close_series.index).fillna(0)
        if isinstance(ce, pd.Series):
            ce = ce.to_frame()
        return ce

    # Handle num_conc_events (whether provided or computed)
    if num_conc_events is None:
        num_conc_events = mp_pandas_obj(
            num_concurrent_events,
            ("molecule", triple_barrier_events.index),
            num_threads,
            close_series_index=close_series.index,
            label_endtime=triple_barrier_events["t1"],
            verbose=verbose,
        )
        processed_ce = process_concurrent_events(num_conc_events)
    else:
        # Ensure precomputed value matches expected format
        processed_ce = process_concurrent_events(num_conc_events.copy())

        # Verify index compatibility
        missing_in_close = processed_ce.index.difference(close_series.index)
        assert missing_in_close.empty, (
            f"num_conc_events contains {len(missing_in_close)} " "indices not in close_series"
        )

    # Compute weights using processed concurrent events
    weights = mp_pandas_obj(
        _apply_weight_by_return,
        ("molecule", triple_barrier_events.index),
        num_threads,
        label_endtime=triple_barrier_events["t1"],
        num_conc_events=processed_ce,  # Use processed version
        close_series=close_series,
        verbose=verbose,
    )

    # Normalize weights
    weights *= weights.shape[0] / weights.sum()
    return weights

Abklingende Gewichtung durch die Zeit

Märkte sind anpassungsfähige Systeme, und während sie sich weiterentwickeln, verlieren ältere Beispiele gegenüber neueren an Bedeutung. Daher möchten wir, dass die oben berechneten Stichprobengewichte mit Zeitverfallsfaktoren multipliziert werden, um den jüngsten Beobachtungen mehr Gewicht zu verleihen. Beachten Sie, dass die Zeit nicht chronologisch zu verstehen ist. In dieser Implementierung erfolgt das Abklingen entsprechend der kumulativen Eindeutigkeit, da ein chronologisches Abklingen die Gewichte bei Vorhandensein redundanter Beobachtungen zu schnell reduzieren würde.

def get_weights_by_time_decay(
    triple_barrier_events,
    close_series,
    num_threads=4,
    last_weight=1,
    linear=True,
    av_uniqueness=None,
    verbose=True,
):
    """
    Advances in Financial Machine Learning, Snippet 4.11, page 70.
    Implementation of Time Decay Factors
    """
    assert (
        bool(triple_barrier_events.isnull().values.any()) is False
        and bool(triple_barrier_events.index.isnull().any()) is False
    ), "NaN values in triple_barrier_events, delete nans"

    # Get average uniqueness if not provided
    if av_uniqueness is None:
        av_uniqueness = get_av_uniqueness_from_triple_barrier(
            triple_barrier_events, close_series, num_threads, verbose=verbose
        )
    elif isinstance(av_uniqueness, pd.Series):
        av_uniqueness = av_uniqueness.to_frame()

    # Calculate cumulative time weights
    cum_time_weights = av_uniqueness["tW"].sort_index().cumsum()

    if linear:
        # Apply linear decay (your existing linear code is correct)
        if last_weight >= 0:
            slope = (1 - last_weight) / cum_time_weights.iloc[-1]
        else:
            slope = 1 / ((last_weight + 1) * cum_time_weights.iloc[-1])
        const = 1 - slope * cum_time_weights.iloc[-1]
        weights = const + slope * cum_time_weights
        weights[weights < 0] = 0
        return weights
    else:
        # Apply exponential decay
        if last_weight == 1:
            return pd.Series(1.0, index=cum_time_weights.index)

        elif cum_time_weights.iloc[-1] == 0:
            return pd.Series(1.0, index=cum_time_weights.index)

        # Calculate normalized position (0 = newest, 1 = oldest)
        elif last_weight > 0:
            # For last_weight > 0, use standard exponential decay
            normalized_position = (cum_time_weights - cum_time_weights.iloc[0]) / (
                cum_time_weights.iloc[-1] - cum_time_weights.iloc[0]
            )
            weights = last_weight**normalized_position
        elif last_weight < 0:
            # For last_weight < 0, implement cutoff (similar to linear case)
            # This is more complex for exponential - you might want to reconsider this case
            cutoff_threshold = abs(last_weight)
            normalized_position = (cum_time_weights - cum_time_weights.iloc[0]) / (
                cum_time_weights.iloc[-1] - cum_time_weights.iloc[0]
            )
            weights = (1 - cutoff_threshold)**normalized_position
            weights[weights < 0] = 0

        return weights

Zeit-Abkling-Faktoren

Abbildung 1. Zeit-Abkling-Faktoren (Linear vs. Exponential)

Klassengewichte

Zusätzlich zu den Stichprobengewichten ist es oft sinnvoll, die Klassen zu gewichten. Klassengewichte sind Gewichte, die unterrepräsentierte Kennzeichen korrigieren. Dies ist besonders kritisch bei Klassifizierungsproblemen, bei denen die wichtigsten Klassen selten vorkommen (King und Zeng [2001]). Nehmen wir zum Beispiel an, dass Sie eine Liquiditätskrise wie den Flash Crash vom 6. Mai 2010 vorhersagen möchten. Diese Ereignisse sind selten im Vergleich zu den Millionen von Beobachtungen, die dazwischen stattfinden. Wenn wir den Stichproben, die mit diesen seltenen Bezeichnungen assoziiert sind, keine höhere Gewichtung zuweisen, wird der ML-Algorithmus die Genauigkeit der häufigsten Bezeichnungen maximieren, und Blitzabstürze werden eher als Ausreißer denn als seltene Ereignisse betrachtet.

ML-Bibliotheken implementieren in der Regel Funktionen zur Handhabung von Klassengewichten. Zum Beispiel bestraft Scikit-Learn Fehler in Stichproben von class[j], j=1,...,J, mit der Gewichtung class_weight[j] anstelle von 1. Dementsprechend zwingen höhere Klassengewichte auf dem Kennzeichen j den Algorithmus dazu, eine höhere Genauigkeit auf j zu erreichen. Wenn sich die Klassengewichte nicht zu J addieren, ist dies gleichbedeutend mit einer Änderung des Regularisierungsparameters des Klassifikators.

Bei Finanzanwendungen sind die Standardkennzeichnungen eines Klassifizierungsalgorithmus {-1, 1}, wobei der Fall Null (oder neutrale Fall) durch eine Vorhersage mit einer Wahrscheinlichkeit von nur geringfügig über 0,5 und unter einem neutralen Schwellenwert impliziert wird. Es gibt keinen Grund, die Genauigkeit einer Klasse gegenüber der anderen zu bevorzugen, und daher ist eine gute Voreinstellung, class_weight='balanced' zuzuweisen. Durch diese Wahl werden die Beobachtungen neu gewichtet, um zu simulieren, dass alle Klassen mit gleicher Häufigkeit auftreten. Im Zusammenhang mit Bagging-Klassifikatoren sollten Sie das Argument class_weight='balanced_subsample' in Betracht ziehen, was bedeutet, dass class_weight='balanced' auf die in-bag-Bootstrap-Stichproben und nicht auf den gesamten Datensatz angewendet wird. Für alle Details ist es hilfreich, den Quellcode zu lesen, der class_weight in Scikit-Learn implementiert.

(López de Prado, 2018, S. 71)


Praktische Umsetzung

Behandlung von Nicht-IID-Daten bei der Bündelung

Die Verletzung der IID-Annahme in Finanzdaten macht das Standard-Bagging unwirksam, da es Bootstrap-Stichproben erzeugt, die von serieller Korrelation geplagt sind. Advances in Financial Machine Learning schlägt drei verschiedene Methoden vor, um diese grundlegende Herausforderung zu bewältigen, wobei die Stichprobengewichtung als Grundlage für alle drei Ansätze dient.

Methode 1: Einschränkung der Bootstrap-Stichprobengröße

Dies ist eine der Methoden, die wir in diesem Artikel anwenden. Es ist ein pragmatischer und rechnerisch effizienter Ansatz, der das Problem des Oversampling direkt angeht.

  • Kerngedanke: Verringern Sie den Umfang der einzelnen Bootstrap-Stichproben radikal. Indem wir eine geringere Anzahl von Beobachtungen ziehen, verringern wir statistisch gesehen die Wahrscheinlichkeit, dass mehrere stark korrelierte Datenpunkte in dieselbe Stichprobe aufgenommen werden.
  • Umsetzung: In sklearn.ensemble.BaggingClassifier wird dies erreicht, indem der Parameter max_samples auf einen Wert gesetzt wird, der deutlich unter 1,0 liegt (z. B. 0,5, 0,3 oder niedriger). Eine praktische Heuristik besteht darin, ihn auf die durchschnittliche Eindeutigkeit des Datensatzes zu setzen: max_samples=out['tW'].mean().
  • Mechanismus: max_samples steuert die absolute oder relative Anzahl der Stichproben, die aus X gezogen werden, um jeden Basisschätzer zu trainieren. Die Einstellung max_samples=0.3 bedeutet, dass jeder Klassifikator auf zufälligen 30 % des Originaldatensatzes trainiert wird, was die Diversität durch Begrenzung der Überschneidungen erzwingt.
  • Pro und Kontra:
    • Vorteile: Einfach zu implementieren, erfordert nur eine geänderte Zeile im Code.
    • Nachteile: Ein stumpfes Instrument; es reduziert Redundanzen, sucht aber nicht aktiv nach einzigartigen Beobachtungen. Dabei können auch wertvolle Daten verloren gehen.

Methode 2: Stichprobengewichtung für die In-Bag-Schätzung

Diese Methode korrigiert das Problem auf der Ebene der einzelnen Basisschätzer und nicht während der Stichprobenphase.

  • Kerngedanke: Verwenden Sie die Standard-Bootstrap-Stichprobe, aber verwenden Sie beim Training jedes Basisschätzers die Stichprobengewichte (Spalte tW), um das Modell zu zwingen, sich auf eindeutige Beobachtungen zu konzentrieren und redundante Beobachtungen zu ignorieren.
  • Umsetzung: Nach der Erstellung einer Bootstrap-Stichprobe mit Standardmethoden wird der sample_weight-Parameter für jeden Basisschätzer auf die im Voraus berechneten Gewichte der In-Bag-Beobachtungen gesetzt. Dazu muss der Basisschätzer Stichprobengewichte unterstützen (z. B. der DecisionTreeClassifier von Scikit-Learn).
  • Mechanismus: Die Verlustfunktion des Modells wird so modifiziert, dass Fehler bei hoch gewichteten (eindeutigen) Beobachtungen stärker bestraft werden als Fehler bei niedrig gewichteten (redundanten) Beobachtungen.
  • Pro und Kontra:
    • Vorteile: Nutzt die bereits berechneten Stichprobengewichte; kann mit anderen Methoden kombiniert werden.
    • Nachteile: Sie verhindert nicht, dass korrelierte Daten in die Bootstrap-Stichprobe gelangen; sie mildert lediglich ihren Einfluss im Nachhinein ab.

Methode 3: Sequentielles Bootstrapping

Dies ist die rigorose, gezielte Lösung, die López de Prado vorschreibt. Wir werden in unserem nächsten Artikel näher darauf eingehen.

  • Kerngedanke: De Standard-Zufallsstichprobe wird vollständig durch einen intelligenten, sequenziellen Algorithmus ersetzt, der die Eindeutigkeit innerhalb jeder Bootstrap-Stichprobe aktiv erzwingt.
  • Umsetzung: Eine nutzerdefinierte Stichprobenroutine, die eine Beobachtung nach der anderen zieht. Eine neue Beobachtung wird der aktuellen Stichprobe nur dann hinzugefügt, wenn sie im Vergleich zu allen bereits in der Stichprobe enthaltenen Beobachtungen hinreichend „einzigartig“ (überschneidungsfrei) ist. Dies erfordert eine vollständige kundenspezifische Implementierung der Resampling-Logik.
  • Mechanismus: Es verwendet direkt die Gleichzeitigkeitsmatrix oder die Kennzeichen-Überlappung, um jede Kandidatenbeobachtung für die Bootstrap-Stichprobe bedingt zu akzeptieren oder abzulehnen.
  • Pro und Kontra:
    • Vorteile: Theoretisch fundiert; konstruiert aktiv möglichst vielfältige und unkorrelierte Stichproben.
    • Nachteile: Sie sind rechenintensiv und komplex in der Umsetzung.

Synthese und unser Ansatz

Diese drei Methoden bilden eine Hierarchie der Ausgereiftheit:

  • Methode 1 (Beschränkung des Stichprobenumfangs) dient als einfache Präventivmaßnahme in der Stichprobenphase.
  • Methode 2 (In-Bag-Gewichtung) dient als Korrekturmaßnahme während der Modelltraining.
  • Methode 3 (Sequentielles Bootstrapping) ist die umfassende, präventive Lösung, die die Ursache bereits in der Probenahmephase beseitigt.

Die Methoden 1 und 2 ergänzen sich und können für ein Konzept der Tiefenverteidigung kombiniert werden: Methode 1 verringert die Wahrscheinlichkeit, dass sich überschneidende Beobachtungen in jede Bootstrap-Stichprobe gezogen werden, während Methode 2 sicherstellt, dass alle überschneidenden Beobachtungen, die es in die Stichprobe schaffen, während des Trainings einen entsprechend reduzierten Einfluss erhalten.

Für die Zwecke dieses Artikels verwenden wir sowohl Methode 1 als auch Methode 2, da sie sich gegenseitig ergänzen, einfach zu handhaben und nachweislich wirksam sind. Durch die Einstellung max_samples=out['tW'].mean() (Methode 1) und die Übergabe von sample_weight=out['tW'] an die fit()-Methode des Klassifikators (Methode 2) schaffen wir eine robuste Pipeline, die Redundanz in unseren Bootstrap-Stichproben verhindert und korrigiert.

Die anspruchsvollere Methode 3, das Sequential Bootstrapping, wird Gegenstand eines eigenen Folgeartikels sein, in dem wir die für ihre Implementierung erforderliche nutzerdefinierte Stichprobenklasse erstellen werden.

Verwendung von Stichprobengewichten beim Modelltraining

Nun zum entscheidenden Teil: Wie verwenden wir diese Gewichte tatsächlich in unserer Pipeline für maschinelles Lernen? Machen wir einen kleinen Abstecher zum Thema Kreuzvalidierung und warum die Standardanwendungen im Finanzwesen versagen. Dies ist notwendig, da dies die Technik ist, mit der wir die Wirksamkeit unserer Gewichtungsmethoden bewerten.

Konzeptionelle Grundlage für die finanzielle Kreuzvalidierung

Die standardmäßige k-fache Kreuzvalidierung (CV) beruht auf der Annahme, dass die Datenpunkte unabhängig und identisch verteilt sind (IID). Finanzielle Zeitreihendaten verletzen diese Grundannahme aufgrund von serieller Korrelation, zeitlichen Abhängigkeiten und Strukturbrüchen. Bei der Verwendung von Standardmethoden besteht die Gefahr von Datenlecks, bei denen Informationen aus der Zukunft unbeabsichtigt das Training eines Modells auf der Grundlage vergangener Daten beeinflussen, was zu einer Überanpassung und unzuverlässigen Leistungsschätzungen führt.

Abbildung 2 veranschaulicht die k Trainings-/Testsplits, die durch einen k-fachen CV durchgeführt werden, wobei k = 5 ist. In diesem Schema:

  1. Der Datensatz wird in k Teilmengen aufgeteilt.
  2. Für i = 1,...,k

K-fache Kreuzvalidierung

Abbildung 2. Zug/Test-Splits in einem 5-fachen CV-Schema

  • (a) Der ML-Algorithmus wird auf allen Teilmengen außer i trainiert.
  • (b) Der angepasste ML-Algorithmus wird an i getestet.

Um dieses Problem zu lösen, führt López de Prado zwei wesentliche Änderungen am standardmäßigen k-fachen CV ein:

  • Entschlacken: Bei dieser Methode werden alle Beobachtungen aus der Trainingsmenge entfernt, deren Bezeichnungen sich zeitlich mit denen der Testmenge überschneiden. Dadurch wird verhindert, dass das Modell Kenntnis von zukünftigen Zeiträumen hat, die es vorhersagen soll.
  • Embargo: Eine zusätzliche Sicherheitsmaßnahme, bei der ein kleiner Teil der Daten unmittelbar nach der Testphase aus dem Trainingssatz entfernt wird, um Lecks durch serielle Korrelation zu vermeiden.

Bereinigung von Überschneidungen im Trainingsset

Abbildung 3. Bereinigung von Überschneidungen in der Trainingsmenge

Embargo für Zugbeobachtungen nach dem Test

Abbildung 4. Sperrung von Zugbeobachtungen nach dem Test

Wir müssen überlappende Trainingsbeobachtungen bereinigen und sperren, wenn wir eine Aufteilung zwischen Training und Test vornehmen, sei es für die Anpassung von Hyperparametern, Backtesting oder Leistungsbewertung. Der folgende Code erweitert die KFold-Klasse von Scikit-Learn, um die Möglichkeit des Durchsickerns von Testinformationen in die Trainingsmenge zu berücksichtigen:

from typing import Callable

import numpy as np
import pandas as pd
from sklearn.base import ClassifierMixin
from sklearn.metrics import accuracy_score, f1_score, log_loss
from sklearn.model_selection import BaseCrossValidator
from sklearn.model_selection._split import _BaseKFold

from ..cross_validation.scoring import probability_weighted_accuracy

class PurgedKFold(_BaseKFold):
    """
    Extend KFold class to work with labels that span intervals

    The train is purged of observations overlapping test-label intervals
    Test set is assumed contiguous (shuffle=False), w/o training samples in between

    :param n_splits: (int) The number of splits. Default to 3
    :param t1: (pd.Series) The information range on which each record is constructed from
        *t1.index*: Time when the information extraction started.
        *t1.value*: Time when the information extraction ended.
    :param pct_embargo: (float) Percent that determines the embargo size.
    """

    def __init__(self, n_splits=3, t1=None, pct_embargo=0.0):
        if not isinstance(t1, pd.Series):
            raise ValueError("Label Through Dates must be a pd.Series")

        super().__init__(n_splits, shuffle=False, random_state=None)

        self.t1 = t1
        self.pct_embargo = pct_embargo

    def split(self, X, y=None, groups=None):
        """
        The main method to call for the PurgedKFold class

        :param X: (pd.DataFrame) Samples dataset that is to be split
        :param y: (pd.Series) Sample labels series
        :param groups: (array-like), with shape (n_samples,), optional
            Group labels for the samples used while splitting the dataset into
            train/test set.
        :return: (tuple) [train list of sample indices, and test list of sample indices]
        """

        if (X.index == self.t1.index).sum() != len(self.t1):
            raise ValueError("X and ThruDateValues must have the same index")

        indices = np.arange(X.shape[0])
        mbrg = int(X.shape[0] * self.pct_embargo)
        test_starts = [(i[0], i[-1] + 1) for i in np.array_split(np.arange(len(X)), self.n_splits)]

        for i, j in test_starts:
            t0 = self.t1.index[i]  # start of test set
            test_indices = indices[i:j]
            max_t1_idx = self.t1.index.searchsorted(self.t1[test_indices].max())
            train_indices = self.t1.index.searchsorted(self.t1[self.t1 <= t0].index)
            if max_t1_idx < X.shape[0]:  # right train (with embargo)
                train_indices = np.concatenate((train_indices, indices[max_t1_idx + mbrg :]))
            yield train_indices, test_indices


Methodik der Bewertung

Scoring-Methoden

Beim maschinellen Lernen im Finanzbereich ist die Wahl der richtigen Bewertungsmetriken entscheidend für die Beurteilung der Modellleistung. Standardmetriken wie die Genauigkeit können im Finanzbereich irreführend sein, insbesondere wenn es um unausgewogene Datensätze oder Meta-Kennzeichen-Anwendungen geht. Betrachten wir die wichtigsten Kennzahlen, die zur Bewertung von ML-Finanzmodellen verwendet werden.

Accuracy

Die Genauigkeit (Accuracy) misst die allgemeine Korrektheit der Vorhersagen, indem sie den Anteil der korrekt klassifizierten Beobachtungen berechnet:

Accuracy = (TP + TN) / (TP + TN + FP + FN)

wobei:

  • TP = Richtig Positiv
  • TN = Richtig Negativ
  • FP = Falsch Positiv
  • FN = Falsch Negativ

Während die Genauigkeit einen allgemeinen Überblick über die Leistung bietet, kann sie bei Finanzanwendungen, bei denen die Klassenverteilung oft unausgewogen ist, trügerisch sein.

Präzision

Die Präzision quantifiziert die Zuverlässigkeit positiver Vorhersagen, indem sie misst, welcher Anteil der vorhergesagten Positivmeldungen tatsächlich korrekt ist:

Precision = TP / (TP + FP)

Eine hohe Genauigkeit bedeutet, dass das Modell, wenn es ein positives Ergebnis vorhersagt, wahrscheinlich richtig liegt – eine wertvolle Eigenschaft für Handelssysteme, bei denen falsche Signale kostspielig sein können.

Recall

Recall (oder Sensitivität) misst, wie gut das Modell tatsächlich positive Fälle identifiziert:

Recall = TP / (TP + FN)

Eine hohe Trefferquote bedeutet, dass das Modell die meisten der verfügbaren Gelegenheiten erfasst, was wichtig ist, wenn das Verpassen eines gewinnbringenden Handels kostspieliger ist als das gelegentliche Eingehen einer suboptimalen Position.

F1 Ergebnis

Der F1-Score behebt die Einschränkungen der Genauigkeit (accuracy) in unausgewogenen Szenarien, indem er Präzision und Recall in einer einzigen Metrik kombiniert:

F1 = 2 × (Precision × Recall) / (Precision + Recall)

Diese Metrik ist besonders wertvoll bei Meta-Kennzeichen-Anwendungen, bei denen die Zahl der negativen Fälle (Etikett „0“) die der positiven Fälle (Etikett „1“) oft deutlich übersteigt. In solchen Situationen würde ein naiver Klassifikator, der immer die Mehrheitsklasse vorhersagt, zwar eine hohe Genauigkeit erreichen, aber keine echten Chancen erkennen.

Wichtige Überlegung: Der F1-Score wird in bestimmten entarteten Fällen undefiniert:

  • Wenn alle beobachteten Werte negativ sind (keine positiven Werte zum Abrufen)
  • Wenn alle vorhergesagten Werte negativ sind (keine positiven Vorhersagen zur Bewertung der Genauigkeit)

Scikit-Learn behandelt diese Randfälle, indem es einen F1-Score von 0 zurückgibt und eine UndefinedMetricWarning ausgibt.

Degenerierte Fälle in der binären Klassifikation verstehen

Die nachstehende Tabelle fasst zusammen, wie sich die verschiedenen Metriken in Extremszenarien verhalten:

Bedingung
Kollabieren
Genauigkeit
Präzision
Recall
F1
Alle beobachteten Einsen
TN=FP=0
=Recall
1 [0,1]
[0,1]
Alle beobachteten Nullen
TP=FN=0
[0,1]
0 Undefiniert
Undefiniert
Alle vorhergesagten Einsen
TN=FN=0
=Präzision
[0,1]
1 [0,1]
Alle vorhergesagten Nullen
TP=FP=0
[0,1]
Undefiniert
0
Undefiniert

Diese Grenzfälle machen deutlich, warum es irreführend sein kann, sich nur auf die Genauigkeit zu verlassen, und warum der F1-Score und der Log-Loss eine robustere Bewertung in praktischen Finanzanwendungen bieten.

Log-Verlust

Der Log-Verlust (oder Kreuzentropie-Verlust) bietet eine differenziertere Bewertung als die Genauigkeit, da er die Zuverlässigkeit der Vorhersage berücksichtigt:

Log-Verlust-Formel

wobei:

  • pn,k = Wahrscheinlichkeit für die Vorhersage n der Klasse k
  • Y = 1-von-K binäre Indikatormatrix
  • yn,k = 1, wenn Beobachtung n das Kennzeichen k hat, sonst 0

In Finanzanwendungen verwenden wir in der Regel den negativen Log-Verlust, um eine intuitive Bewertung zu erhalten (höhere Werte sind besser). Diese Kennzahl ist besonders wichtig, weil:

  1. Sie trägt zur Vorhersagesicherheit bei: Eine falsche Vorhersage mit hohem Vertrauen wird härter bestraft als eine mit niedrigem Vertrauen
  2. Sie steht im Einklang mit PnL-Überlegungen: Kombiniert mit einer auf den Erträgen basierenden Gewichtung der Stichprobe ergibt sich ein Näherungswert für die Auswirkungen des Klassifikators auf Gewinn und Verlust.
  3. Sie spiegelt die Größenordnung der Position wider: Ein höheres Vertrauen in die Vorhersagen führt in der Regel zu größeren Positionsgrößen bei Handelsstrategien.

Im Gegensatz zur Genauigkeit, bei der alle Fehler unabhängig von der Konfidenz gleich behandelt werden, bietet der log-loss eine realistischere Bewertung der potenziellen Auswirkungen eines Klassifikators auf die Handelsleistung.

Angenommen, ein Klassifikator sagt zwei Einsen voraus, wobei die wahren Kennzeichnungen 1 und 0 sind. Die erste Vorhersage ist ein Treffer und die zweite Vorhersage ein Fehlschlag, sodass die Genauigkeit 50 % beträgt. In Abbildung 5 ist der Kreuzentropieverlust dargestellt, wenn diese Vorhersagen aus Wahrscheinlichkeiten im Bereich [0,5, 0,9] stammen. Man kann feststellen, dass auf der rechten Seite der Abbildung der Log-Verlust aufgrund von Fehlversuchen mit hoher Wahrscheinlichkeit groß ist, obwohl die Genauigkeit in allen Fällen 50 % beträgt.

Logarithmischer Verlust in Abhängigkeit von den vorhergesagten Wahrscheinlichkeiten für Erfolg und Misserfolg

Abbildung 5. Logarithmischer Verlust als Funktion der vorhergesagten Wahrscheinlichkeiten für Erfolg und Misserfolg

Wahrscheinlichkeitsgewichtete Genauigkeit (PWA)

Dies erweitert die herkömmliche Genauigkeit, indem korrekte Vorhersagen nach dem Vertrauensniveau gewichtet werden. Eine korrekte Vorhersage mit 90%iger Sicherheit trägt mehr bei als eine mit 51%iger Sicherheit. Dies spiegelt den realen Handel besser wider, bei dem wir die Größe der Positionen auf der Grundlage des Vertrauens in die Vorhersage festlegen. PWA bestraft schlechte Vorhersagen, die mit hohem Vertrauen gemacht wurden, härter als die Genauigkeit, aber weniger hart als der Log-Verlust.

Formel für die wahrscheinlichkeitsgewichtete Genauigkeit

wobei pn = max{pn,k} and yn eine Indikatorfunktion ist, yn ∈ {0, 1}, wobei yn = 1 ist, wenn die Vorhersage richtig war, und yn = 0 andernfalls.

Dies ist gleichbedeutend mit der Standardgenauigkeit, wenn der Klassifikator bei jeder Vorhersage absolute Überzeugung hat(pn = 1 für alle n) (Prado, 2020, p.83). Die Basisanpassung pn – 1/K sorgt dafür, dass zufälliges Raten (Wahrscheinlichkeit = 1/K) ein Gewicht von Null erhält.

import numpy as np
import pandas as pd
from sklearn.utils.multiclass import unique_labels

def probability_weighted_accuracy(y_true, y_prob, sample_weight=None, labels=None, eps=1e-15):
    """
    Calculates the Probability-Weighted Accuracy (PWA) score.

    PWA is a confidence-weighted accuracy that penalizes high-confidence
    mistakes more severely. This version is compatible with sklearn
    conventions: it accepts a `labels` argument to fix the class order,
    applies probability clipping, and supports sample weights.

    Args:
        y_true (array-like): True class labels, shape (n_samples,).
        y_prob (array-like or DataFrame): Predicted probabilities,
            shape (n_samples, n_classes). If DataFrame, columns must be
            class labels.
        sample_weight (array-like, optional): Per-sample weights.
        labels (array-like, optional): List of all expected class labels
            (in the order corresponding to columns of y_prob).
        eps (float): Small value to clip probabilities into [eps, 1 - eps].

    Returns:
        float: PWA score between 0 and 1.
    """
    # 1) Convert inputs to numpy arrays (or reorder DataFrame)
    y_true = np.asarray(y_true)
    if isinstance(y_prob, pd.DataFrame):
        # If labels given, reorder columns; otherwise infer column order
        cols = labels if labels is not None else y_prob.columns.tolist()
        y_prob = y_prob[cols].to_numpy()
    else:
        y_prob = np.asarray(y_prob)

    # 2) Clip probabilities to avoid zeros or ones
    y_prob = np.clip(y_prob, eps, 1 - eps)

    # 3) Determine class list and validate
    if labels is not None:
        classes = np.asarray(labels)
    else:
        # Infer classes from y_true (sorted)
        classes = unique_labels(y_true)
    n_classes = classes.shape[0]

    # 4) Handle binary case where y_prob might be 1D
    if y_prob.ndim == 1:
        # Interpret as probability of class classes[1]
        y_prob = np.vstack([1 - y_prob, y_prob]).T
        n_classes = 2

    # 5) Shape checks
    if y_prob.ndim != 2 or y_prob.shape[1] != n_classes:
        raise ValueError(
            f"y_prob must be shape (n_samples, n_classes={n_classes}), " f"but got {y_prob.shape}"
        )

    if not np.all(np.isin(y_true, classes)):
        missing = set(y_true) - set(classes)
        raise ValueError(f"y_true contains labels not in `labels`: {missing}")

    # 6) Prepare sample weights
    if sample_weight is None:
        sample_weight = np.ones_like(y_true, dtype=float)
    else:
        sample_weight = np.asarray(sample_weight, dtype=float)
        if sample_weight.shape[0] != y_true.shape[0]:
            raise ValueError("sample_weight must have same length as y_true")

    # 7) Predicted class index and its probability
    pred_idx = np.argmax(y_prob, axis=1)
    p_n = y_prob[np.arange(len(y_true)), pred_idx]

    # 8) Correctness indicator y_n ∈ {0,1}
    #    Map y_true labels to indices in `classes`
    label_to_index = {c: i for i, c in enumerate(classes)}
    true_idx = np.vectorize(label_to_index.get)(y_true)
    y_n = (pred_idx == true_idx).astype(int)

    # 9) Confidence weights: p_n – (1/K)
    baseline = 1.0 / n_classes
    conf_w = p_n - baseline

    # 10) Compute numerator and denominator with sample weights
    numerator = np.sum(sample_weight * y_n * conf_w)
    denominator = np.sum(sample_weight * conf_w)

    # 11) Edge case: no confidence (all p_n == 1/K)
    if np.isclose(denominator, 0.0):
        return 0.5  # random-guess baseline

    # 12) Final PWA score
    return numerator / denominator


Experimenteller Aufbau

Daten und Handelsstrategien

Wir evaluieren Stichprobengewichtungstechniken auf EUR/USD M5-Balken, die den Zeitraum vom 2018-01-01 bis 2022-12-31 abdecken. Es wurden zwei verschiedene Meta-Kennzeichen-Strategien getestet, die als Volatilitätsziel die 20-tägige exponentiell gewichtete gleitende Standardabweichung verwendeten:

Meta-gekennzeichnete Bollinger Bands Strategie

Diese Strategie verwendet Bollinger-Bänder, um primäre Handelssignale zu generieren, die dann durch ein Meta-Kennzeichen-Modell gefiltert werden. Das primäre Modell generiert Signale auf der Grundlage von Preisinteraktionen mit dem oberen und unteren Band, während das Metamodell die Wahrscheinlichkeit vorhersagt, dass das Handeln auf jedes Signal profitabel sein wird.

Triple-Barrier-Konfiguration:

  • Profit Target: 1
  • Stop Loss: 2
  • Time Barrier: 4 Stunden
  • Mindestschwelle für das Ergebnis: 0.0

Meta-markierte MA_20_50 Kreuzungsstrategie

Dieser klassische Trendfolgeansatz verwendet die Kreuzung von gleitenden Durchschnitten der 20- und 50-Periode als primäre Signale. Das Meta-Kennzeichen-Modell lernt, diese Signale zu filtern, indem es vorhersagt, welche Überkreuzungen wahrscheinlich zu profitablen Handelsgeschäften führen werden.

Triple-Barrier-Konfiguration:

  • Profit Target: 0
  • Stop Loss: 2
  • Time Barrier: 1 Tag
  • Mindestschwelle für das Ergebnis: 0.0

Bewertungsrahmen

Für jede Strategie wurde ein Random Forest-Klassifikator mit und ohne Stichprobengewichtung trainiert, wobei eine bereinigte K-fache Kreuzvalidierung verwendet wurde, um Datenlecks zu vermeiden.

Mit der nachstehenden Funktion werden alle oben genannten Leistungskennzahlen berechnet:

def ml_cross_val_scores_all(
    classifier: ClassifierMixin,
    X: pd.DataFrame,
    y: pd.Series,
    cv_gen: BaseCrossValidator,
    sample_weight_train: np.ndarray = None,
    sample_weight_score: np.ndarray = None,
):
    # pylint: disable=invalid-name
    # pylint: disable=comparison-with-callable
    """
    Advances in Financial Machine Learning, Snippet 7.4, page 110.

    Using the PurgedKFold Class.

    Function to run a cross-validation evaluation of the classifier using sample weights and a custom CV generator.
    Scores are computed using accuracy_score, probability_weighted_accuracy, log_loss and f1_score.

    Note: This function is different to the book in that it requires the user to pass through a CV object. The book
    will accept a None value as a default and then resort to using PurgedCV, this also meant that extra arguments had to
    be passed to the function. To correct this we have removed the default and require the user to pass a CV object to
    the function.

    Example:

    .. code-block:: python

        cv_gen = PurgedKFold(n_splits=n_splits, t1=t1, pct_embargo=pct_embargo)
        scores_array = ml_cross_val_scores_all(classifier, X, y, cv_gen, sample_weight_train=sample_train,
                                               sample_weight_score=sample_score, scoring=accuracy_score)

    :param classifier: (BaseEstimator) A scikit-learn Classifier object instance.
    :param X: (pd.DataFrame) The dataset of records to evaluate.
    :param y: (pd.Series) The labels corresponding to the X dataset.
    :param cv_gen: (BaseCrossValidator) Cross Validation generator object instance.
    :param sample_weight_train: (np.array) Sample weights used to train the model for each record in the dataset.
    :param sample_weight_score: (np.array) Sample weights used to evaluate the model quality.
    :return: (dict) The computed scores.
    """
    scoring_methods = [accuracy_score, probability_weighted_accuracy, log_loss, f1_score]
    ret_scores = {
        scoring.__name__ if scoring != log_loss else "neg_log_loss": []
        for scoring in scoring_methods
    }

    # If no sample_weight then broadcast a value of 1 to all samples (full weight).
    if sample_weight_train is None:
        sample_weight_train = np.ones((X.shape[0],))

    if sample_weight_score is None:
        sample_weight_score = np.ones((X.shape[0],))

    # Score model on KFolds
    for train, test in cv_gen.split(X=X, y=y):
        fit = classifier.fit(
            X=X.iloc[train, :],
            y=y.iloc[train],
            sample_weight=sample_weight_train[train],
        )
        prob = fit.predict_proba(X.iloc[test, :])
        pred = fit.predict(X.iloc[test, :])
        for method, scoring in zip(ret_scores.keys(), scoring_methods):
            if scoring in (accuracy_score, f1_score):
                score = scoring(y.iloc[test], pred, sample_weight=sample_weight_score[test])
            else:
                score = scoring(
                    y.iloc[test],
                    prob,
                    sample_weight=sample_weight_score[test],
                    labels=classifier.classes_,
                )
                if method == "neg_log_loss":
                    score *= -1
            ret_scores[method].append(score)

    for k, v in ret_scores.items():
        ret_scores[k] = np.array(v)

    return ret_scores


Experimentelle Ergebnisse

Strategie des Leistungsvergleichs

Wir haben zwei verschiedene Strategien mit und ohne Stichprobengewichtung unter Verwendung des 10-fachen CV bewertet:

  • Meta-gekennzeichnete Bollinger-Bänder: Traditionelle Strategie der Rückkehr zum Mittelwert
  • Meta-markiertes MA_20_50 Kreuzen: Klassisches Trendfolgemodell

Leistung der Strategie der Bollinger-Bänder

Kennzahl Ungewichtet Gewichtung der Einzigartigkeit Rendite Gewichtung
Accuracy 0.564 ± 0.044 0.584 ± 0.040 0.693 ± 0.020
PWA 0.563 ± 0.054 0.593 ± 0.044 0.697 ± 0.019
Negativer Log-Verlust -0.688 ± 0.008 -0.682 ± 0.007 -0.631 ± 0.023
Präzision 0.650 ± 0.019 0.658 ± 0.024 0.000 ± 0.000
Recall 0.616 ± 0.167 0.683 ± 0.145 0.000 ± 0.000
F1 Ergebnis 0.622 ± 0.091 0.663 ± 0.073 0.000 ± 0.000

Leistung der MA 20-50 Kreuz-Strategie

Kennzahl Ungewichtet Gewichtung der Einzigartigkeit Rendite Gewichtung
Accuracy 0.589 ± 0.073 0.634 ± 0.068 0.473 ± 0.011
PWA 0.672 ± 0.101 0.740 ± 0.080 0.473 ± 0.011
Negativer Log-Verlust -0.650 ± 0.037 -0.625 ± 0.036 -0.826 ± 0.018
Präzision 0.298 ± 0.026 0.296 ± 0.029 0.473 ± 0.011
Recall 0.588 ± 0.125 0.530 ± 0.108 1.000 ± 0.000
F1 Ergebnis 0.388 ± 0.015 0.372 ± 0.018 0.642 ± 0.010

Wichtige Erkenntnisse aus den Ergebnissen

Die experimentellen Ergebnisse zeigen ein differenziertes und strategieabhängiges Bild davon, wie sich die Stichprobengewichtung auf die Modellleistung auswirkt. Die Wirksamkeit der einzelnen Gewichtungsmethoden ist je nach der zugrunde liegenden Handelslogik sehr unterschiedlich.

Gewichtung der Einzigartigkeit: Ein robuster Standard für Meta-Kennzeichen

Die Methode der Einzigartigkeitsgewichtung zeigte konsistente und aussagekräftige Verbesserungen bei beiden Strategien und etablierte sich als robuste Standardwahl.

  • Strategie der Bollinger Bänder: Die Gewichtung der Einzigartigkeit sorgte für einen abgerundeten Leistungsschub. Die Genauigkeit verbesserte sich von 56,4 % auf 58,4 %, aber noch wichtiger ist, dass der F1-Wert um 6,7 % (von 0,622 auf 0,663) stieg. Das bedeutet, dass das Modell falsche Signale besser herausfiltert und gleichzeitig echte Chancen erfasst. Die Verbesserung der wahrscheinlichkeitsgewichteten Genauigkeit bestätigt außerdem, dass das Vertrauen des Modells besser auf einzigartige, nicht redundante Beispiele kalibriert wurde.
  • MA Kreuz-Strategie: Die Vorteile waren sogar noch ausgeprägter. Die Einzigartigkeitsgewichtung führte zu einem Anstieg der Genauigkeit um 7,5 % (58,9 % auf 63,4 %) und zu einer bemerkenswerten Steigerung der wahrscheinlichkeitsgewichteten Genauigkeit um 10,2 % (67,2 % auf 74,0 %). Während der F1-Score einen leichten Rückgang verzeichnete, sind die dramatischen Zuwächse bei den vertrauensgewichteten Metriken entscheidend für ein Meta-Kennzeichen-Modell, bei dem das Ziel darin besteht, die Positionen auf der Grundlage der Erfolgswahrscheinlichkeit eines Primärsignals zu bestimmen.

Gewichtung der Renditeattribution: Ein abschreckendes Beispiel

In krassem Gegensatz zum Erfolg der Einzigartigkeitsgewichtung führte die Methode der Renditezuweisung zu extremen und unerwünschten Ergebnissen, was einen entscheidenden Fallstrick aufzeigt.

  • Strategie der Bollinger Bänder: Das Modell zerfiel in einen trivialen Klassifikator. Die Präzision, die Wiederauffindbarkeit und die F1-Punktzahl fielen alle auf Null, während die Genauigkeit paradoxerweise auf 69,3 % anstieg. Dieses Muster ist ein klassisches Zeichen für ein Modell, das gelernt hat, immer die Mehrheitsklasse vorherzusagen (wahrscheinlich „0“ oder „Nimm den Handel nicht an“). Es passte sich zu stark an die Größe der vergangenen Renditen an und verlor dadurch seine Vorhersagekraft für die Klassifizierungsaufgabe vollständig.
  • MA Kreuz-Strategie: Ein ähnlicher, wenn auch etwas anderer Fehler trat auf. Das Modell erreichte einen perfekten Recall von 1,0 und einen F1-Wert von 64,2 %, aber nur eine Genauigkeit von 47,3 %. Dies deutet darauf hin, dass das Modell gelernt hat, die positive Klasse („1“ oder „nimm den Handel an“) fast wahllos vorherzusagen, wobei es alle echten Positiven erfasst, aber auch eine große Anzahl von Falsch-Positiven erzeugt. Dieses Verhalten ist für ein Live-Handelssystem unhaltbar.

Das Scheitern der Rückgabe-Attribution unterstreicht, dass Gleichzeitigkeit und Rückgabegröße unterschiedliche Konzepte sind. Die Gewichtung nach Renditen allein verfälscht das Lernsignal und führt dazu, dass das Modell den Gewinnen der Vergangenheit hinterherläuft, anstatt aus einmaligen Informationsereignissen verallgemeinerbare Muster zu lernen. Der Rückgabewert zeigt seinen Wert an, wenn wir die Richtungsabhängigkeit vorhersagen (labels {1, -1}). Vergessen Sie nicht, dass das Meta-Kennzeichen darauf abzielt, unser primäres Modell zu verbessern, indem es falsche Vorhersagen herausfiltert und ein unabhängiges Modell für die Wettgrößenbestimmung bereitstellt, was es zum falschen Ort für die Anwendung der Renditezuweisung macht. Führen Sie Ihre eigenen Tests durch und sehen Sie, was passiert.

Die Allgegenwärtigkeit des Gleichzeitigkeitsproblems

Die signifikanten Leistungsgewinne durch die Einzigartigkeitsgewichtung sowohl bei einer Rückkehr zum Mittelwert (Bollinger Bänder) als auch bei einer Trendfolgestrategie (MA-Kreuz) sind ein deutlicher Beweis dafür, dass die Gleichzeitigkeit von Kennzeichen eine universelle Herausforderung für ML im Finanzbereich darstellt. Es handelt sich dabei nicht um einen Einzelfall, sondern um ein grundlegendes Datenproblem, das die Modelle unabhängig von der Logik der Kernstrategie verzerrt. Für eine solide Modellentwicklung ist es nicht optional, sich damit zu befassen.


Schlussfolgerung

Dieser Artikel befasst sich mit einem der heimtückischsten Probleme des maschinellen Lernens im Finanzbereich: der Verletzung der IID-Annahme aufgrund der Gleichzeitigkeit von Kennzeichen. Unsere experimentellen Ergebnisse liefern ein klares und umsetzbares Urteil: Die Gewichtung von Stichproben auf der Grundlage der zeitlichen Einzigartigkeit ist eine leistungsstarke und notwendige Technik für den Aufbau robuster Meta-Kennzeichen-Klassifikatoren, während die Gewichtung nach der Rückgabeattribution eine gefährliche Ablenkung darstellt.

Die Methode zur Gewichtung der Einzigartigkeit verbesserte die Leistung des Modells durchgängig, indem sie sicherstellte, dass der Einfluss jeder Beobachtung während des Trainings proportional zu ihrem einzigartigen Informationsgehalt war. Bei der Strategie der Bollinger-Bänder wurde das Gleichgewicht zwischen Präzision und Recall (F1-Score) verbessert. Bei der Strategie des MA-Kreuzes wurden sowohl die Standard- als auch die wahrscheinlichkeitsgewichtete Genauigkeit deutlich verbessert. In beiden Fällen wurde das Modell davon abgehalten, falsche Muster aus zeitlich redundanten Daten zu lernen.

Umgekehrt ist das dramatische Scheitern der Renditeattributionsgewichtung eine kritische Warnung. Sie zeigt, dass die Verquickung der informationellen Einzigartigkeit einer Beobachtung mit ihrer finanziellen Rendite zu einem pathologischen Modellverhalten führt, das entweder zu einem völligen Zusammenbruch der Vorhersage führt oder zu rücksichtslosem Überhandel anregt.

Für den Praktiker bedeuten diese Erkenntnisse einen klaren Auftrag:

  1. Berücksichtigen Sie immer die Gleichzeitigkeit: Die IID-Annahme ist für finanzielle Zeitreihen grundsätzlich fehlerhaft. Das Ignorieren der Gleichzeitigkeit von Kennzeichen führt zu überangepassten Modellen und zu Verlusten beim Live-Handel.
  2. Einzigartigkeitsgewichtung implementieren: Die hier vorgestellte Methode, die die durchschnittliche Einzigartigkeit jeder Dreifachschranke berechnet, ist eine überschaubare und sehr effektive Lösung. Sie sollte ein Standardbestandteil jeder ML-Pipeline sein.
  3. Vermeiden Sie eine naive Renditegewichtung: Prüfen Sie sorgfältig, ob die Gewichte der zurückgegebenen Stichproben für Ihre Modelle geeignet sind. Die Renditen sind zwar für die Bewertung der Leistung einer Strategie von entscheidender Bedeutung, können aber je nach Zielsetzung ein irreführender Ersatz für den Wert einer Beobachtung während der Ausbildung sein.
  4. Validieren Sie mit den richtigen Metriken: Wie gezeigt, kann die Genauigkeit allein trügerisch sein. Eine Kombination aus Log-Loss, F1-Score und wahrscheinlichkeitsgewichteter Genauigkeit ist für die richtige Diagnose der Leistung und Kalibrierung eines Modells unerlässlich.

Durch die Gewichtung der Stichproben auf der Grundlage der zeitlichen Einzigartigkeit werden die Modelle nicht mehr auf der Grundlage eines verzerrten, redundanten Bildes des Marktes trainiert, sondern auf der Grundlage eines Datensatzes, der die tatsächliche Häufigkeit und Unabhängigkeit der Informationsereignisse widerspiegelt. Dies ist ein grundlegender Schritt auf dem Weg zur Entwicklung von Modellen des maschinellen Lernens, die über den Backtest hinaus verallgemeinert werden können und in der adaptiven, nicht IID-konformen Realität der Finanzmärkte erfolgreich sind.

Im nächsten Artikel dieser Reihe werden wir einen Schritt weiter gehen, indem wir uns mit dem Sequential Bootstrapping befassen – einer fortschrittlicheren Stichprobentechnik, die aktiv verhindert, dass sich überschneidende Beobachtungen im selben Trainingssatz erscheinen, und damit die Wurzel des Gleichzeitigkeitsproblems in der Phase der Neuauswahl der Daten selbst angeht. 


Anlagen

Dateiname Beschreibung
bollinger_features.py Erstellt Bollinger Bänder-basierte Merkmale für Meta-Kennzeichen-Modelle, einschließlich Volatilitätsmerkmale, technische Indikatoren und gleitende Durchschnittsmerkmale. Enthält auch Visualisierungsfunktionen für das Zeichnen der Bollinger Bändern mit Handelssignalen.
filters.py Implementiert Ereignisfiltermethoden, einschließlich des symmetrischen CUSUM-Filters und des Z-Score-Filters, um signifikante Marktereignisse für das Triple-Barrier-Kennzeichen zu identifizieren.
fractals.py Bietet umfassende fraktale Analysewerkzeuge zur Identifizierung von Marktstrukturpunkten, Trendvalidierung und Whipsaw-Filterung auf der Grundlage der Handelskonzepte von Williams Bill.
ma_crossover_feature_engine.py Spezielles Feature-Engineering für Forex-MA-Crossover-Strategien, einschließlich Währungsstärkenanalyse, Risikoumfeld-Merkmale und Marktmikrostrukturmuster.
misc.py Enthält Dienstfunktionen für die Datenoptimierung, Formatierung, Protokollierung, Leistungsüberwachung und Zeitkonvertierung, die in der gesamten ML-Pipeline verwendet werden.
moving_averages.py Berechnet gleitende Durchschnittsdifferenzen und kreuzende Signale für die Merkmalsgenerierung, mit optionaler korrelationsbasierter Merkmalsauswahl.
multiprocess.py Bietet Dienstprogramme für die Parallelverarbeitung für effiziente Berechnungen auf mehreren CPU-Kernen und implementiert die Multiprocessing-Muster von AFML.
returns.py Berechnet verschiedene renditebasierte Merkmale wie verzögerte Renditen, rollierende Autokorrelationen und Renditeverteilungsstatistiken.
signal_processing.py Konvertiert rohe Strategiesignale in fortlaufende Handelspositionen und Einstiegszeitstempel, wobei CUSUM-Filterung und Signalpersistenz berücksichtigt werden.
strategies.py Definiert Basis- und konkrete Handelsstrategieklassen einschließlich Bollinger Bands und Moving Average Crossover Strategien für die Signalerzeugung.
time.py Generiert zeitbasierte Funktionen, einschließlich zyklischer Kodierung, Handelssitzungsflags und Forex-Markt-Timing-Muster für 24-Stunden-Märkte.
trend_scanning.py Implementiert eine Kennzeichnungsmethode zum Trend-Scanning, die OLS-Regressionen über mehrere Fenster anpasst, um signifikante Trends für die Klassifizierung zu identifizieren.
triple_barrier.py Kernimplementierung der Kennzeichnungsmethode von Triple-Barrier mit Numba-Optimierung für Leistung, einschließlich vertikaler Barrieren und Unterstützung von Meta-Etikettierung.
volatility.py Bietet verschiedene Volatilitätsschätzer, darunter tägliche Volatilität, Parkinson-, Garman-Klass- und Yang-Zhang-Schätzer zur Risikobewertung.
attribution.py Implementiert Methoden zur Gewichtung von Stichproben, einschließlich Renditeattribution und Faktoren eines zeitlichen Abklingens, um Gleichzeitigkeitsprobleme bei ML im Finanzbereich zu lösen, wobei die parallele Verarbeitung für effiziente Berechnungen genutzt wird.
concurrent.py Ermöglicht die Analyse gleichzeitiger Kennzeichen für Ereignisse mit dreifacher Barriere, einschließlich der Zählung gleichzeitiger Ereignisse und der Berechnung der durchschnittlichen Einzigartigkeit von Kennzeichen, um überlappende Etikettierungszeiträume zu berücksichtigen.
optimized_attribution.py Numba-optimierte Version der Rendite- und Time-Decay-Attribution mit 5-10-facher Leistungsverbesserung, die JIT-Kompilierung und vektorisierte Operationen für schnellere Berechnungen der Stichprobengewichte verwendet.
optimized_concurrent.py Numba-optimierte Version der gleichzeitigen Ereignisanalyse mit 5-10-facher Leistungssteigerung, die parallele Verarbeitung und effiziente Speicherzugriffsmuster für schnellere Eindeutigkeitsberechnungen nutzt.


Referenzen und weiterführende Literatur

Primäre Quelle:
López de Prado, M. (2018): Advances in Financial Machine Learning. Wiley.

Verwandte Papiere:

  • López de Prado, M. (2015): "The Future of Empirical Finance." The Journal of Portfolio Management.
  • López de Prado, M. (2020): Machine Learning for Asset Managers. Cambridge University Press.
  • Rao, C., P. Pathak and V. Koltchinskii (1997): "Bootstrap by sequential resampling." Journal of Statistical Planning and Inference, Vol. 64, No. 2, pp. 257–281.
  • King, G. and L. Zeng (2001): "Logistic Regression in Rare Events Data." Working paper, Harvard University. Verfügbar unter https://gking.harvard.edu/files/0s.pdf.
  • Lo, A. (2017): Adaptive Markets, 1st ed. Princeton University Press.

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/19850

Beigefügte Dateien |
strategies.py (4.34 KB)
fractals.py (16.45 KB)
misc.py (19.8 KB)
time.py (8.25 KB)
triple_barrier.py (18.88 KB)
trend_scanning.py (11.02 KB)
filters.py (8.39 KB)
returns.py (6.27 KB)
volatility.py (5.38 KB)
attribution.py (5.88 KB)
concurrent.py (4.96 KB)
Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Klassische Strategien neu interpretieren (Teil 18): Suche nach Kerzenmustern Klassische Strategien neu interpretieren (Teil 18): Suche nach Kerzenmustern
Dieser Artikel hilft neuen Community-Mitgliedern, ihre eigenen Kerzenmuster zu suchen und zu entdecken. Die Beschreibung dieser Muster kann entmutigend sein, da sie eine manuelle Suche und kreative Identifizierung von Verbesserungen erfordert. Hier stellen wir die Engulfing-Kerzen vor und zeigen, wie es für profitablere Handelsanwendungen verbessert werden kann.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Langfristige Handelsgeschäfte optimieren: Engulfing-Kerzenmuster und Liquiditätsstrategien Langfristige Handelsgeschäfte optimieren: Engulfing-Kerzenmuster und Liquiditätsstrategien
Dies ist ein EA, der auf einem hohen Zeitrahmen basiert und langfristige Analysen, Handelsentscheidungen und Ausführungen auf der Grundlage von Analysen auf einem höheren Zeitrahmen von W1, D1 und MN vornimmt. Dieser Artikel befasst sich ausführlich mit einem EA, der speziell für langfristige Händler entwickelt wurde, die geduldig genug sind, um ihre Positionen während turbulenter Kursbewegungen im unteren Zeitrahmen zu halten, ohne ihre Ausrichtung häufig zu ändern, bis die Take-Profit-Ziele erreicht sind.