English Русский Deutsch
preview
MetaTrader 5機械学習の設計図(第8回):パージ済み交差検証と試行枝刈りを用いたベイズ最適化によるハイパーパラメータ最適化

MetaTrader 5機械学習の設計図(第8回):パージ済み交差検証と試行枝刈りを用いたベイズ最適化によるハイパーパラメータ最適化

MetaTrader 5トレーディングシステム |
22 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

目次

  1. 金融における機械学習での標準的なHPOの問題点
  2. Optuna - アーキテクチャと基本概念
  3. データ仕様と_WeightedEstimator
  4. FinancialModelSuggester
  5. 目的関数 - 枝刈りを組み込んだパージ済みK分割
  6. 金融タスクを考慮した枝刈り
  7. オーケストレーションとストレージ
  8. Optunaのスタディ(Study)からScikit-Learnのcv_results_への変換
  9. 可視化
  10. 実践的な考慮事項
  11. 結論
  12. 添付ファイル


はじめに

あなたは、トリプルバリアラベルまたはメタラベルを用いた金融機械学習(金融ML)分類器を構築しており、探索そのものを過学習の製造装置にしてしまわずに、ハイパーパラメータを公平に選択したいと考えています。この用途において、標準的な手法には3つの具体的な問題があります。まず、GridSearchCVRandomizedSearchCVは過去の試行結果を学習しないため、すでに性能が低いと判明しているハイパーパラメータ領域を何度も探索してしまい、計算資源を無駄に消費します。次に、有望でない設定であっても、計算コストの高いPurgedKFoldの最初のフォールドで打ち切ることができないため、最初のフォールドだけで失敗が明らかになっていても、すべてのフォールドを最後まで実行する必要があります。さらに、金融データにおける前提条件との統合も十分ではありません。具体的には、PurgedKFoldを唯一有効な分割手法として利用すること、学習時と評価時で異なるサンプル重みを使用すること、そして長時間の探索がクラッシュ後にも再開でき、並列ワーカーにも対応できる永続ストレージを利用することが自然にサポートされていません。その結果として、計算資源が浪費されるだけでなく、パージ境界をまたぐ情報リークによってアウトオブサンプル評価に偏りが生じ、さらに実験が中断されるたびに最初からやり直さなければならないという脆弱な運用になってしまいます。

本記事では、この問題に対する実践的な代替手法として、Optuna(TPEサンプラー、枝刈り、SQLiteストレージ)を、本連載でこれまで確立した金融向け交差検証およびサンプル重みの設計にネイティブに統合する方法をご紹介します。この記事を読み終える頃には、以下の5つの要素から構成される、実際に実行可能なハイパーパラメータ最適化(HPO)の枠組みを構築できるようになります。

  1. PurgedKFoldによる交差検証を実行し、収益帰属重みスコアリングを用いて評価をおこない、枝刈りのためにフォールドごとの結果を報告する目的関数
  2. scikit-learnの分布指定をtrial.suggest_*()呼び出しへ変換するとともに、サンプル重みの方式と減衰率も同時に最適化するパラメータ変換レイヤーFinancialModelSuggester
  3. エントロピーに基づく経済的ベースラインと、市場レジームに応じてスケールするボラティリティ許容値を適用する金融向けPruner TradingModelPruner
  4. SQLiteストレージを利用して探索結果を永続化し、中断からの再開および並列ワーカーをサポートするオーケストレーターoptimize_trading_model
  5. 既存のscikit-learn分析ツールとの互換性を確保するため、Optunaのスタディをcv_results_形式へ変換するコンバータ

出力成果物は、永続化されたスタディ、再学習(refit)済みのbest_estimator_(チューニング済みのベースモデルをラップする_WeightedEstimator)、および各フォールドのスコアを格納したcv_results_ DataFrameです。これらは、パイプラインの他の部分で使用されているものと同じ下流の診断処理にそのまま利用できます。

本記事は、連載「MetaTrader 5機械学習の設計図」の第8.1回です。これまでの記事では、本システムが依存する各コンポーネントを構築してきました。第1回ではバー単位でのデータリーケージを解消し、第2回ではトリプルバリア法とメタラベルを導入しました。第3回ではトレンドスキャニングラベルを追加し、第4回ではラベルの同時性に対応するとともに平均独自性の重みを導入しました。第5回ではシーケンシャルブートストラッピングを導入し、学習用重みとスコアリング用重みを分離するという設計方針を確立しました。第6回ではキャッシュ基盤を構築し、第7回 ではこれらすべてを再現可能な本番パイプラインとして統合しました。本記事で説明するコンポーネントの本番パイプラインへの統合(clf_hyper_fitラッパーやキャッシュ機構を含む)については、第8.2回で扱います。


1.  金融における機械学習での標準的なHPOの問題点

1.1 なぜハイパーパラメータ最適化(HPO)は金融分野でより重要なのか

多くの機械学習分野では、適切にチューニングされたモデルと良質な特徴量があれば、十分に許容できる結果が得られます。しかし金融分野では、シグナル対ノイズ比が低く、非定常性が常態であり、過学習のコストは資本という形で測定されます。López de Pradoが強調しているように、パイプライン内の自由度はすべて過学習の機会になります。ハイパーパラメータも自由度の一種であり、それらを探索する手法によって、最終的な設定が汎化能力を持つのか、それとも単にデータを記憶しているだけなのかが決まります。

この問題は、金融データの構造によってさらに深刻になります。第4回で説明したように、トリプルバリアラベルは時間的に重複します。その結果生じるラベルの同時性により、標準的なk分割CVでは学習データとテストデータの境界をまたいだ情報リークが発生し、その結果得られるスコアは楽観的なバイアスを持ちます。HPOツールは、試行間で有効な比較をおこなうために、PurgedKFoldとネイティブに連携できる必要があります。

1.2 scikit-learnの限界

GridSearchCVは指数的にスケールします。たとえば、4つのハイパーパラメータについて、それぞれ5種類の値を持つランダムフォレストを探索する場合、組み合わせ数は625通りになります。さらに5分割交差検証を適用すると、3,125回のモデル学習が必要になります。RandomizedSearchCVは計算コストを削減しますが、探索は依然として情報を活用しません。つまり、各試行は独立しています。どちらの手法も、早期停止、試行間学習、永続的なストレージには対応していません。

 

制限事項

金融MLへの影響

 1. 試行間学習がない ハイパーパラメータ空間の悪い領域に対して計算リソースが浪費される
 2. 不良な試行の早期停止がない 初期フォールドですでに低スコアが示されている場合でも、すべてのフォールドが実行される
 3. 永続ストレージがない 中断された探索を再開できない。また、実験間の比較には手作業による管理が必要になる
 4. 硬直的なCV統合 枝刈りのためのフォールド単位のレポート作成には、標準CVインターフェースを壊すような回避策が必要になる

1.3 重要な境界:HPOとストラテジー最適化の違い

先に進む前に、明確に区別すべき境界があります。インテリジェントな探索手法はHPOには適していますが、ストラテジーパラメータの最適化には 有害です。Timothy Mastersはこの点を正確に指摘しています。TPEサンプラーや遺伝的アルゴリズムは、インサンプルのフィットネス曲面における大域的最適解を効率的に探索できます。しかし、金融データでは、その曲面の大部分はノイズです。探索能力が高いほど、得られる結果はより過学習したものになります。

