Capítulo 4. Dominar el Backtesting Vectorizado
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
[Fueron tan tontos como para pensar que se puede mirar al pasado para predecir el futuro.1
El Economista
Desarrollar ideas e hipótesis para un programa de negociación algorítmica suele ser la parte más creativa y a veces incluso divertida de la fase de preparación. Probarlas a fondo suele ser la parte más técnica y que requiere más tiempo. Este capítulo trata del backtesting vectorizado de diferentes estrategias de negociación algorítmica. Abarca los siguientes tipos de estrategias (consulta también "Estrategias de negociación"):
- Estrategias basadas en medias móviles simples (SMA)
-
La idea básica del uso de las SMA para generar señales de compra y venta tiene ya décadas de antigüedad. Las SMA son una herramienta fundamental en el llamado análisis técnico de los precios de las acciones. Se obtiene una señal, por ejemplo, cuando una SMA definida en una ventana temporal más corta -digamos 42 días- cruza una SMA definida en una ventana temporal más larga -digamos 252 días-.
- Estrategias de impulso
-
Son estrategias que se basan en la hipótesis de que el rendimiento reciente persistirá durante algún tiempo adicional. Por ejemplo, se supone que una acción con tendencia a la baja lo hará durante más tiempo, por lo que hay que venderla en corto.
- Estrategias de reversión de la media
-
El razonamiento que subyace a las estrategias de reversión a la media es que los precios de las acciones o de otros instrumentos financieros tienden a volver a algún nivel medio o a algún nivel de tendencia cuando se han desviado demasiado de dichos niveles.
El capítulo procede como sigue. "Hacer uso de la vectorización" presenta la vectorización como un enfoque técnico útil para formular y backtestar estrategias de negociación. " Estrategias basadas en Medias Móviles Simples" es el núcleo de este capítulo y trata en profundidad el backtesting vectorizado de estrategias basadas en SMA. " Estrategias basadas en el impulso" presenta y realiza pruebas retrospectivas de estrategias de negociación basadas en el denominado impulso de la serie temporal ("rendimiento reciente") de una acción. "Estrategias basadas en la reversión a la media " termina el capítulo con la cobertura de las estrategias de reversión a la media. Por último, " Espionaje de datos y sobre ajuste " trata de los peligros del espionaje de datos y el sobreajuste en el contexto del backtesting de estrategias de negociación algorítmica.
El principal objetivo de este capítulo es dominar el enfoque de la implementación vectorizada, que permiten paquetes como NumPy
y pandas
, como herramienta de backtesting eficaz y rápida. Para ello, los enfoques presentados hacen una serie de suposiciones simplificadoras para centrar mejor el debate en el tema principal de la vectorización.
El backtesting vectorizado debe considerarse en los siguientes casos:
- Estrategias de negociación sencillas
-
El enfoque de backtesting vectorizado tiene claramente límites cuando se trata de modelar estrategias de negociación algorítmica. Sin embargo, muchas estrategias populares y sencillas pueden someterse a backtesting vectorizado.
- Exploración interactiva de estrategias
-
El backtesting vectorizado permite una exploración ágil e interactiva de las estrategias de negociación y sus características. Por lo general, bastan unas pocas líneas de código para obtener los primeros resultados, y se pueden probar fácilmente distintas combinaciones de parámetros.
- Visualización como objetivo principal
-
El enfoque se presta bastante bien para visualizaciones de los datos utilizados, estadísticas, señales y resultados de rendimiento. Unas pocas líneas de código Python suelen bastar para generar gráficos atractivos y perspicaces.
- Programas completos de backtesting
-
El backtesting vectorizado es bastante rápido en general, ya que permite probar una gran variedad de combinaciones de parámetros en poco tiempo. Cuando la velocidad es clave, debe considerarse este enfoque.
Utilizar la vectorización
La vectorización, o programación de matrices, se refiere a un estilo de programación en el que las operaciones sobre escalares (es decir, números enteros o de coma flotante) se generalizan a vectores, matrices o incluso matrices multidimensionales. Considera un vector de enteros representado en Python como un objeto list
v = [1, 2, 3, 4, 5]
. Calcular el producto escalar de dicho vector y, digamos, el número 2
requiere en Python puro un bucle for
o algo similar, como una comprensión de lista, que no es más que una sintaxis diferente para un bucle for
:
In
[
1
]:
v
=
[
1
,
2
,
3
,
4
,
5
]
In
[
2
]:
sm
=
[
2
*
i
for
i
in
v
]
In
[
3
]:
sm
Out
[
3
]:
[
2
,
4
,
6
,
8
,
10
]
En principio, Python permite multiplicar un objeto list
por un número entero, pero el modelo de datos de Python devuelve otro objeto list
que, en el caso del ejemplo, contiene dos veces los elementos del objeto original:
In
[
4
]:
2
*
v
Out
[
4
]:
[
1
,
2
,
3
,
4
,
5
,
1
,
2
,
3
,
4
,
5
]
Vectorización con NumPy
El paquete NumPy
para cálculo numérico (véase la página principalNumPy
) introduce la vectorización en Python. La clase principal que proporciona NumPy
es la clase ndarray
, que significa matriz n-dimensional. Se puede crear una instancia de este objeto, por ejemplo, a partir del objeto list
v
. La multiplicación escalar, las transformaciones lineales y operaciones similares del álgebra lineal funcionan entoncescomo se desea:
In
[
5
]
:
import
numpy
as
np
In
[
6
]
:
a
=
np
.
array
(
v
)
In
[
7
]
:
a
Out
[
7
]
:
array
(
[
1
,
2
,
3
,
4
,
5
]
)
In
[
8
]
:
type
(
a
)
Out
[
8
]
:
numpy
.
ndarray
In
[
9
]
:
2
*
a
Out
[
9
]
:
array
(
[
2
,
4
,
6
,
8
,
10
]
)
In
[
10
]
:
0.5
*
a
+
2
Out
[
10
]
:
array
(
[
2.5
,
3.
,
3.5
,
4.
,
4.5
]
)
Importa el paquete
NumPy
.Instancia un objeto
ndarray
basado en el objetolist
.Imprime los datos almacenados como objeto
ndarray
.Busca el tipo del objeto.
Consigue una multiplicación escalar de forma vectorizada.
Consigue una transformación lineal de forma vectorizada.
La transición de una matriz unidimensional (un vector) a una matriz bidimensional (una matriz) es natural. Lo mismo ocurre con las dimensiones superiores:
In
[
11
]
:
a
=
np
.
arange
(
12
)
.
reshape
(
(
4
,
3
)
)
In
[
12
]
:
a
Out
[
12
]
:
array
(
[
[
0
,
1
,
2
]
,
[
3
,
4
,
5
]
,
[
6
,
7
,
8
]
,
[
9
,
10
,
11
]
]
)
In
[
13
]
:
2
*
a
Out
[
13
]
:
array
(
[
[
0
,
2
,
4
]
,
[
6
,
8
,
10
]
,
[
12
,
14
,
16
]
,
[
18
,
20
,
22
]
]
)
In
[
14
]
:
a
*
*
2
Out
[
14
]
:
array
(
[
[
0
,
1
,
4
]
,
[
9
,
16
,
25
]
,
[
36
,
49
,
64
]
,
[
81
,
100
,
121
]
]
)
Crea un objeto unidimensional
ndarray
y lo remodela a dos dimensiones.Calcula el cuadrado de cada elemento del objeto de forma vectorizada.
Además, la clase ndarray
proporciona ciertos métodos que permiten realizar operaciones vectorizadas. A menudo también tienen homólogos en forma de las llamadas funciones universales que proporciona NumPy
:
In
[
15
]
:
a
.
mean
(
)
Out
[
15
]
:
5.5
In
[
16
]
:
np
.
mean
(
a
)
Out
[
16
]
:
5.5
In
[
17
]
:
a
.
mean
(
axis
=
0
)
Out
[
17
]
:
array
(
[
4.5
,
5.5
,
6.5
]
)
In
[
18
]
:
np
.
mean
(
a
,
axis
=
1
)
Out
[
18
]
:
array
(
[
1.
,
4.
,
7.
,
10.
]
)
Calcula la media de todos los elementos mediante una llamada a un método.
Calcula la media de todos los elementos mediante una función universal.
Calcula la media a lo largo del primer eje.
Calcula la media a lo largo del segundo eje.
Como ejemplo financiero, considera la función generate_sample_data()
en "Scripts de Python" que utiliza una discretización de Euler para generar trayectorias de muestra para un movimiento browniano geométrico. La implementación hace uso de múltiples operaciones vectorizadas que se combinan en una sola línea de código.
Consulta el Apéndice A para obtener más detalles sobre la vectorización con NumPy
. Consulta Hilpisch (2018) para ver multitud de aplicaciones de la vectorización en un contexto financiero.
El conjunto de instrucciones estándar y el modelo de datos de Python no suelen permitir operaciones numéricas vectorizadas. NumPy
introduce potentes técnicas de vectorización basadas en la clase de matrices regulares ndarray
que dan lugar a un código conciso y cercano a la notación matemática de, por ejemplo, álgebra lineal en lo que respecta a vectores y matrices.
Vectorización con pandas
El paquete pandas
y la clase central DataFrame
utilizan en gran medida NumPy
y la clase ndarray
. Por lo tanto, la mayoría de los principios de vectorización vistos en el contexto de NumPy
se trasladan a pandas
. La mejor forma de explicar la mecánica es de nuevo a partir de un ejemplo concreto. Para empezar, define primero un objeto bidimensional ndarray
:
In
[
19
]:
a
=
np
.
arange
(
15
)
.
reshape
(
5
,
3
)
In
[
20
]:
a
Out
[
20
]:
array
([[
0
,
1
,
2
],
[
3
,
4
,
5
],
[
6
,
7
,
8
],
[
9
,
10
,
11
],
[
12
,
13
,
14
]])
Para la creación de un objeto DataFrame
, genera a continuación un objeto list
con nombres de columnas y un objeto DatetimeIndex
, ambos de tamaño adecuado dado el objeto ndarray
:
In
[
21
]
:
import
pandas
as
pd
In
[
22
]
:
columns
=
list
(
'
abc
'
)
In
[
23
]
:
columns
Out
[
23
]
:
[
'
a
'
,
'
b
'
,
'
c
'
]
In
[
24
]
:
index
=
pd
.
date_range
(
'
2021-7-1
'
,
periods
=
5
,
freq
=
'
B
'
)
In
[
25
]
:
index
Out
[
25
]
:
DatetimeIndex
(
[
'
2021-07-01
'
,
'
2021-07-02
'
,
'
2021-07-05
'
,
'
2021-07-06
'
,
'
2021-07-07
'
]
,
dtype
=
'
datetime64[ns]
'
,
freq
=
'
B
'
)
In
[
26
]
:
df
=
pd
.
DataFrame
(
a
,
columns
=
columns
,
index
=
index
)
In
[
27
]
:
df
Out
[
27
]
:
a
b
c
2021
-
07
-
01
0
1
2
2021
-
07
-
02
3
4
5
2021
-
07
-
05
6
7
8
2021
-
07
-
06
9
10
11
2021
-
07
-
07
12
13
14
Importa el paquete
pandas
.Crea un objeto
list
a partir del objetostr
.Se crea un objeto
pandas
DatetimeIndex
que tiene una frecuencia de "día laborable" y abarca cinco periodos.Se instancia un objeto
DataFrame
basado en el objetondarray
a
con las etiquetas de columna y los valores de índice especificados.
En principio, la vectorización funciona ahora de forma similar a los objetos de ndarray
. Una diferencia es que las operaciones de agregación dan por defecto resultados por columnas:
In
[
28
]
:
2
*
df
Out
[
28
]
:
a
b
c
2021
-
07
-
01
0
2
4
2021
-
07
-
02
6
8
10
2021
-
07
-
05
12
14
16
2021
-
07
-
06
18
20
22
2021
-
07
-
07
24
26
28
In
[
29
]
:
df
.
sum
(
)
Out
[
29
]
:
a
30
b
35
c
40
dtype
:
int64
In
[
30
]
:
np
.
mean
(
df
)
Out
[
30
]
:
a
6.0
b
7.0
c
8.0
dtype
:
float64
Calcula el producto escalar del objeto
DataFrame
(tratado como una matriz).Calcula la suma por columna.
Calcula la media por columna.
Las operaciones por columnas pueden realizarse haciendo referencia a los nombres de las columnas respectivas, mediante la notación de corchetes o la notación de puntos:
In
[
31
]
:
df
[
'
a
'
]
+
df
[
'
c
'
]
Out
[
31
]
:
2021
-
07
-
01
2
2021
-
07
-
02
8
2021
-
07
-
05
14
2021
-
07
-
06
20
2021
-
07
-
07
26
Freq
:
B
,
dtype
:
int64
In
[
32
]
:
0.5
*
df
.
a
+
2
*
df
.
b
-
df
.
c
Out
[
32
]
:
2021
-
07
-
01
0.0
2021
-
07
-
02
4.5
2021
-
07
-
05
9.0
2021
-
07
-
06
13.5
2021
-
07
-
07
18.0
Freq
:
B
,
dtype
:
float64
Calcula la suma por elementos de las columnas
a
yc
.Calcula una transformación lineal que incluya las tres columnas.
Del mismo modo, las condiciones que producen vectores de resultados booleanos y las selecciones tipo SQL basadas en dichas condiciones son fáciles de implementar:
In
[
33
]
:
df
[
'
a
'
]
>
5
Out
[
33
]
:
2021
-
07
-
01
False
2021
-
07
-
02
False
2021
-
07
-
05
True
2021
-
07
-
06
True
2021
-
07
-
07
True
Freq
:
B
,
Name
:
a
,
dtype
:
bool
In
[
34
]
:
df
[
df
[
'
a
'
]
>
5
]
Out
[
34
]
:
a
b
c
2021
-
07
-
05
6
7
8
2021
-
07
-
06
9
10
11
2021
-
07
-
07
12
13
14
¿Qué elemento de la columna
a
es mayor que cinco?Selecciona todas las filas en las que el elemento de la columna
a
sea mayor que cinco.
Para un backtesting vectorizado de estrategias de negociación, son típicas las comparaciones entre dos columnas o más:
In
[
35
]
:
df
[
'
c
'
]
>
df
[
'
b
'
]
Out
[
35
]
:
2021
-
07
-
01
True
2021
-
07
-
02
True
2021
-
07
-
05
True
2021
-
07
-
06
True
2021
-
07
-
07
True
Freq
:
B
,
dtype
:
bool
In
[
36
]
:
0.15
*
df
.
a
+
df
.
b
>
df
.
c
Out
[
36
]
:
2021
-
07
-
01
False
2021
-
07
-
02
False
2021
-
07
-
05
False
2021
-
07
-
06
True
2021
-
07
-
07
True
Freq
:
B
,
dtype
:
bool
¿Para qué fecha el elemento de la columna
c
es mayor que el de la columnab
?Condición que compara una combinación lineal de las columnas
a
yb
con la columnac
.
La vectorización con pandas
es un concepto potente, en particular para la aplicación de algoritmos financieros y el backtesting vectorizado, como se ilustra en el resto de este capítulo. Para más información sobre los fundamentos de la vectorización con pandas
y ejemplos financieros, consulta Hilpisch (2018, cap. 5).
Mientras que NumPy
aporta enfoques generales de vectorización al mundo de la computación numérica de Python, pandas
permite la vectorización sobre datos de series temporales. Esto es realmente útil para la implementación de algoritmos financieros y el backtesting de estrategias algorítmicas de trading. Al utilizar este enfoque, puedes esperar un código conciso, así como una ejecución más rápida del código, en comparación con el código Python estándar, que hace uso de los bucles for
y modismos similares para lograr el mismo objetivo.
Estrategias basadas en medias móviles simples
La negociación basada en medias móviles simples (SMA) es una estrategia con décadas de antigüedad que tiene sus orígenes en el mundo del análisis técnico bursátil. Brock et al. (1992), por ejemplo, investigan empíricamente este tipo de estrategias de forma sistemática. Escriben
El término "análisis técnico" es un encabezamiento general para una miríada de técnicas de negociación....En este artículo, exploramos dos de las reglas técnicas más sencillas y populares: la media móvil-oscilador y la ruptura del rango de negociación (niveles de resistencia y soporte). En el primer método, las señales de compra y venta se generan mediante dos medias móviles, una de periodo largo y otra de periodo corto....Nuestro estudio revela que el análisis técnico ayuda a predecir los cambios bursátiles.
Entrando en lo básico
Esta subsección se centra en los fundamentos del backtesting de estrategias de negociación que utilizan dos SMA. El ejemplo a seguir funciona con datos de cierre del fin del día (EOD) para el tipo de cambio EUR/USD, tal y como se proporcionan en el archivo csv bajo el archivo de datos EOD. Los datos del conjunto de datos proceden de la API de datos de Refinitiv Eikon y representan valores EOD para los respectivos instrumentos (RICs
):
In
[
37
]
:
raw
=
pd
.
read_csv
(
'
http://hilpisch.com/pyalgo_eikon_eod_data.csv
'
,
index_col
=
0
,
parse_dates
=
True
)
.
dropna
(
)
In
[
38
]
:
raw
.
info
(
)
<
class
'
pandas
.
core
.
frame
.
DataFrame
'
>
DatetimeIndex
:
2516
entries
,
2010
-
01
-
04
to
2019
-
12
-
31
Data
columns
(
total
12
columns
)
:
# Column Non-Null Count Dtype
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0
AAPL
.
O
2516
non
-
null
float64
1
MSFT
.
O
2516
non
-
null
float64
2
INTC
.
O
2516
non
-
null
float64
3
AMZN
.
O
2516
non
-
null
float64
4
GS
.
N
2516
non
-
null
float64
5
SPY
2516
non
-
null
float64
6
.
SPX
2516
non
-
null
float64
7
.
VIX
2516
non
-
null
float64
8
EUR
=
2516
non
-
null
float64
9
XAU
=
2516
non
-
null
float64
10
GDX
2516
non
-
null
float64
11
GLD
2516
non
-
null
float64
dtypes
:
float64
(
12
)
memory
usage
:
255.5
KB
In
[
39
]
:
data
=
pd
.
DataFrame
(
raw
[
'
EUR=
'
]
)
In
[
40
]
:
data
.
rename
(
columns
=
{
'
EUR=
'
:
'
price
'
}
,
inplace
=
True
)
In
[
41
]
:
data
.
info
(
)
<
class
'
pandas
.
core
.
frame
.
DataFrame
'
>
DatetimeIndex
:
2516
entries
,
2010
-
01
-
04
to
2019
-
12
-
31
Data
columns
(
total
1
columns
)
:
# Column Non-Null Count Dtype
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0
price
2516
non
-
null
float64
dtypes
:
float64
(
1
)
memory
usage
:
39.3
KB
Lee los datos del archivo
CSV
almacenado remotamente.Muestra la metainformación del objeto
DataFrame
.Transforma el objeto
Series
en un objetoDataFrame
.Cambia el nombre de la única columna a
price
.Muestra la metainformación del nuevo objeto
DataFrame
.
El cálculo de los SMA se simplifica mediante el método rolling()
, en combinación con una operación de cálculo diferido:
In
[
42
]
:
data
[
'
SMA1
'
]
=
data
[
'
price
'
]
.
rolling
(
42
)
.
mean
(
)
In
[
43
]
:
data
[
'
SMA2
'
]
=
data
[
'
price
'
]
.
rolling
(
252
)
.
mean
(
)
In
[
44
]
:
data
.
tail
(
)
Out
[
44
]
:
price
SMA1
SMA2
Date
2019
-
12
-
24
1.1087
1.107698
1.119630
2019
-
12
-
26
1.1096
1.107740
1.119529
2019
-
12
-
27
1.1175
1.107924
1.119428
2019
-
12
-
30
1.1197
1.108131
1.119333
2019
-
12
-
31
1.1210
1.108279
1.119231
Crea una columna con 42 días de valores de SMA. Los 41 primeros valores serán
NaN
.Crea una columna con 252 días de valores SMA. Los 251 primeros valores serán
NaN
.Imprime las cinco últimas filas del conjunto de datos.
Una visualización de los datos de la serie temporal original en combinación con los SMA ilustra mejor los resultados (ver Figura 4-1):
In
[
45
]:
%
matplotlib
inline
from
pylab
import
mpl
,
plt
plt
.
style
.
use
(
'seaborn'
)
mpl
.
rcParams
[
'savefig.dpi'
]
=
300
mpl
.
rcParams
[
'font.family'
]
=
'serif'
In
[
46
]:
data
.
plot
(
title
=
'EUR/USD | 42 & 252 days SMAs'
,
figsize
=
(
10
,
6
));
El siguiente paso es generar señales, o más bien posicionamientos de mercado, basados en la relación entre las dos SMA. La regla es ir en largo siempre que la SMA más corta esté por encima de la más larga y viceversa. A nuestros efectos, indicamos una posición larga con 1 y una posición corta con -1.
Poder comparar directamente dos columnas del objeto DataFrame
hace que la implementación de la regla sea cosa de una sola línea de código. El posicionamiento en el tiempo se ilustra en la Figura 4-2:
In
[
47
]
:
data
[
'
position
'
]
=
np
.
where
(
data
[
'
SMA1
'
]
>
data
[
'
SMA2
'
]
,
1
,
-
1
)
In
[
48
]
:
data
.
dropna
(
inplace
=
True
)
In
[
49
]
:
data
[
'
position
'
]
.
plot
(
ylim
=
[
-
1.1
,
1.1
]
,
title
=
'
Market Positioning
'
,
figsize
=
(
10
,
6
)
)
;
Implementa la regla de negociación de forma vectorizada.
np.where()
produce+1
para las filas en las que la expresión esTrue
y-1
para las filas en las que la expresión esFalse
.Elimina todas las filas del conjunto de datos que contengan al menos un valor
NaN
.Traza el posicionamiento a lo largo del tiempo.
Para calcular el rendimiento de la estrategia, calcula a continuación los rendimientos logarítmicos basados en la serie temporal financiera original. El código para hacerlo es, de nuevo, bastante conciso debido a la vectorización. La Figura 4-3 muestra el histograma de los rendimientos logarítmicos:
In
[
50
]
:
data
[
'
returns
'
]
=
np
.
log
(
data
[
'
price
'
]
/
data
[
'
price
'
]
.
shift
(
1
)
)
In
[
51
]
:
data
[
'
returns
'
]
.
hist
(
bins
=
35
,
figsize
=
(
10
,
6
)
)
;
Calcula los rendimientos logarítmicos de forma vectorizada sobre la columna
price
.Traza los retornos logarítmicos como un histograma (distribución de frecuencias).
Para obtener los rendimientos de la estrategia, multiplica la columna position
-desplazada un día de negociación- por la columna returns
. Como los rendimientos logarítmicos son aditivos, calcular la suma de las columnas returns
y strategy
proporciona una primera comparación del rendimiento de la estrategia en relación con la propia inversión base.
La comparación de los rendimientos muestra que la estrategia gana a la inversión pasiva de referencia:
In
[
52
]
:
data
[
'
strategy
'
]
=
data
[
'
position
'
]
.
shift
(
1
)
*
data
[
'
returns
'
]
In
[
53
]
:
data
[
[
'
returns
'
,
'
strategy
'
]
]
.
sum
(
)
Out
[
53
]
:
returns
-
0.176731
strategy
0.253121
dtype
:
float64
In
[
54
]
:
data
[
[
'
returns
'
,
'
strategy
'
]
]
.
sum
(
)
.
apply
(
np
.
exp
)
Out
[
54
]
:
returns
0.838006
strategy
1.288039
dtype
:
float64
Deriva los rendimientos logarítmicos de la estrategia dados los posicionamientos y los rendimientos del mercado.
Suma los valores de rentabilidad logarítmica única tanto de la acción como de la estrategia (sólo a título ilustrativo).
Aplica la función exponencial a la suma de los rendimientos logarítmicos para calcular el rendimiento bruto.
El cálculo de la suma acumulada a lo largo del tiempo con cumsum
y, a partir de ella, de los rendimientos acumulados aplicando la función exponencial np.exp()
ofrece una imagen más completa de cómo se compara la estrategia con el rendimiento del instrumento financiero base a lo largo del tiempo. La Figura 4-4 muestra los datos gráficamente e ilustra el rendimiento superior en este caso concreto:
In
[
55
]:
data
[[
'returns'
,
'strategy'
]]
.
cumsum
(
)
.
apply
(
np
.
exp
)
.
plot
(
figsize
=
(
10
,
6
));
Las estadísticas medias anualizadas de riesgo-rentabilidad, tanto de las acciones como de la estrategia, son fáciles de calcular:
In
[
56
]
:
data
[
[
'
returns
'
,
'
strategy
'
]
]
.
mean
(
)
*
252
Out
[
56
]
:
returns
-
0.019671
strategy
0.028174
dtype
:
float64
In
[
57
]
:
np
.
exp
(
data
[
[
'
returns
'
,
'
strategy
'
]
]
.
mean
(
)
*
252
)
-
1
Out
[
57
]
:
returns
-
0.019479
strategy
0.028575
dtype
:
float64
In
[
58
]
:
data
[
[
'
returns
'
,
'
strategy
'
]
]
.
std
(
)
*
252
*
*
0.5
Out
[
58
]
:
returns
0.085414
strategy
0.085405
dtype
:
float64
In
[
59
]
:
(
data
[
[
'
returns
'
,
'
strategy
'
]
]
.
apply
(
np
.
exp
)
-
1
)
.
std
(
)
*
252
*
*
0.5
Out
[
59
]
:
returns
0.085405
strategy
0.085373
dtype
:
float64
Calcula la rentabilidad media anualizada en espacio logarítmico y regular.
Calcula la desviación típica anualizada en espacio logarítmico y regular.
Otras estadísticas de riesgo que suelen interesar en el contexto de los rendimientos de las estrategias de negociación son la reducción máxima y el periodo de reducción más largo. Una estadística de ayuda que se puede utilizar en este contexto es el rendimiento bruto máximo acumulado, calculado mediante el método cummax()
aplicado al rendimiento bruto de la estrategia. La Figura 4-5 muestra las dos series temporales de la estrategia basada en SMA:
In
[
60
]
:
data
[
'
cumret
'
]
=
data
[
'
strategy
'
]
.
cumsum
(
)
.
apply
(
np
.
exp
)
In
[
61
]
:
data
[
'
cummax
'
]
=
data
[
'
cumret
'
]
.
cummax
(
)
In
[
62
]
:
data
[
[
'
cumret
'
,
'
cummax
'
]
]
.
dropna
(
)
.
plot
(
figsize
=
(
10
,
6
)
)
;
Define una nueva columna,
cumret
, con el rendimiento bruto a lo largo del tiempo.Define otra columna con el valor máximo en funcionamiento delrendimiento bruto.
Traza las dos nuevas columnas del objeto
DataFrame
.
La reducción máxima se calcula simplemente como el máximo de la diferencia entre las dos columnas correspondientes. En el ejemplo, la reducción máxima es de unos 18 puntos porcentuales:
In
[
63
]
:
drawdown
=
data
[
'
cummax
'
]
-
data
[
'
cumret
'
]
In
[
64
]
:
drawdown
.
max
(
)
Out
[
64
]
:
0.17779367070195917
Calcula la diferencia por elementos entre las dos columnas.
Selecciona el valor máximo de todas las diferencias.
La determinación del periodo de reducción más largo es un poco más complicada. Requiere aquellas fechas en las que el rendimiento bruto es igual a su máximo acumulado (es decir, en las que se establece un nuevo máximo). Esta información se almacena en un objeto temporal. Después se calculan las diferencias en días entre todas esas fechas y se elige el periodo más largo. Estos periodos pueden ser de sólo un día o de más de 100 días. En este caso, el periodo de reducción más largo dura 596 días, un periodo bastante largo:2
In
[
65
]
:
temp
=
drawdown
[
drawdown
==
0
]
In
[
66
]
:
periods
=
(
temp
.
index
[
1
:
]
.
to_pydatetime
(
)
-
temp
.
index
[
:
-
1
]
.
to_pydatetime
(
)
)
In
[
67
]
:
periods
[
12
:
15
]
Out
[
67
]
:
array
(
[
datetime
.
timedelta
(
days
=
1
)
,
datetime
.
timedelta
(
days
=
1
)
,
datetime
.
timedelta
(
days
=
10
)
]
,
dtype
=
object
)
In
[
68
]
:
periods
.
max
(
)
Out
[
68
]
:
datetime
.
timedelta
(
days
=
596
)
¿Dónde están las diferencias iguales a cero?
Calcula los valores de
timedelta
entre todos los valores del índice.Selecciona el valor máximo de
timedelta
.
El backtesting vectorizado con pandas
suele ser una tarea bastante eficiente gracias a las capacidades del paquete y de la clase principal DataFrame
. Sin embargo, el enfoque interactivo ilustrado hasta ahora no funciona bien cuando se desea implementar un programa de backtesting más amplio que, por ejemplo, optimice los parámetros de una estrategia basada en SMA. Para ello, es aconsejable un enfoque más general.
pandas
resulta ser una potente herramienta para el análisis vectorizado de estrategias de negociación. Muchas estadísticas de interés, como los rendimientos logarítmicos, los rendimientos acumulados, los rendimientos anualizados y la volatilidad, la reducción máxima y el periodo de reducción máxima, pueden calcularse, en general, con una sola línea o unas pocas líneas de código. Poder visualizar los resultados mediante una simple llamada a un método es una ventaja adicional.
Generalizar el enfoque
"Clase de backtesting de SMA" presenta un código Python que contiene una clase para el backtesting vectorizado de estrategias de negociación basadas en SMA. En cierto sentido, es una generalización del enfoque presentado en la subsección anterior. Permite definir una instancia de la clase SMAVectorBacktester
proporcionando los siguientes parámetros:
-
symbol
:RIC
(datos del instrumento) a utilizar -
SMA1
para la ventana temporal en días para la SMA más corta -
SMA2
para la ventana temporal en días para la SMA más larga -
start
: para la fecha de inicio de la selección de datos -
end
: para la fecha final de la selección de datos
La mejor forma de ilustrar la aplicación en sí es mediante una sesión interactiva que hace uso de la clase. El ejemplo replica primero el backtest implementado anteriormente basado en datos del tipo de cambio EUR/USD. A continuación, optimiza los parámetros del SMA para obtener el máximo rendimiento bruto. Basándose en los parámetros óptimos, traza elrendimiento bruto resultante de la estrategia en comparación con el instrumento base durante el periodode tiempo correspondiente:
In
[
69
]
:
import
SMAVectorBacktester
as
SMA
In
[
70
]
:
smabt
=
SMA
.
SMAVectorBacktester
(
'
EUR=
'
,
42
,
252
,
'
2010-1-1
'
,
'
2019-12-31
'
)
In
[
71
]
:
smabt
.
run_strategy
(
)
Out
[
71
]
:
(
1.29
,
0.45
)
In
[
72
]
:
%
%
time
smabt
.
optimize_parameters
(
(
30
,
50
,
2
)
,
(
200
,
300
,
2
)
)
CPU
times
:
user
3.76
s
,
sys
:
15.8
ms
,
total
:
3.78
s
Wall
time
:
3.78
s
Out
[
72
]
:
(
array
(
[
48.
,
238.
]
)
,
1.5
)
In
[
73
]
:
smabt
.
plot_results
(
)
Esto importa el módulo como
SMA
.Se crea una instancia de la clase principal.
Realiza una prueba retrospectiva de la estrategia basada en SMA, dados los parámetros durante la instanciación.
El método
optimize_parameters()
toma como entrada rangos de parámetros con tamaños de paso y determina la combinación óptima mediante un enfoque de fuerza bruta.El método
plot_results()
traza el rendimiento de la estrategia en comparación con el instrumento de referencia, dados los valores de los parámetros almacenados actualmente (aquí del procedimiento de optimización).
El rendimiento bruto de la estrategia con la parametrización original es de 1,24 o 124%. La estrategia optimizada produce un rendimiento absoluto de 1,44 o 144% para la combinación de parámetros SMA1 = 48
y SMA2 = 238
. La Figura 4-6 muestra gráficamente el rendimiento bruto a lo largo del tiempo, de nuevo comparado con el rendimiento del instrumento base, que representa el valor de referencia.
Estrategias basadas en el impulso
Hay dos tipos básicos de estrategias de impulso. El primer tipo son las estrategias de impulso transversal. Seleccionando entre un conjunto más amplio de instrumentos, estas estrategias compran los instrumentos que recientemente han obtenido mejores resultados que sus homólogos (o un índice de referencia) y venden los instrumentos que han obtenido peores resultados. La idea básica es que los instrumentos sigan teniendo un rendimiento superior o inferior, respectivamente, al menos durante un cierto periodo de tiempo. Jegadeesh y Titman (1993, 2001) y Chan et al. (1996) estudian este tipo de estrategias de negociación y sus posibles fuentes de beneficios.
Tradicionalmente, las estrategias de impulso transversal han funcionado bastante bien. Jegadeesh y Titman (1993) escriben:
Este documento documenta que las estrategias que compran acciones que han tenido buenos resultados en el pasado y venden acciones que han tenido malos resultados en el pasado generan rendimientos positivos significativos durante periodos de tenencia de 3 a 12 meses.
El segundo tipo son las estrategias de impulso de series temporales. Estas estrategias compran los instrumentos que recientemente han obtenido buenos resultados y venden los instrumentos que recientemente han obtenido malos resultados. En este caso, el punto de referencia son los rendimientos pasados del propio instrumento. Moskowitz et al. (2012) analizan detalladamente este tipo de estrategia de impulso en una amplia gama de mercados. Escriben
En lugar de centrarse en los rendimientos relativos de los valores en el corte transversal, el impulso de las series temporales se centra exclusivamente en el rendimiento pasado de un valor..... Nuestra observación del impulso de las series temporales en prácticamente todos los instrumentos que examinamos parece cuestionar la hipótesis del "paseo aleatorio", que en su forma más básica implica que saber si un precio subió o bajó en el pasado no debería ser informativo sobre si subirá o bajará en el futuro.
Entrando en lo básico
Considera precios de cierre al final del día para el precio del oro en USD (XAU=
):
In
[
74
]:
data
=
pd
.
DataFrame
(
raw
[
'XAU='
])
In
[
75
]:
data
.
rename
(
columns
=
{
'XAU='
:
'price'
},
inplace
=
True
)
In
[
76
]:
data
[
'returns'
]
=
np
.
log
(
data
[
'price'
]
/
data
[
'price'
]
.
shift
(
1
))
La estrategia de impulso de series temporales más sencilla consiste en comprar la acción si el último rendimiento fue positivo y venderla si fue negativo. Con NumPy
y pandas
esto es fácil de formalizar; basta con tomar el signo de la última rentabilidad disponible como posición de mercado. La Figura 4-7 ilustra el rendimiento de esta estrategia. La estrategia tiene un rendimiento significativamente inferior al instrumento base:
In
[
77
]
:
data
[
'
position
'
]
=
np
.
sign
(
data
[
'
returns
'
]
)
In
[
78
]
:
data
[
'
strategy
'
]
=
data
[
'
position
'
]
.
shift
(
1
)
*
data
[
'
returns
'
]
In
[
79
]
:
data
[
[
'
returns
'
,
'
strategy
'
]
]
.
dropna
(
)
.
cumsum
(
)
.
apply
(
np
.
exp
)
.
plot
(
figsize
=
(
10
,
6
)
)
;
Define una nueva columna con el signo (es decir, 1 ó -1) de la rentabilidad logarítmica correspondiente; los valores resultantes representan las posiciones en el mercado (largas o cortas).
Calcula los rendimientos logarítmicos de la estrategia dados los posicionamientos del mercado.
Traza y compara el rendimiento de la estrategia con el instrumento de referencia.
Utilizando una ventana temporal móvil, la estrategia de impulso de series temporales puede generalizarse a algo más que el último rendimiento. Por ejemplo, se puede utilizar la media de las tres últimas rentabilidades para generar la señal de posicionamiento. La Figura 4-8 muestra que, en este caso, la estrategia lo hace mucho mejor, tanto en términos absolutos como en relación con elinstrumento base:
In
[
80
]
:
data
[
'
position
'
]
=
np
.
sign
(
data
[
'
returns
'
]
.
rolling
(
3
)
.
mean
(
)
)
In
[
81
]
:
data
[
'
strategy
'
]
=
data
[
'
position
'
]
.
shift
(
1
)
*
data
[
'
returns
'
]
In
[
82
]
:
data
[
[
'
returns
'
,
'
strategy
'
]
]
.
dropna
(
)
.
cumsum
(
)
.
apply
(
np
.
exp
)
.
plot
(
figsize
=
(
10
,
6
)
)
;
Sin embargo, el rendimiento es bastante sensible al parámetro de la ventana temporal. Elegir, por ejemplo, los dos últimos rendimientos en lugar de tres conduce a un rendimiento mucho peor, como se muestra en la Figura 4-9.
El impulso de las series temporales también podría esperarse intradía. En realidad, cabría esperar que fuera más pronunciado intradía que interdía. La Figura 4-10 muestra el rendimiento bruto de cinco estrategias de impulso de series temporales para una, tres, cinco, siete y nueve observaciones de rentabilidad, respectivamente. Los datos utilizados son los precios intradía de las acciones de Apple Inc. recuperados de la API de datos de Eikon. La figura se basa en el código que sigue. Básicamente, todas las estrategias superan a la acción en el transcurso de esta ventana temporal intradiaria, aunque algunas sólo ligeramente:
In
[
83
]
:
fn
=
'
../data/AAPL_1min_05052020.csv
'
# fn = '../data/SPX_1min_05052020.csv'
In
[
84
]
:
data
=
pd
.
read_csv
(
fn
,
index_col
=
0
,
parse_dates
=
True
)
In
[
85
]
:
data
.
info
(
)
<
class
'
pandas
.
core
.
frame
.
DataFrame
'
>
DatetimeIndex
:
241
entries
,
2020
-
05
-
05
16
:
00
:
00
to
2020
-
05
-
05
20
:
00
:
00
Data
columns
(
total
6
columns
)
:
# Column Non-Null Count Dtype
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0
HIGH
241
non
-
null
float64
1
LOW
241
non
-
null
float64
2
OPEN
241
non
-
null
float64
3
CLOSE
241
non
-
null
float64
4
COUNT
241
non
-
null
float64
5
VOLUME
241
non
-
null
float64
dtypes
:
float64
(
6
)
memory
usage
:
13.2
KB
In
[
86
]
:
data
[
'
returns
'
]
=
np
.
log
(
data
[
'
CLOSE
'
]
/
data
[
'
CLOSE
'
]
.
shift
(
1
)
)
In
[
87
]
:
to_plot
=
[
'
returns
'
]
In
[
88
]
:
for
m
in
[
1
,
3
,
5
,
7
,
9
]
:
data
[
'
position_
%d
'
%
m
]
=
np
.
sign
(
data
[
'
returns
'
]
.
rolling
(
m
)
.
mean
(
)
)
data
[
'
strategy_
%d
'
%
m
]
=
(
data
[
'
position_
%d
'
%
m
]
.
shift
(
1
)
*
data
[
'
returns
'
]
)
to_plot
.
append
(
'
strategy_
%d
'
%
m
)
In
[
89
]
:
data
[
to_plot
]
.
dropna
(
)
.
cumsum
(
)
.
apply
(
np
.
exp
)
.
plot
(
title
=
'
AAPL intraday 05. May 2020
'
,
figsize
=
(
10
,
6
)
,
style
=
[
'
-
'
,
'
--
'
,
'
--
'
,
'
--
'
,
'
--
'
,
'
--
'
]
)
;
Lee los datos intradiarios de un archivo
CSV
.Calcula los rendimientos log intradía.
Define un objeto
list
para seleccionar las columnas que se trazarán posteriormente.Deriva posicionamientos según el parámetro de la estrategia de impulso.
Calcula los rendimientos logarítmicos de la estrategia resultante.
Añade el nombre de la columna al objeto
list
.Traza todas las columnas relevantes para comparar el rendimiento de las estrategias con el rendimiento del instrumento de referencia.
La Figura 4-11 muestra el rendimiento de las mismas cinco estrategias para el índice S&P 500. De nuevo, las cinco configuraciones de estrategias superan al índice y todas muestran un rendimiento positivo (antes de los costes de transacción).
Generalizar el enfoque
"Momentum Backtesting Class" presenta un módulo Python que contiene la clase MomVectorBacktester
, que permite realizar un backtesting un poco más estandarizado de las estrategias basadas en el impulso. La clase tiene los siguientes atributos:
-
symbol
:RIC
(datos del instrumento) a utilizar -
start
: para la fecha de inicio de la selección de datos -
end
: para la fecha final de la selección de datos -
amount
: para la cantidad inicial a invertir -
tc
para los costes de transacción proporcionales por operación
En comparación con la clase SMAVectorBacktester
, ésta introduce dos generalizaciones importantes: la cantidad fija que hay que invertir al principio del periodo de backtesting y los costes de transacción proporcionales para acercarse más a la realidad del mercado en cuanto a costes. En particular, la adición de los costes de transacción es importante en el contexto de las estrategias de impulso de series temporales, que a menudo dan lugar a un gran número de transacciones a lo largo del tiempo.
La aplicación es tan sencilla y cómoda como antes. El ejemplo replica primero los resultados de la sesión interactiva anterior, pero esta vez con una inversión inicial de 10.000 USD. La Figura 4-12 visualiza el rendimiento de la estrategia, tomando la media de los tres últimos rendimientos para generar señales para el posicionamiento. El segundo caso contemplado es uno con costes de transacción proporcionales del 0,1% por operación. Como ilustra la Figura 4-13, incluso unos costes de transacción pequeños deterioran significativamente el rendimiento en este caso. El factor determinante a este respecto es la frecuencia relativamente alta de las operaciones que requiere la estrategia:
In
[
90
]
:
import
MomVectorBacktester
as
Mom
In
[
91
]
:
mombt
=
Mom
.
MomVectorBacktester
(
'
XAU=
'
,
'
2010-1-1
'
,
'
2019-12-31
'
,
10000
,
0.0
)
In
[
92
]
:
mombt
.
run_strategy
(
momentum
=
3
)
Out
[
92
]
:
(
20797.87
,
7395.53
)
In
[
93
]
:
mombt
.
plot_results
(
)
In
[
94
]
:
mombt
=
Mom
.
MomVectorBacktester
(
'
XAU=
'
,
'
2010-1-1
'
,
'
2019-12-31
'
,
10000
,
0.001
)
In
[
95
]
:
mombt
.
run_strategy
(
momentum
=
3
)
Out
[
95
]
:
(
10749.4
,
-
2652.93
)
In
[
96
]
:
mombt
.
plot_results
(
)
Importa el módulo como
Mom
Instantiza un objeto de la clase backtesting definiendo que el capital inicial sea de 10.000 USD y que los costes de transacción proporcionales sean cero.
Prueba retrospectiva de la estrategia de impulso basada en una ventana temporal de tres días: la estrategia supera a la inversión pasiva de referencia.
Esta vez, se suponen unos costes de transacción proporcionales del 0,1% por operación.
En ese caso, la estrategia pierde básicamente todo el rendimiento superior.
Estrategias basadas en la reversión a la media
A grandes rasgos, las estrategias de reversión a la media se basan en un razonamiento opuesto al de las estrategias de impulso. Si un instrumento financiero se ha comportado "demasiado bien" en relación con su tendencia, se pone en corto, y viceversa. Dicho de otro modo, mientras que las estrategias de impulso (series temporales) suponen una correlación positiva entre los rendimientos, las estrategias de reversión a la media suponen una correlación negativa. Balvers et al. (2000) escriben:
La reversión a la media se refiere a la tendencia de los precios de los activos a volver a una trayectoria tendencial.
Trabajando con una media móvil simple (SMA) como sustituto de una "trayectoria de tendencia", una estrategia de reversión a la media en, por ejemplo, el tipo de cambio EUR/USD puede someterse a pruebas retrospectivas de forma similar a las pruebas retrospectivas de las estrategias basadas en la SMA y en el impulso. La idea es definir un umbral para la distancia entre la cotización actual y la SMA, que señale una posición larga o corta.
Entrando en lo básico
Los ejemplos de que siguen corresponden a dos instrumentos financieros diferentes para los que cabría esperar una reversión a la media significativa, ya que ambos se basan en el precio del oro:
-
GLD
es el símbolo de SPDR Gold Shares, que es el mayor fondo cotizado en bolsa (ETF) de oro con respaldo físico (véase la página de inicio de SPDR Gold Shares). -
GDX
es el símbolo del ETF VanEck Vectors Gold Miners, que invierte en productos de renta variable para seguir el índice NYSE Arca Gold Miners Index (véase la página de información general de VanEck Vectors Gold Miners).
El ejemplo comienza con GDX
e implementa una estrategia de reversión a la media sobre la base de una SMA de 25 días y un valor umbral de 3,5 para que la desviación absoluta del precio actual se desvíe de la SMA para señalar un posicionamiento. La Figura 4-14 muestra las diferencias entre el precio actual de GDX
y la SMA, así como el valor umbral positivo y negativo para generar señales de venta y compra, respectivamente:
In
[
97
]
:
data
=
pd
.
DataFrame
(
raw
[
'
GDX
'
]
)
In
[
98
]
:
data
.
rename
(
columns
=
{
'
GDX
'
:
'
price
'
}
,
inplace
=
True
)
In
[
99
]
:
data
[
'
returns
'
]
=
np
.
log
(
data
[
'
price
'
]
/
data
[
'
price
'
]
.
shift
(
1
)
)
In
[
100
]
:
SMA
=
25
In
[
101
]
:
data
[
'
SMA
'
]
=
data
[
'
price
'
]
.
rolling
(
SMA
)
.
mean
(
)
In
[
102
]
:
threshold
=
3.5
In
[
103
]
:
data
[
'
distance
'
]
=
data
[
'
price
'
]
-
data
[
'
SMA
'
]
In
[
104
]
:
data
[
'
distance
'
]
.
dropna
(
)
.
plot
(
figsize
=
(
10
,
6
)
,
legend
=
True
)
plt
.
axhline
(
threshold
,
color
=
'
r
'
)
plt
.
axhline
(
-
threshold
,
color
=
'
r
'
)
plt
.
axhline
(
0
,
color
=
'
r
'
)
;
El parámetro SMA se define...
...y se calcula la SMA ("trayectoria de tendencia").
Se define el umbral para la generación de la señal.
La distancia se calcula para cada punto en el tiempo.
Se trazan los valores de distancia.
A partir de las diferencias y de los valores umbral fijados, se pueden derivar de nuevo posicionamientos de forma vectorizada. La Figura 4-15 muestra los posicionamientos resultantes:
In
[
105
]
:
data
[
'
position
'
]
=
np
.
where
(
data
[
'
distance
'
]
>
threshold
,
-
1
,
np
.
nan
)
In
[
106
]
:
data
[
'
position
'
]
=
np
.
where
(
data
[
'
distance
'
]
<
-
threshold
,
1
,
data
[
'
position
'
]
)
In
[
107
]
:
data
[
'
position
'
]
=
np
.
where
(
data
[
'
distance
'
]
*
data
[
'
distance
'
]
.
shift
(
1
)
<
0
,
0
,
data
[
'
position
'
]
)
In
[
108
]
:
data
[
'
position
'
]
=
data
[
'
position
'
]
.
ffill
(
)
.
fillna
(
0
)
In
[
109
]
:
data
[
'
position
'
]
.
iloc
[
SMA
:
]
.
plot
(
ylim
=
[
-
1.1
,
1.1
]
,
figsize
=
(
10
,
6
)
)
;
Si el valor de la distancia es mayor que el valor umbral, ponte corto (pon -1 en la nueva columna
position
), de lo contrario ponNaN
.Si el valor de la distancia es inferior al valor del umbral negativo, ve a largo (pon 1); de lo contrario, mantén la columna
position
sin cambios.Si hay un cambio en el signo del valor de la distancia, pasa a mercado neutral (pon 0), de lo contrario mantén la columna
position
sin cambios.Rellena hacia delante todas las posiciones
NaN
con los valores anteriores; sustituye todos los valoresNaN
restantes por 0.Traza las posiciones resultantes a partir de la posición índice
SMA
.
El último paso es obtener los rendimientos de la estrategia, que se muestran en la Figura 4-16. La estrategia supera al ETF GDX
por bastante margen, aunque la parametrización particular conduce a largos periodos con una posición neutral (ni larga ni corta). Estas posiciones neutrales se reflejan en las partes planas de la curva de la estrategia en la Figura 4-16:
In
[
110
]:
data
[
'strategy'
]
=
data
[
'position'
]
.
shift
(
1
)
*
data
[
'returns'
]
In
[
111
]:
data
[[
'returns'
,
'strategy'
]]
.
dropna
()
.
cumsum
(
)
.
apply
(
np
.
exp
)
.
plot
(
figsize
=
(
10
,
6
));
Generalizar el enfoque
Como antes, el backtesting vectorizado es más eficiente de implementar basándose en una clase Python respectiva. La clase MRVectorBacktester
presentada en "Clase de backtesting de reversión a la media " hereda de la clase MomVectorBacktester
y sólo sustituye el método run_strategy()
para adaptarse a las particularidades de la estrategia de reversión a la media.
El ejemplo utiliza ahora GLD
y fija los costes de transacción proporcionales en 0,1%. La cantidad inicial a invertir se fija de nuevo en 10.000 USD. Esta vez la SMA es 43, y el valor umbral se fija en 7,5. La Figura 4-17 muestra el rendimiento de la estrategia de reversión a la media en comparación con el ETF GLD
:
In
[
112
]
:
import
MRVectorBacktester
as
MR
In
[
113
]
:
mrbt
=
MR
.
MRVectorBacktester
(
'
GLD
'
,
'
2010-1-1
'
,
'
2019-12-31
'
,
10000
,
0.001
)
In
[
114
]
:
mrbt
.
run_strategy
(
SMA
=
43
,
threshold
=
7.5
)
Out
[
114
]
:
(
13542.15
,
646.21
)
In
[
115
]
:
mrbt
.
plot_results
(
)
Importa el módulo como
MR
.Instantiza un objeto de la clase
MRVectorBacktester
con un capital inicial de 10.000 USD y unos costes de transacción proporcionales del 0,1% por operación; la estrategia supera significativamente al instrumento de referencia en este caso.Realiza una prueba retrospectiva de la estrategia de reversión a la media con un valor
SMA
de 43 y un valorthreshold
de 7,5.Traza el rendimiento acumulado de la estrategia frente al instrumento base .
Espionaje de datos y sobreajuste
En este capítulo, así como en el resto del libro, se hace hincapié en la aplicación tecnológica de conceptos importantes del trading algorítmico mediante el uso de Python. Las estrategias, los parámetros, los conjuntos de datos y los algoritmos utilizados se eligen a veces de forma arbitraria y a veces de forma intencionada para demostrar algo. Sin duda, al hablar de métodos técnicos aplicados a las finanzas, es más emocionante ymotivador ver ejemplos que muestran "buenos resultados", aunque no sean generalizables en otros instrumentos financieros o periodos de tiempo, por ejemplo.
La capacidad de mostrar ejemplos con buenos resultados suele producirse a costa del espionaje de datos. Según White (2000), el espionaje de datos puede definirse del siguiente modo:
El snooping de datos se produce cuando un determinado conjunto de datos se utiliza más de una vez con fines de inferencia o selección de modelos.
En otras palabras, un determinado enfoque puede aplicarse varias o incluso muchas veces al mismo conjunto de datos para llegar a cifras y gráficos satisfactorios. Esto, por supuesto, es intelectualmente deshonesto en la investigación de estrategias de negociación, porque pretende que una estrategia de negociación tiene un potencial económico que podría no ser realista en un contexto del mundo real. Dado que este libro se centra en el uso de Python como lenguaje de programación para el trading algorítmico, el enfoque de espionaje de datos podría estar justificado. Esto es análogo a un libro de matemáticas que, a modo de ejemplo, resuelve una ecuación que tiene una solución única que puede identificarse fácilmente. En matemáticas, estos ejemplos sencillos son más bien la excepción que la regla, pero sin embargo se utilizan con frecuencia con fines didácticos.
Otro problema que surge en este contexto es el sobreajuste. El sobreajuste en uncontexto de negociación puede describirse del siguiente modo (véase el Instituto Man sobre Sobreajuste):
La sobreadaptación se produce cuando un modelo describe ruido en lugar de señal. El modelo puede tener un buen rendimiento con los datos con los que se probó, pero poco o ningún poder predictivo con nuevos datos en el futuro. El sobreajuste puede describirse como la búsqueda de patrones que en realidad no existen. El sobreajuste tiene un coste: una estrategia sobreajustada tendrá un rendimiento inferior en el futuro.
Incluso una estrategia sencilla, como la basada en dos valores de SMA, permite realizar backtesting de miles de combinaciones de parámetros diferentes. Es casi seguro que algunas de esas combinaciones muestren buenos resultados de rendimiento. Como explican detalladamente Bailey et al. (2015), esto conduce fácilmente a un sobreajuste del backtest, sin que los responsables del mismo sean conscientes del problema. Señalan
Los recientes avances en la investigación algorítmica y la informática de alto rendimiento han hecho que sea casi trivial probar millones y miles de millones de estrategias de inversión alternativas en un conjunto finito de datos de series temporales financieras....[E]s práctica común utilizar esta potencia computacional para calibrar los parámetros de una estrategia de inversión con el fin de maximizar su rendimiento. Pero como la relación señal-ruido es tan débil, a menudo el resultado de dicha calibración es que los parámetros se eligen para sacar provecho del ruido pasado en lugar de la señal futura. El resultado es un backtest sobreajustado.
El problema de la validez de los resultados empíricos, en sentido estadístico, no se limita, por supuesto, al backtesting de estrategias en un contexto financiero.
Ioannidis (2005), refiriéndose a las publicaciones médicas, hace hincapié en las consideraciones probabilísticas y estadísticas a la hora de juzgar la reproducibilidad y validez de los resultados de la investigación:
Cada vez preocupa más que, en la investigación moderna, los resultados falsos puedan ser la mayoría o incluso la inmensa mayoría de las afirmaciones de investigación publicadas. Sin embargo, esto no debería sorprender. Puede demostrarse que la mayoría de los resultados de investigación que se afirman son falsos.... Como se ha demostrado anteriormente, la probabilidad de que un resultado de investigación sea realmente cierto depende de la probabilidad previa de que lo sea (antes de hacer el estudio), de la potencia estadística del estudio y del nivel de significación estadística.
En este contexto, si se demuestra que una estrategia de negociación de este libro funciona bien dado un determinado conjunto de datos, una combinación de parámetros y quizá un algoritmo específico de aprendizaje automático, esto no constituye ningún tipo de recomendación para la configuración concreta ni permite extraer conclusiones más generales sobre la calidad y el potencial de rendimiento de la configuración de la estrategia en cuestión.
Por supuesto, te animamos a que utilices el código y los ejemplos presentados en este libro para explorar tus propias ideas de estrategias de negociación algorítmica e implementarlas en la práctica basándote en tus propios resultados de backtesting, validaciones y conclusiones. Al fin y al cabo, lo que los mercados financieros compensarán es una investigación adecuada y diligente de las estrategias, no el fisgoneo de datos y el sobreajuste impulsados por la fuerza bruta.
Conclusiones
La vectorización es un concepto poderoso en la informática científica, así como para la analítica financiera, en el contexto del backtesting de estrategias algorítmicas de negociación. Este capítulo presenta la vectorización tanto en NumPy
como en pandas
, y la aplica al backtesting de tres tipos de estrategias de negociación: estrategias basadas en medias móviles simples, en el impulso y en la reversión a la media. Hay que admitir que el capítulo hace una serie de suposiciones simplificadoras, y que un backtesting riguroso de las estrategias de negociación debe tener en cuenta más factores que determinan el éxito de la negociación en la práctica, como los problemas de los datos, los problemas de selección, la evitación del sobreajuste o los elementos de microestructura del mercado. Sin embargo, el objetivo principal del capítulo es centrarse en el concepto de vectorización y lo que puede hacer en la negociación algorítmica desde un punto de vista tecnológico y de implementación. Con respecto a todos los ejemplos y resultados concretos presentados, hay que tener en cuenta los problemas del espionaje de datos, el sobreajuste y la significación estadística.
Referencias y otros recursos
Para conocer los fundamentos de la vectorización con NumPy
y pandas
, consulta estos libros:
Para el uso de NumPy
y pandas
en un contexto financiero, consulta estos libros:
Para los temas del espionaje de datos y el sobreajuste, consulta estos documentos:
Para más información de fondo y resultados empíricos sobre estrategias de negociación basadas en medias móviles simples, consulta estas fuentes:
El libro de Ernest Chan trata en detalle las estrategias de negociación basadas en el impulso, así como en la reversión a la media. El libro también es una buena fuente de información sobre las trampas del backtesting de estrategias de negociación:
Estos trabajos de investigación analizan las características y las fuentes de beneficios de las estrategias de impulso transversal, el enfoque tradicional de la negociación basada en el impulso:
El artículo de Moskowitz et al. ofrece un análisis de las llamadas estrategias de impulso de series temporales:
Estos trabajos analizan empíricamente la reversión a la media en los precios de los activos:
Scripts de Python
Esta sección presenta los scripts de Python a los que se hace referencia y que se utilizan en este capítulo.
Clase de Backtesting SMA
A continuación se presenta un código Python con una clase para el backtesting vectorizado de estrategias basadas en medias móviles simples:
#
# Python Module with Class
# for Vectorized Backtesting
# of SMA-based Strategies
#
# Python for Algorithmic Trading
# (c) Dr. Yves J. Hilpisch
# The Python Quants GmbH
#
import
numpy
as
np
import
pandas
as
pd
from
scipy.optimize
import
brute
class
SMAVectorBacktester
(
object
):
''' Class for the vectorized backtesting of SMA-based trading strategies.
Attributes
==========
symbol: str
RIC symbol with which to work
SMA1: int
time window in days for shorter SMA
SMA2: int
time window in days for longer SMA
start: str
start date for data retrieval
end: str
end date for data retrieval
Methods
=======
get_data:
retrieves and prepares the base data set
set_parameters:
sets one or two new SMA parameters
run_strategy:
runs the backtest for the SMA-based strategy
plot_results:
plots the performance of the strategy compared to the symbol
update_and_run:
updates SMA parameters and returns the (negative) absolute performance
optimize_parameters:
implements a brute force optimization for the two SMA parameters
'''
def
__init__
(
self
,
symbol
,
SMA1
,
SMA2
,
start
,
end
):
self
.
symbol
=
symbol
self
.
SMA1
=
SMA1
self
.
SMA2
=
SMA2
self
.
start
=
start
self
.
end
=
end
self
.
results
=
None
self
.
get_data
()
def
get_data
(
self
):
''' Retrieves and prepares the data.
'''
raw
=
pd
.
read_csv
(
'http://hilpisch.com/pyalgo_eikon_eod_data.csv'
,
index_col
=
0
,
parse_dates
=
True
)
.
dropna
()
raw
=
pd
.
DataFrame
(
raw
[
self
.
symbol
])
raw
=
raw
.
loc
[
self
.
start
:
self
.
end
]
raw
.
rename
(
columns
=
{
self
.
symbol
:
'price'
},
inplace
=
True
)
raw
[
'return'
]
=
np
.
log
(
raw
/
raw
.
shift
(
1
))
raw
[
'SMA1'
]
=
raw
[
'price'
]
.
rolling
(
self
.
SMA1
)
.
mean
()
raw
[
'SMA2'
]
=
raw
[
'price'
]
.
rolling
(
self
.
SMA2
)
.
mean
()
self
.
data
=
raw
def
set_parameters
(
self
,
SMA1
=
None
,
SMA2
=
None
):
''' Updates SMA parameters and resp. time series.
'''
if
SMA1
is
not
None
:
self
.
SMA1
=
SMA1
self
.
data
[
'SMA1'
]
=
self
.
data
[
'price'
]
.
rolling
(
self
.
SMA1
)
.
mean
()
if
SMA2
is
not
None
:
self
.
SMA2
=
SMA2
self
.
data
[
'SMA2'
]
=
self
.
data
[
'price'
]
.
rolling
(
self
.
SMA2
)
.
mean
()
def
run_strategy
(
self
):
''' Backtests the trading strategy.
'''
data
=
self
.
data
.
copy
()
.
dropna
()
data
[
'position'
]
=
np
.
where
(
data
[
'SMA1'
]
>
data
[
'SMA2'
],
1
,
-
1
)
data
[
'strategy'
]
=
data
[
'position'
]
.
shift
(
1
)
*
data
[
'return'
]
data
.
dropna
(
inplace
=
True
)
data
[
'creturns'
]
=
data
[
'return'
]
.
cumsum
()
.
apply
(
np
.
exp
)
data
[
'cstrategy'
]
=
data
[
'strategy'
]
.
cumsum
()
.
apply
(
np
.
exp
)
self
.
results
=
data
# gross performance of the strategy
aperf
=
data
[
'cstrategy'
]
.
iloc
[
-
1
]
# out-/underperformance of strategy
operf
=
aperf
-
data
[
'creturns'
]
.
iloc
[
-
1
]
return
round
(
aperf
,
2
),
round
(
operf
,
2
)
def
plot_results
(
self
):
''' Plots the cumulative performance of the trading strategy
compared to the symbol.
'''
if
self
.
results
is
None
:
(
'No results to plot yet. Run a strategy.'
)
title
=
'
%s
| SMA1=
%d
, SMA2=
%d
'
%
(
self
.
symbol
,
self
.
SMA1
,
self
.
SMA2
)
self
.
results
[[
'creturns'
,
'cstrategy'
]]
.
plot
(
title
=
title
,
figsize
=
(
10
,
6
))
def
update_and_run
(
self
,
SMA
):
''' Updates SMA parameters and returns negative absolute performance
(for minimazation algorithm).
Parameters
==========
SMA: tuple
SMA parameter tuple
'''
self
.
set_parameters
(
int
(
SMA
[
0
]),
int
(
SMA
[
1
]))
return
-
self
.
run_strategy
()[
0
]
def
optimize_parameters
(
self
,
SMA1_range
,
SMA2_range
):
''' Finds global maximum given the SMA parameter ranges.
Parameters
==========
SMA1_range, SMA2_range: tuple
tuples of the form (start, end, step size)
'''
opt
=
brute
(
self
.
update_and_run
,
(
SMA1_range
,
SMA2_range
),
finish
=
None
)
return
opt
,
-
self
.
update_and_run
(
opt
)
if
__name__
==
'__main__'
:
smabt
=
SMAVectorBacktester
(
'EUR='
,
42
,
252
,
'2010-1-1'
,
'2020-12-31'
)
(
smabt
.
run_strategy
())
smabt
.
set_parameters
(
SMA1
=
20
,
SMA2
=
100
)
(
smabt
.
run_strategy
())
(
smabt
.
optimize_parameters
((
30
,
56
,
4
),
(
200
,
300
,
4
)))
Clase de Backtesting de Momentum
A continuación se presenta un código Python con una clase para el backtesting vectorizado de estrategias basadas en el momentum de series temporales:
#
# Python Module with Class
# for Vectorized Backtesting
# of Momentum-Based Strategies
#
# Python for Algorithmic Trading
# (c) Dr. Yves J. Hilpisch
# The Python Quants GmbH
#
import
numpy
as
np
import
pandas
as
pd
class
MomVectorBacktester
(
object
):
''' Class for the vectorized backtesting of
momentum-based trading strategies.
Attributes
==========
symbol: str
RIC (financial instrument) to work with
start: str
start date for data selection
end: str
end date for data selection
amount: int, float
amount to be invested at the beginning
tc: float
proportional transaction costs (e.g., 0.5% = 0.005) per trade
Methods
=======
get_data:
retrieves and prepares the base data set
run_strategy:
runs the backtest for the momentum-based strategy
plot_results:
plots the performance of the strategy compared to the symbol
'''
def
__init__
(
self
,
symbol
,
start
,
end
,
amount
,
tc
):
self
.
symbol
=
symbol
self
.
start
=
start
self
.
end
=
end
self
.
amount
=
amount
self
.
tc
=
tc
self
.
results
=
None
self
.
get_data
()
def
get_data
(
self
):
''' Retrieves and prepares the data.
'''
raw
=
pd
.
read_csv
(
'http://hilpisch.com/pyalgo_eikon_eod_data.csv'
,
index_col
=
0
,
parse_dates
=
True
)
.
dropna
()
raw
=
pd
.
DataFrame
(
raw
[
self
.
symbol
])
raw
=
raw
.
loc
[
self
.
start
:
self
.
end
]
raw
.
rename
(
columns
=
{
self
.
symbol
:
'price'
},
inplace
=
True
)
raw
[
'return'
]
=
np
.
log
(
raw
/
raw
.
shift
(
1
))
self
.
data
=
raw
def
run_strategy
(
self
,
momentum
=
1
):
''' Backtests the trading strategy.
'''
self
.
momentum
=
momentum
data
=
self
.
data
.
copy
()
.
dropna
()
data
[
'position'
]
=
np
.
sign
(
data
[
'return'
]
.
rolling
(
momentum
)
.
mean
())
data
[
'strategy'
]
=
data
[
'position'
]
.
shift
(
1
)
*
data
[
'return'
]
# determine when a trade takes place
data
.
dropna
(
inplace
=
True
)
trades
=
data
[
'position'
]
.
diff
()
.
fillna
(
0
)
!=
0
# subtract transaction costs from return when trade takes place
data
[
'strategy'
][
trades
]
-=
self
.
tc
data
[
'creturns'
]
=
self
.
amount
*
data
[
'return'
]
.
cumsum
()
.
apply
(
np
.
exp
)
data
[
'cstrategy'
]
=
self
.
amount
*
\data
[
'strategy'
]
.
cumsum
()
.
apply
(
np
.
exp
)
self
.
results
=
data
# absolute performance of the strategy
aperf
=
self
.
results
[
'cstrategy'
]
.
iloc
[
-
1
]
# out-/underperformance of strategy
operf
=
aperf
-
self
.
results
[
'creturns'
]
.
iloc
[
-
1
]
return
round
(
aperf
,
2
),
round
(
operf
,
2
)
def
plot_results
(
self
):
''' Plots the cumulative performance of the trading strategy
compared to the symbol.
'''
if
self
.
results
is
None
:
(
'No results to plot yet. Run a strategy.'
)
title
=
'
%s
| TC =
%.4f
'
%
(
self
.
symbol
,
self
.
tc
)
self
.
results
[[
'creturns'
,
'cstrategy'
]]
.
plot
(
title
=
title
,
figsize
=
(
10
,
6
))
if
__name__
==
'__main__'
:
mombt
=
MomVectorBacktester
(
'XAU='
,
'2010-1-1'
,
'2020-12-31'
,
10000
,
0.0
)
(
mombt
.
run_strategy
())
(
mombt
.
run_strategy
(
momentum
=
2
))
mombt
=
MomVectorBacktester
(
'XAU='
,
'2010-1-1'
,
'2020-12-31'
,
10000
,
0.001
)
(
mombt
.
run_strategy
(
momentum
=
2
))
Clase de Backtesting de Reversión a la Media
A continuación se presenta un código Python con una clase para el backtesting vectorizado de estrategias basadas en la reversión a la media:.
#
# Python Module with Class
# for Vectorized Backtesting
# of Mean-Reversion Strategies
#
# Python for Algorithmic Trading
# (c) Dr. Yves J. Hilpisch
# The Python Quants GmbH
#
from
MomVectorBacktester
import
*
class
MRVectorBacktester
(
MomVectorBacktester
):
''' Class for the vectorized backtesting of
mean reversion-based trading strategies.
Attributes
==========
symbol: str
RIC symbol with which to work
start: str
start date for data retrieval
end: str
end date for data retrieval
amount: int, float
amount to be invested at the beginning
tc: float
proportional transaction costs (e.g., 0.5% = 0.005) per trade
Methods
=======
get_data:
retrieves and prepares the base data set
run_strategy:
runs the backtest for the mean reversion-based strategy
plot_results:
plots the performance of the strategy compared to the symbol
'''
def
run_strategy
(
self
,
SMA
,
threshold
):
''' Backtests the trading strategy.
'''
data
=
self
.
data
.
copy
()
.
dropna
()
data
[
'sma'
]
=
data
[
'price'
]
.
rolling
(
SMA
)
.
mean
()
data
[
'distance'
]
=
data
[
'price'
]
-
data
[
'sma'
]
data
.
dropna
(
inplace
=
True
)
# sell signals
data
[
'position'
]
=
np
.
where
(
data
[
'distance'
]
>
threshold
,
-
1
,
np
.
nan
)
# buy signals
data
[
'position'
]
=
np
.
where
(
data
[
'distance'
]
<
-
threshold
,
1
,
data
[
'position'
])
# crossing of current price and SMA (zero distance)
data
[
'position'
]
=
np
.
where
(
data
[
'distance'
]
*
data
[
'distance'
]
.
shift
(
1
)
<
0
,
0
,
data
[
'position'
])
data
[
'position'
]
=
data
[
'position'
]
.
ffill
()
.
fillna
(
0
)
data
[
'strategy'
]
=
data
[
'position'
]
.
shift
(
1
)
*
data
[
'return'
]
# determine when a trade takes place
trades
=
data
[
'position'
]
.
diff
()
.
fillna
(
0
)
!=
0
# subtract transaction costs from return when trade takes place
data
[
'strategy'
][
trades
]
-=
self
.
tc
data
[
'creturns'
]
=
self
.
amount
*
\data
[
'return'
]
.
cumsum
()
.
apply
(
np
.
exp
)
data
[
'cstrategy'
]
=
self
.
amount
*
\data
[
'strategy'
]
.
cumsum
()
.
apply
(
np
.
exp
)
self
.
results
=
data
# absolute performance of the strategy
aperf
=
self
.
results
[
'cstrategy'
]
.
iloc
[
-
1
]
# out-/underperformance of strategy
operf
=
aperf
-
self
.
results
[
'creturns'
]
.
iloc
[
-
1
]
return
round
(
aperf
,
2
),
round
(
operf
,
2
)
if
__name__
==
'__main__'
:
mrbt
=
MRVectorBacktester
(
'GDX'
,
'2010-1-1'
,
'2020-12-31'
,
10000
,
0.0
)
(
mrbt
.
run_strategy
(
SMA
=
25
,
threshold
=
5
))
mrbt
=
MRVectorBacktester
(
'GDX'
,
'2010-1-1'
,
'2020-12-31'
,
10000
,
0.001
)
(
mrbt
.
run_strategy
(
SMA
=
25
,
threshold
=
5
))
mrbt
=
MRVectorBacktester
(
'GLD'
,
'2010-1-1'
,
'2020-12-31'
,
10000
,
0.001
)
(
mrbt
.
run_strategy
(
SMA
=
42
,
threshold
=
7.5
))
Get Python para el trading algorítmico now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.