English
preview
MetaTrader 5機械学習の設計図(第4回):金融機械学習パイプラインの隠れた欠陥 - ラベルの同時発生

MetaTrader 5機械学習の設計図(第4回):金融機械学習パイプラインの隠れた欠陥 - ラベルの同時発生

MetaTrader 5トレーディング |
12 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

はじめに

本連載第2回では、金融時系列データから機械学習用ラベルを作成するトリプルバリア法を紹介しました。この手法がリターンのパス依存性に対応し、分類モデルに対してより現実的な学習ラベルを提供することを説明しました。本記事では、読者にトリプルバリア法およびscikit-learnを用いた教師あり機械学習の基礎知識があることを前提としています。

トリプルバリア法を実装すると、多くの機械学習実務者が見落とす重大な課題が発生します。それが「ラベルの同時発生」です。金融データにバリアを適用すると、生成されるラベルは時間的に重複することが多くなります。複数の観測値が同時に「アクティブ」となる場合があり、それらの情報セットは重複する期間にまたがるため、時間的依存性が生じます。この時間的依存性は、ほとんどの機械学習アルゴリズムの基本的な仮定、すなわち学習サンプルが独立かつ同一分布(IID)であることを侵害します。

この侵害には深刻な影響があります。ラベルが同時に発生している観測値で学習したモデルは、同じパターンを何度も学習するため、インサンプルでの性能が過大評価されます。しかし、アウトオブサンプルでは実際のパターンの出現頻度がモデルの想定よりも低いため、性能が低下します。その結果、過学習したモデルはライブ取引で失敗することになります。

本記事では、この課題に対してサンプル重み付けという原理的なアプローチで対応します。具体的には以下を示します。

  • 観測値間の重複度を、同時発生メトリクスで定量化する
  • 各観測値が持つ固有情報を反映したサンプル重みを計算する
  • これらの重みをscikit-learn分類器に組み込み、モデルの一般化性能を向上させる
  • 適切なクロスバリデーション手法を用いて、複数戦略での性能改善を評価する


サンプル重み付け:同時発生問題への対応

同時発生の問題

ほとんどの非金融系の機械学習研究者は、観測値がIID(独立同分布:Independent and Identically Distributed)過程から取得されていると仮定できます。たとえば、多数の患者から血液サンプルを採取し、コレステロール値を測定する場合です。もちろん、さまざまな共通要因によってコレステロール分布の平均や標準偏差は変動しますが、サンプル自体は独立しています。患者ごとに1つの観測値があるためです。しかし、検査室で各チューブの血液を右隣の9本のチューブに流してしまったとします。つまり、チューブ10には患者10の血液だけでなく、患者1~9の血液も混ざっているという状況です。チューブ11には患者11の血液に加え、患者2~10の血液が入っています。こうなると、各患者のコレステロール値が確実にわからない状態で、高コレステロールを予測する特徴量(食事、運動、年齢など)を特定する必要があります。これは、金融機械学習で直面する課題と同等で、さらに血液の混入パターンが非決定的かつ未知であるというハンディキャップが加わります。

同時発生する観測値で学習したモデルは、同じパターンを何度も学習するためインサンプル性能が過大評価されますが、実際のアウトオブサンプルではそのパターン出現頻度がモデルの想定よりも低いため、性能が低下します。

サンプル重み付けは、この問題に対する有効な解決策となります。すべての観測値を同等に扱うのではなく、各観測値が持つ固有情報量に応じて重みを割り当てます。重複が多い観測値は低い重み、真に独立した観測値は高い重みを付与します。

数学的基盤

サンプル重み付けの数学的基盤は、「平均ユニーク性」の概念にあります。各観測値について、情報のどの部分が独自(ユニーク)であり、どの部分が他の同時発生観測値と共有されているかを定量化する必要があります。

López de Pradoの手法では、ラベルの重複行列を用いて計算します。任意の2つの観測値ijについて、それぞれの情報セットが時間的にどれだけ重複しているかを算出します。観測値iがラベルにt₁からt₂までの情報を用い、観測値jt₃からt₄までの情報を用いる場合、重複はこれらの時間区間の交差部分として定義されます。

処理は3ステップでおこないます。

  1. 同時発生数:データの各バーで、何件のイベントがその時点でアクティブかを数えます。たとえば、3つのポジションが同時に存在している場合、その期間の各バーの同時発生数は3です。
  2. ユニーク性:各イベントについて、イベントの継続期間中の各バーで1/同時発生数を計算し、その平均を取ります。たとえば、同時発生数が[3, 4, 3, 2]であれば、平均ユニーク性は(1/3 + 1/4 + 1/3 + 1/2)/4 ≈ 0.354です。
  3. サンプル重み:この平均ユニーク性が、学習時の観測値の重みとなります。

