DataTrader – Trading Algoritmico Avanzato: il Gestore del Portfolio

DataTrader - trading algoritmico - Parte 4

Nell’attuale serie sull’infrastruttura di trading avanzata abbiamo descritto sia la classe di posizione che la classe di portafoglio , due componenti essenziali di un solido backtesting e di un sistema di live trading. In questo articolo estenderemo la discussione alla classe Portfolio Handler , che completerà la descrizione del portfolio Order Management System (OMS).

L’OMS è la spina dorsale di qualsiasi infrastruttura di trading quantitativo. Deve tenere traccia delle posizioni aperte (e chiuse) negli asset, deve raggruppare insieme quelle posizioni all’interno di un portafoglio (con liquidità) e deve modificare quel portafoglio con nuovi segnali di trading, sovrapposizioni di gestione del rischio e regole di dimensionamento delle posizioni.

In questo articolo discuteremo la classe PortfolioHandler. Questa specifica classe ha il compito di gestire un oggetto Portfolio, dicendogli se aprire/chiudere le posizioni in base alle informazioni che riceve dalle classi StrategyPositionSizerRiskManager ExecutionHandler. Questa classe è estremamente importante in quanto lega insieme il resto dei componenti.

Il seguente codice presentato in questo articolo proviene da DataTrader, motore open-source per il backtesting e il live trading. E’ rilasciato con una licenza MIT open source  e l’ultima versione è sempre disponibile su https://github.com/datatrading-info/DataTrader

Nel precedente articolo abbiamo elencato un promemoria dei componenti del sistema che descriveva in dettaglio come tutti i componenti di DataTrader si collegano insieme. Potete darci un’occhiata per ricordarsi come interagiscono.

Rivolgiamo ora la nostra attenzione alla classe PortfolioHandler e vediamo come interagisce con l’oggetto Portfolio.

PortfolioHandler

La prima questione da discutere è il motivo per cui il vecchio approccio della classe  Portfolio implementato in DTForex è stato ora sostituito con una classe  Portfolio che implementa calcoli complessi per monitorare le Position, e con una classe PortfolioHandler meno complessa.

Abbiamo adottato questo approccio in modo da vare un oggetto Portfolio più pulito e preveda solo il monitoraggio del saldo/patrimonio e delle posizioni aperte. Lo scopo principale di questo approccio è quello di consentire “teoricamente” la creazione di diversi oggetti “portafoglio” (ad esempio dal PositionSizer o dal RiskManager), e successivamente creare un insieme di trade necessari per “trasformare” il portafoglio corrente nel portafoglio teoricamente desiderato.

Questo processo è molto più semplice se la classe Portfolio gestisce semplicemente di un raggruppamento di oggetti Position e di un saldo del conto.

L’oggetto Portfolio non prevede l’interazione con gli eventi in coda, il PositionSizer, il RiskManager e il PriceHandler. Queste interazioni vengono gestite dal nuovo oggetto, il PortfolioHandler.

Abbiamo creato l’oggetto PortfolioHandler nel file portfolio_handler.py e di seguito si può consultare il listato completo. di seguito.

Nota che uno qualsiasi di questi elenchi è soggetto ad aggiornamenti, poiché il progetto è soggetto a continue modifiche e miglioramenti.

portfolio_handler.py

            from order import SuggestedOrder
from portfolio import Portfolio


class PortfolioHandler(object):
    def __init__(
        self, initial_cash, events_queue,
        price_handler, position_sizer, risk_manager
    ):
        """
        PortfolioHandler è progettato per interagire con un
        backtest o trading dal vivo, in generale una architettura
        basato sugli eventi. Espone due metodi, on_signal e
        on_fill, che gestiscono come gli oggetti SignalEvent
        e FillEvent vengono trattati.

        Ogni PortfolioHandler contiene un oggetto Portfolio,
        che memorizza gli effettivi oggetti Posizione.

        Il PortfolioHandler accetta un handle per un oggetto
        PositionSizer che determina un meccanismo, basato sul
        Portfolio corrente, per dimensionare un nuovo Ordine.

        PortfolioHandler prende anche un handle per il
        RiskManager, che viene utilizzato per modificare qualsiasi
        Ordine in modo da rimanere in linea con i parametri di rischio.
        """
        self.initial_cash = initial_cash
        self.events_queue = events_queue
        self.price_handler = price_handler
        self.position_sizer = position_sizer
        self.risk_manager = risk_manager
        self.portfolio = Portfolio(price_handler, initial_cash)

    def _create_order_from_signal(self, signal_event):
        """
        Prende un oggetto SignalEvent e lo usa per creare un oggetto
        SuggestedOrder. Questi non sono oggetti OrderEvent,
        poiché devono ancora essere inviati all'oggetto RiskManager.
        In questa fase sono semplicemente "suggerimenti" che il
        RiskManager verificherà, modificherà o eliminerà.
        """
        order = SuggestedOrder(
            signal_event.ticker, signal_event.action
        )
        return order

    def _place_orders_onto_queue(self, order_list):
        """
        Una volta che il RiskManager ha verificato, modificato o eliminato
        ogni oggetto Ordine, vengono inseriti nella coda degli eventi,
        per essere infine eseguiti dal ExecutionHandler.
        """
        for order_event in order_list:
            self.events_queue.put(order_event)

    def _convert_fill_to_portfolio_update(self, fill_event):
        """
        Al ricevimento di un FillEvent, PortfolioHandler converte
        l'evento in una transazione che viene archiviata nell'oggetto
        Portafoglio. Ciò garantisce che il broker e il portafoglio locale
        siano "sincronizzati".

        Inoltre, a fini di backtest, il valore del portafoglio può
        essere stimato in modo realistico, semplicemente modificando
        il modo in cui l'oggetto ExecutionHandler gestisce lo slippage,
        i costi di transazione, la liquidità e l'impatto sul mercato.
        """
        action = fill_event.action
        ticker = fill_event.ticker
        quantity = fill_event.quantity
        price = fill_event.price
        commission = fill_event.commission
        # Crea o modifa la posizione dalle informazioni di portafoglio
        self.portfolio.transact_position(
            action, ticker, quantity,
            price, commission
        )

    def on_signal(self, signal_event):
        """
        Questo è chiamato dal backtester o dall'architettura del trading live
        per formare gli ordini iniziali dal SignalEvent.

        Questi ordini vengono ridimensionati dall'oggetto PositionSizer e quindi
        inviato al RiskManager per verificarlo, modificarlo o eliminarlo.

        Una volta ricevuti dal RiskManager vengono convertiti in
        oggetti OrderEvent completi e rinviati alla coda degli eventi.
        """

        # Crea la lista dell'ordine iniziale da un segnale £vent
        initial_order = self._create_order_from_signal(signal_event)
        # Dimensiona la quantità dell'ordine iniziale
        sized_order = self.position_sizer.size_order(
            self.portfolio, initial_order
        )
        # Affina o elimina l'ordine tramite l'overlay del gestore del rischio
        order_events = self.risk_manager.refine_orders(
            self.portfolio, sized_order
        )
        # Inserisce ordini nella coda degli eventi
        self._place_orders_onto_queue(order_events)

    def on_fill(self, fill_event):
        """
        Questo è chiamato dal backtester o dall'architettura del trading live
        per prendere un FillEvent e aggiornare l'oggetto Portfolio con le nuovi
        posizioni o le posizioni modificate.

        In un ambiente di backtest, questi FillEvents verranno simulati
        da un modello che rappresenta l'esecuzione, mentre nel trading dal vivo
        provengono direttamente da un broker (come Interactive Broker).
        """
        self._convert_fill_to_portfolio_update(fill_event)
        

Il PortfolioHandler importa l’oggetto SuggestedOrder e l’oggetto Portfolio. Il primo è un oggetto differente rispetto a OrderEvent perché non ha attraversato il processo di dimensionamento della posizione o di gestione del rischio. Una volta che un ordine ha superato entrambi i processi, diventa un OrderEvent completo.

Per inizializzare un PortfolioHandler è necessario un saldo di iniziale e i riferimenti alla coda degli eventi, al gestore del prezzo, al sizer delle posizioni e al gestore del rischio. Infine creiamo l’oggetto Portfolio associato al suo interno . Si noti che esso stesso richiede l’accesso al gestore dei prezzi e al saldo iniziale:

            from order import SuggestedOrder
from portfolio import Portfolio


class PortfolioHandler(object):
    def __init__(
        self, initial_cash, events_queue,
        price_handler, position_sizer, risk_manager
    ):
        """
        PortfolioHandler è progettato per interagire con un
        backtest o trading dal vivo, in generale una architettura
        basato sugli eventi. Espone due metodi, on_signal e
        on_fill, che gestiscono come gli oggetti SignalEvent
        e FillEvent vengono trattati.

        Ogni PortfolioHandler contiene un oggetto Portfolio,
        che memorizza gli effettivi oggetti Posizione.

        Il PortfolioHandler accetta un handle per un oggetto
        PositionSizer che determina un meccanismo, basato sul
        Portfolio corrente, per dimensionare un nuovo Ordine.

        PortfolioHandler prende anche un handle per il
        RiskManager, che viene utilizzato per modificare qualsiasi
        Ordine in modo da rimanere in linea con i parametri di rischio.
        """
        self.initial_cash = initial_cash
        self.events_queue = events_queue
        self.price_handler = price_handler
        self.position_sizer = position_sizer
        self.risk_manager = risk_manager
        self.portfolio = Portfolio(price_handler, initial_cash)
        

Nel seguente metodo, _create_order_from_signal creiamo semplicemente il SuggestedOrder dal ticker e dal tipo di operazione. In questa fase si prevede di gestire solo gli ordini a mercato. Gli ordini limite e le forme di esecuzione più esotiche sono oggetto di successive implementazioni:

                def _create_order_from_signal(self, signal_event):
        """
        Prende un oggetto SignalEvent e lo usa per creare un oggetto
        SuggestedOrder. Questi non sono oggetti OrderEvent,
        poiché devono ancora essere inviati all'oggetto RiskManager.
        In questa fase sono semplicemente "suggerimenti" che il
        RiskManager verificherà, modificherà o eliminerà.
        """
        order = SuggestedOrder(
            signal_event.ticker, signal_event.action
        )
        return order
        

_place_orders_onto_queue è un semplice metodo di supporto che accetta un elenco di oggetti OrderEvent e li aggiunge alla coda degli eventi:

                def _place_orders_onto_queue(self, order_list):
        """
        Una volta che il RiskManager ha verificato, modificato o eliminato
        ogni oggetto Ordine, vengono inseriti nella coda degli eventi,
        per essere infine eseguiti dal ExecutionHandler.
        """
        for order_event in order_list:
            self.events_queue.put(order_event)
        

Il seguente metodo _convert_fill_to_portfolio_update, accetta un FillEvent e quindi aggiorna l’oggetto  Portfolio interno per tenere conto della transazione di riempimento. Come si può vedere, mostra che il PortfolioHandler non esegue calcoli matematici, ma delega i calcoli alla classe Portfolio:

                def _convert_fill_to_portfolio_update(self, fill_event):
        """
        Al ricevimento di un FillEvent, PortfolioHandler converte
        l'evento in una transazione che viene archiviata nell'oggetto
        Portafoglio. Ciò garantisce che il broker e il portafoglio locale
        siano "sincronizzati".

        Inoltre, a fini di backtest, il valore del portafoglio può
        essere stimato in modo realistico, semplicemente modificando
        il modo in cui l'oggetto ExecutionHandler gestisce lo slippage,
        i costi di transazione, la liquidità e l'impatto sul mercato.
        """
        action = fill_event.action
        ticker = fill_event.ticker
        quantity = fill_event.quantity
        price = fill_event.price
        commission = fill_event.commission
        # Crea o modifa la posizione dalle informazioni di portafoglio
        self.portfolio.transact_position(
            action, ticker, quantity,
            price, commission
        )
        

Il metodo on_signal lega insieme alcuni dei metodi precedenti. Crea l’ordine suggerito iniziale, quindi lo invia all’oggetto PositionSizer (insieme al portfolio) per essere dimensionato. Una volta restituito l’ordine dimensionato, si invia al RiskManager per gestire qualsiasi rischio associato agli impatti del nuovo ordine sul portafoglio corrente.

Il gestore del rischio restituisce quindi un elenco di ordini. Perché una lista? Ebbene, bisogna considerare il fatto che un trade generato può indurre il gestore del rischio a creare un ordine di copertura in un altro titolo. Quindi è necessario eventualmente restituire più di un ordine.

Una volta creato l’elenco degli ordini, vengono tutti inseriti nella coda degli eventi:

                def on_signal(self, signal_event):
        """
        Questo è chiamato dal backtester o dall'architettura del trading live
        per formare gli ordini iniziali dal SignalEvent.

        Questi ordini vengono ridimensionati dall'oggetto PositionSizer e quindi
        inviato al RiskManager per verificarlo, modificarlo o eliminarlo.

        Una volta ricevuti dal RiskManager vengono convertiti in
        oggetti OrderEvent completi e rinviati alla coda degli eventi.
        """

        # Crea la lista dell'ordine iniziale da un segnale £vent
        initial_order = self._create_order_from_signal(signal_event)
        # Dimensiona la quantità dell'ordine iniziale
        sized_order = self.position_sizer.size_order(
            self.portfolio, initial_order
        )
        # Affina o elimina l'ordine tramite l'overlay del gestore del rischio
        order_events = self.risk_manager.refine_orders(
            self.portfolio, sized_order
        )
        # Inserisce ordini nella coda degli eventi
        self._place_orders_onto_queue(order_events)
        

Il metodo finale del  PortfolioHandler è il on_fill. Questo richiama semplicemente il metodo precedente _convert_fill_to_portfolio_update. Questi due metodi sono stati separati, poiché nelle versioni successive di DataTrader potrebbe essere necessaria una logica più sofisticata. Non desideriamo modificare l’interfaccia on_fill del PortfolioHandler a meno che non sia assolutamente necessario. Questo aiuta a mantenere la compatibilità con le versioni precedenti :

                def on_fill(self, fill_event):
        """
        Questo è chiamato dal backtester o dall'architettura del trading live
        per prendere un FillEvent e aggiornare l'oggetto Portfolio con le nuovi
        posizioni o le posizioni modificate.

        In un ambiente di backtest, questi FillEvents verranno simulati
        da un modello che rappresenta l'esecuzione, mentre nel trading dal vivo
        provengono direttamente da un broker (come Interactive Broker).
        """
        self._convert_fill_to_portfolio_update(fill_event)
        

Questo completa la descrizione della classe PortfolioHandler. Per completezza puoi trovare il codice completo per la classe PortfolioHandler su Github.

portfolio_handler_test.py

Ora che abbiamo creato il PortfolioHandler, dobbiamo testarlo. Per fortuna, la maggior parte dei test matematici si verifica nelle classi Position e Portfolio. Tuttavia, è ancora necessario verificare che il PortfolioHandler “faccia la cosa giusta” quando riceve segnali generati dalla strategia e i riempimenti generati dall’esecuzione.

Sebbene i seguenti test possano sembrare “banali”, posso assicurare che è assolutamente vitale assicurarsi di avere un sistema funzionante man mano che viene aggiunta più complessità, anche se può essere abbastanza noioso scrivere codice di unit test. Uno degli aspetti più frustranti dello sviluppo del software è non fare gli unit test per così “ottenere rapidamente una risposta” e poi rendersi conto di avere un bug e non avere idea di dove si trovi in una vasta raccolta di moduli!

Eseguendo gli unit test mentre scriviamo i singoli moduli si evita questo problema il più possibile. Se viene scoperto un bug, di solito è molto più semplice rintracciarlo. Il tempo speso per testare le unità non è mai sprecato!

Di seguito è riportato un listato completo di portfolio_handler_test.py. Dopo il listato si analizza i singoli oggetti e metodi, come in precedenza:

            import datetime
from decimal import Decimal
import queue
import unittest

from event import FillEvent, OrderEvent, SignalEvent
from portfolio import PortfolioHandler


class PriceHandlerMock(object):
    def __init__(self):
        pass

    def get_best_bid_ask(self, ticker):
        prices = {
            "MSFT": (Decimal("50.28"), Decimal("50.31")),
            "GOOG": (Decimal("705.46"), Decimal("705.46")),
            "AMZN": (Decimal("564.14"), Decimal("565.14")),
        }
        return prices[ticker]


class PositionSizerMock(object):
    def __init__(self):
        pass

    def size_order(self, portfolio, initial_order):
        """
        Questo oggetto PositionSizerMock modifica semplicemente
        la quantità per essere 100 per qualsiasi azione negoziata.
        """
        initial_order.quantity = 100
        return initial_order


class RiskManagerMock(object):
    def __init__(self):
        pass

    def refine_orders(self, portfolio, sized_order):
        """
        Questo oggetto RiskManagerMock consente semplicemente
        la verifica dell'ordine, crea il corrispondente
        OrderEvent e lo aggiunge ad un elenco.
        """
        order_event = OrderEvent(
            sized_order.ticker,
            sized_order.action,
            sized_order.quantity
        )
        return [order_event]


class TestSimpleSignalOrderFillCycleForPortfolioHandler(unittest.TestCase):
    """
    Verifica un semplice ciclo di segnale, ordine e riempimento per il
    PortfolioHandler. Questo è, in effetti, un controllo di integrità.
    """
    def setUp(self):
        """
        Impostare l'oggetto PortfolioHandler fornendolo
        $ 500.000,00 USD di capitale iniziale.
        """
        initial_cash = Decimal("500000.00")
        events_queue = queue.Queue()
        price_handler = PriceHandlerMock()
        position_sizer = PositionSizerMock()
        risk_manager = RiskManagerMock()
        # Create the PortfolioHandler object from the rest
        self.portfolio_handler = PortfolioHandler(
            initial_cash, events_queue, price_handler,
            position_sizer, risk_manager
        )

    def test_create_order_from_signal_basic_check(self):
        """
        Verifica il metodo "_create_order_from_signal"
        per il controllo di integrità.
        """
        signal_event = SignalEvent("MSFT", "BOT")
        order = self.portfolio_handler._create_order_from_signal(signal_event)
        self.assertEqual(order.ticker, "MSFT")
        self.assertEqual(order.action, "BOT")
        self.assertEqual(order.quantity, 0)

    def test_place_orders_onto_queue_basic_check(self):
        """
        Verifica il metodo "_place_orders_onto_queue"
        per il controllo di integrità.
        """
        order = OrderEvent("MSFT", "BOT", 100)
        order_list = [order]
        self.portfolio_handler._place_orders_onto_queue(order_list)
        ret_order = self.portfolio_handler.events_queue.get()
        self.assertEqual(ret_order.ticker, "MSFT")
        self.assertEqual(ret_order.action, "BOT")
        self.assertEqual(ret_order.quantity, 100)

    def test_convert_fill_to_portfolio_update_basic_check(self):
        """
        Verifica il metodo "_convert_fill_to_portfolio_update"
        per il controllo di integrità.
        """
        fill_event_buy = FillEvent(
            datetime.datetime.utcnow(), "MSFT", "BOT",
            100, "ARCA", Decimal("50.25"), Decimal("1.00")
        )
        self.portfolio_handler._convert_fill_to_portfolio_update(fill_event_buy)

        # Controlla i valori di Portfolio all'interno di PortfolioHandler
        port = self.portfolio_handler.portfolio
        self.assertEqual(port.cur_cash, Decimal("494974.00"))

    def test_on_signal_basic_check(self):
        """
        Verifica il metodo "on_signal"
        per il controllo di integrità.
        """
        signal_event = SignalEvent("MSFT", "BOT")
        self.portfolio_handler.on_signal(signal_event)
        ret_order = self.portfolio_handler.events_queue.get()
        self.assertEqual(ret_order.ticker, "MSFT")
        self.assertEqual(ret_order.action, "BOT")
        self.assertEqual(ret_order.quantity, 100)


if __name__ == "__main__":
    unittest.main()
        

La prima operazione è importare i moduli corretti. Usiamo il modulo Decimal, come negli articoli precedenti, così come il modulo unittest . Abbiamo anche bisogno di importare vari oggetti Event utilizzati da PortfolioHandler per comunicare. Infine importiamo lo stesso PortfolioHandler:

            import datetime
from decimal import Decimal
import queue
import unittest

from event import FillEvent, OrderEvent, SignalEvent
from portfolio import PortfolioHandler
        

Dobbiamo creare tre oggetti “fittizi” (vedere l’ articolo precedente per una descrizione degli oggetti fittizi), uno per ciascuno PriceHandlerPositionSizer RiskManager. Il primo, PriceHandlerMock ci fornisce i prezzi denaro / lettera statici per tre azioni: MSFT, GOOG e AMZN. Essenzialmente vogliamo simulare il metodo get_best_bid_ask per i nostri unit test ripetibili:

            class PriceHandlerMock(object):
    def __init__(self):
        pass

    def get_best_bid_ask(self, ticker):
        prices = {
            "MSFT": (Decimal("50.28"), Decimal("50.31")),
            "GOOG": (Decimal("705.46"), Decimal("705.46")),
            "AMZN": (Decimal("564.14"), Decimal("565.14")),
        }
        return prices[ticker]
        

Il secondo oggetto fittizio è il PositionSizerMock. Imposta semplicemente la quantità dell’ordine pari a 100, che è una scelta arbitraria, ma è necessario fissarla per i test unitari. Simula il metodo size_order che si troverà nella “vera” classe PositionSizer, quando sarà completa:

            class PositionSizerMock(object):
    def __init__(self):
        pass

    def size_order(self, portfolio, initial_order):
        """
        Questo oggetto PositionSizerMock modifica semplicemente
        la quantità per essere 100 per qualsiasi azione negoziata.
        """
        initial_order.quantity = 100
        return initial_order
        

L’ultimo oggetto fittizio è il RiskManagerMock. Non fa altro che creare un oggetto OrderEvent e inserirlo in un elenco. Fondamentalmente, non esiste una vera gestione del rischio! Anche se questo può sembrare artificioso, ci consente di eseguire un “controllo di integrità” per verificare che PortfolioHandler può semplicemente effettuare le transazioni più elementari di ordini, esecuzioni e segnali. Man mano che creiamo oggetti RiskManager più sofisticati ,  crescerà la lista di unit test, al fine di testare la nuova funzionalità. In questo modo ci assicuriamo continuamente che la base di codice funzioni come previsto:

            class RiskManagerMock(object):
    def __init__(self):
        pass

    def refine_orders(self, portfolio, sized_order):
        """
        Questo oggetto RiskManagerMock consente semplicemente
        la verifica dell'ordine, crea il corrispondente
        OrderEvent e lo aggiunge ad un elenco.
        """
        order_event = OrderEvent(
            sized_order.ticker,
            sized_order.action,
            sized_order.quantity
        )
        return [order_event]
        

Ora che abbiamo definito i tre oggetti fittizi, possiamo creare gli specifici unit test. Viene chiamata la classe che li esegue TestSimpleSignalOrderFillCycleForPortfolioHandler. Sebbene dettagliata, ci dice esattamente per quale test è stata progettata, vale a dire testare un semplice ciclo di segnale-ordine-riempimento all’interno del gestore del portafoglio.

Per fare ciò, creiamo un saldo di cassa iniziale di 500.000 USD, una coda di eventi e i tre oggetti fittizi sopra menzionati. Infine, creiamo lo stesso PortfolioHandler e lo colleghiamo alla classe di test:

            class TestSimpleSignalOrderFillCycleForPortfolioHandler(unittest.TestCase):
    """
    Verifica un semplice ciclo di segnale, ordine e riempimento per il
    PortfolioHandler. Questo è, in effetti, un controllo di integrità.
    """
    def setUp(self):
        """
        Impostare l'oggetto PortfolioHandler fornendolo
        $ 500.000,00 USD di capitale iniziale.
        """
        initial_cash = Decimal("500000.00")
        events_queue = queue.Queue()
        price_handler = PriceHandlerMock()
        position_sizer = PositionSizerMock()
        risk_manager = RiskManagerMock()
        # Create the PortfolioHandler object from the rest
        self.portfolio_handler = PortfolioHandler(
            initial_cash, events_queue, price_handler,
            position_sizer, risk_manager
        )
        

Il primo test genera semplicemente un falso SignalEvent per acquistare Microsoft. Verifichiamo quindi che sia stato generato l’ordine corretto. Notare che una quantità non è stata impostata in questa fase (è zero). Controlliamo tutte le proprietà per assicurarci che l’ordine sia stato creato correttamente:

                def test_create_order_from_signal_basic_check(self):
        """
        Verifica il metodo "_create_order_from_signal"
        per il controllo di integrità.
        """
        signal_event = SignalEvent("MSFT", "BOT")
        order = self.portfolio_handler._create_order_from_signal(signal_event)
        self.assertEqual(order.ticker, "MSFT")
        self.assertEqual(order.action, "BOT")
        self.assertEqual(order.quantity, 0)
        

