勝率とドローダウン期間の関係

勝率とドローダウン期間の関係を調べてみる。トレーダーはドローダウン期間が続くと、自分の戦略に疑いを抱き、やたらと戦略に手を加えたくなるものである。だが、ドローダウンはトレードには付き物であって、ある程度は初めから覚悟しなければならない。

その「ある程度」とはどのくらいなのか、ということをシミュレーションによって調べてみる。単純化のためにいくつかの仮定を置く。

  • トレードのリターンは1%、または-1%のいずれかとする。つまりペイオフレシオ=1.0である。
  • トレードは1日に1回とする。
  • 1年の営業日を260日とする。

以上の仮定に基づいて1年間トレードするという試行を10回行い、平均のドローダウン期間を勝率ごとに求める。勝率は5%刻みで5%-100%まで調べる。結果は以下の通りである。

win_per durations
0.05    259.0
0.1     259.0
0.15    258.9
0.2     258.6
0.25    257.5
0.3     256.4
0.35    256.9
0.4     238.9
0.45    240.0
0.5     160.0
0.55    109.7
0.6     50.6
0.65    35.2
0.7     19.2
0.75    16.6
0.8     10.8
0.85    7.6
0.9     5.8
0.95    3.6
1.0     0.0

ペイオフレシオ1.0で勝率60%はなかなかのパフォーマンスである。それでも50.6日(2ヶ月強)のドローダウン期間がある。勝率70%となると、むしろありえないくらいの高パフォーマンスである。それでも19.2日(1ヶ月弱)のドローダウン期間がある。

トレーダーとしてはドローダウンの回復に1ヶ月も2ヶ月もかかったのでは大変つらいことである。だが、勝率60%、70%でもこの程度のドローダウン期間は避けがたいのである。だとすれば数ヶ月のドローダウン期間は覚悟すべきだろう。

サンプルプログラム

○以下のコマンドをSpyderの「IPython console」にコピー&ペーストして「Enter」キーを2回押す。

import forex_system as fs
import numpy as np
import pandas as pd

durations = np.zeros(20)
print('win_per' + '\t' + 'durations')
for i in range(20):
    win_per = (i + 1) / 20
    for j in range(10):
        ret = np.random.choice([0.01, -0.01], 260, p=[win_per, 1 - win_per])
        ret = pd.Series(ret)
        temp = fs.calc_durations(ret, 1440)
        durations[i] += temp
    durations[i] = durations[i] / 10
    print(str(win_per) + '\t' + str(durations[i]))
(2017/02/08更新)

許容損失は資金の何%に抑えるべきか

ネットでは2%が多数派のようだが…

許容損失は資金の何%に抑えるべきかという議論がある。FXサイトなどを見ると、どうやら2%とするのが通説のようである。ところが、2%とする根拠は何かというと、ちゃんとした説明は私の知る限り、見当たらない。どうも有名なトレード本に2%がいいと書いてあるから、ということらしい。

私はいつも思うのだが、有名な本に書かれているから、有名な人が言ったから、という理由で無批判に受け入れてしまうというのはいかがなものだろうか。しかも、そういう意見がコピペを繰り返して、いつの間にかに通説になっていくように思われる。この議論に限ったことではないが、他人の言葉などどうでもよく、自分で検証してみるということが大事だと思う。

2%ではすぐに資金を溶かしてしまうことも

というわけで、簡単な検証をしてみた。

単純化するためにペイオフレシオ = 1.0、つまり平均利益=平均損失と仮定し、勝率がそれぞれ30%、40%、50%、60%、70%であるトレーダーが260回トレードしたとする。1年を260営業日とし、毎営業日1回トレードするとすれば、1年後にどうなったかということを見るわけである。そして、更に許容損失を1%、2%、3%で場合分けした。

このようなシミュレーションを1万回行い、その平均を取った。その結果が以下のグラフである。

