使用python中的矢量化解决方案计算最大下拉
是量化金融中常用的风险度量,用于评估经历过的最大负回报 最近,我对使用循环方法计算最大压降的时间感到不耐烦使用python中的矢量化解决方案计算最大下拉,python,numpy,pandas,quantitative-finance,Python,Numpy,Pandas,Quantitative Finance,是量化金融中常用的风险度量,用于评估经历过的最大负回报 最近,我对使用循环方法计算最大压降的时间感到不耐烦 def max_dd_loop(returns): """returns is assumed to be a pandas series""" max_so_far = None start, end = None, None r = returns.add(1).cumprod() for r_start in r.index:
def max_dd_loop(returns):
"""returns is assumed to be a pandas series"""
max_so_far = None
start, end = None, None
r = returns.add(1).cumprod()
for r_start in r.index:
for r_end in r.index:
if r_start < r_end:
current = r.ix[r_end] / r.ix[r_start] - 1
if (max_so_far is None) or (current < max_so_far):
max_so_far = current
start, end = r_start, r_end
return max_so_far, start, end
给定收益的时间序列,我们需要评估从起点到终点的每个组合的总收益 第一个技巧是将收益的时间序列转换为一系列收益指数。给定一系列回报指数,我可以计算任何子周期的回报,回报指数在开始的ri_0和结束的ri_1。计算结果为:ri_1/ri_0-1 第二个技巧是产生回报指数的第二系列倒数。如果r是我的收益指数系列,那么1/r是我的逆指数系列 第三个技巧是取r*(1/r)的矩阵积。转置 r是nx1矩阵。(1/r)。转置是一个1×n矩阵。生成的产品包含ri_j/ri_k的每个组合。只要减去1,我就得到了回报 第四个技巧是确保我限制分母在分子表示周期之前表示周期 下面是我的矢量化函数
import numpy as np
import pandas as pd
def max_dd(returns):
# make into a DataFrame so that it is a 2-dimensional
# matrix such that I can perform an nx1 by 1xn matrix
# multiplication and end up with an nxn matrix
r = pd.DataFrame(returns).add(1).cumprod()
# I copy r.T to ensure r's index is not the same
# object as 1 / r.T's columns object
x = r.dot(1 / r.T.copy()) - 1
x.columns.name, x.index.name = 'start', 'end'
# let's make sure we only calculate a return when start
# is less than end.
y = x.stack().reset_index()
y = y[y.start < y.end]
# my choice is to return the periods and the actual max
# draw down
z = y.set_index(['start', 'end']).iloc[:, 0]
return z.min(), z.argmin()[0], z.argmin()[1]
循环解决方案的相同测试如下:
10: 0.032 seconds
50: 0.044 seconds
100: 0.055 seconds
150: 0.082 seconds
200: 0.047 seconds
10: 0.153 seconds
50: 3.169 seconds
100: 12.355 seconds
150: 27.756 seconds
200: 49.726 seconds
编辑 亚历山大的回答提供了更好的结果。使用修改过的代码进行相同的测试
10: 0.000 seconds
50: 0.000 seconds
100: 0.004 seconds
150: 0.007 seconds
200: 0.008 seconds
我将他的代码修改为以下函数:
def max_dd(returns):
"""Assumes returns is a pandas Series"""
r = returns.add(1).cumprod()
dd = r.div(r.cummax()).sub(1)
mdd = dd.min()
end = dd.argmin()
start = r.loc[:end].argmax()
return mdd, start, end
def max_dd(returns):
r = returns.add(1).cumprod()
dd = r.div(r.cummax()).sub(1)
mdd = drawdown.min()
end = drawdown.argmin()
start = r.loc[:end].argmax()
return mdd, start, end
df_returns
被假定为一个返回数据框架,其中每列是一个单独的策略/经理/安全,每行是一个新日期(例如每月或每天)
我第一次建议使用
.expansing()
窗口,但对于.cumprod()
和.cummax()
内置窗口,显然没有必要使用该窗口来计算任何给定点的最大压降:
df = pd.DataFrame(data={'returns': np.random.normal(0.001, 0.05, 1000)}, index=pd.date_range(start=date(2016,1,1), periods=1000, freq='D'))
df = pd.DataFrame(data={'returns': np.random.normal(0.001, 0.05, 1000)},
index=pd.date_range(start=date(2016, 1, 1), periods=1000, freq='D'))
df['cumulative_return'] = df.returns.add(1).cumprod().subtract(1)
df['max_drawdown'] = df.cumulative_return.add(1).div(df.cumulative_return.cummax().add(1)).subtract(1)
我最近遇到了一个类似的问题,但不是全局MDD,而是要求我查找每个峰值后间隔的MDD。另外,在我的例子中,我应该单独使用每个策略的MDD,因此不需要应用
cumprod
。我的矢量化实现也基于
以下是运行此代码后的示例:
nw max_peaks_idx dd mdd
0 10000.000 0 0.000000 0.000000
1 9696.948 0 -0.030305 -0.030305
2 9538.576 0 -0.046142 -0.046142
3 9303.953 0 -0.069605 -0.069605
4 9247.259 0 -0.075274 -0.075274
5 9421.519 0 -0.057848 -0.075274
6 9315.938 0 -0.068406 -0.075274
7 9235.775 0 -0.076423 -0.076423
8 9091.121 0 -0.090888 -0.090888
9 9033.532 0 -0.096647 -0.096647
10 8947.504 0 -0.105250 -0.105250
11 8841.551 0 -0.115845 -0.115845
这是应用于完整数据集的完整数据集的图像
虽然矢量化了,但这段代码可能比另一段代码慢,因为对于每个时间序列,都应该有许多峰值,每个峰值都需要计算,所以O(n_峰值*n_间隔)
PS:我本可以消除
dd
和mdd
列中的零值,但我发现这些值有助于指示在时间序列中观察到新峰值的时间。看看这个问题和答案是否提供了帮助:也可以看到(可能是重复的?)游戏进行得很晚,但我认为r.loc[:end].argmax()
将给您带来一个问题。您需要r.loc[:end]。排序索引(升序=False)。argmax()
。如果序列中有多个零(多个高水位线),则当前行按原样返回第一次而不是最后一次出现,并产生一个太早的开始日期。@除非我遗漏了什么,如果有多个相同的高水位线,这是一个关于何时出现最大水位下降的解释问题。此外,这不会影响回报率的计算。假设你有一个提取序列(不是回报序列)[0,0,-2,0,-3,-5],按一年的前6个月进行索引。-5水位下降期之前的最高水位线是第三个0(4月),而不是第一个,因此不使用。sort_index(ascending=False)
来反转该系列会错误地告诉您水位下降是从1月开始的。我猜这是一种解释,但在这种方法下,不是每个水位下降都从0月或1月开始吗?我从未见过这样描述的开始日期。HWMs不必完全相同,就可以在缩进序列中获得多个零。谢谢Alexander!这比我给出的答案快得多。我没有注意到cummax()方法。我必须稍微修改代码以返回起点和终点,但这正是我想要的。
returns cumulative_return max_drawdown
2016-01-01 -0.014522 -0.014522 0.000000
2016-01-02 -0.022769 -0.036960 -0.022769
2016-01-03 0.026735 -0.011214 0.000000
2016-01-04 0.054129 0.042308 0.000000
2016-01-05 -0.017562 0.024004 -0.017562
2016-01-06 0.055254 0.080584 0.000000
2016-01-07 0.023135 0.105583 0.000000
2016-01-08 -0.072624 0.025291 -0.072624
2016-01-09 -0.055799 -0.031919 -0.124371
2016-01-10 0.129059 0.093020 -0.011363
2016-01-11 0.056123 0.154364 0.000000
2016-01-12 0.028213 0.186932 0.000000
2016-01-13 0.026914 0.218878 0.000000
2016-01-14 -0.009160 0.207713 -0.009160
2016-01-15 -0.017245 0.186886 -0.026247
2016-01-16 0.003357 0.190869 -0.022979
2016-01-17 -0.009284 0.179813 -0.032050
2016-01-18 -0.027361 0.147533 -0.058533
2016-01-19 -0.058118 0.080841 -0.113250
2016-01-20 -0.049893 0.026914 -0.157492
2016-01-21 -0.013382 0.013173 -0.168766
2016-01-22 -0.020350 -0.007445 -0.185681
2016-01-23 -0.085842 -0.092648 -0.255584
2016-01-24 0.022406 -0.072318 -0.238905
2016-01-25 0.044079 -0.031426 -0.205356
2016-01-26 0.045782 0.012917 -0.168976
2016-01-27 -0.018443 -0.005764 -0.184302
2016-01-28 0.021461 0.015573 -0.166797
2016-01-29 -0.062436 -0.047836 -0.218819
2016-01-30 -0.013274 -0.060475 -0.229189
... ... ... ...
2018-08-28 0.002124 0.559122 -0.478738
2018-08-29 -0.080303 0.433921 -0.520597
2018-08-30 -0.009798 0.419871 -0.525294
2018-08-31 -0.050365 0.348359 -0.549203
2018-09-01 0.080299 0.456631 -0.513004
2018-09-02 0.013601 0.476443 -0.506381
2018-09-03 -0.009678 0.462153 -0.511158
2018-09-04 -0.026805 0.422960 -0.524262
2018-09-05 0.040832 0.481062 -0.504836
2018-09-06 -0.035492 0.428496 -0.522411
2018-09-07 -0.011206 0.412489 -0.527762
2018-09-08 0.069765 0.511031 -0.494817
2018-09-09 0.049546 0.585896 -0.469787
2018-09-10 -0.060201 0.490423 -0.501707
2018-09-11 -0.018913 0.462235 -0.511131
2018-09-12 -0.094803 0.323611 -0.557477
2018-09-13 0.025736 0.357675 -0.546088
2018-09-14 -0.049468 0.290514 -0.568542
2018-09-15 0.018146 0.313932 -0.560713
2018-09-16 -0.034118 0.269104 -0.575700
2018-09-17 0.012191 0.284576 -0.570527
2018-09-18 -0.014888 0.265451 -0.576921
2018-09-19 0.041180 0.317562 -0.559499
2018-09-20 0.001988 0.320182 -0.558623
2018-09-21 -0.092268 0.198372 -0.599348
2018-09-22 -0.015386 0.179933 -0.605513
2018-09-23 -0.021231 0.154883 -0.613888
2018-09-24 -0.023536 0.127701 -0.622976
2018-09-25 0.030160 0.161712 -0.611605
2018-09-26 0.025528 0.191368 -0.601690
def calc_MDD(networth):
df = pd.Series(networth, name="nw").to_frame()
max_peaks_idx = df.nw.expanding(min_periods=1).apply(lambda x: x.argmax()).fillna(0).astype(int)
df['max_peaks_idx'] = pd.Series(max_peaks_idx).to_frame()
nw_peaks = pd.Series(df.nw.iloc[max_peaks_idx.values].values, index=df.nw.index)
df['dd'] = ((df.nw-nw_peaks)/nw_peaks)
df['mdd'] = df.groupby('max_peaks_idx').dd.apply(lambda x: x.expanding(min_periods=1).apply(lambda y: y.min())).fillna(0)
return df
nw max_peaks_idx dd mdd
0 10000.000 0 0.000000 0.000000
1 9696.948 0 -0.030305 -0.030305
2 9538.576 0 -0.046142 -0.046142
3 9303.953 0 -0.069605 -0.069605
4 9247.259 0 -0.075274 -0.075274
5 9421.519 0 -0.057848 -0.075274
6 9315.938 0 -0.068406 -0.075274
7 9235.775 0 -0.076423 -0.076423
8 9091.121 0 -0.090888 -0.090888
9 9033.532 0 -0.096647 -0.096647
10 8947.504 0 -0.105250 -0.105250
11 8841.551 0 -0.115845 -0.115845