Ambiente di Backtesting Even-Driven con Python – Parte VIII

È passato un po ‘di tempo da quando abbiamo la serie di articoli realtivi ad un ambiente di backtesting basato sugli eventi, che abbiamo iniziato a discutere in questo articolo. Nella Parte VI è stato descritto come implementare un modello di ExecutionHandler funzionante per una simulazione storica di backtesting. In questo si vuole implementare il gestore dell’API di Interactive Brokers in modo da poter utilizzare l’ExecutionHandler per il live trading. In precedenza abbiamo visto come come scaricare Trader Workstation e creare un account demo di Interactive Brokers e su come creare un’interfaccia di base verso l’API IB usando IbPy. Questo articolo descrivere come collegare l’interfaccia IbPy all’interno di un sistema event-driven, in modo tale che, quando accoppiato con un feed di dati di mercato real-time, costituirà la base per un sistema di esecuzione automatizzato. L’idea alla base della classe IBExecutionHandler consiste nel ricevere istanze OrderEvent dalla coda degli eventi ed eseguirli direttamente verso l’API di ordine di Interactive Brokers utilizzando la libreria IbPy. La classe gestirà anche i messaggi “Server Response” inviati in rispota dalla stessa API. In questa fase, l’unica azione intrapresa sarà creare le corrispondenti istanze FillEvent corrispondenti che verranno quindi ritrasferite nella coda degli eventi. La stessa classe può essere facilmente resa più complessa, includendo una logica di ottimizzazione dell’esecuzione e una sofisticata gestione degli errori. Tuttavia, in questa fase è opportuno mantenerla relativamente semplice in modo che si possa capire le principali funzionalità ed estenderle nella direzione che si adatta al tuo particolare stile di trading.

Implementazione in Python

Come sempre, il primo passo è creare il file Python e importare le librerie necessarie. Il file si chiama ib_execution.py e risiede nella stessa directory degli altri file event-driven. Importiamo le necessarie librerie per la gestione della data / ora, gli oggetti IbPy e i specifici oggetti Event gestiti da IBExecutionHandler:
# 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
A questo punto è necessario definire la classe IBExecutionHandler. Innanzitutto il costruttore __init__ richiede in input la coda degli eventi. Prevende inoltre la specifica di order_routing, che viene impostata a “SMART” come default. Nel caso l’exchange abbia specifici requisiti, questi possono essere specificati in questo costruttore. Inoltre la currency predefinita è stata impostata sui Dollari USA. All’interno del metodo si crea un dizionario fill_dict, necessario per l’utilizzo nella generazione delle istanze di FillEvent. Si prevede anche un oggetto di connessione tws_conn per archiviare le informazioni di connessione verso l’API di Interactive Brokers. Inoltre si crea un order_id iniziale, che tiene traccia di tutti gli ordini successivi per evitare duplicati. Infine si registra il gestore dei messaggi (che sarà definito dettagliatamente più avanti):
# ib_execution.py

class IBExecutionHandler(ExecutionHandler):
    """
    Handles order execution via the Interactive Brokers
    API, for use against accounts when trading live
    directly.
    """

    def __init__(self, events, 
                 order_routing="SMART", 
                 currency="USD"):
        """
        Initialises the IBExecutionHandler instance.
        """
        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()

L’API di IB utilizza un sistema di eventi basato sui messaggi che consente alla nostra classe di rispondere in modo specifico a determinati messaggi, in analogia allo stesso ambiente di backtesing event-driven stesso. Non si include nessuna gestione degli errori reali (a fini di brevità), ad eccezione dell’output al terminale tramite il metodo _error_handler.

Il metodo _reply_handler, d’altra parte, viene utilizzato per determinare se è necessario creare un’istanza FillEvent. Il metodo verifica se è stato ricevuto un messaggio “openOrder” e controlla se è presente una voce fill_dict relativa a questo particolare orderId. In caso contrario, ne viene creata una.

Inoltre, se il metodo verifica la presenta di un messaggio “orderStatus” e nel caso quel particolare messaggio indichi che un ordine è stato eseguito, allora richiama la funzione create_fill per creare un FillEvent. Si invia anche un messaggio al terminale per scopi di logging / debug:

# ib_execution.py
    
    def _error_handler(self, msg):
        """
        Handles the capturing of error messages
        """
        # Currently no error handling.
        print "Server Error: %s" % msg

    def _reply_handler(self, msg):
        """
        Handles of server replies
        """
        # Handle open order orderId processing
        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)
        # Handle 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)
Il seguente metodo, 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):
        """
        Connect to the Trader Workstation (TWS) running on the
        usual port of 7496, with a clientId of 10.
        The clientId is chosen by us and we will need 
        separate IDs for both the execution connection and
        market data connection, if the latter is used elsewhere.
        """
        tws_conn = ibConnection()
        tws_conn.connect()
        return tws_conn
Per tenere traccia degli ordini separati (ai fini del tracciamento degli eseguiti) viene utilizzato il metodo create_initial_order_id. E’ stato impostato su “1”, ma un approccio più sofisticato prevede la gestione della query IB per conoscere ed utilizzare l’ultimo ID disponibile. Si può sempre reimpostare l’ID dell’ordine corrente dell’API tramite il pannello Trader Workstation –> Configurazione globale –> Impostazioni API:
# ib_execution.py
    
    def create_initial_order_id(self):
        """
        Creates the initial order ID used for Interactive
        Brokers to keep track of submitted orders.
        """
        # There is scope for more logic here, but we
        # will use "1" as the default for now.
        return 1
Il seguente metodo, 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):
        """
        Register the error and server reply 
        message handling functions.
        """
        # Assign the error handling function defined above
        # to the TWS connection
        self.tws_conn.register(self._error_handler, 'Error')

        # Assign all of the server reply messages to the
        # reply_handler function defined above
        self.tws_conn.registerAll(self._reply_handler)
Come descritto nel precedente tutorial relativo all’uso di IbPy, si deve creare un’istanza di Contract ed associarla a un’istanza di Order, che verrà inviata 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):
        """
        Create a Contract object defining what will
        be purchased, at which exchange and in which currency.

        symbol - The ticker symbol for the contract
        sec_type - The security type for the contract ('STK' is 'stock')
        exch - The exchange to carry out the contract on
        prim_exch - The primary exchange to carry out the contract on
        curr - The currency in which to purchase the contract
        """
        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
Il metodo 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):
        """
        Create an Order object (Market/Limit) to go long/short.

        order_type - 'MKT', 'LMT' for Market or Limit orders
        quantity - Integral number of assets to order
        action - 'BUY' or 'SELL'
        """
        order = Order()
        order.m_orderType = order_type
        order.m_totalQuantity = quantity
        order.m_action = action
        return order
Per evitare la duplicazione delle istanze di FillEvent per un particolare ID ordine, si utilizza un dizionario chiamato fill_dict per memorizzare le chiavi che corrispondono a particolari ID ordine. Quando è stato generato un eseguito, la chiave “fill” di una voce per un particolare ID ordine è impostata su True. Nel caso si riceva un successivo messaggio “Server Response” da IB che dichiara che un ordine è stato eseguito (ed è un messaggio duplicato) non si creerà un nuovo eseguito. Il seguente metodo create_fill_dict_entry implementa questa logica:
# ib_execution.py
    
    def create_fill_dict_entry(self, msg):
        """
        Creates an entry in the Fill Dictionary that lists 
        orderIds and provides security information. This is
        needed for the event-driven behaviour of the IB
        server message behaviour.
        """
        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):
        """
        Handles the creation of the FillEvent that will be
        placed onto the events queue subsequent to an order
        being filled.
        """
        fd = self.fill_dict[msg.orderId]

        # Prepare the fill data
        symbol = fd["symbol"]
        exchange = fd["exchange"]
        filled = msg.filled
        direction = fd["direction"]
        fill_cost = msg.avgFillPrice

        # Create a fill event object
        fill = FillEvent(
            datetime.datetime.utcnow(), symbol, 
            exchange, filled, direction, fill_cost
        )

        # Make sure that multiple messages don't create
        # additional fills.
        self.fill_dict[msg.orderId]["filled"] = True

        # Place the fill event onto the event queue
        self.events.put(fill_event)
Dopo aver implementato tutti i metodi precedenti, resta solamente da sviluppare il metodo execute_order della classe base astratta ExecutionHandler. Questo metodo esegue effettivamente il posizionamento dell’ordine tramite l’API di IB. Si verifica innanzitutto che l’evento ricevuto con questo metodo sia realmente un OrderEvent e quindi prepara gli oggetti Contract e Order con i rispettivi parametri. Una volta che sono stati creati entrambi, il metodo placeOrder dell’oggetto di connessione viene richiamato con associato a order_ID. È estremamente importante chiamare il metodo time.sleep(1) per garantire che l’ordine sia effettivamente trasmesso ad IB. La rimozione di questa linea può causare comportamenti incoerenti dell’API, e perfino malfunzionamenti! Infine, si incrementa l’ID ordine al fine di evitare la duplicazione degli ordini:
# ib_execution.py
    
    def execute_order(self, event):
        """
        Creates the necessary InteractiveBrokers order object
        and submits it to IB via their API.

        The results are then queried in order to generate a
        corresponding Fill object, which is placed back on
        the event queue.

        Parameters:
        event - Contains an Event object with order information.
        """
        if event.type == 'ORDER':
            # Prepare the parameters for the asset order
            asset = event.symbol
            asset_type = "STK"
            order_type = event.order_type
            quantity = event.quantity
            direction = event.direction

            # Create the Interactive Brokers contract via the 
            # passed Order event
            ib_contract = self.create_contract(
                asset, asset_type, self.order_routing,
                self.order_routing, self.currency
            )

            # Create the Interactive Brokers order via the 
            # passed Order event
            ib_order = self.create_order(
                order_type, quantity, direction
            )

            # Use the connection to the send the order to IB
            self.tws_conn.placeOrder(
                self.order_id, ib_contract, ib_order
            )

            # NOTE: This following line is crucial.
            # It ensures the order goes through!
            time.sleep(1)

            # Increment the order ID for this session
            self.order_id += 1
Questa classe costituisce la base per gestione dell’esecuzione verso Interactive Brokers e può essere utilizzata al posto del gestore dell’esecuzione simulata, che è adatto solo per il backtesting. Prima che il gestore di IB possa essere utilizzato è necessario creare un gestore del feed dei dati di mercato in tempo reale che deve sostituire il gestore del feed dei dati storici utilizzato nel backtesting. Con questo approccio è possibile riutilizzare la maggior parte delle componenti di un sistema di backtesting per un sistema live, in modo da garantire che il codice “swap out” sia ridotto al minimo e quindi assicurare un comportamento simile, se non identico, tra i due sistemi.

Ambiente di Backtesting Even-Driven con Python – Parte VII

Nel precedente articolo della serie “Ambiente di Backtesting Event-Driven” è stato descritta la gerarchia della classe ExecutionHandler. In questo articolo si introduce l’implementazione delle metriche per misurare le prestazioni di una strategia usando la curva equity DataFrame precedentemente costruita nell’oggetto Portfolio.

Misurare le Performance

Abbiamo già descritto il Sharpe Ratio in un precedente articolo. In quell’articolo il Sharpe Ratio (annualizzato) viene calcolato tramite:

\(\begin{eqnarray*} S_A = \sqrt{N} \frac{\mathbb{E}(R_a – R_b)}{\sqrt{\text{Var} (R_a – R_b)}} \end{eqnarray*}\)

Dove \(R_a\) è il flusso dei rendimenti della curva equity e \(R_b\) è un indice di riferimento, come uno specifico tasso di interesse o un indice azionario. Il massimo drawdown e la durata del drawdown sono due ulteriori misure che gli investitori utilizzano per valutare il rischio in un portafoglio. Il primo rappresenta è più grande discesa, la correzione, da un precedente massimo relativo o massimo assoluto, della curva equity, mentre il secondo è definito come il numero di periodi di trading in cui si verifica. In questo articolo si implementa il Sharpe Ratio, il drawdown massimo e la durata del drawdown come misure delle prestazioni del portafoglio da utilizzare nella suite di Backtesting Event-Driven sviluppato in Python.

Implementazione

Il primo passo è creare un nuovo file performance.py, che memorizzi le funzioni per calcolare il Sharpe Ratio e le informazioni sul drawdown. Come per la maggior parte delle classi che prevedono elevati carichi computanzionali, abbiamo bisogno di importare NumPy e Pandas:
# performance.py

import numpy as np
import pandas as pd

Il Sharpe Ratio è una misura del rischio/rendimento (in realtà è una delle tante!) e prevede un singolo parametro, cioè il numero di periodi da considerare per il ridimensionamento al valore annualizzato.

Di solito questo valore è impostato su 252, ovvero il numero di giorni di negoziazione (mercati aperti) negli Stati Uniti in un anno. Tuttavia, ad esempio, se la strategia apre e chiude posizioni all’interno di un’ora, si deve regolare lo Sharpe per annualizzarlo correttamente. Pertanto, è necessario impostare il periods come 252 * 6.5 = 1638, ovvero il numero di ore di trading statunitensi in un anno. Se si effettua trading sul minuto, questo fattore deve essere impostato come 252 * 6.5 * 60 = 98280.

La funzione create_sharpe_ratio opera su un oggetto Serie di Pandas che abbiamo chiamato returns e calcola semplicemente il rapporto tra la media dei rendimenti percentuali del periodo e le deviazioni standard dei rendimenti percentuali ridimensionato in base al fattore periods:

# performance.py

def create_sharpe_ratio(returns, periods=252):
    """
    Create the Sharpe ratio for the strategy, based on a 
    benchmark of zero (i.e. no risk-free rate information).

    Parameters:
    returns - A pandas Series representing period percentage returns.
    periods - Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc.
    """
    return np.sqrt(periods) * (np.mean(returns)) / np.std(returns)