乱数を使っているので、必ずしも同じ結果にはならないが、大体このようである。グラフによると、許容損失を2%とした場合、勝率30%のトレーダーはほとんどの資金を、勝率40%のトレーダーでも大半の資金をたったの1年で失っている。

そして許容損失が1%であれば損失が減り、3%であれば損失が増えている。とすると、許容損失は少ないほどいいと言える。

勝てなければ何%にしても勝てない

一方、勝率50%のトレーダーは許容損失の大きさの影響は受けず、勝ちもしなければ負けもしない。まあ、当たり前だが。

勝率60%、70%のトレーダーは許容損失を2%とした場合、たったの1年で資金を倍以上にしている。そして許容損失が1%であれば利益が減り、3%であれば利益が増えている(グラフでは資金の増え方がすごいので上の方をカットした)。とすると、許容損失は多いほどいいと言える。

以上、検証結果を総合すると、「資金の何%」という議論は無意味で、勝てるトレーダーはどんどんリスクを取っていいし(注)、勝てないトレーダーはリスクを減らす、というよりトレードしないのが最善ということになりそうだ。

(注)ところがそうはいかない場合もある。「リスク管理」カテゴリの「最適レバレッジ」の記事を参照。

サンプルプログラム

○以下のコマンドを「IPython console」にコピー&ペーストして「Enter」キーを2回押す。

import matplotlib.pyplot as plt
import numpy as np

n_trades = 260
n_tests = 10000
balance = np.empty(4)
risk = np.array(range(4)) / 100
for i in range(5):
    win_per = (i + 3) / 10
    for j in range(4):
        temp = np.empty(n_trades)
        mean = 0
        for k in range(n_tests):
            change = np.random.choice([risk[j], -risk[j]], n_trades,
                                      p=[win_per, 1.0-win_per])
            for l in range(n_trades):
                if l == 0:
                    temp[0] = 1.0 + change[l]
                else:
                    temp[l] = temp[l-1] * (1.0 + change[l])
            mean += temp[n_trades-1]
        mean = mean / n_tests
        balance[j] = mean
    plt.plot(risk, balance, label=str(win_per))
plt.title('Balance and Risk')
plt.xlabel('Risk')
plt.ylabel('Balance')
plt.xlim(0.0, 0.03)
plt.ylim(0.0, 2.0)
plt.xticks([0.00, 0.01, 0.02, 0.03])
plt.legend(title='win_per', fontsize = 'small')
plt.axhline(y=1.0, color='black', linestyle=':')
plt.tight_layout()
plt.savefig('balance_and_risk.png', dpi=150)
plt.show()
(2017/02/09更新)

ポートフォリオの最適化

ポートフォリオと言えば複数銘柄の保有比率を指すことが多いと思う。だが、ここでは複数戦略への資金の投入比率として考えることとする。

また、ポートフォリオの最適化は一般的には目標リターンを設定した上でリスクを最小化することを指すようである。目標リターンを設定しないなら、期待リターンとリスクの比、つまりシャープレシオの最大化と考えてもいいのだろう。ここではシャープレシオの最大化ということを考える。

ポートフォリオにも過剰最適化のリスク

ポートフォリオの最適化にもバックテストにおけると同様、過剰最適化のリスクがある。例えばバックテスト自体はウォークフォワードテストを行ったとしても、投入比率の決定に全期間を用いたなら、これにはトレードの時点で存在しないデータを使う先読みバイアスのリスクがある。

複数戦略の中に期待リターンがマイナスの戦略があったとする。ポートフォリオの最適化を行うと、その戦略のウェイトは0、あるいはほぼ0になるだろう。また、期待リターンがプラスの戦略においても、期待リターンが大きい戦略ほどウェイトが大きくなる傾向が出てくるだろう。

となると、最適化の結果、負ける戦略は排除され、勝てる戦略のみが残ることになる。だが、戦略のどれが勝ち、あるいは負けるのかということは後になって分かることであり、トレードの時点では知りようがないのだから、このような最適化はパフォーマンスを過大評価する。