観測iの平均ユニーク性は、その継続期間中のバーにおける同時発生数の逆数の平均で計算されます。他の観測値と重複しない場合は1.0(最大重み)、多くの観測値と完全に重複する場合は0.0(最小重み)に近づきます。

この方法により自然な重み付けが生まれます。

  • 独立した観測値は最大重み(1.0)
  • 部分的に重複する観測値は比例して減少(0.3〜0.7)
  • 強く重複する観測値は最小重み(<0.3)

この手法の利点は、重複観測を完全に削除せず、冗長性に応じて影響力を比例的に減らす点です。情報を保持しつつ、時間的重複による人工的な増幅を補正できます。

実装:同時発生の計算

サンプル重み付けを実装するには、具体的な文脈で「同時発生」が何を意味するかを慎重に定義する必要があります。トリプルバリア法では、2つの観測値がそれぞれの期間(エントリーからエグジットまで)で重複する場合、同時発生とみなします。

最初の関数は、各時点で何件のイベントがアクティブかを計算します。

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()]

この関数が実際におこなうこと:次の3つの取引があるとします。

  • 取引A:10:00に開始、10:30に終了
  • 取引B:10:15に開始、10:45に終了
  • 取引C:10:50に開始、11:00に終了

10:20時点では取引Aと取引Bが存在しているので、count[10:20] = 2です。10:55時点では取引Cのみが存在しているので、count[10:55] = 1です。この関数は、上記の全タイムラインを構築します。

このラッパー関数は、mp_pandas_objという並列処理ユーティリティ(multiprocess.pyを参照)を用いて、データセット全体に対してこの計算を並列化します。

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

平均ユニーク性の計算

各バーでの同時発生数がわかったら、イベントごとの平均ユニーク性を計算します。

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

オーケストレーター関数で全体をまとめます。

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

リターンアトリビューション

平均ユニーク性は時間的重複を考慮しますが、イベントの規模(リターン)を反映しません。リターンアトリビューション法は、平均ユニーク性とイベント継続期間中の絶対リターンを組み合わせます。

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()

以下は、適切なデータ処理を備えた完全な実装です。

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

時間減衰による重み付け

市場は適応的システムであり、変化するにつれて、過去の事例は新しい事例に比べて関連性が低下します。そのため、上で算出したサンプル重みに時間減衰係数を乗じ、より最近の観測値に高い重みを与えたいと考えます。なお、ここでの「時間」は単純な時系列順を意味するものではありません。この実装では、減衰は累積ユニーク性に基づいておこなわれます。なぜなら、単純な時系列に基づく減衰を適用すると、冗長な観測値が存在する場合に重みが過度に急速に低下してしまうためです。

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

時間減衰係数

図1:時間減衰係数(線形 vs. 指数)

クラス重み

サンプル重みに加えて、クラス重みを適用することも有効です。クラス重みとは、出現頻度が低いラベルを補正するための重みです。これは、特に重要なクラスの発生頻度が低い分類問題において極めて重要です(King and Zeng[2001])。たとえば、2010年5月6日のフラッシュクラッシュのような流動性危機を予測したいとします。このような事象は、その間に発生する数百万件の観測値と比べると非常に稀です。これらの希少ラベルに対応するサンプルに高い重みを割り当てなければ、機械学習アルゴリズムは最も頻出するラベルの精度を最大化しようとし、フラッシュクラッシュは稀な事象ではなく単なる外れ値として扱われてしまいます。

機械学習ライブラリには通常、クラス重みを扱う機能が実装されています。たとえばscikit-learnでは、クラスj (j=1,…,J)のサンプルにおける誤差に対して、1ではなくclass_weight[[j]]を掛けてペナルティを与えます。したがって、ラベルjに対してより高いクラス重みを設定すると、アルゴリズムはそのラベルに対してより高い精度を達成するように強制されます。クラス重みの合計がJと一致しない場合、その効果は分類器の正則化パラメータを変更することと同等になります。

金融アプリケーションでは、分類アルゴリズムの標準的なラベルは{−1, 1}です。0(または中立)のケースは、予測確率が0.5をわずかに上回るが中立閾値未満である場合として暗黙的に扱われます。一方のクラスの精度を優先する理由はないため、デフォルトとしてはclass_weight='balanced'を指定するのが適切です。この設定は、すべてのクラスが同頻度で出現したと仮定するように観測値を再重み付けします。バギング分類器の文脈では、class_weight='balanced_subsample'を検討することもできます。これは、class_weight='balanced'がデータセット全体ではなく、インバッグのブートストラップサンプルに対して適用されることを意味します。詳細については、scikit-learnにおけるclass_weight実装のソースコードを参照すると有益です。

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


