平均リターンは本物か
バックテストで平均リターンが日次 +0.14% と出ました。その戦略は優れているのか、運が良かっただけか。リターンはノイズを含むので、どの標本にも平均があり、コイン投げの戦略でも半分の確率で正の平均になります。仮説検定は明確な問いを立てます。真の平均が mu0(通常0)だとして、mu0 からこれだけ離れた標本平均はどれほど珍しいか。その答えが p値です。p値が小さいことは、「何も起きていない」という仮説の下ではそのデータが非常に起こりにくいことを意味します。
t統計量は、平均をその不確かさを単位として測ります。平均の不確かさは標準誤差 s / sqrt(n) で、標本が大きくなると小さくなるため、データが多いほど同じ平均が説得力を持ちます。すべてのリターンを定数倍しても平均と標準誤差が同じだけ変わるので t は変わりません。経験則として、|t| が2を超えれば偶然とは考えにくくなります。
import math
import numpy as np
returns = np.array([0.004, -0.002, 0.001, 0.003, -0.004, 0.002, 0.006, -0.001, 0.005, 0.002, 0.003, -0.002])
n = len(returns)
mean = returns.mean()
std_error = returns.std(ddof=1) / math.sqrt(n) # the uncertainty of the mean
t = mean / std_error # testing against mu0 = 0
print(round(mean, 5), round(std_error, 6)) # 0.00142 0.000892 the mean is only 1.6 standard errors from 0
print(round(t, 3)) # 1.589t から p 値へ
帰無仮説の下で、標本が大きければ t は標準正規分布のように振る舞うので、両側p値は0から |t| より離れる確率(どちら向きでも)で、erfc(|t| / sqrt(2)) です。t = 1.59 なら11%で、これほど良い結果は偶然でも9回に1回起こり、本物の優位性があるとは言えません。通常の基準は5%(|t| = 1.96)です。小さな標本の本来の基準は裾の厚いスチューデントのt分布なので、正規分布のp値はやや小さすぎます。30点未満なら本物のt表を使いましょう。
p_value = math.erfc(abs(t) / math.sqrt(2))
print(round(p_value, 4)) # 0.1121 an outcome this good happens by chance about 1 time in 9
print(round(math.erfc(1.96 / math.sqrt(2)), 4)) # 0.05 |t| = 1.96 is the usual 5% limit
# Testing against another value: is the mean different from 0.2% a day?
t_other = (mean - 0.002) / std_error
print(round(t_other, 3), round(math.erfc(abs(t_other) / math.sqrt(2)), 3)) # -0.654 0.513信頼区間と対応のある標本
信頼区間は、はい/いいえの検定より多くを語ります。平均 -/+ 1.96 * 標準誤差 は真の平均を約95%の確率で含む範囲です。区間に0が含まれれば、平均は5%水準で有意ではありません。ここでの区間は日次 -0.03%〜+0.32% で、ゼロをまたぎ、平均に比べて広くなっています。同じ日に測った2つの戦略を比べるには、日次の差を0と検定します。対にすることで、両者が共有するノイズ(市場の良い日と悪い日)が消え、別々に2回検定するよりずっと鋭く優位性を検出できます。
low, high = mean - 1.96 * std_error, mean + 1.96 * std_error
print(round(low, 5), round(high, 5)) # -0.00033 0.00316 the interval contains 0: not significant at 5%
# Paired: the same six days for two strategies
a = np.array([0.004, -0.002, 0.001, 0.003, -0.004, 0.002])
b = np.array([0.002, -0.003, 0.000, 0.001, -0.002, 0.0])
d = a - b
t_paired = d.mean() / (d.std(ddof=1) / math.sqrt(len(d)))
print(round(d.mean(), 5), round(t_paired, 3)) # 0.001 1.581 a daily edge of 0.1%, but only 6 days# Same mean and spread, four times the data: t doubles and the p-value collapses
spread = returns.std(ddof=1)
for n in (12, 48):
t_n = mean / (spread / math.sqrt(n))
print(n, round(t_n, 3), round(math.erfc(abs(t_n) / math.sqrt(2)), 4))
# 12 1.589 0.1121
# 48 3.178 0.0015t = (mean - mu0) / (s / sqrt(n)):平均を標準誤差s / sqrt(n)単位で表す。データが多いほど |t| は大きい- 両側p値は大標本で
erfc(|t| / sqrt(2))。|t| > 1.96 なら p < 5%、区間mean -/+ 1.96 * seが0を含めば有意でない - 同じ日の戦略は日次の差のt統計量で比べる。多くの変種を検定することには注意
次は線形回帰(OLS)です。あるリターンを別のリターンで説明する直線をあてはめます。
ラボ:採点される内容
t_statisticで平均をそのノイズ単位で測る25点two_sided_pで t を p 値に変換する25点confidence_intervalで真の平均を区間で囲む25点paired_t_statisticで2つの戦略を日ごとに比べる25点