Mentre il Sharpe Ratio indica il livello di rischio (definito dalla deviazione standard del patrimonio) per unità di rendimento, il “drawdown” è definito come la distanza tra un massimo relativo e un minimo relativo lungo una curva equity. La funzione create_drawdowns calcola sia il drawdown massimo che la durata massima di drawdown. Il primo è la discesa più elevata tra un massimo e minimo relativi, mentre il secondo è definito come il numero di periodi in cui questa discesa si verifica. E’ necessario prestare molta attenzione nell’interpretazione della durata del drawdown in quanto questo fattore identifica i periodi di trading e quindi non è direttamente traducibile in un’unità temporale come “giorni”. La funzione inizia creando due oggetti Serie di Pandas che rappresentano il drawdown e la durata di ogni “barra” di trading. Quindi viene stabilito l’attuale high water mark (HWM) determinando se la curva di equty supera tutti i picchi precedenti. Il drawdown è quindi semplicemente la differenza tra l’attuale HWM e la curva di equity. Se questo valore è negativo, la durata viene aumentata per ogni barra che si verifica fino al raggiungimento del prossimo HWM. La funzione restituisce quindi semplicemente il massimo di ciascuna delle due serie:
# performance.py

def create_drawdowns(equity_curve):
    """
    Calculate the largest peak-to-trough drawdown of the PnL curve
    as well as the duration of the drawdown. Requires that the 
    pnl_returns is a pandas Series.

    Parameters:
    pnl - A pandas Series representing period percentage returns.

    Returns:
    drawdown, duration - Highest peak-to-trough drawdown and duration.
    """

    # Calculate the cumulative returns curve 
    # and set up the High Water Mark
    # Then create the drawdown and duration series
    hwm = [0]
    eq_idx = equity_curve.index
    drawdown = pd.Series(index = eq_idx)
    duration = pd.Series(index = eq_idx)

    # Loop over the index range
    for t in range(1, len(eq_idx)):
        cur_hwm = max(hwm[t-1], equity_curve[t])
        hwm.append(cur_hwm)
        drawdown[t]= hwm[t] - equity_curve[t]
        duration[t]= 0 if drawdown[t] == 0 else duration[t-1] + 1
    return drawdown.max(), duration.max()
Al fine di utilizzare queste misure di performance, si ha bisogno di un metodo per calcolarle dopo che è stato effettuato un backtest, cioè quando è disponibile un’adeguata curva di equity! E’ necessario inoltre associare tale metodo a una particolare gerarchia di oggetti. Dato che le misure di rendimento sono calcolate a partire dal portafoglio, ha senso inserire i calcoli delle prestazioni all’interno di un metodo nella gerarchia della classe Portfolio, che è stata descritta in questo articolo. Il primo compito è aprire portfolio.py e importare le funzioni di performance:
# portfolio.py

..  # Other imports

from performance import create_sharpe_ratio, create_drawdowns
Poiché Portfolio è una classe base astratta, si deve associare un metodo a una delle sue classi derivate, che in questo caso corrisponde a NaivePortfolio. Quindi si crea un metodo chiamato output_summary_stats che elabora la curva equity del portafoglio per generare le informazioni relative allo Sharpe e drawdown. Il metodo è semplice. Utilizza semplicemente le due misure di performance e le applica direttamente al DataFrame Pandas relativo alla curva equity, restituendo le statistiche come una lista di tuple in un formato “user-friendly”:
# portfolio.py

..
..

class NaivePortfolio(object):

    ..
    ..

    def output_summary_stats(self):
        """
        Creates a list of summary statistics for the portfolio such
        as Sharpe Ratio and drawdown information.
        """
        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)
        max_dd, dd_duration = create_drawdowns(pnl)

        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)]
        return stats
Chiaramente questa è un’analisi molto semplice delle prestazioni per un portfolio. Non prende in considerazione l’analisi a livello di singolo trade o altre misure del rapporto rischio / rendimento. Tuttavia è molto semplice da estendere, aggiungendo più metodi in performance.py e quindi incorporandoli in output_summary_stats come richiesto.

Ambiente di Backtesting Even-Driven con Python – Parte VI

In questo articolo continua lo sviluppo di un ambiente di backtesting basato sugli eventi, utilizzando Python. Nel precedente articolo è stata approfondita la gerarchia della classe Portfolio che permette di gestire le posizioni correnti, generare ordini di trading e tenere traccia dei profitti e delle perdite (PnL). Il porossimo passo è implementare l’esecuzione di questi ordini, creando una gerarchia di classi che rappresenta un meccanismo per la simulazione della gestione degli ordini e, infine, collegarsi ad un broker o ad altri intermediari di mercato. L’ExecutionHandler descritto in questo articolo è estremamente semplice, poiché esegue tutti gli ordini al prezzo corrente di mercato. Questo è altamente irrealistico, ma serve come una buona base di partenza da perfezionare successivamente. Come per le precedenti gerarchie di classi astratte di base, bisogna importare le proprietà e i decoratori necessari dalla libreria abc. Inoltre, è necessario importare FillEvent e OrderEvent:
# execution.py

import datetime
import Queue

from abc import ABCMeta, abstractmethod

from event import FillEvent, OrderEvent
La classe ExecutionHandler è simile alle precedenti classi astratte di base e ha solamente un metodo virtuale, execute_order:
# execution.py

class ExecutionHandler(object):
    """
    The ExecutionHandler abstract class handles the interaction
    between a set of order objects generated by a Portfolio and
    the ultimate set of Fill objects that actually occur in the
    market. 

    The handlers can be used to subclass simulated brokerages
    or live brokerages, with identical interfaces. This allows
    strategies to be backtested in a very similar manner to the
    live trading engine.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def execute_order(self, event):
        """
        Takes an Order event and executes it, producing
        a Fill event that gets placed onto the Events queue.

        Parameters:
        event - Contains an Event object with order information.
        """
        raise NotImplementedError("Should implement execute_order()")

Per testare le strategie, è necessario simulare il modo in cui un trade verrà eseguito. L’implementazione più semplice possibile consiste nell’ipotizzare che tutti gli ordini siano stati eseguiti al prezzo corrente di mercato per qualsiasi quantità. Questo è chiaramente estremamente irrealistico e gran parte del lavoro per aumentare il grado di realismo del backtesting consiste nel progettare dei modelli avanzati per simulare lo slippage e il market-impact.

Da notare che all’interno del metodo FillEvent viene passato un valore pari a None per l’attributo fill_cost (vedere la penultima riga in execute_order) come abbiamo descritto per il costo di esecuzione nell’oggetto NaivePortfolio descritto nell’articolo precedente. In un’implementazione più realistica, si utilizza il valore di dati di mercato “attuali” per ottenere un costo di esecuzione più realistico.

Ho inoltre utilizzato ARCA come exchange, anche se per i scopi di backtesting questo è puramente un segnaposto. In un ambiente di esecuzione dal vivo questo attributo diventa molto più importante:

# execution.py

class SimulatedExecutionHandler(ExecutionHandler):
    """
    The simulated execution handler simply converts all order
    objects into their equivalent fill objects automatically
    without latency, slippage or fill-ratio issues.

    This allows a straightforward "first go" test of any strategy,
    before implementation with a more sophisticated execution
    handler.
    """
    
    def __init__(self, events):
        """
        Initialises the handler, setting the event queues
        up internally.

        Parameters:
        events - The Queue of Event objects.
        """
        self.events = events

    def execute_order(self, event):
        """
        Simply converts Order objects into Fill objects naively,
        i.e. without any latency, slippage or fill ratio problems.

        Parameters:
        event - Contains an Event object with order information.
        """
        if event.type == 'ORDER':
            fill_event = FillEvent(datetime.datetime.utcnow(), event.symbol,
                                   'ARCA', event.quantity, event.direction, None)
            self.events.put(fill_event)

Questo conclude le gerarchie di classi necessarie per implementare un ambiente di backtesting basato sugli eventi.

Nel prossimo articolo si descriverà come calcolare un insieme di metriche sul rendimento per la strategia oggetto del backtesting.

Ambiente di Backtesting Even-Driven con Python – Parte V

Nel precedente articolo relativo al backtesting basato sugli eventi abbiamo descritto come costruire la gerarchia della classe Strategy. Le strategie, per come sono state definite, sono utilizzate per generare signals, che sono l’input di un oggetto portfolio al fine di decidere se inviare o meno gli orders. Inizialmente, è naturale creare una classe astratta di base (ABC) del Portfolio da cui si ereditano tutte le sottoclassi successive. Questo articolo descrive un oggetto NaivePortfolio che tiene traccia delle posizioni all’interno di un portafoglio e genera ordini di una quantità fissa di azioni in base ai segnali. Oggetti di portfolio avanzati includono strumenti di gestione del rischio più sofisticati e saranno oggetto di articoli successivi.

Monitoraggio della Posizione e Gestione degli Ordini

Il sistema di gestione degli ordini del portafoglio è probabilmente la componente più complessa di un ambiente backtesting basato sugli eventi. Questa componente ha il compito di tenere traccia di tutte le attuali posizioni aperte sul mercato e del valore di mercato di queste posizioni (note come “holdings”). Questa è semplicemente una stima del valore di liquidazione della posizione ed è derivata in parte dalla funzione di gestione dei dati del backtester. Oltre alle posizioni e alla gestione degli holdings, il portafoglio deve essere a conoscenza dei fattori di rischio e delle tecniche di dimensionamento delle posizioni al fine di ottimizzare gli ordini inviati ad un broker o verso altre forme di accesso al mercato. In analogia alla gerarchia della classe Event, un oggetto Portfolio deve essere in grado di gestire oggetti SignalEvent, generare oggetti OrderEvent e interpretare oggetti FillEvent per aggiornare le posizioni. Pertanto non sorprende che gli oggetti portfolio siano spesso il componente più importante dei sistemi event-driven, in termini di righe di codice (LOC).

Implementazione

Si crea un nuovo file portfolio.py e si importa le librerie necessarie. Queste sono le stesse della maggior parte delle altre implementazioni delle classe astratte di base. In particolare si importa la funzione floor dalla libreria math per generare dimensioni di ordine con valori interi, ed inoltre si importano gli oggetti FillEvent e OrderEvent poiché il Portfolio gestisce entrambi.
# 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 si crea una classe ABC per il Portfolio e si implementano due metodi virtuali update_signal e update_fill. Il primo elabora i nuovi segnali di trading che vengono 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):
    """
    The Portfolio class handles the positions and market
    value of all instruments at a resolution of a "bar",
    i.e. secondly, minutely, 5-min, 30-min, 60 min or EOD.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        raise NotImplementedError("Should implement update_signal()")

    @abstractmethod
    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        raise NotImplementedError("Should implement update_fill()")

L’argomento principale di questo articolo è la classe NaivePortfolio. Questa classe è progettata per gestire il dimensionamento delle posizioni e gli holdings correnti, ma esegue gli ordini di compravendita in modo “stupido”, semplicemente inviandoli direttamente al broker con una dimensione fissa e predeterminata, indipendentemente dalla liquidità detenuta. Queste sono tutte ipotesi irrealistiche, ma aiutano a delineare come funziona un sistema di gestione degli ordini di portafoglio (OMS) basato sugli eventi.

La NaivePortfolio richiede un valore del capitale iniziale, che ho impostato sul valore predefinito di 100.000 USD. Richiede anche una data di inizio.

Il portfolio contiene gli attributi all_positions e current_positions. Il primo memorizza un elenco di tutte le precedenti posizioni registrate ad uno specifico timestamp di un evento di dati di mercato. Una posizione è semplicemente la quantità dell’asset. Le posizioni negative indicano che l’asset è stato ridotto. Il secondo attributo memorizza un dizionario contenente le posizioni correnti per l’ultimo aggiornamento dei dati di mercato.

Oltre agli attributi delle posizioni, il portafoglio memorizza gli holdings, che descrivono il valore corrente di mercato delle posizioni detenute. Il “Valore corrente di mercato” indica, in questo caso, il prezzo di chiusura ottenuto dalla barra OLHCV corrente, che è chiaramente un’approssimazione, ma è abbastanza accettabile in questo momento. L’attributo all_holdings memorizza la lista storica di tutte gli holding dei simboli, mentre current_holdings memorizza il dizionario aggiornato di tutti i valori di holdings dei simboli.

# portfolio.py

class NaivePortfolio(Portfolio):
    """
    The NaivePortfolio object is designed to send orders to
    a brokerage object with a constant quantity size blindly,
    i.e. without any risk management or position sizing. It is
    used to test simpler strategies such as BuyAndHoldStrategy.
    """
    
    def __init__(self, bars, events, start_date, initial_capital=100000.0):
        """
        Initialises the portfolio with bars and an event queue. 
        Also includes a starting datetime index and initial capital 
        (USD unless otherwise stated).

        Parameters:
        bars - The DataHandler object with current market data.
        events - The Event Queue object.
        start_date - The start date (bar) of the portfolio.
        initial_capital - The starting capital 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 semplicemente un dizionario per ogni simbolo, e per ciascuno imposta il valore a zero e quindi aggiunge una chiave datetime, inserendo infine questo oggetto in un elenco. Usa una comprensione del dizionario, che è simile alla comprensione di una lista:
# portfolio.py

    def construct_all_positions(self):
        """
        Constructs the positions list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        return [d]
Il metodo 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):
        """
        Constructs the holdings list using the start_date
        to determine when the time index will begin.
        """
        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_all_holdings(self):
        """
        Constructs the holdings list using the start_date
        to determine when the time index will begin.
        """
        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]