実務実装

非IIDデータにおけるバギングへの対応

金融データではIID仮定が成立しないため、標準的なバギングは効果的に機能しません。なぜなら、生成されるブートストラップサンプルが系列相関に汚染されてしまうからです。『Advances in Financial Machine Learning』では、この根本的課題を克服するために3つの明確な手法が提案されており、そのすべての基盤としてサンプル重み付けが位置付けられています。

方法1:ブートストラップサンプルサイズの制限

本記事で採用している手法の一つです。オーバーサンプリングという問題に直接対処する、実務的かつ計算効率の高いアプローチです。

  • 基本的な考え方:各ブートストラップサンプルのサイズを大幅に縮小します。抽出する観測値数を減らすことで、統計的に高い相関を持つデータ点が同一サンプル内に同時に含まれる確率を低下させます。
  • 実装:sklearn.ensemble.BaggingClassifierでは、max_samplesパラメータを1.0より大幅に小さい値(例:0.5、0.3、あるいはそれ未満)に設定することで実現します。実務上のヒューリスティックとして、データセットの平均ユニーク性に設定する方法があります(max_samples = out['tW'].mean())。
  • メカニズム:max_samplesは、各ベース推定器を学習させるためにXから抽出するサンプル数(絶対値または割合)を制御します。たとえばmax_samples=0.3と設定すると、各分類器は元データのランダムな30%のみで学習され、重複を制限することで多様性を確保します。
  • 長所と短所
    • 長所:実装が非常に簡単で、コード1行の変更で済みます。
    • 短所:根本的な解決策ではなく、冗長性を減らすだけでユニーク性の高い観測値を積極的に選択するわけではありません。また、有用なデータを捨ててしまう可能性もあります。

方法2:インバッグ推定におけるサンプル重み付け

この方法は、サンプリング段階ではなく、各ベース推定器の学習段階で問題を補正します。

  • 基本的な考え方:標準的なブートストラップサンプリングをおこないますが、各ベース推定器を学習させる際に、事前計算済みのサンプル重み(tW列)を使用し、モデルがユニーク性の高い観測値に集中し、冗長な観測値の影響を抑制するようにします。
  • 実装:標準的手法でブートストラップサンプルを作成した後、各基本学習器(base estimator)のsample_weightパラメータにインバッグ観測値の事前計算済み重みを設定します。これには、基本学習器がsample_weightをサポートしている必要があります(例:sklearnのDecisionTreeClassifier)。
  • メカニズム:モデルの損失関数を変更し、高重み(ユニーク性が高い)観測値の誤差にはより大きなペナルティを与え、低重み(冗長)観測値の誤差には小さなペナルティを与えます。
  • 長所と短所
    • 長所:既に計算済みのサンプル重みを活用でき、他の手法と組み合わせ可能です。
    • 短所:相関のあるデータがブートストラップサンプルに入ること自体は防げず、影響を事後的に緩和するに過ぎません。

方法3:逐次ブートストラップ

これはLópez de Pradoが推奨する、理論的に厳密かつ目的指向の解決策です。詳細は次回の記事で扱います。

  • 基本的な考え方:標準的なランダムサンプリングを完全に置き換え、各ブートストラップサンプル内でユニーク性を能動的に確保するインテリジェントな逐次アルゴリズムを用います。
  • 実装:観測値を1つずつ抽出するカスタムサンプリングルーチンを作成します。新しい観測値は、既にサンプルに含まれているすべての観測値と比較して十分に「独自」(重複が少ない)である場合にのみ追加されます。これにはリサンプリングロジックの完全なカスタム実装が必要です。
  • メカニズム:同時発生行列やラベル重複情報を直接利用し、各候補観測値をブートストラップサンプルに採用するか否かを条件付きで決定します。
  • 長所と短所
    • 長所:理論的に妥当であり、最大限に多様かつ非相関なサンプルを能動的に構築します。
    • 短所:計算コストが高く、正確に実装するには複雑です。

総合整理と本記事でのアプローチ

これら3つの手法は、洗練度の階層を形成しています。

  • 方法1(サンプルサイズの制限)は、サンプリング段階における単純な予防策として機能します。
  • 方法2(インバッグ重み付け)は、モデル学習中に補正をおこなう是正策として機能します。
  • 方法3(逐次ブートストラップ)は、サンプリング段階で根本原因に対処する包括的かつ予防的な解決策です。

方法1と方法2は補完的であり、多層防御型のアプローチとして組み合わせることができます。方法1は、重複する観測値が各ブートストラップサンプルに抽出される確率を低減します。一方、方法2は、仮に重複観測値がサンプルに含まれた場合でも、学習時にその影響力が適切に抑制されるようにします。