Il prossimo test consiste nel verificare se gli ordini sono stati inseriti correttamente nella coda (e recuperati). Si noti che dobbiamo racchiudere il OrderEvent in un elenco, in quanto RiskManager produce un elenco di ordini, a causa della suddetta necessità di coprire eventualmente o aggiungere ulteriori ordini oltre a quelli suggeriti dalla Strategy. Infine, affermiamo che l’ordine restituito (che viene prelevato dalla coda) contiene le informazioni appropriate:

                def test_place_orders_onto_queue_basic_check(self):
        """
        Verifica il metodo "_place_orders_onto_queue"
        per il controllo di integrità.
        """
        order = OrderEvent("MSFT", "BOT", 100)
        order_list = [order]
        self.portfolio_handler._place_orders_onto_queue(order_list)
        ret_order = self.portfolio_handler.events_queue.get()
        self.assertEqual(ret_order.ticker, "MSFT")
        self.assertEqual(ret_order.action, "BOT")
        self.assertEqual(ret_order.quantity, 100)
        

Il seguente test crea un FillEvent, come se fosse stato appena ricevuto da un oggetto ExecutionHandler. Al gestore del portafoglio viene quindi chiesto di convertire il riempimento in un effettivo aggiornamento del portafoglio (ovvero registrare la transazione all’interno dell’oggetto Portfolio).

Il test consiste nel verificare che il saldo corrente all’interno del Portfolio sia effettivamente corretto:

                def test_convert_fill_to_portfolio_update_basic_check(self):
        """
        Verifica il metodo "_convert_fill_to_portfolio_update"
        per il controllo di integrità.
        """
        fill_event_buy = FillEvent(
            datetime.datetime.utcnow(), "MSFT", "BOT",
            100, "ARCA", Decimal("50.25"), Decimal("1.00")
        )
        self.portfolio_handler._convert_fill_to_portfolio_update(fill_event_buy)

        # Controlla i valori di Portfolio all'interno di PortfolioHandler
        port = self.portfolio_handler.portfolio
        self.assertEqual(port.cur_cash, Decimal("494974.00"))
        

Il test finale verifica semplicemente il metodo on_signal creando un oggetto SignalEvent, posizionandolo in coda e quindi recuperandolo per verificare che i valori dell’ordine siano quelli previsti. Questo verifica la gestione di base “end to end” degli oggetti PositionSizer RiskManager:

                def test_on_signal_basic_check(self):
        """
        Verifica il metodo "on_signal"
        per il controllo di integrità.
        """
        signal_event = SignalEvent("MSFT", "BOT")
        self.portfolio_handler.on_signal(signal_event)
        ret_order = self.portfolio_handler.events_queue.get()
        self.assertEqual(ret_order.ticker, "MSFT")
        self.assertEqual(ret_order.action, "BOT")
        self.assertEqual(ret_order.quantity, 100)
        

Possiamo vedere chiaramente che ci sono molti più test da fare qui. Abbiamo solo scalfito la superficie con il tipo di situazioni che possono verificarsi. Tuttavia, è sempre utile disporre di una serie di controlli di integrità. Il framework di unit test è altamente estensibile e quando ci imbattiamo in nuove situazioni / bug, possiamo semplicemente scrivere nuovi test e correggere il problema.

Per completezza puoi trovare il codice completo per il test PortfolioHandler su Github.

Prossimi Passi

Abbiamo ora coperto tre degli oggetti principali per il sistema di gestione degli ordini, vale a dire il Position, il Portfolio e il PortfolioHandler. Questi sono gli aspetti matematici “centrali” del codice e come tali dobbiamo essere sicuri che funzionino come previsto.

Anche se discutere di questi oggetti non è così eccitante come costruire un oggetto Strategy, o anche un RiskManager, è fondamentale che funzionino, altrimenti il ​​resto dell’infrastruttura di backtesting e live trading sarà, nella migliore delle ipotesi, inutile e, nel peggiore dei casi, altamente non redditizia!

Abbiamo molti altri componenti da descrivere oltre a quelli menzionati sopra, tra cui il PriceHandler, la classe Backtest, i vari ExecutionHandler che potrebbero collegarsi a Interactive Brokers o OANDA , così come l’implementazione di un oggetto Strategy non banale .

Nel prossimo articolo vedremo una o più di queste classi.

DataTrader – Trading Algoritmico Avanzato: la Classe Portfolio

DataTrader - trading algoritmico - Parte 3

Nell’articolo precedente nella serie Trading Algoritmico Avanzato abbiamo descritto e presentato il codice e gli unit test iniziali per la classe Position che memorizza le informazioni sulla posizione di un trade. In questo articolo consideriamo la classe Portfolio, utilizzata per memorizzare un elenco di classi Position, nonché un saldo del conto.

Nell’ultimo mese abbiamo fatto molti progressi sul progetto DataTrader , il motore open source per il backtesting e il live-trading che l’oggetto di questi articoli. In effetti, abbiamo finalizzato l’intera “prima bozza” end-to-end del codice, che fa uso di una semplicistica strategia di test (e altamente non redditizia!), usata solo per garantire che il codice funzioni come dovrebbe. Tuttavia, è doveroso scrivere questi articoli in sequenza, descrivendo le funzionalità dei singoli moduli.

In questo modo spero che sarà più facile per molti di voi contribuire al progetto aggiungendo vari nuovi componenti, come gestori del rischio o sizer di portafoglio che altri nella comunità di DataTrading possono utilizzare.

In questa fase c’è poca o nessuna documentazione oltre a questi articoli, ma una parte importante per rendere DataTrader una valida libreria di backtest è prevede una documentazione  estremamente dettagliata. Una volta che il codice sarà più consolidato, inizieremo a produrre una documentazione approfondita e tutorial che dovrebbero aiutare a eseguire il backtest in modo rapido e semplice, indipendentemente dalla tua scelta del sistema operativo o dalla frequenza di trading.

Per ribadire, il progetto può sempre essere trovato su  https://github.com/datatrading-info/DataTrader con una licenza MIT open source.

Promemoria per la progettazione dei componenti

Nell’articolo precedente abbiamo introdotto brevemente i moduli che compongono DataTrader. Ora vediamo di ampliare questo elenco per includere il set “completo” delle componenti necessarie per un backtest.

Molti di questi moduli risulteranno familiari agli utenti di DTForex e al precedente backtester basato su eventi utilizzato (DataBacktest). La principale differenza è che queste classi sono state oggetto di unit test e comprendo molto più funzionalità rispetto alle versioni precedenti.

Il design di DataTrader è il seguente:

  • Position – la classe Position incapsula tutti i dati associati a una posizione aperta in un asset. In altre parole, tiene traccia dei profitti e delle perdite (PnL) realizzati e non realizzati calcolando la media delle “gambe” multiple della transazione, inclusi i costi di transazione.
  • Portfolio – La classe Portfolio che racchiude un elenco di Position, nonché un saldo del conto, equity e PnL.
  • PositionSizer – La classe PositionSizer fornisce aPortfolioHandler(vedi seguito) una guida su come dimensionare le posizioni una volta ricevuto un segnale da una strategia. Ad esempio, PositionSizer potrebbe incorporare un approccio basato sul criterio di Kelly .
  • RiskManager – La classe  RiskManager è utilizzata da PortfolioHandler per verificare, modificare o porre il veto a qualsiasi ipotetico trade che passa dal PositionSizer, in base alla corrente composizione del portafoglio e a considerazioni sul rischio esterno (come la correlazione agli indici o la volatilità).
  • PortfolioHandler – La classe PortfolioHandler è responsabile della gestione del Portfolio, dell’interazione con RiskManagerPositionSizer nonché dell’invio degli ordini che devono essere eseguiti da un ExecutionHandler.
  • Event – La classe Event e la sua sottoclasse ereditata vengono utilizzate per trasmettere i messaggi Event a ciascun componente del sistema. Vengono sempre inviati a una coda di eventi Python per essere letti da questi componenti. Le sottoclassi di Event includono TickEventOrderEventSignalEventFillEvent.
  • Strategy – La classe Strategy gestisce la logica di generazione dei segnali di trading in base alle informazioni sui prezzi. Invia questi segnali al PortfolioHandler.
  • ExecutionHandler : La classe ExecutionHandler legge gli OrderEvent e produce FillEvent, in base a uno scenario di riempimento simulato o alle informazioni di riempimento effettive ricevute da un broker, come Interactive Brokers.
  • PriceHandler – Questa classe è progettata per essere eredita da sottoclassi che consentono la connessione verso più origini dati come, ad esempio, CSV, HDF5, RDBMS (MySQL, SQLServer, PostgreSQL), MongoDB o un’API di streaming live di un broker.
  • Backtest : La classe Backtest lega insieme tutti i componenti precedenti per produrre un backtest simulato. Viene “sostituito” con una classe per un motore di live trading (da sviluppare), insieme a un PriceHandler ExecutionHandler, una volta che si vuole passare al live trading.

Cosa manca a questo elenco? Probabilmente il modulo mancante più importante è un meccanismo per calcolare le statistiche della strategia di trading e visualizzare i risultati. Ciò include metriche di performance come il Sharpe Ratio e il Maximum Drawdown, nonché una curva di equity, un profilo dei rendimenti e una curva di drawdown.

Piuttosto che accoppiare fortemente i risultati alla classe PortfolioHandler, come nei precedenti DTForex e DataBacktest, generiamo una classe Result o Statistic che calcola e memorizza le metriche delle performance, in base ai risultati di un backtest . Possiamo quindi utilizzare queste classi per produrre ulteriori funzionalità “client”, come un’interfaccia web o uno strumento GUI, per visualizzare i risultati di un backtest.

Inoltre, nell’elenco precedente non si fa menzione di robustezza, memorizzazione o monitoraggio. Questi sono componenti cruciali in un motore di backtesting e live trading da utilizzare in produzione e verranno aggiunti man mano che il progetto si sviluppa. Questi componenti probabilmente faranno uso di una qualche forma di infrastruttura server / cloud, come Amazon Web Services (o altri fornitori di servizi cloud).

Rivolgiamo ora la nostra attenzione alla classe Portfolio. Negli articoli successivi considereremo il PortfolioHandler e come si interagisce con il PositionSizerRiskManager.

Portfolio

Da sottolineare di nuovo che in questo progetto la classe Portfolio implementata per DataTrader è molto diversa da quella utilizzata in DTForex o DataBacktest. In DataTrader abbiamo diviso il portfolio in due classi, una chiamata Portfolioe l’altra chiamata PortfolioHandler.

Perchè adottiamo questo approccio? In primo luogo, si vuole creare una classe Portfolio snella che si occupi solamente di memorizzare il valore in contanti del portafoglio e un elenco di oggetti Position. L’unico metodo che viene chiamato pubblicamente (per prendere in prestito un termine C ++!) è  transact_position, che prevede semplicemente di aggiornare la posizione di Portfolio in una particolare equity. Gestisce tutti i necessari calcoli di profitti e perdite (PnL), attualizzando sia il PnL realizzato che il PnL non realizzato.

Questo significa che la classe PortfolioHandler può concentrarsi su altre attività, come l’interazione con le classi RiskManagerPositionSizer, lasciando tutti i necessari calcoli finanziari a Portfolio. Questo approccio permette anche di testare in modo più semplice ogni classe individualmente, poiché una è focalizzata nel calcolo finanziario, mentre l’altra viene utilizzata maggiormente per interagire con gli altri componenti.

Descriveremo i listati di codice per entrambi i file position.pyposition_test.py  e quindi illustreremo il funzionamento di ciascuno di essi.

Da notare che questi codici sono soggetti a modifiche, poiché si apportano continuamente modifiche per migliorare questo progetto. Infine speriamo che altri collaboreranno fornendo richieste pull al codebase.

portfolio.py

            from decimal import Decimal
from position import Position


class Portfolio(object):
    def __init__(self, price_handler, cash):
        """
        Alla creazione, l'oggetto Portfolio non contiene posizioni
        e tutti i valori vengono "ripristinati" con il capitale
        iniziale e senza PnL - realizzato o non realizzato.
        """
        self.price_handler = price_handler
        self.init_cash = cash
        self.cur_cash = cash
        self.positions = {}
        self._reset_values()

    def _reset_values(self):
        """
        Questo viene chiamato dopo ogni aggiunta di
        posizione o modifica. Permette che i calcoli
        siano eseguito "da zero" in modo da minimizzare
        errori.

        Tutto il contanti venie ripristinato ai valori
        iniziali e il PnL è impostato a zero.
        """
        self.cur_cash = self.init_cash
        self.equity = self.cur_cash
        self.unrealised_pnl = Decimal('0.00')
        self.realised_pnl = Decimal('0.00')

    def _update_portfolio(self):
        """
         Aggiorna i valori totali del portafoglio (contanti, capitale,
        PnL non realizzato, PnL realizzato, costo base ecc.)
        su i valori correnti per tutti i ticker.

        Questo metodo viene chiamato dopo ogni modifica della posizione.
        """
        for ticker in self.positions:
            pt = self.positions[ticker]
            self.unrealised_pnl += pt.unrealised_pnl
            self.realised_pnl += pt.realised_pnl
            self.cur_cash -= pt.cost_basis
            pnl_diff = pt.realised_pnl - pt.unrealised_pnl
            self.cur_cash += pnl_diff
            self.equity += (
                pt.market_value - pt.cost_basis + pnl_diff
            )

    def _add_position(self, action, ticker, quantity, price, commission):
        """
        Aggiunge un nuovo oggetto Position al Portfolio. Questo
        richiede di ottenere il miglior prezzo bid / ask dal
        gestore del prezzo al fine di calcolare un ragionevole
        " valore di mercato ".

        Una volta aggiunta la posizione, i valori del portafoglio
        vengono aggiornati.
        """
        self._reset_values()
        if ticker not in self.positions:
            bid, ask = self.price_handler.get_best_bid_ask(ticker)
            position = Position(
                action, ticker, quantity,
                price, commission, bid, ask
            )
            self.positions[ticker] = position
            self._update_portfolio()
        else:
            print(
                "Ticker %s is already in the positions list. " \
                "Could not add a new position." % ticker
            )

    def _modify_position(self, action, ticker, quantity, price, commission):
        """
        Modifica un oggetto Posizione corrente nel Portafoglio.
        Ciò richiede di ottenere il miglior prezzo bid / ask dal
        gestore del prezzo al fine di calcolare un ragionevole
        " valore di mercato ".

        Una volta modificata la posizione, il portafoglio valorizza
        vengono aggiornati.
        """
        self._reset_values()
        if ticker in self.positions:
            self.positions[ticker].transact_shares(
                action, quantity, price, commission
            )
            bid, ask = self.price_handler.get_best_bid_ask(ticker)
            self.positions[ticker].update_market_value(bid, ask)
            self._update_portfolio()
        else:
            print(
                "Ticker %s not in the current position list. " \
                "Could not modify a current position." % ticker
            )

    def transact_position(self, action, ticker, quantity, price, commission):
        """
        Gestisce qualsiasi nuova posizione o modifica a 
        una posizione corrente, chiamando il rispettivo
        metodi _add_position e _modify_position. 

        Quindi, questo singolo metodo verrà chiamato da 
        PortfolioHandler per aggiornare il Portfolio stesso.
        """
        if ticker not in self.positions:
            self._add_position(
                action, ticker, quantity,
                price, commission
            )
        else:
            self._modify_position(
                action, ticker, quantity,
                price, commission
            )
        

Come per il codice di position.py nell’articolo precedente, facciamo un ampio uso del modulo Decimal di Python . Come accennato in precedenza, questa è un requisito fondamentale nei calcoli finanziari, altrimenti si introducono errori di arrotondamento dovuti alla matematica delle operazioni in virgola mobile .

Nel metodo di inizializzazione della classe Portfolio prendiamo due parametri di input: un PriceHandler e un saldo di cassa iniziale (che è un tipo di dati Decimal, non un valore in virgola mobile). Questo è tutto ciò di cui abbiamo bisogno per creare un’istanza Portfolio.

Nel metodo stesso creiamo due valori: una liquidità iniziale e una liquidità corrente. Creiamo quindi un dizionario di posizioni e infine chiamiamo il metodo _reset_values, che azzera tutti i calcoli di cassa e azzera tutti i valori PnL:

            from decimal import Decimal
from position import Position


class Portfolio(object):
    def __init__(self, price_handler, cash):
        """
        Alla creazione, l'oggetto Portfolio non contiene posizioni
        e tutti i valori vengono "ripristinati" con il capitale
        iniziale e senza PnL - realizzato o non realizzato.
        """
        self.price_handler = price_handler
        self.init_cash = cash
        self.cur_cash = cash
        self.positions = {}
        self._reset_values()
        

Come accennato in precedenza, _reset_values è richiamato durante l’inizializzazione, ma è anche durante la modifica di ogni posizione. Può sembrare ingombrante, ma riduce notevolmente gli errori nel processo di calcolo. Reimposta semplicemente i valori di liquidità e di capitale correnti al valore iniziale e quindi azzera i valori PnL:

                def _reset_values(self):
        """
        Questo viene chiamato dopo ogni aggiunta di
        posizione o modifica. Permette che i calcoli
        siano eseguito "da zero" in modo da minimizzare
        errori.

        Tutto il contanti venie ripristinato ai valori
        iniziali e il PnL è impostato a zero.
        """
        self.cur_cash = self.init_cash
        self.equity = self.cur_cash
        self.unrealised_pnl = Decimal('0.00')
        self.realised_pnl = Decimal('0.00')
        

Il metodo successivo è _update_portfolio. Questo metodo viene chiamato anche dopo ogni modifica della posizione (cioè transazione). Per ogni ticker nel Portfolio, ogni PnL delle posizioni sono aggiunti al il PnL non realizzato e realizzato dell’intero portafoglio, mentre la liquidità disponibile corrente viene ridotta in base al costo base delle posizioni . Infine, la differenza tra PnL realizzato e non realizzato viene applicata alla liquidità corrente e si rettifica l’equity totale del portafoglio:

                def _update_portfolio(self):
        """
         Aggiorna i valori totali del portafoglio (contanti, capitale,
        PnL non realizzato, PnL realizzato, costo base ecc.)
        su i valori correnti per tutti i ticker.

        Questo metodo viene chiamato dopo ogni modifica della posizione.
        """
        for ticker in self.positions:
            pt = self.positions[ticker]
            self.unrealised_pnl += pt.unrealised_pnl
            self.realised_pnl += pt.realised_pnl
            self.cur_cash -= pt.cost_basis
            pnl_diff = pt.realised_pnl - pt.unrealised_pnl
            self.cur_cash += pnl_diff
            self.equity += (
                pt.market_value - pt.cost_basis + pnl_diff
            )
        

Anche se questo può sembrare un po’ complesso, abbiamo implementato questi calcoli in modo che riflettano il le logiche in cui i portafogli vengono modificati nelle principali società di intermediazione, in particolare Interactive Brokers . Significa che il motore di backtesting dovrebbe produrre valori vicini a quelli del live trading, nell’ipotesi di slippage e costi di transazione.

I prossimi due metodi sono _add_position _modify_position. In origine, questi due metodi erano metodi richiamabili “pubblicamente” per creare nuove posizioni e successivamente modificarle. In seguito abbiamo voluto rendere trasparente all’utente la gestione di aggiungere o modificare una posizione, e così abbiamo introdotto un metodo wrapper, chiamato transact_position che ora utilizza il metodo corretto  a seconda dell’esistenza di un ticker nell’elenco delle posizioni.

Il metodo _add_position prevede in input un’azione (acquisto o vendita), un simbolo ticker e una quantità di asset, il prezzo di riempimento e il costo della commissione, come parametri. Per prima cosa si ripristina i valori dell’intero portafoglio e quindi otteniamo il miglior prezzo di offerta e domanda del ticker dall’oggetto di gestione dei prezzi. Quindi creiamo il nuovo Position, utilizzando questi prezzi di offerta e domanda per ottenere un “valore di mercato” aggiornato. Infine aggiungiamo l’istanza’ Position al dizionario delle posizioni, utilizzando il simbolo ticker come chiave*.

Da notare che chiediamo a _update_portfolio di aggiornare tutti i valori di mercato in questa fase. Il metodo gestisce anche il caso in cui la posizione esiste già, stampando alcune informazioni sulla console. In futuro sostituiremo tutte le istanze dell’output sulla console con meccanismi di registrazione più robusti.

*Questo comporta in seguito implicazioni di progettazione, quando si tratterà di rinominare i simboli ticker, classi di azioni multiple e altre azioni societarie. Tuttavia, per semplicità in questa fase utilizzeremo il simbolo ticker poiché è unico per i nostri scopi.

                def _add_position(self, action, ticker, quantity, price, commission):
        """
        Aggiunge un nuovo oggetto Position al Portfolio. Questo
        richiede di ottenere il miglior prezzo bid / ask dal
        gestore del prezzo al fine di calcolare un ragionevole
        " valore di mercato ".

        Una volta aggiunta la posizione, i valori del portafoglio
        vengono aggiornati.
        """
        self._reset_values()
        if ticker not in self.positions:
            bid, ask = self.price_handler.get_best_bid_ask(ticker)
            position = Position(
                action, ticker, quantity,
                price, commission, bid, ask
            )
            self.positions[ticker] = position
            self._update_portfolio()
        else:
            print(
                "Ticker %s is already in the positions list. " \
                "Could not add a new position." % ticker
            )
        

_modify_position è simile ad “add_position” tranne per il fatto che richiama transact_shares della classe Position invece di creare una nuova posizione:

                def _modify_position(self, action, ticker, quantity, price, commission):
        """
        Modifica un oggetto Posizione corrente nel Portafoglio.
        Ciò richiede di ottenere il miglior prezzo bid / ask dal
        gestore del prezzo al fine di calcolare un ragionevole
        " valore di mercato ".

        Una volta modificata la posizione, il portafoglio valorizza
        vengono aggiornati.
        """
        self._reset_values()
        if ticker in self.positions:
            self.positions[ticker].transact_shares(
                action, quantity, price, commission
            )
            bid, ask = self.price_handler.get_best_bid_ask(ticker)
            self.positions[ticker].update_market_value(bid, ask)
            self._update_portfolio()
        else:
            print(
                "Ticker %s not in the current position list. " \
                "Could not modify a current position." % ticker
            )
        

Il metodo che viene effettivamente chiamato esternamente è transact_position. Comprende sia la creazione che la modifica di un oggetto Position. Sceglie semplicemente il metodo corretto tra _add_position _modify_position quando si effettua una nuova transazione:

                def transact_position(self, action, ticker, quantity, price, commission):
        """
        Gestisce qualsiasi nuova posizione o modifica a 
        una posizione corrente, chiamando il rispettivo
        metodi _add_position e _modify_position. 

        Quindi, questo singolo metodo verrà chiamato da 
        PortfolioHandler per aggiornare il Portfolio stesso.
        """
        if ticker not in self.positions:
            self._add_position(
                action, ticker, quantity,
                price, commission
            )
        else:
            self._modify_position(
                action, ticker, quantity,
                price, commission
            )
        

Questo conclude la classe Portfolio. Fornisce un robusto meccanismo autonomo per raggruppare le classi Position con un saldo di cassa.

Per completezza puoi trovare il codice completo per la classe Portfolio su Github.

portfolio_test.py

Come per position_test.py, abbiamo creato portfolio_test.py, che include un unit test per verificare l’integrità di base per più transazioni di azioni AMZN e GOOG. Sicuramente è necessario fare più lavoro per controllare portafogli più grandi e diversificati, ma almeno possiamo assicurarci che il sistema stia calcolando i valori come dovrebbe.

Come per i test per la classe Position, questi sono stati confrontati con i valori prodotti da Interactive Brokers utilizzando l’account demo di Trader Workstation. Come prima, in futuro è sempre possibile trovare nuovi casi limite e bug, ma speriamo che l’attuale controllo di integrità e test di calcolo dovrebbero fornire fiducia nel funzionamento del Portfolio.

Il listato completo di position_test.py è il seguente:

            from decimal import Decimal
import unittest

from portfolio import Portfolio


class PriceHandlerMock(object):
    def __init__(self):
        pass

    def get_best_bid_ask(self, ticker):
        prices = {
            "GOOG": (Decimal("705.46"), Decimal("705.46")),
            "AMZN": (Decimal("564.14"), Decimal("565.14")),
        }
        return prices[ticker]


