ケリー基準を用いて最適なベッティングサイズを決める


最適なベッティングサイズを決めるには?

本記事では、Stefan Jasen氏によって書かれた"Machine Learning for Algorithmic trading, 2nd edition"のChapter 5, the Kelly Criterionについて勉強した内容をまとめていきます。

1. The optimal size of a bet

ケリー基準では、無限に繰り返される賭けにおいて資本価値の成長率を最大化するようなベットサイズ(optimal $f$)を見つけることを目的としています。このケリー基準の考え方を数式に落とし込むと以下のようになります。

V_n = V_{0}(1+of)^m(1-f)^{n-m}\\
G = lim_{n\rightarrow\infty}\frac{1}{n}log\frac{V_n}{V_0}\\ 
  • $n$ = 賭けの総回数
  • $m$ = 勝った回数
  • $o$ = 1ドルの賭けに勝った場合に手に入る金額
  • $f$ = 現在の資本に対する賭け額の割合
  • $G$ = 資本の成長率
  • $V_{n}$ = n回目時に手元にある金額

一つ目の式は、$n$回目の賭けの結果が出た時点において、手元に残った資本を表しています。二つ目の式は、毎度の賭けにおける資本の成長率を意味していて、資本成長率の幾何平均の対数を取る形になってます。以下では、SymPyを用いてoptimal $f$を計算しています。


#変数を定義
share, odds, probability = sympy.symbols('share odds probability')

#資産の成長率を表す式を定義
G = probability*log(1+odds*share)+(1-probability)*log(1-share)

#Gをshareに関して微分したもの=0を、shareに関して解く
solve(diff(G,share),share)

#出力(期待値/odds)
[(odds*probability -(1- probability))/odds]

#odds=1でshare=f,probability=p,G=yに書き換えた形
f,p = sympy.symbols('f p')
y = p*log(1+f)+(1-p)*log(1-f)
solve(diff(y,f),f)

#出力、これがfに関するG'=0の最適解f。
2*p -1


2. Get data

今回はyahoofinanceからS&P500のdaily close priceを取得します。期間は2001-1-2から2020-12-31です。(使用するデータはJansen氏とは異なります。)


import pandas as pd
import numpy as np
from numpy.linalg import inv
from numpy.random import dirichlet
from sympy import symbols,solve,log,diff
from scipy.optimize import minimize_scalar,newton,minimize
from scipy.integrate import quad
from scipy.stats import norm
import yfinance as yf
import datetime 
import matplotlib.pyplot as plt

GetSPYinfo = yf.Ticker("SPY")
#datetime.daterime()によりDateがDatetimeIndexとして認識される。
#同時にそれ以外のカラムはデータとして認識される。
startDate = datetime.datetime(2001,1,1)
endDate = datetime.datetime(2021,1,1)
df = GetSPYinfo.history(start = startDate, end = endDate)

3. Compute returns & standard deviation

今回は5年移動平均と標準偏差を計算してから、平均値-2σ・平均値・平均値+2σをプロットしていきます。


#年リターンを計算
Annual_returns =df_closed.resample('A').last().pct_change().dropna().to_frame('sp500')

#リターンの5年移動平均および標準偏差を計算
return_params = Annual_returns.rolling(5).agg(['mean','std']).dropna()

#.sp500をつけることで、meanとstdが列ラベルとして認識される。
return_params.sp500

#データフレームreturn_paramsの平均ラベルの列を抜き出して、そこに、平均±2σの列を付け足す。
return_params.sp500[['mean']]
return_ci = (return_params.sp500[['mean']]
               .assign(lower=return_params.sp500['mean'].sub(return_params.sp500['std'].mul(2)))
               .assign(upper=return_params.sp500['mean'].add(return_params.sp500['std'].mul(2))))

#各系列をプロット
plt.plot(return_ci['mean'], color = 'blue')
plt.plot(return_ci['lower'], color = 'orange')
plt.plot(return_ci['upper'], color = 'green')

labels = ['mean','mean-2sigma','mean+2sigma']
plt.legend(labels)

image.png

4. Apply Kelly rule to a single asset

The optimal size of a betにおいて、資産の成長率$G$を表す数式を紹介しました。今からは、$E[G]$を最大化する$f$をscipy.optimizeを用いて計算します。

E[G] =\int log(1+fr)P(r)dr\Leftrightarrow \frac{d}{df}E[G] = \int_{-\infty}^{\infty}\frac{r}{1+fr}P(r)dr = 0
#期待資本成長率を計算する関数を定義
def norm_integral(f,mean,std):
    val, er = quad(lambda s:np.log(1+f*s)*norm.pdf(s,mean,std),mean-3*std, mean+3*std)
    return -val

#期待資本成長率をfで偏微分する関数を定義
def norm_dev_integral(f,mean,std):
    val,er = quad(lambda s:(s/(1+f*s))*norm.pdf(s,mean,std),m-3*std,mean+3*std)
    return val

#期待資本成長率をfに関して最適化する関数を定義
#最適化に用いる(偏微分に用いる)変数f以外の変数をargsで指定
#bounds = [0,2]⇔ 掛け金の比率は0から200%に設定
def get_kelly_share(return_params):
    solution = minimize_scalar(norm_integral,args=(return_params.sp500['mean'],
                                                   return_params.sp500['std']),
                              bounds=[0,2],method='bounded')
    return solution.x

#年率リターンのdfに新たな列fを追加
Annual_returns['optimal f'] = return_params.apply(get_kelly_share, axis=1);

#meanとstdをプロット
return_params.plot(subplots=True,lw=2,figsize=(14,8));

#パフォーマンス評価
#[]だけだとpd.Seriesに変換され、[[]]だとpd.DataFrameに変換される。

print(return_params);
print(Annual_returns);
y = (Annual_returns[['sp500']].assign(kelly=Annual_returns['sp500'].
                                 mul(Annual_returns['optimal f']))).dropna();

#sp500の累積リターンとKelly Criterionを適用した際の累積リターンをプロット
y.add(1).cumprod().sub(1).plot();