ルールは単純です。目的関数がラベル付きデータ上の統計的指標(例:交差検証済み精度、トリプルバリアラベルに対するlog-loss)である場合は、Optunaを使用します。一方、目的関数が過去のエクイティカーブに基づく金融指標(例:シャープレシオ、ドローダウン)である場合は、Optunaを使用してはいけません。HPOにおけるOptunaの優位性は、ストラテジー最適化において危険性を持つ理由と同じ性質に由来します。それは「最適解をあまりにも確実に見つけてしまう」ことです。


2.  Optuna - アーキテクチャと基本概念

2.1 Optunaの仕組み

Optunaは、以下の3つの概念を中心に構築されています。

  • スタディ(Study):完全な最適化セッションを表します。最大化または最小化という方向性を持ち、すべての結果を保存します。SQLiteストレージを使用すると、スタディはPythonセッションをまたいで永続化されます。これは、第6回で確立した時間を考慮した再現性の考え方と完全に一致します。
  • 試行(Trial):1つのハイパーパラメータ設定に対する単一の評価です。各試行は値を提案し、目的関数を評価し、その結果を報告します。
  • サンプラー(Sampler):次に試行する値を選択するアルゴリズムです。デフォルトではTPE (Tree-structured Parzen Estimator)が使用されます。これは、完了済みの試行から学習するベイズ最適化手法です。

scikit-learnのツールとの重要な違いは、Optunaが完了済みの試行から学習する点です。TPEサンプラーは、ハイパーパラメータと目的関数の関係をモデル化し、徐々に有望な領域へ探索範囲を集中させます。その結果、後続の試行では、既知の良好な結果に近い設定を探索する可能性が高くなります。

2.2 枝刈り:ハイパーバンドプルーナー

枝刈りとは、中間結果から競争力がないと判断された試行を、完了前に停止する仕組みです。5分割パージ済み交差検証環境では、最初のフォールドで低いスコアが出た場合、残りのフォールドをスキップできます。これにより、総計算量を直接削減できます。

金融CVでは、HyperbandPrunerをまず第一の選択肢とすべきです。1つの試行あたり5〜10フォールド程度しか存在しない場合、多くのプルーナーはほとんど情報を利用できません。これは、それらが試行内部で動作するためです。一方、Hyperbandは試行で動作します。Hyperbandは「この試行は、このステップ時点で他の試行と比べて悪い性能か」という問いではなく、リソース配分の問題として「固定された計算予算の中で、どの試行により多くのフォールドを割り当てるべきか」のように考えます。

ブラケットの仕組み

コードに記載されている正確なパラメータ(min_resource=1max_resource=5cv=5の場合)、reduction_factor=3(η=3))を使用すると、ブラケットの数は次のようになります。

    s_max = ⌊log₃(5/1)⌋ = ⌊1.46⌋ = 1

したがって、2つのブラケット(s=0とs=1)が試行予算全体に対して同時に実行されます。これは、2つの並行レースとして考えることができます。

ブラケットs=1 - 攻撃的ブラケット:3つの試行が開始されます。それぞれは最初にフォールド1のみで評価されます。上位の⌈3/3⌉ = 1つの試行だけがフォールド2〜5へ進みます。残り2つは、1フォールドの評価後に枝刈りされます。総フォールド評価数:3×1 + 1×4 = 7。

ブラケット s=0 - 保守的ブラケット2つの試行が開始され、両方とも無条件でフォールド5まで実行されます。このブラケットでは、中間スコアに関係なく枝刈りは発生しません。総フォールド評価数:2×5 = 10。

保守的ブラケットは単なるバックアップではありません。これは並行して実行される、保証付きのヘッジです。たとえば、あるハイパーパラメータ設定がフォールド1では悪く見えたとしても(そのフォールドが金融危機期間を含んでいる可能性があります)、実際には最良の設定である場合があります。そのような設定でも、保守的ブラケットによって必ず完全評価を受けます。この点が、HyperbandとSuccessiveHalvingPrunerを区別する重要な部分です。SuccessiveHalvingPrunerは単独ではブラケットs=1のみを実行する仕組みであり、その試行は完全に破棄される可能性があります。

 

ブラケットs=1(攻撃的)

ブラケットs=0(保守的)

 開始試行数 3 2
 フォールド1後に枝刈り 2 0
 全フォールド完了 1 2
 総フォールド評価数 7 10

HyperbandPrunerブラケット構造図1:HyperbandPrunerブラケット構造

HyperbandPrunerの上にカスタムプルーナーを構築すべきか

いいえ。構築すべきではありません。むしろラップすることで動作を壊す可能性があります。HyperbandPrunerは、試行作成時に割り当てられるブラケット情報によって内部状態を管理しています。trial.should_prune()が呼び出されると、Hyperbandはこの試行が所属するブラケット、 現在いる段階(rung)、その段階における試行群の上位1/ηに入っているかどうかを確認します。つまり、これは単純な閾値判定ではなく、状態を持つトーナメント管理システムです。prune()をサブクラス化し、そこに金融ルールを追加すると、2つの失敗モードが発生します。1つ目は、独自のルールによって、Hyperbandが保守的ブラケットに割り当てていた試行を終了させてしまう可能性があることです(これにより完全評価の保証が破壊されます)。2つ目は、Hyperbandのrung記録が不整合になることです。これは、独自ルールによって枝刈りされた試行が、本来予定されていたrungで比較されないためであり、その結果、その後のすべての試行に対する中央値比較が破損します。

TradingModelPrunerが機能する理由は、それがMedianPrunerをラップしているということです。MedianPrunerには、破損する可能性のあるブラケット状態が存在しません。HyperbandPrunerを使用する場合に金融ドメイン固有のロジックを追加する正しい方法は、trial.should_prune()を呼び出す前に目的関数内にチェックを配置することです。

for fold_idx, (train_idx, val_idx) in enumerate(cv.split(X, y)):
    # ... fit and score ...
    fold_scores.append(score)

    # 1. Financial domain check fires first — before Hyperband's bracket logic
    if score < min_score_threshold:
        trial.set_user_attr("pruned_reason", "below_baseline")
        raise TrialPruned(f"Below economic baseline at fold {fold_idx}")

    # 2. Hyperband bracket management — rung comparison across trials
    trial.report(score, step=fold_idx)
    if trial.should_prune():
        raise TrialPruned()

金融ロジックはHyperbandへ報告する前に実行されるため、その試行は単にそのrungには現れず、Hyperbandの内部状態は一貫性を維持します。2つの仕組みは明確に分離されています。

from optuna.pruners import HyperbandPruner

pruner = HyperbandPruner(
    min_resource=1,
    max_resource=n_splits,
    reduction_factor=3,
)

2.3 Define-by-Run API

Optunaでは、探索空間を目的関数の内部で指定します。これにより、条件付きハイパーパラメータ空間を利用できるようになります。また、これによってFinancialModelSuggesterは、ベースモデルのパラメータと並行してweight_schemeweight_decayweight_linearを追加でき、モデルと重み付け戦略を同時に最適化することが可能になります。

