Tests arrière basés sur des événements avec Python - Partie VIII

Auteur:La bonté, Créé: 2019-03-26 16:38:59, mis à jour:

Cela fait un certain temps que nous n'avons pas considéré le backtester basé sur des événements, dont nous avons commencé à discuter dans cet article. Dans la partie VI, j'ai décrit comment coder un modèle de gestion d'exécution stand-in qui fonctionnait pour une situation de backtesting historique.

J'ai déjà discuté de la façon de télécharger Trader Workstation et de créer un compte de démonstration Interactive Brokers ainsi que de la façon de créer une interface de base à l'API IB en utilisant IbPy.

L'idée essentielle de la classe IBExecutionHandler (voir ci-dessous) est de recevoir des instances OrderEvent de la file d'attente d'événements, puis de les exécuter directement contre l'API d'ordre Interactive Brokers à l'aide de la bibliothèque IbPy. La classe gérera également les messages Server Response renvoyés via l'API. À ce stade, la seule action prise sera de créer des instances FillEvent correspondantes qui seront ensuite renvoyées à la file d'attente d'événements.

La classe elle-même pourrait devenir assez complexe, avec une logique d'optimisation de l'exécution ainsi qu'un traitement d'erreur sophistiqué.

Mise en œuvre de Python

Comme toujours, la première tâche consiste à créer le fichier Python et à importer les bibliothèques nécessaires.

Nous importons les bibliothèques de traitement de date/heure nécessaires, les objets IbPy et les objets Event spécifiques qui sont gérés par IBExecutionHandler:

# ib_execution.py

import datetime
import time

from ib.ext.Contract import Contract
from ib.ext.Order import Order
from ib.opt import ibConnection, message

from event import FillEvent, OrderEvent
from execution import ExecutionHandler

Nous définissons maintenant la classe IBExecutionHandler.initle constructeur nécessite d'abord la connaissance de la file d'attente des événements. Il nécessite également la spécification de order_routing, que j'ai défini par défaut à SMART. Si vous avez des exigences d'échange spécifiques, vous pouvez les spécifier ici. La devise par défaut a également été définie sur Dollars américains.

Dans la méthode, nous créons un dictionnaire fill_dict, nécessaire plus tard pour une utilisation dans la génération d'instances FillEvent. Nous créons également un objet de connexion tws_conn pour stocker nos informations de connexion à l'API Interactive Brokers. Nous devons également créer un ordre par défaut initial order_id, qui suit toutes les commandes suivantes pour éviter les doublons. Enfin, nous enregistrons les gestionnaires de messages (que nous définirons plus en détail ci-dessous):

# ib_execution.py

class IBExecutionHandler(ExecutionHandler):
    """
    Handles order execution via the Interactive Brokers
    API, for use against accounts when trading live
    directly.
    """

    def __init__(self, events, 
                 order_routing="SMART", 
                 currency="USD"):
        """
        Initialises the IBExecutionHandler instance.
        """
        self.events = events
        self.order_routing = order_routing
        self.currency = currency
        self.fill_dict = {}

        self.tws_conn = self.create_tws_connection()
        self.order_id = self.create_initial_order_id()
        self.register_handlers()

L'API IB utilise un système d'événements basé sur des messages qui permet à notre classe de répondre de manière particulière à certains messages, de manière similaire au backtester basé sur des événements lui-même.

La méthode _reply_handler, d'autre part, est utilisée pour déterminer si une instance FillEvent doit être créée. La méthode demande si un message openOrder a été reçu et vérifie si une entrée dans notre fill_dict pour cet orderId particulier a déjà été définie. Si ce n'est pas le cas, alors une est créée.

S'il voit un message orderStatus et que ce message spécifique indique qu'une commande a été remplie, alors il appelle create_fill pour créer un FillEvent.

