Motore di Backtesting con Python – Parte IX (Connessione con IB)

È passato un po ‘di tempo da quando abbiamo la serie di articoli realtivi ad un ambiente di backtesting basato sugli eventi, che abbiamo iniziato a discutere in questo articolo. Nella Parte VI è stato descritto come implementare un modello di ExecutionHandler funzionante per una simulazione storica di backtesting. In questo si vuole implementare il gestore dell’API di Interactive Brokers in modo da poter utilizzare l’ExecutionHandler per il live trading.

In precedenza abbiamo visto come come scaricare Trader Workstation e creare un account demo di Interactive Brokers e su come creare un’interfaccia di base verso l’API IB usando IbPy. Questo articolo descrivere come collegare l’interfaccia IbPy all’interno di un sistema event-driven, in modo tale che, quando accoppiato con un feed di dati di mercato real-time, costituirà la base per un sistema di esecuzione automatizzato.

Connessione con Interactive Brokers

L’idea alla base della classe IBExecutionHandler consiste nel ricevere istanze OrderEvent dalla coda degli eventi ed eseguirli direttamente verso l’API di ordine di Interactive Brokers utilizzando la libreria IbPy. La classe gestirà anche i messaggi “Server Response” inviati in rispota dalla stessa API. In questa fase, l’unica azione intrapresa sarà creare le corrispondenti istanze FillEvent corrispondenti che verranno quindi ritrasferite nella coda degli eventi.

La stessa classe può essere facilmente resa più complessa, includendo una logica di ottimizzazione dell’esecuzione e una sofisticata gestione degli errori. Tuttavia, in questa fase è opportuno mantenerla relativamente semplice in modo che si possa capire le principali funzionalità ed estenderle nella direzione che si adatta al tuo particolare stile di trading.

Implementazione

Come sempre, il primo passo è creare il file Python e importare le librerie necessarie. Il file si chiama ib_execution.py e risiede nella stessa directory degli altri file event-driven. Importiamo le necessarie librerie per la gestione della data / ora, gli oggetti IbPy e i specifici oggetti Event gestiti da 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
        

A questo punto è necessario definire la classe IBExecutionHandler. Innanzitutto il costruttore __init__ richiede in input la coda degli eventi. Prevende inoltre la specifica di order_routing, che viene impostata a “SMART” come default. Nel caso l’exchange abbia specifici requisiti, questi possono essere specificati in questo costruttore. Inoltre la currency predefinita è stata impostata sui Dollari USA.

All’interno del metodo si crea un dizionario fill_dict, necessario per l’utilizzo nella generazione delle istanze di FillEvent. Si prevede anche un oggetto di connessione tws_conn per archiviare le informazioni di connessione verso l’API di Interactive Brokers. Inoltre si crea un order_id iniziale, che tiene traccia di tutti gli ordini successivi per evitare duplicati. Infine si registra il gestore dei messaggi (che sarà definito dettagliatamente più avanti):

            # ib_execution.py

class IBExecutionHandler(ExecutionHandler):
    """
    Gestisce l'esecuzione degli ordini tramite l'API di Interactive 
    Brokers, da utilizzare direttamente sui conti reali durante il 
    live trading.
    """

    def __init__(self, events,
                 order_routing="SMART",
                 currency="USD"):
        """
        Inizializza l'instanza di IBExecutionHandler.
        """
        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 di IB utilizza un sistema di eventi basato sui messaggi che consente alla nostra classe di rispondere in modo specifico a determinati messaggi, in analogia allo stesso ambiente di backtesing event-driven stesso. Non si include nessuna gestione degli errori reali (a fini di brevità), ad eccezione dell’output al terminale tramite il metodo _error_handler.

Il metodo _reply_handler, d’altra parte, viene utilizzato per determinare se è necessario creare un’istanza FillEvent. Il metodo verifica se è stato ricevuto un messaggio “openOrder” e controlla se è presente una voce fill_dict relativa a questo particolare orderId. In caso contrario, ne viene creata una.

Inoltre, se il metodo verifica la presenta di un messaggio “orderStatus” e nel caso quel particolare messaggio indichi che un ordine è stato eseguito, allora richiama la funzione create_fill per creare un FillEvent. Si invia anche un messaggio al terminale per scopi di logging / debug:

            # ib_execution.py 
   
    def _error_handler(self, msg):
        """
        Gestore per la cattura dei messagi di errori
        """
        # Al momento non c'è gestione degli errori.
        print
        "Server Error: %s" % msg


    def _reply_handler(self, msg):
        """
        Gestione delle risposte dal server
        """
        # Gestisce il processo degli orderId degli ordini aperti
        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)
        # Gestione dell'esecuzione degli ordini (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)
        
