モデルに比較可能な入力を与える
250という価格はモデルにとって意味がありません。銘柄によって異なり、時間とともに上昇するので、2020年の値は2010年の値とまったく似ません。特徴量エンジニアリングは、生データを時間や資産をまたいで比較できる入力に変えます。価格の代わりにリターン、値幅の代わりにボラティリティ、水準の代わりに標準化したスコアです。良い特徴量はモデルの選択以上に結果を左右し、それぞれはその日に分かっていたことだけを表さなければなりません。
import numpy as np
prices = np.array([100.0, 101.0, 99.0, 102.0, 104.0, 103.0, 106.0])
window = 3
momentum = np.full(len(prices), np.nan)
momentum[window:] = prices[window:] / prices[:-window] - 1 # the return over the last 3 days
print(np.round(momentum, 4)) # nan for the first 3 days: no price 3 days back過去を見る窓
ほとんどの特徴量は後ろ向きの窓、つまり今日を含む直近 n 個の値の統計量です。モメンタムは今日と n 日前を比べ、ローリング・ボラティリティは直近 n 個のリターンの標準偏差で、ローリング・Zスコアは今日が窓の平均から窓の標準偏差いくつ分離れているかを表します。sliding_window_view を使えば、すべての窓を行列の行として得て、軸に沿って統計量を計算でき、ループは要りません。最初の n - 1 日は完全な窓がなく nan のままです。
import numpy as np
prices = np.array([100.0, 101.0, 99.0, 102.0, 104.0, 103.0, 106.0])
returns = np.diff(prices) / prices[:-1]
window = 3
windows = np.lib.stride_tricks.sliding_window_view(returns, window) # every 3-day window as a row
vol = np.full(len(returns), np.nan)
vol[window - 1:] = windows.std(axis=1, ddof=1)
print(np.round(vol, 4)) # nan nan then one value per complete window
p_windows = np.lib.stride_tricks.sliding_window_view(prices, window)
z = np.full(len(prices), np.nan)
z[window - 1:] = (prices[window - 1:] - p_windows.mean(axis=1)) / p_windows.std(axis=1, ddof=1)
print(np.round(z, 3)) # how unusual each price is for its last 3 days# Features put prices of different stocks on the same footing
import numpy as np
cheap = np.array([5.0, 5.2, 5.1, 5.4, 5.5])
dear = np.array([500.0, 520.0, 510.0, 540.0, 550.0]) # ten times the price, the same movements
print(cheap[-1] / cheap[-3] - 1, dear[-1] / dear[-3] - 1) # identical momentum, about 0.0784 for both
print(dear[-1] - dear[-3], cheap[-1] - cheap[-3]) # price changes (40.0 and 0.4) would not be comparable目的変数は未来を見る
モデルが予測するのは目的変数で、通常は先の h 日間のリターン prices[t + h] / prices[t] - 1 です。先を見てよいのはこの列だけで、最後の h 日は未来がまだ起きていないため nan になるので、学習前にその行を除きます。よくある間違いは、中心化した窓、全データで計算した統計量、入力に再利用した将来リターンなど、目的変数を誤って含んでしまう特徴量です。検査は簡単で、t 日の特徴量は後の日を追加しても変わってはいけません。
import numpy as np
def forward_return(prices, horizon):
out = np.full(len(prices), np.nan)
out[:-horizon] = prices[horizon:] / prices[:-horizon] - 1
return out
def momentum(prices, window):
out = np.full(len(prices), np.nan)
out[window:] = prices[window:] / prices[:-window] - 1
return out
prices = np.array([100.0, 101.0, 99.0, 102.0, 104.0, 103.0, 106.0])
print(np.round(momentum(prices, 2), 4)) # looks back: known on each day
print(np.round(forward_return(prices, 2), 4)) # looks forward: the target, nan for the last 2 days
# The prefix test: a feature computed on the first 5 days equals the first 5 values of the full computation
print(np.allclose(momentum(prices[:5], 2), momentum(prices, 2)[:5], equal_nan=True)) # True実は目的変数になっている特徴量に注意予測が良すぎる特徴量は危険信号です。中心化した窓のモメンタム、明日のリターンを含むボラティリティ、標本全体で標準化したZスコアは、どれもバックテストを輝かせ、実運用で失敗します。すべての特徴量は、
t 日の朝に本番で計算できるように作り、上のプレフィックス検査で確かめます。- 価格を比較可能な特徴量に変える:モメンタム
prices[t] / prices[t - n] - 1、ローリング・ボラティリティ、ローリング・Zスコア。いずれも今日を含む後ろ向きの窓 sliding_window_viewが各窓を行として与えるので統計量にループは不要。最初のn - 1日はnanのまま- 将来リターン
prices[t + h] / prices[t] - 1は目的変数であって特徴量ではない。各特徴量をプレフィックス検査で確かめる
次はウォークフォワード検証です。時間が実際に流れる順序でモデルを検証します。
ラボ:採点される内容
momentumでトレンドを測る25点rolling_volで直近のリスクを追う25点rolling_zscoreで今日がどれだけ異常かを示す25点forward_returnで予測対象を定義する25点