2.4 永続ストレージと再開機能

Optunaはスタディの結果をSQLite(またはPostgreSQL、MySQL)に保存します。クラッシュやタイムアウトが発生した場合でも、load_if_exists=Trueを使用することで、最後に完了した試行から処理を再開できます。これは第7回で確立した再現性の原則と一致します。つまり、すべての実験は再開可能であり、監査可能であり、再実行した際には同一の結果を生成できなければなりません。複数のワーカーは、同一のスタディに対して試行を並列実行できます。その際、30秒のSQLiteタイムアウトによってロック競合が防止されます。


3.  データ仕様と_WeightedEstimator

このシステム内のすべてのコンポーネントは、第7回で構築された本番パイプラインから継承された共通のデータコントラクトを共有しています。

  • X:特徴量DataFrame。特徴量エンジニアリングパイプラインによって生成され、DatetimeIndexを持ちます。
  • y:Xと整列したターゲットSeries。第2回および第3回で導入したトリプルバリアラベルまたはトレンドスキャニングラベルです。
  • events:イベント時刻をインデックスとするDataFrame。最低限、t1(バリアタッチ時刻) w(収益帰属重み |return|) tW第4回で導入した独自性ベースの重み)を含みます。
  • data_index:完全なバーデータセットのDatetimeIndex。 _WeightedEstimatorが正しいバー集合上で重みを計算するために使用します。

3.1 2つの異なる重みの役割

コードを説明する前に、重みの扱いについて明確にする必要があります。これは、Pradoの例では異なる目的を持つ2つの量が混同されているためです。この区別は、第5回で導入したml_cross_val_scores_allの、sample_weight_trainsample_weight_scoreを分離する設計によって先に示されています。

平均独自性の重み(events['tW']) は、「この観測値は、どれだけ独立した情報を提供しているか」という問いに答えます。これはestimator.fit()に使用します。目的は、重複している観測値がモデルによって暗黙的に過大評価されることを防ぐことです。これは、第4回で特定した同時性の問題に対応します。

収益帰属重み(events['w'])は、「この観測値に対する正しい、または誤った予測は、P&Lにどれだけ影響するか」という問いに答えます。これは検証用スコアラーに使用します。これがない場合、CVメトリクスは、値動きのない日の予測と大きな変動が発生した日の予測を同等に扱います。その結果、気づかれにくい形で試行間比較を歪めます。

# Fitting: _WeightedEstimator applies tW internally via scheme='uniqueness'
fit = clone(model).fit(X_train, y_train)

# Scoring: return-attribution weights from events['w']
w_val = events['w'].iloc[val_idx].to_numpy()
score = -log_loss(y_val, y_prob, sample_weight=w_val)

Pradoは単一の結合重み(独自性 × |return|)を計算し、それを学習とスコアリングの両方に使用します。これは妥当な近似です。しかし、上記のように重みを分離して使用する方が、各重みが本来担当すべき役割に対応しているため、より厳密に正しい方法です。さらに、重みスキーム自体(unweighteduniquenessreturn)は、FinancialModelSuggesterがモデルパラメータと同時に最適化するハイパーパラメータです。

戦略の種類について重要な注意点があります。トレンドフォロー型モデルでは、学習時に独自性の重みではなく、リターン属性の重みを使用した方が有効な場合があります。平均独自性はラベルの重複をペナルティとして扱います。しかし、トレンド市場では、ラベルは意図的に長期間存在し、大きく重複します。そのため、独自性の重みで学習されたトレンドモデルは、本来利用すべき条件そのものを信用しないように学習してしまいます。収益帰属重みは重複について何も判断しません。単純に大きな値動きにより大きな重みを与えます。weight_schemeハイパーパラメータによって、この選択自体を探索可能な次元として扱うことができます。

3.2 _WeightedEstimator

_WeightedEstimatorは、(afml.production.model_developmentから提供される)scikit-learn互換のラッパーです。重み計算処理を完全にカプセル化します。これは、ベース推定器、events DataFrame、完全なバーデータのdata_index、Optunaによってサンプリングされた重みスキームのパラメータを受け取ります。fit(X_train, y_train)が明示的なsample_weight引数なしで呼び出された場合、_WeightedEstimator は内部でdata_indexeventsを使用して適切な重みを計算し、その重みをベース推定器へ渡します。この設計により、目的関数ループの内部から重み計算処理を分離でき、重みスキームを第一級のハイパーパラメータとして扱うことが可能になります。


4.  FinancialModelSuggester

FinancialModelSuggesterは、scikit-learn形式のパラメータ分布をtrial.suggest_*()の呼び出しへ変換し、サンプリングされたパラメータを用いて_WeightedEstimatorを構成します。このクラスは、対になる2つのメソッドを提供します。(1) 目的関数内で確率的に使用するためのメソッドsuggest_and_apply、および (2) study.best_paramsから決定論的にモデルを再構築するためのメソッドapply_from_paramsです。中央のWEIGHT_KEYSレジストリによって、両方のメソッドにおいて重みハイパーパラメータとモデルハイパーパラメータが分離されます。これにより、apply_from_paramsで実行される検証処理では、必要な場合にのみ、重み関連キーをベースモデルが受け入れるパラメータと照合するようになっています。