class TestAmazonGooglePortfolio(unittest.TestCase):
    """
    Prova un portafoglio composto da Amazon e
    Google con vari ordini per creare
    "round-trip" per entrambi.

    Questi ordini sono stati eseguiti in un conto demo
    di Interactive Brokers e verificata l'uguaglianza
    per contanti, equità e PnL.
    """

    def setUp(self):
        """
        Imposta l'oggetto Portfolio che memorizzerà una
        raccolta di oggetti Position, prevedendo
        $500.000,00 USD per il saldo iniziale del conte
        """
        ph = PriceHandlerMock()
        cash = Decimal("500000.00")
        self.portfolio = Portfolio(ph, cash)


    def test_calculate_round_trip(self):
        """
        Acquisto/vendita più lotti di AMZN e GOOG
        a vari prezzi / commissioni per controllare
        il calcolo e la gestione dei costi.
        """
        # Acquista 300 AMZN su due transazion
        self.portfolio.transact_position(
            "BOT", "AMZN", 100,
            Decimal("566.56"), Decimal("1.00")
        )
        self.portfolio.transact_position(
            "BOT", "AMZN", 200,
            Decimal("566.395"), Decimal("1.00")
        )
        # Acquista 200 GOOG su una transazione
        self.portfolio.transact_position(
            "BOT", "GOOG", 200,
            Decimal("707.50"), Decimal("1.00")
        )
        # Aggiunge 100 azioni sulla posizione di AMZN
        self.portfolio.transact_position(
            "SLD", "AMZN", 100,
            Decimal("565.83"), Decimal("1.00")
        )
        # Aggiunge 200 azioni alla posizione di GOOG
        self.portfolio.transact_position(
            "BOT", "GOOG", 200,
            Decimal("705.545"), Decimal("1.00")
        )
        # Vende 200 azioni di AMZN
        self.portfolio.transact_position(
            "SLD", "AMZN", 200,
            Decimal("565.59"), Decimal("1.00")
        )
        # Transazioni Multiple costruite in una (in IB)
        # Vendi 300 GOOG dal portfolio
        self.portfolio.transact_position(
            "SLD", "GOOG", 100,
            Decimal("704.92"), Decimal("1.00")
        )
        self.portfolio.transact_position(
            "SLD", "GOOG", 100,
            Decimal("704.90"), Decimal("0.00")
        )
        self.portfolio.transact_position(
            "SLD", "GOOG", 100,
            Decimal("704.92"), Decimal("0.50")
        )
        # Infine vendiamo le rimanenti 100 azioni di GOOG
        self.portfolio.transact_position(
            "SLD", "GOOG", 100,
            Decimal("704.78"), Decimal("1.00")
        )

        # I numeri seguenti sono derivati dall'account demo di 
        # Interactive Brokers usando i seguenti trade con i 
        # prezzi forniti dal loro feed dati in demo.
        self.assertEqual(self.portfolio.cur_cash, Decimal("499100.50"))
        self.assertEqual(self.portfolio.equity, Decimal("499100.50"))
        self.assertEqual(self.portfolio.unrealised_pnl, Decimal("0.00"))
        self.assertEqual(self.portfolio.realised_pnl, Decimal("-899.50"))


if __name__ == "__main__":
    unittest.main()
        

Il primo compito è eseguire le  corrette importazioni. Importiamo il modulo unittest e l’oggetto Portfolio stesso:

            from decimal import Decimal
import unittest

from portfolio import Portfolio
        

Per creare una classe Portfolio funzionante , abbiamo bisogno di una classe PriceHandler per fornire valori bid e ask per ogni ticker. Tuttavia, non abbiamo ancora codificato alcun oggetto di gestione dei prezzi, quindi cosa dobbiamo fare?

A quanto pare, questo è un modello comune negli unit test. Per superare questa difficoltà, possiamo creare un oggetto fittizio . In sostanza, un mock-object è una classe che simula il comportamento della sua controparte reale, consentendo così di testare la funzionalità su altre classi che ne fanno uso. Quindi è necessario creare una classe PriceHandlerMock che fornisca la stessa interfaccia di a PriceHandler, ma restituisca solo valori preimpostati, invece di eseguire calcoli su prezzi “reali”.

L’oggetto PriceHandlerMock ha un metodo di inizializzazione vuoto, ma espone il metodo get_best_bid_ask che si trova sul reale PriceHandler. Restituisce semplicemente valori bid/ask preimpostati per le azioni GOOG e AMZN con cui effettueremo le transazioni nei seguenti ulteriori unit test:

            class PriceHandlerMock(object):
    def __init__(self):
        pass

    def get_best_bid_ask(self, ticker):
        prices = {
            "GOOG": (Decimal("705.46"), Decimal("705.46")),
            "AMZN": (Decimal("564.14"), Decimal("565.14")),
        }
        return prices[ticker]
        

Gli unit test consistono nel creare una nuova classe chiamata in modo piuttosto parlante TestAmazonGooglePortfolio che, come tutti gli unit test in Python, è derivato dalla classe unittest.TestCase.

Nel metodo setUp impostiamo l’oggetto fittizio del gestore del prezzo, il saldo iniziale e creiamo un Portfolio:

            class TestAmazonGooglePortfolio(unittest.TestCase):
    """
    Prova un portafoglio composto da Amazon e
    Google con vari ordini per creare
    "round-trip" per entrambi.

    Questi ordini sono stati eseguiti in un conto demo
    di Interactive Brokers e verificata l'uguaglianza
    per contanti, equità e PnL.
    """

    def setUp(self):
        """
        Imposta l'oggetto Portfolio che memorizzerà una
        raccolta di oggetti Position, prevedendo
        $500.000,00 USD per il saldo iniziale del conte
        """
        ph = PriceHandlerMock()
        cash = Decimal("500000.00")
        self.portfolio = Portfolio(ph, cash)
        

L’unico metodo di unit test che creiamo è chiamato test_calculate_round_trip. Il suo obiettivo è calcolare i trade round-trip per AMZN e GOOG, assicurandosi che i calcoli finanziari delle classi PositionPortfolio siano corretti.  In questo caso “corretto” significa che corrispondono ai valori calcolati da Interactive Brokers quando abbiamo eseguito questa situazione in Trader Workstation. Abbiamo codificato questi valori negli unit test.

La prima parte del metodo esegue più transazioni sia per GOOG che per AMZN a vari prezzi e costi di commissione. Abbiamo preso questi prezzi direttamente da quelli calcolati da Interactive Brokers (IB) quando abbiamo effettuato questi trade nel conto demo. “BOT” è la terminologia IB per l’acquisto di un’azione, mentre “SLD” è la terminologia per la vendita di un’azione.

Una volta completata la serie completa di transazioni, le posizioni vengono entrambe compensate con quantità zero. Non avranno alcun PnL non realizzato, ma avranno un PnL realizzato, così come modifiche alla liquidità corrente e al valore del patrimonio netto totale:

                def test_calculate_round_trip(self):
        """
        Acquisto/vendita più lotti di AMZN e GOOG
        a vari prezzi / commissioni per controllare
        il calcolo e la gestione dei costi.
        """
        # Acquista 300 AMZN su due transazion
        self.portfolio.transact_position(
            "BOT", "AMZN", 100,
            Decimal("566.56"), Decimal("1.00")
        )
        self.portfolio.transact_position(
            "BOT", "AMZN", 200,
            Decimal("566.395"), Decimal("1.00")
        )
        # Acquista 200 GOOG su una transazione
        self.portfolio.transact_position(
            "BOT", "GOOG", 200,
            Decimal("707.50"), Decimal("1.00")
        )
        # Aggiunge 100 azioni sulla posizione di AMZN
        self.portfolio.transact_position(
            "SLD", "AMZN", 100,
            Decimal("565.83"), Decimal("1.00")
        )
        # Aggiunge 200 azioni alla posizione di GOOG
        self.portfolio.transact_position(
            "BOT", "GOOG", 200,
            Decimal("705.545"), Decimal("1.00")
        )
        # Vende 200 azioni di AMZN
        self.portfolio.transact_position(
            "SLD", "AMZN", 200,
            Decimal("565.59"), Decimal("1.00")
        )
        # Transazioni Multiple costruite in una (in IB)
        # Vendi 300 GOOG dal portfolio
        self.portfolio.transact_position(
            "SLD", "GOOG", 100,
            Decimal("704.92"), Decimal("1.00")
        )
        self.portfolio.transact_position(
            "SLD", "GOOG", 100,
            Decimal("704.90"), Decimal("0.00")
        )
        self.portfolio.transact_position(
            "SLD", "GOOG", 100,
            Decimal("704.92"), Decimal("0.50")
        )
        # Infine vendiamo le rimanenti 100 azioni di GOOG
        self.portfolio.transact_position(
            "SLD", "GOOG", 100,
            Decimal("704.78"), Decimal("1.00")
        )

        # I numeri seguenti sono derivati dall'account demo di 
        # Interactive Brokers usando i seguenti trade con i 
        # prezzi forniti dal loro feed dati in demo.
        self.assertEqual(self.portfolio.cur_cash, Decimal("499100.50"))
        self.assertEqual(self.portfolio.equity, Decimal("499100.50"))
        self.assertEqual(self.portfolio.unrealised_pnl, Decimal("0.00"))
        self.assertEqual(self.portfolio.realised_pnl, Decimal("-899.50"))


if __name__ == "__main__":
    unittest.main()
        

Chiaramente c’è spazio per produrre molti più unit test per questa classe, specialmente quando vengono utilizzate posizioni più esotiche, come quelle con forex, futures o opzioni. Tuttavia, in questa fase vogliamo semplicemente gestire azioni ed ETF, il che significa una gestione delle posizioni più diretta.

Prossimi Passi

Ora che abbiamo discusso sia le classo  Position Portfolio dobbiamo approfondire il PortfolioHandler. Questa è la classe che interagisce con PositionSizerRiskManager per produrre ordini e ricevere esecuzioni che alla fine determinano il portafoglio azionario (e quindi la redditività!).

Dal momento che siamo molto più avanti con l’effettivo sviluppo effettivo del software di DataTrader rispetto agli articoli che spiegano come funziona, presenteremo al più presto alcune strategie di trading avanzate utilizzando questo software, piuttosto che aspettare che tutti gli articoli siano stati completati.

DataTrader – Trading Algoritmico Avanzato: Gestione delle Posizioni

DataTrader - trading algoritmico - Parte 2

In questo articolo descriviamo i primi risultati della nuova infrastruttura avanzata di trading che vogliamo implementare, seguendo le linee guida pubblicate nel precedente articolo.

In queste linee guida abbiamo previsto di riutilizzare e migliorare il più possibile il codice base di DataBacktest (backtesting basato su eventi) e di DTForex al fine di non duplicare sforzi e lavoro. Nonostante vari tentativi di utilizzare gran parte del codice di queste due librerie, abbiamo concluso che è necessario “ricominciare da zero” e rivedere le modalità di gestione/dimensionamento delle posizioni, la gestione del rischio e la costruzione del portafoglio.

In particolare, bisogna modificare il design di DTForex e di DataBacktest in modo da integrare il corretto dimensionamento della posizione e dei livelli di gestione del rischio, che non erano presenti nelle versioni precedenti. Questi due componenti forniscono solidi strumenti per la costruzione del portafoglio che aiutano nella creazione di portafogli “professionali”, piuttosto che quelli presenti in un semplice backtester vettorizzato.

In questa fase non prevediamo di implementare nessuna logica specifica per la gestione del rischio o di dimensionamento della posizione, poiché questa logica è altamente specifica per ogni singolo trader. Sarà comunque oggetto di esempi per semplici scenari come il posizionamento tramite il criterio di Kelly o livelli di rischio in base alla volatilità, ma non implementeremo complesse gestioni per il rischio e per le posizioni. In questo modo è possibile utilizzare le versioni “standard” che sviluppiamo, ma anche sostituire completamente questi componenti con la logica personalizzata che meglio si adatta alle proprie esigenze. La filosofia del sistema non costringe a utilizzare una particolare metodologia di gestione del rischio.

Ad oggi abbiamo codificato le basi per la gestione del portafoglio. L’intero strumento di backtest e trading, che abbiamo chiamato DataTrader , è lungi dall’essere pronto per la produzione. In effetti, possiamo dire che in questa fase siamo in uno stato “pre-alfa”! Nonostante il fatto che il sistema sia agli inizi del suo sviluppo, abbiamo compiuto uno sforzo significativo per effettuare gli unit test delle prime componenti, nonché nel testare i valori calcolati dal broker esterno. Al momento siamo abbastanza fiduciosi nel meccanismo di gestione della posizione. Tuttavia, in caso di nuovi casi limite, si possono semplicemente aggiungere nuovi unit test per migliorare ulteriormente la robustezza.

Il progetto è ora disponibile (in uno stato estremamente precoce!) su github.com/datatrading-info/DataTrader con licenza MIT open source liberale. In questa fase manca la documentazione, quindi aggiungo il link solo per coloro che desiderano navigare nel codice.

Progettazione dei componenti

Nel precedente articolo di questa serie sul Trading Algoritmico Avanzato abbiamo descritto la roadmap per lo sviluppo dell’infrastruttura software. In questo articolo descriviamo uno degli aspetti più importanti del sistema, ovvero la componente Position, che costituisce la base del Portfolioe successivamente del PortfolioHandler.

Quando si progetta un tale sistema è necessario considerare come si dividono le comportamenti in vari sottomoduli separati . Ciò non solo permette di evitare un forte accoppiamento dei componenti, ma consente anche test molto più semplici, poiché ogni componente può essere testato separatamente.

Il design scelto per l’infrastruttura di gestione del portafoglio è costituito dai seguenti componenti:

  • Position : questa classe incapsula tutti i dati associati a una posizione aperta per un asset. In altre parole, tiene traccia dei profitti e delle perdite (PnL) realizzati e non realizzati calcolando la media delle “gambe” multiple della transazione, inclusi i costi di transazione.
  • Portfolio – La classe Portfolio che racchiude un elenco di posizioni, nonché un saldo del conto, equity e PnL.
  • PositionSizer – La classe PositionSizer fornisce al PortfolioHandler (di seguito) una guida su come dimensionare le posizioni una volta ricevuto un segnale di strategia. Ad esempio, PositionSizer potrebbe incorporare un approccio del criterio di Kelly.
  • RiskManager – Il RiskManager viene utilizzato dal PortfolioHandler per verificare, modificare o porre il veto a qualsiasi negoziazione suggerita che passa dal PositionSizer, sulla base dell’attuale composizione del portafoglio e di considerazioni di rischio esterno (come la correlazione agli indici o la volatilità).
  • PortfolioHandler – La classe PortfolioHandler è responsabile della gestione del portafoglio corrente, interagendo con RiskManager e PositionSizer, nonché inviando ordini da eseguire da un gestore di esecuzione.

Notare che questa organizzazione di componenti è in qualche modo diversa dal modo in cui opera il sistema di backtest in DTForex . In tal caso l’oggetto Portfolio è l’equivalente alla precedente classe PortfolioHandler. In questo nuovo progetto sono stati divise in due classi separate in modo da prevedere una gestione molto più semplice del rischio  e del dimensionamento della posizione con le classi RiskManagerPositionSizer.

Rivolgeremo ora la nostra attenzione alla classe Position. Negli articoli successivi vedremo le classi PortfolioPortfolioHandler e le componenti per il dimensionamento del rischio/posizione.

La classe Position

La classe Position è molto simile all’omonima classe nel progetto DTForex , con la differenza che è stata progettata, almeno in questa fase, per essere utilizzata con strumenti azionari, piuttosto che nel mercato forex. Quindi non esiste la nozione di una valuta “base” o “quotata”. Tuttavia, conserviamo la possibilità di attualizzare il PnL non realizzato, tramite l’aggiornamento dei prezzi bid/ask quotati sul mercato.

Di seguito il listato  completo del codice e poi la descrizione del suo funzionamento.

Da notare che qualsiasi di questi listati è soggetto a modifiche, poiché si prevedono aggiornamenti continui a questo progetto.

            from decimal import Decimal


TWOPLACES = Decimal("0.01")
FIVEPLACES = Decimal("0.00001")


class Position(object):
    def __init__(
        self, action, ticker, init_quantity,
        init_price, init_commission,
        bid, ask
    ):
        """
        Imposta il "conto" iniziale della posizione che è zero per
        la maggior parte degli item, ad eccezione dell'iniziale
        acquisto / vendita.

        Quindi calcola i valori iniziali e infine aggiorna il
        valore di mercato della transazione.
        """
        self.action = action
        self.ticker = ticker
        self.quantity = init_quantity
        self.init_price = init_price
        self.init_commission = init_commission

        self.realised_pnl = Decimal("0.00")
        self.unrealised_pnl = Decimal("0.00")

        self.buys = Decimal("0")
        self.sells = Decimal("0")
        self.avg_bot = Decimal("0.00")
        self.avg_sld = Decimal("0.00")
        self.total_bot = Decimal("0.00")
        self.total_sld = Decimal("0.00")
        self.total_commission = init_commission

        self._calculate_initial_value()
        self.update_market_value(bid, ask)

    def _calculate_initial_value(self):
        """
        A seconda che l'azione fosse un acquisto o una vendita (" BOT "
        o " SLD ") si calcola il costo medio di acquisto, il costo totale
        di acquisto, prezzo medio e costo basi.

        Infine, calcola il totale netto con e senza commissioni.

        """

        if self.action == "BOT":
            self.buys = self.quantity
            self.avg_bot = self.init_price.quantize(FIVEPLACES)
            self.total_bot = (self.buys * self.avg_bot).quantize(TWOPLACES)
            self.avg_price = (
                (self.init_price * self.quantity + self.init_commission)/self.quantity
            ).quantize(FIVEPLACES)
            self.cost_basis = (
                self.quantity * self.avg_price
            ).quantize(TWOPLACES)
        else:  # action == "SLD"
            self.sells = self.quantity
            self.avg_sld = self.init_price.quantize(FIVEPLACES)
            self.total_sld = (self.sells * self.avg_sld).quantize(TWOPLACES)
            self.avg_price = (
                (self.init_price * self.quantity - self.init_commission)/self.quantity
            ).quantize(FIVEPLACES)
            self.cost_basis = (
                -self.quantity * self.avg_price
            ).quantize(TWOPLACES)
        self.net = self.buys - self.sells
        self.net_total = (self.total_sld - self.total_bot).quantize(TWOPLACES)
        self.net_incl_comm = (self.net_total - self.init_commission)
                               .quantize(TWOPLACES)

    def update_market_value(self, bid, ask):
        """
        Il valore di mercato è difficile da calcolare davo che abbiamo accesso
        alla parte superiore del portafoglio ordini tramite Interactive
        Brokers, il che significa che il vero prezzo è sconosciuto
        fino all'esecuzione.

        Tuttavia, può essere stimato tramite il prezzo medio come
        differenza tra bid e ask. Una volta calcolato il valore di mercato,
        questo consente il calcolo del profitto realizzato e non realizzato,
        e la perdita per qualsiasi transazione.
        """
        midpoint = (bid+ask)/Decimal("2.0")
        self.market_value = (
            self.quantity * midpoint
        ).quantize(TWOPLACES)

        self.unrealised_pnl = (
            self.market_value - self.cost_basis
        ).quantize(TWOPLACES)
        
        self.realised_pnl = (
            self.market_value + self.net_incl_comm
        )

    def transact_shares(self, action, quantity, price, commission):
        """
        Calcola le rettifiche alla classe Position che si verificano
        una volta acquistate e vendute nuove azioni.

        Si preoccupa di aggiornare la media e il totale degli 
        acquisti/vendite, calcola i costi base e PnL,
        come effettuato tramite Interactive Brokers TWS.       
        """
        prev_quantity = self.quantity
        prev_commission = self.total_commission

        self.total_commission += commission

        if action == "BOT":
            self.avg_bot = (
                (self.avg_bot*self.buys + price*quantity)/(self.buys + quantity)
            ).quantize(FIVEPLACES)
            if self.action != "SLD":
                self.avg_price = (
                    (
                        self.avg_price*self.buys +
                        price*quantity+commission
                    )/(self.buys + quantity)
                ).quantize(FIVEPLACES)
            self.buys += quantity
            self.total_bot = (self.buys * self.avg_bot).quantize(TWOPLACES)

        else:
            self.avg_sld = (
                (self.avg_sld*self.sells + price*quantity)/(self.sells + quantity)
            ).quantize(FIVEPLACES)
            if self.action != "BOT":
                self.avg_price = (
                    (
                        self.avg_price*self.sells +
                        price*quantity-commission
                    )/(self.sells + quantity)
                ).quantize(FIVEPLACES)
            self.sells += quantity
            self.total_sld = (self.sells * self.avg_sld).quantize(TWOPLACES)

        # Aggiornamento valori netti, inclusi commissioni
        self.net = self.buys - self.sells
        self.quantity = self.net
        self.net_total = (
            self.total_sld - self.total_bot
        ).quantize(TWOPLACES)
        self.net_incl_comm = (
            self.net_total - self.total_commission
        ).quantize(TWOPLACES)

        # Aggiornamento prezzo medio e costi base
        self.cost_basis = (
            self.quantity * self.avg_price
        ).quantize(TWOPLACES)
        

Da notare che l’intero progetto fa un uso estensivo del modulo Decimal . Questa è una requisito fondamentale nelle applicazioni finanziarie, altrimenti si finisce per causare errori di arrotondamento significativi a causa della matematica nella gestione in virgola mobile .

Abbiamo quindi introdotto due variabili, TWOPLACES FIVEPLACES, che sono utilizzate successivamente per definire il livello di precisione desiderato per l’arrotondamento nei calcoli.

            TWOPLACES = Decimale ( "0,01" ) 
CINQUE POSTI = Decimale ( "0,00001" )
        

La classe Position richiede un’azione di transazione: “Acquista” o “Vendi”. Abbiamo utilizzato i codici BOT e SLD di Interactive Brokers in tutto il codice. Inoltre, la classe Position richiede un simbolo ‘ticker’, una quantità da negoziare, il prezzo di acquisto o di vendita e la commissione.

            
class Position(object):
    def __init__(
        self, action, ticker, init_quantity,
        init_price, init_commission,
        bid, ask
    ):
        """
        Imposta il "conto" iniziale della posizione che è zero per
        la maggior parte degli item, ad eccezione dell'iniziale
        acquisto / vendita.

        Quindi calcola i valori iniziali e infine aggiorna il
        valore di mercato della transazione.
        """
        self.action = action
        self.ticker = ticker
        self.quantity = init_quantity
        self.init_price = init_price
        self.init_commission = init_commission

        

Inoltre la classe Position tiene traccia di poche altre metriche, che rispecchiano pienamente quelli gestiti da Interactive Brokers. In particolare si gestisce il PnL, la quantità di acquisti e vendite, il prezzo medio di acquisto e il prezzo medio di vendita, il prezzo di acquisto totale e il prezzo di vendita totale, nonché la commissione totale applicata fino ad oggi.

                    self.realised_pnl = Decimal("0.00")
        self.unrealised_pnl = Decimal("0.00")

        self.buys = Decimal("0")
        self.sells = Decimal("0")
        self.avg_bot = Decimal("0.00")
        self.avg_sld = Decimal("0.00")
        self.total_bot = Decimal("0.00")
        self.total_sld = Decimal("0.00")
        self.total_commission = init_commission
        

La classe Position è stata strutturata in questo modo perché è molto difficile definire l’idea di un “trade”. Ad esempio, immaginiamo di eseguire le seguenti transazioni:

  • Giorno 1 : acquisto di 100 azioni di GOOG. Totale 100.
  • Giorno 2 : acquisto di 200 azioni di GOOG. Totale 300.
  • Giorno 3 – Vendita di 400 azioni di GOOG. Totale -100.
  • Giorno 4 – Acquisto di 200 azioni di GOOG. Totale 100.
  • Giorno 5 – Vendita di 100 azioni di GOOG. Totale 0.

Questo costituisce un “round trip”. Come possiamo determinare il profitto in questo caso? Calcoliamo il profitto su ogni parte della transazione? Calcoliamo il profitto solo quando la quantità torna a zero?

Questi problemi vengono risolti utilizzando un PnL realizzato e non realizzato. In questo modo possiamo conoscere in ogni momento quanto abbiamo guadagnao fino ad oggi e quanto potremmo guadagnare, tenendo traccia di questi due valori. A livello di Portfolio possiamo semplicemente sommare questi valori e calcolare valore totale del PnL in qualsiasi momento.

Infine, nel metodo di inizializzazione __init__, calcoliamo i valori iniziali e aggiorniamo il valore di mercato con l’ultimo bid/ask:

                    self._calculate_initial_value()
        self.update_market_value(bid, ask)
        