もっとも、ウォークフォワードテストでパフォーマンスの良い戦略をポートフォリオに入れ、悪い戦略は入れない、ということは最適化の前に行われているだろう。これも一種の先読みバイアスではある。

等ウェイトも馬鹿にできない

それはともかくとして、ポートフォリオの最適化はウォークフォワードテストと同様、インサンプル期間で行い、パフォーマンスの評価はアウトオブサンプル期間で行うのが先読みバイアスのリスクを少しでも減らす方策の1つだろう。とりあえずそうするとしても、まだ問題がある。

一応、単体でも勝てる戦略であったとしても、比較的短いインサンプル期間では期待リターンがマイナスになることもある。すると、続くアウトオブサンプル期間において、その戦略のウェイトは0、あるいはほぼ0になってしまうと考えられる。果たして、それでよいのだろうか。

調子の良い戦略のウェイトを増やし、調子の悪い戦略のウェイトを減らすのは一見、理に適っているようにも見える。現在の値動きが過去の値動きの影響を受けないなら確かにそうかもしれない。

だが、買い有利の相場と売り有利の相場が交互に来るとか、順張り有利の相場と逆張り有利の相場が交互に来るとかいうことも考えられる。すると、買いで負けたので売りに転換してさらに負けるとか、順張りで負けたので逆張りに転換してさらに負けるとかいった具合で往復ビンタを食らうことにもなりかねない。

実際にこのタイプの最適化を行うと、ある戦略のウェイトがほぼ100%を占めたと思うと、次の期間では別の戦略のウェイトがほぼ100%を占める、といったことがよく起こる。これは、ほぼ100%を占めていた戦略が次の期間ではほぼ0%になっていた、つまり、ほぼ100%の資金を投入した戦略が負けたということであり、ほぼ0%の戦略が次の期間ではほぼ100%を占めていた、つまり、ほとんど資金を投入しなかった戦略が勝ったということである。

その結果、等ウェイトでのパフォーマンスを下回るということも往々にしてある。最適化がかえって劣化をもたらすのである。

逆に何も考えずに等ウェイトでポートフォリオを組むというのはいかにも芸がなく、頭の悪いやり方にも見えるが、意外と等ウェイトでのパフォーマンスを超える最適化というのも難しかったりする。等ウェイトもなかなか馬鹿にできないもので、無理に最適化はせず、等ウェイトでよい、とするのも1つの考え方である。

(2016/11/19更新)

破産レバレッジ?

割とどうでもいいが、利益を最大化させる最適レバレッジではなくて、破産させるレバレッジを考えてみる。とりあえず破産レバレッジとでも名付けておこう。計算式は

破産レバレッジ = 1 / 最小リターン * (-1)

とする。

もし最小リターンが-1%だったとする。すると破産レバレッジは1 / (-0.01) * (-1) = 100.0となる。これは簡単に理解できるだろう。最悪で1%の負けでもレバレッジ100倍なら100%の負け、つまり破産である。それまでいくら稼いでいても、である。

最悪の負けはすべてのトレードの中で少なくとも1つはある。それがトレードの最初であろうと、最後であろうと、それ以外であろうと、破産レバレッジでそのトレードが行われた瞬間、たちまち破産するのである。

もちろん、最小リターンがマイナスでない場合、つまりすべてのトレードで1度も負けがない場合、破産レバレッジは意味をなさない。あくまでも勝つこともあれば負けることもあるトレードというのが前提である。

さて、破産レバレッジをシミュレーションで確認してみる。ここに1トレード当たりの期待利益が0.1%、リスクが0.5%の戦略があるとする。だが、最小リターンは実際にシミュレーションしてみないと出てこない数値なので、トレードを100回行うとして先ずは実行してみる。

シミュレーション結果によると、破産レバレッジは63.7157243783で、実測値は63.72となっている。実測値は0.01のインターバルで測っているので、もっと細かくやれば理論値と一致するだろう。最適レバレッジの計算に比べれば非常に単純明快であり、近似式も必要ない。

