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.
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.
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).
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.
@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?
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));
}
También se ha publicado aquí, donde el código se puede formatear bien. stackoverflow.com/questions/24937128/
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
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.
(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 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.
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.
0 votos
Perdón por la confusión, efectivamente me refería a un algoritmo/código que sea eficiente para la CPU para calcular la reducción máxima retrospectiva.