QuantXとは??
smart trade社が無料で提供しているシステムトレードアルゴリズムをブラウザ上でpythonを使って開発できるプラットフォーム
ケリー基準とは??
一回のトレードに対して全資産の何%を投資すればいいのかを計算式によって教えてくれるもの
使用するアルゴリズムの勝率をP、ペイオフレイシオをRとした時のケリー基準Fは以下の式によって求められる
F = [ ( R + 1 ) × P - 1 ] ÷ R
さて実際に具体的な数値を用いて計算してみましょう。
例えば勝率 P=65%、ペイオフレイシオ R=1.3のトレードアルゴリズムAがあるとする。
これらの値を上式に代入して計算するとFの値は、0.38 と求まる。これは何を意味しているかというと、トレードアルゴリズムAを用いる場合、一回のトレードに対して全資産の38%を投資すれば良い、ということである。
QuantX上で計算するのに必要な情報は??
まずアルゴリズムの勝率Pを計算するためには、勝ちトレードの数と負けトレードの数がが必要である。
さらにペイオフレイシオRは以下の式で表される。
ペイオフレイシオP = \frac{勝ちトレードの平均利益}{負けトレードの平均損失}=\frac{\frac{勝ちトレードによる総利益}{勝ちトレードの数}}{\frac{負けトレードによる総損失}{負けトレードの数}}
以上より、ケリー基準を計算するには
・勝ちトレードの数、負けトレードの数
・勝ちトレードによる総利益、負けトレードによる総損失
の4つの情報が必要である。
##どういう順序でケリー基準を使うのか?
まず最初に、適当な期間に対してbacktestを行います。
その際、最初のbacktestはケリー基準を設定できません(ケリー基準は一つ前のbacktestから算出するものなので)ので、何%の割合でポジションを取ればいいのかという情報がない。
そこで一番最初のbacktestでは、一回で投資する割合に対して適当な数値を設定します。(今回は初期値を10%とした)
そのbacktestの結果からケリー基準を算出して、次に行うbacktestに対してそのケリー基準を当てはめて計算する。
QuantX上での実装
###①まずは初期設定
ここの部分で、アルゴリズムにかける銘柄、使用するデータ、損切り利確ライン等を設定する。
またケリー基準の設定もここで行いますが、
一番最初のbacktestでは、ケリー基準を設定できないので
ctx.kelly_criterion_dict = {}
2回目以降のbacktestでは、一つ前のbacktestの結果から算出されたケリー基準の情報を辞書型で代入していく。
ctx.kelly_criterion_dict = {
'jp.stock.1321': 0.09975970415264362, 'jp.stock.4063': -0.006488478707581729, 'jp.stock.6758': -0.00652575709654466, 'jp.stock.6762': 0.16020129348725792, 'jp.stock.4911': 0.182084824520133, 'jp.stock.4543': -0.065083030319116, 'jp.stock.4503': 0.2504850265906157, 'jp.stock.4519': 0.22928340313706172
}
ここで、ケリー基準がマイナスになっているものはトレードの期待値がマイナスなので、トレードしない。(後ほど詳述します。)
import pandas as pd
import talib as ta
import numpy as np
def initialize(ctx):
# 設定, 6行目から20行目は変えないようにお願いします。
ctx.flag_profit = True #利益確定売りを用いるかTrueなら用いるFalseなら用いない
ctx.flag_loss = True #損切りを用いるかTrueなら用いるFalseなら用いない
ctx.loss_cut = -0.03 #損切りのボーダーマイナス%
ctx.profit_taking = 0.05 #利益確定売りのボーダープラス%
ctx.codes = [
1321,4063,6758,6762,4911,4543,4503,4519, # アルゴリズムにかけたい銘柄を選択する
]
ctx.symbol_list = ["jp.stock.{}".format(code) for code in ctx.codes]
#ここに前回のbacktestで得られたkelly基準を入力する
ctx.kelly_criterion_dict = {}
ctx.configure(
channels={ # 利用チャンネル
"jp.stock": {
"symbols":ctx.symbol_list,
"columns": [
"close_price", # 終値
"close_price_adj" # 終値(株式分割調整後)
]}})
###②シグナルの登録
この部分で、実験したいものや好みのテクニカル指標(など)を用いて、
全テスト期間のうち”いつ買うか”や”いつ売るか”の情報が入ったシグナルを生成します。
(そのシグナルを使って③で実際に注文を行なっていく)
#シグナル定義
def _my_signal(data):
# この部分に作成するアルゴの指標を書き込んで下さい。
#シグナル定義
#各銘柄の終値(株式分割調整後)を取得、欠損データの補完
cp = data["close_price_adj"].fillna(method="ffill")
#単純移動平均線(SMA)の設定
#データの入れ物を用意
s_sma = pd.DataFrame(data=0,columns=[], index=cp.index)
m_sma = pd.DataFrame(data=0,columns=[], index=cp.index)
l_sma = pd.DataFrame(data=0,columns=[], index=cp.index)
#TA-Libによる計算
for (sym,val) in cp.items():
s_sma[sym] = ta.SMA(cp[sym].values.astype(np.double), timeperiod=5)
m_sma[sym] = ta.SMA(cp[sym].values.astype(np.double), timeperiod=25)
l_sma[sym] = ta.SMA(cp[sym].values.astype(np.double), timeperiod=75)
#SMAの売買シグナルの定義
s_sma_ratio = s_sma/m_sma
l_sma_ratio = m_sma/l_sma
buy_sig = (s_sma_ratio > 1.00) & (s_sma_ratio < 1.10) & (l_sma_ratio > 1.00) & (l_sma_ratio < 1.10)
sell_sig = (s_sma_ratio < 0.99) & (l_sma_ratio < 0.99)
return {
"sma5":s_sma,
"sma25":m_sma,
"sma75":l_sma,
"buy:sig": buy_sig,
"sell:sig": sell_sig,
}
# シグナル登録
ctx.regist_signal("my_signal", _my_signal)
ここで生成されるsignalは以下のようなDataframeになっている。
例えば、buy_signalは
こんな感じのデータが入っている。
以下の③handle_signalで実際に注文を行うのですが、このbuy_siganlを用いていつ買うかを決めている。
具体的に上のシグナルを使って、「jp.stock.7201の場合、値が入ってる日付(2017/05/01と2017/05/03)は買い注文をし、Noneになっている日(2017/5/2)は買い注文はしない」というような処理を③で行なっている。
③シグナルを使って注文を実行していく
ここの部分では先ほど述べたように②で作成したbuy_signalとsell_signalを利用して注文を実行していく。
(1)
def handle_signals(ctx, date, current):
'''
current: pd.DataFrame
'''
df = current.copy()
done_syms = set([])
(2)
ここでやっと本題のケリー基準の計算に取り掛かります。
ケリー基準の計算に必要な「勝ち取引回数、負け取引回数、勝ち取引による総利益、負け取引による総損失」のデータを各銘柄に対して辞書型に保存します。
まずはこれら4つのデータを保存するための空の辞書をlocalStorage(というメモリ的なところ)に作成する。
ここで出来上がるのは以下のような辞書である。
{
'jp.stock.1321': {'win_num': 0, 'lose_num': 0, 'win_amount': 0, 'lose_amount': 0},
'jp.stock.4063': {'win_num': 0, 'lose_num': 0, 'win_amount': 0, 'lose_amount': 0},
'jp.stock.4785': {'win_num': 0, 'lose_num': 0, 'win_amount': 0, 'lose_amount': 0},
・・・
}
for sym in ctx.symbol_list:
if sym not in ctx.localStorage:
ctx.localStorage[sym] = {}
for key in ["win_num", "lose_num", "win_amount", "lose_amount"]:
ctx.localStorage[sym][key] = 0
else:
pass
(3)
次に下の部分で、エグジット注文(利確or損切りの注文)を行う。
ここで先ほど作った辞書に、銘柄ごとに
①損切りをするたびに、
負けトレード数に+1、負けトレードの総損失に+val["pnl"](これはトレードの損益額を表している)
②利確をするたびに、
勝ちトレード数に+1、勝ちトレードの総利益に+val["pnl"]
を行う。
この計算を全ての銘柄に対して全test期間で行うことによって、各銘柄に対する「勝ち取引回数、負け取引回数、勝ち取引による総利益、負け取引による総損失」の4つのデータを保存できる。
for (sym,val) in ctx.portfolio.positions.items():
returns = val["returns"]
symbol_dict = ctx.localStorage[sym]
#利確した時に勝ち取引回数をプラス1、勝ち取引による総利益をプラス損益額する
if (ctx.flag_loss) & (returns < ctx.loss_cut):
symbol_dict["lose_amount"] += val["pnl"]
symbol_dict["lose_num"] += 1
sec = ctx.getSecurity(sym)
sec.order(-val["amount"], comment="損切り(%f)" % returns)
done_syms.add(sym)
#損切りした時に負け取引回数をプラス1、負け取引による総損失をプラス損益額する
elif (ctx.flag_profit) & (returns > ctx.profit_taking):
symbol_dict["win_amount"] += val["pnl"]
symbol_dict["win_num"] += 1
sec = ctx.getSecurity(sym)
sec.order(-val["amount"], comment="利益確定売(%f)" % returns)
done_syms.add(sym)
(4)
ここではtarget_order_percentを用いたエントリー注文を行う。。
この注文方法は、指定した銘柄の総保有額が総資産評価額(現金+保有ポジション評価額)に対して指定の割合となるように注文を行なうものである。
また、②で生成したbuy_signalを元に、いつ買うかを決めている
target_order_percentを使う際に、とるポジションの全資産に対する割合を指定しないといけない。
ここでケリー基準を使います!
①initialize内の kelly_criterion_dict で各銘柄に対してケリー基準が指定されていれば(つまりbacktestが2回目以降なら)そのケリー基準の値を使ってtarget_order_percentの割合を指定する。kelly_criterion_dictが空の場合(つまり最初のbacktestの場合)は初期値として、target_order_percentの割合を0.10 に指定する。
ここでkelly_criterionの値が与えられているがその値が負である銘柄に関しては、買い注文を行わない。
というのも、
R:ペイオフレイシオ,P:勝率とした時(R>0、P>0)
F = [ ( R + 1 ) × P -1)]÷R < 0 \\
\\
( R + 1 ) × P - 1<0 \\
R×P+P-1<0\\
ここでR = \frac{平均利益}{平均損失} = \frac{a}{b}とすると(a>0、b>0)\\
トレードの期待値= 平均利益×勝率+平均損失×(1ー勝率)\\
= a×P + (-b)×(1−P) = b×(R×P+P-1)<0\\
となる。\\
つまりケリー基準が負の時、トレードの期待値は負になる。
ゆえにケリー基準がkelly_criterion_dictによって与えられているがその値が負である銘柄に関しては、買い注文を行わない。
# 買いシグナル
df_long = df[df["buy:sig"]]
if not df_long.empty:
for (sym, val) in df_long.iterrows():
if sym in done_syms:
continue
'''
initialize内でkelly_criterionが指定されていればその値を持ってきて
指定されていなければ初期値として0.10をorder_percentとして用いる
'''
if sym in ctx.kelly_criterion_dict:
order_percent = ctx.kelly_criterion_dict[sym]
else:
order_percent = 0.1
#また、kelly_criterionが負の値(つまり期待値がマイナス)の銘柄に関しては取引しない。
if order_percent >= 0:
sec = ctx.getSecurity(sym)
sec.order_target_percent(order_percent, comment="SIGNAL BUY")
else:
pass
(5)
backtest期間の最終日(手動で打ち込む)に、各銘柄に対して、
localStorage内にこれまで保存してきた4つのデータを用いて「勝率、ペイオフレイシオ」を計算しさらにそれらの値から「ケリー基準」を算出する。
ここでケリー基準の値を算出する際に、トレードした回数があまりにも少ないと適切な値を得られないと考えて、勝ちトレードと負けトレードの合計が15回未満の銘柄に関してはケリー基準を算出しないことにした。
算出したケリー基準は辞書型でlogにはかせる。{銘柄:ケリー基準,銘柄:ケリー基準,銘柄:ケリー基準}の形。辞書は次回のbacktestの時のinitialize内のkelly_criterion_dictに入れる。
if str(date) == "2019-06-04 00:00:00":#最終日は手で入力しかない??
win_rate_dict = {}
payoff_ratio_dict = {}
new_kelly_criterion_dict = {}
for sym in ctx.symbol_list:
last_day_symbol_dict = ctx.localStorage[sym]
total_win_num = last_day_symbol_dict["win_num"]
total_lose_num = last_day_symbol_dict["lose_num"]
trade_num = total_win_num + total_lose_num
'''
総取引回数が少なすぎると正確にケリーの公式を使えないので
最低でも15回は取引している銘柄のみに対して算出する
ただこの15回という数値は割と適当に決めた(いい決め方探したい)
'''
if trade_num >= 15:
win_rate_dict = total_win_num / trade_num #勝率
win_rate[sym] = win_rate
ave_win = last_day_symbol_dict["win_amount"] / total_win_num #平均利益額
ave_lose = last_day_symbol_dict["lose_amount"] / total_lose_num #平均損失額
payoff_ratio = abs( ave_win / ave_lose ) #ペイオフレイシオ
payoff_ratio_dict[sym] = payoff_ratio #ペイオフレイシオの辞書
new_kelly_criterion_dict[sym] = (( payoff_ratio + 1 ) * win_rate - 1 ) / payoff_ratio #ケリー基準の算出
else:
new_kelly_criterion_dict[sym] = 0
ctx.logger.debug(win_rate_dict)
ctx.logger.debug(payoff_ratio_dict)
ctx.logger.debug(new_kelly_criterion_dict)
以上。
以下に全コードをつなげたものを載せておく。
import pandas as pd
import talib as ta
import numpy as np
def initialize(ctx):
# 設定, 6行目から20行目は変えないようにお願いします。
ctx.flag_profit = True #利益確定売りを用いるかTrueなら用いるFalseなら用いない
ctx.flag_loss = True #損切りを用いるかTrueなら用いるFalseなら用いない
ctx.loss_cut = -0.03 #損切りのボーダーマイナス%
ctx.profit_taking = 0.05 #利益確定売りのボーダープラス%
ctx.codes = [
1321,4063,6758,6762,4911,4543,4503,4519,
]
ctx.symbol_list = ["jp.stock.{}".format(code) for code in ctx.codes]
#ここに前回のbacktestで得られたkelly基準を入力する
ctx.kelly_criterion_dict = {}
ctx.configure(
channels={ # 利用チャンネル
"jp.stock": {
"symbols":ctx.symbol_list,
"columns": [
#"open_price_adj", # 始値(株式分割調整後)
#"high_price_adj", # 高値(株式分割調整後)
#"low_price_adj", # 安値(株式分割調整後)
"close_price", # 終値
"close_price_adj", # 終値(株式分割調整後)
# "volume_adj", # 出来高
# "txn_volume", # 売買代金
]}})
#シグナル定義
def _my_signal(data):
# この部分に作成するアルゴの指標を書き込んで下さい。
#シグナル定義
#各銘柄の終値(株式分割調整後)を取得、欠損データの補完
cp = data["close_price_adj"].fillna(method="ffill")
#単純移動平均線(SMA)の設定
#データの入れ物を用意
s_sma = pd.DataFrame(data=0,columns=[], index=cp.index)
m_sma = pd.DataFrame(data=0,columns=[], index=cp.index)
l_sma = pd.DataFrame(data=0,columns=[], index=cp.index)
#TA-Libによる計算
for (sym,val) in cp.items():
s_sma[sym] = ta.SMA(cp[sym].values.astype(np.double), timeperiod=5)
m_sma[sym] = ta.SMA(cp[sym].values.astype(np.double), timeperiod=25)
l_sma[sym] = ta.SMA(cp[sym].values.astype(np.double), timeperiod=75)
#SMAの売買シグナルの定義
s_sma_ratio = s_sma/m_sma
l_sma_ratio = m_sma/l_sma
buy_sig = (s_sma_ratio > 1.00) & (s_sma_ratio < 1.10) & (l_sma_ratio > 1.00) & (l_sma_ratio < 1.10)
sell_sig = (s_sma_ratio < 0.99) & (l_sma_ratio < 0.99)
return {
"sma5":s_sma,
"sma25":m_sma,
"sma75":l_sma,
"buy:sig": buy_sig,
"sell:sig": sell_sig,
}
# シグナル登録
ctx.regist_signal("my_signal", _my_signal)
def handle_signals(ctx, date, current):
'''
current: pd.DataFrame
'''
df = current.copy()
done_syms = set([])
'''
各銘柄ごとに、
勝ち取引回数、負け取引回数
勝ち取引による総利益、負け取引による総損失
を記録するための空の辞書をlocalStorage内に作成する
'''
for sym in ctx.symbol_list:
if sym not in ctx.localStorage:
ctx.localStorage[sym] = {}
for key in ["win_num", "lose_num", "win_amount", "lose_amount"]:
ctx.localStorage[sym][key] = 0
else:
pass
for (sym,val) in ctx.portfolio.positions.items():
returns = val["returns"]
symbol_dict = ctx.localStorage[sym]
#利確した時に勝ち取引回数をプラス1、勝ち取引による総利益をプラス損益額する
if (ctx.flag_loss) & (returns < ctx.loss_cut):
symbol_dict["lose_amount"] += val["pnl"]
symbol_dict["lose_num"] += 1
sec = ctx.getSecurity(sym)
sec.order(-val["amount"], comment="損切り(%f)" % returns)
done_syms.add(sym)
#損切りした時に負け取引回数をプラス1、負け取引による総損失をプラス損益額する
elif (ctx.flag_profit) & (returns > ctx.profit_taking):
symbol_dict["win_amount"] += val["pnl"]
symbol_dict["win_num"] += 1
sec = ctx.getSecurity(sym)
sec.order(-val["amount"], comment="利益確定売(%f)" % returns)
done_syms.add(sym)
# 買いシグナル
df_long = df[df["buy:sig"]]
if not df_long.empty:
for (sym, val) in df_long.iterrows():
if sym in done_syms:
continue
'''
initialize内でkelly_criterionが指定されていればその値を持ってきて
指定されていなければ初期値として0.10をorder_percentとして用いる
'''
if sym in ctx.kelly_criterion_dict:
order_percent = ctx.kelly_criterion_dict[sym]
else:
order_percent = 0.1
#また、kelly_criterionが負の値(つまり期待値がマイナス)の銘柄に関しては取引しない。
if order_percent >= 0:
sec = ctx.getSecurity(sym)
sec.order_target_percent(order_percent, comment="SIGNAL BUY")
else:
pass
'''
backtest期間の最終日に、localstorageの情報から、
各銘柄に対して新しいkelly_criterionを算出しdebugする
'''
if str(date) == "2019-06-04 00:00:00":#最終日は手で入力しかない??
win_rate_dict = {}
payoff_ratio_dict = {}
new_kelly_criterion_dict = {}
for sym in ctx.symbol_list:
last_day_symbol_dict = ctx.localStorage[sym]
total_win_num = last_day_symbol_dict["win_num"]
total_lose_num = last_day_symbol_dict["lose_num"]
trade_num = total_win_num + total_lose_num
if trade_num >= 15:
win_rate = total_win_num / trade_num #勝率
win_rate_dict[sym] = win_rate
ave_win = last_day_symbol_dict["win_amount"] / total_win_num #平均利益額
ave_lose = last_day_symbol_dict["lose_amount"] / total_lose_num #平均損失額
payoff_ratio = abs( ave_win / ave_lose ) #ペイオフレイシオ
payoff_ratio_dict[sym] = payoff_ratio #ペイオフレイシオの辞書
new_kelly_criterion_dict[sym] = (( payoff_ratio + 1 ) * win_rate - 1 ) / payoff_ratio #ケリー基準の算出
else:
new_kelly_criterion_dict[sym] = 0
ctx.logger.debug(win_rate_dict)
ctx.logger.debug(payoff_ratio_dict)
ctx.logger.debug(new_kelly_criterion_dict)
QuantX上で以下の二通りの計算を行ってみた
上記のコードを用いて以下の2通りに対して計算を行った。
① 同じテスト期間(2016/06/04~2019/06/04)でひたすらケリー基準のtestを回す
つまり、
1回目:ケリー基準を初期値として10%
2回目:1回目のback test の結果から算出したケリー基準を用いてback test を行う
3回目:2回目のback test の結果から算出したケリー基準を用いてback test を行う
・・・
n回目:n-1回目のback test の結果から算出したケリー基準を用いてback test を行う
予想としては回すたびに成績が上がっていくのでは??
②前半期間(2013/06/04~2016/06/04)のback test で得られたケリー基準を、
後半期間(2016/06/04~2019/06/04)のback test に用いてみる
予想としては、後半期間(2013/06/04~2019/06/04)の back test に対してケリー基準を使った場合は、ケリー基準を使わなかった場合に比べて成績上がるのでは??
#実際に実験してみましょう!
①同じテスト期間(2016/06/04~2019/06/04)でひたすらケリー基準のtestを回す。
logに吐かれた辞書達
#勝率
{'jp.stock.1321': 0.46153846153846156, 'jp.stock.4063': 0.39473684210526316, 'jp.stock.6758': 0.375, 'jp.stock.6762': 0.5087719298245614, 'jp.stock.4911': 0.4909090909090909, 'jp.stock.4543': 0.4, 'jp.stock.4503': 0.5757575757575758, 'jp.stock.4519': 0.5777777777777777}
#ペイオフレイシオ
{'jp.stock.1321': 1.488372458218429, 'jp.stock.4063': 1.5085367909197016, 'jp.stock.6758': 1.6381593860302452, 'jp.stock.6762': 1.409264060040011, 'jp.stock.4911': 1.6484809145461368, 'jp.stock.4543': 1.2900922220024045, 'jp.stock.4503': 1.3042675298881854, 'jp.stock.4519': 1.2115610837549864}
#ケリー基準
{'jp.stock.1321': 0.09975970415264362, 'jp.stock.4063': -0.006488478707581729, 'jp.stock.6758': -0.00652575709654466, 'jp.stock.6762': 0.16020129348725792, 'jp.stock.4911': 0.182084824520133, 'jp.stock.4543': -0.065083030319116, 'jp.stock.4503': 0.2504850265906157, 'jp.stock.4519': 0.22928340313706172}
ちなみに第一回目のbacktestでは、銘柄全体としての勝率は0.471でペイオフレイシオ1.446。
これらの値から計算すると破産確率は0.097% < 1%
・2回目(先ほどのbacktestで算出されたケリー基準を用いる)
1回目も2回目も同じ期間(2016/06/03~2019/06/03)に対してbacktestを行なっており、
2回目のbacktestではケリー基準を用いることにより、1回目のbacktestでパフォーマンスの良かった銘柄の重率をあげパフォーマンスが悪かった銘柄に対して重率を下げているので成績は上がって当然ではある。
第二回めのbacktestでは、全体としての勝率は0.493でペイオフレイシオは1.57なので破産確率は0.001% << 1%
・3回目(2回目のbacktestで算出されたケリー基準を用いる)
・4回目(3回目のbacktestで算出されたケリー基準を用いる)
下がった。
②前半期間(2013/06/04~2016/06/04)のback test で得られたケリー基準を、
後半期間(2016/06/04~2019/06/04)のback test に用いてみる。
まずはケリー基準を用いずに後半期間に対してbacktestを行う
次に前半期間backtestを行いケリー基準を算出し、そのケリー基準を用いて後半期間に対してback testを行う。
前半期間に対してbacktestをかけると以下のようなケリー基準が算出される。
{'jp.stock.1321': -0.1409228002075559, 'jp.stock.4063': 0.08296824525881397, 'jp.stock.6758': 0.19937645102881876, 'jp.stock.6762': 0.4031308031569174, 'jp.stock.4911': 0.09167549048309995, 'jp.stock.4543': 0.19352881623416263, 'jp.stock.4503': 0.22521929898539453, 'jp.stock.4519': 0.23123783316193733}
MaxDrawdown、Sharpratio、volatility等が悪化している。
ケリー基準を用いた場合に成績が悪化した。
一般にケリー基準を用いるとvolatilityが上がることが知られている。
これを回避するためにハーフケリーというものがある。
これはケリー基準を用いて計算した結果が例えば「20%」だったとしましょう。
この時、ハーフケリーを使うならその半分の10%を賭ける、という資金管理法である。
こうすることによって収益率がやや下がりますが、volatilityを抑えることができるのでより安定な運用が可能となります。
まとまった時間が取れれば
「ハーフケリー、3/4ケリーなどの資金管理した場合の運用結果を比較してみた!!」
みたいな記事もかければ、と思います。
QuantXではテクニカル指標に対するbacktestだけではなく資金管理もできる!ということが伝わったでしょうか??
最後までご覧いただきありがとうございました。