過去を再生する
バックテストは、売買ルールが過去のデータでいくら稼いだかを調べます。ベクトル化版は配列全体を一度に扱い、日ごとのループはなく、日ごとのシグナルとポジション、そしてリターンとの掛け算が1回あるだけです。短く、速く、検算しやすいので、アイデアを選別する最初の道具になります。限界(注文も現金も部分約定もない)は、次のトピックが加えるものそのものです。
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03]) # the asset's daily returns
signal = np.array([2.5, 0.4, -1.0, 0.0]) # any score: positive = buy, negative = sell
position = np.sign(signal) # +1, +1, -1, 0
print(position)1日のずれ
今日の終値から計算したシグナルは今日の終値の後でしか取引できないため、稼ぐのは今日ではなく明日のリターンです。配列ではこれはシフトで、strategy[t] = position[t - 1] * returns[t] となり、初日より前のポジションはありません。これを忘れると、戦略はすでに知っているリターンで取引することになり、バックテストは素晴らしく見えます。シフトは1行ですが、この関数全体で最も重要な1行です。
# Yesterday's position earns today's return
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03])
position = np.array([1.0, 1.0, -1.0, 0.0])
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
print(strategy) # [ 0. 0.02 -0.01 -0.03]
print(position * returns) # the same-day version: [ 0.01 0.02 0.01 0. ] looks better, and is cheating資産曲線と統計
日次リターンは複利でつながり、各日の値は初期値に 1 + リターン の累積積を掛けたものです。この資産曲線から、3つの数が結果を要約します。総リターンは最終値から1を引いたもの。シャープ・レシオは日次リターンの平均を標準偏差で割り、年率化のため sqrt(periods) を掛けたもの。最大ドローダウンは過去のピークからの最悪の下落 equity / running max - 1 で、投資家が実際に味わったはずの損失です。
# Equity curve and the three statistics for a short run
import numpy as np
strategy = np.array([0.0, 0.02, -0.01, -0.03, 0.04])
equity = np.cumprod(1 + strategy)
print(np.round(equity, 5))
print(round(float(equity[-1] - 1), 5)) # total return
print(round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 3)) # annualised Sharpe
print(round(float((equity / np.maximum.accumulate(equity) - 1).min()), 5)) # maximum drawdown# A moving-average rule on a simulated price path, with its statistics
import numpy as np
rng = np.random.default_rng(7)
returns = rng.normal(0.0004, 0.01, size=500)
prices = 100 * np.cumprod(1 + returns)
fast = np.convolve(prices, np.ones(10) / 10, mode="valid")[-(len(prices) - 49):] # 10-day mean, aligned
slow = np.convolve(prices, np.ones(50) / 50, mode="valid") # 50-day mean
position = np.zeros(len(prices))
position[49:] = np.sign(fast - slow)
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
equity = np.cumprod(1 + strategy)
drawdown = equity / np.maximum.accumulate(equity) - 1
print(round(float(equity[-1] - 1), 4), round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 4), round(float(drawdown.min()), 4))ベクトル化バックテストは選別であって判決ではない終値で、見えた価格のまま、手数料なしでポジション全体を売買できると仮定しています。実際の取引にはコストやスリッページや制約があり、それらは後続のトピックで扱います。同じ過去データで多数のルールを試せば、偶然よく見えるものが必ず見つかります。有望な結果は、取引する理由ではなく、さらに検証する理由です。
- ポジション=シグナルの符号。戦略リターンは
position[t - 1] * returns[t]で、今日のリターンは昨日の判断のもの - 資産曲線は
start * cumprod(1 + r)。総リターン、年率シャープ、最大ドローダウンで要約する - ベクトル化バックテストは速い選別。コストや約定を無視し、同じ過去データでの多数の試行は運を生む
次はイベント駆動型バックテスターです。現金・注文・約定を伴い、バーを1本ずつ再生します。
ラボ:採点される内容
signal_to_positionでシグナルをポジションに変える25点strategy_returnsで前日のポジションの損益を求める25点equity_curveで日々を複利でつなぐ25点backtest_statsで結果を要約する25点