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)
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
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
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.”