Zipline – Introduccion

Zipline es una librería de backtesting por eventos, desarolláda por el fondo de inversion Quantopian. Desde el cierre de la plataforma en el 2020, todos las librerías han pasado a ser código libre, y mantenidas por la comunidad.(Alphalens,Pyfolio,Empyrical,etc..) 

Las caracteristicas que hacen de zipline la mejor librería de backtesting son, su facilidad de uso, donde únicamente te vas a tener que focalizar en el desarrollo del modelo, y no en como crear un backtester.

Esto no se trata de re-inventar la rueda, se trata de llegar exactamente donde necesitamos. Ademas de traer integración con el ecosistema de python de forma nativa, con pandas, numpy, scikit-learn etc… Las posibilidades se vuelven infinitas. 

En este lab, asumimos que el usuario tiene una instalación con Zipline y el bundle de stocks de Quandl. Si no tiene nada instalado, en google  encontraras tutoriales de todo tipo. En este lab no entraremos en detalles técnicos sobre la instalación y configuración de la plataforma. 

zipline logo

Programando nuestro primer sistema en Zipline

Como viene siendo habitual, la primera celda se utiliza para cargar las librerias necesarias. En este lab hemos simplificado al minimo el uso de librerias, para dejar mas claro los contenidos.

import pandas as pd
import numpy as np
from datetime import datetime

import matplotlib.pyplot as plt
import seaborn as sns 

import pytz

from zipline.api import order_target_percent, symbol, record
from zipline import run_algorithm

sns.set_style('whitegrid')

Todo algoritmo de zipline se compone obligatoriamente de dos funciones que el usuario debe definir. 

  • Initialize(context) 
  • handle_data(context,data) 

Antes de lanzar el algoritmo , zipline llama a la funcion initialize() y le pasa la variable context, un espacio persistente que almacena las variables que accederá posteriormente en alguna iteración del backtester, que recordamos que funciona por eventos, es decir, vela a vela. 

La funcion initialize()

def initialize(context):
    context.stock = symbol("LOW")
    context.i = 0

La variable stock, almacena el ticker sobre el cual vamos a trabajar, en este caso es $LOW, y la variable i es unicamente para realizar conteos de vela, para tener un periodo lo suficientemente ancho para poder calcular las medias.

Una vez cargadas las variables via initialize() , zipline llama a la funcion handle_data() una vez por cada evento(vela). En cada llamada pasa la misma variables context y el precios en formato (OHLCV) y algunos metadatos 

La funcion handle_data()

def handle_data(context, data):
    context.i += 1
    if context.i < 50:
        return
    
    current = data.current(context.stock, 'open')
    avg = data.history(context.stock, 'open', bar_count=50, frequency="1d").mean()

    
    if current > avg:
        stock_weight = 1
    else:
        stock_weight = 0
    
    order_target_percent(context.stock, stock_weight)
    
    record(asset_price=data.current(context.stock, 'price'),
           asset_avg=avg)

Mediante el primer bloque de codigo dentro de handle_data()

 context.i += 1
 if context.i < 50:
    return

Lo que realiza el codigo es esperar mas de 50 velas para empezar. Esto se hace, con el factor que mas datos necesite para calcularlo. En este caso es una media de 50 periodos, pues entonces hasta que no tengamos 51, no empezaremos a backtestear nada, ya que no habria suficiente informacion como para calcular la media del precio.

    current = data.current(context.stock, 'open')
    avg = data.history(context.stock, 'open', bar_count=50, frequency="1d").mean()

Aqui calculamos el precio del open de la vela actual y el precio medio de las ultimas 50 velas. mediante data.current obtenemos el evento en curso. y mediante data.history obtenemos el historico de las ultimas n velas.

    if current > avg:
        stock_weight = 1
    else:
        stock_weight = 0

en este simple condicional, evaluamos si el open de la vela en curso esta por encima o por debajo de la media de 50 periodos, y si es True ,devuelve un peso de 1, que seria un 100%, y si la condición es negativa nos devuelve un peso de 0, es decir, estaría fuera de mercado.

Es una estrategia extremadamente sencilla, hecha a modo de prueba de concepto, una vez entendida la forma de trabajar de zipline, podrá crear todo tipo de estrategias, sin excepción, tal como en otras plataformas, donde en algún punto encuentras los limites.