ただ、最小リターンがシミュレーションしないと出てこないというのが曲者で、トレードを1万回、100万回と増やしていけば最小リターンはより小さく、そして破産レバレッジもより小さくなるだろう。言い換えるとトレードを永遠に続ければいつかは必ず破産することになる。

だが、心配することはない。一生かけてもできない数のトレードでも破産レバレッジが0近くになってトレードが無理、ということにはならない。

下のサンプルプログラムの「trades」の数を変更してシミューレションしてみれば分かるが、トレードを50万回やっても破産レバレッジは45倍前後で、日本国内の規制である25倍よりまだ大きいのである。もちろん、1トレード当たりの期待利益が0.1%、リスクが0.5%の戦略であればの話である。期待利益がマイナスの戦略ならレバレッジなど関係なく、遅かれ早かれ破産するので、トレードはやめたほうがいい。

サンプルプログラム

○以下のコマンドをSpyderの「IPython console」にコピー&ペーストして「Enter」キーを2回押す。

# coding: utf-8
import matplotlib.pyplot as plt
import numpy as np

trades = 100
ret = np.random.normal(0.001, 0.005, trades)
x = np.array(range(1, 101))
y = np.zeros(trades)
for i in range(trades):
    if i == 0:
        y[i] = 1.0 * (1.0 + ret[i])
    else:
        y[i] = y[i-1] * (1.0 + ret[i])
    if y[i] < 0.0:
        y[i] = 0.0
mean = np.mean(ret)
std = np.std(ret)
worst_kelly = 1.0 / np.min(ret) * (-1)
ax=plt.subplot()
plt.plot(x, y)
plt.xlabel('Trades')
plt.ylabel('Equity curve')
plt.text(0.05, 0.9, 'Worst Kelly = ' + str(worst_kelly),
         transform=ax.transAxes)
plt.savefig('bankruptcy_leverage1.png', dpi=150)
plt.show()
plt.close()

n = 10000
equity_curve = np.empty(trades)
x = np.array(range(n)) / 100
y = np.empty(n)
for j in range(n):
    leverage = j / 100
    for i in range(trades):
        if i == 0:
            equity_curve[i] = 1.0 * (1.0 + ret[i] * leverage)
        else:
            equity_curve[i] = equity_curve[i-1] * (1.0 + ret[i] * leverage)
        if equity_curve[i] < 0.0:
            equity_curve[i] = 0.0
    y[j] = equity_curve[trades-1]
argmin = np.argmin(y)
ax=plt.subplot()
plt.plot(x, y)
plt.axvline(x=worst_kelly, color='green')
plt.axvline(x=x[argmin], color='green')
plt.axhline(y=1.0, color='red')
plt.xlabel('Leverage')
plt.ylabel('Balance')
plt.text(0.05, 0.9, 'Worst Kelly(actual value) = ' + str(x[argmin]),
         transform=ax.transAxes)
plt.text(0.05, 0.85, 'Worst Kelly = ' + str(worst_kelly),
         transform=ax.transAxes)
plt.savefig('bankruptcy_leverage2.png', dpi=150)
plt.show()
plt.close()
(2016/10/28更新)

最適レバレッジの近似式の誤差

ケリー基準を用いた最適レバレッジでは近似式による計算がよく使われていると思う。

近似式は

最適レバレッジ = リターンの平均 / (リターンの標準偏差 * リターンの標準偏差)

となっている。

本来の式はレバレッジについて級数展開して求めることができるが、レバレッジが小さい場合、第3項以降は無視できるほど小さくなるので、近似式では初めの2項を用いて求めている。つまり、レバレッジが小さくない場合、近似式はかなり大きな誤差を生じることになる。細かい点については以下のページがとても参考になる。

http://www.geocities.jp/y_infty/management/criterion_3.html

