Oltre ai “soliti” trucchi relativi all’arbitraggio statistico, al trend-following e all’analisi fondamentale, molti fondi quantitativi (e trader retail!) si impegnano in tecniche di elaborazione del linguaggio naturale (NLP) per costruire strategie sistematiche. Tali tecniche rientrano nell’ambito della Sentiment Analysis .
In questo articolo descriviamo ed implementiamo un gruppo di strategie di trading quantitativo che si basano una serie di segnali di sentiment generati da un’API del fornitore. Questi segnali forniscono una scala intera che va da -3 (“sentimento negativo più forte”) a +6 (“sentimento positivo più forte”), associata a una data e a un simbolo ticker, che può essere utilizzata come soglia di ingresso e di uscita in un backtesting event-driven.
Una sfida chiave nello sviluppo di un tale sistema è l’integrazione degli eventi che rappresentano il sentiment, che sono archiviati in un file CSV di righe “datetime-ticker-sentiment”, in un sistema di trading basato sugli eventi di solito progettato per operare direttamente sui dati dei prezzi.
Questo articolo inizia con una breve discussione sulle modalità di esecuzione dell’analisi di sentiment, e alla descrizione tecnica delle API del fornitore e dei file di esempio. L’articolo continua descrivendo la funzionalità di sentiment aggiunta recentemente a DataTrader, incluso il relativo codice Python. Infine l’articolo analizza i risultati di tre separati backtest della strategia del sentiment applicata ai titoli S&P500 nei settori della tecnologia, della difesa e dell’energia.
Analisi del sentiment
L’obiettivo dell’analisi del sentiment è, generalmente, quello di prendere grandi quantità di dati “non strutturati” (come post di blog, articoli di giornale, rapporti di ricerca, tweet, video, immagini, ecc.) e utilizzare tecniche di PNL per quantificare il “sentiment” positivo o negativo su determinati asset.
Per le azioni, in particolare, equivale spesso a un’analisi di apprendimento automatico statistico del linguaggio utilizzato, individuando se contiene un fraseggio rialzista o ribassista. Questo fraseggio può essere quantificato in termini di forza del sentiment, che si traduce in valori numerici. In altre parole, i valori positivi riflettono un sentimento rialzista mentre i valori negativi rappresentano un sentimento ribassista.
Negli ultimi anni c’è stata una crescita costante di fornitori di analisi di sentiment, tra cui Sentdex , PsychSignal e Accern . Tutti utilizzano tecniche proprietarie per identificare “entità” all’interno di dati alternativi e quindi associare un punteggio di sentimento con timestamp a qualsiasi informazione estratta. Queste informazioni possono quindi essere aggregate in un periodo di tempo (ad esempio un giorno), al fine di produrre tuple di date-entity-sentiment. Tali tuple costituiscono la base di un segnale di trading.
Descrivere le metodologie di analisi di grandi quantità di “big data” e quantificare il sentimento esula dallo scopo di questo articolo. Uno strumento di analisi del sentimento pronto per la produzione end-to-end è una grande impresa di ingegneria del software. Quindi i trader retails preferisco spesso ottenere questi segnali dai fornitori e utilizzarli come parte di un portafoglio più ampio di segnali quantitativi per formare una strategia.
Questo articolo descrive una strategia di trading basata sui dati del sentiment di un particolare fornitore, cioè Sentdex, e le modalità di generazione di segnali long-only grazie a questi dati.
Le API di Sentdex e file di esempio
Sentdex fornisce un’API che consente di scaricare i dati sul sentiment per un’ampia varietà di strumenti finanziari. I dati sono disponibili con timeframe di un minuto o di un giorno. Maggiori dettagli sulla loro offerta (a pagamento) possono essere trovati nella loro pagina API .
L’API non sarà discussa in questo articolo poiché è un prodotto a pagamento ed è utile principalmente come generatore di eventi di paper trading o di trading reale. Dato che questo articolo riguarda le strategie di backtesting su dati storici, è più appropriato utilizzare un file statico, archiviato localmente per rappresentare i dati del sentiment.
Fortuitamente, Sentdex fornisce un file di dati campione (che può essere scaricato qui) che contiene quasi cinque anni di segnali di sentiment, a risoluzione giornaliera, per molti dei componenti dell’S&P500.
Di seguito viene presentato 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
Da notare 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 utilizzati nelle tre simulazioni descritte in questo articolo.
La strategia di trading
La complessità di questa implementazione deriva principalmente dagli adeguamenti al framework open-source di backtesting event-driven DataTrader, piuttosto che dalla strategia stessa, che è abbastanza semplice dopo aver generato il segnale del sentiment. La strategia è stata volutamente mantenuta semplice e vi sono ampi margini di modifica e ottimizzazione, che saranno oggetto di articoli successivi.
In questo esempio la strategia è solo long, ma è facilmente modificabile per includere posizioni short. La strategia determina le soglie di entrata e di uscita, che sono poi rispettivamente usate per aprire o chiudere posizioni long.
Implementiamo tre strategie, identiche ad eccezione della selezione dei titoli su cui operano. 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:
- Entrata long su un ticker se il suo valore di sentiment raggiunge +6
- Chiusura di una posizione su un ticker se il suo valore di sentiment raggiunge -1
Non esiste un’allocazione percentuale per ciascuna azione. Usiamo una quantità fissa di azioni per ciascuna allocazione durante tutta la strategia. Tuttavia, questa quantità fissa viene modificata per ciascuno dei tre settori di cui sopra.
Una modifica ovvia sarebbe quella di creare un investimento pesato in dollari che regoli dinamicamente l’allocazione in base alle dimensioni del capitale. Tuttavia, in questo articolo il dimensionamento della posizione è semplificato per facilitare la comprensione la logica base di generazione dell’evento di sentiment.
Dati
Per attuare questa strategia è necessario disporre di dati sui prezzi OHLCV giornalieri per le azioni nel periodo coperto da questi backtest. In questo articolo vengono eseguite tre simulazioni separate, ciascuna contenente un gruppo di cinque titoli dell’S&P500. Il primo gruppo è costituito da titoli tecnologici/di base di consumo:
Ticker | Nome | Periodo | Collegamento |
---|---|---|---|
MSFT | Microsoft | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
AMZN | Amazon.com | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
GOOG | Alphabet | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
AAPL | Apple | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
IBM | International Business Machines | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
Il secondo gruppo è costituito da una serie di titoli di difesa, anch’essi dell’S&P500:
Ticker | Nome | Periodo | Collegamento |
---|---|---|---|
BA | The Boeing Company | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
LMT | Lockheed Martin | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
NOC | Northrop Grumman | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
GD | General Dynamics | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
RTN | Raytheon | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
L’ultimo set di ticker è costituito da titoli energetici, ancora una volta dall’S&P500:
Ticker | Nome | Periodo | Collegamento |
---|---|---|---|
XOM | Exxon Mobile | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
CVX | Chevron | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
SLB | Schlumberger | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
OSSI | Occidental Petroleum | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
COP | ConocoPhilips | 15 ottobre 2012 – 2 febbraio 2016 | Yahoo Finanza |
Questi dati dovranno essere inseriti nella directory specificata dal file delle impostazioni di QSTrader se si desidera replicare i risultati.
Inoltre, il file di esempio dell’API Sentdex dovrà essere posizionato nella stessa directory dei dati di DataTrader.
Implementazione Python
Gestione del sentiment con DataTrader
Per eseguire il backtest delle strategie basate sul sentiment, è necessario capire come incorporare i “segnali” del sentiment nel backtest.
L’attuale modello di DataTrader per il backtesting prevede un “motore” per la gestione della risposta agli eventi. Questo è implementato in un ciclo while
di grandi dimensioni che itera su tutti gli oggetti TickEvent
e BarEvent
. Il codice permette di concatenare qualsiasi dataset di serie finanziare storiche di ticker, archiviati in un database o in file CSV, e quindi iterare riga per riga, con ogni riga è un elemento di un DataFrame pandas formato da un TickEvent
o da un BarEvent
per ogni ticker.
Il precedente codice che implementava questa logica era il seguente:
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)
Questo codice continua il ciclo 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
È 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)
La modifica finale al codebase di DataTrader è all’interno dell’oggetto Backtest
. In particolare abbiamo modificato il dispatcher di eventi per gestire i nuovi tipi di oggetti SentimentEvent
che devono essere inviati a un appropriato oggetto Strategy
.
In particolare, 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.
La prima cosa consiste nell’importare le 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. In una strategia live questa sarebbe una delle prime parti da ottimizzare. 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
)
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.
Quindi la strategia 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 datatrading.info, 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
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 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. Inoltre, ha 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.
Il 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.
Sebbene questa strategia 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
Per il codice completo riportato in questo articolo, utilizzando il framework open-source di backtesting event-driven DataTrader si può consultare il seguente repository di github:
https://github.com/datatrading-info/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()