class FinancialModelSuggester:
    """
    Translates Scikit-Learn style distribution dictionaries into
    Optuna trial suggestions for rigorous statistical HPT.

    Two core methods form a dual pair:
        suggest_and_apply  — Trial → params → model  (stochastic, used in objective)
        apply_from_params  — params → model           (deterministic, used for refit)
    """

    # Central registry: separates weight keys from model keys in both methods
    WEIGHT_KEYS = frozenset({"weight_scheme", "weight_decay", "weight_linear"})

    @classmethod
    def suggest_and_apply(
        cls,
        trial: optuna.Trial,
        base_model,
        param_distributions: dict,
        events: pd.DataFrame,
        data_index: pd.DatetimeIndex,
    ):
        # 1. Suggest weight hyperparameters — optimised jointly with the model
        scheme = trial.suggest_categorical("weight_scheme",
                                           ["unweighted", "uniqueness", "return"])
        decay = trial.suggest_float("weight_decay", 0.1, 1.0)
        linear = trial.suggest_categorical("weight_linear", [True, False])

        # 2. Suggest base model hyperparameters from scikit-learn-style distributions
        sampled_params = {}
        for name, dist in param_distributions.items():
            if isinstance(dist, list):
                sampled_params[name] = trial.suggest_categorical(name, dist)
            elif hasattr(dist, 'ppf'):                         # scipy.stats distribution
                low, high = dist.support()
                if dist.dist.name == "randint":
                    sampled_params[name] = trial.suggest_int(name, int(low), int(high))
                else:
                    is_log = dist.dist.name in ['reciprocal', 'loguniform']
                    sampled_params[name] = trial.suggest_float(name, low, high, log=is_log)
            elif isinstance(dist, range):
                try:
                    sampled_params[name] = trial.suggest_int(name, dist.start, dist.stop - 1)
                except AttributeError:
                    low, high = dist.support()
                    sampled_params[name] = trial.suggest_int(name, int(low), int(high))
            else:
                sampled_params[name] = dist

        # 3. Clone and configure — _WeightedEstimator handles weight computation internally
        new_base = clone(base_model)
        new_base.set_params(**sampled_params)

        return _WeightedEstimator(
            base_estimator=new_base,
            events=events,
            data_index=data_index,
            scheme=scheme,
            decay=decay,
            linear=linear,
        )

    @classmethod
    def apply_from_params(
        cls,
        params: dict,
        base_model,
        events: pd.DataFrame,
        data_index: pd.DatetimeIndex,
    ) -> "_WeightedEstimator":
        """
        Reconstruct a WeightedEstimator from a flat params dict (e.g. study.best_params).
        Validates model params against the base model's accepted parameter set.
        """
        weight_params = {k: params[k] for k in cls.WEIGHT_KEYS if k in params}
        model_params = {k: v for k, v in params.items() if k not in cls.WEIGHT_KEYS}

        # Defensive validation: catch invalid params before refit
        valid_keys = set(base_model.get_params().keys())
        invalid = set(model_params) - valid_keys
        if invalid:
            raise ValueError(
                f"Parameters {invalid} are not valid for "
                f"{type(base_model).__name__}. Valid: {sorted(valid_keys)}"
            )

        new_base = clone(base_model)
        new_base.set_params(**model_params)

        return _WeightedEstimator(
            base_estimator=new_base,
            events=events,
            data_index=data_index,
            scheme=weight_params.get("weight_scheme", "unweighted"),
            decay=weight_params.get("weight_decay", 1.0),
            linear=weight_params.get("weight_linear", False),
        )

    @classmethod
    def get_search_space(cls, model_name: str):
        """
        Returns curated parameter distributions for financial models.
        Note min_weight_fraction_leaf for random forests: requires each leaf to account
        for at least a fraction of total sample weight. This interacts directly with
        return-attribution weights and is a stronger regulariser than min_samples_leaf
        when weights vary widely across market regimes.
        """
        spaces = {
            "random_forest": {
                "n_estimators":             range(100, 1000),
                "max_depth":                range(3, 7),
                "min_weight_fraction_leaf": stats.uniform(0.025, 0.1),
                "max_features":             ["sqrt", "log2", 0.5, 1.0],
                "ccp_alpha":                stats.loguniform(1e-5, 1e-2),
            },
            "xgboost": {
                "n_estimators":     range(100, 1000),
                "learning_rate":    stats.loguniform(1e-3, 0.1),
                "max_depth":        range(2, 8),
                "subsample":        stats.uniform(0.6, 0.4),
                "colsample_bytree": stats.uniform(0.6, 0.4),
                "gamma":            stats.uniform(0, 5),
            },
        }
        return spaces.get(model_name.lower(), {})


5.  目的関数 - 枝刈りを組み込んだパージ済みK分割

目的関数は、各コンポーネントを統合する役割を担います。ここでは、第4回で導入したPurgedKFoldによる交差検証を実行し、検証メトリクスに収益帰属重みを適用するとともに、各フォールド終了後にtrial.report()を通じてフォールド単位のスコアをHyperbandPrunerへ報告します。このフォールド単位の直接的なレポート機能こそが、scikit-learnのGridSearchCVでは、大幅な回避策なしには実現できない重要な機能です。

ここで注目すべき点が2つあります。1つ目は、clone(model).fit(X_train, y_train)を明示的なsample_weight引数なしで呼び出していることです。これは正しい実装です。_WeightedEstimatorが、この試行でサンプリングされた重みスキームに基づいて学習用の重みを内部で計算し、自動的に適用するためです。2つ目は、events['w']から取得するw_valは、学習時にどの重みスキームが選択されたかに関係なく、スコアリングには常に収益帰属重みとして使用されることです。これは、予測を常に経済的重要性に基づいて評価するためです。この役割の分離は、第5回ml_cross_val_scores_allで採用したsample_weight_trainsample_weight_scoreを分離する設計方針と対応しています。

def optimize_trading_model_with_pruning(
    trial: optuna.Trial,
    X, y, events, data_index,
    classifier,
    param_distributions: dict,
    n_splits: int = 5,
    metric: str = "neg_log_loss",
):
    """
    Objective function for tuning models using Purged K-Fold cross-validation.
    Uses separate weights for fitting (_WeightedEstimator) and scoring (events['w']),
    mirroring the sample_weight_train / sample_weight_score convention from Part 5.
    """
    suggester = FinancialModelSuggester()
    model = suggester.suggest_and_apply(
        trial, classifier, param_distributions, events, data_index
    )

    t1 = events['t1']
    cv = PurgedKFold(n_splits=n_splits, t1=t1, pct_embargo=0.01)

    # Convert once before the loop — avoids repeated pandas overhead per fold
    # and ensures val_idx (numpy integer array from PurgedKFold) indexes correctly
    w_score = events['w'].to_numpy()

    fold_scores = []

    for fold_idx, (train_idx, val_idx) in enumerate(cv.split(X, y)):
        X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
        y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]

        # _WeightedEstimator applies training weights (uniqueness or return) internally
        fit = clone(model).fit(X_train, y_train)

        # Slice pre-converted numpy array — no pandas overhead inside the hot loop
        w_val = w_score[val_idx]

        if metric == "neg_log_loss":
            y_prob = fit.predict_proba(X_val)
            score = -log_loss(y_val, y_prob, sample_weight=w_val)
        else:
            y_pred = fit.predict(X_val)
            score = f1_score(y_val, y_pred, sample_weight=w_val)

        fold_scores.append(score)

        # Financial baseline check fires before Hyperband's bracket logic
        # (only active when TradingModelPruner is not the pruner)
        trial.report(score, step=fold_idx)

        if trial.should_prune():
            avg_score_so_far = np.mean(fold_scores)
            trial.set_user_attr("pruned_at_fold", fold_idx)
            trial.set_user_attr("score_when_pruned", avg_score_so_far)
            trial.set_user_attr("total_folds_attempted", len(fold_scores))
            raise TrialPruned(f"Pruned at fold {fold_idx}. Avg: {avg_score_so_far:.4f}")

    final_score = np.mean(fold_scores)
    trial.set_user_attr("fold_scores", fold_scores)
    trial.set_user_attr("score_std", np.std(fold_scores))
    return final_score

trial.user_attrs["fold_scores"]に保存されるフォールドごとのスコアは、単なる診断用メタデータではありません。これらは、 check_for_overfittingコールバックとcv_results_ DataFrame内のフォールド単位の列を支える重要なデータです。5分割のPurged CVにおいてscore_stdが高い場合、それはモデルが真に汎化可能なシグナルではなく、特定の市場レジームに固有のパターンを捉えていることを示唆している場合が少なくありません。これは、第7回で説明したマルチレジームバーサンプリングが明らかにすることを目的としている、まさにそのレジーム感応性です。


6.  金融タスクを考慮した枝刈り