本記事では、その補完性、実装の簡便さ、そして実証的に有効性が確認されていることを理由に、方法1と方法2の両方を採用します。具体的には、max_samples=out['tW'].mean()(方法1)と設定し、さらに分類器のfit()メソッドにsample_weight=out['tW']を渡す(方法2)ことで、ブートストラップサンプル内の冗長性を予防しつつ補正する堅牢なパイプラインを構築します。

より高度な方法3である逐次ブートストラップについては、専用の続編記事で取り上げ、実装に必要なカスタムサンプリングクラスを構築していきます。

モデル学習におけるサンプル重みの利用

ここからが重要な部分です。これらの重みを、実際にどのように機械学習パイプラインで使用するのでしょうか。まずは少し寄り道をして、クロスバリデーションというテーマと、なぜ標準的な手法が金融分野では機能しないのかを確認します。これは、重み付け手法の有効性を評価するために用いる重要な手法だからです。

金融クロスバリデーションの概念的基盤

標準的なk分割クロスバリデーション(CV)は、データが独立同分布(IID)であるという前提に依存しています。しかし、金融の時系列データは、系列相関、時間依存性、構造変化を含むため、この中核となる前提に違反しています。標準手法を使用すると、データリーケージのリスクが生じます。つまり、本来予測すべき将来の情報が、過去データで学習するモデルに意図せず含まれてしまい、過学習や信頼性の低い性能評価につながります。

図2は、k = 5の場合のk分割クロスバリデーションにおけるk回の学習/テスト分割を示しています。この方式では以下をおこないます。

  1. データセットはk個の部分集合に分割されます。
  2. i = 1,…,kについて

k分割クロスバリデーション

図2:5分割CVにおける学習/テスト分割

  • (a) 機械学習アルゴリズムは、i番目以外のすべての部分集合で学習されます。
  • (b) 学習済みアルゴリズムは、i番目の部分集合でテストされます。

この問題に対処するために、López de Pradoは標準的なk分割CVに対して2つの重要な修正を導入しています。

  • パージ:テストセット内のラベルと時間的に重複する訓練セット内の観測値を削除します。これにより、モデルが予測対象となる未来期間の情報を事前に知ることを防ぎます。
  • エンバーゴ:追加的な安全措置として、テスト期間直後の一定割合のデータを学習セットからさらに除外します。これは、系列相関を通じたリーケージを防ぐための措置です。

訓練セットにおける重複のパージ

図3:訓練セットにおける重複のパージ

テスト後の学習観測値に対するエンバーゴ

図4:テスト後の学習観測値に対するエンバーゴ

ハイパーパラメータの調整、バックテスト、性能評価などどのような目的であっても、学習/テスト分割をおこなう際には、重複する訓練観測値に対してパージおよびエンバーゴを適用する必要があります。以下のコードは、テスト情報が訓練セットへ漏洩する可能性を考慮するために、scikit-learnのKFoldクラスを拡張したものです。

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


評価手法

スコアリング指標

金融機械学習においては、モデル性能を評価するための適切な指標の選択が極めて重要です。特に、不均衡データセットやメタラベリングを扱う場合、正解率のような標準的指標は誤解を招く可能性があります。ここでは、金融機械学習モデルの評価に用いられる主要な指標を確認します。

正解率

正解率は、正しく分類された観測値の割合を計算することで、予測の全体的な正確さを測定します。

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

ここで

  • TP = 真陽性
  • TN = 真陰性
  • FP = 偽陽性
  • FN = 偽陰性

正解率は性能の全体像を示す指標ではありますが、金融アプリケーションではクラス分布がしばしば不均衡であるため、誤解を招く可能性があります。

適合率

適合率は、陽性と予測したもののうち実際に陽性であった割合を測定することで、陽性予測の信頼性を定量化します。

Precision = TP / (TP + FP)

高い適合率は、モデルが陽性予測をおこなった場合にそれが正しい可能性が高いことを示します。これは、誤ったシグナルがコストにつながる取引システムにおいて重要な特性です。

再現率

再現率(または感度)は、実際に陽性であるケースをどれだけ正しく検出できているかを測定します。

Recall = TP / (TP + FN)

高い再現率は、利用可能な機会の大部分をモデルが捉えていることを意味します。これは、時折最適でないポジションを取るよりも、利益の出る取引機会を逃すほうがコストになる場合に重要です。

F1スコア

F1スコアは、不均衡データにおける正解率の限界を補うために、適合率と再現率を単一の指標に統合したものです。

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

この指標は、特にメタラベリングの応用において有用です。そこでは陰性ケース(ラベル「0」)が陽性ケース(ラベル「1」)を大幅に上回ることがよくあります。そのような状況では、常に多数派クラスを予測する単純な分類器でも高い正解率を達成できますが、実際の取引機会をまったく識別できない可能性があります。

