【问题标题】:Compute *rolling* maximum drawdown of pandas Series计算*滚动*熊猫系列的最大回撤
【发布时间】:2014-01-30 06:05:06
【问题描述】:

编写一个计算时间序列最大回撤的函数非常容易。用O(n) 时间而不是O(n^2) 时间编写它需要一点思考。但这并不是那么糟糕。这将起作用:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def max_dd(ser):
    max2here = pd.expanding_max(ser)
    dd2here = ser - max2here
    return dd2here.min()

让我们建立一个简短的系列来尝试一下:

np.random.seed(0)
n = 100
s = pd.Series(np.random.randn(n).cumsum())
s.plot()
plt.show()

正如预期的那样,max_dd(s) 最终显示在 -17.6 左右。好,好,大。现在说我有兴趣计算这个系列的滚动回撤。 IE。对于每一步,我想从指定长度的前一个子系列中计算最大回撤。使用pd.rolling_apply 很容易做到这一点。它是这样工作的:

rolling_dd = pd.rolling_apply(s, 10, max_dd, min_periods=0)
df = pd.concat([s, rolling_dd], axis=1)
df.columns = ['s', 'rol_dd_10']
df.plot()

这非常有效。但是感觉很慢。 pandas 或其他工具包中是否有特别灵活的算法可以快速执行此操作?我尝试编写一些定制的东西:它跟踪各种中间数据(观察到的最大值的位置、先前发现的回撤位置)以减少大量冗余计算。它确实节省了一些时间,但不是很多,也不是尽可能多的。

我认为这是因为 Python/Numpy/Pandas 中的所有循环开销。但是我目前在 Cython 中的流利程度还不够,无法真正知道如何从那个角度开始攻击它。我希望有人以前尝试过这个。或者,也许有人可能想看看我的“手工”代码并愿意帮助我将其转换为 Cython。


编辑: 对于想要查看此处提到的所有功能(以及其他一些功能!)的任何人,请查看 iPython 笔记本:http://nbviewer.ipython.org/gist/8one6/8506455

它展示了解决这个问题的一些方法是如何关联的,检查它们是否给出了相同的结果,并展示了它们在不同大小的数据上的运行时间。

如果有人感兴趣,我在帖子中提到的“定制”算法是rolling_dd_custom。如果在 Cython 中实施,我认为这可能是一个非常快速的解决方案。