TradingModelPrunerMedianPrunerを拡張し、金融的な観点に基づく3つのルールを追加したプルーナーです。これはHyperbandPrunerの万能な代替ではなく、有効に機能する条件範囲が明確にあります。MedianPrunerをラップしているため、最初のn_startup_trials=10件の完了した試行ではまったく動作せず、その後さらに20〜30件程度の試行を経て初めて信頼できる比較が可能になります。そのため、30試行未満の小規模なスタディでは、HyperbandPrunerよりも計算量削減効果は小さくなります。TradingModelPrunerがより適した選択となる条件は、次の4つです。

十分に理解された金融商品に対して、大規模な試行予算を使用する場合:既知の特性を持つ金融商品に対して100試行以上を実行すると、妥当なlog-lossの下限値がどの程度かについて経験的な基準が得られます。その下限を大幅に下回るスコアがフォールド1終了時点で得られた試行は、レジーム依存で性能が低いモデルではなく、単に壊れた構成である可能性が高くなります。エントロピーベースラインは、そのような試行を即座かつ無条件に終了させます。HyperbandPrunerにはこの判断はできません。HyperbandPrunerは試行同士の相対順位を比較するだけであり、絶対的な経済的基準とは比較しないためです。例として、3:1のバリア比でキャリブレーションしたEURUSDのトリプルバリアラベルでは、log-lossは一貫して約−0.65付近になります。フォールド1で−0.92を返す試行は、他の試行の状況にかかわらず、直ちに終了させるべきです。

リターン重みの分散が大きいトレンド相場の金融商品:強いトレンド相場では、events['w']の変動係数は大きくなります。少数の大きな値動きを持つ観測値の重みが、その他の観測値より一桁以上大きくなることがあります。TradingModelPrunerは、この変動係数に比例してvolatility_toleranceを調整します。そのため、平均回帰的なフォールドで一時的に低いスコアとなったトレンドモデルであっても、大きなフォールド間スコア変動を許容し、枝刈りをおこないません。一方、HyperbandPrunerは、その変動がモデル品質によるものか、市場構造によるものかを区別せず、相対順位だけで枝刈りをおこないます。たとえば、上昇トレンド期間のGBPJPYについて、週足トリプルバリアラベルで学習したモデルでは、各フォールドが異なるモメンタム局面を含むため、フォールドごとのスコア分散が大きくなります。TradingModelPrunerはこのような状況を許容しますが、HyperbandPrunerでは枝刈りされる可能性があります。

CV期間に既知のレジーム転換が含まれる場合:学習期間に、中央銀行の政策変更、ボラティリティレジームの変化、市場マイクロストラクチャの変化などの構造的な転換点が含まれる場合、あるフォールドだけが他のフォールドと質的に異なる性質を持つことになります。両方のレジームで最も良い性能を示すモデルが、必ずしもフォールド1終了時点で最良に見えるとは限りません。TradingModelPrunerでは、n_warmup_steps=2により、分散ベースの枝刈りが開始されるまで2フォールド分の猶予期間が設けられます。たとえば、2019年から2022年までを対象としたスタディでは、あるフォールドがCOVID期の高ボラティリティを含むことになります。分散ベースの枝刈りを開始する前に2フォールドを評価することで、このフォールドだけを理由に、レジームをまたいで良好に汎化する構成が除外されることを防げます。

HyperbandPrunerによる初回実行後のフォローアップスタディ:実践的なワークフローとしては、まずHyperbandPrunerで50試行程度のスタディを実施し、スコアの分布を把握します。続いて、TradingModelPrunerを用いて150試行程度の集中的なスタディを実施します。最初のスタディによって、multiplierを適切に設定するためのスコア範囲を把握できます。2回目のスタディでは、最初の実行結果を事前知識として持つTPEサンプラーが、すでにノイズフロア付近であることが判明している構成を再び探索することを防げます。

ベースラインエントロピー閾値は、生のラベル分布ではなく、収益帰属重みによって重み付けされたラベル分布を使用します。これは正しいベースラインです。なぜなら、常に多数派クラスを予測するだけの単純な分類器は、クラスを経済的重要性で重み付けした場合、このエントロピー値を達成するためです。また、ボラティリティ許容値は、収益帰属重みの変動係数に比例して決定されます。トレンド相場(高いCV)では許容範囲が広くなり、平均回帰相場(低いCV)では許容範囲が狭くなります。

class TradingModelPruner(MedianPruner):
    """
    Financial-aware pruner that adjusts thresholds based on label entropy
    and return-attribution weighted volatility.
    """
    def __init__(
        self,
        y,
        sample_weight,       # Return-attribution weights: events['w']
        n_startup_trials: int = 10,
        n_warmup_steps: int = 2,
        multiplier: float = 1.15,
    ):
        super().__init__(n_startup_trials=n_startup_trials,
                         n_warmup_steps=n_warmup_steps)

        # Baseline entropy from return-attribution weighted label distribution
        weighted_counts = pd.Series(sample_weight).groupby(y.values).sum()
        probs = weighted_counts / weighted_counts.sum()

        if set(y.unique()) != {0, 1}:
            self.baseline_entropy = -np.sum(probs * np.log(probs))
            self.min_score_threshold = -self.baseline_entropy * multiplier
        else:
            majority_ratio = probs.max()
            self.min_score_threshold = majority_ratio / multiplier

        # Volatility tolerance scales with CV of weights:
        # trending regimes (high CV) → higher fold-score variance is acceptable
        weight_cv = np.std(sample_weight) / np.mean(sample_weight)
        self.volatility_tolerance = 0.1 * (1 + weight_cv)

    def prune(self, study, trial) -> bool:
        step = trial.last_step
        if step is None:
            return False

        if trial.number >= 5 and len(trial.intermediate_values) >= 3:
            # Rule 1: Worse than economically-weighted naive baseline?
            current_score = trial.intermediate_values.get(step)
            if trial.number > 1 and current_score < self.min_score_threshold:
                return True

            # Rule 2: Unstable across recent folds?
            recent_scores = list(trial.intermediate_values.values())[-3:]
            if np.std(recent_scores) > self.volatility_tolerance:
                return True

        # Rule 3: Standard median pruning
        return super().prune(study, trial)

要約すると、判断基準は、n_trials < 30 → HyperbandPrunerを使用、n_trials ≥ 100かつ上記4つの条件のうち少なくとも1つに該当する場合 → TradingModelPrunerを使用、未知の銘柄での最初のスタディ → HyperbandPrunerを使用となります。


7.  オーケストレーションとストレージ

optimize_trading_modelは、スタディを作成し、プルーナーとサンプラーを設定し、SQLiteストレージへ接続するとともに、再学習を実行します。デフォルトのpruner_typeHyperbandPrunerです。スタディの実行前に分類器のn_jobs=1を設定し、再学習後に-1へ戻すことで、オーバーサブスクリプション(超過割り当て)を防止します。study.optimizeが並列ワーカーを実行している場合、各ワーカーはすでに1つのCPUコアを使用しています。その状態で内部の分類器も複数コアを要求すると、入れ子になった並列処理が発生し、スループットは向上するどころか低下します。同じ原則は、第7回で説明したclf_hyper_fitラッパーにも適用されています。