重要な考慮事項:F1スコアは、特定の退化ケースにおいて未定義になります。

  • 観測値がすべて陰性である場合(再現率を計算するための陽性が存在しない)
  • 予測値がすべて陰性である場合(適合率を評価するための陽性予測が存在しない)

Scikit-learnは、これらのエッジケースにおいてF1スコアを0として返し、UndefinedMetricWarningを発行します。

二値分類における退化ケースの理解

以下の表は、極端な状況において各評価指標がどのように振る舞うかをまとめたものです。

条件
崩壊内容
正解率:
適合率
再現率
F1
すべての観測値が1
TN=FP=0
=再現率
1 [0,1]
[0,1]
すべての観測値が0
TP=FN=0
[0,1]
0 未定義
未定義
すべての予測値が1
TN=FN=0
=適合率
[0,1]
1 [0,1]
すべての予測値が0
TP=FP=0
[0,1]
未定義
0
未定義

これらのエッジケースは、正解率のみに依存することがいかに誤解を招く可能性があるかを示しています。そのため、実務的な金融アプリケーションでは、F1スコアや対数損失のほうがより堅牢な評価を提供します。

対数損失

対数損失(クロスエントロピー損失)は、予測の正誤だけでなく予測確信度も考慮することで、正解率よりも精緻な評価を提供します。

対数損失の式

ここで

  • pn,k = 観測nに対するクラスkの予測確率
  • Y = 1-of-Kの二値インジケーター行列
  • yn,k = 観測nのラベルがkであれば1、それ以外は0

金融アプリケーションでは、直感的なスコアリング(値が高いほど良い)を維持するため、通常は負の対数損失を用います。この指標が特に重要である理由は以下の通りです。

  1. 予測確信度を考慮する:高い確信度での誤予測は、低い確信度での誤予測よりも強く罰せられます。
  2. 損益(PnL)との整合性:リターンに基づくサンプル重みと組み合わせることで、分類器が損益に与える影響を近似できます。
  3. ポジションサイズを反映する:高い確信度の予測は、取引戦略において通常より大きなポジションサイズにつながります。

正解率は確信度に関係なくすべての誤りを同等に扱いますが、対数損失は分類器が取引パフォーマンスに与える潜在的影響を、より現実的に評価します。

たとえば、ある分類器が2つの「1」を予測し、真のラベルがそれぞれ1と0であったとします。1つ目の予測は的中し、2つ目は外れです。この場合、正解率は50%です。図5は、これらの予測確率が[0.5, 0.9]の範囲にあるときのクロスエントロピー損失を示しています。図の右側では、高確率での誤予測により対数損失が大きくなっていることがわかります。正解率は常に50%であるにもかかわらずです。

予測確率(的中および外れ)に対する対数損失

図5:的中および外れの予測確率に対する対数損失

確率加重正解率(PWA)

確率加重正解率(Probability Weighted Accuracy, PWA)は、従来の正解率を拡張した指標であり、正しい予測に対してその確信度に応じた重みを与えます。たとえば、90%の確信度で正しく予測した場合は、51%の確信度で正しく予測した場合よりも大きく貢献します。これは、実取引において予測確信度に基づいてポジションサイズを決定する状況をより適切に反映します。PWAは、高い確信度でおこなわれた誤予測を正解率よりも強く罰しますが、対数損失ほど強くは罰しません。

確率加重正解率の計算式

ここでpn = max{pn,k}、y nは指示関数であり、yn ∈ {0, 1}です。予測が正しい場合はyn = 1、それ以外の場合はyn = 0になります。

この指標は、分類器がすべての予測に対して完全な確信を持つ場合(すべてのnについてpn = 1)には、標準的な正解率と等価になります(Prado, 2020, p.83)。また、基準調整項pn - 1/Kは、ランダム推測(確率 = 1/K)がゼロの重みとなるように設計されています。

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


実験設定

データと取引戦略

サンプル重み手法の評価には、2018-01-01から2022-12-31までのEUR/USD M5足データを使用しました。2つの異なるメタラベリング戦略を、20日間指数移動標準偏差をボラティリティ目標としてテストしました。

メタラベル付きボリンジャーバンド戦略

この戦略はボリンジャーバンドを用いてプライマリ売買シグナルを生成し、メタラベリングモデルでフィルタリングします。プライマリモデルは価格と上限および下限バンドの交差に基づいてシグナルを出し、メタモデルは各シグナルに基づいて利益が出る可能性を予測します。

トリプルバリア設定

  • 利益目標:1
  • 損切り:2
  • 時間バリア:4時間
  • 最小リターン閾値:0.0