# ib_execution.py
    
    def _error_handler(self, msg):
        """
        Handles the capturing of error messages
        """
        # Currently no error handling.
        print "Server Error: %s" % msg

    def _reply_handler(self, msg):
        """
        Handles of server replies
        """
        # Handle open order orderId processing
        if msg.typeName == "openOrder" and \
            msg.orderId == self.order_id and \
            not self.fill_dict.has_key(msg.orderId):
            self.create_fill_dict_entry(msg)
        # Handle Fills
        if msg.typeName == "orderStatus" and \
            msg.status == "Filled" and \
            self.fill_dict[msg.orderId]["filled"] == False:
            self.create_fill(msg)      
        print "Server Response: %s, %s\n" % (msg.typeName, msg)

La méthode suivante, create_tws_connection, crée une connexion à l'API IB en utilisant l'objet IbPy ibConnection. Elle utilise un port par défaut de 7496 et un clientId par défaut de 10. Une fois l'objet créé, la méthode connect est appelée pour effectuer la connexion:

# ib_execution.py
    
    def create_tws_connection(self):
        """
        Connect to the Trader Workstation (TWS) running on the
        usual port of 7496, with a clientId of 10.
        The clientId is chosen by us and we will need 
        separate IDs for both the execution connection and
        market data connection, if the latter is used elsewhere.
        """
        tws_conn = ibConnection()
        tws_conn.connect()
        return tws_conn

Pour garder une trace des commandes séparées (aux fins du suivi des remplissages), la méthode suivante create_initial_order_id est utilisée. Je l'ai définie par défaut à 1, mais une approche plus sophistiquée serait de demander IB pour le dernier identifiant disponible et de l'utiliser. Vous pouvez toujours réinitialiser l'identifiant de commande actuel de l'API via le Trader Workstation > Configuration globale > Panneau paramètres API:

# ib_execution.py
    
    def create_initial_order_id(self):
        """
        Creates the initial order ID used for Interactive
        Brokers to keep track of submitted orders.
        """
        # There is scope for more logic here, but we
        # will use "1" as the default for now.
        return 1

La méthode suivante, register_handlers, enregistre simplement les méthodes de traitement des erreurs et des réponses définies ci-dessus avec la connexion TWS:

# ib_execution.py
    
    def register_handlers(self):
        """
        Register the error and server reply 
        message handling functions.
        """
        # Assign the error handling function defined above
        # to the TWS connection
        self.tws_conn.register(self._error_handler, 'Error')

        # Assign all of the server reply messages to the
        # reply_handler function defined above
        self.tws_conn.registerAll(self._reply_handler)

Comme dans le tutoriel précédent sur l'utilisation d'IbPy, nous devons créer une instance de contrat, puis l'associer à une instance d'ordre, qui sera envoyée à l'API IB. La méthode suivante, create_contract, génère le premier composant de cette paire. Elle s'attend à un symbole de ticker, un type de sécurité (par exemple, stock ou futur), un échange / échange primaire et une devise. Elle renvoie l'instance de contrat:

# ib_execution.py
    
    def create_contract(self, symbol, sec_type, exch, prim_exch, curr):
        """
        Create a Contract object defining what will
        be purchased, at which exchange and in which currency.

        symbol - The ticker symbol for the contract
        sec_type - The security type for the contract ('STK' is 'stock')
        exch - The exchange to carry out the contract on
        prim_exch - The primary exchange to carry out the contract on
        curr - The currency in which to purchase the contract
        """
        contract = Contract()
        contract.m_symbol = symbol
        contract.m_secType = sec_type
        contract.m_exchange = exch
        contract.m_primaryExch = prim_exch
        contract.m_currency = curr
        return contract

La méthode suivante, create_order, génère la deuxième composante de la paire, à savoir l'instance d'ordre. Elle s'attend à un type d'ordre (par exemple marché ou limite), une quantité de l'actif à négocier et une action (acheter ou vendre). Elle renvoie l'instance d'ordre:

# ib_execution.py
    
    def create_order(self, order_type, quantity, action):
        """
        Create an Order object (Market/Limit) to go long/short.

        order_type - 'MKT', 'LMT' for Market or Limit orders
        quantity - Integral number of assets to order
        action - 'BUY' or 'SELL'
        """
        order = Order()
        order.m_orderType = order_type
        order.m_totalQuantity = quantity
        order.m_action = action
        return order

