In questo articolo descriviamo come effettuare il backtest di una strategia di analisi del sentiment tramite un sistema di trading quantitativo. Infatti, oltre ai “soliti” trucchi relativi all’arbitraggio statistico, al trend-following e all’analisi fondamentale, molti fondi quantitativi (e anche noi trader retail!) ci dedichiamo a tecniche di elaborazione del linguaggio naturale (NLP) per costruire strategie sistematiche. Queste tecniche rientrano nell’ambito della Sentiment Analysis.
Una delle sfide principali nello sviluppo di un sistema simile consiste nell’integrare eventi di sentiment, archiviati in un file CSV con righe “datetime-ticker-sentiment”, in un sistema di trading basato su eventi, tipicamente progettato per operare direttamente sui dati dei prezzi. L’obbiettivo descrivere ed implementare un gruppo di strategie di trading quantitativo basate su una serie di segnali di sentiment che otteniamo da un’API fornita da un provider. Questi segnali forniscono una scala intera da -3 (“sentimento negativo più forte”) a +6 (“sentimento positivo più forte”), associata a una data e a un simbolo ticker, che utilizziamo come soglia di ingresso e di uscita in un backtest event-driven.
L’articolo inizia con una breve discussione sulle modalità di esecuzione dell’analisi di sentiment e con la descrizione tecnica delle API del fornitore e dei file di esempio. Proseguiamo descrivendo la funzionalità di sentiment che abbiamo recentemente aggiunto a DataTrader, incluso il relativo codice Python. Infine analizziamo i risultati di tre distinti backtest della strategia di sentiment applicata ai titoli S&P500 nei settori della tecnologia, della difesa e dell’energia.
Analisi del sentiment
Con l’analisi del sentiment vogliamo, in generale, elaborare grandi quantità di dati “non strutturati” (come post di blog, articoli di giornale, rapporti di ricerca, tweet, video, immagini, ecc.) e applicare tecniche di PNL per quantificare il “sentiment” positivo o negativo relativo a determinati asset.
Nel caso delle azioni, ci concentriamo in particolare sull’analisi statistica del linguaggio usato, per capire se contiene un fraseggio rialzista o ribassista. Questo fraseggio lo quantifichiamo in termini di forza del sentiment, che traduciamo in valori numerici. In altre parole, i valori positivi indicano un sentimento rialzista, mentre quelli negativi riflettono un sentimento ribassista.
Negli ultimi anni abbiamo assistito a una crescita costante di fornitori di analisi del sentiment, tra cui Sentdex, PsychSignal e Accern. Tutti applicano tecniche proprietarie per identificare “entità” all’interno dei dati alternativi e associare un punteggio di sentiment con timestamp a ogni informazione estratta. Possiamo poi aggregare queste informazioni in un determinato intervallo temporale (ad esempio un giorno) per ottenere tuple del tipo data-entità-sentiment. Queste tuple rappresentano la base di un segnale di trading.
Non intendiamo descrivere in dettaglio le metodologie di analisi di grandi volumi di “big data” e la quantificazione del sentiment, poiché questo esula dallo scopo dell’articolo. Sviluppare uno strumento di analisi del sentiment end-to-end, pronto per l’uso in produzione, richiede un notevole impegno ingegneristico. Per questo motivo, molti trader retail come noi preferiamo ottenere questi segnali direttamente dai fornitori e integrarli in un portafoglio più ampio di segnali quantitativi per costruire strategie sistematiche.
In questo articolo presentiamo una strategia di analisi del sentiment basata sui dati forniti da Sentdex e mostriamo come possiamo generare segnali long-only a partire da questi dati.
Le API di Sentdex e file di esempio
Sentdex mette a disposizione un’API che consente di scaricare dati di sentiment per un’ampia varietà di strumenti finanziari. Possiamo ottenere questi dati con una risoluzione temporale di un minuto o di un giorno. Troviamo maggiori dettagli sulla loro offerta (a pagamento) alla pagina API.
Non approfondiamo l’uso dell’API in questo articolo, poiché si tratta di un servizio a pagamento pensato soprattutto per generare eventi in scenari di paper trading o trading reale. Poiché ci concentriamo sulle strategie di backtesting con dati storici, preferiamo utilizzare un file statico archiviato localmente per rappresentare i dati di sentiment.
Fortunatamente, Sentdex fornisce un file di dati campione (scaricabile qui) che contiene quasi cinque anni di segnali di sentiment, a risoluzione giornaliera, per molti dei componenti dell’S&P500.
Di seguito presentiamo uno snippet del file:
date,symbol,sentiment_signal
2012-10-15,AAPL,6
2012-10-16,AAPL,2
2012-10-17,AAPL,6
2012-10-18,AAPL,6
2012-10-19,AAPL,6
2012-10-20,AAPL,6
2012-10-21,AAPL,1
2012-10-22,MSFT,6
2012-10-22,GOOG,6
2012-10-22,AAPL,-1
2012-10-23,AAPL,-3
2012-10-23,GOOG,-3
2012-10-23,MSFT,6
2012-10-24,GOOG,-1
2012-10-24,MSFT,-3
2012-10-24,AAPL,-1
Notiamo che ogni riga contiene una data, un simbolo ticker e un numero intero che rappresenta la forza del sentimento, compresa tra +6 (“forte sentimento positivo”) e -3 (“forte sentimento negativo”).
Questo file di esempio costituisce la base dei dati sul sentiment che utilizziamo nelle tre simulazioni descritte in questo articolo.
La strategia di analisi del sentiment
La complessità di questa implementazione deriva principalmente dagli adeguamenti al framework open-source di backtesting event-driven DataTrader, più che dalla strategia in sé, che risulta abbastanza semplice dopo aver generato il segnale del sentiment. Manteniamo volutamente la strategia semplice, lasciando ampi margini di modifica e ottimizzazione che tratteremo in articoli successivi.
In questo esempio operiamo solo long, ma possiamo facilmente modificare la strategia per includere posizioni short. Stabiliamo soglie di entrata e uscita, che utilizziamo per aprire o chiudere posizioni long.
Implementiamo tre strategie identiche, con l’unica differenza nella selezione dei titoli su cui operiamo. L’elenco delle azioni è il seguente:
- Tecnologia: MSFT, AMZN, GOOG, IBM, AAPL
- Energia: XOM, CVX, SLB, OXY, COP
- Difesa: BA, GD, NOC, LMT, RTN
Le regole della strategia sono le seguenti:
- Apriamo una posizione long su un ticker se il suo valore di sentiment raggiunge +6
- Chiudiamo una posizione su un ticker se il suo valore di sentiment scende a -1
Non applichiamo un’allocazione percentuale per ciascun titolo. Utilizziamo una quantità fissa di azioni per ciascuna allocazione durante tutta la strategia. Tuttavia, modifichiamo questa quantità fissa in base ai tre settori sopra indicati.
Una modifica ovvia sarebbe creare un investimento pesato in dollari che regola dinamicamente l’allocazione in base alle dimensioni del capitale. Tuttavia, in questo articolo semplifichiamo il dimensionamento della posizione per facilitare la comprensione della logica base di generazione dell’evento di sentiment.
Set di dati storici
Per attuare questa strategia di analisi del sentiment abbiamo bisogno dei dati giornalieri OHLCV dei titoli relativi al periodo coperto da questi backtest. Eseguiamo tre simulazioni separate, ciascuna con un gruppo di cinque titoli dell’S&P500.
Abbiamo un gruppo comprende titoli tecnologici/di consumo di base:
Ticker | Nome | Periodo |
---|---|---|
MSFT | Microsoft | 15 ottobre 2012 – 2 febbraio 2016 |
AMZN | Amazon.com | 15 ottobre 2012 – 2 febbraio 2016 |
GOOG | Alphabet | 15 ottobre 2012 – 2 febbraio 2016 |
AAPL | Apple | 15 ottobre 2012 – 2 febbraio 2016 |
IBM | International Business Machines | 15 ottobre 2012 – 2 febbraio 2016 |
Prendiamo poi in considerazione un gruppo che comprende titoli del settore difesa, anch’essi facenti parte dell’S&P500:
Ticker | Nome | Periodo |
---|---|---|
BA | The Boeing Company | 15 ottobre 2012 – 2 febbraio 2016 |
LMT | Lockheed Martin | 15 ottobre 2012 – 2 febbraio 2016 |
NOC | Northrop Grumman | 15 ottobre 2012 – 2 febbraio 2016 |
GD | General Dynamics | 15 ottobre 2012 – 2 febbraio 2016 |
RTN | Raytheon | 15 ottobre 2012 – 2 febbraio 2016 |
Infine effettuiamo il backtest su un gruppo formato da titoli energetici dell’S&P500:
Ticker | Nome | Periodo |
---|---|---|
XOM | Exxon Mobile | 15 ottobre 2012 – 2 febbraio 2016 |
CVX | Chevron | 15 ottobre 2012 – 2 febbraio 2016 |
SLB | Schlumberger | 15 ottobre 2012 – 2 febbraio 2016 |
OSSI | Occidental Petroleum | 15 ottobre 2012 – 2 febbraio 2016 |
COP | ConocoPhilips | 15 ottobre 2012 – 2 febbraio 2016 |
Dobbiamo inserire questi dati nella directory specificata dal file delle impostazioni di QSTrader per replicare i risultati.
Inoltre, posizioniamo il file di esempio dell’API Sentdex nella stessa directory dei dati di DataTrader.
Implementazione Python
Gestione del sentiment con DataTrader
Per eseguire il backtest di una strategia di analisi del sentiment, dobbiamo capire come incorporare i “segnali” del sentiment nel backtest.
Il modello attuale di DataTrader per il backtesting prevede un “motore” che gestisce la risposta agli eventi. Implementiamo questo motore in un ciclo while
che itera su tutti gli oggetti TickEvent
e BarEvent
. Il codice ci consente di concatenare qualsiasi dataset di serie storiche finanziarie di ticker, archiviati in un database o in file CSV, e quindi iterare riga per riga, con ogni riga che rappresenta un elemento di un DataFrame pandas formato da un TickEvent
o da un BarEvent
per ciascun ticker.
In precedenza questa logica era implementata con il seguente codice:
while self.price_handler.continue_backtest:
try:
event = self.events_queue.get(False)
except queue.Empty:
self.price_handler.stream_next()
else:
if event is not None:
if event.type == EventType.TICK or event.type == EventType.BAR:
self.cur_time = event.time
self.strategy.calculate_signals(event)
self.portfolio_handler.update_portfolio_value()
self.statistics.update(event.time, self.portfolio_handler)
elif event.type == EventType.SIGNAL:
self.portfolio_handler.on_signal(event)
elif event.type == EventType.ORDER:
self.execution_handler.execute_order(event)
elif event.type == EventType.FILL:
self.portfolio_handler.on_fill(event)
else:
raise NotImplemented("Unsupported event.type '%s'" % event.type)
Vediamo che il ciclo continua al termine del backtest, determinato dall’oggetto PriceHandler
. Ad ogni iterazione si estrae (se esiste) l’ultimo Event
dalla coda e lo invia al corretto gestore a seconda del tipo di evento. Tuttavia, in questo caso il file CSV dei segnali di sentiment precedentemente menzionato contiene anche il timestamp di ogni segnale. Quindi è necessario “iniettare” l’appropriato segnale di sentiment per un particolare ticker nel corretto momento del backtest.
A tale scopo abbiamo creato un nuovo evento chiamato SentimentEvent
. Memorizza un timestamp, un ticker e un valore di sentiment (che può essere un valore a virgola mobile, un intero o una stringa) che viene poi inviato all’oggetto Strategy
per generare un SignalEvent
. Il codice del SentimentEvent
implementato all’interno di DataTrader è il seguente:
class SentimentEvent(Event):
"""
Gestisce l'evento di streaming di un valore "Sentiment" associato
a un ticker. Può essere utilizzato per un servizio generico
"data-ticker-sentiment", spesso fornito da molti fornitori di dati.
"""
def __init__(self, timestamp, ticker, sentiment):
"""
Inizializza il SentimentEvent.
Parameters:
timestamp - il timestamp in cui l'ordine è stato eseguito.
ticker - Il simbolo del ticker, ad es. "GOOG".
sentiment - Una stringa, un valore float o un valore intero
di "sentiment", ad es. "rialzista", -1, 5.4, ecc.
"""
self.type = EventType.SENTIMENT
self.timestamp = timestamp
self.ticker = ticker
self.sentiment = sentiment
Gestione dei dati
È stata anche creata una nuova gerarchia di oggetti chiamata AbstractSentimentHandler
. In questo modo possiamo gestire differenti sottoclassi degli oggetti del gestore del sentiment a seconda del fornitore dei dati, che condividono un’interfaccia comune verso il motore degli eventi di DataTrader. Dato che indicator di sentimento sono quasi sempre tuple “timestamp-ticker-sentiment”, è utile creare un’interfaccia unificata.
Per gestire il file CSV di esempio di Sentdex abbiamo implementato l’oggetto SentdexSentimentHandler
. Come con la maggior parte dei gestori, richiede un handle per la coda degli eventi, un sottoinsieme di ticker su cui agire e una data di inizio e fine:
class SentdexSentimentHandler(AbstractSentimentHandler):
"""
SentdexSentimentHandler è progettato per fornire al motore di backtesting
un gestore di analisi del sentimento del provider Sentdex
(http://sentdex.com/financial-analysis/).
Utilizza un file CSV con tuple / righe di data-ticker-sentiment.
Quindi, per evitare impliciti bias di lookahead, viene fornito
un metodo specifico "stream_sentiment_events_on_date" che
consente di recuperare solo i segnali di sentiment
per una data particolare.
"""
def __init__(
self, csv_dir, filename,
events_queue, tickers=None,
start_date=None, end_date=None
):
self.csv_dir = csv_dir
self.filename = filename
self.events_queue = events_queue
self.tickers = tickers
self.start_date = start_date
self.end_date = end_date
self.sent_df = self._open_sentiment_csv()
Questa classe contiene due metodi principali. Il primo metodo è _open_sentiment_csv
che permette di aprire un file CSV e trasferire i dati all’interno di un pandas DataFrame insieme al ticker associato e al filtro delle date:
def _open_sentiment_csv(self):
"""
Apre il file CSV contenente le informazioni sull'analisi
del sentiment per tutti i titoli rappresentati e lo
inserisce in un DataFrame pandas.
"""
sentiment_path = os.path.join(self.csv_dir, self.filename)
sent_df = pd.read_csv(
sentiment_path, parse_dates=True,
header=0, index_col=0,
names=("Date", "Ticker", "Sentiment")
)
if self.start_date is not None:
sent_df = sent_df[self.start_date.strftime("%Y-%m-%d"):]
if self.end_date is not None:
sent_df = sent_df[:self.end_date.strftime("%Y-%m-%d")]
if self.tickers is not None:
sent_df = sent_df[sent_df["Ticker"].isin(self.tickers)]
return sent_df
Il secondo metodo è stream_next
, usato per “trasmettere in streaming” il successivo segnale di sentiment all’interno della coda degli eventi. Poiché il file CSV Sentdex contiene più ticker nella stessa data, è necessario specificare un stream_date
in modo da non introdurre un lookahead bias. In altre parole, il gestore di eventi non dovrebbe mai vedere un segnale di sentiment che viene generato “in futuro” sbirciando troppo avanti nel file CSV.
Fondamentalmente, questo metodo produce più oggetti SentimentEvent
, tutti quelli che sono stati generati in un determinato giorno:
while self.price_handler.continue_backtest:
try:
event = self.events_queue.get(False)
except queue.Empty:
self.price_handler.stream_next()
else:
if event is not None:
if event.type == EventType.TICK or event.type == EventType.BAR:
self.cur_time = event.time
# Creazione di ogni evento di sentiment
if self.sentiment_handler is not None:
self.sentiment_handler.stream_next(
stream_date=self.cur_time
)
self.strategy.calculate_signals(event)
self.portfolio_handler.update_portfolio_value()
self.statistics.update(event.time, self.portfolio_handler)
elif event.type == EventType.SENTIMENT:
self.strategy.calculate_signals(event)
elif event.type == EventType.SIGNAL:
self.portfolio_handler.on_signal(event)
elif event.type == EventType.ORDER:
self.execution_handler.execute_order(event)
elif event.type == EventType.FILL:
self.portfolio_handler.on_fill(event)
else:
raise NotImplemented("Unsupported event.type '%s'" % event.type)
Gestione degli eventi
La modifica finale al codebase di DataTrader è all’interno dell’oggetto Backtest
. Abbiamo modificato il dispatcher di eventi per gestire i nuovi tipi di oggetti SentimentEvent
che devono essere inviati a un appropriato oggetto Strategy
.
All’interno della gestione degli eventi TICK
o eventi BAR
, sono state aggiunte alcune righe in più. Controlliamo se si tratta di una strategia che contiene il SentimentHandler
o meno, ed in caso positivo sono creati tutti gli oggetti SentimentEvent
oggetti per uno specifico giorno, a cui si fa riferimento nel file di sentiment di Sentdex.
Inoltre, è stata aggiornata la logica di invio di tali eventi all’oggetto Strategy
, che li analizza per generare i segnali:
def stream_next(self, stream_date=None):
"""
Trasmetti il set successivo di valori di sentiment di
un ticker negli oggetti SentimentEvent.
"""
if stream_date is not None:
stream_date_str = stream_date.strftime("%Y-%m-%d")
date_df = self.sent_df.ix[stream_date_str:stream_date_str]
for row in date_df.iterrows():
sev = SentimentEvent(
stream_date, row[1]["Ticker"],
row[1]["Sentiment"]
)
self.events_queue.put(sev)
else:
print("No stream_date provided for stream_next sentiment event!")
Queste sono le modifiche che abbiamo apportato a DataTrader, e sono disponibile nell’ultima versione disponibile su Github , quindi se desideri replicare queste strategie, assicurati di aggiornare la tua copia locale di DataTrader con l’ultima versione.
Codice della strategia di analisi del sentiment
I codici completi per questa strategia e per eseguire il backtest sono disponibili alla fine dell’articolo.
Le modifiche di cui sopra a DataTrader forniscono la struttura necessaria per eseguire una strategia di analisi del sentiment. Tuttavia dobbiamo descrivere come implementare le regole di entrata e di uscita della nostra strategia. A quanto pare, la maggior parte del “lavoro sporco” è stato effettuato nei moduli descritti in precedenza. L’esecuzione della strategia stessa è relativamente semplice.
Iniziamo con l’importazione delle librerie necessarie, tra cui gli oggetti base di DataTrader che in interagiscono con una sottoclasse di Strategy
:
# sentdex_sentiment_strategy.py
from datatrader.event import (SignalEvent, EventType)
from datatrader.strategy.base import AbstractStrategy
La nuova sottoclasse si chiama SentdexSentimentStrategy
. Richiede solo un elenco di ticker su cui agire, un handle per la coda degli eventi, un valore intero sent_buy
per la soglia di sentiment di ingresso e un corrispondente sent_sell
per la soglia di uscita. Entrambi sono specificati successivamente nel codice del backtest.
Inoltre, per la negoziazione è richiesta una quantità base di azioni. Al fine di mantenere la strategia relativamente semplice, il dimensionamento della posizione prevede esclusivamente una quantità di base per ciascun ticker in qualsiasi momento della strategia, sia in acquisto che in vendita. In altre parole, non prevediamo nessuna regolazione dinamica delle dimensioni delle posizioni o dell’allocazione percentuale per i ticker. Questa logica sarebbe una delle prime parti da ottimizzare in una strategia live. Dato che probabilmente il codice di dimensionamento della posizione può distrarre dall’obiettivo principale di questa strategia, cioè il “sentiment”, in questo articolo è stato deciso di mantenerlo semplice.
Infine usiamo self.invested
come dizionario per memorizzare lo stato di negoziazione di ogni ticker. In particolare per ogni ticker è associato un valore booleano True
/ False
, a seconda che una posizione long sia aperta o meno:
class SentdexSentimentStrategy(AbstractStrategy):
"""
Requisiti:
tickers - La lista dei simboli dei ticker
events_queue - La coda degli eventi
sent_buy - soglia di entrata
sent_sell - soglia di uscita
base_quantity - Numero di azioni ogni azione
"""
def __init__(
self, tickers, events_queue,
sent_buy, sent_sell, base_quantity
):
self.tickers = tickers
self.events_queue = events_queue
self.sent_buy = sent_buy
self.sent_sell = sent_sell
self.qty = base_quantity
self.time = None
self.tickers.remove("SPY")
self.invested = dict(
(ticker, False) for ticker in self.tickers
)
Calcolo dei segnali
Come per tutte le sottoclassi di AbstractStrategy
il metodo calculate_signals
contiene le effettive regole di trading basate sugli eventi. In tutte le altre strategie di DataTrader descritte fino ad oggi, questo metodo gestisce gli oggetti BarEvent
o TickEvent
.
In ogni strategia presentata finora, la prima riga di questo metodo controlla sempre il tipo di evento ( if event.type == EventType...
). In questo modo si fornisce una maggiore flessibilità nelle sottoclassi AbstractStrategy
, poiché possono rispondere a eventi arbitrari, non solo a quelli basati sui dati dei prezzi degli asset.
Una volta che l’evento è stato confermato come un SentimentEvent
, il codice controlla se quel particolare ticker è già stato scambiato. In caso contrario, controlla se il sentiment supera il valore intero della soglia di ingresso del sentiment e quindi crea un valore della quantità base di azioni per andare long. Se siamo già a mercato per questo ticker e la soglia del sentiment corrente è inferiore alla soglia di uscita prevista, si chiude la posizione.
La strategia di analisi del sentiment presentata di seguito va solo long. È molto semplice estenderla anche per lo short trading. Un esempio di codice per lo shorting è stato descritto in altre strategie di trading presenti su tradingquant.it, in particolare nel codice descritto per il pairs trading con il filtro di Kalman.
def calculate_signals(self, event):
"""
Calcola i segnali della strategia
"""
if event.type == EventType.SENTIMENT:
ticker = event.ticker
# Segnale Long
if (
self.invested[ticker] is False and
event.sentiment >= self.sent_buy
):
print("LONG %s at %s" % (ticker, event.timestamp))
self.events_queue.put(SignalEvent(ticker, "BOT", self.qty))
self.invested[ticker] = True
# Chiusura segnale
if (
self.invested[ticker] is True and
event.sentiment <= self.sent_sell
):
print("CLOSING LONG %s at %s" % (ticker, event.timestamp))
self.events_queue.put(SignalEvent(ticker, "SLD", self.qty))
self.invested[ticker] = False
Configurazione della strategia
Come per tutte le strategie implementate con DataTrader, esiste un corrispondente file di backtest che specifica i parametri della strategia. È molto simile a molti dei file di backtest precedenti e quindi il codice completo viene riportato solo alla fine di questo articolo.
Le differenze principali sono l’inizializzazione dell’oggetto SentimentHandler
e l’impostazione dei parametri per le soglie di ingresso e di uscita. Questi sono impostati a 6 per l’ingresso e -1 per l’uscita, come indicato nelle regole della strategia descritta in precedenza. È istruttivo (e potenzialmente più redditizio!) ottimizzare questi valori per vari set di ticker.
Il file sentdex_sample.csv
è inserito nella directory CSV_DATA_DIR
di DataTrader, dove di solito risiedono anche i dati sui prezzi. Le date di inizio e di fine riflettono i dati forniti dal file di esempio di Sentdex che contiene le previsioni del sentiment.
..
..
start_date = datetime.datetime(2012, 10, 15)
end_date = datetime.datetime(2016, 2, 2)
..
..
# Uso della strategia di Sentdex Sentiment
sentiment_handler = SentdexSentimentHandler(
config.CSV_DATA_DIR, "sentdex_sample.csv",
events_queue, tickers=tickers,
start_date=start_date, end_date=end_date
)
base_quantity = 2000
sent_buy = 6
sent_sell = -1
strategy = SentdexSentimentStrategy(
tickers, events_queue,
sent_buy, sent_sell, base_quantity
)
strategy = Strategies(strategy, DisplayStrategy())
Per eseguire questa strategia è necessario utilizzare il proprio ambiente virtuale di DataTrader (come sempre) e digitare nel terminale quanto segue, dove l’elenco dei ticker deve essere modificato per adattarsi alla particolare strategia in uso. Assicurarsi di includere SPY se si desidera un confronto con il benchmark.
Il seguente esempio consiste in una selezione di titoli del settore difensivo dell’S&P500, tra cui Boeing, General Dynamics, Lockheed Martin, Northrop-Grumman e Raytheon:
$ python sentdex_sentiment_backtest.py --tickers=BA,GD,LMT,NOC,RTN,SPY
Di seguito un estratto dell’output per il set di azione del settore difensivo:
..
..
---------------------------------
Backtest complete.
Sharpe Ratio: 1.62808089233
Max Drawdown: 0.0977963517677
Max Drawdown Pct: 0.0977963517677
Risultati della strategia
Costi di transazione
Vediamo ora i risultati della strategia di analisi del sentiment al netto dei costi di transazione. I costi sono simulati utilizzando i prezzi fissi delle azioni statunitensi di Interactive Brokers per le azioni del Nord America. Questi sono ragionevolmente rappresentativi dei costi che potrebbero essere applicatei ad una vera strategia di trading.
Sentimento sui titoli S&P500 Tech
La quantità base di condivisioni utilizzate per ciascun ticker è 2.000.