Il metodo _calculate_initial_value è il seguente:

                def _calculate_initial_value(self):
        """
        A seconda che l'azione fosse un acquisto o una vendita (" BOT "
        o " SLD ") si calcola il costo medio di acquisto, il costo totale
        di acquisto, prezzo medio e costo basi.

        Infine, calcola il totale netto con e senza commissioni.

        """

        if self.action == "BOT":
            self.buys = self.quantity
            self.avg_bot = self.init_price.quantize(FIVEPLACES)
            self.total_bot = (self.buys * self.avg_bot).quantize(TWOPLACES)
            self.avg_price = (
                (self.init_price * self.quantity + self.init_commission)/self.quantity
            ).quantize(FIVEPLACES)
            self.cost_basis = (
                self.quantity * self.avg_price
            ).quantize(TWOPLACES)
        else:  # action == "SLD"
            self.sells = self.quantity
            self.avg_sld = self.init_price.quantize(FIVEPLACES)
            self.total_sld = (self.sells * self.avg_sld).quantize(TWOPLACES)
            self.avg_price = (
                (self.init_price * self.quantity - self.init_commission)/self.quantity
            ).quantize(FIVEPLACES)
            self.cost_basis = (
                -self.quantity * self.avg_price
            ).quantize(TWOPLACES)
        self.net = self.buys - self.sells
        self.net_total = (self.total_sld - self.total_bot).quantize(TWOPLACES)
        self.net_incl_comm = (self.net_total - self.init_commission)
                                  .quantize(TWOPLACES)
        

Questo metodo esegue i calcoli iniziali all’apertura di una nuova posizione. Per le azioni  “BOT” (acquisti), il numero di acquisti viene incrementato, vengono calcolati i valori medi e totali acquistati, così come il prezzo medio della posizione. La costo base è calcolato come la quantità corrente moltiplicata per il prezzo medio pagato, che corrisponde al “prezzo totale pagato finora”. Inoltre, vengono calcolati anche la quantità netta, il totale netto e la commissione netta.

Il metodo successivo è update_market_value. Questo è un metodo complicato da implementare poiché si basa su un approccio specifico per il calcolo del “valore di mercato”. Non esiste una scelta corretta, poiché non esiste il “valore di mercato”. Ci sono però alcuni utili approcci:

  • Punto medio – Una scelta comune è prendere il punto medio dello spread denaro-lettera. Questo prende i prezzi di offerta e domanda più alti del portafoglio ordini (per un particolare exchange) e calcola la media.
  • Ultimo prezzo negoziato – Questo è l’ultimo prezzo a cui è stato scambiato un titolo. Calcolarlo è complicato perché lo scambio potrebbe essere avvenuto su più prezzi (lotti diversi a prezzi diversi). Quindi potrebbe essere una media ponderata.
  • Bid o Ask – A seconda del lato della transazione (cioè un acquisto o una vendita), come indicazione si può utilizzare il prezzo bid o ask più alto.

Tuttavia, nessuno di questi prezzi sarà probabilmente quello applicato nella realtà. Le dinamiche del portafoglio ordini, lo slippage e l’impatto del mercato faranno sì che il prezzo di vendita reale differisca dal bid / ask attualmente quotato.

Nel seguente metodo abbiamo optato per il calcolo del punto medio per fornire un senso di “valore di mercato”. È importante sottolineare, tuttavia, che questo valore può trovare la sua strada nei calcoli della gestione del rischio e del dimensionamento della posizione, quindi è necessario assicurarsi di essere soddisfatti di come viene calcolato e modificarlo di conseguenza se si desidera utilizzarlo nei tuoi motori di trading.

Una volta calcolato il valore di mercato permette il successivo calcolo del PnL non realizzato e realizzato della posizione:

                def update_market_value(self, bid, ask):
        """
        Il valore di mercato è difficile da calcolare davo che abbiamo accesso
        alla parte superiore del portafoglio ordini tramite Interactive
        Brokers, il che significa che il vero prezzo è sconosciuto
        fino all'esecuzione.

        Tuttavia, può essere stimato tramite il prezzo medio come
        differenza tra bid e ask. Una volta calcolato il valore di mercato,
        questo consente il calcolo del profitto realizzato e non realizzato,
        e la perdita per qualsiasi transazione.
        """
        midpoint = (bid+ask)/Decimal("2.0")
        self.market_value = (
            self.quantity * midpoint
        ).quantize(TWOPLACES)

        self.unrealised_pnl = (
            self.market_value - self.cost_basis
        ).quantize(TWOPLACES)

        self.realised_pnl = (
            self.market_value + self.net_incl_comm
        )
        

Il metodo finale è transact_shares. Questo è il metodo chiamato dalla classe Portfolio per eseguire effettivamente una transazione. Non riportiamo l’intero il metodo, che si può trovare nel listato precedente e su Github, ma ci concentriamo su alcune sezioni importanti.

Nel listato di codice riportato di seguito si evidenzia che se l’azione è un acquisto (“BOT”), il prezzo medio di acquisto viene ricalcolato. Se l’azione originale era anch’essa un acquisto, il prezzo medio viene necessariamente modificato. Il totale degli acquisti viene aumentato della nuova quantità e il prezzo totale di acquisto viene modificato. La logica è simile per il lato vendita / “SLD”:

                  if action == "BOT":
            self.avg_bot = (
                (self.avg_bot*self.buys + price*quantity)/(self.buys + quantity)
            ).quantize(FIVEPLACES)
            if self.action != "SLD":
                self.avg_price = (
                    (
                        self.avg_price*self.buys +
                        price*quantity+commission
                    )/(self.buys + quantity)
                ).quantize(FIVEPLACES)
            self.buys += quantity
            self.total_bot = (self.buys * self.avg_bot).quantize(TWOPLACES)
        

Infine, i tutti valori netti vengono adeguati. I calcoli sono relativamente semplici e possono essere seguiti nel seguente listato. Si noti che sono tutti i valori sono arrotondati a due cifre decimali:

                    # Aggiustamento valori netti, incluse le commissioni
        self.net = self.buys - self.sells
        self.quantity = self.net
        self.net_total = (
            self.total_sld - self.total_bot
        ).quantize(TWOPLACES)
        self.net_incl_comm = (
            self.net_total - self.total_commission
        ).quantize(TWOPLACES)

        # Aggiustamento dei prezzi medi e i costi base
        self.cost_basis = (
            self.quantity * self.avg_price
        ).quantize(TWOPLACES)
        

Questo conclude la spiegazione del codice implementato per la classe Position. Fornisce un robusto meccanismo per gestire il calcolo della posizione e la memorizzazione.

Per completezza puoi trovare il codice completo della classe Position su Github in position.py .

position_test.py

Per testare i calcoli all’interno delle classe Position abbiamo creato i seguenti unit test, confrontandoli con transazioni simili effettuate all’interno di Interactive Brokers. E’ sicuramente possibile rilevare in futuro  nuovi casi limite e bug che richiederanno la correzione degli errori, ma questi unit test forniscono un elevato livello di attendibilità nei risultati futuri.

Il listato completo del file position_test.py è il seguente:

            from decimal import Decimal
import unittest

from position import Position


class TestRoundTripXOMPosition(unittest.TestCase):
    """
    Prova un trade round-trip per Exxon-Mobil dove il trade iniziale
    è un acquisto / long di 100 azioni di XOM, al prezzo di
    $ 74,78, con una commissione di $ 1,00.
    """

    def setUp(self):
        """
        Imposta l'oggetto Position che memorizzerà il PnL.
        """
        self.position = Position(
            "BOT", "XOM", Decimal('100'),
            Decimal("74.78"), Decimal("1.00"),
            Decimal('74.78'), Decimal('74.80')
        )

    def test_calculate_round_trip(self):
        """
        Dopo il successivo acquisto, si effettuano altri due acquisti / long
        e poi chiudere la posizione con due ulteriori vendite / short.

        I seguenti prezzi sono stati confrontati con quelli calcolati
        tramite Interactive Brokers 'Trader Workstation (TWS).
        """
        self.position.transact_shares(
            "BOT", Decimal('100'), Decimal('74.63'), Decimal('1.00')
        )
        self.position.transact_shares(
            "BOT", Decimal('250'), Decimal('74.620'), Decimal('1.25')
        )
        self.position.transact_shares(
            "SLD", Decimal('200'), Decimal('74.58'), Decimal('1.00')
        )
        self.position.transact_shares(
            "SLD", Decimal('250'), Decimal('75.26'), Decimal('1.25')
        )
        self.position.update_market_value(Decimal("77.75"), Decimal("77.77"))

        self.assertEqual(self.position.action, "BOT")
        self.assertEqual(self.position.ticker, "XOM")
        self.assertEqual(self.position.quantity, Decimal("0"))

        self.assertEqual(self.position.buys, Decimal("450"))
        self.assertEqual(self.position.sells, Decimal("450"))
        self.assertEqual(self.position.net, Decimal("0"))
        self.assertEqual(self.position.avg_bot, Decimal("74.65778"))
        self.assertEqual(self.position.avg_sld, Decimal("74.95778"))
        self.assertEqual(self.position.total_bot, Decimal("33596.00"))
        self.assertEqual(self.position.total_sld, Decimal("33731.00"))
        self.assertEqual(self.position.net_total, Decimal("135.00"))
        self.assertEqual(self.position.total_commission, Decimal("5.50"))
        self.assertEqual(self.position.net_incl_comm, Decimal("129.50"))

        self.assertEqual(self.position.avg_price, Decimal("74.665"))
        self.assertEqual(self.position.cost_basis, Decimal("0.00"))
        self.assertEqual(self.position.market_value, Decimal("0.00"))
        self.assertEqual(self.position.unrealised_pnl, Decimal("0.00"))
        self.assertEqual(self.position.realised_pnl, Decimal("129.50"))


class TestRoundTripPGPosition(unittest.TestCase):
    """
    Prova uno trade round-trip per Proctor & Gamble dove il trade iniziale
    è una vendita / short di 100 azioni di PG, al prezzo di
    $ 77,69, con una commissione di $ 1,00.
    """

    def setUp(self):
        self.position = Position(
            "SLD", "PG", Decimal('100'),
            Decimal("77.69"), Decimal("1.00"),
            Decimal('77.68'), Decimal('77.70')
        )

    def test_calculate_round_trip(self):
        """
        Dopo la successiva vendita, eseguire altre due vendite / cortometraggi
        e poi chiudere la posizione con altri due acquisti / long.

        I seguenti prezzi sono stati confrontati con quelli calcolati
        tramite Interactive Brokers 'Trader Workstation (TWS).
        """
        self.position.transact_shares(
            "SLD", Decimal('100'), Decimal('77.68'), Decimal('1.00')
        )
        self.position.transact_shares(
            "SLD", Decimal('50'), Decimal('77.70'), Decimal('1.00')
        )
        self.position.transact_shares(
            "BOT", Decimal('100'), Decimal('77.77'), Decimal('1.00')
        )
        self.position.transact_shares(
            "BOT", Decimal('150'), Decimal('77.73'), Decimal('1.00')
        )
        self.position.update_market_value(Decimal("77.72"), Decimal("77.72"))

        self.assertEqual(self.position.action, "SLD")
        self.assertEqual(self.position.ticker, "PG")
        self.assertEqual(self.position.quantity, Decimal("0"))

        self.assertEqual(self.position.buys, Decimal("250"))
        self.assertEqual(self.position.sells, Decimal("250"))
        self.assertEqual(self.position.net, Decimal("0"))
        self.assertEqual(self.position.avg_bot, Decimal("77.746"))
        self.assertEqual(self.position.avg_sld, Decimal("77.688"))
        self.assertEqual(self.position.total_bot, Decimal("19436.50"))
        self.assertEqual(self.position.total_sld, Decimal("19422.00"))
        self.assertEqual(self.position.net_total, Decimal("-14.50"))
        self.assertEqual(self.position.total_commission, Decimal("5.00"))
        self.assertEqual(self.position.net_incl_comm, Decimal("-19.50"))

        self.assertEqual(self.position.avg_price, Decimal("77.67600"))
        self.assertEqual(self.position.cost_basis, Decimal("0.00"))
        self.assertEqual(self.position.market_value, Decimal("0.00"))
        self.assertEqual(self.position.unrealised_pnl, Decimal("0.00"))
        self.assertEqual(self.position.realised_pnl, Decimal("-19.50"))


if __name__ == "__main__":
    unittest.main()
        

Le importazioni per questo modulo sono semplici. Importiamo ancora una volta la classe Decimal, ma aggiungiamo anche il modulo unittest e la stessa classe Position, poiché è in fase di test:

            from decimal import Decimal
import unittest

from position import Position
        

Per coloro che non hanno ancora visto uno unit test di Python, l’idea di base è quella di creare una classe chiamata TestXXXX che eredita la classe unittest.TestCase, come riportato nel successivo listato. La classe espone un metodo setUp che consente di utilizzare qualsiasi dato o stato per il resto di quel particolare test . Ecco un esempio di configurazione di un unit test per un trade “round-trip” per Exxon-Mobil / XOM:

            class TestRoundTripXOMPosition(unittest.TestCase):
    """
    Prova un trade round-trip per Exxon-Mobil dove il trade iniziale
    è un acquisto / long di 100 azioni di XOM, al prezzo di
    $ 74,78, con una commissione di $ 1,00.
    """

    def setUp(self):
        """
        Imposta l'oggetto Position che memorizzerà il PnL.
        """
        self.position = Position(
            "BOT", "XOM", Decimal('100'),
            Decimal("74.78"), Decimal("1.00"),
            Decimal('74.78'), Decimal('74.80')
        )
        

Si noti che self.positionè impostata per essere una nuova classe Position, in cui vengono acquistate 100 azioni di XOM per 74,78 USD.

I successivi metodi, nel formato di test_XXXX, consentono l’unit test dei vari aspetti del sistema. In questo particolare metodo, dopo l’acquisto iniziale, vengono effettuati altri due acquisti long e infine due vendite per portare a zero la posizione:

                def test_calculate_round_trip(self):
        """
        Dopo il successivo acquisto, si effettuano altri due acquisti / long
        e poi chiudere la posizione con due ulteriori vendite / short.

        I seguenti prezzi sono stati confrontati con quelli calcolati
        tramite Interactive Brokers 'Trader Workstation (TWS).
        """
        self.position.transact_shares(
            "BOT", Decimal('100'), Decimal('74.63'), Decimal('1.00')
        )
        self.position.transact_shares(
            "BOT", Decimal('250'), Decimal('74.620'), Decimal('1.25')
        )
        self.position.transact_shares(
            "SLD", Decimal('200'), Decimal('74.58'), Decimal('1.00')
        )
        self.position.transact_shares(
            "SLD", Decimal('250'), Decimal('75.26'), Decimal('1.25')
        )
        self.position.update_market_value(Decimal("77.75"), Decimal("77.77"))
        

transact_shares viene chiamato quattro volte e infine il valore di mercato viene aggiornato da update_market_value. A questo punto self.position memorizza tutti i vari calcoli ed è pronto per essere testato, utilizzando il metodo assertEqual derivato dalla classe unittest.TestCase. Si noti come tutte le varie proprietà della classe Position vengono verificate rispetto a valori calcolati esternamente (in questo caso da Interactive Brokers TWS):

                    self.assertEqual(self.position.action, "BOT")
        self.assertEqual(self.position.ticker, "XOM")
        self.assertEqual(self.position.quantity, Decimal("0"))

        self.assertEqual(self.position.buys, Decimal("450"))
        self.assertEqual(self.position.sells, Decimal("450"))
        self.assertEqual(self.position.net, Decimal("0"))
        self.assertEqual(self.position.avg_bot, Decimal("74.65778"))
        self.assertEqual(self.position.avg_sld, Decimal("74.95778"))
        self.assertEqual(self.position.total_bot, Decimal("33596.00"))
        self.assertEqual(self.position.total_sld, Decimal("33731.00"))
        self.assertEqual(self.position.net_total, Decimal("135.00"))
        self.assertEqual(self.position.total_commission, Decimal("5.50"))
        self.assertEqual(self.position.net_incl_comm, Decimal("129.50"))

        self.assertEqual(self.position.avg_price, Decimal("74.665"))
        self.assertEqual(self.position.cost_basis, Decimal("0.00"))
        self.assertEqual(self.position.market_value, Decimal("0.00"))
        self.assertEqual(self.position.unrealised_pnl, Decimal("0.00"))
        self.assertEqual(self.position.realised_pnl, Decimal("129.50"))
        

Quando si eseguo questo specifico script Python all’interno di un ambiente virtuale (sotto la riga di comando in Ubuntu), ricevo il seguente output:

            (datatrader)[email protected]:~/sites/datatrader/approot/position$ python position_test.py 
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
        

Si noti che ci sono due test all’interno del file, quindi si può dare un’occhiata al secondo test presente nel listato completo in modo da familiarizzare con i calcoli.

Chiaramente ci sono molti altri test che potrebbero essere effettuati sulla classe Position. In questa fase possiamo essere rassicurati che gestisce le funzionalità di base per detenere una posizione azionaria. Col tempo, la classe verrà probabilmente ampliata per far fronte a strumenti forex, futures e opzioni, consentendo di attuare una sofisticata strategia di investimento all’interno di un portafoglio.

Prossimi Passi

Nei seguenti articoli esamineremo le classi Portfolio PortfolioHandler. Entrambi sono già stati codificati e testati. Se si desidera “saltare avanti” e vedere il codice, puoi dare un’occhiata al repository completo di DataTrader qui: github.com/datatrading-info/DataTrader

DataTrader – Trading Algoritmico Avanzato: Introduzione

DataTrader - trading algoritmico - Parte 1

Fino ad oggi su DataTrading abbiamo considerato due principali motori di backtesting quantitativo e di trading live. Il primo è derivato dalla serie Backtesting Event-Drive.  Il secondo è un motore di trading live e backtesting sul Forex, DTForex, che si collega all’API del broker OANDA.

In questo articolo introduciamo una versione più “aggiornata” della serie di backtesting guidata dagli eventi e/o del “DTForex” che funzioni su altre classi di asset. La funzionalità principale che vogliamo introdurre è il collegamento con Interactive Brokers. E’ quindi arrivato il momento di aggiornare l’infrastruttura di trading che utilizziamo.

Per questi motivi abbiamo deciso di scrivere una nuova serie di articoli su come progettare e costruire un’infrastruttura end-to-end che implementi un portafoglio completo e un sistema di gestione degli ordini, compreso un ambiente di backtesting/ricerca, la possibilità di deploy su server remoti ed esecuzione algoritmica. Inoltre il sistema dovrà essere multi-asset, ma è generalmente preferibile iniziare con un singola asset class per ridurre al minimo l’eccessiva capacità di configurazione.

Inizieremo con le azioni / ETF statunitensi scambiati con Interactive Brokers, con frequenza giornaliera, poiché questa è spesso la richiesta più popolare.

In particolare, il risultato di questa serie di articoli sarà la mia nuova infrastruttura di trading personale, e del progetto DataTrading, quindi avrò molto interesse personale nell’assicurarmi che sia robusta, affidabile e altamente efficiente! Documenterò il processo in modo “end-to-end” in modo che sia possibile replicare completamente i risultati.

In questo modo qualsiasi articolo futuro di questo sito che discuta le prestazioni di una strategia di trading sarà basato su questa libreria, consentendo a chiunque di replicare completamente i risultati purché si utilizzi: 

  • a) gli stessi identici dati storici elaborati
  • b) un insieme identico di seed casuali a qualsiasi modello stocastico utilizzato all’interno del codice. 

Ovviamente vedremo come assicurarci che questi due criteri siano soddisfatti!

Anche in questo caso, come in precedenza, renderò disponibile il software su GitHub con una licenza open source in stile MIT open source, insieme a tutti gli script e ai file di configurazione. In questo modo sarà possibile progettare il vostro sistema usando il mio come modello, o di iniziare a sviluppare strategie sapendo che avrete una solida libreria che farà il “lavoro sporco” per voi.

Considerazioni sul Design del Sistema

L’obiettivo finale di questo progetto è costruire un portafoglio e un sistema di gestione degli ordini completamente open source, ma di livello professionale, pronto per la produzione, con livelli di gestione del rischio tra le posizioni, i portafogli e l’infrastruttura nel suo insieme.

Sarà automatizzato end-to-end, il che significa che è necessario un intervento umano minimo affinché il sistema possa operare una volta impostato “live”. È impossibile eliminare completamente l’intervento umano, soprattutto quando si tratta di input di qualità dei dati (ad esempio in caso di tick errati), ma è certamente possibile prevedere che il sistema funzioni in modo automatizzato per la maggior parte del tempo.

Considerazioni sul Trading Algoritmico

Il sistema rispecchierà l’infrastruttura che potrebbe essere trovata in un software commerciale o nei sistemi utilizzati da piccolo fondo quantistico o dal team quantistico di un trading desk. Sarà altamente modulare e liberamente configurabile. I componenti principali sono l’archivio dati (l’anagrafica dei titoli), il generatore di segnali, sistema di gestione portafoglio / ordini, il livello di rischio e l’interfaccia verso i broker.

Come descritto negli articoli precedenti, questi componenti tendono a sovrapporsi ed essere accorpati, ma di seguito è riportato un elenco di componenti di “livello istituzionale” attorno ai quali desideriamo costruire il sistema:

  • Integrazione con il fornitore di dati : il primo componente fondamentale prevede l’interazione con uno o più fornitori di dati, solitamente tramite una qualche forma di API. Il sistema supporterà inizialmente l’acquisizione dei dati tramite Quandl , DTN IQFeed e Interactive Brokers.
  • Importazione e pulizia dei dati: tra l’archiviazione e il download dei dati è necessario un livello di filtraggio / pulizia che memorizzerà i dati solo se superano determinati controlli. Contrassegnerà i dati “cattivi” e attiva notifiche se i dati non sono disponibili.
  • Memorizzazione dei dati sui prezzi : è necessario creare un database principale dei prezzi intraday, archiviando i simboli e i valori dei prezzi ottenuti da un broker o un fornitore di dati.
  • Memorizzazione dei dati di trading – Tutti gli ordini, le operazioni e gli stati del portafoglio devono essere archiviati nel tempo. A questo scopo si prevede di usare una forma di serializzazione / persistenza degli oggetti, come la libreria pickle di Python .
  • Archiviazione dei dati di configurazione : è necessario memorizzare in un database le informazioni di configurazione dipendenti dal tempo con un riferimento storico, in formato tabulare o, ancora una volta, in formato pickle.
  • Ambiente di ricerca / backtesting – il sistema deve prevede un ambiente di ricerca / backtesting collegato al database dei prezzi e utilizzerà una logica avanzata per simulare il flusso di esecuzione degli ordini a mercato e quindi generare backtest realistici.
  • Generazione dei segnali – Abbiamo già descritto gli approcci principali al machine learning, all’analisi di serie temporali e alle statistiche bayesiane. Possiamo implementare queste tecniche per generare i segnali e produrre raccomandazioni di trading per il gestore del portafoglio.
  • Portfolio / Order Management – Il “cuore” del sistema è il portfolio e il sistema di gestione degli ordini (OMS) che riceve i segnali del generatore e li utilizza come “raccomandazioni” per la costruzione degli ordini. L’OMS comunica direttamente con la componente di gestione del rischio per determinare come devono essere costruiti questi ordini.
  • Gestione del rischio – Il gestore del rischio fornisce un meccanismo di “veto” o di modifica per l’OMS, in modo da considerare/includere i specifici pesi settoriali, i vincoli di leva finanziaria, la disponibilità di margine del broker e i limiti di volume medio giornaliero. Il livello di rischio prevede anche la gestione della “copertura ombrello”, fornendo al portafoglio la capacità di copertura a livello di mercato o di settore.
  • Interfaccia al broker: l’interfaccia consiste nel codice di connessione verso le API di un broker (in questo caso Interactive Brokers) e nell’implementazione di più tipi di ordine come market, limit, stop, ecc.
  • Esecuzione algoritmica : implementa e utilizza algoritmi di esecuzione automatizzata al fine di mitigare gli effetti dell’impatto sul mercato.
  • Performance e P&L – L’analisi delle performance è fondamentalmente per rispondere alla domanda “Quanto ho guadagnato?”. In realtà non è una domanda così semplice a cui rispondere! Descriveremo in modo approfondito come contabilizzare correttamente il PnL in un sistema di trading professionale.

Considerazioni sullo Sviluppo del Software

L’obbiettivo di questo sistema, a differenza della maggior parte dei sistemi di algotrading “retails”, è prestare la massima attenzione ad aspetti di solito trascurati, come l’alta disponibilità, la ridondanza, il monitoraggio, la rendicontazione, la contabilità, la qualità dei dati e la gestione del rischio all’interno del sistema. In questo modo si può creare un sistema che prevede un significativo grado di affidabilità nel processo di automazione, consentendo (eventualmente) di concentrarci sull’ottimizzazione della generazione del segnale e della gestione del portafoglio.

