見出し画像

難しいことは分らんが資金管理について考える。

 今回は、期待値が正の戦略に対して、利益を最大化するために必要な資金管理についてのお話です。資金管理については、これまで戦略の期待値を正にすることに意識するあまり中々考えられずにいましたが、最近システムトレード基本と原則という書籍を読み、資金管理って大事だなと思い調査しようと思い立ちました。

ケリー基準ってなに?

 資金管理を考えるにあたって、有名な戦略がケリー基準に従った方法です。ケリー基準とは、単純なギャンブルから株式などの複雑なゲームまで幅広い範囲において最終的な資金を最大化するために応用されてきた資金戦略です。詳しい理論や考え方についてはカナヲさんの以下の記事を参考にすると良いかと思います。

 こちらの記事の実装によりますと、資金に対する1トレードで用いる金額の割合の最適値であるオプティマルfは、
                    val = ∫[μ-3σ, μ+3σ] log(1+f*s)*確率密度関数(s, μ, σ)ds
               (μ: リターンのn移動平均, σ: リターンのn移動標準偏差)

を最大化するようなfによって与えられるらしいので、この値によってロットを計算します。

f → 最適ロットの簡単な説明

資金: x、
アセット価格: P、
オプティマルf: f

であるとき、1取引にあてる最適資金yは
y = x * f
最適ロットlは
l = y / P
  = (x * f) / P
となるのでこの式によって、fから最適ロットlが計算できます。

検証してみた

 以上のことを用いて実際にどの程度差があるのかを検証してみましょう。検証の条件は以下の通りとします。

  • とある戦略の2022-01-01~2022-09-01までの比較

  • 最大ロットは0.2BTCとする(適当)

  • 初期資金は100000JPY

  • 常に全資金をベット VS ケリー基準に従ってベット

コード例

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import warnings
warnings.simplefilter('ignore')
from scipy.optimize import minimize_scalar, newton, minimize
from scipy.integrate import quad
from scipy.stats import norm
from dateutil.parser import parse

df = df.loc[parse('2022-01-01'):]

df = df[df['ret'] != 0]
df['ret_mean'] = df['ret'].rolling(100).mean().dropna()
df['ret_std'] = df['ret'].rolling(100).std().dropna()
df = df.dropna()

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

def get_kelly_share(df):
    r = 1
    m, M = 0, r
    solution = minimize_scalar(norm_integral, args=(df['ret_mean'], df['ret_std']), bounds=[m, M], method='bounded')
    return solution.x

s_qty = 100000 #(JPY)
m_lot = 0.01
M_lot = 0.2
df['f'] = df[['ret_mean', 'ret_std']].apply(get_kelly_share, axis=1)
df['f'] = np.where(df['f'] < 0.1, 0.1, df['f']) # リターンの平均が負のときはfが極端に小さくなるので閾値を設定
df['f'] = df['f'].shift(1).fillna(0.0001) # 最初は最小注文数量となるように十分小さい値とする
comp_lot = []
comp_plus = []
kelly_lot = []
kelly_plus = []
for i, (r, f, p) in enumerate(zip(df['ret'].values, df['f'].values, df['cl'].values)):
    if i == 0:
        comp_lot.append(s_qty / p)
        comp_plus.append(s_qty * r)
        if (s_qty * f) / p < m_lot:
            kelly_lot.append(m_lot)
            kelly_plus.append(p * m_lot * r)
        else:
            kelly_lot.append((s_qty * f) / p)
            kelly_plus.append(s_qty * f * r)
    else:
        if (s_qty + sum(comp_plus)) / p > M_lot:
            comp_lot.append(M_lot)
            comp_plus.append(p * M_lot * r)
        elif (s_qty + sum(comp_plus)) / p < m_lot:
            comp_lot.append(m_lot)
            comp_plus.append(p * m_lot * r)
        else:
            comp_lot.append((s_qty + sum(comp_plus)) / p)
            comp_plus.append((s_qty + sum(comp_plus)) * r)
        if ((s_qty + sum(kelly_plus)) * f) / p > M_lot:
            kelly_lot.append(M_lot)
            kelly_plus.append(p * M_lot * r)
        elif ((s_qty + sum(kelly_plus)) * f) / p < m_lot:
            kelly_lot.append(m_lot)
            kelly_plus.append(p * m_lot * r)
        else:
            kelly_lot.append(((s_qty + sum(kelly_plus)) * f) / p)
            kelly_plus.append((s_qty + sum(kelly_plus)) * f * r)


