strategy - Calcular la extracción máxima con una solución vectorizada en python
python high frequency trading (3)
Dada una serie temporal de rendimientos, necesitamos evaluar el rendimiento agregado para cada combinación de punto de partida a punto final.
El primer truco es convertir una serie de tiempo de devoluciones en una serie de índices de retorno. Dada una serie de índices de rendimiento, puedo calcular el retorno sobre cualquier subperíodo con el índice de retorno al principio ri_0 y al final ri_1. El cálculo es: ri_1 / ri_0 - 1.
El segundo truco es producir una segunda serie de inversos de índices de retorno. Si r es mi serie de índices de retorno, entonces 1 / r es mi serie de inversos.
El tercer truco es tomar el producto matricial de r * (1 / r) .Transponer.
r es una matriz nx 1. (1 / r) .Transpose es una matriz de 1 xn. El producto resultante contiene todas las combinaciones de ri_j / ri_k. Solo resta 1 y de hecho tengo devoluciones.
El cuarto truco es asegurarme de que estoy restringiendo mi denominador para representar períodos anteriores a los representados por el numerador.
A continuación está mi función vectorizada.
import numpy as np
import pandas as pd
def max_dd(returns):
# make into a DataFrame so that it is a 2-dimensional
# matrix such that I can perform an nx1 by 1xn matrix
# multiplication and end up with an nxn matrix
r = pd.DataFrame(returns).add(1).cumprod()
# I copy r.T to ensure r''s index is not the same
# object as 1 / r.T''s columns object
x = r.dot(1 / r.T.copy()) - 1
x.columns.name, x.index.name = ''start'', ''end''
# let''s make sure we only calculate a return when start
# is less than end.
y = x.stack().reset_index()
y = y[y.start < y.end]
# my choice is to return the periods and the actual max
# draw down
z = y.set_index([''start'', ''end'']).iloc[:, 0]
return z.min(), z.argmin()[0], z.argmin()[1]
¿Cómo funciona esto?
para la solución vectorizada ejecuté 10 iteraciones sobre la serie temporal de longitudes [10, 50, 100, 150, 200]. El tiempo que tomó es el siguiente:
10: 0.032 seconds
50: 0.044 seconds
100: 0.055 seconds
150: 0.082 seconds
200: 0.047 seconds
La misma prueba para la solución en bucle es la siguiente:
10: 0.153 seconds
50: 3.169 seconds
100: 12.355 seconds
150: 27.756 seconds
200: 49.726 seconds
Editar
La respuesta de Alexander proporciona resultados superiores. La misma prueba usando código modificado
10: 0.000 seconds
50: 0.000 seconds
100: 0.004 seconds
150: 0.007 seconds
200: 0.008 seconds
Modifiqué su código en la siguiente función:
def max_dd(returns):
r = returns.add(1).cumprod()
dd = r.div(r.cummax()).sub(1)
mdd = drawdown.min()
end = drawdown.argmin()
start = r.loc[:end].argmax()
return mdd, start, end
La Disposición máxima es una medida de riesgo común utilizada en las finanzas cuantitativas para evaluar el rendimiento negativo más grande que se haya experimentado.
Recientemente, me volví impaciente con el tiempo para calcular la reducción máxima utilizando mi enfoque en bucle.
def max_dd_loop(returns):
"""returns is assumed to be a pandas series"""
max_so_far = None
start, end = None, None
r = returns.add(1).cumprod()
for r_start in r.index:
for r_end in r.index:
if r_start < r_end:
current = r.ix[r_end] / r.ix[r_start] - 1
if (max_so_far is None) or (current < max_so_far):
max_so_far = current
start, end = r_start, r_end
return max_so_far, start, end
Estoy familiarizado con la percepción común de que una solución vectorizada sería mejor.
Las preguntas son:
- ¿Puedo vectorizar este problema?
- ¿Cómo se ve esta solución?
- ¿Qué tan beneficioso es?
Editar
Modifiqué la respuesta de Alexander en la siguiente función:
def max_dd(returns):
"""Assumes returns is a pandas Series"""
r = returns.add(1).cumprod()
dd = r.div(r.cummax()).sub(1)
mdd = dd.min()
end = dd.argmin()
start = r.loc[:end].argmax()
return mdd, start, end
df_returns
se supone que es un marco de datos de devoluciones, donde cada columna es una estrategia / administrador / seguridad independiente, y cada fila es una nueva fecha (por ejemplo, mensual o diaria).
cum_returns = (1 + df_returns).cumprod()
drawdown = 1 - cum_returns.div(cum_returns.cummax())
Primero sugerí usar la ventana .expanding()
pero obviamente no es necesario con las .cumprod()
y .cummax()
para calcular la reducción máxima hasta un punto dado:
df = pd.DataFrame(data={''returns'': np.random.normal(0.001, 0.05, 1000)}, index=pd.date_range(start=date(2016,1,1), periods=1000, freq=''D''))
df = pd.DataFrame(data={''returns'': np.random.normal(0.001, 0.05, 1000)},
index=pd.date_range(start=date(2016, 1, 1), periods=1000, freq=''D''))
df[''cumulative_return''] = df.returns.add(1).cumprod().subtract(1)
df[''max_drawdown''] = df.cumulative_return.add(1).div(df.cumulative_return.cummax().add(1)).subtract(1)
returns cumulative_return max_drawdown
2016-01-01 -0.014522 -0.014522 0.000000
2016-01-02 -0.022769 -0.036960 -0.022769
2016-01-03 0.026735 -0.011214 0.000000
2016-01-04 0.054129 0.042308 0.000000
2016-01-05 -0.017562 0.024004 -0.017562
2016-01-06 0.055254 0.080584 0.000000
2016-01-07 0.023135 0.105583 0.000000
2016-01-08 -0.072624 0.025291 -0.072624
2016-01-09 -0.055799 -0.031919 -0.124371
2016-01-10 0.129059 0.093020 -0.011363
2016-01-11 0.056123 0.154364 0.000000
2016-01-12 0.028213 0.186932 0.000000
2016-01-13 0.026914 0.218878 0.000000
2016-01-14 -0.009160 0.207713 -0.009160
2016-01-15 -0.017245 0.186886 -0.026247
2016-01-16 0.003357 0.190869 -0.022979
2016-01-17 -0.009284 0.179813 -0.032050
2016-01-18 -0.027361 0.147533 -0.058533
2016-01-19 -0.058118 0.080841 -0.113250
2016-01-20 -0.049893 0.026914 -0.157492
2016-01-21 -0.013382 0.013173 -0.168766
2016-01-22 -0.020350 -0.007445 -0.185681
2016-01-23 -0.085842 -0.092648 -0.255584
2016-01-24 0.022406 -0.072318 -0.238905
2016-01-25 0.044079 -0.031426 -0.205356
2016-01-26 0.045782 0.012917 -0.168976
2016-01-27 -0.018443 -0.005764 -0.184302
2016-01-28 0.021461 0.015573 -0.166797
2016-01-29 -0.062436 -0.047836 -0.218819
2016-01-30 -0.013274 -0.060475 -0.229189
... ... ... ...
2018-08-28 0.002124 0.559122 -0.478738
2018-08-29 -0.080303 0.433921 -0.520597
2018-08-30 -0.009798 0.419871 -0.525294
2018-08-31 -0.050365 0.348359 -0.549203
2018-09-01 0.080299 0.456631 -0.513004
2018-09-02 0.013601 0.476443 -0.506381
2018-09-03 -0.009678 0.462153 -0.511158
2018-09-04 -0.026805 0.422960 -0.524262
2018-09-05 0.040832 0.481062 -0.504836
2018-09-06 -0.035492 0.428496 -0.522411
2018-09-07 -0.011206 0.412489 -0.527762
2018-09-08 0.069765 0.511031 -0.494817
2018-09-09 0.049546 0.585896 -0.469787
2018-09-10 -0.060201 0.490423 -0.501707
2018-09-11 -0.018913 0.462235 -0.511131
2018-09-12 -0.094803 0.323611 -0.557477
2018-09-13 0.025736 0.357675 -0.546088
2018-09-14 -0.049468 0.290514 -0.568542
2018-09-15 0.018146 0.313932 -0.560713
2018-09-16 -0.034118 0.269104 -0.575700
2018-09-17 0.012191 0.284576 -0.570527
2018-09-18 -0.014888 0.265451 -0.576921
2018-09-19 0.041180 0.317562 -0.559499
2018-09-20 0.001988 0.320182 -0.558623
2018-09-21 -0.092268 0.198372 -0.599348
2018-09-22 -0.015386 0.179933 -0.605513
2018-09-23 -0.021231 0.154883 -0.613888
2018-09-24 -0.023536 0.127701 -0.622976
2018-09-25 0.030160 0.161712 -0.611605
2018-09-26 0.025528 0.191368 -0.601690