I seguenti concetti, la maggior parte dei quali sono presi dal settore dell’ingegneria del software professionale, forniranno la base del progetto:

  • Pianificazione automatizzata delle attività: si prevede un robusto software di pianificazione automatizzata delle attività, come la gestione dei cron jobs, al fine di garantire l’esecuzione di attività periodiche e schedulabili.
  • Alta disponibilità : il sistema prevede un grado significativo di alta disponibilità grazie alla ridondanza, utilizzando più istanze dei database e server delle applicazioni.
  • Backup e ripristino : si prevede l’esecuzione di backup di tutti i nostri dati utilizzando robusti sistemi cloud (come Amazon RDS), che consentiranno il ripristino diretto in caso di errore nei database.
  • Monitoraggio – il sistema deve essere continuamente monitorato, comprese le metriche “normali” di utilizzo della CPU, utilizzo della RAM, capacità del disco rigido e I / O di rete, permettendoci di monitorare la “salute” del nostro sistema di trading nel tempo.
  • Registrazione : per quanto possibile, il sistema deve registrare il più possibile in un sistema di logging al fine di consentire l’individuazione rapida di guasti e permettere direttamente il debug.
  • Reporting – il sistema prevede di calcolare in real-time le performance, confrontala con i benchmark desiderati e valutare dinamicamente il rischio.
  • Sistemi di controllo della versione : il codice sorgente, gli script e i file di configurazione devono essere gestiti tramite un controllo della versione, ovvero GitHub , evitando di copiare e incollare le nuove versioni del codice localmente e in remoto
  • Sviluppo Test-Driven: come per DTForex , si prevede un approccio di sviluppo completamente basato sui test (TDD), scrivendo i test unitari per il codice
  • Distribuzione continua – Al fine di ridurre al minimo l’introduzione di bug che potrebbero eliminare rapidamente i profitti, si prevede di implementare i concetti di integrazione continua e distribuzione continua per il deploy sul server. Nei prossimi articoli si descriveranno maggiori dettagli su questo tema.
  • Distribuzione remota: si prevede di utilizzare una distribuzione completamente basata su cloud / server remoto in modo tale che l’infrastruttura di trading non abbia una dipendenza locale.

Prossimi Passi

Il primo passo è descrivere lo stack del software e gli strumenti da utilizzare per costruire il sistema di trading. Questo include l’hosting provider, il controllo della versione e i sistemi di distribuzione continua, gli strumenti di monitoraggio e i meccanismi di archiviazione dei dati (inclusi backup e ripristino), nonché la fondamentale scelta del broker e le prestazione dell’interfaccia API che mette a disposizione.

Nel prossimo articolo descriviamo tutti i fornitori che sono all’altezza delle prestazione previste dal sistema, oltre a una ragionevole stima dei costi. Inoltre descriviamo in modo dettagliato le effettive implementazioni di questa infrastruttura.

White Noise e Random Walks nell’Analisi delle Serie Temporali

white noise e random walks analisi serie temporali trading quantitativo

Nel precedente articolo relativo all’analisi delle serie temporali abbiamo descritto l’importanza della correlazione seriale e della sua utilità nel contesto del trading quantitativo.

In questo articolo vediamo come usare la correlazione seriale all’interno dei primi modelli di analisi delle serie temporali, inclusi alcuni semplici modelli stocastici lineari. In particolare, descriviamo il White Noise e Random Walks.

Obiettivo dell'analisi delle serie temporali

Prima di immergerci nelle definizioni, voglio riassumere le nostre motivazioni per studiare questi modelli e il nostro obiettivo finale nello studio dell’analisi delle serie temporali.

Fondamentalmente siamo interessati a migliorare la redditività dei nostri algoritmi di trading. Come analisti quantitativi, non ci basiamo su “supposizioni” o “intuizioni”.

Il nostro approccio consiste nel quantificare il più possibile, sia per rimuovere qualsiasi coinvolgimento emotivo dal processo di trading sia per garantire (per quanto possibile) la ripetibilità del nostro trading.

Al fine di migliorare la redditività dei nostri modelli di trading, dobbiamo utilizzare tecniche statistiche per identificare comportamenti coerenti in specifici strumenti che possono essere sfruttati per realizzare un profitto. Per trovare questo comportamento dobbiamo esplorare come le proprietà dei prezzi degli asset cambiano nel tempo.

L’analisi delle serie temporali ci aiuta a raggiungere questo obiettivo. Ci fornisce un solido quadro statistico per valutare il comportamento delle serie temporali, come i prezzi degli asset, al fine di aiutarci a definire una strategia che sfrutti questo comportamento.

L’analisi delle serie temporali ci fornisce un solido quadro statistico per valutare il comportamento dei prezzi degli asset.

Finora abbiamo discusso la correlazione seriale ed esaminato la struttura di correlazione di base dei dati simulati. Inoltre abbiamo definito la stazionarietà e considerato le proprietà di secondo ordine delle serie temporali. Tutti questi attributi ci aiuteranno a identificare i modelli tra le serie temporali. Se non hai letto l’articolo precedente sulla correlazione seriale , ti consiglio caldamente di farlo prima di continuare con questo articolo.

Di seguito esamineremo come possiamo sfruttare parte della struttura dei prezzi degli asset che abbiamo identificato utilizzando modelli di serie temporali .

Processo di modellazione delle serie storiche

Allora, cos’è un modello di una serie temporale? Essenzialmente, è un modello matematico che tenta di “spiegare” la correlazione seriale presente in una serie temporale.

Con il termine “spiega” si intende che il modello “adattato” ad una serie temporale dovrebbe tenere conto di alcune o tutte le correlazioni seriali presenti nel correlogramma. Cioè, adattando il modello a una serie storica, stiamo riducendo la correlazione seriale.

Il nostro processo, come ricercatori quantitativi, consiste nel considerare un’ampia varietà di modelli, comprese le loro ipotesi e la loro complessità, e quindi scegliere un modello in modo tale che sia quello “più semplice” per spiegare la correlazione seriale.

Una volta che abbiamo definito tale modello, possiamo usarlo per prevedere i valori futuri o, più in generale, il comportamento futuro. Questa previsione è ovviamente estremamente utile nel trading quantitativo.

Se possiamo prevedere la direzione del movimento di un asset, allora abbiamo la base di una strategia di trading (tenendo ovviamente conto dei costi di transazione!). Inoltre, se possiamo prevedere la volatilità di un asset, allora abbiamo la base per un’altra strategia di trading o per un approccio di gestione del rischio. Questo è il motivo per cui siamo interessati alle proprietà di secondo ordine, poiché fornisco i mezzi per aiutarci a fare previsioni.

Una domanda da porsi in questi casi è “Come facciamo a sapere quando abbiamo una buona misura per un modello?”. Quali criteri utilizziamo per giudicare quale modello è il migliore? In effetti, ce ne sono diversi! Considereremo questi criteri in questa serie di articoli.

Riassumiamo il processo generale che seguiremo in tutta la serie:

  • Delineare un’ipotesi su una particolare serie temporale e sul suo comportamento
  • Ottenere il correlogramma delle serie temporali (utilizzando le librerie Python) e valutare la sua correlazione seriale
  • Utilizzare la nostra conoscenza dei modelli di serie temporali e adattare un modello appropriato per ridurre la correlazione seriale nei residui (vedi sotto per una definizione) del modello e delle sue serie temporali
  • Affinare l’adattamento fino a quando non ci sono più correlazioni e utilizzare i criteri matematici per valutare l’adattamento del modello
  • Utilizzare il modello e le sue proprietà di secondo ordine per fare previsioni sui valori futuri
  • Valutare l’accuratezza di queste previsioni utilizzando tecniche statistiche (come matrici di confusione , curve ROC per la classificazione o metriche regressive come MSE , MAPE ecc.)
  • Iterare questo processo fino a quando la precisione è ottimale e quindi utilizzare tali previsioni per creare strategie di trading

Questo è il nostro processo di base. La complessità sorgerà quando considereremo modelli più avanzati che tengono conto di correlazioni seriali aggiuntive nelle nostre serie temporali.

In questo articolo prenderemo in considerazione due dei modelli di serie temporali più basilari, ovvero il White Noise e Random Walks. Questi modelli formeranno la base dei seguenti modelli più avanzati, quindi è essenziale comprenderli nei dettagli.

Tuttavia, prima di introdurre uno di questi modelli, descriviamo alcuni concetti più astratti che ci aiuteranno a unificare il nostro approccio ai modelli delle serie temporali. In particolare, definiremo l’operatore di spostamento all’indietro e l’ operatore di differenza .

Operatori di spostamento all'indietro e di differenza

L’operatore di spostamento all’indietro (BSO) e l’operatore di differenza ci consentono di scrivere molti differenti modelli di serie temporali in uno specifico modo che ci aiuta a capire come differiscono l’uno dall’altro.

Dal momento che useremo queste notazioni molto frequentemente, ha senso definirli ora.

Operatore di spostamento all’indietro
L’ operatore di spostamento all’indietro o di ritardo , \({\bf B}\), accetta un elemento della serie temporale come argomento e restituisce l’elemento in un’unità di tempo precedente: \({\bf B} x_t = x_ {t-1} \).

L’applicazione ripetuta dell’operatore ci permette di tornare indietro di \(n\) volte: \({\bf B} ^ n x_t = x_{tn} \).

In futuro utilizzeremo il BSO per definire molti dei nostri modelli di serie temporali.

Inoltre, quando si arriva a studiare modelli di serie temporali che non sono stazionari (cioè, la loro media e varianza possono cambiare nel tempo), possiamo usare una procedura di differenziazione per prendere una serie non stazionaria e produrre una serie stazionaria.

Operatore differenza
L’operatore differenza , \( \nabla \), accetta un elemento della serie temporale come argomento e restituisce la differenza tra l’elemento e quello di un’unità di tempo precedente: \(\nabla x_t = x_t – x_{t-1} \) o \(\nabla x_t = (1 – {\bf B}) x_t \).
Come con BSO, possiamo applicare ripetutamente l’operatore di differenza: \( \nabla ^ n = (1 – {\bf B})^n \).

Ora che abbiamo discusso di questi operatori astratti, consideriamo alcuni modelli concreti di serie temporali.

White Noise

Cominciamo cercando di motivare il concetto di rumore bianco. In precedenza, abbiamo descritto che il nostro approccio di base consiste nel provare ad adattare i modelli a una serie temporale fino a quando le serie rimanenti non presentano alcuna correlazione seriale. Ciò motiva la definizione della serie degli errori residui:

Serie dei residui La serie degli errori residui(o residui), \(x_t\), è una serie temporale della differenza tra un valore osservato e un valore previsto, da un modello di serie temporale, in un determinato momento \(t\). Se \(y_t\) è il valore osservato e \(\hat{y}_t \) è il valore previsto, diciamo: \(x_t = y_t – \hat{y}_t \) sono i residui.

Il punto chiave è verificare se il modello di serie temporale scelto è in grado di “spiegare” la correlazione seriale nelle osservazioni, allora gli stessi residui sono serialmente non correlati. Questo significa che ogni elemento della serie dei residui non correlati serialmente è una realizzazione indipendente di una specifica distribuzione di probabilità. In altre parole, gli stessi residui sono indipendenti e distribuiti in modo identico (iid). Quindi, se vogliamo creare modelli di serie temporali che spieghino qualsiasi correlazione seriale, è naturale iniziare con un processo che produce variabili casuali indipendenti da una certa distribuzione. Questo porta direttamente al concetto di rumore bianco (discreto):

Rumore bianco discreto Si consideri una serie temporale \( \{w_t: t=1,…n\}\). Se gli elementi della serie, \( w_i \), sono indipendenti e distribuiti in modo identico (iid), con media zero, varianza \( \sigma^2 \) e nessuna correlazione seriale (cioè \( \text {Cor} (w_i, w_j) \neq 0, \forall i \neq j \)) allora diciamo che la serie temporale è un rumore bianco discreto (DWN).

In particolare, se i valori \( w_i \) sono tratti da una distribuzione normale standard (cioè \( w_t \sim N (0, \sigma^2) \)), la serie è nota come Rumore bianco gaussiano . Il White Noise è utile in molti contesti. In particolare, può essere utilizzato per simulare una serie “sintetica” . Come accennato in precedenza, una serie temporale storica è solo un’istanza osservata. Se siamo in grado di simulare più realizzazioni, allora possiamo creare “molte storie” e quindi generare statistiche per alcuni dei parametri di modelli particolari. Questo ci aiuterà a perfezionare i nostri modelli e quindi ad aumentare la precisione nelle nostre previsioni. Ora che abbiamo definito il Discrete White Noise, esamineremo alcuni dei suoi attributi, incluse le sue proprietà di secondo ordine e il suo correlogramma.

Proprietà di secondo ordine

Le proprietà di secondo ordine del DWN sono semplici e  facilmente intuibili dalla sua definizione. In particolare, la media delle serie è zero e non c’è autocorrelazione per definizione:

\(\begin{eqnarray} \mu_w = E(w_t) = 0 \end{eqnarray}\)