Ad ogni “battito” o impulso del sistema, cioè ogni volta che vengono richiesti nuovi dati di mercato dall’oggetto DataHandler, il portfolio deve aggiornare il valore corrente di mercato di tutte le posizioni detenute. In uno scenario di trading live queste informazioni possono essere scaricate e analizzate direttamente dal broker, ma per un’implementazione di backtesting è necessario calcolare manualmente questi valori. Sfortunatamente non esiste una cosa come il “valore corrente di mercato” a causa degli spread bid / ask e delle problematiche di liquidità. Quindi è necessario stimarlo moltiplicando la quantità del bene detenuta per un determinato “prezzo”. L’approccio utilizzato in questo esempio prevede di utilizzare il prezzo di chiusura dell’ultima barra ricevuta. Per una strategia intraday questo è relativamente realistico. Per una strategia quotidiana questo è meno realistico in quanto il prezzo di apertura può differire molto dal prezzo di chiusura. Il metodo update_timeindex gestisce il monitoraggio dei nuovi holdings. In particolare ricava i prezzi più recenti dal gestore dei dati di mercato e crea un nuovo dizionario di simboli per rappresentare le posizioni correnti, impostando le posizioni “nuove” uguali alle posizioni “correnti”. Questi vengono modificati solo quando si riceva un FillEvent, che viene successivamente gestito dal portfolio. Il metodo quindi aggiunge questo insieme di posizioni correnti alla lista all_positions. Successivamente, le posizioni vengono aggiornate in modo simile, con l’eccezione che il valore di mercato viene ricalcolato moltiplicando il conteggio delle posizioni correnti con il prezzo di chiusura dell’ultima barra (self.current_positions [s] * bars [s] [0] [ 5]). Infine, i nuovi holdings sono agggiunti a all_holdings:
# portfolio.py

    def update_timeindex(self, event):
        """
        Adds a new record to the positions matrix for the current 
        market data bar. This reflects the PREVIOUS bar, i.e. all
        current market data at this stage is known (OLHCVI).

        Makes use of a MarketEvent from the events queue.
        """
        bars = {}
        for sym in self.symbol_list:
            bars[sym] = self.bars.get_latest_bars(sym, N=1)

        # Update positions
        dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dp['datetime'] = bars[self.symbol_list[0]][0][1]

        for s in self.symbol_list:
            dp[s] = self.current_positions[s]

        # Append the current positions
        self.all_positions.append(dp)

        # Update holdings
        dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dh['datetime'] = bars[self.symbol_list[0]][0][1]
        dh['cash'] = self.current_holdings['cash']
        dh['commission'] = self.current_holdings['commission']
        dh['total'] = self.current_holdings['cash']

        for s in self.symbol_list:
            # Approximation to the real value
            market_value = self.current_positions[s] * bars[s][0][5]
            dh[s] = market_value
            dh['total'] += market_value

        # Append the current holdings
        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):
        """
        Takes a FilltEvent object and updates the position matrix
        to reflect the new position.

        Parameters:
        fill - The FillEvent object to update the positions with.
        """
        # 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

        # Update positions list with new quantities
        self.current_positions[fill.symbol] += fill_dir*fill.quantity
Il corrispondente update_holdings_from_fill è simile al metodo precedente ma aggiorna i valori di holdings. Per simulare il costo di riempimento, il metodo seguente non utilizza il costo associato a FillEvent. Perchè questo approccio? In parole povere, in un ambiente di backtesting il costo di riempimento è in realtà sconosciuto e quindi deve essere stimato. Quindi il costo di riempimento è impostato sul “prezzo corrente di mercato” (il prezzo di chiusura dell’ultima barra). Le posizioni per un particolare simbolo vengono quindi impostate per essere uguali al costo di riempimento moltiplicato per la quantità del trade. Una volta che il costo di riempimento è noto, gli holdings correnti, i contanti e i valori totali possono essere aggiornati. Anche la commissione cumulativa viene aggiornata:
# portfolio.py

    def update_holdings_from_fill(self, fill):
        """
        Takes a FillEvent object and updates the holdings matrix
        to reflect the holdings value.

        Parameters:
        fill - The FillEvent object to update the holdings with.
        """
        # 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

        # Update holdings list with new quantities
        fill_cost = self.bars.get_latest_bars(fill.symbol)[0][5]  # Close price
        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)
Qui viene implementato il metodo virtuale update_fill della classe ABC Portfolio . Esegue semplicemente i due metodi precedenti, update_positions_from_fill e update_holdings_from_fill, che sono già stati discussi sopra:
 # portfolio.py

def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        if event.type == 'FILL':
            self.update_positions_from_fill(event)
            self.update_holdings_from_fill(event)

L’oggetto Portfolio, 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):
        """
        Simply transacts an OrderEvent object as a constant quantity
        sizing of the signal object, without risk management or
        position sizing considerations.

        Parameters:
        signal - The SignalEvent signal information.
        """
        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):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        if event.type == 'SIGNAL':
            order_event = self.generate_naive_order(event)
            self.events.put(order_event)
L’ultimo metodo di 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):
        """
        Creates a pandas DataFrame from the all_holdings
        list of dictionaries.
        """
        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

L’oggetto Portfolio è la componente più complessa dell’intero sistema di backtesting basato sugli eventi. L’implementazione è complessa, quindi in questo articolo 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.

Nel prossimo articolo considereremo 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.

Ambiente di Backtesting Even-Driven con Python – Parte IV

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. 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 is an abstract base class providing an interface for
    all subsequent (inherited) strategy handling objects.

    The goal of a (derived) Strategy object is to generate Signal
    objects for particular symbols based on the inputs of Bars 
    (OLHCVI) generated by a DataHandler object.

    This is designed to work both with historic and live data as
    the Strategy object is agnostic to the data source,
    since it obtains the bar tuples from a queue object.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def calculate_signals(self):
        """
        Provides the mechanisms to calculate the list of signals.
        """
        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):
    """
    This is an extremely simple strategy that goes LONG all of the 
    symbols as soon as a bar is received. It will never exit a position.

    It is primarily used as a testing mechanism for the Strategy class
    as well as a benchmark upon which to compare other strategies.
    """

    def __init__(self, bars, events):
        """
        Initialises the buy and hold strategy.

        Parameters:
        bars - The DataHandler object that provides bar information
        events - The Event Queue object.
        """
        self.bars = bars
        self.symbol_list = self.bars.symbol_list
        self.events = events

        # Once buy & hold signal is given, these are set to True
        self.bought = self._calculate_initial_bought()
Nell’inizializzazione di BuyAndHoldStrategy, l’attributo bought viene instanziato con un dictionary (una struttura data 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):
        """
        Adds keys to the bought dictionary for all symbols
        and sets them to 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" we generate a single signal per symbol
        and then no additional signals. This means we are 
        constantly long the market from the date of strategy
        initialisation.

        Parameters
        event - A MarketEvent object. 
        """
        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”)

Ambiente di Backtesting Even-Driven con Python – Parte III

Nei due articoli precedenti della serie abbiamo introdotto i concetti base di un sistema di backtesting basato sugli eventi e la gerarchia di classi per l’oggetto Event. In questo articolo vediamo come vengono utilizzati i dati di mercato, sia in un contesto storico di backtesting sia per l’esecuzione del live trading.

Uno dei nostri obiettivi con un sistema di trading basato sugli eventi è di minimizzare la duplicazione del codice tra l’elemento di backtesting e l’elemento di esecuzione live. Idealmente, è ottimale utilizzare la stessa metodologia di generazione del segnale e le stesse componenti di gestione del portafoglio sia per i test storici che per trading reale. Affinché questo funzioni, l’oggetto Strategy, che genera i segnali, e l’oggetto Portfolio, che fornisce gli ordini basati su di essi, devono utilizzare un’identica interfaccia verso un feed di dati finanziari, sia per la versione di backtesting che per quella live.

Questo requisito motiva la necessità di una gerarchia di classi basata sull’oggetto DataHandler, che l’implementa un’interfaccia, disponibile a tutte le sottoclassi, per fornire i dati di mercato alle rimanenti componenti del sistema. In questo modo, si può intercambiare qualsiasi sottoclasse di “fornitura” di dati finanziari senza influenzare la strategia o il calcolo del portafoglio.

Esempi di sottoclassi specifiche possono includere HistoricCSVDataHandler, QuandlDataHandler, SecuritiesMasterDataHandler, InteractiveBrokersMarketFeedDataHandler ecc. In questo tutorial descriviamo solamente la creazione di un gestore CSV di dati storici, che caricherà da un CSV i dati intraday per le azioni nel formato Open-Low-High-Close- Volume-OpenInterest. Questo può quindi essere usato per alimentare con i dati “candela-per-candela” le classi Strategy e Portfolio per ogni heartbeat (o impulso) del sistema, evitando così i bias di look-ahead.

Il primo compito è importare le librerie necessarie. Nello specifico, si includono Pandas e gli strumenti astratti della classe base. Dato che DataHandler genera MarketEvents, si importa anche event.py come descritto nel tutorial precedente:

# data.py

import datetime
import os, os.path
import pandas as pd

from abc import ABCMeta, abstractmethod

from event import MarketEvent

La classe DataHandler è una classe base astratta (ABC), cioè è impossibile istanziare direttamente un’istanza. Possono essere istanziate solamente le sottoclassi. Con questo approccio la classe ABC fornisce un’interfaccia che tutte le successive sottoclassi di DataHandler devono rispettare, garantendo in tal modo la compatibilità con altre classi che comunicano con esse.

Facciamo uso della proprietà __metaclass__ per far sapere a Python che questa è una classe ABC. Inoltre usiamo il decoratore @abstractmethod per far sapere a Python che il metodo verrà sovrascritto dalle sottoclassi (questo è identico a un metodo virtuale puro di C++).

I due metodi fondamentali sono get_latest_bars e update_bars. Il primo restituisce le ultime barre N a partire dal timestamp dall’attuale “impulso”, necessarie per far eseguire le elaborazioni previste nelle classi Strategy. Il secondo metodo fornisce un meccanismo di “alimentazione a goccia” per posizionare le informazioni OLHCV su una nuova struttura dati in modo da evitare la distorsione lookahead. Si noti che verranno sollevate eccezioni se si verifica un tentativo di istanziazione della classe:

# data.py

