计算*滚动*熊猫系列的最大缩编

时间:2014-01-11 03:52:31

标签: python algorithm numpy pandas

编写一个计算时间序列最大缩写的函数非常容易。在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()

Time series

正如预期的那样,max_dd(s)在-17.6附近显示出一些东西。好,很棒,很棒。现在说我有兴趣计算这个系列的滚动缩幅。即对于每个步骤,我想计算指定长度的前一个子系列的最大值。使用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()

rolling drawdown

这完美无缺。但感觉很慢。在pandas或其他工具包中是否有一个特别灵活的算法来快速完成这项工作?我参加了一个射击在定制写的东西:它跟踪各种中间数据(观测最大值的位置,先前发现提款的位置)的以减少大量冗余的计算。它确实节省了一些时间,但不是很多,而且几乎没有尽可能多的时间。

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


编辑: 谁想要的这里提到的所有职能进行审查看看IPython的笔记本在(和其他一些人!):http://nbviewer.ipython.org/gist/8one6/8506455

它显示了如何的一些方法对这个问题的涉及,将检查它们得到相同的结果,并且示出了对各种尺寸的数据将其运行时。

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

5 个答案:

答案 0 :(得分:20)

这是滚动最大缩编功能的numpy版本。 windowed_view是单行函数的包装器,它使用numpy.lib.stride_tricks.as_strided来生成1d数组的内存高效2d窗口视图(下面的完整代码)。一旦我们有了这个窗口视图,计算基本上与你的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计算。

rolling drawdown plot

时间比较,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,您可以在将数组传递给Series之前使用fillna方法预处理rolling_max_dd

答案 1 :(得分:4)

为了后代和完整性,这里是我在Cython中的结论。 MemoryViews大大加快了速度。有一些工作要做,以确保我正确输入所有内容(对不起,c语言的新手)。但最后我觉得它运作得很好。对于典型的用例,加速比常规python为~100x或~150x。要调用的函数是cy_rolling_dd_custom_mv,其中第一个参数(ser)应该是1-d 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

答案 2 :(得分:1)

这是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倍加速

答案 3 :(得分:0)

# 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)     

enter image description here

[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
  • 画下
  • Max Drawn Down
  • 购买并按住Draw Down
  • 购买并持有Max Draw Down
  • 滚动下拉
  • Rolling Max Drawn Down
  • 滚动购买并保持平局
  • 滚动买入和持有最大跌幅

没有熊猫,cython或numpy依赖。通过简单列表进行所有计算。

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

答案 4 :(得分:-3)

大家好。 如果要以滚动窗口的计算有效方式解决这个问题,这是一个非常复杂的问题。 我已经在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;
        }
    }
}

以下是完全高效实施的代码。希望代码注释有意义。

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));
}