Trading con Interactive Brokers – Creare un Trading System Parte 9

Trading con Interactive Brokers

In questa lezione del corso su come creare un trading system event-driven con Python, spieghiamo come realizzare il trading con Interactive Brokers. Implementiamo il gestore dell’API di Interactive Brokers, così da utilizzare l’ExecutionHandler per il live trading. Abbiamo già presentato il modello base di ExecutionHandler per il backtest nella lezione precedente.

Nel corso dedicato alle basi del trading algoritmico con Python, abbiamo mostrato come scaricare Trader Workstation, creare un account demo su Interactive Brokers e implementare una semplice interfaccia API con IbPy. Ora colleghiamo l’interfaccia IbPy all’interno di un sistema event-driven per il trading, così da creare una base operativa per un sistema di esecuzione automatizzato insieme a un feed dati real-time.

Trading con Interactive Brokers

La classe IBExecutionHandler riceve istanze OrderEvent dalla coda degli eventi ed esegue direttamente ordini tramite l’API di Interactive Brokers usando la libreria IbPy. Inoltre, gestisce i messaggi di risposta “Server Response” inviati dall’API. In questa fase, genera le istanzeFillEvent e le reinserisce nella coda degli eventi.

Possiamo estendere facilmente questa classe, includendo logiche avanzate per l’ottimizzazione dell’esecuzione e la gestione degli errori. Tuttavia, in questa fase manteniamo l’implementazione semplice per comprendere le funzioni principali e adattarle al tuo stile di trading.

Implementazione della classe Python

Per gestire il trading con Interactive Brokers, creiamo un file Python chiamato ib_execution.py, situato nella stessa directory degli altri moduli event-driven. Importiamo le librerie necessarie per gestire data e ora, gli oggetti di IbPy e gli eventi specifici che IBExecutionHandler elabora.

				
					# 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
				
			

Definiamo ora la classe IBExecutionHandler. Il costruttore __init__ accetta come input la coda degli eventi. Inoltre, consente di specificare order_routing, che di default usa “SMART”. Quando l’exchange richiede parametri particolari, puoi inserirli direttamente nel costruttore. La currency predefinita risulta impostata sui Dollari USA.

Il metodo crea un dizionario fill_dict, necessario per generare istanze della classe FillEvent. In aggiunta, definisce un oggetto tws_conn per archiviare i dati di connessione con l’API di Interactive Brokers. Viene creato anche un order_id iniziale, utile per tenere traccia degli ordini ed evitare duplicazioni. Infine, il gestore dei messaggi viene registrato (sarà descritto in dettaglio 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()
				
			

Gestione degli eventi

Il trading con Interactive Brokers utilizza l’API di IB, che funziona con un sistema di eventi basato su messaggi. Questo sistema consente alla nostra classe di reagire in modo specifico a determinati messaggi, proprio come accade nell’ambiente di backtesting event-driven. Per brevità, il codice non gestisce direttamente gli errori reali, ma invia un output al terminale tramite il metodo _error_handler.

Il metodo _reply_handler gestisce la logica che determina quando creare un’istanza FillEvent. Quando riceve un messaggio “openOrder”, controlla se esiste una voce fill_dict associata all’orderId. Se non la trova, crea automaticamente una nuova voce.

Se riceve un messaggio “orderStatus” che indica l’esecuzione di un ordine, il metodo richiama la funzione create_fill per generare un FillEvent. Invia anche un messaggio al terminale per finalità di debug o logging.

				
					# 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 tracciare ordini distinti e monitorare correttamente gli eseguiti, usa il metodo create_initial_order_id. Il valore iniziale è impostato su “1”, ma un approccio più avanzato nella gestione ordini per il trading algoritmico prevede di interrogare IB per ottenere l’ultimo ID disponibile. Puoi modificare l’ID corrente degli ordini API direttamente nel 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)


				
			

Creare il contratto

Il corretto uso IbPy prevede la creazione di un’istanza di Contract ed associarla a un’istanza di Order, da inviare 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 di duplicare le istanze di FillEvent associate a uno specifico ID ordine, il sistema memorizza queste informazioni in un dizionario chiamato fill_dict. Le chiavi di questo dizionario identificano ogni ID ordine univoco. 

Quando il sistema genera un eseguito, imposta la chiave “fill” del relativo ID ordine su True. Se successivamente Interactive Brokers invia un messaggio “Server Response” che segnala un ordine già eseguito (ma duplicato), il sistema riconosce l’esecuzione come già avvenuta e non crea un nuovo eseguito. Il metodo create_fill_dict_entry applica esattamente 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)

				
			

Esecuzione degli ordini

Dopo aver completato tutti i metodi precedenti, per completare il trading con Interactive Brokers bisogna implementare il metodo execute_order della classe astratta ExecutionHandler. Questo metodo gestisce direttamente il posizionamento dell’ordine tramite l’API di IB.

Per prima cosa, il metodo controlla che l’evento ricevuto sia effettivamente un OrderEvent, quindi prepara gli oggetti Contract e Order con i parametri corretti. Dopo averli costruiti entrambi, richiama il metodo placeOrder sull’oggetto di connessione, utilizzando l’order_ID.

Risulta fondamentale inserire il metodo time.sleep(1) per garantire che l’ordine venga trasmesso correttamente a IB. Omettere questa linea può provocare comportamenti anomali dell’API o addirittura malfunzionamenti!

Per concludere, incrementa l’ID dell’ordine per evitare ogni possibile duplicazione durante l’invio successivo.

				
					# 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
				
			

Conclusione

Questa classe rappresenta la base per gestire l’esecuzione e fare trading con Interactive Brokers, sostituendo il gestore dell’esecuzione simulata, utile solo per il backtesting. Prima di attivare il gestore di IB, occorre creare un gestore per il feed dei dati di mercato in tempo reale, che prende il posto di quello storico utilizzato nei test.

Grazie a questo approccio, possiamo riutilizzare gran parte delle componenti del sistema di backtesting per costruire un sistema live. Così riduci al minimo le modifiche necessarie nel codice, garantendo un comportamento molto simile, se non identico, tra il sistema in tempo reale e quello in fase di test.

Il codice completo presentato in questa lezione, basato sul sistema di trading event-driven TQTradingSystem, è disponibile nel seguente repository GitHub: https://github.com/tradingquant-it/TQTradingSystem.”

Torna in alto