Nella precedente lezione del corso su come creare un Trading System event-driven abbiamo spiegato la costruzione della gerarchia della classe Strategy
. In questa lezione analizziamo l’oggetto NaivePortfolio
, che implementa la logica del portafoglio del trading system.
La logica prevede di monitore le posizioni di un portafoglio e generare ordini con una quantità fissa di azioni in risposta ai segnali ricevuti. Le versioni più avanzate dell’oggetto Portfolio integrano strumenti di gestione del rischio sofisticati, che approfondiremo nelle prossime lezioni.
Le strategie, così come le abbiamo definite, generano signals che forniscono input all’oggetto portfolio per decidere quando inviare gli orders. Il primo passo consiste nel definire una classe astratta di base (ABC) Portfolio
da cui derivano tutte le sottoclassi successive.
Logica del Portafoglio
Nel backtesting basato su eventi, la Logica del Portafoglio rappresenta probabilmente la parte più complessa del sistema di gestione degli ordini. Il portafoglio controlla in tempo reale tutte le posizioni aperte sul mercato e valuta il loro valore corrente (gli “holdings”). Questa valutazione stima il valore di liquidazione e sfrutta le funzionalità di gestione dei dati del backtester.
Oltre a monitorare le posizioni e gli holdings, il portfolio considera i fattori di rischio e applica le tecniche di dimensionamento per ottimizzare ogni ordine destinato al broker o ad altri canali di accesso al mercato.
Seguendo la struttura della classe Event
, l’oggetto Portfolio
gestisce i SignalEvent
, genera i OrderEvent
e interpreta i FillEvent
per aggiornare le posizioni in portafoglio. Non sorprende quindi che, all’interno dei sistemi event-driven, la Logica del Portafoglio occupi spesso il maggior numero di righe di codice.
Implementazione della classe Portafoglio
Per iniziare, creiamo un nuovo file chiamato portfolio.py
e importiamo le librerie necessarie. Queste librerie sono comuni alla maggior parte delle implementazioni di classi astratte di base. Importiamo in particolare la funzione floor
dalla libreria math
per calcolare ordini con quantità intere. Inoltre, includiamo gli oggetti FillEvent
e OrderEvent
, fondamentali nella gestione completa del Portfolio.
# portfolio.py
import datetime
import numpy as np
import pandas as pd
import queue
from abc import ABCMeta, abstractmethod
from math import floor
from event import FillEvent, OrderEvent
A questo punto creiamo una classe ABC per il Portfolio
e implementiamo due metodi virtuali update_signal
e update_fill
. Il primo elabora i nuovi segnali di trading prelevati dalla coda degli eventi, mentre il secondo gestisce gli ordini eseguiti e ricevuti dall’oggetto di gestione dell’esecuzione.
# portfolio.py
class Portfolio(object):
"""
La classe Portfolio gestisce le posizioni e il valore di
mercato di tutti gli strumenti alla risoluzione di una "barra",
cioè ogni secondo, ogni minuto, 5 minuti, 30 minuti, 60 minuti o EOD.
"""
__metaclass__ = ABCMeta
@abstractmethod
def update_signal(self, event):
"""
Azioni su un SignalEvent per generare nuovi ordini
basati sulla logica di portafoglio
"""
raise NotImplementedError("Should implement update_signal()")
@abstractmethod
def update_fill(self, event):
"""
Aggiorna le posizioni e il patrimonio del portafoglio
da un FillEvent.
"""
raise NotImplementedError("Should implement update_fill()")
Il cuore della logica del portafoglio è la classe NaivePortfolio
, progettata per gestire il dimensionamento delle posizioni e gli holdings correnti. Questa classe esegue gli ordini di compravendita in modo semplice: li invia direttamente al broker usando una dimensione fissa e predeterminata, senza considerare la liquidità disponibile. Sebbene si tratti di ipotesi poco realistiche, queste scelte aiutano a comprendere come funziona un sistema event-driven per la gestione degli ordini di portafoglio.
La classe NaivePortfolio
Per inizializzare NaivePortfolio
, basta fornire un valore di capitale iniziale e una data di inizio. In questo esempio, il capitale iniziale è fissato a 100.000 USD.
Il portafoglio utilizza due attributi principali: all_positions
e current_positions
. Il primo raccoglie tutte le posizioni storiche associate a ciascun timestamp di un evento di mercato. Ogni posizione rappresenta la quantità posseduta di un asset. Se la quantità è negativa, indica una riduzione dell’asset. Il secondo attributo conserva le posizioni attuali, organizzate in un dizionario aggiornato all’ultimo evento di mercato.
Oltre a gestire le posizioni, il portafoglio registra anche gli holdings, ovvero il valore di mercato corrente degli asset in portafoglio. In questo contesto, il valore corrente corrisponde al prezzo di chiusura della barra OHLCV più recente. Anche se questa è un’approssimazione, risulta sufficiente per illustrare la logica del portafoglio. L’attributo all_holdings
raccoglie la cronologia completa dei valori degli holdings, mentre current_holdings
conserva il dizionario aggiornato degli attuali
# portfolio.py
class NaivePortfolio(Portfolio):
"""
L'oggetto NaivePortfolio è progettato per inviare ordini a
un oggetto di intermediazione con una dimensione di quantità costante,
cioè senza alcuna gestione del rischio o dimensionamento della posizione. È
utilizzato per testare strategie più semplici come BuyAndHoldStrategy.
"""
def __init__(self, bars, events, start_date, initial_capital=100000.0):
"""
Inizializza il portfolio con la coda delle barre e degli eventi.
Include anche un indice datetime iniziale e un capitale iniziale
(USD se non diversamente specificato).
Parametri:
bars - L'oggetto DataHandler con i dati di mercato correnti.
events: l'oggetto Event Queue (coda di eventi).
start_date - La data di inizio (barra) del portfolio.
initial_capital - Il capitale iniziale 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()
Il seguente metodo, construct_all_positions
, crea un dizionario per ogni simbolo e imposta il valore a zero. Inoltre aggiunge una chiave datetime e inserisce il dizionario in un elenco. Usa una comprensione del dizionario, che è simile alla comprensione di una lista:
# portfolio.py
def construct_all_positions(self):
"""
Costruisce l'elenco delle posizioni utilizzando start_date
per determinare quando inizierà l'indice temporale.
"""
d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
d['datetime'] = self.start_date
return [d]
construct_all_holdings
è simile al precedente, ma aggiunge delle chiavi extra per memorizzare i contanti, le commissioni e il totale, che rappresentano rispettivamente la riserva di denaro nel conto dopo eventuali acquisti, la commissione cumulativa maturata e il totale del conto azionario inclusi i contanti e le posizioni aperte. Le posizioni short sono considerate negative. I contanti (cash) e il totale (total) sono entrambi inizializzati con il capitale iniziale:
# portfolio.py
def construct_all_holdings(self):
"""
Costruisce l'elenco delle partecipazioni utilizzando start_date
per determinare quando inizierà l'indice temporale.
"""
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]
Il metodo seguente, construct_current_holdings
è quasi identico al metodo precedente, tranne per il fatto che non racchiude il dizionario in un elenco:
# portfolio.py
def construct_current_holdings(self):
"""
Questo costruisce il dizionario che conterrà l'istantaneo
valore del portafoglio attraverso tutti i simboli.
"""
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
Gestione degli eventi nella logica del portafoglio
Ad ogni “battito” o impulso del sistema, cioè ogni volta che l’oggetto DataHandler
richiede nuovi dati di mercato, il portfolio aggiorna il valore corrente di tutte le posizioni detenute. Nel trading live il broker fornisce e analizza direttamente queste informazioni, mentre nel backtesting bisogna calcolare manualmente tali valori.
Non esiste un vero “valore corrente di mercato” a causa dello spread bid/ask e dei problemi legati alla liquidità. Per questo, si stima il valore moltiplicando la quantità posseduta per un determinato “prezzo”. In questo esempio si usa il prezzo di chiusura dell’ultima barra disponibile. Questa scelta risulta piuttosto realistica per strategie intraday, mentre lo è meno per quelle giornaliere, dove il prezzo di apertura può variare notevolmente.
Il metodo update_timeindex
si occupa del monitoraggio dei nuovi holdings. Ricava i prezzi più recenti dal gestore dei dati e costruisce un nuovo dizionario di simboli per rappresentare le posizioni correnti, assegnando alle “nuove” posizioni lo stesso valore delle “correnti”. La logica del portafoglio modifica questi valori solo quando riceve un FillEvent
, che viene poi elaborato dal portfolio. Il metodo aggiunge quindi questo set di posizioni a all_positions
. Successivamente, aggiorna le posizioni con la stessa logica, ma ricalcola il valore moltiplicando il numero delle posizioni per il prezzo di chiusura dell’ultima barra (self.current_positions[s] * bars[s][0][5]
). Infine, inserisce i nuovi holdings in all_holdings
.
# portfolio.py
def update_timeindex(self, event):
"""
Aggiunge un nuovo record alla matrice delle posizioni per la barra corrente
dei dati di mercato. Questo riflette la barra PRECEDENTE, cioè in questa fase
tutti gli attuali dati di mercato sono noti (OLHCVI).
Utilizza un MarketEvent dalla coda degli eventi.
"""
latest_datetime = self.bars.get_latest_bar_datetime(
self.symbol_list[0]
)
# Update positions
dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
dp['datetime'] = latest_datetime
for s in self.symbol_list:
dp[s] = self.current_positions[s]
# Aggiunge le posizioni correnti
self.all_positions.append(dp)
# Aggiorno delle holdings
dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
dh['datetime'] = latest_datetime
dh['cash'] = self.current_holdings['cash']
dh['commission'] = self.current_holdings['commission']
dh['total'] = self.current_holdings['cash']
for s in self.symbol_list:
# Approossimazione ad un valore reale
market_value = market_value = self.current_positions[s] * \
self.bars.get_latest_bar_value(s, "adj_close")"adj_close")
dh[s] = market_value
dh['total'] += market_value
# Aggiunta alle holdings correnti
self.all_holdings.append(dh)
Il metodo update_positions_from_fill
determina se FillEvent
è un Buy o un Sell e quindi aggiorna di conseguenza il dizionario current_positions
aggiungendo / sottraendo la corretta quantità di asset:
# portfolio.py
def update_positions_from_fill(self, fill):
"""
Prende un oggetto FilltEvent e aggiorna la matrice delle posizioni
per riflettere le nuove posizioni.
Parametri:
fill - L'oggetto FillEvent da aggiornare con le posizioni.
"""
# 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
# Aggiorna le posizioni con le nuove quantità
self.current_positions[fill.symbol] += fill_dir * fill.quantity
I costi di esecuzione
Il metodo update_holdings_from_fill
, simile a quello precedente, aggiorna i valori di holdings. Per simulare il prezzo di esecuzione, il metodo ignora il prezzo presente nel FillEvent
.
Perché adottare questo approccio? In un ambiente di backtesting, il sistema non conosce il prezzo di esecuzione reale, quindi occorre stimarlo. Impostiamo il prezzo di esecuzione sul “prezzo corrente di mercato”, ovvero il prezzo di chiusura dell’ultima barra. Il sistema imposta le posizioni per ogni simbolo pari al prezzo di esecuzione moltiplicato per la quantità del trade.
Dopo aver determinato il prezzo di esecuzione, il sistema aggiorna gli holdings correnti, la liquidità disponibile e il valore totale. Inoltre, aggiorna anche la commissione cumulativa per riflettere fedelmente l’architettura event-driven di un trading system.
# portfolio.py
def update_holdings_from_fill(self, fill):
"""
Prende un oggetto FillEvent e aggiorna la matrice delle holdings
per riflettere il valore delle holdings.
Parametri:
fill - L'oggetto FillEvent da aggiornare con le holdings.
"""
# Controllo se l'oggetto fill è un buy o sell
fill_dir = 0
if fill.direction == 'BUY':
fill_dir = 1
if fill.direction == 'SELL':
fill_dir = -1
# Aggiorna la lista di holdings con le nuove quantità
fill_cost = self.bars.get_latest_bar_value(fill.symbol, "adj_close")
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)
Implementiamo anche il metodo virtuale update_fill
della classe ABC Portfolio
. Il metodo esegue semplicemente le due precedenti funzioni, update_positions_from_fill
e update_holdings_from_fill
:
# portfolio.py
def update_fill(self, event):
"""
Aggiorna le attuali posizioni e holdings del portafoglio da un FillEvent.
"""
if event.type == 'FILL':
self.update_positions_from_fill(event)
self.update_holdings_from_fill(event)
Gestione degli Ordini
La logica del portafoglio di un trading system, oltre a gestire i FillEvents
, deve anche occuparsi della generazione degli OrderEvents
al ricevimento di uno o più SignalEvents
. Il metodo generate_naive_order
prende un segnale di long o short di un asset e invia un ordine per aprire una posizione per 100 shares di tale asset. Chiaramente 100 è un valore arbitrario. In un’implementazione realistica questo valore sarà determinato da una gestione del rischio o da un overlay di ridimensionamento della posizione. Tuttavia, questo è un NaivePortfolio
e quindi “ingenuamente” invia tutti gli ordini direttamente dai segnali, senza un sistema di dimensionamento della posizione.
Il metodo gestisce il long, lo short e l’uscita di una posizione, in base alla quantità corrente e allo specifico simbolo. Infine vengono generati i corrispondenti oggetti OrderEvent
:
# portfolio.py
def generate_naive_order(self, signal):
"""
Trasmette semplicemente un oggetto OrderEvent con una quantità costante
che dipendente dell'oggetto segnale, senza gestione del rischio o
considerazioni sul dimensionamento della posizione.
Parametri:
signal - L'oggetto SignalEvent.
"""
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
Il metodo update_signal
richiama semplicemente il metodo precedente e aggiunge l’ordine generato alla coda degli eventi:
# portfolio.py
def update_signal(self, event):
"""
Azioni a seguito di un SignalEvent per generare nuovi ordini
basati sulla logica del portafoglio
"""
if event.type == 'SIGNAL':
order_event = self.generate_naive_order(event)
self.events.put(order_event)
NaivePortfolio
prevede la generazione di una curva equity. Crea semplicemente un flusso dei rendimenti, utilizzato per i calcoli delle prestazioni e quindi normalizza la curva equity in base alla percentuale. La dimensione iniziale dell’account è pari a 1,0:
# portfolio.py
def create_equity_curve_dataframe(self):
"""
Crea un DataFrame pandas dalla lista di dizionari "all_holdings"
"""
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
Il metodo finale nel NaivePortfolio
è l’output della curva azionaria e di varie statistiche sulle performance della strategia. L’ultima riga genera un file, equity.csv, nella stessa directory del codice, che può essere caricato in uno script Matplotlib Python (o un foglio di calcolo come MS Excel o LibreOffice Calc) per un’analisi successiva.
Si noti che la Durata del Drawdown è data in termini di numero assoluto di “barre” per le quali si è svolto il Drawdown, al contrario di un determinato periodo di tempo.
# portfolio.py
def output_summary_stats(self):
"""
Crea un elenco di statistiche di riepilogo per il portafoglio
come lo Sharpe Ratio e le informazioni sul drowdown.
"""
total_return = self.equity_curve['equity_curve'][-1]
returns = self.equity_curve['returns']
pnl = self.equity_curve['equity_curve']
sharpe_ratio = create_sharpe_ratio(returns, periods=252 * 60 * 6.5)
drawdown, max_dd, dd_duration = create_drawdowns(pnl)
self.equity_curve['drawdown'] = drawdown
stats = [("Total Return", "%0.2f%%" % \
((total_return - 1.0) * 100.0)),
("Sharpe Ratio", "%0.2f" % sharpe_ratio),
("Max Drawdown", "%0.2f%%" % (max_dd * 100.0)),
("Drawdown Duration", "%d" % dd_duration)]
self.equity_curve.to_csv('equity.csv')
return stats
Conclusione
L’oggetto NaivePortfolio
è la componente più complessa dell’intero trading system basato sugli eventi. L’implementazione è complessa, quindi in questa lezione abbiamo semplificato alcuni aspetti tra cui la gestione delle posizioni. Le versioni successive prenderanno in considerazione la gestione del rischio e il dimensionamento delle posizioni, che porterà a un’idea molto più realistica delle prestazioni della strategia.
Nella prossima lezione descriviamo l’ultimo modulo di un sistema di backtesting event-driven, ovvero l’oggetto ExecutionHandler
, che viene utilizzato per prelevare oggetti OrderEvent
e creare oggetti FillEvent
.
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.”