load_if_exists=Trueを指定すると、同じstudy_namedb_pathで呼び出した際に、タイムアウトした実行は最後に完了した試行から再開されます。これを 第6回のキャッシュアーキテクチャと組み合わせることで、中断後にパイプライン全体を再実行した場合でも、キャッシュされた前処理結果がすべて再利用され、Optunaのスタディも中断した地点から正確に再開されます。そのため、計算資源が無駄になることはありません。

def optimize_trading_model(
    classifier,
    X: pd.DataFrame,
    y: pd.Series,
    events: pd.DataFrame,
    data_index: pd.DatetimeIndex,
    param_distributions: dict,
    n_trials: int = 100,
    timeout: int = 3600,
    n_splits: int = 5,
    pruner_type: str = "hyperband",
    metric: str = "neg_log_loss",
    study_name: str = None,
    db_path: str = None,
    random_state: int = 42,
    refit: bool = True,
):
    if pruner_type == "median":
        pruner = TradingModelPruner(y, sample_weight=events.loc[X.index, 'w'])
    elif pruner_type == "hyperband":
        pruner = HyperbandPruner(min_resource=1, max_resource=n_splits,
                                 reduction_factor=3)
    else:
        pruner = SuccessiveHalvingPruner()

    sampler = TPESampler(seed=random_state)
    storage_url = f"sqlite:///{db_path}.db?timeout=30"   # 30s timeout for parallel workers

    try:
        study = optuna.create_study(
            direction="maximize",
            sampler=sampler,
            pruner=pruner,
            study_name=study_name,
            storage=storage_url,
            load_if_exists=True,           # resume from last completed trial
        )

        if hasattr(classifier, 'n_jobs'):
            classifier.set_params(n_jobs=1)       # prevent oversubscription
        if hasattr(classifier, 'random_state'):
            classifier.set_params(random_state=random_state)

        def objective(trial):
            return optimize_trading_model_with_pruning(
                trial, X, y, events, data_index,
                classifier, param_distributions, n_splits, metric
            )

        study.optimize(
            objective,
            n_trials=n_trials,
            timeout=timeout,
            callbacks=[print_best_trial, save_intermediate_results,
                       check_for_overfitting],
        )

        if refit:
            best_model = FinancialModelSuggester.apply_from_params(
                study.best_trial.params, classifier, events, data_index
            )
            if hasattr(best_model, 'n_jobs'):
                best_model.set_params(n_jobs=-1)
            best_model.fit(X, y)
            study.best_estimator_ = best_model

        cv_results = optuna_to_cv_results(study)
        return study, cv_results

    except StorageInternalError as e:
        logger.error(f"Storage Error: {db_path} is locked or unreachable. {e}")
    except Exception as e:
        raise e

3つのコールバックは、目的関数を変更せずに実行状況を把握できるようにします。print_best_trialは、スコアの改善をコンソールへ出力します。save_intermediate_resultsは、完了した各試行をoptuna_results/ディレクトリ内にJSON形式で保存します。これは、第7回で構築したロギング基盤に対応する軽量な監査証跡です。check_for_overfittingは、score_stdが0.3を超えた場合に警告を出力します。


8.  Optunaのスタディ(Study)からscikit-learnのcv_results_への変換

パイプライン内の既存の分析コード(afml.cross_validation.hyper_fit_analysisのハイパーパラメータ分析関数を含む)は、scikit-learn形式cv_results_ DataFrameを前提としています。条件付き探索空間では、疎な行が生成されます。たとえば、xgboostの探索空間には learning_rategammaが含まれますが、これらはランダムフォレストの試行には存在しません。optuna_to_cv_resultsは、完了した試行ごとに1行を持つDataFrameを構築し、存在しないパラメータをNaNで埋めることで、これに対応します。trial.user_attrs["fold_scores"]に保存されたフォールドごとのスコアは、split0_test_scoresplit1_test_scoreなどに展開されます。これにより、scikit-learnのネイティブなCV結果で利用できるものと同じフォールドレベルの一貫性分析を利用できます。

def optuna_to_cv_results(study):
    """Converts an Optuna study into a Scikit-Learn style cv_results_ DataFrame."""
    rows = []
    for trial in study.trials:
        if trial.state != optuna.trial.TrialState.COMPLETE:
            continue       # exclude pruned trials from ranking

        res = {
            "mean_test_score": trial.value,
            "std_test_score": trial.user_attrs.get("score_std", 0),
            "mean_fit_time": (trial.datetime_complete -
                               trial.datetime_start).total_seconds(),
            "params": trial.params,
        }
        for k, v in trial.params.items():
            res[f"param_{k}"] = v

        fold_scores = trial.user_attrs.get("fold_scores", [])
        for i, score in enumerate(fold_scores):
            res[f"split{i}_test_score"] = score

        rows.append(res)

    return pd.DataFrame(rows)


9.  可視化

スタディの完了後は、Optunaの可視化機能群によって、探索プロセスに関する診断的な知見を得ることができます。すべての関数はoptuna.visualizationから利用できるPlotly Figureを返します。以下の図は、合成した金融MLのスタディから生成した代表的な例です。これらのプロットは、ハイパーパラメータ空間に関する、cv_results_ DataFrameだけでは把握できない情報を可視化するため、特に有用です。

plot_optimization_history(study)

試行ごとの目的関数値(点)と、その時点までの最良値(線)を表示します。TPEが目的関数の曲面をモデル化するにつれて、各試行のスコアと最良スコアとのギャップは狭くなっていきます。最良スコアの線が横ばいになれば収束を示し、改善が続いている場合は、さらに試行を実行する価値があることを示します。実際には、4~5個のパラメータを持つランダムフォレストの探索空間では、TPEは通常30~60試行で収束します。

最適化履歴

図2:最適化履歴

plot_intermediate_values(study)

各試行におけるフォールドごとの累積平均を表示し、枝刈りされた試行は途中で終了する短い線として表示されます。これはHyperbandPrunerにとって最も重要な診断プロットです。緑色の線は全フォールドを完了した試行であり、スコア範囲の上位付近に集まる傾向があります。赤色の線は早期に枝刈りされた試行であり、フォールド1で終了します。どの線も枝刈りされていない場合は、reduction_factorを調整する必要があるか、あるいは目的関数の曲面が平坦すぎて、フォールド1のスコアが十分な情報を持っていない可能性があります。

中間値(枝刈り診断)


図3:中間値(枝刈り診断)

plot_param_importances(study)

fANOVAを用いて、各ハイパーパラメータが目的関数の分散にどの程度寄与しているかを推定します。金融MLでは、正則化パラメータ(min_weight_fraction_leafccp_alphamax_depth)が、容量パラメータ(n_estimators)よりも支配的になることが一般的です。これは、学習期間のレジームへの過学習が主要な失敗要因であることを反映しています。weight_schemeの重要度は特に有益です。重要度が高い場合は、独自性の重みと収益帰属重みのどちらで学習するかという選択が、その金融商品や戦略の種類に対して測定可能な差を生み出していることを示します。

パラメータの重要度

図4:パラメータの重要度(fANOVA)

plot_parallel_coordinate(study)

