16 votos

Algoritmo más rápido para calcular la reducción máxima retrospectiva

Pregunta simple - ¿cuál sería el algoritmo más rápido para calcular la máxima reducción retrospectiva?

He encontrado algunas charlas interesantes pero me preguntaba qué pensaba la gente de esta pregunta.

1 votos

Su pregunta no me queda clara. ¿Se refiere a la técnica más fácil de codificar, o a un código que sea eficiente para la CPU?

0 votos

Un enfoque vectorial (desde el principio) es muy rápido.

0 votos

Probablemente debería aclarar su pregunta. La mayoría de los lectores suponen que estás preguntando por la reducción máxima retrospectiva, mientras que yo deduzco del PDF que quieres calcular una expectativa de la misma.

7voto

Markus Olsson Puntos 12651

No te voy a dar la respuesta en bandeja de plata, pero espero que lo siguiente te sirva para empezar:

a) debe definir con exactitud el periodo de retrospección en el que desea obtener la máxima reducción.

b) debe mantener un seguimiento del precio máximo mientras itera la ventana de revisión.

c) es necesario llevar un registro del precio mínimo subsiguiente a cualquier nuevo máximo, por lo que cada vez que se hace un nuevo máximo es necesario restablecer el mínimo del máximo a cero (relativamente hablando como una divergencia del valor máximo)

esto debería llevarte fácilmente a donde quieres llegar sin tener que iterar la serie temporal más de una vez. No estoy de acuerdo en que un enfoque vectorizado resuelva este problema (@Pat, por favor, proporciona una respuesta si no estás de acuerdo, tendría curiosidad por saber cómo abordarías esto de manera vectorizada porque el algoritmo aquí depende de la trayectoria).

0 votos

Sólo hay 1 camino desde el inicio (y sólo se requiere 1 iteración con el resultado de dd vectorizado de ese camino). Lo que describes arriba es un dd rodante (*Nota que especifiqué desde el inicio). Si usted tiene una serie de tiempo y puede mostrar un ejemplo de su algoritmo y el tiempo, voy a reproducir utilizando un enfoque vectorizado y comparar los tiempos.

1 votos

@pat, "rolling dd", bueno, eso es más o menos la práctica de la industria. A nadie le importa el máximo draw down en una ventana de diez años, al menos no conozco a demasiados inversores con una memoria de tan largo plazo. Por favor, sigue adelante y muestra tu versión vectorizada (incluso aquella en la que defines la dd máxima desde el inicio), añadiría mucho valor a otros usuarios. ¿Qué te retiene?

6voto

Picacodigos Puntos 106

Hola gente. Este es un problema bastante complejo si se quiere resolver esto de una manera computacionalmente eficiente para una ventana rodante. Me he adelantado y he escrito una solución para esto en C#. Quiero compartir esto ya que el esfuerzo requerido para replicar este trabajo es bastante alto.

En primer lugar, aquí están los resultados:

aquí tomamos una implementación de reducción simple y recalculamos para la ventana completa cada vez

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

aquí comparamos con los resultados generados a partir de mi eficiente algoritmo de ventana móvil en el que sólo se añade la última observación y luego hace su magia

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

En la ventana de 500 períodos. Estamos logrando una mejora de aproximadamente 20:1 en el tiempo de cálculo.

Aquí está el código de la clase de reducción simple utilizada para las comparaciones:

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

Y aquí está el código para la implementación completa y eficiente. Espero que los comentarios del código tengan sentido.

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;

    }

}

Ejemplo de uso:

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

1 votos

También se ha publicado aquí, donde el código se puede formatear bien. stackoverflow.com/questions/24937128/

3voto

Templar Puntos 2164

Zipline, el backtester de python de código abierto, tiene una implementación por lotes e iterativa para la reducción máxima.

Aquí está el lote: https://github.com/quantopian/zipline/blob/master/zipline/finance/risk.py#L284

Aquí está la iteración: https://github.com/quantopian/zipline/blob/master/zipline/finance/risk.py#L578

revelación: soy uno de los mantenedores de la tirolesa

1 votos

Los enlaces anteriores ya no funcionan

3voto

Vim Puntos 841

En Python, una implementación muy hábil que explota el rolling funcionalidad en pandas es así

import pandas as pd

def get_max_dd(nvs: pd.Series, window=None) -> float:
    """
    :param nvs: net value series
    :param window: lookback window, int or None
     if None, look back entire history
    """
    n = len(nvs)
    if window is None:
        window = n
    # rolling peak values
    peak_series = nvs.rolling(window=window, min_periods=1).max()
    return (nvs / peak_series - 1.0).min()

Esto será más rápido que escribir manualmente bucles for, etc., porque pandas es subyacente a la aceleración C. Al utilizar sólo pandas 's native components we kind of "steal" a lot of speed boost from C.

1voto

Kyle Cronin Puntos 554

(Tras la aclaración, esta respuesta ya no es pertinente)

La reducción máxima esperada va a ser muy sensible a su elección de SDE, y a su calibración. Por lo tanto, debe jugar con una variedad de parametrizaciones para estimar el error de su modelo.

En lo que respecta al cálculo eficiente, podemos considerar que se trata de un pago muy similar al de una opción lookback (como en el PDF que has enlazado). Al igual que en el caso de las opciones lookback, el primer instinto es valorarlas mediante técnicas de Monte Carlo, pero en realidad se puede hacer mucho más rápido utilizando un solucionador de PDE de varios niveles, al menos para SDEs suficientemente simples.

La forma en que un solucionador de PDE de 2 niveles funciona para un pago como este es que, en lugar de tener una cuadrícula de $(S,t)$ sobre los que se ejecutan las ecuaciones de diferencia y las condiciones de contorno, se tiene una malla de $( \{M,S\}, t )$ valores, donde $M$ representa el máximo alcanzado hasta ahora. Obviamente, hay algunas condiciones de contorno nuevas que lo acompañan, por ejemplo que $\frac{\partial M}{\partial S}=1$ en la línea y por encima de ella $S=M$ .

Diferenciando y actualizando en esta cuadrícula, al final se obtiene un valor $V_{0,0}$ correspondiente al máximo de hoy $M_0$ y el precio de las acciones $S_0$ .

Véase el apartado 5.3.2 de este pdf para saber cómo funciona con las miradas hacia atrás. Los drawdowns máximos serán muy similares.

Finanhelp.com

FinanHelp es una comunidad para personas con conocimientos de economía y finanzas, o quiere aprender. Puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X