メタラベル付きMA_20_50クロス戦略

この古典的トレンドフォロー戦略では、20期間と50期間移動平均線のクロスをプライマリシグナルとし、メタラベリングモデルがどのクロスが利益につながるかを予測してフィルタリングします。

トリプルバリア設定

  • 利益目標:0
  • 損切り:2
  • 時間バリア:1日
  • 最小リターン閾値:0.0

評価フレームワーク

各戦略に対して、サンプル重み付けありとなしのランダムフォレスト分類器を使用し、Purged K-Foldクロスバリデーションでデータリークを防ぎました。

以下の関数で、上記で説明したすべてのパフォーマンス指標を計算します。

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


実験結果

戦略パフォーマンス比較

10分割クロスバリデーションを用いて、サンプル重み付けありとなしで2つの異なる戦略を評価しました。

  • メタラベル付きボリンジャーバンド戦略:従来型の平均回帰戦略
  • メタラベル付きMA_20_50クロス戦略:古典的なトレンドフォロー戦略

ボリンジャーバンド戦略のパフォーマンス

指標 重み付けなし ユニーク性重み付け リターン重み付け
正解率: 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
負の対数損失 -0.688 ± 0.008 -0.682 ± 0.007 -0.631 ± 0.023
適合率 0.650 ± 0.019 0.658 ± 0.024 0.000 ± 0.000
再現率 0.616 ± 0.167 0.683 ± 0.145 0.000 ± 0.000
F1スコア 0.622 ± 0.091 0.663 ± 0.073 0.000 ± 0.000

MA 20-50クロス戦略のパフォーマンス

指標 重み付けなし ユニーク性重み付け リターン重み付け
正解率: 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
負の対数損失 -0.650 ± 0.037 -0.625 ± 0.036 -0.826 ± 0.018
適合率 0.298 ± 0.026 0.296 ± 0.029 0.473 ± 0.011
再現率 0.588 ± 0.125 0.530 ± 0.108 1.000 ± 0.000
F1スコア 0.388 ± 0.015 0.372 ± 0.018 0.642 ± 0.010

結果から得られた重要な洞察

今回の実験結果は、サンプル重み付けがモデルパフォーマンスに与える影響が、戦略ごとに微妙に異なることを示しています。各重み付け手法の有効性は、基礎となる取引ロジックによって大きく左右されます。

ユニーク性重み付け:メタラベリングの堅牢なデフォルト

ユニーク性重み付けは、両戦略において一貫して有意な改善を示し、堅牢なデフォルト手法として位置付けられます。

  • ボリンジャーバンド戦略:ユニーク性重み付けにより全体的なパフォーマンス向上が確認されました。正解率は56.4%から58.4%に向上しましたが、より重要なのはF1スコアが0.622から0.663に6.7%上昇した点です。これは、適合率と再現率のバランスが改善され、モデルが偽シグナルをより効果的に除外しつつ、真の機会をより確実に捉える能力が向上したことを示しています。PWAの改善も、独自かつ冗長でないサンプルに対するモデルの信頼度が適切に調整されたことを裏付けます。
  • MAクロス戦略:改善効果はさらに顕著でした。ユニーク性重み付けにより正解率が58.9%から63.4%に7.5%向上し、PWAは67.2%から74.0%へ10.2%上昇しました。F1スコアはわずかに低下しましたが、確率加重指標の大幅な改善は、メタラベリングモデルにおいて重要です。なぜなら、メタラベリングではプライマリシグナルの成功確率に基づいてポジションサイズを決定することが目的だからです。

リターンアトリビューションによる重み付け:注意すべき落とし穴

ユニーク性重み付けの成功とは対照的に、リターンアトリビューション法は極端で望ましくない結果を生み出し、重要な落とし穴を浮き彫りにしました。

  • ボリンジャーバンド戦略:モデルは実質的に単純な分類器になってしまいました。適合率、再現率、F1スコアはすべて0に落ちましたが、正解率は逆に69.3%に跳ね上がっています。このパターンは、モデルが常に多数派クラス(おそらく0、「取引しない」)を予測するだけになり、過去リターンの大きさに過剰適合して分類能力を失った典型的な例です。
  • MAクロス戦略:やや異なる失敗モードが発生しました。再現率は1.0に達し、F1スコアは64.2%でしたが、正解率は47.3%にとどまりました。これは、モデルがほぼ無差別に陽性クラス(1、「取引する」)を予測し、真陽性はすべて捕捉したものの、偽陽性も大量に生じたことを示しています。この挙動はライブ取引システムでは容認できません。

