【问题标题】:Calculate max draw down with a vectorized solution in python使用python中的矢量化解决方案计算最大下降
【发布时间】:2016-08-13 12:45:20
【问题描述】:

Maximum Drawdown 是量化金融中用于评估最大负回报的常见风险指标。

最近,我对使用循环方法计算最大回撤的时间感到不耐烦。

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

我熟悉矢量化解决方案会更好的普遍看法。

问题是:

  • 我可以向量化这个问题吗?
  • 这个解决方案是什么样的?
  • 它有多大好处?

编辑

我将 Alexander 的答案修改为以下函数:

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

【问题讨论】:

  • 看看这个问答是否有帮助:stackoverflow.com/questions/21058333/…
  • 另见stackoverflow.com/questions/22607324/…(可能重复?)
  • 游戏迟到了,但我认为r.loc[:end].argmax() 会给你一个问题。你想要r.loc[:end].sort_index(ascending=False).argmax()。如果您的系列中有多个零(多个高水位线),则当前行将按原样返回第一次出现而不是最后一次出现,并产生一个过早的开始日期。
  • @BradSolomon 除非我遗漏了什么,如果有多个相同的高水位线,那么最大回撤期发生的时间是一个解释问题。此外,这不会影响回报的计算。
  • 假设您有一个回撤系列(不是返回系列)[0, 0, -2, 0, -3, -5] 由前 6 个月索引那一年。 -5 回撤期之前的高水位线是第三个 0(4 月),而不是第一个,所以不使用.sort_index(ascending=False) 来反转系列会错误地告诉您您的回撤开始于 1 月。我猜这是解释,但在此之下方法不是每次回撤都从第 0 个月或第 1 个月开始吗?我从未见过这样描述的开始日期。 HWM 不一定要相同才能在您的回撤系列中获得多个零。

标签: python numpy pandas quantitative-finance


【解决方案1】:

df_returns 被假定为一个回报数据框,其中每一列是一个单独的策略/经理/证券,每一行是一个新日期(例如每月或每天)。

cum_returns = (1 + df_returns).cumprod()
drawdown =  1 - cum_returns.div(cum_returns.cummax())

【讨论】:

  • 谢谢亚历山大!这比我给出的答案要快得多。我没有注意到 cummax() 方法。我必须稍微修改代码以返回起点和终点,但这正是我想要的。
  • 这没有正确考虑到系列中的第一个回报。此示例中的高水位线应为 1 而不是 0.9。 df_returns = pd.Series([-.1,-.1,-.1,-.1]) cum_returns = (1 + df_returns).cumprod() drawdown = 1 - cum_returns.div(cum_returns.cummax()) ) print(cum_returns.cummax()) print(drawdown.max()) 0 0.9 1 0.9 2 0.9 3 0.9 4 0.9 数据类型:float64 0.18999999999999995
  • @Pilgrim 您的观察似乎是正确的。需要在初始投资日期包括零回报,例如df_returns = pd.concat([pd.Series(0, index=[{initial_investment_date}]), df_returns])。这可能是在最初的问题中假设的,但没有样本数据,因此无法确定。
【解决方案2】:

我最初建议使用 .expanding() 窗口,但显然没有必要使用 .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)

            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

【讨论】:

    【解决方案3】:

    给定一个时间序列的回报,我们需要评估每个起点到终点组合的总回报。

    第一个技巧是将时间序列的回报转换为一系列回报指数。给定一系列回报指数,我可以计算任何子时期的回报,回报指数在开始 ri_0 和结束 ri_1。计算为:ri_1 / ri_0 - 1。

    第二个技巧是产生第二个返回指数的倒数序列。如果 r 是我的一系列回报指数,那么 1 / r 是我的一系列倒数。

    第三个技巧是取r * (1 / r)的矩阵乘积。转置。

    r 是一个 n x 1 矩阵。 (1 / r).Transpose 是一个 1 x 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、50、100、150、200] 的时间序列上运行了 10 次迭代。所用时间如下:

    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
    

    编辑

    Alexander 的回答提供了卓越的结果。使用修改后的代码进行相同的测试

    10:   0.000 seconds
    50:   0.000 seconds
    100:  0.004 seconds
    150:  0.007 seconds
    200:  0.008 seconds
    

    我将他的代码修改成如下函数:

    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
    

    【讨论】:

      【解决方案4】:

      我最近遇到了类似的问题,但不是全局 MDD,而是要求我找到每个峰值之后的间隔的 MDD。此外,就我而言,我应该单独采用每个策略的 MDD,因此不需要应用 cumprod。我的矢量化实现也是基于Investopedia

      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
      

      这是应用于完整数据集的完整图像。

      虽然是矢量化的,但这段代码可能比另一个慢,因为对于每个时间序列,应该有很多峰值,每个峰值都需要计算,所以 O(n_peaks*n_intervals)。

      PS:我本可以消除 ddmdd 列中的零值,但我发现这些值有助于指示在时间序列中观察到新峰值的时间。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-10-18
        相关资源
        最近更新 更多