Il seguente metodo, create_tws_connection, crea una connessione all’API di IB usando l’oggetto ibConnection di IbPy. Utilizza la porta predefinita 7496 e un clientId predefinito a 10. Una volta creato l’oggetto, viene richiamato il metodo di connessione per eseguire la connessione:
            # ib_execution.py
    
    def create_tws_connection(self):
        """
        Collegamento alla Trader Workstation (TWS) in esecuzione 
        sulla porta standard 7496, con un clientId di 10.
        Il clientId è scelto da noi e avremo bisogno ID separati 
        sia per la connessione di esecuzione che per la connessione
        ai dati di mercato, se quest'ultima è utilizzata altrove.
        """
        tws_conn = ibConnection()
        tws_conn.connect()
        return tws_conn
        
Per tenere traccia degli ordini separati (ai fini del tracciamento degli eseguiti) viene utilizzato il metodo create_initial_order_id. E’ stato impostato su “1”, ma un approccio più sofisticato prevede la gestione della query IB per conoscere ed utilizzare l’ultimo ID disponibile. Si può sempre reimpostare l’ID dell’ordine corrente dell’API tramite il pannello Trader Workstation –> Configurazione globale –> Impostazioni API:
            # ib_execution.py
   
    def create_initial_order_id(self):
        """
        Crea l'iniziale ID dell'ordine utilizzato da Interactive
        Broker per tenere traccia degli ordini inviati.
        """
        # Qui c'è spazio per una maggiore logica, ma 
        # per ora useremo "1" come predefinito.
        return 1
        
Il seguente metodo, register_handlers, registra semplicemente i metodi per la gestione degli errori e delle risposte, definiti in precedenza con la connessione TWS:
            # ib_execution.py
    
    def register_handlers(self):
        """
        Registra le funzioni di gestione di errori e dei 
        messaggi di risposta dal server.
        """
        # Assegna la funzione di gestione degli errori definita
        # sopra alla connessione TWS 
        self.tws_conn.register(self._error_handler, 'Error')

        # Assegna tutti i messaggi di risposta del server alla
        # funzione reply_handler definita sopra
        self.tws_conn.registerAll(self._reply_handler)
        
Come descritto nel precedente tutorial relativo all’uso di IbPy, si deve creare un’istanza di Contract ed associarla a un’istanza di Order, che verrà inviata all’API di IB. Il seguente metodo, create_contract, genera la prima componente di questa coppia. Si aspetta in input un simbolo ticker, un tipo di sicurezza (ad esempio, azioni o futures), un exchange primario e una valuta. Restituisce l’istanza di Contract:
            # ib_execution.py
    
    def create_contract(self, symbol, sec_type, exch, prim_exch, curr):
        """
        Crea un oggetto Contract definendo cosa sarà
        acquistato, in quale exchange e in quale valuta.

        symbol - Il simbolo del ticker per il contratto
        sec_type - Il tipo di asset per il contratto ("STK" è "stock")
        exch - La borsa su cui eseguire il contratto
        prim_exch - Lo scambio principale su cui eseguire il contratto
        curr - La valuta in cui acquistare il contratto
        """
        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
        
Il metodo create_order genera la seconda componente della coppia, ovvero l’istanza di Order. Questo metodo prevede in input un tipo di ordine (ad es. market o limit), una quantità del bene da scambiare e una “posizione” (acquisto o vendita). Restituisce l’istanza di Order:
            # ib_execution.py
    
    def create_order(self, order_type, quantity, action):
        """
        Crea un oggetto Ordine (Market/Limit) per andare long/short.

        order_type - "MKT", "LMT" per ordini a mercato o limite
        quantity - Numero intero di asset dell'ordine
        action - 'BUY' o 'SELL'
        """
        order = Order()
        order.m_orderType = order_type
        order.m_totalQuantity = quantity
        order.m_action = action
        return order
        
Per evitare la duplicazione delle istanze di FillEvent per un particolare ID ordine, si utilizza un dizionario chiamato fill_dict per memorizzare le chiavi che corrispondono a particolari ID ordine. Quando è stato generato un eseguito, la chiave “fill” di una voce per un particolare ID ordine è impostata su True. Nel caso si riceva un successivo messaggio “Server Response” da IB che dichiara che un ordine è stato eseguito (ed è un messaggio duplicato) non si creerà un nuovo eseguito. Il seguente metodo create_fill_dict_entry implementa questa logica:
            # ib_execution.py
    
    def create_fill_dict_entry(self, msg):
        """
        Crea una voce nel dizionario Fill che elenca gli orderIds
        e fornisce informazioni sull'asset. Ciò è necessario
        per il comportamento guidato dagli eventi del gestore
        dei messaggi del server IB.
        """
        self.fill_dict[msg.orderId] = {
            "symbol": msg.contract.m_symbol,
            "exchange": msg.contract.m_exchange,
            "direction": msg.order.m_action,
            "filled": False
        }
        

 