各試行を、目的関数値に応じて色付けした線として、平行座標上に表示します。高性能な試行(緑色)は目に見える帯状に集まり、パラメータ間の相互作用を示します。たとえば、深い決定木は、ccp_alphaによる強いコスト複雑度枝刈りと組み合わせた場合にのみ性能が向上することが分かります。これは、個々のパラメータではなく、どのパラメータの組み合わせが有効であるかを特定するための最も適したプロットです。

図5

図5:平行座標プロット

plot_edf([study_optuna, study_random])

複数のスタディについて、全試行の目的関数値に対する経験累積分布関数を表示します。EDFが右側へシフトしているスタディは、試行全体の分布にわたって、もう一方のスタディを確率的に支配しています。これは、同じ試行数でOptunaがRandomizedSearchCVより優れていることを示すための適切な方法です。単一のbest_scoreだけを比較すると、偶然良い結果を引き当てた影響を受けやすく、そのような比較を唯一の評価基準として使用すべきではありません。EDFは分布全体を示します。

vis.plot_edfoptuna.Studyオブジェクトのリストを受け取ります。一方、RandomizedSearchCVcv_results_ DataFrameを生成するため、変換処理が必要です。randomized_search_to_studyは、cv_results_の各行を、CategoricalDistributionのスタブを用いた完了済みのoptuna.試行 としてラップします。plot_edftrial.valueだけを参照するため、分布の型は重要ではありません。補助関数plot_edf_comparisonは、この変換とトレース名の変更を1回の呼び出しで処理します。

OptunaとRandomizedSearchCVの比較

図6:EDFの比較 - OptunaとRandomizedSearchCV

import optuna.visualization as vis
from optuna.distributions import CategoricalDistribution

fig = vis.plot_optimization_history(study)
fig.show()

fig = vis.plot_intermediate_values(study)   # primary HyperbandPruner diagnostic
fig.show()

fig = vis.plot_param_importances(study)     # weight_scheme importance is informative
fig.show()

fig = vis.plot_parallel_coordinate(study)
fig.show()

def randomized_search_to_study(
    gs,
    study_name: str = "randomized_search_baseline",
) -> optuna.Study:
    """
    Convert a fitted RandomizedSearchCV into an Optuna Study for use
    with vis.plot_edf.

    vis.plot_edf requires optuna.Study objects and reads only trial.value.
    CategoricalDistribution stubs satisfy the API without affecting the plot.
    NaN mean_test_score rows (failed fits) are skipped.
    """
    study = optuna.create_study(direction="maximize", study_name=study_name)
    cv_df = pd.DataFrame(gs.cv_results_)
    param_cols = [c for c in cv_df.columns if c.startswith("param_")]

    for _, row in cv_df.iterrows():
        score = row["mean_test_score"]
        if pd.isna(score):
            continue

        params = {}
        for col in param_cols:
            val = row[col]
            # Skip NaN params from conditional search spaces
            if not (pd.isna(val) if not isinstance(val, str) else False):
                params[col.replace("param_", "")] = val

        distributions = {
            k: CategoricalDistribution([v]) for k, v in params.items()
        }
        trial = optuna.trial.create_trial(
            params=params,
            distributions=distributions,
            value=float(score),
        )
        study.add_trial(trial)

    return study

def plot_edf_comparison(study_optuna, gs_random):
    """
    Compare an Optuna study against a fitted RandomizedSearchCV via EDF.
    Handles the conversion and trace renaming in one call.
    """
    study_random = randomized_search_to_study(gs_random)
    fig = vis.plot_edf([study_optuna, study_random])

    for trace in fig.data:
        if "random" in trace.name.lower():
            trace.name = "RandomizedSearchCV"
        else:
            trace.name = f"Optuna ({study_optuna.study_name})"

    fig.update_layout(
        title="EDF Comparison — Optuna (TPE + Hyperband) vs RandomizedSearchCV"
    )
    return fig

# Usage:
# study, cv_results = optimize_trading_model(...)
# gs = RandomizedSearchCV(...); gs.fit(X, y, sample_weight=events['w'])
# fig = plot_edf_comparison(study, gs)
# fig.show()


10.  実践的な考慮事項

ベース推定器のランダムシードを固定します。これを行わない場合、試行間の性能差はハイパーパラメータの品質ではなく、ランダム性を反映することになります。optimize_trading_modelは、スタディ開始前に分類器のrandom_stateを設定します。これは、weight_schemeのみが異なる試行間の比較において特に重要です。その差は重みスキームによるものでなければならず、ランダムフォレストの確率的な挙動によるものであってはなりません。

根拠のある探索空間の範囲を設定します。たとえば、n_estimatorsを[1, 10000]の範囲にすると、極端な値に対して初期試行が浪費されます。get_search_spaceファクトリは、開始点として調整された分布を提供します。plot_contourが最良領域が現在の範囲の境界にあることを示した場合のみ、範囲を広げてください。

乗法的なパラメータには対数スケールを使用します。learning_rateccp_alphaは複数桁の範囲に及びます。FinancialModelSuggesterdist.dist.nameを確認してloguniformおよびreciprocal分布を識別し、trial.suggest_floatlog=Trueを渡すことで、これを自動的に処理します。

枝刈りの前に、少なくとも1つの猶予フォールドを許可します。金融CVのフォールドは異なる市場レジームをカバーします。TradingModelPrunerは、分散ベースのルールを適用する前にn_warmup_steps=2を適用します。HyperbandPrunerの保守的なブラケットは、構造的に同じ保護を提供します。つまり、各ブラケットで少なくとも1つの試行が必ず完了します。

weight_schemeの重要度を慎重に解釈します。plot_param_importancesweight_schemeを最も重要なパラメータとして示した場合、それは必ずしもそのスキームがモデル品質に最も影響していることを意味しません。それは、他のハイパーパラメータが事前知識によってすでに十分に制約されており、探索空間の改善が必要であることを意味している可能性があります。


結論

optimize_trading_modelを実行すると、再現可能なOptunaのスタディと、3つの即時利用可能で監査可能な成果物が得られます。1つ目は、スタディそのものです(SQLiteに永続化され、load_if_exists=Trueによって再開可能であり、並列ワーカーと互換性があります)。2つ目は、再学習済みのbest_estimator_です(チューニング済みのベースモデルをラップする_WeightedEstimatorであり、予測に利用可能です)。3つ目は、scikit-learn互換のcv_results_ DataFrameです(フォールドごとのスコアとフォールド安定性メタデータを含み、パイプライン全体で使用されているものと同じ下流診断に利用できます)。実務上の利点は、序論で特定した金融分野のボトルネックを解消します。早期の枝刈り(HyperbandPruner、またはキャリブレーションされたベースラインを持つ大規模スタディ向けのTradingModelPruner)は、1つまたは数個のフォールド後に有望でない試行を終了させることで、総計算量を大幅に削減します。永続的なSQLiteストレージと第6回のキャッシュアーキテクチャにより、高コストな前処理を再実行することなく、長時間の実験を再開できます。また、学習用重みとスコアリング用重みを分離することで、モデル学習と評価の両方において経済的重要性が尊重されます。