class DataHandler(object):
    """
    DataHandler is an abstract base class providing an interface for
    all subsequent (inherited) data handlers (both live and historic).

    The goal of a (derived) DataHandler object is to output a generated
    set of bars (OLHCVI) for each symbol requested. 

    This will replicate how a live strategy would function as current
    market data would be sent "down the pipe". Thus a historic and live
    system will be treated identically by the rest of the backtesting suite.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def get_latest_bars(self, symbol, N=1):
        """
        Returns the last N bars from the latest_symbol list,
        or fewer if less bars are available.
        """
        raise NotImplementedError("Should implement get_latest_bars()")

    @abstractmethod
    def update_bars(self):
        """
        Pushes the latest bar to the latest symbol structure
        for all symbols in the symbol list.
        """
        raise NotImplementedError("Should implement update_bars()")
Dopo aver definito la classe DataHandler, il passo successivo è creare un gestore per i file CSV di dati storici. In particolare, HistoricCSVDataHandler prenderà più file CSV, uno per ciascun simbolo, e li convertirà in un DataFrame di Panda. Il gestore dati richiede alcuni parametri, ovvero una coda di eventi su cui inviare informazioni di MarketEvent, il percorso assoluto dei file CSV e un elenco di simboli. Di seguito l’inizializzazione della classe:
# data.py

class HistoricCSVDataHandler(DataHandler):
    """
    HistoricCSVDataHandler is designed to read CSV files for
    each requested symbol from disk and provide an interface
    to obtain the "latest" bar in a manner identical to a live
    trading interface. 
    """

    def __init__(self, events, csv_dir, symbol_list):
        """
        Initialises the historic data handler by requesting
        the location of the CSV files and a list of symbols.

        It will be assumed that all files are of the form
        'symbol.csv', where symbol is a string in the list.

        Parameters:
        events - The Event Queue.
        csv_dir - Absolute directory path to the CSV files.
        symbol_list - A list of symbol strings.
        """
        self.events = events
        self.csv_dir = csv_dir
        self.symbol_list = symbol_list

        self.symbol_data = {}
        self.latest_symbol_data = {}
        self.continue_backtest = True       

        self._open_convert_csv_files()
Questa funzione prevede quindi di aprire i file nel formato “SYMBOL.csv” dove il SYMBOL è il simbolo del ticker. Il formato dei file corrisponde a quello fornito da DTN IQFeed, ma si può facilmente modificare per gestire formati di dati aggiuntivi. L’apertura dei file è gestita dal seguente metodo _open_convert_csv_files. Uno dei vantaggi dell’uso della libreria Pandas come archivio all’interno di HistoricCSVDataHandler è la possibilità di unire gli indici di tutti i simboli tracciati. Ciò consente di correggere i punti di dati mancanti in avanti, indietro o interpolati all’interno di questi spazi, in modo tale che i ticker possano essere confrontati “candela-per-candela”. Questo è necessario, ad esempio, per strategie di mean-reverting. Si noti l’uso dei metodi union e reindex quando si combina gli indici di tutti i simboli:
# data.py

    def _open_convert_csv_files(self):
        """
        Opens the CSV files from the data directory, converting
        them into pandas DataFrames within a symbol dictionary.

        For this handler it will be assumed that the data is
        taken from DTN IQFeed. Thus its format will be respected.
        """
        comb_index = None
        for s in self.symbol_list:
            # Load the CSV file with no header information, indexed on date
            self.symbol_data[s] = pd.io.parsers.read_csv(
                                      os.path.join(self.csv_dir, '%s.csv' % s),
                                      header=0, index_col=0, 
                                      names=['datetime','open','low','high','close','volume','oi']
                                  )

            # Combine the index to pad forward values
            if comb_index is None:
                comb_index = self.symbol_data[s].index
            else:
                comb_index.union(self.symbol_data[s].index)

            # Set the latest symbol_data to None
            self.latest_symbol_data[s] = []

        # Reindex the dataframes
        for s in self.symbol_list:
            self.symbol_data[s] = self.symbol_data[s].reindex(index=comb_index, method='pad').iterrows()
Il metodo _get_new_bar crea un generatore python per fornire una versione formattata dei dati OLCHV. Questo significa che le successive chiamate al metodo genereranno una nuova barra fino al raggiungimento della fine dei dati del simbolo:
# data.py

    def _get_new_bar(self, symbol):
        """
        Returns the latest bar from the data feed as a tuple of 
        (sybmbol, datetime, open, low, high, close, volume).
        """
        for b in self.symbol_data[symbol]:
            yield tuple([symbol, datetime.datetime.strptime(b[0], '%Y-%m-%d %H:%M:%S'), 
                        b[1][0], b[1][1], b[1][2], b[1][3], b[1][4]])       
Il primo metodo astratto di DataHandler da implementare è get_latest_bars. Questo metodo fornisce semplicemente un elenco delle ultime barre N dalla struttura latest_symbol_data. L’impostazione N = 1 consente il recupero della barra corrente (racchiusa nell’elenco):
# data.py

    def get_latest_bars(self, symbol, N=1):
        """
        Returns the last N bars from the latest_symbol list,
        or N-k if less available.
        """
        try:
            bars_list = self.latest_symbol_data[symbol]
        except KeyError:
            print "That symbol is not available in the historical data set."
        else:
            return bars_list[-N:]
Il secondo metodo astratto, update_bars, genera semplicemente un MarketEvent che viene aggiunto alla coda, e aggiunge le ultime barre a latest_symbol_data:
# data.py

    def update_bars(self):
        """
        Pushes the latest bar to the latest_symbol_data structure
        for all symbols in the symbol list.
        """
        for s in self.symbol_list:
            try:
                bar = self._get_new_bar(s).next()
            except StopIteration:
                self.continue_backtest = False
            else:
                if bar is not None:
                    self.latest_symbol_data[s].append(bar)
        self.events.put(MarketEvent())

A questo punto abbiamo implementato un oggetto derivato da DataHandler, che viene utilizzato dai restanti componenti per tenere traccia dei dati di mercato. Gli oggetti Strategy, Portfolio ed ExecutionHandler richiedono gli attuali dati di mercato, quindi ha senso centralizzare questa gestione al fine di evitare la duplicazione del codice e di possibili bug.

Nel prossimo articolo vedremo la gerarchia della classe Strategy e descriviamo come una strategia può essere progettata per gestire più simboli, generando così più SignalEvents per l’oggetto Portfolio.

Ambiente di Backtesting Even-Driven con Python – Parte II

Nel precedente articolo abbiamo introdotto la struttura base di un ambiente di backtesting event-driven. Il resto di questa serie di articoli si concentrerà su ciascuna delle gerarchie di classi  che costituiscono il sistema generale. In questo articolo i descrivono gli Eventi e come questi possono essere usati per scambiare informazioni tra gli oggetti.

Come discusso nel precedente articolo, il sistema di trading utilizza due loop: uno esterno e uno interno. Il loop interno gestisce l’acquisizione degli eventi da una coda in memoria, e il loro smistamento verso gli specific componenti che gestiscono la conseguente azione.

In questo sistema ci sono quattro tipi di eventi:

  • MarketEvent: viene attivato quando il loop esterno inizia un nuovo “impulso”. Si verifica quando l’oggetto DataHandler riceve un nuovo aggiornamento dei dati di mercato per tutti i simboli che sono attualmente monitorati. Viene utilizzato per attivare l’oggetto Strategy che genera nuovi segnali di trading. L’oggetto Event contiene semplicemente un’identificazione che si tratta di un evento di mercato, senza altre strutture.
  • SignalEvent: l’oggetto Strategy utilizza i dati di mercato per creare nuovi SignalEvent. SignalEvents contiene un simbolo ticker, il timestamp di quando è stato generato e una direzione (long o short). I SignalEvents sono utilizzati dall’oggetto Portfolio come consigli su come effettuare i trade.
  • OrderEvent: quando un oggetto Portfolio riceve i SignalEvents, questi sono valutati nel contesto generale del portfolio, in termini di rischio e dimensionamento della posizione. Questo processo genera un OrderEvents che verrà inviato a un ExecutionHandler.
  • FillEvent: dopo la ricezione di un OrderEvent, l’ExecutionHandler deve eseguire l’ordine. Una volta che un ordine è stato eseguito, genera un FillEvent, che descrive il costo di acquisto o vendita, nonché i costi di transazione, come le commissioni o lo slippage.

La classe genitore è chiamata Event. È una classe base e non fornisce alcuna funzionalità o interfaccia specifica. Nelle implementazioni successive gli oggetti Event svilupperanno una maggiore complessità e quindi stiamo definendo la progettazione di tali sistemi creando una gerarchia di classi.

# event.py

class Event(object):
    """
    Event is base class providing an interface for all subsequent 
    (inherited) events, that will trigger further events in the 
    trading infrastructure.   
    """
    pass
La classe MarketEvent eredita da Event e prevede semplicemente di autoidentificare l’evento come di tipo “MARKET”.
# event.py

class MarketEvent(Event):
    """
    Handles the event of receiving a new market update with 
    corresponding bars.
    """

    def __init__(self):
        """
        Initialises the MarketEvent.
        """
        self.type = 'MARKET'
La classe SignalEvent richiede un simbolo ticker, un timestamp della generazione e una direzione per “avvisare” un oggetto Portfolio.
# event.py

class SignalEvent(Event):
    """
    Handles the event of sending a Signal from a Strategy object.
    This is received by a Portfolio object and acted upon.
    """
    
    def __init__(self, symbol, datetime, signal_type):
        """
        Initialises the SignalEvent.

        Parameters:
        symbol - The ticker symbol, e.g. 'GOOG'.
        datetime - The timestamp at which the signal was generated.
        signal_type - 'LONG' or 'SHORT'.
        """
        
        self.type = 'SIGNAL'
        self.symbol = symbol
        self.datetime = datetime
        self.signal_type = signal_type
La classe OrderEvent è leggermente più complessa rispetto alla SignalEvent poiché contiene un campo quantità, oltre alle già citate proprietà di SignalEvent. La quantità è determinata dai vincoli del portafoglio. Inoltre, OrderEvent ha un metodo print_order(), utilizzato per inviare le informazioni alla console, se necessario.
# event.py

class OrderEvent(Event):
    """
    Handles the event of sending an Order to an execution system.
    The order contains a symbol (e.g. GOOG), a type (market or limit),
    quantity and a direction.
    """

    def __init__(self, symbol, order_type, quantity, direction):
        """
        Initialises the order type, setting whether it is
        a Market order ('MKT') or Limit order ('LMT'), has
        a quantity (integral) and its direction ('BUY' or
        'SELL').

        Parameters:
        symbol - The instrument to trade.
        order_type - 'MKT' or 'LMT' for Market or Limit.
        quantity - Non-negative integer for quantity.
        direction - 'BUY' or 'SELL' for long or short.
        """
        
        self.type = 'ORDER'
        self.symbol = symbol
        self.order_type = order_type
        self.quantity = quantity
        self.direction = direction

    def print_order(self):
        """
        Outputs the values within the Order.
        """
        print "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" % \
            (self.symbol, self.order_type, self.quantity, self.direction)
La classe FillEvent è l’evento con la maggiore complessità. Contiene un timestamp di quando è stato effettuato un ordine, il simbolo dell’ordine e l’exchange su cui è stato eseguito, la quantità di azioni negoziate, il prezzo effettivo dell’acquisto e la commissione sostenuta. La commissione viene calcolata utilizzando le commissioni di Interactive Brokers. Per l’azionario statunitenze questa commissione è pari ad un minimo di 1 USD per ordine, con una tariffa fissa di 0,005 USD per azione.
# event.py

class FillEvent(Event):
    """
    Encapsulates the notion of a Filled Order, as returned
    from a brokerage. Stores the quantity of an instrument
    actually filled and at what price. In addition, stores
    the commission of the trade from the brokerage.
    """

    def __init__(self, timeindex, symbol, exchange, quantity, 
                 direction, fill_cost, commission=None):
        """
        Initialises the FillEvent object. Sets the symbol, exchange,
        quantity, direction, cost of fill and an optional 
        commission.

        If commission is not provided, the Fill object will
        calculate it based on the trade size and Interactive
        Brokers fees.

        Parameters:
        timeindex - The bar-resolution when the order was filled.
        symbol - The instrument which was filled.
        exchange - The exchange where the order was filled.
        quantity - The filled quantity.
        direction - The direction of fill ('BUY' or 'SELL')
        fill_cost - The holdings value in dollars.
        commission - An optional commission sent from IB.
        """
        
        self.type = 'FILL'
        self.timeindex = timeindex
        self.symbol = symbol
        self.exchange = exchange
        self.quantity = quantity
        self.direction = direction
        self.fill_cost = fill_cost

        # Calculate commission
        if commission is None:
            self.commission = self.calculate_ib_commission()
        else:
            self.commission = commission

    def calculate_ib_commission(self):
        """
        Calculates the fees of trading based on an Interactive
        Brokers fee structure for API, in USD.

        This does not include exchange or ECN fees.

        Based on "US API Directed Orders":
        https://www.interactivebrokers.com/en/index.php?f=commission&p=stocks2
        """
        full_cost = 1.3
        if self.quantity <= 500:
            full_cost = max(1.3, 0.013 * self.quantity)
        else: # Greater than 500
            full_cost = max(1.3, 0.008 * self.quantity)
        full_cost = min(full_cost, 0.5 / 100.0 * self.quantity * self.fill_cost)
        return full_cost
Nel prossimo articolo della serie vedremo come sviluppare la gerarchia della classe DataHandler che permetta sia backtesting storico che il live trading, tramite la stessa classe di interfaccia.

Ambiente di Backtesting Even-Driven con Python – Parte I

Negli ultimi mesi abbiamo descritto su DataTrading come testare le varie strategie di trading utilizzando Python e Pandas. La natura vettoriale di Pandas permette elaborazioni estremamente rapide su set di dati di grandi dimensioni siano. Tuttavia, gli approcci di backtesting vettorializzato che abbiamo studiato finora presentano alcune criticità nelle modalità di simulazione dell’esecuzione dei trade. In questa serie di articoli discuteremo un approccio più realistico alla simulazione della strategia, usando Python per costruire un ambiente di backtesting basato sugli eventi.

Software basati sugli Eventi

Prima di approfondire lo sviluppo di questo ambiente di backtesting, è necessario introdurre i concetti base dei sistemi basati sugli eventi. I videogiochi forniscono un classico caso d’uso di tale tipologia di software e sono un semplice esempio da studiare. Un videogioco ha più componenti che interagiscono tra loro in un ambiente real-time con elevati frame-rate. Questo viene gestito grazie all’esecuzione di una serie di calcoli all’interno di un ciclo “infinito” noto come event-loop o game-loop.

Ad ogni tick del game-loop viene chiamata una funzione che si occupa di acquisire l’ultimo evento, quest’ultimo è stato generato da una corrispondente azione precedente all’interno del loop. A seconda della natura dell’evento, come ad esempio la pressione di un tasto o il clic del mouse, sono eseguite specifiche azioni che interromperanno il ciclo o genereranno alcuni eventi aggiuntivi.

Di seguito un esempio del pseudo-codice dell’event-loop:

while True:  # Run the loop forever
    new_event = get_new_event()   # Get the latest event

    # Based on the event type, perform an action
    if new_event.type == "LEFT_MOUSE_CLICK":
        open_menu()
    elif new_event.type == "ESCAPE_KEY_PRESS":
        quit_game()
    elif new_event.type == "UP_KEY_PRESS":
        move_player_north()
    # ... and many more events

    redraw_screen()   # Update the screen to provide animation
    tick(50)   # Wait 50 milliseconds

Il codice verifica continuamente la presenza di nuovi eventi e, in caso affermativo, esegue azioni a seconda del tipo di eventi. In particolare, permette l’illusione di un sistema con risposta in tempo reale dato che il codice viene continuamente ripetuto e quindi si ha una verifica continua degli eventi. Ovviamente, questo è esattamente quello di cui abbiamo bisogno per effettuare simulazioni di trading ad alta frequenza.

Perchè abbiamo bisogno di un Backtesting Event-Driven

I sistemi basati sugli eventi offrono molti vantaggi rispetto a un approccio vettorializzato:

  • Riutilizzo del codice – un backtester basato sugli eventi, in base alla progettazione, può essere utilizzato sia per il backtesting storico sia per il live trading con una minima modifica dei componenti. Questo non è possibile per i backtesting vettorizzati dove tutti i dati devono essere disponibili contemporaneamente per poter effettuare analisi statistiche.
  • Bias di Look-Ahead – con un backtesting basato sugli eventi non vi è alcun bias di previsione perché l’acquisizione dei dati finanziari è gestita come un “evento” su cui si deve effettuare specifiche azioni. In questo modo è possibile un ambiente di backtestering event-driven è alimentato  “instante dopo instante” con i dati di mercato, replicando il comportamento di un sistema di gestione degli ordini e del portafoglio.
  • Realismo – I backtesting basati sugli eventi consentono una significativa personalizzazione del modalità di esecuzione degli ordini e dei costi di transazione sostenuti. È semplice gestire il market-order e il limit-order, oltre al market-on-open (MOO) e al market-on-close (MOC), poiché è possibile costruire un gestore di exchange personalizzato.

 

Sebbene i sistemi basati sugli eventi siano dotati di numerosi vantaggi, essi presentano due importanti svantaggi rispetto ai più semplici sistemi vettorizzati. Innanzitutto sono molto più complessi da implementare e testare. Ci sono più “parti mobili” che causano una maggiore probabilità di introdurre bug. Per mitigare questa criticità è possibile implementare una  metodologia di testing del software, come il test-driven development.

In secondo luogo, hanno tempi di esecuzione più lenti da eseguire rispetto a un sistema vettorializzato. Le operazioni vettoriali ottimizzate non possono essere utilizzate quando si eseguono calcoli matematici. Discuteremo dei modi per superare queste limitazioni negli articoli successivi.

Struttura di un sistema di Backtesting Event-Driven

Per applicare un approccio event-driven a un sistema di backtesting è necessario definire i componenti base (o oggetti) che gestiscono compiti specifici:

  • Event: l’Event è la classe fondamentale di un sistema event-driven. Contiene un attributto “tipo” (ad esempo, “MARKET”, “SIGNAL”, “ORDER” o “FILL”) che determina come viene gestito uno specifico evento all’interno dell’event-loop.
  • Event Queue: la Coda degli Eventi è un oggetto Python Queue che memorizza tutti gli oggetti della sotto-classe Event generati dal resto del software.
  • DataHandler: il DataHandler è una classe base astratta (ABC) che presenta un’interfaccia per la gestione di dati storici o del mercato in tempo reale. Fornisce una significativa flessibilità in quanto i moduli della strategia e del portfolio possono essere riutilizzati da entrambi gli approcci. Il DataHandler genera un nuovo MarketEvent ad loop del sistema (vedi sotto).
  • Strategy: anche la Strategy è una classe ABC e presenta un’interfaccia per elaborare i dati di mercato e generare i corrispondenti SignalEvents, che vengono infine utilizzati dall’oggetto Portfolio. Un SignalEvent contiene un simbolo ticker, una direzione (LONG or SHORT) e un timestamp.
  • Portfolio: si tratta di una classe ABC che implementa la gestione degli ordini associata alle posizioni attuali e future di una strategia. Svolge anche la gestione del rischio in tutto il portafoglio, compresa l’esposizione settoriale e il dimensionamento delle posizioni. In un’implementazione più sofisticata, questo potrebbe essere delegato a una classe RiskManagement. La classe Portfolio prende un SignalEvents dalla coda e genera uno o più OrderEvents che vengono aggiunti alla coda.
  • ExecutionHandler: l’ExecutionHandler simula una connessione a una società di intermediazione o broker. Il suo compito consiste nel prelevare gli OrderEvents dalla coda ed eseguirli, tramite un approccio simulato o una connessione reale verso il broker. Una volta eseguiti gli ordini, il gestore crea i FillEvents, che descrivono ciò che è stato effettivamente scambiato, comprese le commissioni, lo spread e lo slippage (se modellato).
  • Loop – Tutti questi componenti sono racchiusi in un event-loop che gestisce correttamente tutti i tipi di eventi, indirizzandoli al componente appropriato.

Questo è il modello base di un motore di trading. Vi è un significativo margine di espansione, in particolare per quanto riguarda l’utilizzo del portafoglio. Inoltre, i diversi modelli di costo delle transazioni possono essere implementati utilizzando una propria gerarchia di classi. In questa fase però introdurrebbe una complessità inutile all’interno di questa serie di articoli, quindi al momento non viene approfondita ulteriormente. Nei tutorial successivi si potrà pensare di  espandere il sistema per includere ulteriori gradi di realismo.

Di seguito potete trovare il codice Python che mostra  come il backtester funziona in pratica. Ci sono due loop nidificati all’interno del codice. Il loop esterno è usato per dare al backtester un impulso, o ritmo. Nel live trading questa è la frequenza con cui vengono acquisiti i nuovi dati di mercato. Per le strategie di backtesting questo non è strettamente necessario poiché il backtester utilizza i dati di mercato forniti in forma di drip-feed (vedi la riga bars.update_bars ()).

Il ciclo interno gestisce effettivamente gli eventi dall’oggetto Queue. Gli Eventi specifici sono delegati al rispettivo componente e successivamente vengono aggiunti nuovi eventi alla coda. Quando la coda degli eventi è vuota, si riprende il ciclo esterno:

# Declare the components with respective parameters
bars = DataHandler(..)
strategy = Strategy(..)
port = Portfolio(..)
broker = ExecutionHandler(..)

while True:
    # Update the bars (specific backtest code, as opposed to live trading)
    if bars.continue_backtest == True:
        bars.update_bars()
    else:
        break
    
    # Handle the events
    while True:
        try:
            event = events.get(False)
        except Queue.Empty:
            break
        else:
            if event is not None:
                if event.type == 'MARKET':
                    strategy.calculate_signals(event)
                    port.update_timeindex(event)

                elif event.type == 'SIGNAL':
                    port.update_signal(event)

                elif event.type == 'ORDER':
                    broker.execute_order(event)

                elif event.type == 'FILL':
                    port.update_fill(event)

    # 10-Minute heartbeat
    time.sleep(10*60)
Questo è lo schema di base di come è progettato un ambiente di backtesting basato sugli eventi. Nel prossimo articolo si descrive la gerarchia della classe Events.

Backtesting di Strategie di Trading Sistematico in Python: Considerazioni e Piattaforme Open Source

Il backtesting è probabilmente la parte più critica del processo di produzione di una Strategia di Trading Sistematico (STS), e si colloca tra lo sviluppo della strategia e la sua implementazione (trading dal vivo). Se una strategia è viziata, è auspicabile che un backtesting rigoroso metta in evidenza queste criticità, evitando che una strategia in perdita venga resa operativa.

Numerose funzionalità correlate si sovrappongono al backtesting, tra cui il trading simulato (o paper trading) e il trading dal vivo (o live trading). Il backtesting utilizza i dati storici per quantificare le prestazioni di una  STS. I simulatori di trading fanno un ulteriore passo avanti visualizzando l’attivazione dei trade e la performance dei prezzi per ogni barra o candela. Il trading simulato / live implementa un STS testandola in tempo reale: segnalando i trade, generando ordini, indirizzando ordini verso il broker, quindi mantenendo le posizioni man mano che gli ordini vengono eseguiti.

La maggior parte dei framework va oltre il backtesting ed includono alcune funzionalità di trading dal vivo. Questo è utile se si desidera eseguire l’implementazione dal framework di backtesting, che funziona anche collegato ad un broker e le fonti di dati preferite. Quantopian / Zipline fa un ulteriore passo avanti, fornendo una soluzione di sviluppo, backtesting e implementazione completamente integrata.

La comunità Python è ben fornita di questi strumenti, infatti ha a disposizione almeno sei framework di backtesting open source. Questi sono comunque in varie fasi di sviluppo e documentazione. Se vuoi lavorare con un team che costruisce un framework di backtesting open source, controlla i loro repository Github.

Prima di valutare i framework di backtesting, vale la pena definire i requisiti della tua STS.

Quali sono gli asset che vuoi tradare? La maggior parte dei framework supportano i dati del mercato azionario statunitense, mentre nel caso una strategia operi su derivati, ETF o altri strumenti, è necessario prevedere una specifica gestione se non è prevista dallo specifico framework. Il framework può gestire futures e opzioni a lunghezza finita e generare automaticamente operazioni di rollover? Per quanto riguarda i mercati illiquidi, quanto deve essere realistica un’ipotesi quando si eseguono ordini di grandi dimensioni?

Su quale frequenza dei dati è basata la tua STS? Un sistema di trading che richiede ogni tick o bid / ask ha un insieme molto diverso di problemi di gestione dei dati rispetto a un intervallo di 5 minuti o ogni ora. I grandi fondi hedge e i società HFT hanno investito in modo significativo nella costruzione di strutture di backtesting robuste e scalabili per gestire il volume e la frequenza dei dati. Alcune piattaforme forniscono un ricco set di dati ricco per varie le classi di attività come titoli S&P, con una risoluzione di un minuto.

Che tipo di ordine richiede la tua STS? Come minimo, gli ordini limite, gli ordini stop e gli OCO dovrebbero essere supportati dal framework.

Livello di assistenza/supporto e di documentazione. Le piattaforme in fase iniziale hanno una scarsa documentazione, in poche hanno un’assistenza dedicata che non sia i forum della community.

Le componenti di un Framework di Backtesting

Acquisizione dei dati e STS: i componenti di acquisizione dati utilizzano i file di definizione della STS e i relativi script per fornire i dati necessari per il test. Se il framework richiede che qualsiasi STS sia ricodificata prima del backtest, allora il framework dovrebbe supportare le funzioni per gli indicatori tecnici più popolari per accelerare i test della STS. Gli utenti determinano il periodo storico nel quale effettuare il backtesting in base a ciò che fornisce il framework o che cosa sono in grado di importare.

Il test delle performance applica la logica della STS alla finestra dei dati storici richiesti e calcola un’ampia gamma di metriche di rischio e rendimento, compresi il Shape Ratio, il Drawdown massimo e il Sortino Ratio. La maggior parte di tutti i framework supporta un numero decente di capacità di visualizzazione, tra cui le curve equity e le statistiche dettagliate.

L’ottimizzazione tende a richiedere la grande maggioranza delle risorse di calcolo durante l’analisi di una STS. Se la tua STS richiede un’ottimizzazione, concentrati su un framework che supporti l’elaborazione  distribuita / parallela e scalabile.

Nel contesto di strategie sviluppate utilizzando indicatori tecnici, gli sviluppatori di sistemi tentano di trovare un insieme ottimale di parametri per ciascun indicatore. Molto semplicemente, l’ottimizzazione potrebbe scoprire che una STS basata sul crossover di due medie mobile di 6 e 10 giorni ha registrato maggiori profitti sul test con i dati storici rispetto a qualsiasi altra combinazione di periodi compresi tra 1 e 20. Già con questo semplice esempio, 20 * 20 = 400 combinazioni di parametri devono essere calcolati e classificati.

In un contesto di portafoglio, l’ottimizzazione cerca di trovare la ponderazione ottimale di ogni asset nel portafoglio, inclusi strumenti shorted e leveraged. Su base periodica, il portafoglio viene ribilanciato, con conseguente acquisto e vendita degli strumenti che compongono il portafoglio, come richiesto per allinearsi con l’ottimizzazione effettuata.

Il dimensionamento delle posizioni è un ulteriore utilizzo dell’ottimizzazione, che aiuta gli sviluppatori di sistemi a simulare e analizzare l’impatto della leva e il dimensionamento dinamico della posizione su una STS e sulle prestazioni del portafoglio.

Cinque Framework di Backtesting implementati in Python

Le funzionalità standard di  piattaforme per il backtesting, scritti in Python ed open source devono includere:

  • Logica Event-Driven
  • Licenze molto flessibili e non restrittive
  • Una buona raccolta di indicatori tecnici predefiniti
  • Calcolo della metrica delle prestazioni standard / capacità di visualizzazione / reporting

PyAlgoTrade

PyAlgoTrade è un framework di backtesting maturo, completamente documentato, con funzionalità di paper trading e live. Il supporto dati include Yahoo! Finance, Google Finance, NinjaTrader e qualsiasi tipo di serie temporali basate su CSV come Quandl. I tipi di ordine supportati includono Market, Limit, Stop e StopLimit.

PyAlgoTrade supporta il trading di Bitcoin tramite Bitstamp e la gestione degli eventi di Twitter in tempo reale.

Pagina del progetto: github.com/gbeced/pyalgotrade
Licenza: Apache 2.0

 

bt – Backtesting for Python

bt mira a favorire la creazione di blocchi di strategie facilmente testabili, riutilizzabili e flessibili per facilitare il rapido sviluppo di complesse strategie di trading”.

Il framework è particolarmente adatto alla verifica di STS basate sul portafoglio, perchè include algoritmi dedicati all’analisi degli asset e al ribilanciamento del portafoglio. La modifica di una strategia da eseguire su diverse frequenze temporali o ponderazioni delle risorse alternative comporta un minimo di modifica del codice. bt è costruito sulla base di ffn – una libreria di funzioni finanziarie per Python.

Pagina del progetto: pmorissette.github.io/bt
Licenza: MIT

 

Backtrader

Questa piattaforma è eccezionalmente ben documentata, con un blog di presentazione e una comunità on-line attiva per la pubblicazione di domande e richieste di funzionalità. Backtrader supporta una serie di formati di dati, inclusi file CSV, DataFrame di Pandas  e feed di dati in tempo reale da tre broker. Questi feed di dati sono accessibili simultaneamente e possono anche rappresentare diversi intervalli temporali. I Broker supportati includono Oanda per il trading FX e il trading di classi multi-asset tramite Interactive Brokers e Visual Chart.

Pagina del progetto: www.backtrader.com
Github: github.com/mementum/backtrader
Licenza: GPL v3.0

 

pysystemtrade

Lo sviluppatore di pysystemtrade Rob Carver ha un ottimo post che discute perché ha deciso di creare un altro framework di backtesting di Python e gli argomenti a favore e contro lo sviluppo del framework. Il framework di backtesting per pysystemtrade è discusso nel libro di Rob, “Systematic Trading”.

pysystemtrade elenca una serie di funzionalità di roadmap, tra cui un back tester completo che include tecniche di ottimizzazione e calibrazione e scambi di futures completamente automatizzati con Interactive Brokers. I contributori open source sono i benvenuti.

Pagina del progetto: github.com/robcarver17/pysystemtrade
Licenza: GPL v3.0

 

Zipline

Zipline è un simulatore di trading algoritmico con funzionalità di paper trading e live trading. Accessibile tramite l’interfaccia IPython Notebook basata su browser, Zipline fornisce una facile alternativa agli strumenti da riga di comando. Supportato e sviluppato da Quantopian, Zipline può essere utilizzato come framework di backtesting autonomo o come parte di un ambiente di sviluppo, test e distribuzione completo di STS per Quantopian / Zipline. Zipline fornisce 10 anni di dati storici sulle azioni statunitensi a risoluzione minima e una serie di opzioni per l’importazione dei dati.

Pagina del progetto: zipline.io
Github: github.com/quantopian/zipline
Licenza: Apache 2.0

 

Conclusioni

È la natura umana concentrarsi sulla ricompensa dello sviluppo di un STS (si spera proficuo), quindi operare il prima possibile sul mercato reale (perché siamo fiduciosi), senza spendere sufficientemente tempo e risorse per testare a fondo la strategia. Ma il backtesting non è solo un deterrente che ci impedisce di implementare strategie imperfette e di perdere capitale con il trading, ma fornisce anche una serie di strumenti diagnostici che possono informarci sul processo di sviluppo di una STS. Ad esempio, testare un’indentica STS su due intervalli di tempo diversi, comprendere il massimo drawdown di una strategia nel contesto delle correlazioni di asset e creare portafogli più intelligenti effettuando il backtest delle allocazioni di asset in più aree geografiche.

Nei post futuri, mi dedicherò a descrivere dettagliatamente le caratteristiche e le funzionalità del framework BackTrader e l’uso di varie tecniche di campionamento come bootstrap e jackknife per i modelli di trading predittivo di backtesting.

I migliori linguaggi di programmazione per il trading algoritmico

“Qual è il miglior linguaggio di programmazione per il trading algoritmico?” è una delle domande più frequenti. In breve la risposta è che non esiste un linguaggio “migliore”. Bisogna considerare molti aspetti come i parametri strategici, le prestazioni, la modularità, lo sviluppo, la resilienza e i costi. In questo articolo si descrive le componenti fondamentali dell’architettura del sistema di trading algoritmico, e come le decisioni relative all’implementazione influenzano la scelta del linguaggio.

In primo luogo, saranno presi in considerazione i componenti principali di un sistema di trading algoritmico, come gli strumenti di ricerca, l’ottimizzatore del portafoglio, la gestione del rischio e il motore di esecuzione. Successivamente, saranno esaminate come le diverse strategie di trading influenzino il design del sistema. In particolare, saranno discussi sia la frequenza degli scambi che il probabile volume di scambi.

Una volta selezionata la strategia di trading, è necessario progettare l’intero sistema. Ciò include la scelta dell’hardware, dei sistemi operativi e della resilienza del sistema contro eventi rari e potenzialmente catastrofici. Mentre l’architettura viene presa in considerazione, è necessario prestare attenzione alle prestazioni, sia per gli strumenti di backtesting, sia per l’ambiente di esecuzione live.

Cosa fa un Sistema di Trading Algoritmico?

Prima di decidere il linguaggio “migliore” con cui implementare un sistema di trading automatico è necessario definirne i requisiti. Il sistema sarà basato esclusivamente sull’esecuzione? Il sistema richiederà un modulo di gestione del rischio o di ottimizzazione del portafoglio? Il sistema richiederà un backtester ad alte prestazioni? Per la maggior parte delle strategie il sistema di trading può essere suddiviso in due categorie: Ricerca e Generazione di segnali.

La ricerca riguarda la valutazione di una prestazione strategica rispetto ai dati storici. Il processo di valutazione di una strategia di trading su dati di mercato precedenti è noto come backtesting. La dimensione dei dati e la complessità algoritmica avranno un grande impatto sull’intensità computazionale del backtester. La velocità della CPU e la concorrenza sono spesso i fattori limitanti nell’ottimizzazione della velocità di esecuzione della ricerca.

La generazione del segnale riguarda la generazione di un insieme di segnali di trading da un algoritmo e l’invio di tali ordini al mercato, solitamente tramite un broker. Per alcune strategie è richiesto un alto livello di prestazioni. Problemi di I / O come la larghezza di banda e la latenza della rete sono spesso il fattore limitante nell’ottimizzazione dei sistemi di esecuzione. Quindi la scelta dei linguaggi per ciascun componente dell’intero sistema potrebbe essere molto diversa.

Tipologia, Frequenza e Volume di una Strategia

Il tipo di strategia algoritmica utilizzata avrà un impatto sostanziale sul design del sistema. Sarà necessario considerare i mercati scambiati, i fornitori di dati esterni, la frequenza e il volume della strategia, il trade-off tra facilità di sviluppo e ottimizzazione delle prestazioni, nonché qualsiasi hardware personalizzato.

Le scelte tecnologiche per una strategia sul mercato azionario statunitense a bassa frequenza sono molto diverse da quelle di una strategia di arbitraggio statistico ad alta frequenza che opera sul mercato dei futures. Prima della scelta del linguaggio, devono essere valutati i vari fornitori di dati pertinenti alla strategia in questione.

Sarà necessario considerare la connettività verso il fornitore, la struttura delle API, la tempestività dei dati, i requisiti di archiviazione e la resilienza di fronte alla possibilità che un fornitore interrompa il flusso dati. È anche saggio avere un accesso rapido a più fornitori! I vari strumenti hanno tutti i propri formati e standard di archiviazione. Tutto questo deve essere preso in considerazione durante la progettazione della piattaforma.

La frequenza della strategia è probabilmente uno dei maggiori fattori che influenzano la scelta dello stack tecnologico. Le strategie che impiegano i dati su timeframe ridotti (come i minuti, o addirittura i secondi) richiedono un’analisi approfondita in termini di prestazioni.

Una strategia che opera sul secondo (cioè sui tick) comporta una progettazione focalizzata sulle prestazioni, come requisito principale. Per le strategie ad alta frequenza sarà necessario memorizzare e valutare una notevole quantità di dati di mercato. Software come HDF5 o kdb+ sono comunemente usati per questi ruoli.

Per elaborare gli ampi volumi di dati necessari per le applicazioni HFT, è necessario utilizzare un backtester e un sistema di esecuzione ampiamente ottimizzati. C / C ++ (probabilmente con un pò di assembler) è probabilmente il linguaggio ideale. Strategie ad altissima frequenza quasi certamente richiedono hardware personalizzato come FPGA, ed ottimizzazione del kernel e dell’interfaccia di rete.

Sistemi di Ricerca

I sistemi di ricerca consistono generalmente in una combinazione tra sviluppo interattivo e scripting automatizzato. Il primo si svolge all’interno di un IDE come Visual Studio, MatLab o R Studio. Quest’ultimo permette estesi calcoli numerici su moltissimi parametri e dati. Ciò porta alla scelta di un linguiggio che fornisca un ambiente semplice per testare il codice, ma fornisca anche prestazioni sufficienti per valutare le strategie multiparametriche.

I migliori IDE per questi scopi includono;

  • Microsoft Visual C++ / C#, che contiene estese utilità di debug, capacità di completamento del codice (tramite “Intellisense”) e panoramiche chiare dell’intero stack del progetto (tramite il database ORM, LINQ);
  • MatLab, che è progettato per l’algebra lineare numerica estesa e le operazioni vettorializzate, ma solamente in modalità da console interattiva;
  • R Studio, che avvolge la console del linguaggio statistico R in un IDE completo;
  • Eclipse IDE per Linux Java e C ++;
  • Python IDE semi-proprietari come Enthought Canopy, che includono librerie di analisi dei dati come NumPy, SciPy, scikit-learn e panda in un unico ambiente interattivo (console).

Per il backtesting numerico, sono datti tutti i suddetti linguaggi, anche se non è necessario utilizzare una GUI / IDE poiché il codice verrà eseguito “in background”. La prima considerazione in questa fase è quella della velocità di esecuzione. Un linguaggio compilato (come C ++) è spesso utile se le dimensioni dei parametri di backtesting sono grandi. 

Linguaggi interpretati come Python fanno spesso fanno uso di librerie ad alte prestazioni, come NumPy / Panda, per la fase di backtesting, al fine di mantenere un ragionevole grado di competitività con gli equivalenti codici compilati. In definitiva, il linguaggio scelto per il backtesting sarà determinato da specifiche esigenze algoritmiche e dalla gamma di librerie disponibili per quel linguaggio (maggiori dettagli in seguito). Tuttavia, il linguaggio utilizzato per i backtesting e gli ambienti di ricerca possono essere completamente indipendente da quelli utilizzati nell’ottimizzazione del portfolio, nella gestione del rischio e nelle componenti di esecuzione.

Ottimizzazione del Portafoglio e Gestione del Rischio

L’ottimizzazione del portafoglio e la gestione del rischio sono componenti spesso trascurate dai tradere algoritmici retail. Questo è quasi sempre un errore. Questi strumenti forniscono l’unico meccanismo con il quale preservare il capitale, che deve essere uno dei principali obiettivi di ogni trader. Non solo tentano di diminuire il numero di scommesse “rischiose”, ma minimizzano anche il tasso di insuccesso delle operazioni stesse, riducendo i costi di transazione.

Versioni sofisticate di questi componenti possono avere un effetto significativo sulla qualità e sulla costanza della redditività. Una strategia può essere resa stabile grazie a semplice modifiche sul meccanismo di ottimizzazione del portafoglio e di gestione del rischio, in modo da poter gestire più sistemi. Quindi devono essere considerati componenti essenziali all’inizio del progetto di un sistema di trading algoritmico.

Il lavoro del modulo di gestione del portafoglio consiste nel prendere in input un set di trade desiderati e produrre un sottoinsieme di trade effettivi che riducono al minimo il tasso di perdita, monitorando le esposizioni a vari fattori (come settori, classi di attività, volatilità ecc.) ed ottimizzare l’allocazione del capitale per le varie strategie.

La costruzione del portfolio spesso si riduce a un problema di algebra lineare (come la fattorizzazione della matrice) e quindi le prestazioni dipendono fortemente dalle performance dell’implementazione disponibile per gli algoritmi di algebra lineare numerica. Le librerie più usate sono  uBLAS, LAPACK e NAG per C ++. MatLab possiede anche funzioni native ampiamente ottimizzate che operano con le matrici. Python utilizza NumPy / SciPy per tali calcoli.

Un portafoglio costantemente riequilibrato richiederà una libreria nativa compilata (e ben ottimizzata!) al fine di eseguire queste operazioni matricali, se non si vuole creare un collo di bottiglia al sistema di trading.

La gestione del rischio è un altro modulo estremamente importante di un sistema di trading algoritmico. Il rischio può presentarsi in molte forme: aumento della volatilità (sebbene ciò possa essere visto come auspicabile per determinate strategie!), aumento delle correlazioni tra asset class, default del broker, malfunzionamenti del server, gli eventi “black swan” e bug non rilevati nel codice, per dirne alcuni.

Le componenti di questo modulo cercano e anticipano gli effetti di un’eccessiva volatilità e della correlazione tra asset e il loro possibile effetto sul capitale di trading. Spesso questo si riduce a un insieme di calcoli statistici come gli “stress test” di Montecarlo. Le performance computazionali di questi algoritmi sono vincolate alle prestazioni della CPU. Queste simulazioni sono altamente parallelizzabili (vedi di seguito) e, in una certa misura, è possibile affermare che “l’hardware non è un problema”.

Sistema di Esecuzione

Il compito del modulo di esecuzione consiste nel ricevere i segnali di trading, filtrati dai moduli di ottimizzazione del portafoglio e di gestione del rischio, ed inviarli a un broker, o altri punti di accesso al mercato. Per la maggior parte delle strategie di trading algoritmico retail si traduce in una connessione API o FIX ad una società di intermediazione come Interactive Brokers. Le considerazioni principali sulla scelta del linguaggio devono tenere conto anche della qualità dell’API, in quali linguaggi è disponibile l’interfaccia dell’API, la frequenza di esecuzione e lo slippage previsto.

La “qualità” dell’API si riferisce a quanto è ben documentato, quale tipo di prestazioni fornisce, se è necessario accedere tramite un software standalone o se è possibile stabilire un collegamento in modo headless (cioè senza GUI). Nel caso di Interactive Brokers, lo strumento Trader WorkStation deve essere eseguito in un ambiente GUI per poter accedere alle loro API. Una volta ho dovuto installare una versione desktop Ubuntu su un server cloud Amazon solamente per poter accedere a Interactive Brokers da remoto, solo per questo motivo!

La maggior parte delle API fornirà un’interfaccia C++ e/o Java. Di solito spetta alla comunità open-source sviluppare wrapper specifici per linguaggio per C#, Python, R, Excel e MatLab. Si noti che con ogni plug-in aggiuntivo utilizzato (in particolare i wrapper API) vi è spazio per l’insinuarsi di bug nel sistema. Verifica sempre i plug-in di questo tipo e assicurati che vengano attivamente mantenuti. Un indicatore utile consiste nel valutare quanti nuovi aggiornamenti ad un codebase sono stati fatti negli ultimi mesi.

La frequenza di esecuzione è di fondamentale importanza nell’algoritmo di esecuzione. Dato che centinaia di ordini possono essere inviati ogni minuto, le prestazioni diventano fondamentali. Lo slippage sarà amplificato da un sistema di esecuzione mal progettato e questo avrà un impatto drammatico sulla redditività.

I linguaggi a tipizzazione statica (vedi sotto) come C++ / Java sono generalmente ottimali per l’esecuzione ma c’è un compromesso in termini di tempo di sviluppo, test e facilità di manutenzione. I linguaggi tipizzati dinamicamente, come Python e Perl sono generalmente “abbastanza veloci”. Accertarsi sempre che i componenti siano progettati in modo modulare (vedere di seguito) in modo che possano essere “sostituiti” con la scalabilità del sistema.

Pianificazione dell'Architettura e Processo di Sviluppo

Finora abbiamo descritto le componenti di un sistema di trading e i requisiti di frequenza e volume sono stati discussi sopra, mentre dobbiamo ancora approfondire l’infrastruttura di sistema. Chi opera come trader retail o lavora in un piccolo fondo probabilmente “indossa molti cappelli”. Sarà necessario prevedere il modello alpha, la gestione del rischio, i parametri di esecuzione e anche l’implementazione finale del sistema. Prima di approfondire i specifici linguaggi, è bene introdurre alcuni approcci per progettare un’architettura  ottimale.

Separazione degli Interessi

Una delle decisioni più importanti che devono essere prese all’inizio è come “separare gli interessi” di un sistema di trading. Nello sviluppo del software, significa essenzialmente come suddividere i diversi aspetti del sistema in componenti modulari separati.

Esponendo le interfacce a ciascuno dei componenti è facile sostituire delle parti del sistema con altre versioni che migliorano prestazioni, affidabilità o manutenzione, senza modificare alcun codice esterno al modulo. Questo è il “miglior approccio” per tali sistemi nel caso di strategie a bassa frequenza. Per il trading HFT e UHFT questo approccio potrebbe essere ignorato al fine di focalizzare il sistema su prestazioni ancora maggiori. 

La creazione di una mappa delle componenti di un sistema di trading algoritmico è un tema vasto un solo articolo. Tuttavia, un approccio ottimale consiste nell’assicurarsi che vi siano componenti separati per gli input dei dati storici e i dati in tempo reale, l’archiviazione dei dati, l’API di accesso ai dati, il backtesting, i parametri della strategia, l’ottimizzazione del portafoglio, la gestione del rischio e i sistemi di esecuzione automatizzata.

Ad esempio, se l’archivio dati in uso è attualmente sottoperformante, anche a livelli significativi di ottimizzazione, può essere sostituito con riscritture minime nell’input dei dati o nell’API di accesso ai dati. Per quanto riguarda il backtester e le componenti successive, non c’è differenza.

Un altro vantaggio dei componenti separati è che consente di utilizzare diversi linguaggi di programmazione nel sistema globale. Non è necessario essere limitato a un solo linguaggiose il metodo di comunicazione dei componenti è indipendente dal linguaggio. Questo è il caso quando si prevede una comunicazione tra i moduli tramite TCP / IP, ZeroMQ o qualche altro protocollo indipendente.

Come esempio concreto, si consideri il caso in cui un sistema di backtesting viene scritto in C ++ per le prestazioni di “number crunching”, mentre il gestore del portafoglio e i sistemi di esecuzione sono scritti in Python usando SciPy e IBPy.

Considerazioni sulle Prestazioni

Le prestazioni sono fondamentali per la maggior parte delle strategie di trading. Per le strategie ad alta frequenza è il fattore più importante. Le “Prestazioni” coprono una vasta gamma di problemi, come velocità di esecuzione algoritmica, la latenza della rete, la larghezza di banda, I / O di dati, simultaneità / parallelismo e lo scaling. Ognuna di queste aree è coperta individualmente da voluminosi libri di testo, quindi questo articolo si introducono alcuni concetti di ogni argomento. La scelta dell’architettura e del linguaggio sono discussi in termini di effetti sulla performance.

La saggezza prevalente, come affermato da Donald Knuth, uno dei padri dell’informatica, è che “l’ottimizzazione prematura è la radice di tutti i mali”. Questo è quasi sempre valido – tranne quando si costruisce un algoritmo di trading ad alta frequenza! Per coloro che sono interessati alle strategie di bassa frequenza, un approccio comune è quello di costruire un sistema nel modo più semplice possibile e ottimizzare solo quando iniziano a comparire rallentamenti.

 

Gli strumenti di profilazione vengono utilizzati per determinare dove si verificano i colli di bottiglia. È possibile creare profili per tutti i fattori sopra elencati, in ambiente MS Windows o Linux. Ci sono molti strumenti disponibili per qualsiasi sistema operativo e linguaggio, così come software di terze parti.

C ++, Java, Python, R e MatLab hanno tutti a disposizione librerie ad alte prestazioni (nativamente o importandole esternamente) per la struttura del database e il lavoro computazionale. C ++ viene fornito con la libreria nativa, mentre Python utilizza le librerie NumPy / SciPy. I comuni algoritmi matematici si trovano in queste librerie, quindi raramente è necessario scrivere una nuova implementazione.

Un’eccezione è nel caso fosse richiesta un’architettura hardware altamente personalizzata ed un algoritmo deve far un ampio uso di estensioni proprietarie (come le cache personalizzate). Tuttavia, spesso “la reinvenzione della ruota” fa perdere tempo che potrebbe essere speso meglio nello sviluppo e nell’ottimizzazione di altre parti dell’infrastruttura di trading. Il tempo di sviluppo è estremamente prezioso, specialmente nel contesto dei soli sviluppatori.

 

La latenza è spesso un problema del sistema di esecuzione ma si verifica anche nei tool di ricerca nel si trovano solitamente sulla stessa macchina. Per il primo, la latenza può verificarsi in più punti il processo di esecuzione. I database devono essere consultati (latenza del disco / rete), i segnali devono essere generati (sistema operativo, latenza della messaggistica del kernel), i segnali di trading devono essere inviati (latenza NIC) e gli ordini devono essere elaborati (latenza interna dei sistemi di exchange).

Per le operazioni a frequenza più elevata è necessario acquisire familiarità con l’ottimizzazione del kernel e l’ottimizzazione della trasmissione di rete. Questa è un’area complessa ed è significativamente oltre lo scopo di questo articolo, ma se si desidera un algoritmo UHFT, è necessario avere un’approfondita della conoscenza del software e dell’hardware!

 

Il caching è uno strumento che deve essere presente nella “cassetta degli attrezzi” di uno sviluppatore di trading quantitativo. Il caching fa riferimento al concetto di archiviazione dei dati utilizzati frequentemente in un modo che consentire un accesso più performante, a scapito della potenziale inconsistenza dei dati. Un caso di uso comune si verifica nello sviluppo web quando si prendono i dati da un database relazionale presente su un supporto fisico (hard disk) e vengono caricati in memoria. Qualsiasi richiesta successiva per i dati non deve “accedere il database” e quindi si ottengono guadagni significativi in termini di prestazioni.

Per il trading il caching può essere estremamente utile. Ad esempio, lo stato corrente di un portafoglio di strategie può essere memorizzato in una cache fino a quando non viene ribilanciato, in modo tale che l’elenco non debba essere rigenerato su ciascun ciclo dell’algoritmo di trading. È probabile che tale rigenerazione sia un’operazione di CPU o di I / O molto lenta e dispendiosa.

Tuttavia, il caching non è privo di problemi. La rigenerazione dei dati della cache, a causa della natura volatile dello storage della cache, può comportare una notevole richiesta di lavoro. Un altro problema è la dog-piling, un tipo di errore a cascata che può verificarsi quando i sistemi di calcolo in parallelo con meccanismi di caching sono sottoposti a carichi molto elevati, come la generazione multipla di una nuova copia.

L’allocazione dinamica della memoria è un’operazione costosa in termini di esecuzione del software. Pertanto, è imperativo che le applicazioni di trading con prestazioni più elevate siano ben consapevoli di come la memoria viene allocata e rilasciata durante il flusso del programma. I nuovi standard linguistici come Java, C # e Python eseguono tutti una garbage collection automatica, che effettua una deallocazione dinamica della memoria allocata quando gli oggetti escono dal flusso del programma.

La garbage collection è estremamente utile durante lo sviluppo poiché riduce gli errori e facilita la leggibilità del codice. Tuttavia, è spesso non ottimale per determinate strategie di trading ad alta frequenza. In questi casi è richiesta una garbage collection personalizzata. In Java, ad esempio, attivando il garbage collector e la configurazione heap, è possibile ottenere prestazioni elevate per le strategie HFT.

C ++ non fornisce un garbage collector nativo e quindi è necessario gestire tutta l’allocazione / deallocazione di memoria come parte dell’implementazione di un oggetto. Anche se potenzialmente soggetti a errori (potenzialmente causati da puntatori non più validi) è estremamente utile avere un controllo preciso su come gli oggetti appaiono nell’heap di determinate applicazioni. Quando si sceglie un linguaggio è necessario verificare come funziona il garbage collector e se può essere modificato per ottimizzare per un particolare caso d’uso.

 

Molte operazioni nei sistemi di trading algoritmico sono ideali per la parallelizzazione. Questo si riferisce al concetto di eseguire più operazioni programmatiche allo stesso tempo, cioè in “parallelo”. I cosiddetti algoritmi “altamente paralleli” includono funzioni che possono essere eseguite completamente indipendentemente dalle altre. Alcune operazioni statistiche, come le simulazioni MonteCarlo, sono un buon esempio di algoritmi altamente paralleli, in quanto ogni estrazione casuale e la successiva processazione  possono essere calcolati senza dover aspettare il processo delle altre estrazioni..

Altri algoritmi sono solo parzialmente paralleli. Un esempio sono le simulazioni fluidodinamiche, dove il calcolo può essere suddiviso, ma alla fine questi calcoli devono comunicare tra loro e quindi le operazioni sono parzialmente sequenziali. Gli algoritmi in parallelo sono soggetti alla Legge di Amdahl, che fornisce un teorico limite all’aumento delle prestazioni di un algoritmo parallelizzato quando soggetto a N processi separati (ad esempio su un core o thread della CPU).

La parallelizzazione è diventata sempre più importante come mezzo di ottimizzazione poiché le velocità di clock dei processori hanno raggiunto un limite tecnico, per aumentare le prestazioni i moderni processori contengono molti core con cui eseguire calcoli paralleli. L’ascesa dell’hardware grafico consumer (prevalentemente per i videogiochi) ha portato allo sviluppo di Graphical Processing Units (GPU), che contengono centinaia di “core” per operazioni altamente concorrenti. Tali GPU sono ora molto convenienti. Framework di alto livello, come la CUDA di Nvidia, hanno portato ad una rapida diffusione nel mondo accademico e finanziario.

Tale hardware GPU è generalmente adatto solo per gli aspetti di ricerca della finanza quantitativa, mentre altri hardware più specializzati (inclusi i Field-Programmable Gate Arrays – FPGA) vengono utilizzati per (U) HFT. Oggigiorno, la maggior parte dei linguaggi moderni prevede un certo grado di concorrenza / multithreading. Pertanto è semplice ottimizzare un backtester, poiché tutti i calcoli sono generalmente indipendenti dagli altri.

 

Lo scaling del software si riferisce alla capacità del sistema di gestire carichi di lavoro sempre più consistenti sotto forma di maggiori richieste, maggiore utilizzo del processore e maggiore allocazione della memoria. Nel trading algoritmico una strategia è scalabile se può accettare maggiori quantità di capitale e produrre comunque rendimenti consistenti. Lo stack della tecnologia di trading viene scalato o ridimensianato se deve gestire maggiori volumi di scambio e una maggiore latenza, senza rallentamenti.

La difficoltà nella progettazione di sistemi scalabili consiste nel prevedere in anticipo dove si verificherà un collo di bottiglia. Il logging, il testing, la profilazione e il rigido monitoraggio contribuiranno notevolmente ad aumentare la scalabilità di un sistema. Gli stessi linguaggi sono spesso descritti come “non scalabili”. Questo è generalmente il risultato di disinformazione, piuttosto che fatti reali. Infatti è lo stack tecnologico, nella sua totalità, che dovrebbe essere scalabile, non certo il linguaggio. Ovviamente certi linguaggi hanno prestazioni migliori di altri in particolari casi d’uso, ma un linguaggio non è mai “migliore” di un’altro in tutti gli aspetti. Uno dei metodi per gestire la scalabilità è adottare un approccio di suddivisione in moduli, come affermato sopra. Al fine di introdurre ulteriormente la capacità di gestire i “picchi” nel sistema (cioè una volatilità improvvisa che innesca una serie di trade), è utile creare una “architettura di accodamento dei messaggi”, cioè prevedere un sistema di message queue tra i componenti in modo che gli ordini vengano “impilati” se un determinato componente non è in grado di elaborare molte richieste.

Invece di perdere queste richieste, sono semplicemente tenute in coda fino a quando il messaggio non viene gestito. Questo approccio è particolarmente utile per l’invio dei trade al motore di esecuzione. Se il motore sta subendo una forte latenza, eseguirà il backup delle transazioni. Una coda tra il generatore di segnali di trading e l’API di esecuzione allevierà questo problema a scapito di un potenziale slippage. RabbitMQ è uno dei migliori software open source per la gestione delle code di messaggi. 

Hardware e Sistema Operativo

L’hardware che esegue la strategia può avere un impatto significativo sulla redditività dell’algoritmo. Questo non è un problema limitato ai trader ad alta frequenza. Una scelta sbagliata nell’hardware e nel sistema operativo può causare un arresto anomalo della macchina o il riavvio nel momento più inopportuno. Quindi è necessario considerare dove risiederà l’applicazione. La scelta è generalmente tra un personal desktop, un server remoto, un provider di “cloud” o un server exchange condiviso.

Le macchine desktop sono semplici da installare e da amministrare, specialmente con i più recenti sistemi operativi di facile utilizzo come Windows 7/8, Mac OSX e Ubuntu. I sistemi desktop possiedono tuttavia alcuni svantaggi significativi. In primo piano, è probabile che le versioni dei sistemi operativi progettate per macchine desktop richiedano il riavvio e il patching del sistema (e spesso una perdita di tempo). Usano anche più risorse computazionali dato che richiedono un’interfaccia grafica (GUI).

L’utilizzo dell’hardware in un ambiente domestico (o locale) può portare a problemi di connettività Internet e di alimentazione elettrica. Il vantaggio principale di un sistema desktop è che è possibile acquistare una notevole potenza di calcolo per la frazione del costo di un server remoto dedicato (o un sistema basato su cloud) di velocità comparabile.

Un server dedicato o una macchina basata su cloud, anche se spesso più costosa dell’opzione desktop, consente una maggiore ridondanza dell’infrastruttura, come i backup automatici dei dati, la possibilità di assicurare in modo più diretto l’uptime e il monitoraggio remoto. Sono più difficili da amministrare poiché richiedono di utilizzare le funzionalità di accesso remoto del sistema operativo.

In Windows questo è generalmente svolto tramite l’interfaccia grafica del Remote Desktop Protocol (RDP). Nei sistemi basati su Unix si utilizza la command-line Secure SHell (SSH). L’infrastruttura server basata su Unix è quasi sempre basata su command-line che rende immediatamente inutilizzabili strumenti di programmazione basati su GUI (come MatLab o Excel).

Un server co-localizzato è semplicemente un server dedicato che risiede il più vicino possibile all’exchange in modo da ridurre la latenza dell’algoritmo di trading. Questo è assolutamente necessario per determinate strategie di trading ad alta frequenza, che si basano sulla bassa latenza per generare l’alpha.

L’aspetto finale sulla scelta dell’hardware consiste nell’indipendenza del linguaggio di programmazione  rispetto alla piattaforma. È necessario che il codice venga eseguito su più sistemi operativi diversi? Il codice è progettato per essere eseguito su una particolare tipo di architettura del processore, come Intel x86 / x64 o sarà possibile eseguire su processori RISC come quelli prodotti da ARM? Questi problemi dipenderanno in larga misura dalla frequenza e dal tipo di strategia implementata.

Resilienza e Testing

Uno dei modi migliori per perdere un sacco di soldi nel trading algoritmico è creare un sistema senza resilienza. Questo si riferisce all’affidabilità del sistema quando soggetto a eventi rari, come il fallimento del broker, un’improvvisa ed eccessiva volatilità, tempi di inattività per un fornitore di server cloud o la cancellazione accidentale di un intero database. Anni di profitti possono essere eliminati in pochi secondi con un’architettura mal progettata. È assolutamente essenziale considerare problemi come debuggng, testing, registrazione, backup, alta disponibilità e monitoraggio come componenti principali del sistema.

È probabile che in qualsiasi applicazione di trading quantitativa personalizzata ragionevolmente complicata, almeno il 50% del tempo di sviluppo sarà speso per il debugging, il test e la manutenzione.

Quasi tutti i linguaggi di programmazione sono forniti con un debugger associato o possiedono ottime alternative di terze parti. In sostanza, un debugger consente l’esecuzione di un programma con l’inserimento di punti di interruzione arbitrari nel percorso del codice, che interrompono temporaneamente l’esecuzione per indagare sullo stato del sistema. Il principale vantaggio del debugging consiste nella possibilità di esaminare il comportamento del codice prima di un noto punto di arresto.

Il debug è un componente essenziale nella casetta degli attrezzi per l’analisi degli errori di programmazione. Tuttavia, sono più ampiamente utilizzati in linguaggi compilati come C ++ o Java, poiché i linguaggi interpretati come Python il debug è molto più facilei da eseguire grazie al un minor numero di LOC e di istruzioni meno dettagliate. Nonostante questa tendenza, Python viene fornito con il pdb, un sofisticato strumento di debugging. L’IDE Microsoft Visual C++ è dotato di estese funzionalità di debug, mentre per il programmatore Linux C++ con command-line esiste il debugger gdb.

 

I testing nello sviluppo del software si riferisce al processo di applicazione di parametri e risultati noti a funzioni, metodi e oggetti specifici all’interno di una porzione di codice, al fine di simulare il comportamento e valutare le prestazioni, contribuendo a garantire che un sistema si comporti come dovrebbe. Un paradigma più recente è noto come Test Driven Development (TDD), in cui il codice di test è sviluppato a partire da una specifica interfaccia astratta. Prima del completamento della codebase effettiva, tutti i test falliranno. Poiché il codice è scritto per “riempire gli spazi vuoti”, i test finali dovrebbero essere tutti positivi quando termina lo sviluppo.

Scegliere il linguaggio

Dopo aver descritto i fattori che influenzano lo sviluppo di un sistema di trading algoritmico ad alte prestazioni. Il prossimo passo è capire come sono classificati i vari linguaggi di programmazione.

Tipologie di Sistemi

Quando si sceglie il linguaggio per l’infrastruttura di trading è necessario considerare il tipo di sistema. Per il trading si può utilizzare linguaggi a tipizzazione statica o dinamico. Un linguaggio tipizzato staticamente esegue verifiche dei tipi di dati(ad esempio interi, float, classi personalizzate ecc.) durante il processo di compilazione. Tali linguaggi includono C++ e Java. Un linguaggio tipizzato in modo dinamico esegue la maggior parte del controlli in fase di esecuzione. Tali linguaggi includono Python, Perl e JavaScript.

Per un sistema altamente numerico come un motore di trading algoritmico, il controllo del tipo al momento della compilazione può essere estremamente utile, in quanto può eliminare molti bug che altrimenti causerebbero errori numerici. Tuttavia, il controllo dei tipi non risolve tutti bug quindi è necessario implementare la gestione delle eccezioni in modo da controllare le operazioni impreviste. I linguaggi ‘dinamici’ (cioè quelli che sono tipizzati dinamicamente) possono spesso presentari errori di runtime che potrebbero essere catturati con un controllo del tipo dati in fase di compilazione. Per questo motivo è nato il concetto di TDD (vedi sopra) e gli unit test che, se eseguito correttamente, offre spesso più sicurezza rispetto al solo controllo in fase di compilazione.

Un altro vantaggio dei linguaggi tipizzati staticamente è l’ottimizzazione effettuata dal compilatore che non sono disponibili per un linguaggio dinamico, infatti grazie alla definizione delle tipologie di dati i requisiti di utilizzo della memoria sono già noti in fase di compilazione. In effetti, parte dell’inefficienza di molti linguaggi tipizzati dinamicamente deriva dal fatto che la verifica sul controllo del tipo di alcuni oggetti in fase di esecuzione, con un notevole calo delle prestazioni. Le librerie per le linguaggi dinamici, come NumPy / SciPy, riescono a minimizzare queste criticità grazie all’introduzione di una tipologia all’interno degli array.

Open-Source o Proprietario?

Uno sviluppatore di trading algoritmico può scegliere se utilizzare tecnologie proprietarie (commerciali) o open source. Ci sono vantaggi e svantaggi per entrambi gli approcci. È necessario considerare molti fattori, quali l’attività della comunity circonda un linguaggio, la facilità di installazione e manutenzione, la qualità della documentazione e gli eventuali costi di licenza / manutenzione.

Lo stack Microsoft .NET (incluso Visual C ++, Visual C#) e MatLab di MathWorks sono due delle maggiori opzioni per le soluzioni proprietarie. Entrambi gli strumenti hanno avuto significativi sviluppi nello spazio finanziario, dove il primo costituisce lo stack predominante dell’infrastruttura di trading delle banche di investimento banking, mentre il secondo è ampiamente utilizzato per le ricerche quantitative all’interno dei fondi di investimento.

Sia Microsoft che MathWorks forniscono un documentazione di alta qualità per i loro prodotti. Inoltre, inoltre dispongono di comunity molto attive sul web. Il software .NET consente l’integrazione coesiva con più linguaggi come C++, C# e VB, oltre ad un facile collegamento agli altri prodotti Microsoft come il database SQL Server tramite LINQ. MatLab ha anche molti plugin/librerie (alcuni gratuiti) per quasi tutti gli ambiti di ricerca quantitativa.

Ci sono anche degli svantaggi. Per entrambi i software i costi per un trader solitario non sono da trascurare (sebbene Microsoft offra gratuitamente la versione entry-level di Visual Studio). Gli strumenti Microsoft funzionano ottimamente l’uno con l’altro, ma si integrano meno facilmente con un  codice esterno. Visual Studio deve anche essere eseguito su Microsoft Windows, che è probabilmente molto meno performante di un equivalente server Linux facilmente ottimizzato.

Inoltre MatLab non ha a disposizione alcuni plugin chiave come un buon wrapper  perl’API di Interactive Brokers, uno dei pochi broker adatti al trading algoritmico ad alte prestazioni. Il problema principale con i prodotti proprietari è la mancanza di disponibilità del codice sorgente. Ciò significa che se le prestazioni sono veramente fondamentali, entrambi questi strumenti non sono la scelta migliore.

La maggioranza degli strumenti open source si basano su ambienti Linux, come MySQL / PostgreSQL, Python, R, C ++ e Java sono utilizati per implementazioni ad alte prestazioni. Tuttavia, non sono utilizzati solo per questo scopo. Python e R, in particolare, contengono una vasta gamma di librerie numeriche per eseguire praticamente qualsiasi tipo immaginabile di analisi dei dati immaginabile, spesso con velocità di esecuzione paragonabili ai linguaggi compilati.

Il vantaggio principale nell’uso di linguaggi interpretati è la velocità dei tempi di sviluppo. Python e R richiedono molte meno righe di codice (LOC) per ottenere funzionalità simili, principalmente grazie all’ampia gamma di librerie disponibili. Inoltre, spesso consentono lo sviluppo basato su console interattiva, riducendo rapidamente il processo di sviluppo iterativo.

Dato che il tempo di uno sviluppatore è estremamente prezioso mentre spesso la velocità di esecuzione non è così importantante (a meno che non si stia implementando un sistema HFT), vale la pena considerare ampiamente uno stack tecnologico open source. Python e R sono circondati da una solida comunity di sviluppatori e sono estremamente ben supportate, grazie alla loro popolarità. La documentazione è eccellente e gli errori (almeno per le librerie di base) rimangono scarsi.

Gli strumenti open source soffrono però la mancanza di un contratto di supporto commerciale dedicato, e funzionano in modo ottimale solo su ambienti senza interfacce utente. Un tipico server Linux (come Ubuntu) sarà spesso completamente command-line. Inoltre, se Python e R si dimostrano lenti nell’esecuzione di determinate attività, esistono meccanismi per integrare il codice C++ al fine di migliorare la velocità di esecuzione, ma richiede una certa esperienza nella programmazione multilinguaggio.

Mentre il software proprietario non è immune da problemi di dipendenza / versioning, è molto meno comune dover gestire versioni di librerie errate in tali ambienti. I sistemi operativi open source come Linux possono essere più complicati da amministrare.

E’ mia personale opinione e preferenza costruire tutti i miei strumenti di trading con tecnologie open source. In particolare utilizzo: Ubuntu, MySQL, Python, C++ e R. La maturità, la dimensione della community, la capacità di “scavare a fondo” se si verificano problemi e ridurre la TCO sono di gran lunga superiori alla semplicità delle GUI proprietarie e alla facilità d’installazione. Detto questo, Microsoft Visual Studio (specialmente per C++) è un fantastico ambiente di sviluppo integrato (IDE) che raccomando vivamente.

Batterie incluse?

Il titolo di questa sezione fa riferimento alle funzionalità “out of the box” di un linguaggio – quali librerie contiene e quanto sono affidabili? È qui che i linguaggi maturi hanno un vantaggio rispetto alle nuove varianti. C++, Java e Python dispongono ora di ampie librerie per la programmazione di rete, HTTP, interazione con sistemi operativi, GUI, espressioni regolari (regex), iterazione e algoritmi di base.

Il C++ è famoso per la sua STL (Standard Template Library) che contiene un’ampia gamma “gratuita” di strutture dati e algoritmi ad alte prestazioni. Python è noto per essere in grado di comunicare con quasi ogni altro tipo di sistema / protocollo (in particolare il web), principalmente attraverso la propria libreria standard. R è ricco di strumenti statistici ed econometrici nativi, mentre MatLab è estremamente ottimizzato per qualsiasi codice algebrico lineare numerico (che può essere usato per l’ottimizzazione del portafoglio).

Al di fuori delle librerie standard, C ++ fa uso della libreria Boost, che riempie le “parti mancanti” della libreria standard. In effetti, molte parti di Boost sono entrate nello standard TR1 e successivamente disponibili nelle specifiche C++ 11, incluso il supporto nativo per espressioni lambda e simultaneità.

Python ha la combinazione di librerie ad altre prestazioni per l’analisi dei dati NumPy / SciPy / Pandas, che ha ottenuto un’ampia diffusione nei sistema di trading algoritmico. Inoltre, esistono plugin ad alte prestazioni per l’accesso ai principali database relazionali, come MySQL ++ (MySQL / C ++), JDBC (Java / MatLab), MySQLdb (MySQL / Python) e psychopg2 (PostgreSQL / Python). Python può persino comunicare con R tramite il plugin RPy!

Un aspetto spesso trascurato di un sistema di tradingdurante la fase iniziale di progettazione è la connettività all’API del broker. La maggior parte delle API supporta nativamente C++ e Java, ma alcune supportano direttamente anche C# e Python, o tramite wrapper fornito dalla comunity. In particolare, Interactive Brokers può essere collegato tramite il plugin IBPy. Se sono richieste prestazioni elevate, i broker supporteranno il protocollo FIX.

 

Conclusioni

Ora è evidente come la scelta dei linguaggi di programmazione per un sistema di trading algoritmico non è semplice e richiede una profonda riflessione. Le principali considerazioni sono: prestazioni, facilità di sviluppo, resilienza e testing, separazione degli interessi, manutenzione, disponibilità del codice sorgente, costi di licenza e maturità delle librerie.

Il vantaggio di un’architettura separata consiste nel poter “collegare” diversi linguaggi ai diversi aspetti di uno stack di trading, man mano che cambiano i requisiti. Un sistema di trading è uno strumento in evoluzione ed è probabile che qualsiasi scelta si evolverà insieme ad esso.