una vez que tenemos claro sobre que decision tomara el modelo, vamos a realizar el backtest, tenemos claro el peso que tendrá la posición mediante stock_weight, lo que vamos a hacer es lanzar la orden del día.

    order_target_percent(context.stock, stock_weight)

Ademas, añadiremos al dataframe resultante que nos devolvera zipline, la informacion necesaria para una posterior evaluacion

    record(asset_price=data.current(context.stock, 'price'),
           asset_avg=avg)

Grabaremos el precio del activo, y la media del activo. Posteriormente se deberia utilizar esa informacion para plotear un buy and hold, y la media por ejemplo.

La funcion run_algorithm()

Para ejecutar el algoritmo, se utiliza la funcion run_algorithm() que consta de los siguientes parametros:

  • start = Fecha de inicio del backtest, con tzinfo, que para ello utilizamos pytz
  • end = Fecha de fin del backtest, con tzinfo.
  • initialize = utilizamos la funcion initialize definida anteriormente
  • handle_data = utiliza la funcion handle_data, definida anteriormente
  • capital_base = capital inicial para empezar el backtest
  • data_frequency = timeframe de las velas
  • bundle = conjunto de datos que utilizamos
  • bechmark_returns = retornos del bechmark para comparar la estrategia.
  • analyze = opcional, se puede vincular con un modulo de analisis, pero en este lab, se realizara a posterior.

Utilizamos el magic(%%) time, para controlar el tiempo que utiliza el sistema en realizar un backtest, al ser un backtest por eventos, no son son rapidos.

Posteriormente asignamos a la variable results el output de la funcion run_alogirthm, para poder trabajar con posterioridad con la informacion resultante del backtest.

%%time
start_date = pd.Timestamp(datetime(2012, 1, 1, tzinfo=pytz.UTC))
end_date = pd.Timestamp(datetime(2018, 12, 31, tzinfo=pytz.UTC))

results = run_algorithm(
    start=start_date,
    end=end_date,
    initialize=initialize,
    handle_data=handle_data,
    capital_base=10000,
    data_frequency='daily',
    bundle='quandl',
    benchmark_returns=None
)

Resultados

Dedicaremos el proximo lab al analisis de resultados, pero para ver que tal ha ido esta estrategia, dejaremos un codigo que nos dibujara algunos parametros y dejaremos el melon abierto para la proxima entrega.

# Function for annualized return
def ann_ret(ts):
    return np.power((ts[-1] / ts[0]), (252 / len(ts))) - 1


# Function for drawdown
def dd(ts):
    return np.min(ts / np.maximum.accumulate(ts)) - 1
results = results['2012':]
df = results.copy().filter(items=["portfolio_value", "gross_leverage"])
rolling_window = results.portfolio_value.rolling(252)

df["annualized"] = rolling_window.apply(ann_ret)
df["drawdown"] = rolling_window.apply(dd)
df.dropna(inplace=True)
df["portfolio_value"] /= 100

fig, axes = plt.subplots(nrows=6, figsize=(15,25))
results.algorithm_period_return.plot(ax=axes[0], title='Retorno acumulado en %')
results.algo_volatility.plot(ax=axes[1], title='Volatilidad en %')
results.sortino.plot(ax=axes[2], title='Sharpe y Sortino')
results.sharpe.plot(ax=axes[2])
results.gross_leverage.plot(ax=axes[3],title='Leverage')
df.drawdown.plot(ax=axes[4],title='DD')
df.annualized.plot(ax=axes[5],title='ret ann')
sns.despine()
fig.tight_layout();
report

En proximas entradas trabajaremos con

Si te has quedado con ganas de deborar mas informacion, te recomiendo que te pases por este articulo tambien

DISCLAIMER: Tras un periodo de inactividad a causa de unas estrictas condiciones contractuales con empresas del sector, que me impedian la publciacion de opiniones o cualquier otro contenido de ambito financiero, se volvera a publicar articulos pues han expirado todos los acuerdos.

En este blog UNICAMENTE se hablan de conocimientos matematicos, aplicados a series temporales u otros datos mediante el uso de tecnologias tales como python. NINGUNA de las conclusiones que obtenga de este blog son RECOMENDACIONES de inversion. Retornos pasados no garantizan retornos futuros, ademas de avisarle que en toda operacion financiera, de una manera u otra su capital esta en riesgo. Ademas el autor de este articulo, puede o no tener posiciones largas o cortas en los activos mencionados.

Todo el contenido esta sujeto a derechos de autor (C) Quantarmy 2021