リターンアトリビューション法の失敗は、同時発生とリターンの大きさが異なる概念であることを示しています。リターンのみで重み付けをおこなうと学習信号が歪み、モデルは過去の利益に過剰に適合してしまい、独自の情報イベントから一般化可能なパターンを学べません。リターンの大きさは、方向性(ラベル{1, -1})を予測する場合に有効です。メタラベリングはプライマリモデルの誤予測を除外し、独立したポジションサイズのモデルを提供することが目的なので、ここでリターンによる重み付けを適用するのは不適切です。自身でテストをおこない、結果を確認してください。

同時発生問題の普遍性

ユニーク性重み付けによるボリンジャーバンド(平均回帰)戦略とMAクロス(トレンドフォロー)戦略の両方での顕著なパフォーマンス向上は、同時発生が金融機械学習における普遍的な課題であることを強く示しています。これは単なる例外ではなく、根本的なデータリーケージ問題であり、戦略の基本ロジックに関係なくモデルをバイアスさせます。堅牢なモデル開発において、対処は必須です。


結論

本記事では、金融機械学習における最も厄介な問題の一つ、すなわち同時発生によるIID仮定の違反に取り組みました。実験結果は明確かつ実用的な結論を示しています。時間的ユニーク性に基づく重み付けは、堅牢なメタラベリング分類器を構築するために強力かつ必要な手法である一方、リターンアトリビューションによる重み付けは危険な誤誘導であるということです。

ユニーク性重み付けは、各観測の学習時における影響力がその独自情報量に比例するよう保証することで、モデルのパフォーマンスを一貫して向上させました。ボリンジャーバンド戦略では、適合率と再現率のバランス(F1スコア)が改善されました。MAクロス戦略では、通常の正解率およびPWAが大幅に向上しました。どちらのケースでも、モデルは時間的に冗長なデータからの誤ったパターン学習を回避できました。

一方、リターンアトリビューションによる重み付けの劇的な失敗は重要な警告を示しています。観測の情報的ユニーク性と金融リターンを混同すると、モデルは異常な挙動を示し、完全に予測不能になるか、無謀な過剰取引を誘発することを実証しました。

実務者にとって、これらの知見は明確な指針となります。

  1. 同時発生を必ず考慮する:金融時系列データにおけるIID仮定は根本的に成り立ちません。同時発生を無視すると、過剰適合モデルやライブ取引での損失につながります。
  2. ユニーク性重み付けを実装する:本記事で示した、各トリプルバリアラベルの平均ユニーク性を計算する手法は、実用的かつ非常に効果的です。金融機械学習パイプラインの標準要素とすべきです。
  3. 安易なリターン重み付けを避ける:リターンに基づく重み付けがモデルに適切かどうかを慎重に評価してください。リターンは戦略評価には重要ですが、学習時の観測価値の指標としては誤解を招くことがあります。
  4. 適切な指標で検証する:正解率だけでは判断を誤る可能性があります。対数損失、F1スコア、PWAの組み合わせにより、モデルのパフォーマンスとキャリブレーションを正しく評価できます。

時間的なユニーク性に基づくサンプル重み付けを導入することで、市場の歪んだ冗長なデータに基づいてモデルを学習させるのではなく、情報イベントの真の頻度と独立性を反映したデータセットでモデルを学習させることが可能になります。これは、バックテストを超えて一般化可能な機械学習モデルを構築し、金融市場の適応的かつ非IIDな現実で成功するための基礎的ステップです。

次回の記事では、このアプローチをさらに進め、逐次ブートストラップを探求します。これは、同じ学習セット内に重複する観測が出現するのを積極的に防ぐ高度なサンプリング手法であり、データ再サンプリング段階で同時発生問題の根本原因に対処するものです。 


添付ファイル

