σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft Academyロードマップ
Ctrl K
AO
+1.2k 人の学習者
ログイン参加する ↗
ロードマップ
AO
+1.2k 人の学習者
ログイン
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python ロードマップ›モジュール 8 · 確率モデルと機械学習›トピック 8.10·~1 時間上級

ウォークフォワード検証

ロードマップ上の位置モジュール 8/11 · トピック 10/10
8.10 ウォークフォワード検証
—ラボスコア— / 100合計XP—連続日数—

実際に取引したであろうやり方で検証する

通常の機械学習のように学習行とテスト行をランダムに分けると、時系列では破綻します。学習データにテスト日より後の日が含まれ、隣り合う日は相関するため、テストは学習の一部の写しになってしまいます。ウォークフォワード検証は時間を守ります。過去のブロックで学習し、その直後のブロックでテストし、窓をずらして繰り返します。どの予測も未来を一度も見たことのないモデルが出したもので、これがまさにライブ取引の状況です。

import numpy as np

n, train_size, test_size = 10, 4, 2
start = 0
while start + train_size + test_size <= n:
    train = np.arange(start, start + train_size)
    test = np.arange(start + train_size, start + train_size + test_size)
    print("train", train.tolist(), "test", test.tolist())
    start += test_size                                   # slide forward by one test block

ローリングか拡大か、そしてエンバーゴ

2種類あります。ローリング窓は学習の長さを固定し、最も古い行を捨てます。局面変化に適応でき、コストは一定です。拡大窓は常に0行目から始まるので学習データが増え、すべての観測を使えますが、古いデータは陳腐化しているかもしれません。3つ目の要素がギャップ(エンバーゴ)です。特徴量や目的変数が h 日前や先を見る場合、テスト期間の直前の行はそれと情報を共有するので、学習とテストの間の数行を飛ばします。

def walk_forward_splits(n, train_size, test_size, gap=0):
    splits, start = [], 0
    while start + train_size + gap + test_size <= n:
        train = list(range(start, start + train_size))
        test = list(range(start + train_size + gap, start + train_size + gap + test_size))
        splits.append((train, test))
        start += test_size
    return splits

for train, test in walk_forward_splits(12, 5, 2, gap=1):
    print(train, "->", test)                         # the row between them is skipped

フォールドごとに採点する

各フォールドで学習行に当てはめ、テスト行で採点するので、1つの数ではなくフォールドごとの誤差が得られます。そのばらつきが、モデルが時間を通じてどれだけ安定しているかを示します。有用な基準は自明な予測で、ここでは各テスト日に学習期間の平均を使います。誤差をフォールドごとに表示してください。平均では良いがある局面でひどいモデルは、常にそこそこ良いモデルとは違います。

import numpy as np

def walk_forward_splits(n, train_size, test_size):
    out, start = [], 0
    while start + train_size + test_size <= n:
        out.append((np.arange(start, start + train_size), np.arange(start + train_size, start + train_size + test_size)))
        start += test_size
    return out

rng = np.random.default_rng(2)
returns = rng.normal(0.0004, 0.01, size=300)
errors = []
for train, test in walk_forward_splits(len(returns), 100, 50):
    errors.append(np.mean((returns[test] - returns[train].mean()) ** 2))
print(np.round(np.array(errors) * 1e4, 3))                      # squared error in basis points squared, fold by fold

リークを見つける

分割がクリーンなのは、すべての学習行がすべてのテスト行より厳密に前にあるときだけです。train.max() < test.min() の確認は1行で済み、最もよくあるバグを捕まえます。並べ替えられた・シャッフルされたインデックス、1つずれたスライス、デフォルトでシャッフルするライブラリ関数の分割などです。この確認をフォールドを作るコードの中に入れ、リークが起きたら見栄えの良い結果を出す代わりに実行を止めるようにしましょう。

import numpy as np

def leaks_future(train_idx, test_idx):
    return bool(np.max(train_idx) >= np.min(test_idx))

train = np.arange(0, 100)
print(leaks_future(train, np.arange(100, 120)))                     # False: clean
shuffled = np.random.default_rng(1).permutation(120)                # what a shuffling splitter does
print(leaks_future(shuffled[:100], shuffled[100:]))                 # True: training rows after test rows
フォールドが少なくても証拠は増えない重なり合うテストブロックは同じ日を再利用し、各フォールドも結局は同じ1つの市場から来ています。8フォールド中7つで好成績の戦略でも、共通の1つの出来事のおかげかもしれません。平均だけでなくフォールド結果のばらつきも報告し、ウォークフォワードの結果を最良の誠実な推定として扱ってください。それでも実運用ではさらに悪くなります。同じフォールドでモデルを調整しておいて、そのフォールドを証拠として引用してはいけません。
  • ウォークフォワード:過去のブロックで学習し次のブロックでテストして窓をずらす。どのテスト行もその学習行すべての後にある
  • ローリング窓は学習サイズ固定、拡大窓は0行目から。特徴量が複数日にまたがるなら、学習とテストの間にギャップ(エンバーゴ)を置く
  • フォールドを個別に採点してばらつきを見る。train.max() < test.min() を確認する。報告するフォールドでは決して調整しない

次は指値注文板(リミット・オーダーブック)です。価格が実際に形成されるデータ構造です。

ラボ:採点される内容

  • walk_forward_splits で窓を時間方向に動かす25点
  • expanding_splits で履歴をすべて残す25点
  • forecast_mse で予測をフォールドごとに検証する25点
  • leaks_future でリークを見つける25点
エクスプローラー · lab-111-1
▾ lab-111-1
…
bash · ブラウザ内
learner@quantcraft:~/lab-111-1$
Pythonは未読み込みPythonLn 1, Col 1Spaces: 4UTF-8

このトピックをリセットしますか?

lab-111-1 の内容はすべてスターターファイルに戻り、ターミナルも消去されます。現在の解答は失われ、元に戻せません。

リセットされるもの
  • main.py → スターターコード
  • ターミナル履歴とテスト出力
  • 未提出の変更
保持されるもの
  • ラボの最高スコア
  • XPと連続日数
  • 講義の進捗とメモ

コードを実行するにはログインしてください。

レクチャーはすべて無料で読めます。コードの実行、テスト、ラボの提出にはアカウントの作成かログインが必要です。

  • ブラウザ内IDEとbashターミナル。インストール不要
  • 即時採点、XP、ロードマップでの現在地
アカウントを作成ログイン
または
GitHubで続ける