In questa serie di articoli relativa all’implementazione di un ambiente di backtesting basato sugli eventi abbiamo già descritto la struttura degli event-loop, la gerarchia della classe Event e la componente per la gestione dei dati. In questo articolo si introduce la gerarchia della classe Strategy
. Gli oggetti “strategia” prendono i dati di mercato come input e producono eventi di tipo Signal Trading come output.
Un oggetto Strategy
include tutti i calcoli sui dati di mercato che generano segnali advisory per l’oggetto Portfolio
. In questa fase di sviluppo dell’ambiente di backtesting event-driven non introduciamo i concetto di indicatore o filtro, come quelli che sono usati nell’analisi tecnica classica. Questi sono tuttavia buoni candidati per la creazione di una gerarchia di classi, ma vanno oltre lo scopo di questo articolo.
Gestione delle Strategia
La gerarchia della classe Strategy è relativamente semplice poiché consiste in una classe base astratta con un singolo metodo puro virtuale per generare oggetti SignalEvent
. Per creare la gerarchia della strategia è necessario importare NumPy, Pandas, l’oggetto Queue, i strumenti della classe base astratta e SignalEvent
:
# strategy.py
import datetime
import numpy as np
import pandas as pd
import queue
from abc import ABCMeta, abstractmethod
from event import SignalEvent
La classe base astratta Strategy
definisce semplicemente il metodo virtuale calculate_signals
. Questo metodo sarà usato nelle classi derivate per gestire la creazione di oggetti SignalEvent
a seconda degli aggiornamenti dei dati di mercato:
# strategy.py
class Strategy(object):
"""
Strategy è una classe base astratta che fornisce un'interfaccia per
tutti i successivi oggetti (ereditati) di gestione della strategia.
L'obiettivo di un oggetto (derivato da) Strategy è generare un oggetto
Signal per specifici simboli basati sugli input di Bars
(OLHCVI) generati da un oggetto DataHandler.
Questo è progettato per funzionare sia con dati storici che in tempo reale
quindi l'oggetto Strategy è agnostico rispetto all'origine dati,
poiché ricava le tuple di barre da un oggetto Queue (coda).
"""
__metaclass__ = ABCMeta
@abstractmethod
def calculate_signals(self):
"""
Fornisce il meccanismo per calcolare la lista di segnali.
"""
raise NotImplementedError("Should implement calculate_signals()")
Come mostrato nel codice precedente, la definizione della classe astratta Strategy
è semplice.
Un primo esempio di sottoclasse dell’oggetto Strategy
è la creazione della classe BuyAndHoldStrategy
, che implementa la classica strategia buy and hold. Questa strategia compra un asset ad una certo istante e lo conserva all’interno del portafoglio. Quindi viene generato un solo segnale per ogni asset.
Il costruttore (__init__
) prevede, come input, il gestore dei dati di mercato e l’oggetto della coda degli eventi Events
:
# strategy.py
class BuyAndHoldStrategy(Strategy):
"""
Questa è una strategia estremamente semplice che va LONG su tutti
i simboli non appena viene ricevuta una barra. Non uscirà mai da una posizione.
Viene utilizzato principalmente come meccanismo di test per la classe Strategy
nonché un benchmark con cui confrontare le altre strategie.
"""
def __init__(self, bars, events):
"""
Inizializza la strategia "buy and hold".
Parametri:
bars - L'oggetto DataHandler che fornisce le informazioni sui prezzi
events - L'oggetto Event Queue (coda di eventi).
"""
self.bars = bars
self.symbol_list = self.bars.symbol_list
self.events = events
# Quando il segnale "buy & hold" viene inviato, questi sono impostati a True
self.bought = self._calculate_initial_bought()
Nell’inizializzazione di BuyAndHoldStrategy
, l’attributo bought
viene instanziato con un dictionary (una struttura dati nativa di Python) di chiavi per ogni simbolo, tutte impostate con False. Una volta che un asset è andato “long”, la relativa chiave viene impostata su True. In sostanza ciò consente alla Strategia di sapere su quali asset è “sul mercato” o meno:
# strategy.py
def _calculate_initial_bought(self):
"""
Aggiunge le chiavi di tutti i simboli al dizionario "bought"
e li imposta su False.
"""
bought = {}
for s in self.symbol_list:
bought[s] = False
return bought
Il metodo virtuale calculate_signals
viene concretamente implementato in questa classe. Il metodo scorre su tutti i simboli nell’elenco dei simboli e recupera la barra OLHCV più recente dal gestore dei dati di mercato. Quindi controlla se quel simbolo è stato “comprato” (cioè se abbiamo una posizione aperta a mercato per questo simbolo o no) e, in caso negativo, crea un singolo oggetto SignalEvent
. Quest’ultimo viene poi inserito nella coda degli eventi e il dizionario bought
viene correttamente aggiornato con True per questo specifico simbolo:
# strategy.py
def calculate_signals(self, event):
"""
For "Buy and Hold" generiamo un singolo segnale per simbolo
e quindi nessun segnale aggiuntivo. Ciò significa che siamo
costantemente LONG sul mercato a partire dalla data di
inizializzazione della strategia.
Parametri
event - Un oggetto MarketEvent.
"""
if event.type == 'MARKET':
for s in self.symbol_list:
bars = self.bars.get_latest_bars(s, N=1)
if bars is not None and bars != []:
if self.bought[s] == False:
# (Symbol, Datetime, Type = LONG, SHORT or EXIT)
signal = SignalEvent(bars[0][0], bars[0][1], 'LONG')
self.events.put(signal)
self.bought[s] = True
Questa semplice strategia è sufficiente per dimostrare la natura di una gerarchia basata su eventi. Negli articoli successivi considereremo strategie più sofisticate come il pairs trading.
Infine nel prossimo articolo considereremo come creare la gerarchia della classe Portfolio
che tenga traccia delle nostre posizioni con un profitto e una perdita (“PnL”)
Per il codice completo riportato in questo articolo, utilizzando il modulo di backtesting event-driven DataBacktest si può consultare il seguente repository di github:
https://github.com/datatrading-info/DataBacktest