ファイル名 説明
bollinger_features.py メタラベリングモデル向けのボリンジャーバンドベースの特徴量を作成します。ボラティリティ、テクニカル指標、移動平均の特徴量を含み、売買シグナルとともにボリンジャーバンドをプロットする可視化関数も含みます。
filters.py イベントフィルタリング手法を実装します。対照的CUSUMフィルタやZスコアフィルタを用いて、トリプルバリア法における重要な市場イベントを抽出します。
fractals.py 市場構造のポイントを特定するフラクタル分析ツールを提供します。トレンドの検証や、ウィリアムズ・ビルの取引概念に基づくホイップソーフィルタリングも含まれます。
ma_crossover_feature_engine.py 外国為替のMAクロス戦略向けの特徴量エンジニアリングを専門的におこないます。通貨強度分析、リスク環境特徴量、市場のマイクロストラクチャーパターンを含みます。
misc.py データ最適化、フォーマット変換、ログ記録、パフォーマンスモニタリング、時間変換など、MLパイプライン全体で使用するユーティリティ関数を含みます。
moving_averages.py 移動平均差分とクロスオーバーシグナルを計算し、特徴量生成をおこないます。必要に応じて相関ベースの特徴選択も可能です。
multiprocess.py 複数CPUコアを活用した効率的な計算のための並列処理ユーティリティを提供します。『Advances in Financial Machine Learning』のマルチプロセッシングパターンを実装しています。
returns.py 過去リターン、ローリング自己相関、リターン分布統計など、さまざまなリターンベースの特徴量を計算します。
signal_processing.py 生の戦略シグナルを連続的なポジションとエントリータイムスタンプに変換します。CUSUMフィルタリングやシグナルの持続時間も処理します。
strategies.py 基盤戦略クラスと具体的な戦略クラスを定義します。ボリンジャーバンドや移動平均クロスオーバー戦略のシグナル生成を含みます。
time.py 時間ベースの特徴量を生成します。周期エンコーディング、取引セッションフラグ、24時間市場向けの外国為替市場タイミングパターンを含みます。
trend_scanning.py 複数ウィンドウにわたるOLS回帰を用いて重要なトレンドを特定するトレンドスキャニングラベリング手法を実装します。
triple_barrier.py トリプルバリア法のコア実装です。Numba最適化を施しており、垂直バリアやメタラベリングをサポートします。
volatility.py 日次ボラティリティ、パーキンソン、ガーマン・クラッス、ヤン・ザンなど、さまざまなボラティリティ推定器を提供し、リスク評価に使用します。
attribution.py ラベルの同時発生問題に対応するため、リターンアトリビューションや時間減衰係数を用いたサンプル重み付けを実装します。並列処理により効率的な計算を実現しています。
concurrent.py トリプルバリアイベントの同時発生ラベル解析を扱います。同時発生イベントのカウントや平均ラベルのユニーク性計算により、重複するラベリング期間を補正します。
optimized_attribution.py リターンと時間減衰の重み付けをNumba最適化したバージョンです。JITコンパイルとベクトル化により、サンプル重み計算を5~10倍高速化します。
optimized_concurrent.py 同時発生イベント解析をNumba最適化したバージョンです。並列処理と効率的なメモリアクセスにより、平均ラベルユニーク性計算を5~10倍高速化します。


参考文献および追加資料

主要文献
López de Prado, M.(2018):Advances in Financial Machine Learning.Wiley.

関連論文・書籍

  • 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."ハーバード大学のワーキングペーパー。 https://gking.harvard.edu/files/0s.pdfで入手可能。
  • Lo, A.(2017):Adaptive Markets, 1st ed.Princeton University Press.

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19850

添付されたファイル |
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)
プライスアクション分析ツールキットの開発(第48回):加重バイアスダッシュボードを備えた多時間軸ハーモニー指数 プライスアクション分析ツールキットの開発(第48回):加重バイアスダッシュボードを備えた多時間軸ハーモニー指数
本記事では、「多時間軸ハーモニー指数」を紹介します。これはMetaTrader 5向けの高度なエキスパートアドバイザー(EA)で、複数の時間軸からのトレンドの傾向を加重平均し、EMAによって平滑化したうえで、見やすいチャートパネル型ダッシュボードに表示します。さらに、カスタマイズ可能なアラート機能に加え、強いバイアスの閾値を超えた際には自動で売買シグナルをチャート上に描画します。複数時間軸分析を活用し、市場構造に沿ったエントリーを目指すトレーダーに最適なEAです。
MQL5での取引戦略の自動化(第37回):ビジュアル指標付きレギュラーRSIダイバージェンス・コンバージェンス検出 MQL5での取引戦略の自動化(第37回):ビジュアル指標付きレギュラーRSIダイバージェンス・コンバージェンス検出
本記事では、スイングポイントの強さを考慮し、バー制限や許容幅のチェックを組み合わせて、レギュラーRSIダイバージェンスを検出するMQL5エキスパートアドバイザー(EA)を作成します。このEAは、強気または弱気シグナルに基づいて固定ロットでエントリーし、SL/TPをpips単位で設定でき、任意でトレーリングストップも適用可能です。視覚要素として、チャート上に色分けされたラインおよびラベル付きスイングポイントを表示し、戦略分析を強化します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5でスマート取引マネージャーを構築する:損益分岐点、トレーリングストップ、部分決済を自動化する MQL5でスマート取引マネージャーを構築する:損益分岐点、トレーリングストップ、部分決済を自動化する
「スマート取引マネージャー」エキスパートアドバイザー(EA)をMQL5で構築し、損益分岐点へのストップロス移動、トレーリングストップ、部分決済などの機能で取引管理を自動化する方法を学びましょう。これは、時間を節約し、取引の一貫性を向上させたいトレーダー向けの、実践的かつステップバイステップのガイドです。