すぐに実行できる主要な設計上のポイント:

  • トリプルバリアラベルには、内部CVとしてPurgedKFoldのみを使用し、枝刈りのために常にフォールド単位のスコアを報告してください。CPCVはバックテスト用のツールであり、ハイパーパラメータの組み合わせをランキングするために適したスカラー出力を生成しません。
  • 小規模から中規模のスタディ(約30試行未満)または未知の金融商品の初回スタディでは、デフォルトとしてHyperbandPrunerを優先してください。大規模な試行予算(100以上)とキャリブレーションされた経済的ベースラインがある場合は、TradingModelPrunerを使用してください。
  • 学習用の重みスキーム(unweighteduniquenessreturn)をハイパーパラメータとして扱い、第5回のデュアル重みの方針に従って、スコアリングには常に収益帰属重み(events['w'])を使用してください。
  • ランダムシードを固定し、十分な知識に基づいた探索範囲を使用し(ccp_alphalearning_rateのような乗法的パラメータには対数スケールを使用)、フォールド間のレジームの異質性を考慮するため、枝刈り前にグレース期間を設けてください。

最後に、境界線を尊重してください。Optunaの指向性探索は、ラベル付きデータに対する統計的HPOに適したツールです。これは、目的関数(トリプルバリアラベルに対する交差検証済み対数損失)が汎化性能の有効な指標であるため、最適解を信頼性高く発見します。しかし、これは過去のP&Lに基づくストラテジー最適化の安全な代替手段ではありません。そのような場合、同じ信頼性によって、Optunaはランダムサーチよりも危険になり得ます。第8.2回では、このHPOコンターをclf_hyper_fitラッパーを通じて本番パイプラインへ統合し、既存のキャッシュ基盤およびMetaTrader 5へのデプロイ用ONNXエクスポートパスへ接続する方法を示します。


添付ファイル

以下の表では、本記事に添付された各ファイルと、HPOシステムにおける役割について説明します。本番モジュールおよび交差検証モジュールのファイルは第7回と共有されています。optuna_hyper_fitという接頭辞を持つファイルは、本記事で導入されます。

 

ファイル

モジュール

本記事における役割

主要な依存関係

 1. optuna_hyper_fit.py afml.cross_validation メインHPOモジュール。FinancialModelSuggesteroptimize_trading_model_with_pruningTradingModelPruneroptimize_trading_modeloptuna_to_cv_results、および本記事で導入するEDF比較ユーティリティを含みます。 cross_validation.py (PurgedKFold)、model_development.py (_WeightedEstimator)、optuna ≥ 3.0、scikit-learn ≥ 1.3、loguru
 2. model_development.py afml.production _WeightedEstimatorを提供します。これはscikit-learn互換のラッパーであり、学習用サンプルの重み(独自性またはリターン属性)を内部で計算・適用するため、目的関数ループで明示的に処理する必要がありません。また、第7回からのTickDataLoaderおよびより広範なパイプライン基盤も提供します。 utils.py (date_conversion)、MetaTrader5 Pythonパッケージ、scikit-learn、pandas、numpy
 3. utils.py afml.production 本番モジュール全体で共有されるユーティリティ関数を提供します。これには、TickDataLoaderで使用される日付変換ヘルパー、およびパイプラインのエントリーポイントで呼び出されるデータ検証ルーチンが含まれます。 pandas、numpy、MetaTrader5 Pythonパッケージ
 4. cross_validation.py afml.cross_validation 金融HPOにおいて唯一有効な内部CVスプリッターであるPurgedKFoldを提供します。events DataFrameの t1と1%のエンバーゴを使用し、optimize_trading_model_with_pruning内部から直接呼び出されます。また、外側のバックテストループ用のCombinatorialPurgedCVも提供します。これはHPOとは異なるものであり、本記事の関数内部では使用されません。 scikit-learn (BaseCrossValidator)、pandas、numpy
 5. hyper_fit_analysis.py afml.cross_validation optuna_to_cv_resultsによって生成された cv_results_ DataFrameを使用するスタディ後の分析関数を提供します。フォールド単位の一貫性プロット、パラメータ相互作用のサマリー、および本記事が基盤としているデュアル重みスコアリング規約を確立した、第5回ml_cross_val_scores_all関数を含みます。 cross_validation.py (PurgedKFold)、pandas、numpy、scikit-learn、matplotlib


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

添付されたファイル |
afml.zip (575.39 KB)
ラリー・ウィリアムズの『市場の秘密』(第15回):相場環境を用いた「隠れスマッシュデー」反転戦略 ラリー・ウィリアムズの『市場の秘密』(第15回):相場環境を用いた「隠れスマッシュデー」反転戦略
ラリー・ウィリアムズの「隠れスマッシュデー」の反転を自動化するMQL5エキスパートアドバイザー(EA)を構築します。カスタムインジケータから確定済みのシグナルを読み取り、コンテキストフィルタ(Supertrendの方向一致および任意の取引日ルール)を適用します。また、スマッシュバーの構造またはATRを基準としたストップロスモデル、固定またはリスクベースのポジションサイズによってリスクを管理します。その結果、テストおよび拡張に対応した、再現可能なフレームワークを構築できます。
初心者からエキスパートへ:MQL5におけるマルチタイムフレーム構造確認による流動性戦略の強化 初心者からエキスパートへ:MQL5におけるマルチタイムフレーム構造確認による流動性戦略の強化
上位足の流動性構造と下位足の反転パターンが一致すると、次の価格変動の方向性と発生確率の両方に大きな影響を与えます。上位足で形成された構造的な流動性ゾーンと、下位足での明確な反転確認シグナルを組み合わせることで、エントリータイミングの精度とトレード全体の質を向上させることができます。本記事では、上位足の構造的な確認を活用して流動性ベースのトレード戦略を強化する方法と、そのアプローチをMQL5で効果的に実装する方法について解説します。
プライスアクション分析ツールキットの開発(第64回):手動で描画したトレンドラインと自動監視の同期 プライスアクション分析ツールキットの開発(第64回):手動で描画したトレンドラインと自動監視の同期
手動で描画したトレンドラインを監視するには、常にチャートを確認し続ける必要があり、その結果、重要な価格反応を見逃す可能性があります。本記事では、MQL5 において手動で描画したトレンドラインと自動監視ロジックを同期させるトレンドライン監視エキスパートアドバイザーを開発し、価格が監視対象ラインに接近、タッチ、またはブレイクした際にアラートを生成します。
MetaTrader 5における低頻度定量戦略(第1回):OLAP対応データストアの構築 MetaTrader 5における低頻度定量戦略(第1回):OLAP対応データストアの構築
Parquetストレージ、Hive形式のパーティション、そしてDuckDBを基盤とした、定量分析のための実用的なデータパイプラインについて説明します。選択したSQLiteテーブルをParquetへ移行する方法、市場データをデータソース、銘柄、時間足、日付によって構造化する方法、そしてSQLウィンドウ関数を使用してそれをクエリする方法について詳しく説明します。ゴールデンクロスの例では、銘柄横断的なフォワードリターン評価を示します。付属するPythonスクリプトでは、データのダウンロード、変換、実行を処理します。