La strategia di analisi del sentiment delle azioni tecnologiche registra un CAGR del 21,0% rispetto al benchmark del 9,4%, utilizzando 2.000 azioni di ciascuno dei cinque ticker. Genera ampi guadagni in soli tre mesi, vale a dire maggio 2013, ottobre 2013 e luglio 2015. Il resto del tempo è per lo più basso o piatto. Notiamo un’ampia durata di prelievo di 318 giorni tra la metà del 2014 e la metà del 2015 e un ampio prelievo massimo giornaliero del 17,23%, rispetto al 13,04% del benchmark.
Inoltre si ha uno Sharpe ratio di 1,12 rispetto al 0,75 del benchmark, ma le prestazioni non sono abbastanza significative da giustificare il passaggio live di questa strategia.
Sentiment sui titoli energetici S&P500
La quantità base di condivisioni utilizzate per ciascun ticker è 5.000.

La strategia di analisi del sentiment di un mix di titoli energetici si comporta in modo abbastanza diverso rispetto al paniere di titoli tecnologici. È molto volatile, registrando mesi con grandi guadagni e altri mesi con grandi perdite. Il suo drawdown massimo è pari al 27,49%, che è sufficiente ad eliminare da qualsiasi ulteriore considerazione come strategia quantitativa profittevole. Inoltre, la strategia sembra perdere ogni efficacia dopo la metà del 2014, quando scende sott’acqua e rimane piatta fino al 2015.
Ha uno scarso Sharpe ratio pari a 0,63 rispetto al benchmark di 0,75. Quindi questa non è una strategia praticabile se portata avanti nella sua forma attuale.
Sentiment sui titoli della difesa S&P500
La quantità base di condivisioni utilizzate per ciascun ticker è 2.000.

