Pruebas de retroceso basadas en eventos con Python - Parte V

El autor:La bondad, Creado: 2019-03-25 15:54:16, Actualizado:

En el artículo anterior sobre backtesting basado en eventos, consideramos cómo construir una jerarquía de clases de estrategia. Las estrategias, tal como se definen aquí, se utilizan para generar señales, que son utilizadas por un objeto de cartera para tomar decisiones sobre si enviar órdenes.

Este artículo describe un objeto NaivePortfolio que realiza un seguimiento de las posiciones dentro de una cartera y genera órdenes de una cantidad fija de acciones basadas en señales.

Seguimiento de posiciones y gestión de pedidos

El sistema de gestión de pedidos de cartera es posiblemente el componente más complejo de un backtester basado en eventos. Su función es realizar un seguimiento de todas las posiciones actuales de mercado, así como el valor de mercado de las posiciones (conocido como holdings). Esto es simplemente una estimación del valor de liquidación de la posición y se deriva en parte de la instalación de manejo de datos del backtester.

Además de la gestión de posiciones y participaciones, la cartera también debe tener en cuenta los factores de riesgo y las técnicas de dimensionamiento de las posiciones para optimizar las órdenes que se envían a un intermediario u otra forma de acceso al mercado.

Continuando en la línea de la jerarquía de clases Event, un objeto Portfolio debe ser capaz de manejar objetos SignalEvent, generar objetos OrderEvent e interpretar objetos FillEvent para actualizar posiciones.

Aplicación

Creamos un nuevo archivoportfolio.pyy importar las bibliotecas necesarias. Estas son las mismas que la mayoría de las otras implementaciones de clases base abstractas. Necesitamos importar la función de piso de la biblioteca matemática para generar tamaños de orden de valor entero. También necesitamos los objetos FillEvent y OrderEvent ya que el Portfolio maneja ambos.

# portfolio.py

import datetime
import numpy as np
import pandas as pd
import Queue

desde abc importar ABCMeta, método abstracto desde el piso de importación de matemáticas

desde la importación de eventos FillEvent, OrderEvent Como antes, creamos un ABC para Portfolio y tenemos dos métodos virtuales puros update_signal y update_fill. El primero maneja las nuevas señales comerciales que se capturan de la cola de eventos y el segundo maneja los rellenos recibidos de un objeto de manejo de ejecución.

# portfolio.py

class Portfolio(object):
    """
    The Portfolio class handles the positions and market
    value of all instruments at a resolution of a "bar",
    i.e. secondly, minutely, 5-min, 30-min, 60 min or EOD.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        raise NotImplementedError("Should implement update_signal()")

    @abstractmethod
    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        raise NotImplementedError("Should implement update_fill()")

El tema principal de este artículo es la clase NaivePortfolio. Está diseñada para manejar el tamaño de las posiciones y las tenencias actuales, pero llevará a cabo órdenes de negociación de una manera dumb simplemente enviándolas directamente a la correduría con un tamaño de cantidad fijo predeterminado, independientemente del efectivo mantenido.

El NaivePortfolio requiere un valor de capital inicial, que he establecido como valor por defecto de 100.000 USD. También requiere una fecha de inicio.

La cartera contiene los miembros de all_positions y current_positions. El primero almacena una lista de todas las posiciones anteriores registradas en el momento de un evento de datos de mercado. Una posición es simplemente la cantidad del activo. Las posiciones negativas significan que el activo ha sido corto. El último miembro almacena un diccionario que contiene las posiciones actuales para la última actualización de la barra de mercado.

Además de los miembros de las posiciones, la cartera almacena las tenencias, que describen el valor de mercado actual de las posiciones mantenidas. Valor de mercado actual en este caso significa el precio de cierre obtenido a partir de la barra de mercado actual, que es claramente una aproximación, pero es lo suficientemente razonable por el momento. all_holdings almacena la lista histórica de todas las tenencias de símbolos, mientras que current_holdings almacena el diccionario más actualizado de todos los valores de las tenencias de símbolos.

# portfolio.py

class NaivePortfolio(Portfolio):
    """
    The NaivePortfolio object is designed to send orders to
    a brokerage object with a constant quantity size blindly,
    i.e. without any risk management or position sizing. It is
    used to test simpler strategies such as BuyAndHoldStrategy.
    """
    
    def __init__(self, bars, events, start_date, initial_capital=100000.0):
        """
        Initialises the portfolio with bars and an event queue. 
        Also includes a starting datetime index and initial capital 
        (USD unless otherwise stated).

        Parameters:
        bars - The DataHandler object with current market data.
        events - The Event Queue object.
        start_date - The start date (bar) of the portfolio.
        initial_capital - The starting capital in USD.
        """
        self.bars = bars
        self.events = events
        self.symbol_list = self.bars.symbol_list
        self.start_date = start_date
        self.initial_capital = initial_capital
        
        self.all_positions = self.construct_all_positions()
        self.current_positions = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )

        self.all_holdings = self.construct_all_holdings()
        self.current_holdings = self.construct_current_holdings()

El siguiente método, construct_all_positions, simplemente crea un diccionario para cada símbolo, establece el valor a cero para cada uno y luego agrega una clave de fecha y hora, finalmente agregándola a una lista.

# portfolio.py

    def construct_all_positions(self):
        """
        Constructs the positions list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        return [d]