【问题讨论】:

    标签: python algorithm numpy pandas


    【解决方案1】:

    这是滚动最大回撤函数的 numpy 版本。 windowed_view 是一个单行函数的包装器,它使用 numpy.lib.stride_tricks.as_strided 来生成一维数组的内存高效二维窗口视图(下面的完整代码)。一旦我们有了这个窗口视图,计算基本上与您的max_dd 相同,但为一个 numpy 数组编写,并沿第二个轴(即axis=1)应用。

    def rolling_max_dd(x, window_size, min_periods=1):
        """Compute the rolling maximum drawdown of `x`.
    
        `x` must be a 1d numpy array.
        `min_periods` should satisfy `1 <= min_periods <= window_size`.
    
        Returns an 1d array with length `len(x) - min_periods + 1`.
        """
        if min_periods < window_size:
            pad = np.empty(window_size - min_periods)
            pad.fill(x[0])
            x = np.concatenate((pad, x))
        y = windowed_view(x, window_size)
        running_max_y = np.maximum.accumulate(y, axis=1)
        dd = y - running_max_y
        return dd.min(axis=1)
    

    这是一个完整的演示函数的脚本:

    import numpy as np
    from numpy.lib.stride_tricks import as_strided
    import pandas as pd
    import matplotlib.pyplot as plt
    
    
    def windowed_view(x, window_size):
        """Creat a 2d windowed view of a 1d array.
    
        `x` must be a 1d numpy array.
    
        `numpy.lib.stride_tricks.as_strided` is used to create the view.
        The data is not copied.
    
        Example:
    
        >>> x = np.array([1, 2, 3, 4, 5, 6])
        >>> windowed_view(x, 3)
        array([[1, 2, 3],
               [2, 3, 4],
               [3, 4, 5],
               [4, 5, 6]])
        """
        y = as_strided(x, shape=(x.size - window_size + 1, window_size),
                       strides=(x.strides[0], x.strides[0]))
        return y
    
    
    def rolling_max_dd(x, window_size, min_periods=1):
        """Compute the rolling maximum drawdown of `x`.
    
        `x` must be a 1d numpy array.
        `min_periods` should satisfy `1 <= min_periods <= window_size`.
    
        Returns an 1d array with length `len(x) - min_periods + 1`.
        """
        if min_periods < window_size:
            pad = np.empty(window_size - min_periods)
            pad.fill(x[0])
            x = np.concatenate((pad, x))
        y = windowed_view(x, window_size)
        running_max_y = np.maximum.accumulate(y, axis=1)
        dd = y - running_max_y
        return dd.min(axis=1)
    
    
    def max_dd(ser):
        max2here = pd.expanding_max(ser)
        dd2here = ser - max2here
        return dd2here.min()
    
    
    if __name__ == "__main__":
        np.random.seed(0)
        n = 100
        s = pd.Series(np.random.randn(n).cumsum())
    
        window_length = 10
    
        rolling_dd = pd.rolling_apply(s, window_length, max_dd, min_periods=0)
        df = pd.concat([s, rolling_dd], axis=1)
        df.columns = ['s', 'rol_dd_%d' % window_length]
        df.plot(linewidth=3, alpha=0.4)
    
        my_rmdd = rolling_max_dd(s.values, window_length, min_periods=1)
        plt.plot(my_rmdd, 'g.')
    
        plt.show()
    

    该图显示了您的代码生成的曲线。绿点由rolling_max_dd 计算。

    时间比较,与n = 10000window_length = 500

    In [2]: %timeit rolling_dd = pd.rolling_apply(s, window_length, max_dd, min_periods=0)
    1 loops, best of 3: 247 ms per loop
    
    In [3]: %timeit my_rmdd = rolling_max_dd(s.values, window_length, min_periods=1)
    10 loops, best of 3: 38.2 ms per loop
    

    rolling_max_dd 大约快 6.5 倍。对于较小的窗口长度,加速效果更好。例如,使用window_length = 200,它几乎快了 13 倍。

    要处理 NA,您可以在将数组传递给 rolling_max_dd 之前使用 fillna 方法预处理 Series

    【讨论】:

    • 这种比较在上下文中有点不公平,因为到达padded_serwindow_length 需要计算,你没有计时。 (不过,您的方法仍然会胜出一个数量级。)
    • 没错,我只对计算的主要部分进行了计时。我在代码中添加了填充以获得与系列开头的 pandas rolling_apply 函数相同的输出。如果只需要“完整”窗口的结果,则可以删除填充步骤,而是将 rolling_max_dd 的结果移动 window_length - 1。
    • 有什么理由用您选择的特定值填充吗?我发现这个选择有点令人困惑,尽管我不认为它会导致问题。 (我可能会用该系列的第一个值填充。)另外,我倾向于接受这个答案,但在我这样做之前,您介意发布完整解决方案的时间吗? IE。您能否发布一个可以替代我的方法的单个函数的时间,以便比较是苹果对苹果?能否请您在n = 10000window=500 发布时间?
    • 另外:您的方法是在numpy 而不是pandas 中完成的。那么它与NA的关系如何?我使用 pandas 编写的简单版本以我认为适合手头问题的方式处理 NA。
    • 我更新了代码以在rolling_max_dd 函数中包含填充,并更改了时间比较以使用更大的nwindow_length。它不像以前那么令人印象深刻。 :( 填充值现在只是系列的第一个值(不确定我之前在想什么)。可以通过使用 fillna 方法预处理系列来处理 NA。
    【解决方案2】:

    为了后代和完整性,这就是我在 Cython 中总结的内容。 MemoryViews 大大加快了速度。有一些工作要做以确保我正确输入了所有内容(抱歉,c 类型语言的新手)。但最后我认为它工作得很好。对于典型的用例,与常规 python 相比的加速比约为 100 倍或 150 倍。要调用的函数是 cy_rolling_dd_custom_mv,其中第一个参数 (ser) 应该是一维 numpy 数组,第二个参数 (window) 应该是一个正整数。该函数返回一个 numpy 内存视图,在大多数情况下运行良好。如果你需要得到一个很好的输出数组,你可以显式调用np.array(result)

    import numpy as np
    cimport numpy as np
    cimport cython
    
    DTYPE = np.float64
    ctypedef np.float64_t DTYPE_t
    
    @cython.boundscheck(False)
    @cython.wraparound(False)
    @cython.nonecheck(False)
    cpdef tuple cy_dd_custom_mv(double[:] ser):
        cdef double running_global_peak = ser[0]
        cdef double min_since_global_peak = ser[0]
        cdef double running_max_dd = 0
    
        cdef long running_global_peak_id = 0
        cdef long running_max_dd_peak_id = 0
        cdef long running_max_dd_trough_id = 0
    
        cdef long i
        cdef double val
        for i in xrange(ser.shape[0]):
            val = ser[i]
            if val >= running_global_peak:
                running_global_peak = val
                running_global_peak_id = i
                min_since_global_peak = val
            if val < min_since_global_peak:
                min_since_global_peak = val
                if val - running_global_peak <= running_max_dd:
                    running_max_dd = val - running_global_peak
                    running_max_dd_peak_id = running_global_peak_id
                    running_max_dd_trough_id = i
        return (running_max_dd, running_max_dd_peak_id, running_max_dd_trough_id, running_global_peak_id)
    
    @cython.boundscheck(False)
    @cython.wraparound(False)
    @cython.nonecheck(False)
    def cy_rolling_dd_custom_mv(double[:] ser, long window):
        cdef double[:, :] result
        result = np.zeros((ser.shape[0], 4))
    
        cdef double running_global_peak = ser[0]
        cdef double min_since_global_peak = ser[0]
        cdef double running_max_dd = 0
        cdef long running_global_peak_id = 0
        cdef long running_max_dd_peak_id = 0
        cdef long running_max_dd_trough_id = 0
        cdef long i
        cdef double val
        cdef int prob_1
        cdef int prob_2
        cdef tuple intermed
        cdef long newthing
    
        for i in xrange(ser.shape[0]):
            val = ser[i]
            if i < window:
                if val >= running_global_peak:
                    running_global_peak = val
                    running_global_peak_id = i
                    min_since_global_peak = val
                if val < min_since_global_peak:
                    min_since_global_peak = val
                    if val - running_global_peak <= running_max_dd:
                        running_max_dd = val - running_global_peak
                        running_max_dd_peak_id = running_global_peak_id
                        running_max_dd_trough_id = i
    
                result[i, 0] = <double>running_max_dd
                result[i, 1] = <double>running_max_dd_peak_id
                result[i, 2] = <double>running_max_dd_trough_id
                result[i, 3] = <double>running_global_peak_id
    
            else:
                prob_1 = 1 if result[i-1, 3] <= float(i - window) else 0
                prob_2 = 1 if result[i-1, 1] <= float(i - window) else 0
                if prob_1 or prob_2:
                    intermed = cy_dd_custom_mv(ser[i-window+1:i+1])
                    result[i, 0] = <double>intermed[0]
                    result[i, 1] = <double>(intermed[1] + i - window + 1)
                    result[i, 2] = <double>(intermed[2] + i - window + 1)
                    result[i, 3] = <double>(intermed[3] + i - window + 1)
                else:
                    newthing = <long>(int(result[i-1, 3]))
                    result[i, 3] = i if ser[i] >= ser[newthing] else result[i-1, 3]
                    if val - ser[newthing] <= result[i-1, 0]:
                        result[i, 0] = <double>(val - ser[newthing])
                        result[i, 1] = <double>result[i-1, 3]
                        result[i, 2] = <double>i
                    else:
                        result[i, 0] = <double>result[i-1, 0]
                        result[i, 1] = <double>result[i-1, 1]
                        result[i, 2] = <double>result[i-1, 2]
        cdef double[:] finalresult = result[:, 0]
        return finalresult
    

    【讨论】:

      【解决方案3】:

      这是一个 Numba 加速的解决方案:

      import pandas as pd
      import numpy as np
      import numba
      from time import time
      
      n = 10000
      returns = pd.Series(np.random.normal(1.001, 0.01, n), pd.date_range("2020-01-01", periods=n, freq="1min"))
      
      @numba.njit
      def max_drawdown(cum_returns):
          max_drawdown = 0.0
          current_max_ret = cum_returns[0]
          for ret in cum_returns:
              if ret > current_max_ret:
                  current_max_ret = ret
              max_drawdown = max(max_drawdown, 1 - ret / current_max_ret)
          return max_drawdown
      
      t = time()
      rolling_1h_max_dd = returns.cumprod().rolling("1h").apply(max_drawdown, raw=True)
      print("Fast:", time() - t);
      
      def max_drawdown_slow(x):
          return (1 - x / x.cummax()).max()
      
      t = time()
      rolling_1h_max_dd_slow = returns.cumprod().rolling("1h").apply(max_drawdown_slow, raw=False)
      print("Slow:", time() - t);
      
      assert rolling_1h_max_dd.equals(rolling_1h_max_dd_slow)
      

      输出:

      Fast: 0.05633878707885742
      Slow: 4.540301084518433
      

      => 80 倍加速

      【讨论】:

        【解决方案4】:
        # BEGIN: TRADEWAVE MOVING AVERAGE CROSSOVER EXAMPLE
        THRESHOLD = 0.005 
        INTERVAL = 43200 
        SHORT = 10 
        LONG = 90 
        
        def initialize():
        
            storage.invested = storage.get('invested', False)
        
        def tick():
        
            short_term = data(interval=INTERVAL).btc_usd.ma(SHORT)
            long_term = data(interval=INTERVAL).btc_usd.ma(LONG)
            diff = 100 * (short_term - long_term) / ((short_term + long_term) / 2)
        
            if diff >= THRESHOLD and not storage.invested:
                buy(pairs.btc_usd)
                storage.invested = True
            elif diff <= -THRESHOLD and storage.invested:
                sell(pairs.btc_usd)
                storage.invested = False
        
            plot('short_term', short_term) 
            plot('long_term', long_term)
            # END: TRADEWAVE MOVING AVERAGE CROSSOVER EXAMPLE  
            ##############################################################
        
            ##############################################################
            # BEGIN MAX DRAW DOWN by litepresence
            # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv  
        
            dd()
        
        ROLLING = 30 # days       
        
        def dd():
        
            dd, storage.max_dd = max_dd(0)
            bnh_dd, storage.max_bnh_dd = bnh_max_dd(0)
            rolling_dd, storage.max_rolling_dd = max_dd(
                ROLLING*86400/info.interval)    
            rolling_bnh_dd, storage.max_rolling_bnh_dd = bnh_max_dd(
                ROLLING*86400/info.interval)
        
            plot('dd', dd, secondary=True)   
            plot('bnh_dd', bnh_dd, secondary=True)    
            plot('rolling_dd', rolling_dd, secondary=True)  
            plot('rolling_bnh_dd', rolling_bnh_dd, secondary=True)       
            plot('zero', 0, secondary=True)
            if info.tick == 0:
                plot('dd_floor', -200, secondary=True)
        
        def max_dd(rolling):
        
            port_value = float(portfolio.usd+portfolio.btc*data.btc_usd.price)
            max_value = 'max_value_' + str(rolling)
            values_since_max = 'values_since_max_' + str(rolling)
            max_dd = 'max_dd_' + str(rolling)
            storage[max_value] = storage.get(max_value, [port_value])
            storage[values_since_max] = storage.get(values_since_max, [port_value])
            storage[max_dd] = storage.get(max_dd, [0])
            storage[max_value].append(port_value)    
            if port_value > max(storage[max_value]):
                storage[values_since_max] = [port_value]
            else:
                storage[values_since_max].append(port_value)
            storage[max_value] = storage[max_value][-rolling:]
            storage[values_since_max] = storage[values_since_max][-rolling:]    
            dd = -100*(max(storage[max_value]) - storage[values_since_max][-1]
                )/max(storage[max_value])
            storage[max_dd].append(float(dd))
            storage[max_dd] = storage[max_dd][-rolling:]
            max_dd = min(storage[max_dd])
        
            return (dd, max_dd)
        
        def bnh_max_dd(rolling):
        
            coin = data.btc_usd.price
            bnh_max_value = 'bnh_max_value_' + str(rolling)
            bnh_values_since_max = 'bnh_values_since_max_' + str(rolling)
            bnh_max_dd = 'bnh_max_dd_' + str(rolling)    
            storage[bnh_max_value] = storage.get(bnh_max_value, [coin])
            storage[bnh_values_since_max] = storage.get(bnh_values_since_max, [coin])
            storage[bnh_max_dd] = storage.get(bnh_max_dd, [0]) 
            storage[bnh_max_value].append(coin)
            if coin > max(storage[bnh_max_value]):
                storage[bnh_values_since_max] = [coin]        
            else:
                storage[bnh_values_since_max].append(coin)
            storage[bnh_max_value] = storage[bnh_max_value][-rolling:]        
            storage[bnh_values_since_max] = storage[bnh_values_since_max][-rolling:]  
            bnh_dd = -100*(max(storage[bnh_max_value]) - storage[bnh_values_since_max][-1]
                )/max(storage[bnh_max_value])
            storage[bnh_max_dd].append(float(bnh_dd))
            storage[bnh_max_dd] = storage[bnh_max_dd][-rolling:]  
            bnh_max_dd = min(storage[bnh_max_dd])   
        
            return (bnh_dd, bnh_max_dd)    
        
        
        def stop():
        
            log('MAX DD......: %.2f pct' % storage.max_dd)
            log('R MAX DD....: %.2f pct' % storage.max_rolling_dd)
            log('MAX BNH DD..: %.2f pct' % storage.max_bnh_dd)
            log('R MAX BNH DD: %.2f pct' % storage.max_rolling_bnh_dd)     
        

        [2015-03-04 00:00:00] MAX DD......: -67.94 pct
        [2015-03-04 00:00:00] R MAX DD....: -4.93 pct
        [2015-03-04 00:00:00] MAX BNH DD..: -82.88 pct
        [2015-03-04 00:00:00] R MAX BNH DD: -26.38 pct
        
        • 下拉
        • 最大下降
        • 买入并持平
        • 买入并持有最大回撤
        • 向下滚动
        • 向下滚动最大值
        • 滚动买入并持有回撤
        • 滚动买入并持有最大回撤

        没有 pandas、cython 或 numpy 依赖项。通过简单列表进行所有计算。

        定义可重复用于同一脚本中的多个滚动窗口大小。您必须为您的平台编辑系列输入,因为这是专为 tradewave.net 上的比特币交易而设计的

        【讨论】:

        • 抱歉,这里使用了 tradewave 内置的“highcharts”模块
        【解决方案5】:

        大家好。 如果您想以计算有效的方式为滚动窗口解决此问题,这是一个相当复杂的问题。 我已经在 C# 中编写了一个解决方案。 我想分享这一点,因为复制这项工作所需的工作量非常大。

        首先,结果如下:

        这里我们采用一个简单的drawdown实现,每次都重新计算整个窗口

        test1 - simple drawdown test with 30 period rolling window. run 100 times.
        total seconds 0.8060461
        test2 - simple drawdown test with 60 period rolling window. run 100 times.
        total seconds 1.416081
        test3 - simple drawdown test with 180 period rolling window. run 100 times.
        total seconds 3.6602093
        test4 - simple drawdown test with 360 period rolling window. run 100 times.
        total seconds 6.696383
        test5 - simple drawdown test with 500 period rolling window. run 100 times.
        total seconds 8.9815137
        

        在这里,我们与我的高效滚动窗口算法生成的结果进行比较,其中只添加了最新的观察结果,然后它就变魔术了

        test6 - running drawdown test with 30 period rolling window. run 100 times.
        total seconds 0.2940168
        test7 - running drawdown test with 60 period rolling window. run 100 times.
        total seconds 0.3050175
        test8 - running drawdown test with 180 period rolling window. run 100 times.
        total seconds 0.3780216
        test9 - running drawdown test with 360 period rolling window. run 100 times.
        total seconds 0.4560261
        test10 - running drawdown test with 500 period rolling window. run 100 times.
        total seconds 0.5050288
        

        在 500 个周期窗口。我们在计算时间上实现了大约 20:1 的改进。

        这里是用于比较的简单drawdown类的代码:

        public class SimpleDrawDown
        {
            public double Peak { get; set; }
            public double Trough { get; set; }
            public double MaxDrawDown { get; set; }
        
            public SimpleDrawDown()
            {
                Peak = double.NegativeInfinity;
                Trough = double.PositiveInfinity;
                MaxDrawDown = 0;
            }
        
            public void Calculate(double newValue)
            {
                if (newValue > Peak)
                {
                    Peak = newValue;
                    Trough = Peak;
                }
                else if (newValue < Trough)
                {
                    Trough = newValue;
                    var tmpDrawDown = Peak - Trough;
                    if (tmpDrawDown > MaxDrawDown)
                        MaxDrawDown = tmpDrawDown;
                }
            }
        }
        

        这里是完整高效实现的代码。希望代码 cmets 有意义。

        internal class DrawDown
        {
            int _n;
            int _startIndex, _endIndex, _troughIndex;
            public int Count { get; set; }
            LinkedList<double> _values;
            public double Peak { get; set; }
            public double Trough { get; set; }
            public bool SkipMoveBackDoubleCalc { get; set; }
        
            public int PeakIndex
            {
                get
                {
                    return _startIndex;
                }
            }
            public int TroughIndex
            {
                get
                {
                    return _troughIndex;
                }
            }
        
            //peak to trough return
            public double DrawDownAmount
            {
                get
                {
                    return Peak - Trough;
                }
            }
        
            /// <summary>
            /// 
            /// </summary>
            /// <param name="n">max window for drawdown period</param>
            /// <param name="peak">drawdown peak i.e. start value</param>
            public DrawDown(int n, double peak)
            {
                _n = n - 1;
                _startIndex = _n;
                _endIndex = _n;
                _troughIndex = _n;
                Count = 1;
                _values = new LinkedList<double>();
                _values.AddLast(peak);
                Peak = peak;
                Trough = peak;
            }
        
            /// <summary>
            /// adds a new observation on the drawdown curve
            /// </summary>
            /// <param name="newValue"></param>
            public void Add(double newValue)
            {
                //push the start of this drawdown backwards
                //_startIndex--;
                //the end of the drawdown is the current period end
                _endIndex = _n;
                //the total periods increases with a new observation
                Count++;
                //track what all point values are in the drawdown curve
                _values.AddLast(newValue);
                //update if we have a new trough
                if (newValue < Trough)
                {
                    Trough = newValue;
                    _troughIndex = _endIndex;
                }
            }
        
            /// <summary>
            /// Shift this Drawdown backwards in the observation window
            /// </summary>
            /// <param name="trackingNewPeak">whether we are already tracking a new peak or not</param>
            /// <returns>a new drawdown to track if a new peak becomes active</returns>
            public DrawDown MoveBack(bool trackingNewPeak, bool recomputeWindow = true)
            {
                if (!SkipMoveBackDoubleCalc)
                {
                    _startIndex--;
                    _endIndex--;
                    _troughIndex--;
                    if (recomputeWindow)
                        return RecomputeDrawdownToWindowSize(trackingNewPeak);
                }
                else
                    SkipMoveBackDoubleCalc = false;
        
                return null;
            }
        
            private DrawDown RecomputeDrawdownToWindowSize(bool trackingNewPeak)
            {
                //the start of this drawdown has fallen out of the start of our observation window, so we have to recalculate the peak of the drawdown
                if (_startIndex < 0)
                {
                    Peak = double.NegativeInfinity;
                    _values.RemoveFirst();
                    Count--;
        
                    //there is the possibility now that there is a higher peak, within the current drawdown curve, than our first observation
                    //when we find it, remove all data points prior to this point
                    //the new peak must be before the current known trough point
                    int iObservation = 0, iNewPeak = 0, iNewTrough = _troughIndex, iTmpNewPeak = 0, iTempTrough = 0;
                    double newDrawDown = 0, tmpPeak = 0, tmpTrough = double.NegativeInfinity;
                    DrawDown newDrawDownObj = null;
                    foreach (var pointOnDrawDown in _values)
                    {
                        if (iObservation < _troughIndex)
                        {
                            if (pointOnDrawDown > Peak)
                            {
                                iNewPeak = iObservation;
                                Peak = pointOnDrawDown;
                            }
                        }
                        else if (iObservation == _troughIndex)
                        {
                            newDrawDown = Peak - Trough;
                            tmpPeak = Peak;
                        }
                        else
                        {
                            //now continue on through the remaining points, to determine if there is a nested-drawdown, that is now larger than the newDrawDown
                            //e.g. higher peak beyond _troughIndex, with higher trough than that at _troughIndex, but where new peak minus new trough is > newDrawDown
                            if (pointOnDrawDown > tmpPeak)
                            {
                                tmpPeak = pointOnDrawDown;
                                tmpTrough = tmpPeak;
                                iTmpNewPeak = iObservation;
                                //we need a new drawdown object, as we have a new higher peak
                                if (!trackingNewPeak) 
                                    newDrawDownObj = new DrawDown(_n + 1, tmpPeak);
                            }
                            else
                            {
                                if (!trackingNewPeak && newDrawDownObj != null)
                                {
                                    newDrawDownObj.MoveBack(true, false); //recomputeWindow is irrelevant for this as it will never fall before period 0 in this usage scenario
                                    newDrawDownObj.Add(pointOnDrawDown);  //keep tracking this new drawdown peak
                                }
        
                                if (pointOnDrawDown < tmpTrough)
                                {
                                    tmpTrough = pointOnDrawDown;
                                    iTempTrough = iObservation;
                                    var tmpDrawDown = tmpPeak - tmpTrough;
        
                                    if (tmpDrawDown > newDrawDown)
                                    {
                                        newDrawDown = tmpDrawDown;
                                        iNewPeak = iTmpNewPeak;
                                        iNewTrough = iTempTrough;
                                        Peak = tmpPeak;
                                        Trough = tmpTrough;
                                    }
                                }
                            }
                        }
                        iObservation++;
                    }
        
                    _startIndex = iNewPeak; //our drawdown now starts from here in our observation window
                    _troughIndex = iNewTrough;
                    for (int i = 0; i < _startIndex; i++)
                    {
                        _values.RemoveFirst(); //get rid of the data points prior to this new drawdown peak
                        Count--;
                    }
                    return newDrawDownObj;
                }
                return null;
            }
        
        }
        
        public class RunningDrawDown
        {
        
            int _n;
            List<DrawDown> _drawdownObjs;
            DrawDown _currentDrawDown;
            DrawDown _maxDrawDownObj;
        
            /// <summary>
            /// The Peak of the MaxDrawDown
            /// </summary>
            public double DrawDownPeak
            {
                get
                {
                    if (_maxDrawDownObj == null) return double.NegativeInfinity;
                    return _maxDrawDownObj.Peak;
                }
            }
            /// <summary>
            /// The Trough of the Max DrawDown
            /// </summary>
            public double DrawDownTrough
            {
                get
                {
                    if (_maxDrawDownObj == null) return double.PositiveInfinity;
                    return _maxDrawDownObj.Trough;
                }
            }
            /// <summary>
            /// The Size of the DrawDown - Peak to Trough
            /// </summary>
            public double DrawDown
            {
                get
                {
                    if (_maxDrawDownObj == null) return 0;
                    return _maxDrawDownObj.DrawDownAmount;
                }
            }
            /// <summary>
            /// The Index into the Window that the Peak of the DrawDown is seen
            /// </summary>
            public int PeakIndex
            {
                get
                {
                    if (_maxDrawDownObj == null) return 0;
                    return _maxDrawDownObj.PeakIndex;
                }
            }
            /// <summary>
            /// The Index into the Window that the Trough of the DrawDown is seen
            /// </summary>
            public int TroughIndex
            {
                get
                {
                    if (_maxDrawDownObj == null) return 0;
                    return _maxDrawDownObj.TroughIndex;
                }
            }
        
            /// <summary>
            /// Creates a running window for the calculation of MaxDrawDown within the window
            /// </summary>
            /// <param name="n">the number of periods within the window</param>
            public RunningDrawDown(int n)
            {
                _n = n;
                _currentDrawDown = null;
                _drawdownObjs = new List<DrawDown>();
            }
        
            /// <summary>
            /// The new value to add onto the end of the current window (the first value will drop off)
            /// </summary>
            /// <param name="newValue">the new point on the curve</param>
            public void Calculate(double newValue)
            {
                if (double.IsNaN(newValue)) return;
        
                if (_currentDrawDown == null)
                {
                    var drawDown = new DrawDown(_n, newValue);
                    _currentDrawDown = drawDown;
                    _maxDrawDownObj = drawDown;
                }
                else
                {
                    //shift current drawdown back one. and if the first observation falling outside the window means we encounter a new peak after the current trough, we start tracking a new drawdown
                    var drawDownFromNewPeak = _currentDrawDown.MoveBack(false);
        
                    //this is a special case, where a new lower peak (now the highest) is created due to the drop of of the pre-existing highest peak, and we are not yet tracking a new peak
                    if (drawDownFromNewPeak != null)
                    {
                        _drawdownObjs.Add(_currentDrawDown); //record this drawdown into our running drawdowns list)
                        _currentDrawDown.SkipMoveBackDoubleCalc = true; //MoveBack() is calculated again below in _drawdownObjs collection, so we make sure that is skipped this first time
                        _currentDrawDown = drawDownFromNewPeak;
                        _currentDrawDown.MoveBack(true);
                    }
        
                    if (newValue > _currentDrawDown.Peak)
                    {
                        //we need a new drawdown object, as we have a new higher peak
                        var drawDown = new DrawDown(_n, newValue);
                        //do we have an existing drawdown object, and does it have more than 1 observation
                        if (_currentDrawDown.Count > 1)
                        {
                            _drawdownObjs.Add(_currentDrawDown); //record this drawdown into our running drawdowns list)
                            _currentDrawDown.SkipMoveBackDoubleCalc = true; //MoveBack() is calculated again below in _drawdownObjs collection, so we make sure that is skipped this first time
                        }
                        _currentDrawDown = drawDown;
                    }
                    else
                    {
                        //add the new observation to the current drawdown
                        _currentDrawDown.Add(newValue);
                    }
                }
        
                //does our new drawdown surpass any of the previous drawdowns?
                //if so, we can drop the old drawdowns, as for the remainer of the old drawdowns lives in our lookup window, they will be smaller than the new one
                var newDrawDown = _currentDrawDown.DrawDownAmount;
                _maxDrawDownObj = _currentDrawDown;
                var maxDrawDown = newDrawDown;
                var keepDrawDownsList = new List<DrawDown>();
                foreach (var drawDownObj in _drawdownObjs)
                {
                    drawDownObj.MoveBack(true);
                    if (drawDownObj.DrawDownAmount > newDrawDown)
                    {
                        keepDrawDownsList.Add(drawDownObj);
                    }
        
                    //also calculate our max drawdown here
                    if (drawDownObj.DrawDownAmount > maxDrawDown)
                    {
                        maxDrawDown = drawDownObj.DrawDownAmount;
                        _maxDrawDownObj = drawDownObj;
                    }
        
                }
                _drawdownObjs = keepDrawDownsList;
        
            }
        
        }
        

        示例用法:

        RunningDrawDown rd = new RunningDrawDown(500);
        foreach (var input in data)
        {
            rd.Calculate(input);
            Console.WriteLine(string.Format("max draw {0:0.00000}, peak {1:0.00000}, trough {2:0.00000}, drawstart {3:0.00000}, drawend {4:0.00000}",
                rd.DrawDown, rd.DrawDownPeak, rd.DrawDownTrough, rd.PeakIndex, rd.TroughIndex));
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-10-05
          • 2018-09-04
          • 2017-08-27
          • 2019-10-06
          • 2019-11-19
          • 2018-07-10
          • 2022-10-16
          相关资源
          最近更新 更多