Le azioni del settore difesa forniscono una storia diversa rispetto a tecnologia ed energia. La strategia possiede molti mesi di solidi guadagni e ha un Sharpe ratio giornaliero di 1,69 per sole posizioni long. Il suo drawdown massimo di 9,69% è inferiore a quello del benchmark. Ha anche un CAGR interessanti pari al 25,45%. Nonostante questi vantaggi, ha ottenuto la maggior parte dei suoi guadagni nel 2013, con il 2014 e il 2015 che hanno registrato rendimenti molto inferiori.
Conclusioni
Sebbene questa strategia di analisi del sentiment sia sicuramente interessante, c’è ancora molto da fare per metterla in produzione. Dovrebbe essere testata su un periodo molto più ampio. Inoltre, l’aggiunta di posizioni short consentirebbe alla strategia di essere in qualche modo neutrale rispetto al mercato, sperando di ridurre il beta di mercato.
L’ottimizzazione del dimensionamento della posizione e la gestione del rischio sono i passaggi logici successivi e avrebbero probabilmente un effetto significativo sulla performance. Un’ultima modifica consisterebbe nell’aumentare la diversificazione aggiungendo molti più titoli al mix, magari trasversalmente ai settori. Chiaramente vi sono notevoli margini di miglioramento.
Negli articoli successivi molte di queste ottimizzazioni verranno esplorate tramite la modifica degli oggetti PositionSizer
e RiskManager
presenti in DataTrader. Ciò contribuirà ad avvicinare queste strategie all’implementazione per il live-trading.
Codice Completo
Il codice completo presentato in questo articolo, basato sul framework di trading quantitativo event-driven DataTrader, è disponibile nel seguente repository GitHub: https://github.com/tradingquant-it/DataTrader.”
# sentdex_sentiment_strategy.py
from datatrader.event import (SignalEvent, EventType)
from datatrader.strategy.base import AbstractStrategy
class SentdexSentimentStrategy(AbstractStrategy):
"""
Requisiti:
tickers - La lista dei simboli dei ticker
events_queue - La coda degli eventi
sent_buy - soglia di entrata
sent_sell - soglia di uscita
base_quantity - Numero di azioni ogni azione
"""
def __init__(
self, tickers, events_queue,
sent_buy, sent_sell, base_quantity
):
self.tickers = tickers
self.events_queue = events_queue
self.sent_buy = sent_buy
self.sent_sell = sent_sell
self.qty = base_quantity
self.time = None
self.tickers.remove("SPY")
self.invested = dict(
(ticker, False) for ticker in self.tickers
)
def calculate_signals(self, event):
"""
Calcola i segnali della strategia
"""
if event.type == EventType.SENTIMENT:
ticker = event.ticker
# Segnale Long
if (
self.invested[ticker] is False and
event.sentiment >= self.sent_buy
):
print("LONG %s at %s" % (ticker, event.timestamp))
self.events_queue.put(SignalEvent(ticker, "BOT", self.qty))
self.invested[ticker] = True
# Chiusura segnale
if (
self.invested[ticker] is True and
event.sentiment <= self.sent_sell
):
print("CLOSING LONG %s at %s" % (ticker, event.timestamp))
self.events_queue.put(SignalEvent(ticker, "SLD", self.qty))
self.invested[ticker] = False
# sentiment_sentdex_backtest.py
import click
import datetime
import numpy as np
from datatrader import settings
from datatrader.compat import queue
from datatrader.price_parser import PriceParser
from datatrader.price_handler.yahoo_daily_csv_bar import YahooDailyCsvBarPriceHandler
from datatrader.sentiment_handler.sentdex_sentiment_handler import SentdexSentimentHandler
from datatrader.strategy.base import Strategies
from datatrader.position_sizer.naive import NaivePositionSizer
from datatrader.risk_manager.example import ExampleRiskManager
from datatrader.portfolio_handler import PortfolioHandler
from datatrader.compliance.example import ExampleCompliance
from datatrader.execution_handler.ib_simulated import IBSimulatedExecutionHandler
from datatrader.statistics.tearsheet import TearsheetStatistics
from datatrader.trading_session import TradingSession
from sentdex_sentiment_strategy import SentdexSentimentStrategy
def run(config, testing, tickers, filename):
# Impostazione delle variabili necessarie per il backtest
# Informazioni sul Backtest
events_queue = queue.Queue()
csv_dir = config.CSV_DATA_DIR
initial_equity = PriceParser.parse(500000.00)
# Uso del Manager dei Prezzi di Yahoo Daily
start_date = datetime.datetime(2012, 10, 15)
end_date = datetime.datetime(2016, 2, 2)
price_handler = YahooDailyCsvBarPriceHandler(
csv_dir, events_queue, tickers,
start_date=start_date, end_date=end_date
)
# Uso della strategia Sentdex Sentiment trading
sentiment_handler = SentdexSentimentHandler(
config.CSV_DATA_DIR, "sentdex_sample.csv",
events_queue, tickers=tickers,
start_date=start_date, end_date=end_date
)
base_quantity = 2000
sent_buy = 6
sent_sell = -1
strategy = SentdexSentimentStrategy(
tickers, events_queue,
sent_buy, sent_sell, base_quantity
)
strategy = Strategies(strategy)
# Uso di un Position Sizer standard
position_sizer = NaivePositionSizer()
# Uso di Manager di Risk di esempio
risk_manager = ExampleRiskManager()
# Use del Manager di Portfolio di default
portfolio_handler = PortfolioHandler(
PriceParser.parse(initial_equity), events_queue, price_handler,
position_sizer, risk_manager
)
# Uso del componente ExampleCompliance
compliance = ExampleCompliance(config)
# Uso un Manager di Esecuzione che simula IB
execution_handler = IBSimulatedExecutionHandler(
events_queue, price_handler, compliance
)
# Uso delle statistiche di default
title = ["Sentiment Sentdex Strategy"]
statistics = TearsheetStatistics(
config, portfolio_handler, title,
benchmark="SPY"
)
# Settaggio del backtest
backtest = TradingSession(
config, strategy, tickers,
initial_equity, start_date, end_date, events_queue,
price_handler=price_handler,
portfolio_handler=portfolio_handler,
compliance=compliance,
position_sizer=position_sizer,
execution_handler=execution_handler,
risk_manager=risk_manager,
statistics=statistics,
sentiment_handler=sentiment_handler,
title=title, benchmark='SPY'
)
results = backtest.start_trading(testing=testing)
statistics.save(filename)
return results
@click.command()
@click.option('--config', default=settings.DEFAULT_CONFIG_FILENAME, help='Config filename')
@click.option('--testing/--no-testing', default=False, help='Enable testing mode')
@click.option('--tickers', default='SPY', help='Tickers (use comma)')
@click.option('--filename', default='', help='Pickle (.pkl) statistics filename')
def main(config, testing, tickers, filename):
tickers = tickers.split(",")
config = settings.from_file(config, testing)
run(config, testing, tickers, filename)
if __name__ == "__main__":
main()