Python(NumPy, pandas) で最大ドローダウンを計算してみました

バックテストをしていて最大ドローダウンを計算したかったので調べてみました。

環境

  • Python 3.5.2
  • pandas 0.22.0
  • NumPy 1.14.3

最大ドローダウン

最大資産(累積利益)からの下落率のこと。システムトレードなどでは、最大ドローダウン(下落率)をどれだけに設定するかがリスク管理の観点から重要な要素の一つとなる。

野村證券 | ドローダウン(証券用語解説集)

Stack Overflow

検索したら Stack Overflow に同じ質問をしていた人がいました。

Just find out where running maximum minus current value is largest:

n = 1000
xs = np.random.randn(n).cumsum()
i = np.argmax(np.maximum.accumulate(xs) - xs) # end of the period
j = np.argmax(xs[:i]) # start of period

plt.plot(xs)
plt.plot([i, j], [xs[i], xs[j]], 'o', color='Red', markersize=10)

numpy - Start, End and Duration of Maximum Drawdown in Python - Stack Overflow

計算しているのは 3 行だけみたいです。 これだけで計算できてしまうのですね。

関数

NumPy はあまり使ったことがないのでそれぞれが何をしているのかよくわかりませんでした。 なので、それぞれの関数を調べてみました。

randn

Return a sample (or samples) from the “standard normal” distribution.

numpy.random.randn

名前からしてもランダムな値を返してくれそうです。 引数に指定した数だけ配列を作ってランダムな値を返してくれるようです。

np.random.random(3)

結果です。

array([0.23766355, 0.23769627, 0.38886935])

randn はテストデータを作るために使っているようなので、最大ドローダウンの計算とはあまり関係ないみたいでした。

cumsum

Return the cumulative sum of the elements along a given axis.

numpy.cumsum

合計しながら累積してくれる関数のようです。

l = [-20, 7675, 965, -8790, -3463, 18715, 2526, -5699, -6384, 4865]
ndarray1 = np.array(l).cumsum()
ndarray1

結果です。

array([  -20,  7655,  8620,  -170, -3633, 15082, 17608, 11909,  5525, 10390])

こういう関数は JavaScript にはない気がします。

const cumsum = (a) => (
  a.reduce((pv, cv) => {
    pv.push((pv.length > 0 ? pv[pv.length - 1] : 0) + cv)
    return pv
  }, [])
)
console.log(cumsum([1, 2, 3, 4, 5]))
// [ 1, 3, 6, 10, 15 ]

関係ないですけれども、 JavaScript で cumsum を実装してみました。 アキュムレーターの pvpush しているのがなんか気持ち悪いです。

話を戻して。 cumsum した結果のグラフを載せておきます。

accumulate

Accumulate the result of applying the operator to all elements.

numpy.ufunc.accumulate

numpy.ufunc.accumulate は、すべての要素に ufunc を適用しながら累積してくれるようです。

ちょっと、 numpy.cumsum の “cumulative” と、 numpy.ufunc.accumulate の “Accumulate” の違いが気になったので調べてみました。

なんで “accumulate” の方は “ac-” がついているのかな?

ac‐

音節ac‐ 発音記号・読み方/æk/

【接頭辞】

(c,q,k の前にくるときの) ad‐ の異形.

acの意味・使い方 - 英和辞典 Weblio辞書


ad‐

音節ad‐ 発音記号・読み方/æd, əd/

【接頭辞】

「…へ」「…に」 《★「移動」「方向」「変化」「完成」「近似」「固着」「付加」「増加」「開始」の意,また単なる強意》.

adの意味・使い方 - 英和辞典 Weblio辞書

“ac-” は “ad-” の異形のようです。 「変化」とか「増加」とかの意味がありそうかな。

次は、なんで “cumulative” の方は “-tive” で、 “Accumulate” の方は “-late” なのかな?

cumulate と accumulate はどう違いますか?

In both of those sentences “accumulate” is correct. “Cumulate” might also be correct but it's not really a commonly-used word. I don't think a native English speaker would ever use cumulate in those contexts Related words like cumulative are more common but I never hear people use the word cumulate in everyday speech.

cumulate と accumulate はどう違いますか? | HiNative


What is the difference between “accumulative” and “cumulative”?

I'm having a hard time understanding the real distinction between accumulative and cumulative.

meaning - What is the difference between “accumulative” and “cumulative”? - English Language & Usage Stack Exchange

英語を母国語とする人でも単語の意味を質問するんだ?と思いましたが、日本人も日本語の単語を質問しているので、当然と言えば当然か、と思いました。