El método construct_all_holdings es similar al anterior, pero agrega claves adicionales para efectivo, comisión y total, que representan, respectivamente, el efectivo sobrante en la cuenta después de cualquier compra, la comisión acumulada y el capital total de la cuenta, incluido el efectivo y cualquier posición abierta. Las posiciones cortas se tratan como negativas.

# portfolio.py

    def construct_all_holdings(self):
        """
        Constructs the holdings list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return [d]

El siguiente método, construct_current_holdings es casi idéntico al método anterior, excepto que no envuelve el diccionario en una lista:

# portfolio.py

    def construct_current_holdings(self):
        """
        This constructs the dictionary which will hold the instantaneous
        value of the portfolio across all symbols.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return d

En un escenario de negociación en vivo, esta información se puede descargar y analizar directamente desde el corredor, pero para una implementación de backtesting es necesario calcular estos valores manualmente.

Por desgracia, no existe tal cosa como el valor de mercado actual debido a los diferenciales de oferta / demanda y los problemas de liquidez. Por lo tanto, es necesario estimarlo multiplicando la cantidad del activo mantenido por un precio. El enfoque que he adoptado aquí es utilizar el precio de cierre de la última barra recibida. Para una estrategia intradiaria esto es relativamente realista. Para una estrategia diaria esto es menos realista ya que el precio de apertura puede diferir sustancialmente del precio de cierre.

El método update_timeindex maneja el seguimiento de las nuevas posiciones. Primero obtiene los precios más recientes del procesador de datos de mercado y crea un nuevo diccionario de símbolos para representar las posiciones actuales, estableciendo las posiciones new iguales a las posiciones current. Éstas solo se cambian cuando se obtiene un FillEvent, que se maneja más adelante en la cartera. El método luego agrega este conjunto de posiciones actuales a la lista de todas las posiciones. A continuación, las posiciones se actualizan de manera similar, con la excepción de que el valor de mercado se recalcula multiplicando las posiciones actuales con el precio de cierre de la última barra (self.current_positions[s] * barras[s][0][5]). Finalmente, las nuevas posiciones se agregan a todas las posiciones:

# portfolio.py

    def update_timeindex(self, event):
        """
        Adds a new record to the positions matrix for the current 
        market data bar. This reflects the PREVIOUS bar, i.e. all
        current market data at this stage is known (OLHCVI).

        Makes use of a MarketEvent from the events queue.
        """
        bars = {}
        for sym in self.symbol_list:
            bars[sym] = self.bars.get_latest_bars(sym, N=1)

        # Update positions
        dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dp['datetime'] = bars[self.symbol_list[0]][0][1]

        for s in self.symbol_list:
            dp[s] = self.current_positions[s]

        # Append the current positions
        self.all_positions.append(dp)

        # Update holdings
        dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dh['datetime'] = bars[self.symbol_list[0]][0][1]
        dh['cash'] = self.current_holdings['cash']
        dh['commission'] = self.current_holdings['commission']
        dh['total'] = self.current_holdings['cash']

        for s in self.symbol_list:
            # Approximation to the real value
            market_value = self.current_positions[s] * bars[s][0][5]
            dh[s] = market_value
            dh['total'] += market_value

        # Append the current holdings
        self.all_holdings.append(dh)

El método update_positions_from_fill determina si un FillEvent es una compra o una venta y luego actualiza el diccionario current_positions en consecuencia agregando/ restando la cantidad correcta de acciones:

# portfolio.py

    def update_positions_from_fill(self, fill):
        """
        Takes a FilltEvent object and updates the position matrix
        to reflect the new position.

        Parameters:
        fill - The FillEvent object to update the positions with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update positions list with new quantities
        self.current_positions[fill.symbol] += fill_dir*fill.quantity

El correspondiente update_holdings_from_fill es similar al método anterior, pero actualiza los valores de las tenencias en su lugar. Para simular el costo de un llenado, el siguiente método no utiliza el costo asociado del FillEvent. ¿Por qué es esto? En pocas palabras, en un entorno de backtesting el costo de llenado es realmente desconocido y, por lo tanto, debe estimarse. Por lo tanto, el costo de llenado se establece en el precio de mercado actual (el precio de cierre de la última barra). Las tenencias para un símbolo en particular se establecen entonces igual al costo de llenado multiplicado por la cantidad transaccionada.

Una vez que se conoce el costo de llenado, se pueden actualizar las tenencias actuales, el efectivo y los valores totales.

# portfolio.py

    def update_holdings_from_fill(self, fill):
        """
        Takes a FillEvent object and updates the holdings matrix
        to reflect the holdings value.

        Parameters:
        fill - The FillEvent object to update the holdings with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update holdings list with new quantities
        fill_cost = self.bars.get_latest_bars(fill.symbol)[0][5]  # Close price
        cost = fill_dir * fill_cost * fill.quantity
        self.current_holdings[fill.symbol] += cost
        self.current_holdings['commission'] += fill.commission
        self.current_holdings['cash'] -= (cost + fill.commission)
        self.current_holdings['total'] -= (cost + fill.commission)

El método virtual puro update_fill del Portfolio ABC se implementa aquí. Simplemente ejecuta los dos métodos anteriores, update_positions_from_fill y update_holdings_from_fill, que ya se han discutido anteriormente:

# portfolio.py

    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        if event.type == 'FILL':
            self.update_positions_from_fill(event)
            self.update_holdings_from_fill(event)

Si bien el objeto de cartera debe manejar FillEvents, también debe cuidar de generar OrderEvents al recibir uno o más SignalEvents. El método generate_naive_order simplemente toma una señal para hacer long o short de un activo y luego envía una orden para hacerlo para 100 acciones de dicho activo. Claramente 100 es un valor arbitrario. En una implementación realista, este valor será determinado por una superposición de gestión de riesgos o tamaño de posición. Sin embargo, este es un NaivePortfolio y por lo tanto naively envía todas las órdenes directamente desde las señales, sin un sistema de riesgo.

El método maneja el deseo, el cortocircuito y la salida de una posición, basado en la cantidad actual y el símbolo particular.

# portfolio.py

    def generate_naive_order(self, signal):
        """
        Simply transacts an OrderEvent object as a constant quantity
        sizing of the signal object, without risk management or
        position sizing considerations.

        Parameters:
        signal - The SignalEvent signal information.
        """
        order = None

        symbol = signal.symbol
        direction = signal.signal_type
        strength = signal.strength

        mkt_quantity = floor(100 * strength)
        cur_quantity = self.current_positions[symbol]
        order_type = 'MKT'

        if direction == 'LONG' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
        if direction == 'SHORT' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')   
    
        if direction == 'EXIT' and cur_quantity > 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
        if direction == 'EXIT' and cur_quantity < 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
        return order

El método update_signal simplemente llama el método anterior y agrega el orden generado a la cola de eventos:

# portfolio.py

    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        if event.type == 'SIGNAL':
            order_event = self.generate_naive_order(event)
            self.events.put(order_event)

El método final en el NaivePortfolio es la generación de una curva de renta variable. Esto simplemente crea un flujo de rendimientos, útil para los cálculos de rendimiento y luego normaliza la curva de renta variable para que se base en porcentaje. Por lo tanto, el tamaño inicial de la cuenta es igual a 1.0:

# portfolio.py

    def create_equity_curve_dataframe(self):
        """
        Creates a pandas DataFrame from the all_holdings
        list of dictionaries.
        """
        curve = pd.DataFrame(self.all_holdings)
        curve.set_index('datetime', inplace=True)
        curve['returns'] = curve['total'].pct_change()
        curve['equity_curve'] = (1.0+curve['returns']).cumprod()
        self.equity_curve = curve

El objeto de cartera es el aspecto más complejo de todo el sistema de backtest impulsado por eventos. La implementación aquí, aunque intrincada, es relativamente elemental en su manejo de posiciones.

En el siguiente artículo consideraremos la última pieza del backtester impulsado por eventos, a saber, un objeto ExecutionHandler, que se utiliza para tomar objetos OrderEvent y crear objetos FillEvent a partir de ellos.


Más.