Il metodo create_fill si occupa di creare effettivamente l’istanza di FillEvent e la inserisce all’interno della coda degli eventi:

            # ib_execution.py
    
    def create_fill(self, msg):
        """
        Gestisce la creazione del FillEvent che saranno
        inseriti nella coda degli eventi successivamente
        alla completa esecuzione di un ordine.
        """
        fd = self.fill_dict[msg.orderId]

        # Preparazione dei dati di esecuzione
        symbol = fd["symbol"]
        exchange = fd["exchange"]
        filled = msg.filled
        direction = fd["direction"]
        fill_cost = msg.avgFillPrice

        # Crea un oggetto di Fill Event
        fill_event = FillEvent(
            datetime.datetime.utcnow(), symbol,
            exchange, filled, direction, fill_cost
        )

        # Controllo per evitare che messaggi multipli non
        # creino dati addizionali.
        self.fill_dict[msg.orderId]["filled"] = True

        # Inserisce il fill event nella coda di eventi
        self.events.put(fill_event)
        

Dopo aver implementato tutti i metodi precedenti, resta solamente da sviluppare il metodo execute_order della classe base astratta ExecutionHandler. Questo metodo esegue effettivamente il posizionamento dell’ordine tramite l’API di IB.

Si verifica innanzitutto che l’evento ricevuto con questo metodo sia realmente un OrderEvent e quindi prepara gli oggetti Contract e Order con i rispettivi parametri. Una volta che sono stati creati entrambi, il metodo placeOrder dell’oggetto di connessione viene richiamato con associato a order_ID.

È estremamente importante chiamare il metodo time.sleep(1) per garantire che l’ordine sia effettivamente trasmesso ad IB. La rimozione di questa linea può causare comportamenti incoerenti dell’API, e perfino malfunzionamenti!

Infine, si incrementa l’ID ordine al fine di evitare la duplicazione degli ordini:

            # ib_execution.py
    
    def execute_order(self, event):
        """
        Crea il necessario oggetto ordine InteractiveBrokers
        e lo invia a IB tramite la loro API.

        I risultati vengono quindi interrogati per generare il 
        corrispondente oggetto Fill, che viene nuovamente posizionato
        nella coda degli eventi.

        Parametri:
        event - Contiene un oggetto Event con informazioni sull'ordine.
        """
        if event.type == 'ORDER':
            # Prepara i parametri per l'ordine dell'asset
            asset = event.symbol
            asset_type = "STK"
            order_type = event.order_type
            quantity = event.quantity
            direction = event.direction

            # Crea un contratto per Interactive Brokers tramite
            # l'evento Order in inuput
            ib_contract = self.create_contract(
                asset, asset_type, self.order_routing,
                self.order_routing, self.currency
            )

            # Crea un ordine per Interactive Brokers tramite
            # l'evento Order in inuput
            ib_order = self.create_order(
                order_type, quantity, direction
            )

            # Usa la connessione per inviare l'ordine a IB
            self.tws_conn.placeOrder(
                self.order_id, ib_contract, ib_order
            )

            # NOTE: questa linea è cruciale
            # Questo assicura che l'ordina sia effettivamente trasmesso!
            time.sleep(1)

            # Incrementa l'ordene ID per questa sessione
            self.order_id += 1
        

Questa classe costituisce la base per gestione dell’esecuzione verso Interactive Brokers e può essere utilizzata al posto del gestore dell’esecuzione simulata, che è adatto solo per il backtesting. Prima che il gestore di IB possa essere utilizzato è necessario creare un gestore del feed dei dati di mercato in tempo reale che deve sostituire il gestore del feed dei dati storici utilizzato nel backtesting.

Con questo approccio è possibile riutilizzare la maggior parte delle componenti di un sistema di backtesting per un sistema live, in modo da garantire che il codice “swap out” sia ridotto al minimo e quindi assicurare un comportamento simile, se non identico, tra i due sistemi.

 

Per il codice completo riportato in questo articolo, utilizzando il modulo di backtesting event-driven DataBacktest si può consultare il seguente repository di github:
https://github.com/datatrading-info/DataBacktest

Torna in alto
Scroll to Top