cumulative と Accumulate ってどちらも “累積” のような意味があるみたいです。 細かい違いはわからないですけれども。 形容詞的に使う場合は cumulative を使うことが多いのかな。 動詞的に使う場合は Accumulate を使うことが多いのかな。 上の引用の回答から察するとそう感じました。

話を戻して。

A universal function (or ufunc for short) is a function that operates on ndarrays in an element-by-element fashion, supporting array broadcasting, type casting, and several other standard features. That is, a ufunc is a “vectorized” wrapper for a function

Universal functions (ufunc)

ufunc 自体の説明は読んでもあまりよくわかりませんでした。 numpy.ufunc.accumulate で言うと、 accumulate (累積)しながら Universal (汎用)な関数 (ufunc) を適用してくれるものだと認識しました。

Examples

1-D array examples:

>>> np.add.accumulate([2, 3, 5])
array([ 2,  5, 10])
>>> np.multiply.accumulate([2, 3, 5])
array([ 2,  6, 30])

numpy.ufunc.accumulate — NumPy v1.14 Manual

accumulate の Examples を見ると、 add という ufunc で加算しながら累積できて、 multiply という ufunc で乗算しながら累積できることがわかりました。 np.add.accumulatenumpy.cumsum と同じ意味ですね。

ndarray2 = np.maximum.accumulate(ndarray1)
ndarray2

結果です。

array([  -20,  7655,  8620,  8620,  8620, 15082, 17608, 17608, 17608, 17608])

グラフを載せておきます。

このグラフの 2 つのラインの差が一番大きいところが最大ドローダウンになるみたいです。 パッと見た感じではインデックスが 4 のところぽいですけれども、どうかな。

Basic Operations

Arithmetic operators on arrays apply elementwise. A new array is created and filled with the result.

Basic Operations

NumPy の ndarray 同士を算術演算子で計算することができるみたいです。 要素ごとの計算を繰り返しの処理なしに 1 行で記述できてしまうみたいです。 NumPy 便利です。

np.maximum.accumulate した結果から cumsum した結果を減算します。

ndarray3 = ndarray2 - ndarray1
ndarray3

結果です。

array([    0,     0,     0,  8790, 12253,     0,     0,  5699, 12083,  7218])

やっぱりインデックス 4 のところが一番大きいみたいでした。

max

Stack Overflow の質問では最大ドローダウンの値を求めてはいないみたいでしたが、今回は値を求めたかったので、 numpy.amax 関数を使ってみました。

np.amax(ndarray3)

結果です。

12253

インデックス 4 のところの 12253 の値を求めることができました。

なので、最大ドローダウンは 12,253 ということになります。 最大ドローダウンは口座残高の 20% 程度におさまるといいなと思っているのですけれども、そんな売買ルールができるかな。

NumPy の ndarray を使って最大ドローダウンを計算する関数

最大ドローダウンを計算する関数にしてみました。

def mdd(a):
    a1 = a.cumsum()
    a2 = np.maximum.accumulate(a1)
    a3 = a2 - a1
    return a3.max()

mdd(np.array([-20, 7675, 965, -8790, -3463, 18715, 2526, -5699, -6384, 4865]))

結果です。

12253

pandas の Series を使って最大ドローダウンを計算する関数

pandas でもできそうでしたので、同じように最大ドローダウンを計算する関数にしてみました。

def mdd(s):
    s1 = s.cumsum()
    s2 = s1.cummax()
    s3 = s2 - s1
    return s3.max()

mdd(pd.Series([-20, 7675, 965, -8790, -3463, 18715, 2526, -5699, -6384, 4865]))

cumsumcummaxmax の関数を使ってみました。

結果です。

12253

Gist のソース

調べたことを Jupyter Notebook にして Gist にアップしました。

終わり

こういうのを調べてみて NumPy や pandas の便利なところが少しずつ分かってきた気がします。

27 Sep 2018 追記

最大ドローダウン率も計算してみました。

NumPy

def mddr(a):
    a1 = a.cumsum()
    a2 = np.maximum.accumulate(a1)
    a3 = a2 - a1
    i = np.argmax(a3)
    r = (a2[i] - a1[i]) / a2[i]
    return r

mddr(np.array([-20, 7675, 965, -8790, -3463, 18715, 2526, -5699, -6384, 4865]))

numpy.argmax を使いました。

結果です。

1.421461716937355

pandas

def mddr(s):
    s1 = s.cumsum()
    s2 = s1.cummax()
    s3 = s2 - s1
    i = s3.idxmax()
    r = (s2[i] - s1[i]) / s2[i]
    return r

mddr(pd.Series([-20, 7675, 965, -8790, -3463, 18715, 2526, -5699, -6384, 4865]))

pandas.Series.idxmax を使いました。

結果です。

1.421461716937355