Pour éviter de dupliquer les instances de FillEvent pour un ID d'ordre particulier, nous utilisons un dictionnaire appelé fill_dict pour stocker les clés qui correspondent à des ID d'ordre particuliers. Lorsqu'un remplissage a été généré, la clé filled d'une entrée pour un ID d'ordre particulier est définie sur True. Si un message ultérieur Server Response est reçu d'IB indiquant qu'une commande a été remplie (et est un message en double), il ne conduira pas à un nouveau remplissage. La méthode suivante create_fill_dict_entry effectue ceci:

# ib_execution.py
    
    def create_fill_dict_entry(self, msg):
        """
        Creates an entry in the Fill Dictionary that lists 
        orderIds and provides security information. This is
        needed for the event-driven behaviour of the IB
        server message behaviour.
        """
        self.fill_dict[msg.orderId] = {
            "symbol": msg.contract.m_symbol,
            "exchange": msg.contract.m_exchange,
            "direction": msg.order.m_action,
            "filled": False
        }

La méthode suivante, create_fill, crée en fait l'instance FillEvent et la place sur la file d'attente des événements:

# ib_execution.py
    
    def create_fill(self, msg):
        """
        Handles the creation of the FillEvent that will be
        placed onto the events queue subsequent to an order
        being filled.
        """
        fd = self.fill_dict[msg.orderId]

        # Prepare the fill data
        symbol = fd["symbol"]
        exchange = fd["exchange"]
        filled = msg.filled
        direction = fd["direction"]
        fill_cost = msg.avgFillPrice

        # Create a fill event object
        fill = FillEvent(
            datetime.datetime.utcnow(), symbol, 
            exchange, filled, direction, fill_cost
        )

        # Make sure that multiple messages don't create
        # additional fills.
        self.fill_dict[msg.orderId]["filled"] = True

        # Place the fill event onto the event queue
        self.events.put(fill_event)

Maintenant que toutes les méthodes précédentes ont été implémentées, il reste à remplacer la méthode execute_order de la classe de base abstraite ExecutionHandler.

Nous vérifions d'abord que l'événement reçu par cette méthode est en fait un OrderEvent, puis préparons les objets Contract et Order avec leurs paramètres respectifs.

Il est extrêmement important d'appeler la méthode time.sleep ((1) pour s'assurer que l'ordre passe réellement à l'IB. La suppression de cette ligne entraîne un comportement incohérent de l'API, du moins sur mon système!

Enfin, nous augmentons l'identifiant de commande pour nous assurer que nous ne dupliquons pas les commandes:

# ib_execution.py
    
    def execute_order(self, event):
        """
        Creates the necessary InteractiveBrokers order object
        and submits it to IB via their API.

        The results are then queried in order to generate a
        corresponding Fill object, which is placed back on
        the event queue.

        Parameters:
        event - Contains an Event object with order information.
        """
        if event.type == 'ORDER':
            # Prepare the parameters for the asset order
            asset = event.symbol
            asset_type = "STK"
            order_type = event.order_type
            quantity = event.quantity
            direction = event.direction

            # Create the Interactive Brokers contract via the 
            # passed Order event
            ib_contract = self.create_contract(
                asset, asset_type, self.order_routing,
                self.order_routing, self.currency
            )

            # Create the Interactive Brokers order via the 
            # passed Order event
            ib_order = self.create_order(
                order_type, quantity, direction
            )

            # Use the connection to the send the order to IB
            self.tws_conn.placeOrder(
                self.order_id, ib_contract, ib_order
            )

            # NOTE: This following line is crucial.
            # It ensures the order goes through!
            time.sleep(1)

            # Increment the order ID for this session
            self.order_id += 1

Cette classe constitue la base d'un gestionnaire d'exécution Interactive Brokers et peut être utilisée à la place du gestionnaire d'exécution simulé, qui ne convient qu'au backtesting.

De cette façon, nous réutilisons autant que possible les systèmes backtest et live pour nous assurer que le code swap out est minimisé et que le comportement des deux systèmes est similaire, voire identique.


Plus de