私はこれまで、どのくらいの誤差があるのかということを確かめることはせずに近似式を用いてきたが、レバレッジをx、累積リターンをyとした場合のグラフを作成すると、yが最大値となるxと近似式による最適レバレッジとの間になかなか無視できない誤差が時として生じることに気が付いた。そこで、どのくらいの誤差があるのかということを確かめてみた。

ここに1トレード当たりの期待利益が0.1%、リスクが0.5%の戦略があるとする。近似式を用いると最適レバレッジは0.001 / (0.005 * 0.005) = 40.0となる。この戦略に基づいて100回トレードを行うシミュレーションを繰り返し、その中から誤差の小さいものと大きいものとを選んでみた。

誤差の小さい場合

先ずは誤差が小さい場合の例を見てみよう。

誤差は0.01以下で、この程度の誤差であれば確かに無視してもよさそうだ。

誤差の大きい場合

次に誤差が大きい場合を見てみる。

誤差は15以上で大きいといえば大きいが、より問題であるのは近似式での最適レバレッジが本来の最適レバレッジでは破産レベルになっていることだ。これはさすがに無視できない。

近似式は使わないべきか

このように書くと、近似式は恐くて使えないという誤解を与えてしまうかもしれない。だが、このような誤差は何十回もシミュレーションをして、ようやく1回見かけるくらいレアなものである。また、レバレッジが小さくても誤差が大きい場合や逆にレバレッジが大きくても誤差が小さい場合もあるが、やはり近似式の性質上、レバレッジが大きいほど誤差も大きい傾向がある。

FXは日本国内では規制によってレバレッジ25倍までとなっているし、この程度のレバレッジなら、誤差も大したことはない。したがって、日本国内の規制にしたがったトレードをしている限り、近似式で問題ないだろう。

サンプルプログラム

○以下のコマンドをSpyderの「IPython console」にコピー&ペーストして「Enter」キーを2回押す。

# coding: utf-8
import matplotlib.pyplot as plt
import numpy as np

trades = 100
ret = np.random.normal(0.001, 0.005, trades)
x = np.array(range(1, 101))
y = np.zeros(trades)
for i in range(trades):
    if i == 0:
        y[i] = 1.0 * (1.0 + ret[i])
    else:
        y[i] = y[i-1] * (1.0 + ret[i])
    if y[i] < 0.0:
        y[i] = 0.0
mean = np.mean(ret)
std = np.std(ret)
kelly = mean / (std * std)
ax=plt.subplot()
plt.plot(x, y)
plt.xlabel('Trades')
plt.ylabel('Equity curve')
plt.text(0.05, 0.9, 'Kelly Approximation = ' + str(kelly),
         transform=ax.transAxes)
plt.savefig('optimal_leverage1.png', dpi=150)
plt.show()
plt.close()

n = 10000
equity_curve = np.empty(trades)
x = np.array(range(n)) / 100
y = np.empty(n)
for j in range(n):
    leverage = j / 100
    for i in range(trades):
        if i == 0:
            equity_curve[i] = 1.0 * (1.0 + ret[i] * leverage)
        else:
            equity_curve[i] = equity_curve[i-1] * (1.0 + ret[i] * leverage)
        if equity_curve[i] < 0.0:
            equity_curve[i] = 0.0
    y[j] = equity_curve[trades-1]
argmax = np.argmax(y)
ax=plt.subplot()
plt.plot(x, y)
plt.axvline(x=kelly, color='green')
plt.axvline(x=x[argmax], color='green')
plt.axhline(y=1.0, color='red')
plt.xlabel('Leverage')
plt.ylabel('Balance')
plt.text(0.05, 0.9, 'Kelly = ' + str(x[argmax]), transform=ax.transAxes)
plt.text(0.05, 0.85, 'Kelly Approximation = ' + str(kelly),
         transform=ax.transAxes)
plt.savefig('optimal_leverage2.png', dpi=150)
plt.show()
plt.close()
(2016/10/28更新)