σμ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 ロードマップ›モジュール 6 · バックテストと取引コスト›トピック 6.3·~50 分上級

ルックアヘッドバイアス

ロードマップ上の位置モジュール 6/11 · トピック 3/6
6.3 ルックアヘッドバイアス
—ラボスコア— / 100合計XP—連続日数—

知りすぎていること

先読みバイアス(ルックアヘッド)とは、バックテストが判断時点では入手できなかった情報を使ってしまうことです。机上では素晴らしい戦略が実運用で損をする、最も一般的な理由です。明白な1行であることはまれで、位置合わせ(今日の終値のシグナルを同じ終値で売買する)、標本全体で計算した統計量、後から改訂されたデータ、後知恵で選んだ指数などに隠れています。対策は習慣で、判断に使うあらゆる数について、それがいつ分かったかを問うことです。

import numpy as np

rng = np.random.default_rng(5)
returns = rng.normal(0, 0.01, size=250)             # pure noise: no strategy can really make money here

signal = returns.copy()                              # "signal" = the return of the same day
position = np.sign(signal)

biased = np.prod(1 + position * returns) - 1         # trades on the return it is about to earn
honest_position = np.concatenate([[0.0], position[:-1]])
honest = np.prod(1 + honest_position * returns) - 1
print(round(float(biased), 2), round(float(honest), 2))        # 6.0 0.01: a 600% "return" on noise, against 1%

位置合わせ:1日のラグ

位置合わせバイアスの対策はラグで、t 日に計算した値は t + 1 日から使います。配列では先頭にゼロを入れたシフトで、out[t] = signal[t - periods] です。検査は単純です。ノイズの系列には優位性がないので、ノイズで大儲けするルールはズルをしています。同じデータで偏りのある版とラグ付きの版を比べれば、その差がバイアスの大きさです。

# lag_signal: delay a series, with zeros where there is nothing to look back at
import numpy as np

def lag_signal(signal, periods=1):
    out = np.zeros(len(signal))
    if periods < len(signal):
        out[periods:] = signal[:len(signal) - periods]
    return out

s = np.array([1.0, -1.0, 1.0, 1.0, -1.0])
print(lag_signal(s))             # [ 0.  1. -1.  1.  1.]
print(lag_signal(s, 2))          # [ 0.  0.  1. -1.  1.]
# trailing_mean: the mean of the last `window` values, using only today and the past
import numpy as np

def trailing_mean(values, window):
    total = np.cumsum(values)
    out = np.full(len(values), np.nan)
    out[window - 1:] = (total[window - 1:] - np.concatenate([[0.0], total[:len(values) - window]])) / window
    return out

x = np.array([1.0, 2.0, 6.0, 7.0])
print(trailing_mean(x, 2))                         # [ nan 1.5 4.  6.5]
print(trailing_mean(x[:3], 2))                     # [ nan 1.5 4. ]: adding day 4 changed nothing before it

統計量は時間とともに伸びるべき

2つ目の漏れは目立ちません。系列を標本全体の平均と標準偏差で標準化すると、最後にならないと存在しない数を使うことになり、初期の値が未来の情報でスケールされます。過去だけを使い、移動窓(今日までの直近 n 個)か拡大窓(今日までのすべて)にします。この種の特徴量の検査は、最初の k 日だけで計算した結果が、全体を計算した結果の最初の k 個と一致することです。

# Whole-sample z-scores leak the future; expanding ones do not
import numpy as np

x = np.array([1.0, 3.0, 2.0, 8.0, 4.0])
whole = (x - x.mean()) / x.std()

n = np.arange(1, len(x) + 1)
mean = np.cumsum(x) / n
var = np.cumsum(x ** 2) / n - mean ** 2
with np.errstate(divide="ignore", invalid="ignore"):
    expanding = (x - mean) / np.sqrt(var)
expanding[0] = np.nan

print(np.round(whole, 3))
print(np.round(expanding, 3))
# Prefix test: computing on x[:3] gives the first three expanding values, but not the first three whole-sample ones
print(np.round((x[:3] - x[:3].mean()) / x[:3].std(), 3))
未来が隠れるほかの場所後から分割調整された価格、公表日ではなく期末日が付いた財務諸表、今日時点の指数構成銘柄リスト、標本全体で当てはめてから一部でテストしたモデル、テスト期間で調整したハイパーパラメータ。どれも同じ誤りで、分かる前の時点で数を使っています。
  • 先読みバイアスは、情報が分かる前に使うことすべて。典型は位置合わせで、t 日のシグナルは t + 1 日から取引できる
  • 特徴量は過去だけから作る。移動窓か拡大窓を使い、標本全体の平均や標準偏差は使わない
  • プレフィックス検査:最初の k 日での特徴量やバックテストは、全体の最初の k 個と一致するはず。ノイズ系列で儲かってはいけない

次は取引コストです。きれいなバックテストがまず支払うべきものです。

ラボ:採点される内容

  • lag_signal でシグナルを遅らせる25点
  • biased_vs_honest でズルの大きさを測る25点
  • trailing_mean で過去だけを平均する25点
  • expanding_zscore で先読みなしに標準化する25点
エクスプローラー · lab-89-1
▾ lab-89-1
…
bash · ブラウザ内
learner@quantcraft:~/lab-89-1$
Pythonは未読み込みPythonLn 1, Col 1Spaces: 4UTF-8

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

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

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

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

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

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