\(\rho_k = \text{Cor}(w_t, w_{t+k}) = \left\{\begin{aligned} &1 && \text{if} \enspace k = 0 \\ &0 && \text{if} \enspace k \neq 0 \end{aligned} \right.\)
 

Correlogramma

Possiamo anche tracciare il correlogramma di un DWN usando Python. Per prima cosa definiamo un seed casuale pari a 1, in modo le estrazioni casuali siano identiche per ogni lancio dello script. Quindi campioniamo 1000 elementi da una distribuzione normale e tracciamo l’autocorrelazione:
            import numpy as np
from matplotlib import pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.tsa.stattools import acf

np.random.seed(1)
whiteNoise = np.random.standard_normal(1000)

acf_coef = acf(whiteNoise)
plot_acf(acf_coef, lags=30)
plt.show()
        
trading-quantitativo-collerograma-white noise python

Si noti che a \( k = 4 \), \( k = 12 \) e \( k = 16 \), abbiamo tre picchi che differiscono da zero per un livello del 5%. Tuttavia, questo è facilmente prevedibile a causa della variazione del campionamento dalla distribuzione normale.

Ancora una volta, dobbiamo essere estremamente attenti nella nostra interpretazione dei risultati. In questo caso, ci aspettiamo davvero che accada qualcosa di fisicamente significativo per \( k = 4 \), \( k = 12 \) o \( k = 16 \)?

Si noti che il modello DWN ha un solo parametro, vale a dire la varianza \( \sigma^2 \). Per fortuna, è semplice stimare la varianza con Python, possiamo semplicemente usare la funzione var della libreria Numpy:

            var = np.var(whiteNoise)
print(var)
        

Dato che abbiamo specificamente definito il white noise come una distribuzione normale (quindi con una media pari a 0 e una deviazione standard pari a 1 (e quindi una varianza di 1). Python calcola la varianza del campionaria come 0.962369, che è vicino al valore ideale di 1.

Il Discrete White Noise è fondamentalmente usato come modello per i residui . Stiamo cercando di adattare altri modelli di serie temporali alle nostre serie osservate, a quel punto utilizziamo il DWN come conferma che abbiamo eliminato qualsiasi correlazione seriale rimanente dai residui e quindi abbiamo un buon adattamento del modello.

Ora che abbiamo esaminato il DWN, descriviamo un modello famoso per (alcune) serie temporali finanziarie, ovvero il Random Walk.

Random Walks

Una passeggiata casuale è un altro modello di serie temporale in cui l’osservazione corrente è uguale all’osservazione precedente con un incremento o decremento casuale. È formalmente definito come segue:

Passeggiata casuale
Una passeggiata casuale è un modello di serie temporale \( {x_t} \) tale che \( x_t = x_ {t-1} + w_t \), dove \( w_t \) è una serie di rumore bianco discreto.

In precedenza abbiamo definito l’operatore di spostamento all’indietro \( {\bf B} \). Possiamo applicare il BSO alla passeggiata casuale:

\(\begin{eqnarray} x_t = {\bf B} x_t + w_t = x_ {t-1} + w_t \end{eqnarray}\)

E facendo un ulteriore passo indietro:

\( \begin{eqnarray} x_{t-1} = {\bf B} x_{t-1} + w_{t-1} = x_{t-2} + w_{t-1} \end{eqnarray}\)

Se ripetiamo questo processo fino alla fine della serie temporale otteniamo

\(\begin{eqnarray} x_t = (1 + {\bf B} + {\bf B}^2 + \ldots) w_t \end{eqnarray}\)

da cui

\( x_t = w_t + w_{t-1} + w_{t-2} + \ldots\)

Quindi è chiaro come la passeggiata aleatoria sia semplicemente la somma degli elementi di una serie di rumore bianco discreto.

Proprietà di secondo ordine

Le proprietà di secondo ordine di una passeggiata aleatoria sono un po ‘più interessanti di quelle del rumore bianco discreto. Sebbene la media di una passeggiata aleatoria sia ancora zero, la covarianza è in realtà dipendente dal tempo. Quindi una passeggiata aleatoria non è stazionaria :

\(\begin{eqnarray} \mu_x &=& 0 \\ \gamma_k (t) &=& \text{Cov}(x_t, x_{t+k}) = t \sigma^2 \end{eqnarray}\)

In particolare, la covarianza è uguale alla varianza moltiplicata per il tempo. Quindi, con l’aumentare del tempo, aumenta anche la varianza.

Cosa significa questo per passeggiate casuali? In parole povere, significa che non ha molto senso estrapolare “trend” a lungo termine, poiché sono letteralmente processi casuali .

Correlogramma

L’autocorrelazione di una passeggiata casuale (che è anch’essa dipendente dal tempo) può essere definita come segue:

\(\begin{eqnarray} \rho_k (t) = \frac{\text{Cov}(x_t, x_{t+k})} {\sqrt{\text{Var}(x_t) \text{Var}(x_{t+k})}} = \frac{t \sigma^2}{\sqrt{t \sigma^2 (t+k) \sigma^2}} = \frac{1}{\sqrt{1+k/t}} \end{eqnarray}\)

Quindi se si considera una lunga serie temporale, con ritardi a breve termine, allora otteniamo un’autocorrelazione quasi unitaria. Cioè, abbiamo un’autocorrelazione estremamente elevata che non diminuisce molto rapidamente all’aumentare del ritardo. Possiamo simulare una serie del genere usando Python.

In primo luogo, definiamo il seed in modo che si possa replicare esattamente gli stessi risultati. Quindi creiamo due sequenze di estrazioni casuali (\( x \) e \( w \)), ognuna delle quali ha lo stesso valore (come definito dal seed).

Quindi eseguiamo un ciclo attraverso ogni elemento di \( x \) e gli assegniamo il valore dell’elemento precedente di \( x \) più il valore corrente di \( w \). In questo modo ricaviamo una passeggiata casuale. Quindi tracciamo i risultati usando type="l"per darci un grafico a linee, piuttosto che un grafico di punti circolari:

            from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.tsa.arima_process import ArmaProcess
from statsmodels.tsa.stattools import acf
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
steps = np.random.standard_normal(1000)
steps[0] = 0
random_walk = np.cumsum(steps)

plt.figure(figsize=[10, 7.5]); # Dimensioni del grafico
plt.plot(random_walk)
plt.title("Simulated Random Walk")
plt.show()
        
trading-quantitativo-random walks python

È anche abbastanza semplice disegnare il correlogramma:

            random_walk_acf_coef = acf(random_walk)
plot_acf(random_walk, lags=20);
plt.show()
        
trading-quantitativo-correlograma random walks python

Adattamento dei Modelli di Random Walk ai dati Finanziari

Come descritto anche nell’articolo precedente, è quindi necessario adattare i modelli ai dati che abbiamo già simulato.

Chiaramente questo è un po’ artificioso, dato che abbiamo simulato anche la random walk. Tuttavia, è utile come semplice descrizione delle basi del processo di adattamento . In situazioni reali non conosceremo il modello di generazione per i dati dello strumento sottostante, possiamo solamente adattare i modelli e quindi valutarne il correlogramma.

Abbiamo affermato come questo processo è utile per semplificare il processo di verifica della corretta implementazione del modello, cercando di garantire che le stime dei parametri siano vicine a quelle utilizzate nelle simulazioni.

Adattamento ai dati simulati

Dato che impiegheremo molto tempo ad adattare i modelli alle serie temporali finanziarie, dovremmo prima fare pratica sui dati simulati, in modo da avere pratica del processo quando si inizia a utilizzare i dati reali.

Abbiamo già simulato una random walk aleatoria, quindi possiamo anche usare quella realizzazione per vedere se il modello proposto (per un processo aleatorio) è accurato.

Come possiamo sapere se il modello ipotizzato per la random walk è adatto per i nostri dati simulati? Bene, facciamo uso della definizione di un processo aleatorio, quindi si prevede che la differenza tra due valori vicini è uguale possa essere descritto da un processo di rumore bianco discreto.

Quindi, se creiamo una serie di differenze di elementi dalla nostra serie simulata, dovremmo avere una serie che assomigli al rumore bianco discreto!

In Python è semplicemente implementato tramite l’uso della funzione diff. Dopo aver creato la serie ellei differenze, si stampa il grafico del correlogramma e quindi valutare quanto sia vicino al rumore bianco discreto:

            random_walk_diff = np.diff(random_walk, n=1)

random_walk_diff_acf_coef = acf(random_walk_diff)
plot_acf(random_walk_diff, lags=20);
plt.show()
        
trading-quantitativo-random walks differenze

Cosa possiamo notare da questo grafico? C’è un picco statisticamente significativo per \( k = 12 \), ma solo marginalmente. Da ricordare che ci si aspettiamo di avere almeno il 5% dei picchi che sono statisticamente significativi, a causa della variazione del campionamento.

Quindi possiamo ragionevolmente affermare che il correlogramma assomiglia a quello del rumore bianco discreto. Ciò implica che il modello di random walk si adatta bene ai nostri dati simulati. Questo è esattamente quello che dovremmo aspettarci, dal momento che abbiamo inizialmente simulato una random walk!

Adattamento ai dati finanziari

Applichiamo ora il nostro modello di random walk ad alcuni effettivi dati finanziari. Tramite la libreria Python, pandas, possiamo usare il pacchetto Pandas Datareader per ottenere facilmente i dati finanziari tramite Yahoo Finance.

Vedremo se un modello di passeggiata aleatoria è adatto per alcuni dati azionari. In particolare, sceglierò Microsoft (MSFT), ma puoi sperimentare con il tuo ticker preferito!

Prima di poter scaricare qualsiasi dato, dobbiamo installare il pacchetto pandas-datareader poiché non fa parte dell’installazione predefinita di Pandas. E’ sufficiente eseguire il seguente comando:

            pip install pandas-datareader
        

E’ quindi possibile scaricare i dati di MSFT con una semplice chiamata ad una funzione di pandas-datareader

            mstf = pdr.get_data_yahoo('MSFT', start='01/01/2000', end='08/01/2017')
        
Questo creerà un oggetto chiamato msft dove possiamo accedere alla serie dei prezzi di chiusura aggiustati per lo specifico titolo azionario. Il nostro processo consisterà nel calcolare la differenza dei valori di chiusura, omettere eventuali valori mancanti e quindi applicarli alla funzione di autocorrelazione. Quando tracciamo il correlogramma, cerchiamo prove della presenza di rumore bianco discreto, ovvero una serie di residui non correlata in modo seriale. Per eseguire ciò in Python, eseguiamo il seguente comando:
            mstf_close = mstf['Adj Close']
msft_diff = np.diff(mstf_close, n=1)

msft_diff_acf_coef = acf(msft_diff,missing="drop")
plot_acf(msft_diff_acf_coef, lags=20)
plt.show()
        

L’ultima parte (missing = "drop") specifica alla funzione acf di ignorare i valori NaN. L’output della funzioneacf è il seguente:

trading-quantitativo-msft differenze

Notiamo che la maggior parte dei picchi di lag non differisce dallo zero per più del 5%. Tuttavia ce ne sono alcuni che sono marginalmente superiori. Dato che i ritardi \( k_i \) per i quali si verifica un picco anomalo sono indipendenti da \( k = 0 \), potremmo essere inclini a pensare che questi siano causati da variazioni stocastiche e non rappresentino alcuna correlazione seriale presente nella serie.

Quindi possiamo concludere, con un ragionevole grado di certezza, che i prezzi di chiusura rettificati di MSFT sono ben approssimati da una random walk.

Proviamo ora lo stesso approccio sullo stesso indice S&P500. Il simbolo Yahoo Finance per l’S&P500 è ^GSPC. Quindi, se inseriamo i seguenti comandi in Python, possiamo tracciare il correlogramma della serie di differenze dell’S & P500:

            sp500 = pdr.get_data_yahoo('^GSPC', start='01/01/2000', end='08/01/2017')
sp500_close = sp500['Adj Close']

sp500_diff = np.diff(sp500_close, n=1)
sp500_diff_acf_coef = acf(sp500_diff,missing="drop")
plot_acf(sp500_diff_acf_coef, lags=20)
plt.show()
        
trading-quantitativo-GSPC differenze

Questo correlogramma è sicuramente più interessante. Si noti che esiste una correlazione negativa per \( k = 1 \). È improbabile che ciò sia dovuto alla variazione casuale del campionamento.

Da notare anche che ci sono picchi per \( k = 2 \), \( k = 15 \) e \( k = 18 \). Sebbene sia più difficile giustificare la loro esistenza oltre a quella della variazione casuale, possono essere indicativi di un processo di ritardo più lungo.

Quindi è molto più difficile giustificare una random walk come un buon modello per la serie dei prezzi di chiusura aggiustati dell’S&P500. Quindi dobbiamo usare modelli più sofisticati, vale a dire i modelli di ordine autoregressivo p , che sarà l’argomento del prossimo articolo!

Correlazione Seriale nell’Analisi delle Serie Storiche

Time-series-analysis-2-trading-quantitativo

Nell’articolo precedente abbiamo definito l’analisi delle serie temporali come mezzo per aiutarci a creare strategie di trading. In questo articolo esamineremo uno degli aspetti più importanti delle serie temporali, ovvero la correlazione seriale (nota anche come autocorrelazione).

Prima di immergerci nella definizione di correlazione seriale, discuteremo l’ampio scopo della modellazione di serie temporali e perché siamo interessati alla correlazione seriale.

Quando ci vengono fornite una o più serie temporali finanziarie, siamo principalmente interessati alla previsione o alla simulazione dei dati. È relativamente semplice identificare trend deterministiche e anche variazioni stagionali, oltre che scomporre una serie in queste componenti. Tuttavia, una volta scomposta una serie temporale in questo modo, resta esclusa una componente casuale.

A volte una serie temporale di questo tipo può essere ben modellata da variabili casuali indipendenti. Tuttavia, vi sono molte situazioni, in particolare nella finanza, in cui elementi consecutivi di questa serie temporale di componenti casuali avranno una correlazione . Cioè, il comportamento dei punti sequenziali nelle serie rimanenti si influenzano a vicenda in modo dipendente. Un esempio importante si verifica nel trading di coppie con ritorno alla media. Il ritorno alla media si presenta come correlazione tra variabili sequenziali nelle serie temporali.

Il nostro compito come modellatori quantitativi è cercare di identificare la struttura di queste correlazioni, in quanto ci consentiranno di migliorare notevolmente le nostre previsioni e quindi la potenziale redditività di una strategia. Inoltre, l’identificazione della struttura di correlazione migliorerà il realismo di qualsiasi serie temporale simulata basata su tale modello. Ciò è estremamente utile per migliorare l’efficacia delle componenti di gestione del rischio nell’attuazione della strategia.

Quando le osservazioni sequenziali di una serie temporale sono correlate nel modo descritto sopra, diciamo che esiste una correlazione seriale (o autocorrelazione) nelle serie temporali.

Ora che abbiamo delineato l’utilità dello studio della correlazione seriale, dobbiamo definirla in modo rigorosamente matematico. Prima di poterlo fare, dobbiamo introdurre concetti più semplici, incluse l’aspettativa e la varianza.

Aspettativa, varianza e covarianza

Molte di queste definizioni risulteranno familiari alla maggior parte dei lettori di DataTrading, ma le descriverò specificamente per motivi di completezza.

La prima definizione è quella del valore atteso o aspettativa :

Aspettativa
Il valore atteso o aspettativa, \(E(x)\), di una variabile casuale \(x\) è il suo valore medio nella popolazione. Indichiamo l’aspettativa di \(x\) con \(\mu\), in modo tale che \(E(x) = \mu \).

Ora che abbiamo la definizione di aspettativa possiamo definire la varianza , che caratterizza lo “spread” di una variabile casuale:

Varianza
La varianza di una variabile casuale è l’aspettativa delle deviazioni al quadrato della variabile dalla media, indicata con \(\sigma^2(x) = E[(x-\mu)^2] \).

Notare che la varianza è sempre non negativa. Questo ci permette di definire la deviazione standard :

Deviazione standard
La deviazione standard di una variabile casuale \(x\), \(\sigma(x) \), è la radice quadrata della varianza di \(x\).

Ora che abbiamo delineato queste definizioni statistiche elementari, possiamo generalizzare la varianza al concetto di covarianza tra due variabili casuali. La covarianza ci dice quanto linearmente siano correlate queste due variabili:

Covarianza
La covarianza di due variabili casuali \(x\) e \(y\), ciascuna con le rispettive aspettative \(\mu_x\) e \(\mu_y\), è data da \(\sigma(x, y) = E [(x-\mu_x) (y – \mu_y)]\).

La covarianza ci dice come due variabili si muovono insieme.

Tuttavia, poiché siamo in una situazione statistica, non abbiamo accesso alla popolazione significa \(\mu_x\) e \(\mu_y\). Invece dobbiamo stimare la covarianza da un campione . Per questo usiamo il rispettivo campione significa \(\bar{x}\) e \(\bar{y}\).

Se consideriamo un insieme di \(n\) coppie di elementi di variabili casuali da \(x\) e \(y\), dato da \((x_i, y_i)\), la covarianza campionaria , \(\text{Cov}(x, y)\) (anche a volte indicato con \(q(x, y)\)) è dato da:

\(\begin{eqnarray} \text {Cov} (x, y) = \frac {1} {n-1} \sum ^ n_ {i = 1} (x_i – \bar {x}) (y_i – \bar { y}) \end{eqnarray}\)

Nota: alcuni di voi potrebbero chiedersi perché dividiamo per \(n-1\) al denominatore, invece che per \(n\). Questa è una domanda valida! E’ opportuno scegliere \(n-1\) in moda da rendere \(\text {Cov} (x, y)\) un Bias di Stima.

Esempio: Covarianza campionaria in Python

Per calcolare la covarianza possiamo utilizzare la libreria Numpy di Python. NumPy non ha una funzione per calcolare direttamente la covarianza tra due variabili, ma prevede una funzione per calcolare una matrice di covarianza chiamata cov() che possiamo usare per recuperare la covarianza. Come default, la funzione cov() calcolerà la covarianza campionaria tra le variabili casuali in input.

Nel seguente codice simuleremo due vettori di lunghezza 100, ciascuno con una sequenza di numeri interi crescente linearmente con l’aggiunta di rumore normalmente distribuito. Quindi stiamo costruendo variabili associate linearmente in base alla progettazione .

Per prima cosa costruiremo un grafico a dispersione e quindi calcoleremo la covarianza del campione utilizzando la funzione cov(). Per assicurarsi di lavorare esattamente gli stessi dati di seguito utilizzati, si imposta un seed casuale rispettivamente di 1 e 2 per ogni variabile:

            np.random.seed(1)
x = np.arange(100) + 20 * np.random.normal(size=100)

np.random.seed(2)
y = np.arange(100) + 20 * np.random.normal(size=100)
        
trading-quantitativo-covaranzia-fig1
Grafico a dispersione di due variabili in aumento lineare con rumore normalmente distribuito

Esiste un’associazione relativamente chiara tra le due variabili. 

Possiamo ora calcolare la covarianza per le due variabili come l’elemento [0,1] della matrice di covarianza quadrata:

            sigma = np.cov(x,y)[0,1]
print(sigma)
        

La covarianza campionaria è pari a 926,1397…

Uno svantaggio dell’utilizzo della covarianza per stimare l’associazione lineare tra due variabili casuali è che la covarianza è una misura dimensionale . Cioè, non è normalizzato dalla diffusione dei dati e quindi è difficile fare confronti tra set di dati con grandi differenze di diffusione. Questo motiva l’introduzione di un altro concetto, vale a dire la correlazione.

Correlazione

La correlazione è una misura adimensionale della relazione tra due variabili tale che a ciascun valore della prima corrisponda un valore della seconda, seguendo una certa regolarità. In sostanza, è la covarianza di due variabili casuali, normalizzate con i rispettivi spread. La correlazione tra due variabili è spesso indicata con \(\rho(x, y)\):

\(\begin{eqnarray} \rho(x,y) = \frac {E[(x-\mu_x) (y-\mu_y)]} {\sigma_x \sigma_y} = \frac {\sigma(x,y)} {\sigma_x \sigma_y} \end{eqnarray}\)

Il prodotto dei due spread al denominatore vincola la correlazione ad assumere valori nell’intervallo \([-1,1]\):

  • Una correlazione di \(\rho(x, y) = +1\) indica un’esatta relazione lineare positiva
  • Una correlazione di \(\rho(x, y) = 0\) indica nessuna relazione lineare
  • Una correlazione di \(\rho(x, y) = -1\) indica un’esatta relazione lineare negativa

Come per la covarianza, possiamo definire la correlazione campionaria, \(\text{Cor}(x, y)\) come:

\(\begin{eqnarray} \text{Cor} (x, y) = \frac {\text{Cov (x, y)}} {\text {sd}(x) \text{sd} (y)} \end{eqnarray}\)

Dove \(\text{Cov} (x, y) \) è la covarianza campionaria di \(x\) e \(y\), mentre \(\text{sd}(x)\) è la deviazione standard campionaria di \(x\).

Esempio: correlazione campionaria in Python

NumPy fornisce la funzione corrcoef() per calcolare direttamente la correlazione tra due variabili. Come cov(), restituisce una matrice, in questo caso una matrice di correlazione, e possiamo accedere alla correlazione che ci interessa tramite il valore [0,1] della matrice quadrata restituita.

Usando gli stessi vettori \(x\) e \(y\) dell’esempio precedente, il seguente codice Python calcola la correlazione campionaria:

            corr = np.corrcoef(x,y)[0,1]
print(corr)
        

La correlazione del campione è fornita come 0,69839 che mostra un’associazione lineare positiva ragionevolmente forte tra i due vettori, come previsto.

Stazionarietà nelle serie storiche

Ora che abbiamo delineato le definizioni generali di aspettativa, varianza, deviazione standard, covarianza e correlazione, possiamo descrivere come applicare questi concetti ai dati delle serie temporali.

In primo luogo, introduciamo un concetto noto come stazionarietà. Questo è un aspetto estremamente importante delle serie temporali e gran parte dell’analisi effettuata sui dati delle serie temporali finanziarie riguarderà la stazionarietà. Dopo aver discusso la stazionarietà, siamo in grado di parlare di correlazione seriale e costruire alcuni grafici di correlogramma.

Iniziamo cercando di applicare le definizioni di cui sopra ai dati di serie temporali, partendo dalla media / aspettativa:

Media di una serie storica
La media di una serie temporale \( x_t \), \( \mu(t) \), corrispondete all’aspettativa \( E(x_t) = \mu(t)\).

In questa definizione ci sono due punti importanti da sottolineare:

  • \( \mu = \mu(t)\), cioè la media (in generale) è una funzione del tempo.
  • Questa aspettativa viene ricavata dall’insieme della popolazione di tutte le possibili serie temporali che possono essere generate con il modello delle serie temporali. In particolare, NON corrispondere all’espressione \( (x_1 + x_2 + … + x_k) / k \) (descritta più avanti).

Questa definizione è utile quando siamo in grado di generare molte realizzazioni di un modello di serie temporale. Tuttavia questo non è possibile nella realtà: siamo “bloccati” con una sola storia passata e quindi abbiamo, quasi sempre, accesso solo a una singola serie storica per una particolare risorsa o situazione.

Quindi come si procede se si vuole stimare la media, dato che non abbiamo accesso a queste ipotetiche realizzazioni dall’insieme? Bene, ci sono due opzioni:

  • Stima semplicemente la media in ogni punto utilizzando il valore osservato.
  • Scomporre le serie temporali per rimuovere eventuali tendenze deterministiche o effetti di stagionalità, ottenendo una serie residua . Una volta ottenuta questa serie possiamo ipotizzare che la serie residua sia stazionaria nella media , cioè che \( \mu (t) = \mu \), un valore fisso indipendente dal tempo. Diventa quindi possibile stimare questa media costante della popolazione utilizzando la media campionaria \( \bar {x} = \sum ^ {n}_{t = 1} \frac {x_t} {n} [&latex].

Stazionarietà della Media
Una serie temporale è media-stazionaria se \(\) \mu(t) = \ mu \), cioè è costante nel tempo.

Ora che abbiamo visto come possiamo descrivere i valori delle aspettative, possiamo usarli per arricchire la definizione di varianza. Ancora una volta facciamo l’ipotesi semplificativa che la serie temporale in esame ha media stazionaria. Con questo assunto possiamo definire la varianza come:

Varianza di una serie storica
La varianza \( \sigma ^ 2 (t) \) di un modello di serie temporale con media stazionaria è data da \( \sigma ^ 2 (t) = E [(x_t – \mu) ^ 2] \).

Questa è una semplice estensione della varianza definita in precedenza per le variabili casuali, tranne per il fatto che \(\ sigma ^ 2 (t) \) è una funzione del tempo. È importante notare come la definizione si basi fortemente sul fatto che la serie temporale ha una media stazionaria (cioè che \(\mu \) non è dipendente dal tempo).

Si può notare che questa definizione porta a una situazione complicata. Se la varianza stessa varia nel tempo, come possiamo stimarla da una singola serie temporale? Come prima, la presenza di \( E (..) \) richiede un insieme di serie temporali e tuttavia spesso ne abbiamo solo una!

Ancora una volta, semplifichiamo la situazione facendo un’ipotesi. In particolare, come per la media, assumiamo una varianza costante della popolazione, indicata con \( \sigma ^ 2 \), che non è funzione del tempo. Una volta fatta questa ipotesi, siamo in grado di stimarne il valore utilizzando la definizione di varianza campionaria introdotta in precedenza:

\(\begin{eqnarray} \text {Var (x)} = \frac {\sum (x_t – \ bar {x}) ^ 2} {n-1} \end{eqnarray}\)

Affinché questo funzioni dobbiamo essere in grado di stimare la media campionaria, \( \bar {x} \). Inoltre, come per la covarianza campionaria definita sopra, dobbiamo usare \( n-1 \) al denominatore per rendere la varianza campionaria uno stimatore imparziale.

Stazionarietà della varianza
Una serie temporale è stazionaria nella varianza se \(\) \ sigma ^ 2 (t) = \ sigma ^ 2 \(\), è costante nel tempo.

È qui che dobbiamo stare attenti! Con le serie temporali ci troviamo nella situazione dove le osservazioni sequenziali possono essere correlate. Questo può influenzare lo stimatore, ovvero sovrastimare o sottovalutare la reale varianza della popolazione.

Ciò sarà particolarmente problematico nelle serie temporali di cui abbiamo pochi dati e quindi abbiamo solo un piccolo numero di osservazioni. In una serie ad alta correlazione, tali osservazioni saranno vicine l’una all’altra e quindi introduco un bias .

In pratica, e in particolare nella finanza ad alta frequenza, ci troviamo spesso nella situazione di avere un numero considerevole di osservazioni. Lo svantaggio è che spesso non possiamo presumere che le serie finanziarie siano veramente stazionarie nella media o stazionarie nella varianza .

Man mano che progrediamo con questa serie di articoli e sviluppiamo modelli più sofisticati, affronteremo questi problemi al fine di migliorare le nostre previsioni e simulazioni.

Siamo ora in grado di applicare le nostre definizioni di serie temporali di media e varianza a quella di correlazione seriale.

Correlazione seriale

L’essenza della correlazione seriale consiste nel verificare come le osservazioni sequenziali in una serie temporale si influenzano a vicenda . Se riusciamo a trovare una struttura in queste osservazioni, probabilmente potremmo migliorare le nostre previsioni e l’accuratezza della simulazione. Ciò porterà a una maggiore redditività nelle nostre strategie di trading o ad approcci migliori nella gestione del rischio.

Iniziamo con un’altra definizione. Se assumiamo, come in precedenza, di avere una serie temporale stazionaria nella media e stazionaria nella varianza allora possiamo parlare di stazionarietà del secondo ordine :

Stazionarietà di secondo ordine
Una serie temporale è stazionaria di secondo ordine se la correlazione tra le osservazioni sequenziali è una funzione solamente del ritardo (lag) , cioè il numero di periodi temporali che separa ciascuna osservazione sequenziale.

Infine, siamo in grado di definire la covarianza seriale e la correlazione seriale!

Autocovarianza di una serie storica
Se un modello di serie temporale è stazionario di secondo ordine, allora la covarianza seriale o autocovarianza , di ritardo \( k \) $, è definita come: \( C_k = E [(x_t- \mu) (x_ {t + k} – \mu)] \).

L’autocovarianza \(C_k\) non è una funzione del tempo perché implica un’aspettativa \(E(..)\), che, come visto in precedenza, viene presa dall’insieme della popolazione delle possibili serie temporali. Ciò significa che è costante per tutti i valori di \(t\).

Da cui si ricava la definizione di correlazione seriale o autocorrelazione semplicemente dividendo per il quadrato della diffusione della serie. Questo è possibile perché la serie temporale è stazionaria nella varianza e quindi \( \sigma^2(t) = \sigma^ 2 \):

Autocorrelazione di una serie storica
La correlazione seriale o autocorrelazione del ritardo \(k\), \(\rho_k\), di una serie temporale stazionaria di secondo ordine è data dall’autocovarianza della serie normalizzata dal prodotto dello spread. Cioè \(\rho_k = \frac {C_k} {\sigma^2} \).

Da notare che \( \rho_0 = \frac{C_0} {\sigma^2} = \frac{E [(x_t – \mu) ^ 2]} {\sigma ^ 2} = \frac {\sigma ^ 2} {\sigma ^ 2} = 1 \). Cioè, il primo ritardo di \( k = 0 \) darà sempre un valore unitario.

Come con le definizioni precedenti di covarianza e correlazione, possiamo definire l’autocovarianza e l’autocorrelazione campionarie. In particolare, denotiamo l’autocovarianza campionaria con \(c\) minuscola per differenziare il valore della popolazione dato dalla \( C \) maiuscola.

La funzione di autocovarianza di esempio \( c_k \) è data da:

\(\begin{eqnarray} c_k = \ frac {1} {n} \ sum ^ {nk} _ {t = 1} (x_t – \ bar {x}) (x_ {t + k} – \ bar {x}) \end{eqnarray}\)

La funzione di autocorrelazione di esempio \(r_k\) è data da:

\(\begin{eqnarray} r_k = \ frac {c_k} {c_0} \end{eqnarray}\)

Ora che abbiamo definito la funzione di autocorrelazione campionaria siamo in grado di definire e tracciare il correlogramma , strumento essenziale nell’analisi delle serie temporali.

Il Correlogramma

Un correlogramma è semplicemente un grafico della funzione di autocorrelazione per valori sequenziali del ritardo, \( k = 0,1, …, n \). Ci permette di vedere la struttura di correlazione in ogni lag.

L’utilizzo principale dei correlogrammi è quello di rilevare eventuali autocorrelazioni successive alla rimozione di trend deterministici e effetti di stagionalità.

Se abbiamo adattato un modello di serie temporale, il correlogramma ci aiuta a giustificare che questo modello è ben adattato o se è necessario perfezionarlo ulteriormente per rimuovere qualsiasi autocorrelazione aggiuntiva.

Ecco un esempio di correlogramma, graficato tramite Python utilizzando la funzione acf, per una sequenza di variabili casuali normalmente distribuite. Di seguito potete trovare il codice Python completo:

            from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.tsa.stattools import acf

import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
s = np.random.normal(size=100)

acf_coef = acf(s)
plot_acf(acf_coef, lags=20)

plt.show()

        
trading-quantitativo-collerograma-autocorrelazione-python

Da notare le caratteristiche specifiche del grafico del correlogramma:

  • In primo luogo, poiché la correlazione campionaria del ritardo \( k = 0 \) è data da \( r_0 = \frac {c_0} {c_0} = 1 \), nel grafico avremo sempre una linea di altezza uguale ad 1 per il ritardo \( k = 0 \). In effetti, questo ci fornisce un punto di riferimento su cui giudicare le restanti autocorrelazioni per i ritardi successivi. Si noti inoltre che l’ACF dell’asse y è adimensionale, poiché la correlazione è essa stessa adimensionale.
  • L’area blu rappresenta il confine per cui se i valori non rientrano in questa area, si ha evidenza che la nostra correlazione al lag \( k \), \( r_k \), sia uguale a zero al livello del 5%. Tuttavia dobbiamo fare attenzione perché dovremmo aspettarci che il 5% di questi ritardi superi comunque questi valori! Inoltre stiamo visualizzando i valori correlati e quindi se un ritardo cade al di fuori di questi limiti, è più probabile che lo facciano anche i prossimi valori sequenziali. In pratica stiamo cercando ritardi che possano avere qualche motivo di fondo per superare il livello del 5%. Ad esempio, in una serie temporale di materie prime potremmo vedere effetti imprevisti della stagionalità  a determinati ritardi (possibilmente intervalli mensili, trimestrali o annuali).

Ecco un paio di esempi di correlogrammi per sequenze di dati.

Esempio 1 – Trend lineare fisso

Il seguente codice Python genera una sequenza di numeri interi da 1 a 100 e quindi traccia l’autocorrelazione:

            w = np.arange(1, 100)
acf_coef = acf(w, nlags=100)
plot_acf(acf_coef, lags=20)
plt.show()
        
trading-quantitativo-collerograma-autocorrelazione-python-fixed linear

Da notare come il grafico ACF diminuisce in modo quasi lineare all’aumentare dei ritardi. Quindi un correlogramma di questo tipo è una chiara indicazione della presenza di un trend.

Esempio 2 – Sequenza ripetuta

Il seguente codice Python genera una sequenza ripetuta di numeri con periodo \( p = 10 \) e quindi traccia l’autocorrelazione:

            a = np.arange(1,11)
s = []
for i in range (0,10):
    for j in range (0,10):
        s.append(a[j])

acf_coef = acf(s)
plot_acf(acf_coef, lags=20)
plt.show()
        
trading-quantitativo-collerograma-autocorrelazione-python-periodico

Possiamo vedere che ci sono picchi significativi per i ritardi 10 e 20. Ciò ha senso, poiché le sequenze si ripetono con un periodo di 10. È interessante notare che esiste una correlazione negativa esattamente del -0,5 per i ritardi 5 e 15. Questo è molto caratteristico delle serie temporali stagionali e un comportamento di questo tipo in un correlogramma è solitamente indicativo che la stagionalità / effetti periodici non sono stati completamente considerati nel modello.

Prossimi passi

Ora che abbiamo descritto in modo approfondito l’autocorrelazione e i correlogrammi, nei prossimi articoli passeremo ai modelli lineari e inizieremo il processo di previsione .

Sebbene i modelli lineari siano lontani dallo stato dell’arte nell’analisi delle serie temporali, dobbiamo sviluppare la teoria su casi più semplici prima di poterla applicare ai modelli non lineari più interessanti che sono oggi in uso.

Guida Introduttiva all’Analisi delle Serie Temporali

guida introduttiva analisi serie temporali trading algoritmico

Negli ultimi mesi abbiamo esaminato vari strumenti per aiutarci a identificare i modelli sfruttabili nei prezzi degli asset. In particolare abbiamo considerato l’econometria di base, il machine learning statistico e la statistica bayesiana.

Sebbene questi siano tutti ottimi strumenti moderni per l’analisi dei dati, la stragrande maggioranza dei modelli di asset nel settore utilizza ancora l’analisi statistica delle serie temporali. In questo articolo introduciamo l’analisi delle serie temporali, ne delimitiamo l’ambito e vediamo come applicare le tecniche a varie frequenze di dati finanziari.

Che cos'è l'analisi delle serie temporali?

In primo luogo, una serie temporale è definita come una quantità misurata sequenzialmente nel tempo per un certo intervallo.

Nella sua forma più ampia, l’analisi delle serie temporali consiste nel dedurre cosa è successo nel passato ad una serie di punti di dati, nel tentativo di prevedere cosa accadrà in futuro.

Tuttavia, adotteremo un approccio statistico quantitativo alle serie temporali, assumendo che le nostre serie temporali siano realizzazioni di sequenze di variabili casuali. Cioè, supponiamo che esista un sottostante processo di generazione per le serie temporali basato su una o più distribuzioni statistiche da cui sono tratte queste variabili.

L’analisi delle serie temporali tenta di comprendere il passato e prevedere il futuro.

Questa sequenza di variabili casuali è nota come processo stocastico a tempo discreto (DTSP). Nel trading quantitativo ci preoccupiamo di adattare i modelli statistici a questi DTSP per dedurre le relazioni sottostanti tra le serie o prevedere valori futuri al fine di generare segnali di trading.

Le serie temporali in generale, comprese quelle al di fuori del mondo finanziario, spesso contengono le seguenti caratteristiche:

  • Tendenze: una tendenza è un movimento direzionale coerente in una serie temporale. Queste tendenze saranno deterministiche o stocastiche. Il primo ci consente di fornire una motivazione di fondo per il trend, mentre il secondo è una caratteristica casuale di una serie che difficilmente spiegheremo. Le tendenze appaiono spesso nelle serie finanziarie, in particolare nei prezzi delle materie prime, e molti fondi CTA (Commodity Trading Advisor) utilizzano sofisticati modelli di identificazione delle tendenze nei loro algoritmi di trading.
  • Variazione stagionale: molte serie temporali contengono variazioni stagionali. Ciò è particolarmente vero nelle serie che rappresentano le vendite aziendali o i livelli climatici. Nella finanza quantitativa vediamo spesso variazioni stagionali delle materie prime, in particolare quelle relative alle stagioni di crescita o alla variazione annuale della temperatura (come il gas naturale).
  • Dipendenza seriale – Una delle caratteristiche più importanti delle serie temporali, in particolare delle serie finanziarie, è quella della correlazione seriale. Ciò si verifica quando le osservazioni di serie temporali che sono vicine nel tempo tendono ad essere correlate. Il clustering della volatilità è un aspetto della correlazione seriale particolarmente importante nel trading quantitativo.

Come possiamo applicare l'analisi delle serie temporali nella finanza quantitativa?

Il nostro obiettivo come ricercatori quantitativi è identificare tendenze, variazioni stagionali e correlazioni applicando metodi statistici sulle serie temporali e, infine, generare segnali di trading o filtri basati su inferenze o previsioni.

Il nostro approccio sarà:

  • Previsione e predizione dei valori futuri – Per operare con successo, avremo bisogno di prevedere con precisione i prezzi futuri degli asset, almeno in senso statistico.
  • Simulare le serie – Una volta identificate le proprietà statistiche delle serie temporali finanziarie, possiamo utilizzarle per generare simulazioni di futuri scenari. Questo ci consente di stimare il numero di trade, i costi di transizione attesi, il profilo di rendimento atteso, l’investimento tecnico e finanziario richiesto in infrastrutture e quindi, in definitiva, il profilo di rischio e la redditività di una particolare strategia o portafoglio.
  • Dedurre le relazioni – L’identificazione delle relazioni tra serie temporali e altri valori quantitativi ci consente di migliorare i nostri segnali di trading attraverso meccanismi di filtraggio. Ad esempio, se possiamo dedurre come lo spread in una coppia di valute estere varia con il volume bid / ask, allora possiamo filtrare qualsiasi potenziale trade che potrebbe verificarsi in un periodo dove si prevede uno spread elevato, in modo da ridurre i costi di transazione.

Inoltre, possiamo applicare i test statistici standard (classici / empirici o bayesiani) ai modelli di serie temporali al fine di giustificare determinati comportamenti, come il cambio di regime nei mercati azionari.

Roadmap dell'Analisi delle Serie Temporali su DataTrading

Fino ad oggi, gli articoli precedenti relativi all’apprendimento statistico, all’econometria e all’analisi bayesiana sono stati per lo più di natura introduttiva e non hanno considerato le applicazioni di tali tecniche alle moderne informazioni ad alta frequenza sui prezzi degli strumenti finanziari.

Per applicare alcune di queste tecniche ai dati ad alta frequenza abbiamo bisogno di un framework matematico in cui unificare la nostra ricerca. L’analisi delle serie temporali fornisce tale unificazione e ci consente di discutere diversi modelli indipendenti all’interno di un contesto statistico.

A tal fine utilizzeremo strumenti bayesiani e tecniche di machine learning in combinazione con i seguenti metodi per prevedere il livello e la direzione dei prezzi, agire come filtri e determinare il “cambio di regime”, ovvero determinare quando le nostre serie temporali hanno cambiato il loro comportamento statistico sottostante.

La nostra tabella di marcia per lo studi delle serie temporali è la seguente. Ciascuno dei seguenti argomenti sarà oggetto di uno articolo o di una serie di articoli. Dopo aver esaminato a fondo questi metodi, saremo in grado di creare alcuni moderni e sofisticati modelli per esaminare i dati ad alta frequenza.

  • Introduzione alle serie temporali : questo articolo delinea l’area dell’analisi delle serie temporali, il suo ambito e come può essere applicato ai dati finanziari.
  • Correlazione – Un aspetto assolutamente fondamentale della modellazione delle serie temporali è il concetto di correlazione seriale . Lo definiremo e descriveremo una delle più grandi insidie dell’analisi delle serie temporali, ovvero che “la correlazione non implica causalità” .
  • Previsione – In questa sezione considereremo il concetto di previsione , ovvero fare previsioni relativamente alla direzione o al futuro livello per una particolare serie temporale e come viene eseguita nella pratica.
  • Modelli stocastici – Si prenderà in esame alcuni modelli stocastici che ampliano il concetto di Moto Browniano Geometrico e Volatilità Stocastica, inclusi il rumore bianco e i modelli autoregressivi .
  • Regressione – Quando abbiamo trend deterministici (opposti a quelli stocastici) nei dati, possiamo giustificare la loro estrapolazione utilizzando modelli di regressione. Considereremo sia la regressione lineare che non lineare e terremo conto della correlazione seriale.
  • Modelli stazionari – I modelli stazionari presumono che le proprietà statistiche di una serie (vale a dire la media e la varianza) siano costanti nel tempo. Possiamo utilizzare modelli Moving Average (MA), nonché combinarli con modelli autoregressivi per formare modelli ARMA.
  • Modelli non stazionari : molte serie temporali finanziarie non sono stazionarie, ovvero hanno media e varianza variabili. In particolare, i prezzi degli asset hanno spesso periodi di elevata volatilità. Per queste serie abbiamo bisogno di usare modelli non stazionari come ARIMA, ARCH e GARCH.
  • Modellazione multivariata – In passato abbiamo già descritto modelli multivariati su DataTrading, in particolare quando abbiamo considerato il mean-reverting per le coppie di azioni. In questa sezione definiremo più rigorosamente la cointegrazione e esamineremo ulteriori test per essa. Considereremo anche modelli vettoriali autoregressivi (VAR) [da non confondere con Value-at-Risk!].
  • Rappresentazione in spazio di Stato Modeling – E’ presa in prestito della moderna teoria del controllo utilizzata in ingegneria e ci permette di modellare le serie temporali con parametri che variano rapidamente (come la variabile di pendenza \(\beta\) tra due risorse cointegrate in una regressione lineare). In particolare, prenderemo in considerazione il famoso filtro di Kalman e il modello Markov nascosto . Questo sarà uno degli usi principali dell’analisi bayesiana nelle serie temporali.

Come queste Analisi sono collegati con altri articoli statistici su DataTrading?

L’obiettivo di DataTrading è sempre stato quello di provare a delineare il quadro matematico e statistico per l’analisi quantitativa e il trading quantitativo, dalle basi fino alle tecniche più avanzate.

Ad oggi abbiamo dedicato la maggior parte del tempo a tecniche introduttive e intermedie. Tuttavia, ora concentreremo la nostra attenzione sulle recenti tecniche avanzate, utilizzate dai hedge funds quantitativi.

Questo non solo aiuterà coloro che desiderano ottenere una carriera nel settore, ma fornirà anche ai trader retail tra di voi un kit di strumenti e metodi molto più ampio, nonché un approccio unificato al trading.

Si può affermare con certezza che la gran parte dei professionisti che lavorano nei fondi quantitativi utilizza tecniche molto sofisticate per “catturare l’alfa”.

Tuttavia, molte di queste aziende sono così grandi da non essere interessate a strategie con “capacità limitate”, ovvero quelle che non sono scalabili oltre 1-2 milioni di dollari. In qualità di retail, se possiamo applicare un sofisticato trading framework a queste aree, possiamo raggiungere una redditività a lungo termine.

Alla fine combineremo i nostri articoli sull’analisi delle serie temporali con l’approccio bayesiano alla verifica delle ipotesi e alla selezione del modello, insieme al codice Python, per produrre modelli di serie temporali non lineari e non stazionari che possono essere negoziate ad alta frequenza.

Inoltre, con il nostro software DTForex per il backtest ad alta frequenza di più coppie di valute, disponiamo di un framework già pronto per testare questi modelli, almeno sui mercati valutari.

Il prossimo articolo della serie discuterà la correlazione e il motivo per cui è uno degli aspetti più fondamentali dell’analisi delle serie temporali.

DTForex #7 – Nuova Interfaccia di Backtesting

forex-python-trading-algoritmico-007

In questo articolo descriviamo come semplificare l’interfaccia per costruire un nuovo backtest, incapsulando molto del codice “boilerplate” in una nuova classe Backtest. Inoltre vediamo come modificare il sistema per poter gestire più coppie di valute.

Infine vediamo come testare la nuova interfaccia tramite la solita strategia di esempio di Moving Average Crossover, sia su GBP/USD che su EUR/USD.

Nuova Interfaccia di Backtest

Abbiamo modificato l’interfaccia di backtest in modo tale da creare semplicemente un’istanza di Backteste popolarla con i componenti di trading, invece di dover creare un file  backtest.py file personalizzato come in precedenza.

Il modo migliore per iniziare con il nuovo approccio è dare un’occhiata alla directory  examples/ e aprire mac.py:

            from backtest import Backtest
from execution import SimulatedExecution
from portfolio import Portfolio
from settings import settings
from strategy import MovingAverageCrossStrategy
from data.price import HistoricCSVPriceHandler

if __name__ == "__main__":
    # Trading su GBP/USD e EUR/USD
    pairs = ["GBPUSD", "EURUSD"]

    # Crea i parametri della strategia per MovingAverageCrossStrategy
    strategy_params = {
        "short_window": 500,
        "long_window": 2000
    }

    # Crea ed esegue il backtest
    backtest = Backtest(
        pairs, HistoricCSVPriceHandler,
        MovingAverageCrossStrategy, strategy_params,
        Portfolio, SimulatedExecution,
        equity=settings.EQUITY
    )
    backtest.simulate_trading()
        

Il codice è relativamente semplice. In primo luogo il codice importa i componenti necessari, ovvero il BacktestSimulatedExecutionPortfolioMovingAverageCrossStrategy HistoricCSVPriceHandler.

In secondo luogo, definiamo le coppie di valute da negoziare e quindi creiamo un dizionario noto come strategy_params. Questo contiene essenzialmente qualsiasi argomento delle key words che potremmo voler passare alla strategia. Nel caso di un Moving Average Crossover dobbiamo impostare le lunghezze dei periodi delle medie mobili. Questi valori sono in termini di “tick”.

Infine creiamo un’istanza Backtest e passiamo tutti gli oggetti come parametri. Quindi, eseguiamo il backtest stesso.

All’interno del nuovo backtest.py chiamiamo questo metodo:

            # backtest.py
..
..
    def simulate_trading(self):
        """
        Simula il backtest e calcola le performance del portfolio
        """
        self._run_backtest()
        self._output_performance()
        print("Backtest complete.")
        

Esegue il calcolo del backtest (ovvero l’aggiornamento del portafoglio all’arrivo dei tick), nonché il calcolo e l’output delle prestazioni in equity.csv.

Come descritto negli articoli precedenti, possiamo ancora produrre un grafico dell’output con lo script backtest/output.py. Useremo questo script di seguito quando discuteremo dell’implementazione di più coppie di valute.

Gestione di più Coppie di Valute

Siamo finalmente in grado di  testare la prima strategia di trading (non banale) su dati di tick ad alta frequenza per più coppie di valute!

A tale scopo è necessario modificare le modalità di gestione all’interno di MovingAverageCrossStrategy.

Di seguito il codice completo:

            class MovingAverageCrossStrategy(object):
    """
    Una strategia base di Moving Average Crossover che genera
    due medie mobili semplici (SMA), con finestre predefinite
    di 500 tick per la SMA  breve e 2.000 tick per la SMA
    lunga.

    La strategia è "solo long" nel senso che aprirà solo una
    posizione long una volta che la SMA breve supera la SMA
    lunga. Chiuderà la posizione (prendendo un corrispondente
    ordine di vendita) quando la SMA lunga incrocia nuovamente
    la SMA breve.

    La strategia utilizza un calcolo SMA a rotazione per
    aumentare l'efficienza eliminando la necessità di chiamare due
    calcoli della media mobile completa su ogni tick.
    """
    def __init__(
            self, pairs, events,
            short_window=500, long_window=2000
    ):
        self.pairs = pairs
        self.pairs_dict = self.create_pairs_dict()
        self.events = events
        self.short_window = short_window
        self.long_window = long_window

    def create_pairs_dict(self):
        attr_dict = {
            "ticks": 0,
            "invested": False,
            "short_sma": None,
            "long_sma": None
        }
        pairs_dict = {}
        for p in self.pairs:
            pairs_dict[p] = copy.deepcopy(attr_dict)
        return pairs_dict

    def calc_rolling_sma(self, sma_m_1, window, price):
        return ((sma_m_1 * (window - 1)) + price) / window

    def calculate_signals(self, event):
        if event.type == 'TICK':
            pair = event.instrument
            price = event.bid
            pd = self.pairs_dict[pair]
            if pd["ticks"] == 0:
                pd["short_sma"] = price
                pd["long_sma"] = price
            else:
                pd["short_sma"] = self.calc_rolling_sma(
                    pd["short_sma"], self.short_window, price
                )
                pd["long_sma"] = self.calc_rolling_sma(
                    pd["long_sma"], self.long_window, price
                )
            # Si avvia la strategia solamente dopo aver creato una 
            # accurata finestra di breve periodo
            if pd["ticks"] > self.short_window:
                if pd["short_sma"] > pd["long_sma"] and not pd["invested"]:
                    signal = SignalEvent(pair, "market", "buy", event.time)
                    self.events.put(signal)
                    pd["invested"] = True
                if pd["short_sma"] < pd["long_sma"] and pd["invested"]:
                    signal = SignalEvent(pair, "market", "sell", event.time)
                    self.events.put(signal)
                    pd["invested"] = False
            pd["ticks"] += 1
                
        

Essenzialmente creiamo un dizionario degli attributi attr_dict che memorizza il numero di tick trascorsi e se la strategia è “a mercato” per quella particolare coppia.

In calculate_signals aspettiamo di ricevere un TickEvent e quindi calcoliamo le medie mobili semplici per il breve e lungo periodo. Una volta che la SMA breve incrocia al rialzo la SMA lunga per una particolare coppia, la strategia va long ed esce nel modo visto nei precedenti articoli, sebbene lo faccia separatamente per ciascuna coppia.

Abbiamo utilizzato 2 mesi di dati sia per GBP/USD che per EUR/USD e il backtest richiede un po ‘di tempo per essere eseguito. Tuttavia, una volta completato il backtest, siamo in grado di utilizzare backtest/output.py per produrre il seguente grafico delle prestazioni:

trading-algoritmico-forex-7-mac-results

Chiaramente le prestazioni non sono eccezionali in quanto la strategia rimane quasi interamente “sott’acqua” col passare del tempo. Detto questo, non dovremmo aspettarci molto da una strategia di base sui dati tick ad alta frequenza. In futuro esamineremo approcci molto più sofisticati al trading su questa scala temporale.

Si spera che questo sistema possa fornire un utile punto di partenza per lo sviluppo di strategie più sofisticate. Non vedo l’ora di scoprire cosa inventeranno gli altri nel prossimo futuro!

Conclusioni

In questa serie di articoli abbiamo visto le basi di un sistema di trading automatico sul mercato del Forex , implementato in Python. Nonostante il sistema sia completo di funzionalità per il backtest e il paper/live trading, ci sono ancora molti aspetti su cui lavorare.

In particolare si può rendere il sistema molto più veloce, in modo da permettere di effettuare ricerche di parametri in tempi ragionevoli. Sebbene Python sia un ottimo strumento, uno svantaggio è che è relativamente lento rispetto a C / C ++. Quindi si può lavorare sul cercare di migliorare la velocità di esecuzione sia del backtest che dei calcoli delle prestazioni.

Inoltre, un altro aspetto che merita di essere implementato è la gestione di altri tipi di ordine rispetto al semplice ordine di mercato. Per attuare adeguate strategie HFT sul broker OANDA dovremo utilizzare gli ordini limite. Ciò richiederà probabilmente una rielaborazione del modo in cui il sistema esegue attualmente le operazioni, ma consentirà di realizzare un universo molto più ampio di strategie di trading.

 

Per il codice completo riportato in questo articolo, utilizzando il modulo di backtesting event-driven per il forex (DTForex) si può consultare il seguente repository di github:
https://github.com/datatrading-info/DTForex

Se si desidera leggere gli altri articoli di questa serie, sono disponibili ai seguenti link:

DTForex #6 – Backtesting su più giorni e Visualizzazione dei Risultati

forex-python-trading-algoritmico-006

In questo articolo descriviamo le ultime modifiche che abbiamo inserito nel sistema di trading sul mercato forex. In particolare abbiamo aggiunto alcune nuove funzionalità tra cui:

  • Documentazione : ora ho creato una sezione DTForex sul sito, che include tutti gli articoli della serie sul  trading algoritmo per il Forex e la documentazione per DTForex. In particolare, include istruzioni dettagliate per l’installazione e una guida all’uso sia per il backtesting che per il trading dal vivo.
  • Generazione di dati tick simulati – Dal momento che è difficile scaricare in blocco molti dati tick sul forex (o almeno per alcuni dei data provider che uso!) ho deciso che sarebbe stato più semplice generare semplicemente alcuni dati tick casuali per testare il sistema.
  • Backtest di più giorni – Una funzionalità essenziale per il sistema DTForex è la capacità di eseguire il backtest su più giorni di dati tick. L’ultima versione di DTForex supporta sia il backtesting di più giorni che quello di più coppie di valute, rendendolo sostanzialmente più utile.
  • Tracciare i risultati del backtesting – Sebbene l’output della console sia utile, niente batte la possibilità di visualizzare una curva di equity e un drawdown storico. Ho utilizzato la libreria Seaborn per tracciare i vari grafici delle prestazioni.

Script per Simulare i Dati di Tick

Una caratteristica estremamente importante per un sistema di trading è la capacità di eseguire un backtest su dati di tick storici che coprono un periodo di tempo di più giorni . In precedenza il sistema prevedeva solo il backtest tramite un singolo file. Questa non era una soluzione scalabile in quanto tale file deve essere caricato in memoria e poi strutturato in DataFrame di pandas . Sebbene i file di dati dei tick prodotti non siano enormi (circa 3,5 MB ciascuno), si sommano rapidamente se consideriamo più coppie di valute per periodi di mesi o anche più.

Per iniziare a creare una funzionalità per più giorni / più file, si inizia a scaricare più file dal feed tick storico di DukasCopy . Purtroppo ho avuto qualche problema e non sono riuscito a scaricare i file necessari per testare il sistema.

Dal momento che non è essenziale avere serie temporali storiche per testare il sistema, può essere più semplice scrivere uno script per generare automaticamente dei dati di tick simulati. Questo script è stato inserito nel file scripts/generate_simulated_pair.py. Il codice può essere visionato qui .

L’idea di base dello script è generare un elenco di timestamp distribuiti in modo casuale, ognuno dei quali possiede sia valori bid/ask che valori di volume. Lo spread tra l’offerta e la domanda è costante, mentre i valori bid / ask stessi sono generati come un random walk.

Dal momento che non si testerà mai alcuna strategia reale con questi dati, non c’è bisogno di preoccuparsi  delle sue proprietà statistiche o dei suoi valori assoluti in relazione alle coppie di valute forex reali. Finché si garantisce il corretto formato e una lunghezza approssimativa, si può usare  per testare il sistema di backtesting di più giorni.

Lo script è attualmente codificato per generare dati forex per l’intero mese di gennaio 2017. Utilizza la libreria Calendar di Python per considerare i giorni lavorativi (anche se non abbiamo ancora escluso le festività) e quindi genera una serie di file nel formato BBBQQQ_YYYYMMDD.csv, dove BBBQQQsarà una specifica coppia di valute specificata (es. EURUSD) ed YYYYMMDD è la data specificata (es 20170112.).

Questi file vengono inseriti nella directory  CSV_DATA_DIR, che è specificata file  settings.py  dell’applicazione.

Per generare i dati è necessario eseguire il seguente comando, dove BBBQQQdeve essere sostituito con lo specifico della valuta di interesse, es EURUSD:

            python scripts/generate_simulated_pair.py BBBQQQ
        

Il file richiederà una modifica per generare dati per più mesi o anni. Ogni file di tick giornalieri ha una dimensione dell’ordine di 3,2 MB.

In futuro si modificherà questo script per generare dati per un periodo di più mesi o anni in base a uno specifico elenco di coppie di valute, anziché i valori codificati. Tuttavia, per il momento questo è sufficiente per iniziare.

Si tenga presente che il formato corrisponde esattamente a quello dei dati storici dei tick forniti da DukasCopy, che è il set di dati che stiamo attualmente utilizzando.

Implementazione di un Backtesting per più giorni

Successivamente alla generazione di dati tick simulati, si passa all’implementazione del backtesting per più giorni. Sebbene il piano a lungo termine prevede di utilizzare un sistema di archiviazione di dati storico più robusto come PyTables con HDF5 , per il momento si utilizza un set di file CSV, un file per ogni giorno per ogni coppia di valute.

Questa è una soluzione scalabile all’aumentare del numero di giorni. La natura event-driven del sistema richiede che siano presenti solo \(N\) file in memoria contemporaneamente, dove \(N\) è il numero di coppie di valute scambiate in un particolare giorno.

L’idea di base del sistema prevede che la classe HistoricCSVPriceHandler continui a utilizzare il metodo stream_next_tick, ma con una modifica per tenere conto di dati per più giorni caricando ogni giorno di dati in modo sequenziale.

L’implementazione prevede di terminare il backtest quando si riceve l’eccezione StopIteration generata dal metodo next(..) per self.all_pairs come mostrato in questo frammento di pseudocodice:

            # price.py

..
..

def stream_next_tick(self):
	  ..
	  ..
    try:
        index, row = next(self.all_pairs)
    except StopIteration:
        return
    else:
    	..
    	..
      # Aggiungere un tick alla coda
        

Nella nuova implementazione, questo snippet viene modificato come segue:

            # price.py

..
..

def stream_next_tick(self):
    ..
    ..
    try:
        index, row = next(self.cur_date_pairs)
    except StopIteration:
        # Fine dei dati per l'attuale giorno
        if self._update_csv_for_day():
            index, row = next(self.cur_date_pairs)
        else: # Fine dei dati
            self.continue_backtest = False
            return

    ..
    ..

    # Aggiunta del tick nella coda
        

In questo frammento, quando viene generato un  StopIteration, il codice verifica il risultato di self._update_csv_for_day(). Se il risultato è True il backtest continua (il self.cur_date_pairs, che potrebbe essere stato modificato nei dati dei giorni successivi). Se il risultato è False, il backtest termina.

Questo approccio è molto efficiente in termini di memoria poiché solo un limitato numero di giorni di dati è caricato in un punto qualsiasi. Significa che possiamo potenzialmente eseguire mesi di backtesting e siamo limitati solo dalla velocità di elaborazione della CPU e dalla quantità di dati che possiamo generare o acquisire.

Abbiamo quindi aggiornato la documentazione per riflettere il fatto che il sistema ora si aspetta più giorni di dati in un formato particolare, in una directory particolare che deve essere specificata.

Rappresentazione Grafica dei Risultati del Backtest tramite la libreria Seaborn

Un backtest è relativamente inutile se non siamo in grado di visualizzare le prestazioni della strategia nel tempo. Sebbene il sistema sia stato per lo più basato su console fino ad oggi, in questo articolo iniziamo ad introdurre le basi per un’interfaccia utente grafica (GUI).

In particolare, iniziamo con creare i soliti “tre pannelli” di grafici che spesso accompagnano le metriche di performance per i sistemi di trading quantitativo, vale a dire la curva equity, il profilo dei rendimenti e la curva dei drawdown. Tutti e tre vengono calcolati per ogni tick e vengono emessi in un file chiamato equity.csv nella directory specificata in  OUTPUT_RESULTS_DIR presente in settings.py.

Per visualizzare i dati utilizziamo una libreria chiamata Seaborn , che produce grafica di qualità elevata che ha un aspetto sostanzialmente migliore rispetto ai grafici predefiniti prodotti da Matplotlib. La grafica è molto simile a quella prodotta dal pacchetto ggplot2 di R. Inoltre Seaborn si basa effettivamente  su Matplotlib, quindi si puo ancora utilizzare l’API Matplotlib.

Per consentire la visualizzazione dei risultati abbiamo creato lo script output.py che risiede nella directory backtest/. Il codice dello script è il seguente:

            # output.py

import os, os.path

import pandas as pd
import matplotlib
try:
    matplotlib.use('TkAgg')
except:
    pass
import matplotlib.pyplot as plt
import seaborn as sns

from qsforex.settings import OUTPUT_RESULTS_DIR


if __name__ == "__main__":
    """
    A simple script to plot the balance of the portfolio, or
    "equity curve", as a function of time.

    It requires OUTPUT_RESULTS_DIR to be set in the project
    settings.
    """
    sns.set_palette("deep", desat=.6)
    sns.set_context(rc={"figure.figsize": (8, 4)})

    equity_file = os.path.join(OUTPUT_RESULTS_DIR, "equity.csv")
    equity = pd.io.parsers.read_csv(
        equity_file, parse_dates=True, header=0, index_col=0
    )

    # Plot three charts: Equity curve, period returns, drawdowns
    fig = plt.figure()
    fig.patch.set_facecolor('white')     # Set the outer colour to white
    
    # Plot the equity curve
    ax1 = fig.add_subplot(311, ylabel='Portfolio value')
    equity["Equity"].plot(ax=ax1, color=sns.color_palette()[0])

    # Plot the returns
    ax2 = fig.add_subplot(312, ylabel='Period returns')
    equity['Returns'].plot(ax=ax2, color=sns.color_palette()[1])

    # Plot the returns
    ax3 = fig.add_subplot(313, ylabel='Drawdowns')
    equity['Drawdown'].plot(ax=ax3, color=sns.color_palette()[2])

    # Plot the figure
    plt.show()
        

Come puoi vedere, lo script importa Seaborn e apre il file equity.csv in un DataFrame pandas, quindi crea semplicemente tre grafici, rispettivamente per la curva di equity, i rendimenti e il drawdown.

Nota che il grafico di drawdown stesso è effettivamente calcolato da una funzione di supporto che risiede performance/performance.py, che viene chiamata dalla classe Portfolio alla fine di un backtest.

Un esempio dell’output per la strategia MovingAverageCrossStrategy, per un set di dati di EURUSD generato casualmente per il mese di gennaio 2017, è il seguente: 

trading-algoritmico-forex-6-output

In particolare, è possibile vedere le sezioni piatte della curva azionaria nei fine settimana in cui non sono presenti dati (almeno, per questo set di dati simulato). Inoltre, la strategia perde denaro in modo piuttosto prevedibile su questo set di dati simulato in modo casuale.

Questo è un buon test del sistema. Stiamo semplicemente tentando di seguire una tendenza su una serie temporale generata casualmente. Le perdite si verificano a causa dello spread fisso introdotto nel processo di simulazione.

Ciò rende palese che se vogliamo realizzare un profitto consistente nel trading forex con frequenze più alte avremo bisogno di uno specifico vantaggio quantificabile che generi rendimenti positivi oltre i costi di transazione come spread e slippage.

Avremo molto altro da dire su questo punto estremamente importante nei prossimi articoli di questa serie sul trading algoritmico del Forex.

Prossimi Passi

Calcoli della posizione di fissaggio

Abbiamo notato che i calcoli effettuati dalla classe Position non rispecchiano esattamente il modo in cui OANDA (il broker utilizzato per il sistema trading.py) calcola i trade di cross valutari.

Quindi, uno dei passaggi successivi più importanti è eseguire e testare effettivamente le nuove modifiche al file position.py e aggiornare anche gli unit test implementati in position_test.py. Questo avrà un effetto a catena sui file portfolio.py portfolio_test.py.

Valutazione della prestazione

Sebbene ora disponiamo di un set di base di grafici delle prestazioni tramite la curva di equity, il profilo dei rendimenti e le serie dei drawdown, abbiamo bisogno di misure di performance più quantificate.

In particolare, avremo bisogno di metriche a livello di strategia, inclusi i comuni rapporti di rischio/rendimento come lo Sharpe Ratio, Information Ratio e Sortino Ratio. Avremo anche bisogno di statistiche sul drawdown inclusa la distribuzione dei drawdown, oltre a statistiche descrittive come il massimo drawdown. Altre metriche utili includono il tasso di crescita annuale composto (CAGR) e il rendimento totale.

A livello di trade/posizione vogliamo vedere metriche come il profitto/perdita medio, il profitto/perdita massimo, rapporto di profitto e rapporto di vincita / perdita. Dal momento che abbiamo costruito fin dall’inizio la classe Position come parte fondamentale del software, non dovrebbe essere troppo problematico generare queste metriche tramite alcuni metodi aggiuntivi.

 

Per il codice completo riportato in questo articolo, utilizzando il modulo di backtesting event-driven per il forex (DTForex) si può consultare il seguente repository di github:
https://github.com/datatrading-info/DTForex

Se si desidera leggere gli altri articoli di questa serie, sono disponibili ai seguenti link:

DTForex #5 – Trading su diverse Coppie di Valute

forex-python-trading-algoritmico-005

Nel precedente articolo della serie sul trading algoritmico per il forex abbiamo descritto  alcune importanti modifiche al software DTForex. Questi aggiornamento hanno aumentato in modo signicativo le funzionalità del sistema, al punto che è quasi pronto per il backtesting con dati storici di tick su una gamma di coppie di valute.

In questo articolo descriviamo le seguenti modifiche apportate al sistema:

  • Ulteriori modifiche agli oggetti Position Portfolio per consentire lo scambio di più coppie di valute e valute non denominate nella valuta del conto, cioè con un conto nominato in EUR si può ora negoziare anche GBP/USD, ad esempio.
  • Revisione completa delle modalità con cui Position Portfolio calcolano di apertura, chiusura, aggiunta e rimozione di unità. L’oggetto Position esegue ora la maggior parte della logica, lasciando all’oggetto Portfolio la gestione ad alto livello.
  • Aggiunta della prima strategia non banale, ovvero la ben nota strategia Moving Average Crossover con una coppia di medie mobili semplici (SMA).
  • Modifica di backtest.py per renderlo single-threaded e deterministico. Nonostante il mio ottimismo sul fatto che un approccio multi-thread non sarebbe troppo dannoso per l’accuratezza della simulazione, ho trovato difficile ottenere risultati soddisfacenti di backtest con un approccio multi-thread.
  • Introduzione di uno script molto semplice di output basato su Matplotlib per visualizzare la curva di equity.

Gestione di coppie di valute multiple

Una caratteristica del sistema di trading che abbiamo discusso molte volte negli articoli di questa serie è la possibilità capacità di gestire più coppie di valute.

In questa articolo vediamo come modificare il software per consentire la gestione di conti nominati in valute diverse da EUR, che era la sola valuta codificata in precedenza. Descriviamo anche come poter negoziare altre coppie di valute, ad eccezione di quelle che consistono in una base o quotazione in Yen giapponese (JPY). La limitazione sullo Yen è dovuta alle modalità di calcolo delle dimensioni dei tick nelle coppie di valute con JPY.

Per ottenere questo è necessario modificare la logica di calcolo del profitto quando le unità vengono rimosse o la posizione viene chiusa. Di seguito vediamo il nuovo codice per calcolo dei pips, nel file position.py:

            def calculate_pips(self):
    mult = Decimal("1")
    if self.position_type == "long":
        mult = Decimal("1")
    elif self.position_type == "short":
        mult = Decimal("-1")
    pips = (mult * (self.cur_price - self.avg_price)).quantize(
        Decimal("0.00001"), ROUND_HALF_DOWN
    )
    return pips
        

Se chiudiamo la posizione per realizzare un guadagno o una perdita, dobbiamo utilizzare il seguente codice per close_position, da inserire nel file position.py:

            def close_position(self):
    ticker_cp = self.ticker.prices[self.currency_pair]
    ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
    if self.position_type == "long":
        remove_price = ticker_cp["ask"]
        qh_close = ticker_qh["bid"]
    else:
        remove_price = ticker_cp["bid"]
        qh_close = ticker_qh["ask"]
    self.update_position_price()
    # Calcolo dele PnL
    pnl = self.calculate_pips() * qh_close * self.units
    return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
        

In primo luogo otteniamo i prezzi denaro e lettera sia per la coppia di valute negoziata che per la coppia di valute di base (“quote/home”). Ad esempio, per un conto denominato in EUR, dove stiamo negoziando GBP/USD, dobbiamo ottenere i prezzi per “USD/EUR”, poiché GBP è la valuta di base e USD è la quotazione.

In questa fase controlliamo se la posizione stessa è una posizione long o short e quindi calcoliamo il “prezzo di rimozione” per la coppia negoziata e il “prezzo di rimozione” per la coppia quote/home, che calcolati rispettivamente da remove_priceqh_close.

Quindi aggiorniamo i prezzi correnti e medi all’interno della posizione e infine calcoliamo il P&L moltiplicando i pip, il prezzo di rimozione per quote/home e quindi il numero di unità che stiamo chiudendo.

Abbiamo completamente eliminato la necessità di valutare la “esposizione”, che era una variabile ridondante. Questa formula fornisce quindi correttamente il P&L rispetto a qualsiasi scambio di coppie di valute (non denominate in JPY).

Revisione della posizione e gestione del portafoglio

Oltre alla possibilità di negoziare più coppie di valute, vediamo come perfezionare la logica in cui Position Portfolio “condividono” la responsabilità di aprire e chiudere le posizioni, nonché di aggiungere e sottrarre unità. In particolare, dobbiamo spostare molto del codice di gestione della posizione da portfolio.py a  position.py

Questo è più naturale poiché la posizione dovrebbe prendersi cura di se stessa e non delegarla al portafoglio!

In particolare, dobbiamo creare o migrare i metodi add_unitsremove_units close_position:

            
    def add_units(self, units):
        cp = self.ticker.prices[self.currency_pair]
        if self.position_type == "long":
            add_price = cp["ask"]
        else:
            add_price = cp["bid"]
        new_total_units = self.units + units
        new_total_cost = self.avg_price * self.units + add_price * units
        self.avg_price = new_total_cost / new_total_units
        self.units = new_total_units
        self.update_position_price()

    def remove_units(self, units):
        dec_units = Decimal(str(units))
        ticker_cp = self.ticker.prices[self.currency_pair]
        ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
        if self.position_type == "long":
            remove_price = ticker_cp["ask"]
            qh_close = ticker_qh["bid"]
        else:
            remove_price = ticker_cp["bid"]
            qh_close = ticker_qh["ask"]
        self.units -= dec_units
        self.update_position_price()
        # Calcolo dele PnL
        pnl = self.calculate_pips() * qh_close * dec_units
        return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))

    def close_position(self):
        ticker_cp = self.ticker.prices[self.currency_pair]
        ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
        if self.position_type == "long":
            remove_price = ticker_cp["ask"]
            qh_close = ticker_qh["bid"]
        else:
            remove_price = ticker_cp["bid"]
            qh_close = ticker_qh["ask"]
        self.update_position_price()
        # Calcolo dele PnL
        pnl = self.calculate_pips() * qh_close * self.units
        return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
        