comp_plus[0] = comp_plus[0] + s_qty
df['comp_ret'] = np.array(comp_plus).cumsum()
df['comp_lot'] = np.array(comp_lot)

kelly_plus[0] = kelly_plus[0] + s_qty
df['kelly_ret'] = np.array(kelly_plus).cumsum()
df['kelly_lot'] = np.array(kelly_lot)

N = len(df)
win_rate = len(df[df['ret'] > 0]) / len(df)
comp_e, kelly_e = win_rate * np.mean(comp_plus / df['comp_ret']), win_rate * np.mean(kelly_plus / df['kelly_ret'])
comp_E, kelly_E = N * comp_e, N *  kelly_e
comp_s, kelly_s = (comp_plus[1:] / df['comp_ret'].iloc[1:]).std(), (kelly_plus[1:] / df['kelly_ret'].iloc[1:]).std()
comp_S, kelly_S = np.sqrt(N) * comp_s, np.sqrt(N) * kelly_s
comp_sr, kelly_sr = (comp_E / comp_S), (kelly_E / kelly_S)
comp_maxDD, kelly_maxDD = (9 / 4) * ((comp_s ** 2) / comp_e), (9 / 4) * ((kelly_s ** 2) / kelly_e)
display('シャープレシオ -> comp: {0}, kelly: {1}'.format(comp_sr, kelly_sr))

※forループで各レコードのfを最適化をしながら回しているのでレコード数が多いデータだと実行時間に時間がかかることに注意してください。

実行結果

  • 全資金ベット VS ケリー基準に従ってベット(損益の変化)


comp_ret:全資金ベット、kelly_ret:ケリー基準でベット、cum_ret:素のバックテスト結果
  • 全資金ベット VS ケリー基準に従ってベット(ロットの変化)


  • シャープレシオ

全資金ベット:3.60
ケリー基準に従ってベット:3.66

  • 最大ドローダウン

全資金ベット:16.6%
ケリー基準に従ってベット:11.4%

 このような結果となりました。まず感じたのは複利パワーは偉大だ!ということです。確かに複利運用は、今までの利益を失う可能性があるというリスクがありますが、そのリスクに見合うだけのリターンもあるということが分かりました。次に、全資金を運用する場合と、ケリー基準に従って運用する場合を比較すると、全資金を運用した方が最終的な利益が大きいことが分かります。これは、ケリー基準の場合と比較してよりリスクを取った運用方法なので当然だと考えられます。一方でシャープレシオ、最大ドローダウンに関してはケリー基準に従って運用した方がよい結果となりました。この結果から、ケリー基準での運用がより精神的に安定して運用ができることが分かります。
 ちなみに、ケリー基準に従って運用した場合において、リターンの移動平均が負となるとき、fは計算上極端に小さな値となります。そのため、上の結果ではfに閾値を設定しています。上の場合、閾値は0.1ですが、より積極的な値でも良いかもしれません。

注意点

 注意点として、

1. 実際に運用する場合はロジックごとにロット数に上限がある
2. 上のコードは1取引=1カラムで完了する必要がある

ということです。
 1について、実際に運用する場合は、まず最初にロジックの上限となるロットをフォワードテスト等で調査し、コード内のM_lotを決定してやればよいですが、そもそもロットの上限なんてものは定数ではなく相場環境によって変化する値ですので、この検証でこの部分も含めて正確に議論するのは困難であると考えられます。よって、少なくてもこのくらいならいけるだろうという値で検証してみるぐらいでいいのかなと思います。
 3については、直前までの結果を利用してfを最適化していることから1取引が1レコードで完了しないロジックについてはその分だけ最適化に利用する部分を限定させる必要があるということです。

感想

 いかがだったでしょうか?感覚としては、
・リスクの高さ
    固定ロット(単利)運用 << ケリー基準運用 < 全資金運用
のような関係であり、リスクの取り方によって得られるリターンも大きく変わるという感じでした。複利運用はやりたいけど、全資金を各トレードにかけるのはリスクが大きいと考えているので、ケリー基準に従った運用をするのは大アリだと感じました。

 今回検証していく中で、資金管理はまだまだ奥深く自分自身まだあまり理解できていない部分もあると感じました。もしかしたら間違っている部分や、もっとこうした方がいいという部分があるかもしれないので、もしそのように感じたbotterの方は是非コメント欄やツイッターで指摘していただけると嬉しいです。

いいなと思ったら応援しよう!