Negli ultimi due metodi si può vedere come è implementata la nuova formula per il calcolo del profitto. Di conseguenza, molte delle funzionalità della classe Portfolio sono state ridotte. In particolare, i metodi add_new_positionadd_position_unitsremove_position_unitsclose_position sono stati modificati a seguito dello spostamento del calcolo all’interno dell’oggetto Position:
            def add_new_position(self, position_type, currency_pair, units, ticker):
    ps = Position(
        self.home_currency, position_type, 
        currency_pair, units, ticker
    )
    self.positions[currency_pair] = ps

def add_position_units(self, currency_pair, units):
    if currency_pair not in self.positions:
        return False
    else:
        ps = self.positions[currency_pair]
        ps.add_units(units)
        return True

def remove_position_units(self, currency_pair, units):
    if currency_pair not in self.positions:
        return False
    else:
        ps = self.positions[currency_pair]
        pnl = ps.remove_units(units)
        self.balance += pnl
        return True

def close_position(self, currency_pair):
    if currency_pair not in self.positions:
        return False
    else:
        ps = self.positions[currency_pair]
        pnl = ps.close_position()
        self.balance += pnl
        del[self.positions[currency_pair]]
        return True
        

In sostanza, tutti  i metodi (a parte add_new_position) controllano semplicemente se la posizione esiste per quella coppia di valute e quindi chiamano il corrispondente metodo in Position, tenendo conto del profitto se necessario.

Strategia di crossover della media mobile

In DataTrading abbiamo già descritto una strategia Moving Average Crossover, nel contesto del mercato azionario. È una strategia utile come banco di prova del sistema perché i calcoli sono facile da replicare, anche a mano (almeno a frequenze più basse!), al fine di verificare che il backtester si stia comportando come dovrebbe.

L’idea di base della strategia è la seguente:

  • Vengono creati due filtri separati di media mobile semplici, con periodi variabili della finestra, di una particolare serie temporale.
  • I segnali di acquisto dell’asset si verificano quando la media mobile più breve supera la media mobile più lunga.
  • Se la media più lunga successivamente supera la media più breve, l’asset viene venduto.

La strategia funziona bene quando una serie temporale entra in un periodo di forte tendenza e poi lentamente inverte la tendenza.

L’implementazione è semplice. In primo luogo, implementiamo un metodo calc_rolling_sma che ci consente di utilizzare in modo più efficiente il calcolo SMA del periodo di tempo precedente per generare quello nuovo, senza dover ricalcolare completamente l’SMA in ogni fase.

In secondo luogo, generiamo segnali in due casi. Nel primo caso generiamo un segnale se la SMA breve supera la SMA lunga e non siamo long nella coppia di valute. Nel secondo caso generiamo un segnale se la SMA lunga supera la SMA breve e siamo già long nello strumento.

In questo esempio abbiamo impostato il periodo della finestra a 500 tick per la SMA breve e 2.000 tick per la SMA lunga. Ovviamente in un ambiente di produzione questi parametri devono essere ottimizzati, ma funzionano bene per i nostri scopi di test.

            class MovingAverageCrossStrategy(object):
    """
    Una strategia base di Moving Average Crossover che genera
    due medie mobili semplici (SMA), con finestre predefinite
    di 500 tick per la SMA  breve e 2.000 tick per la SMA
    lunga.

    La strategia è "solo long" nel senso che aprirà solo una
    posizione long una volta che la SMA breve supera la SMA
    lunga. Chiuderà la posizione (prendendo un corrispondente
    ordine di vendita) quando la SMA lunga incrocia nuovamente 
    la SMA breve.

    La strategia utilizza un calcolo SMA a rotazione per
    aumentare l'efficienza eliminando la necessità di chiamare due
    calcoli della media mobile completa su ogni tick.
    """

    def __init__(
            self, pairs, events,
            short_window=500, long_window=2000
    ):
        self.pairs = pairs
        self.events = events
        self.ticks = 0
        self.invested = False

        self.short_window = short_window
        self.long_window = long_window
        self.short_sma = None
        self.long_sma = None

    def calc_rolling_sma(self, sma_m_1, window, price):
        return ((sma_m_1 * (window - 1)) + price) / window

    def calculate_signals(self, event):
        if event.type == 'TICK':
            price = event.bid
            if self.ticks == 0:
                self.short_sma = price
                self.long_sma = price
            else:
                self.short_sma = self.calc_rolling_sma(
                    self.short_sma, self.short_window, price
                )
                self.long_sma = self.calc_rolling_sma(
                    self.long_sma, self.long_window, price
                )
            # Si avvia la strategia solamente dopo aver creato una accurata 
            # finestra di breve periodo
            if self.ticks > self.short_window:
                if self.short_sma > self.long_sma and not self.invested:
                    signal = SignalEvent(self.pairs[0], "market", "buy", event.time)
                    self.events.put(signal)
                    self.invested = True
                if self.short_sma < self.long_sma and self.invested:
                    signal = SignalEvent(self.pairs[0], "market", "sell", event.time)
                    self.events.put(signal)
                    self.invested = False
            self.ticks += 1
        

Backtester a thread singolo

Un altro cambiamento importante è modificare il componente del backtest in modo da essere a singolo thread, anziché multi-thread.

Dobbiamo prevede questa modifica perché è molto complesso sincronizzare i thread da eseguire in un modo simile a quello che si avrebbe nel trading live senza introdurre errori e bias che comprometto i risultati del backtest. In particolare, con un backtester multi-thread si hanno i prezzi di entrata e di uscita molto irrealistici, perchè si verificano tipicamente dopo alcune ore (virtuali) l’effettiva ricezione del tick.

Per evitare questa criticità è sufficiente incorporare lo streaming dell’oggetto TickEventnel ciclo di backtest, come implementato nel seguente frammento di backtest.py:

            def backtest(events, ticker, strategy, portfolio,
        execution, heartbeat, max_iters=200000
    ):
    """
    Esegue un ciclo while infinito che esegue il polling
    della coda degli eventi e indirizza ogni evento al
    componente della strategia del gestore di esecuzione.
    Il ciclo si fermerà quindi per "heartbeat" secondi
    e continuerà fino a quando si supera il numero massimo 
    di iterazioni.
    """
    iters = 0
    while True and iters < max_iters:
        ticker.stream_next_tick()
        try:
            event = events.get(False)
        except queue.Empty:
            pass
        else:
            if event is not None:
                if event.type == 'TICK':
                    strategy.calculate_signals(event)
                elif event.type == 'SIGNAL':
                    portfolio.execute_signal(event)
                elif event.type == 'ORDER':
                    execution.execute_order(event)
        time.sleep(heartbeat)
        iters += 1
    portfolio.output_results()
        

Da notare la linea ticker.stream_next_tick(). Questo metodo viene chiamato prima di un polling della coda degli eventi e quindi ci assicuriamo che un nuovo evento tick venga elaborato prima che la coda venga nuovamente interrogata.

In questo modo un segnale è eseguito all’arrivo di nuovi dati di mercato, anche se c’è un certo ritardo nel processo di esecuzione degli ordini a causa dello slippage.

Abbiamo anche impostato un valore max_iters che controlla per quanto tempo continua il ciclo di backtest. In pratica questo dovrà essere abbastanza grande quando si tratta di più valute in più giorni. In questo caso è stato impostato su un valore predefinito che consente di elaborare i dati di un singolo giorno di una coppia di valute.

Il metodo stream_next_tick della classe del price handler è simile a, stream_to_queue tranne per il fatto che chiama manualmente il metodo iterativo next(), invece di eseguire il tick streaming in un ciclo for:

                def stream_next_tick(self):
        """
        Il Backtester è ora passato ad un modello a un thread singolo
        in modo da riprodurre completamente i risultati su ogni esecuzione.
        Ciò significa che il metodo stream_to_queue non può essere usato 
        ed è sostituito dal metodo stream_next_tick.
        
        Questo metodo viene chiamato dalla funzione di backtesting, esterna
        a questa classe e inserisce un solo tick nella coda, ed inoltre
        aggiornare l'attuale bid / ask e l'inverso bid / ask.
        """
        try:
            index, row = self.all_pairs.next()
        except StopIteration:
            return
        else:
            self.prices[row["Pair"]]["bid"] = Decimal(str(row["Bid"])).quantize(
                Decimal("0.00001", ROUND_HALF_DOWN)
            )
            self.prices[row["Pair"]]["ask"] = Decimal(str(row["Ask"])).quantize(
                Decimal("0.00001", ROUND_HALF_DOWN)
            )
            self.prices[row["Pair"]]["time"] = index
            inv_pair, inv_bid, inv_ask = self.invert_prices(row)
            self.prices[inv_pair]["bid"] = inv_bid
            self.prices[inv_pair]["ask"] = inv_ask
            self.prices[inv_pair]["time"] = index
            tev = TickEvent(row["Pair"], index, row["Bid"], row["Ask"])
            self.events_queue.put(tev)
        

Da notare che si interrompe al ricevimento di un’eccezione StopIteration. Ciò consente al codice di riprendere l’esecuzione anziché bloccarsi.

Visualizzazione risultati con Matplotlib

Dobbiamo anche creare uno script di output, utilizzando Matplotlib in modo molto semplice per visualizzare la curva di equity. Il file output.py è inserito all’interno della directory backtest di DTForex ed il codice è riportato di seguito:.

            import os, os.path

import pandas as pd
import matplotlib.pyplot as plt

from settings import OUTPUT_RESULTS_DIR


if __name__ == "__main__":
    """
    Un semplice script per visualizzare il grafico del bilancio del portfolio, o
    "curva di equity", in funzione del tempo.

    Richiede l'impostazione di OUTPUT_RESULTS_DIR nel settings del progetto.
    """
    equity_file = os.path.join(OUTPUT_RESULTS_DIR, "equity.csv")
    equity = pd.io.parsers.read_csv(
        equity_file, header=True, 
        names=["time", "balance"], 
        parse_dates=True, index_col=0
    )
    equity["balance"].plot()
    plt.show()
        

Da notare che  settings.py deve ora prevedere la nuova variabile OUTPUT_RESULTS_DIR, che deve essere presente e valorizzata nelle impostazioni. In questo esempio abbiamo impostato a una directory temporanea fuori dalla struttura del progetto in modo da non aggiungere accidentalmente nessun risultato di backtest al codice base del progetto!

La curva di equity è costruita  aggiungendo un valore di portafoglio (“balance”) a una lista di dizionari, con un dizionario corrispondente a una marca temporale.

Una volta completato il backtest, l’elenco dei dizionari viene convertito in un DataFrame di pandas e il metodo to_csv viene utilizzato per l’output equity.csv.

Questo script di output legge semplicemente il file e visualizza il grafico della colonna balance del DataFrame.

Di seguito il codice per i metodi append_equity_row output_results della classe Portfolio:

                def append_equity_row(self, time, balance):
        d = {"time": time, "balance": balance}
        self.equity.append(d)

    def output_results(self):
        filename = "equity.csv"
        out_file = os.path.join(OUTPUT_RESULTS_DIR, filename)
        df_equity = pd.DataFrame.from_records(self.equity, index='time')
        df_equity.to_csv(out_file)
        print
        "Simulation complete and results exported to %s" % filename
        

Ogni volta che viene chiamato execute_signal, si richiamata il metodo precedente e si aggiunge il valore di timestamp / saldo al membro equity.

Alla fine del backtest viene chiamato output_results che semplicemente converte l’elenco dei dizionari in un DataFrame e quindi l’output nella directory specificata in OUTPUT_RESULTS_DIR.

Sfortunatamente, questo non è un modo particolarmente appropriato per creare una curva di equity poiché si verifica solo quando viene generato un segnale. Ciò significa che non tiene conto del P&L non realizzato .

Anche se questo è il modo in cui avviene realmente il trading (non si fa effettivamente un profitto/perdita  fino a quando non si chiude una posizione!), Significa che la curva dell’equità rimarrà completamente piatta tra gli aggiornamenti del saldo del portafoglio. Peggio ancora, Matplotlib per impostazione predefinita esegue l’interpolazione lineare tra questi punti, fornendo così la falsa impressione del P&L non realizzato.

La soluzione a questo problema è creare un tracker P&L non realizzato per la classe Position che si aggiorna correttamente ad ogni tick. Questo è un po ‘più costoso dal punto di vista computazionale, ma consente una curva di equity più utile e realistica. Descriveremo questa funzione in un prossimo articolo!

Prossimi Passi

La prossima funzionalità da prevedere per DTForex è la possibilità di effettuare il backtesting con dati relativi a molti giorni. Attualmente l’oggetto HistoricCSVPriceHandler carica solo il valore di un singolo giorno di dati tick DukasCopy per qualsiasi coppia di valute specificata.

Per consentire backtest per periodi che coprono più giorni, sarà necessario caricare e trasmettere sequenzialmente un singolo giorno in modo da evitare di riempire la RAM con l’intera cronologia dei dati dei tick. Ciò richiederà una modifica al funzionamento del metodo stream_next_tick. Una volta completato, consentirà il backtesting della strategia a lungo termine su più coppie.

Un altro compito è migliorare l’output della curva di equity Per calcolare una qualsiasi delle normali metriche di performance (come lo Sharpe Ratio ), avremo bisogno di calcolare i rendimenti percentuali in un determinato periodo di tempo. Tuttavia, ciò richiede di raggruppare i dati del tick in barre per calcolare un rendimento di periodo di tempo.

Tale binning deve avvenire su una frequenza di campionamento che è simile alla frequenza di negoziazione o lo Sharpe Ratio non rifletterà il vero rischio / rendimento della strategia. Questo raggruppamento non è un esercizio banale in quanto ci sono molti presupposti che contribuiscono a generare un “prezzo” per ogni campione.

Una volta completate queste due attività e acquisiti dati sufficienti, saremo in grado di eseguire il backtest di un’ampia gamma di strategie forex basate sui dati tick e di produrre curve azionarie al netto della maggior parte dei costi di transazione. Inoltre, sarà estremamente semplice testare queste strategie sul conto di paper trading fornito da OANDA.

Ciò dovrebbe consentire di prendere decisioni più precise sull’opportunità di eseguire una strategia, rispetto ai test effettuati con un sistema di backtesting più “orientato alla ricerca”.

 

Per il codice completo riportato in questo articolo, utilizzando il modulo di backtesting event-driven per il forex (DTForex) si può consultare il seguente repository di github:
https://github.com/datatrading-info/DTForex

Se si desidera leggere gli altri articoli di questa serie, sono disponibili ai seguenti link: