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.

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:

DTForex #4 – Aggiunta del motore di Backtesting

forex-python-trading-algoritmico-004

Negli ultimi giorno sono stato impegnato a lavorare sul progetto open-source DTForex. Ho apportato alcuni utili miglioramenti e ho pensato di condividerli con questo nuovo articolo della serie dedicata al trading algoritmico sul mercato forex.

In particolare, ho apportato le seguenti modifiche, che descriveremo a lungo in questo articolo:

  • Modifica dell’oggetto Position per correggere un errore nella gestione delle aperture e delle chiusure di una posizione
  • Aggiunta della funzionalità di gestione dei dati storici tramite il download di file di dati tick da DukasCopy
  • Implementazione della prima versione di un backtester basato su eventi sulla base dei dati di tick giornalieri

Correzione degli errori di gestione della posizione

La prima modifica che introduciamo è una nuova logica per gestire gli ordini acquisto/vendita nell’oggetto Position

Inizialmente l’oggetto Position è stato progettato in modo molto snello, delegando all’oggetto Portfolio la maggior parte del lavoro per il calcolo dei prezzi di posizione

Tuttavia, questo ha aumentato inutilmente la complessità della classe Portfolio, che rende il codice difficile da leggere e capirne la logica. Inoltre diventa particolarmente problematico quando si vuole implementare una gestione personalizzata del portafoglio senza doversi preoccupare della gestione delle posizioni “standard”.

Inoltre abbiamo verificato la presenza di un errore concettuale nella logica implementata: abbiamo mescolato l’acquisto e la vendita di ordini con essere in una posizione long o short. Questo significava il calcolo non corretto del P&L alla chiusura di una posizione il calcolo.

Abbiamo quindi modificato l’oggetto Position per accettare i prezzi bid e ask, invece di “aggiungere” e “rimuovere” i prezzi, che erano originariamente determinati a monte dell’oggetto Position tramite il PortfolioIn questo modo l’oggetto Position tiene traccia se siamo long o short e utilizza il corretto prezzo di bid/ask come valore di acquisto o di chiusura.

Abbiamo anche modificato gli unit test per riflettere la nuova interfaccia. Nonostante il fatto che queste modifiche richiedano del tempo per essere completate, fornisce una maggiore fiducia nei risultati. Ciò è particolarmente vero se consideriamo strategie più sofisticate.

Di seguito vediamo il codice del nuovo file position.py:

            from decimal import Decimal, getcontext, ROUND_HALF_DOWN


class Position(object):
    def __init__(
        self, position_type, market, 
        units, exposure, bid, ask
    ):
        self.position_type = position_type  # Long or short
        self.market = market
        self.units = units
        self.exposure = Decimal(str(exposure))

        # Long or short
        if self.position_type == "long":
            self.avg_price = Decimal(str(ask))
            self.cur_price = Decimal(str(bid))
        else:
            self.avg_price = Decimal(str(bid))
            self.cur_price = Decimal(str(ask))

        self.profit_base = self.calculate_profit_base(self.exposure)
        self.profit_perc = self.calculate_profit_perc(self.exposure)

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

    def calculate_profit_base(self, exposure):
        pips = self.calculate_pips()        
        return (pips * exposure / self.cur_price).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def calculate_profit_perc(self, exposure):
        return (self.profit_base / exposure * Decimal("100.00")).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def update_position_price(self, bid, ask, exposure):
        if self.position_type == "long":
            self.cur_price = Decimal(str(bid))
        else:
            self.cur_price = Decimal(str(ask))
        self.profit_base = self.calculate_profit_base(exposure)
        self.profit_perc = self.calculate_profit_perc(exposure)
        

Gestione dei dati storici dei tick

La successiva importante funzionalità da prevedere all’interno di un completo sistema di trading è l’abilità di effettuare un backtesting ad alta frequenza .

Un prerequisito essenziale consiste nella creazione di un archivio per i dati di tick delle coppie di valute. Tali dati possono diventare piuttosto grandi. Ad esempio, i dati di tick di un giorno per una singola coppia di valute da DukasCopy in formato CSV ha una dimensione di circa 3,3 Mb.

Si può quindi facilmente intuire come il backtest intraday di oltre 20 coppie di valute, su più anni, con significative variazioni dei parametri, può portare rapidamente a gigabyte di dati che devono essere elaborati.

Tali dati necessitano di una gestione speciale, compresa la creazione di un database di titoli, al alte prestazioni e completamente automatizzato. Discuteremo di questo sistema in futuro, ma per ora i file CSV saranno sufficienti per i nostri scopi.

Per mettere sullo stesso piano i dati storici di backtest e di live streaming, dobbiamo creare una classe atratta di gestione dei prezzi chiamata PriceHandler.

PriceHandler è un esempio di una classe base astratta, dove si prevede che qualsiasi classe ereditata deve sovrascrivere i metodi “puramente virtuali”. L’unico metodo obbligatorio è stream_to_queue, che viene chiamato tramite il thread dei prezzi quando il sistema viene attivato (live trading o backtest). La funzione stream_to_queue recuepra i dati sui prezzi da una sorgente che dipende dalla particolare implementazione della classe, quindi utilizza il metodo .put() della libreria queue per aggiungere un oggetto TickEvent.

In questo modo tutte le sottoclassi di PriceHandler possono interfacciarsi con il resto del sistema di trading senza che i componenti rimanenti sappiano (o si preoccupino!) di come vengono generati i dati sui prezzi.

Questo ci offre una notevole flessibilità per collegare file flat, archivi di file come HDF5, database relazionali come PostgreSQL o anche risorse esterne come siti Web, al motore di backtesting o di trading live.

Di seguito il codice dell’oggetto PriceHandler:

            from abc import ABCMeta, abstractmethod

..
..

class PriceHandler(object):
    """
    PriceHandler è una classe base astratta che fornisce un'interfaccia per
    tutti i successivi gestori di dati (ereditati) (sia live che storici).

    L'obiettivo di un oggetto PriceHandler (derivato) è produrre un insieme di
    bid / ask / timestamp "tick" per ogni coppia di valute e inserirli
    una coda di eventi.

    Questo replicherà il modo in cui una strategia live funzionerebbe con i dati
    dei tick che sarebbero trasmessi in streaming tramite un broker.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def stream_to_queue(self):
        """
        Trasmette una sequenza di eventi di dati tick (timestamp, bid, ask)
        come tuple alla coda degli eventi.
        """
        raise NotImplementedError("Should implement stream_to_queue()")
        

Abbiamo bisogno inoltre di  una sottoclasse chiamata HistoricCSVPriceHandler, che preveda due metodi.

Il primo è chiamato _open_convert_csv_filese utilizza Pandas per caricare un file CSV in un DataFrame e formare le colonne Bid e Ask. Il secondo metodo, stream_to_queue scorre attraverso questo DataFrame e ad ogni iterazione aggiunge un oggetto TickEvent alla coda degli eventi.

Inoltre, i prezzi correnti di bid/ask correnti impostati a livello di classe, e vengono successivamente interrogati tramite l’oggetto Portfolio.

Di seguito il codice di HistoricCSVPriceHandler:

            class HistoricCSVPriceHandler(PriceHandler):
    """
    HistoricCSVPriceHandler è progettato per leggere un file CSV di
    dati tick per ciascuna coppia di valute richiesta e trasmetterli in streaming
    alla coda degli eventi.
    """

    def __init__(self, pairs, events_queue, csv_dir):
        """
        Inizializza il gestore dati storici richiedendo
        la posizione dei file CSV e un elenco di simboli.

        Si presume che tutti i file siano nella forma
        'pair.csv', dove " pair " è la coppia di valute. Per
        EUR/USD il nome del file è EURUSD.csv.

        Parametri:
        pairs - L'elenco delle coppie di valute da ottenere.
        events_queue - La coda degli eventi a cui inviare i tick.
        csv_dir: percorso di directory assoluto per i file CSV.
        """
        self.pairs = pairs
        self.events_queue = events_queue
        self.csv_dir = csv_dir
        self.cur_bid = None
        self.cur_ask = None

    def _open_convert_csv_files(self):
        """
        Apre i file CSV dalla directory su disco, converte i dati
        in un DataFrame di pandas con un dizionario di coppie.
        """
        pair_path = os.path.join(self.csv_dir, '%s.csv' % self.pairs[0])
        self.pair = pd.io.parsers.read_csv(
            pair_path, header=True, index_col=0, parse_dates=True,
            names=("Time", "Ask", "Bid", "AskVolume", "BidVolume")
        ).iterrows()

    def stream_to_queue(self):
        self._open_convert_csv_files()
        for index, row in self.pair:
            self.cur_bid = Decimal(str(row["Bid"])).quantize(
                Decimal("0.00001", ROUND_HALF_DOWN)
            )
            self.cur_ask = Decimal(str(row["Ask"])).quantize(
                Decimal("0.00001", ROUND_HALF_DOWN)
            )
            tev = TickEvent(self.pairs[0], index, row["Bid"], row["Ask"])
            self.events_queue.put(tev)
        

Ora che abbiamo una funzionalità per gestire i dati storici di base, siamo in grado di creare un backtester completamente guidato dagli eventi.

Funzionalità di BackTesting Event-Driven

Nel trading algoritmico è fondamentale utilizzare un motore di backtesting che si avvicina il più possibile ad un motore di trading live. Ciò è dovuto al fatto che una sofisticata gestione dei costi di transazione, soprattutto ad alta frequenza, è spesso il fattore determinante per stabilire se una strategia sarà redditizia o meno.

Tale gestione dei costi di transazione ad alta frequenza può essere realmente simulata solo con l’uso di un motore di esecuzione basato su eventi multi-thread. Sebbene un tale sistema sia significativamente più complicato di un basilare backtester vettorializzato di “ricerca” di P&L, potrà simulare più fedelmente il comportamento reale e ci consentirà di prendere decisioni migliori nella scelta delle strategie.

Inoltre, possiamo iterare più rapidamente col passare del tempo, perché non dovremo passare continuamente dalla strategia di “livello di ricerca” alla strategia di “livello di implementazione” poiché sono la stessa cosa. Gli unici due componenti che cambiano sono la classe di streaming dei prezzi e la classe di esecuzione. Tutto il resto sarà identico tra i sistemi di backtesting e live trading.

In effetti, questo significa che il nuovo codice backtest.py è quasi identico al codice trading.py che gestisce il trading real o il trading practice con OANDA. Abbiamo solo bisogno di prevedere l’importazione delle classi HistoricPriceCSVHandlerSimulatedExecution al posto delle classi StreamingPriceHandlerOANDAExecutionHandler. Tutto il resto rimane lo stesso.

Di seguito il codice di backtest.py:

            import copy, sys
import queue
import threading
import time
from decimal import Decimal, getcontext

from execution import SimulatedExecution
from portfolio import Portfolio
from settings import settings
from strategy import TestStrategy
from data.price import HistoricCSVPriceHandler


def trade(events, strategy, portfolio, execution, heartbeat):
    """
    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à.
    """
    while True:
        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)


if __name__ == "__main__":
    # Imposta il numero di decimali a 2
    getcontext().prec = 2

    heartbeat = 0.0  # mezzo secondo tra ogni polling
    events = queue.Queue()
    equity = settings.EQUITY

    # Carica il file CSV dei dati storici
    pairs = ["EURUSD"]
    csv_dir = settings.CSV_DATA_DIR
    if csv_dir is None:
        print("No historic data directory provided - backtest terminating.")
        sys.exit()

    # Crea la classe di streaming dei dati storici di tick
    prices = HistoricCSVPriceHandler(pairs, events, csv_dir)

    # Crea il generatore della strategia/signale, passando lo 
    # strumento e la coda degli eventi
    strategy = TestStrategy(pairs[0], events)

    # Crea l'oggetto portfolio per tracciare i trade
    portfolio = Portfolio(prices, events, equity=equity)

    # Crea il gestore di esecuzione simulato
    execution = SimulatedExecution()

    # Crea due thread separati: uno per il ciclo di trading
    # e un'altro per la classe di streaming dei prezzi di mercato
    trade_thread = threading.Thread(
        target=trade, args=(
            events, strategy, portfolio, execution, heartbeat
        )
    )
    price_thread = threading.Thread(target=prices.stream_to_queue, args=[])

    # Avvia entrambi i thread
    trade_thread.start()
    price_thread.start()
        

L’utilizzo di un sistema di esecuzione multi-thread per il backtest ha il principale svantaggio di non essere deterministicoCiò significa che eseguendo più volte il backtest degli stessi dati si avranno risultati differenti, anche se piccole.

Questo accade perché non è possiamo garantire lo stesso ordine delle istruzioni eseguite dai thread, per esecuzioni differenti della stessa simulazione. Ad esempio, quando si inseriscono elementi nella coda, si potrebbero ottenere nove oggetti TickEvent inseriti nella coda nel backtest n.1, ma potremmo ottenerne undici nel backtest n.2.

Poiché l’oggetto Strategy esegue il polling della coda degli oggetti TickEvent, vedrà prezzi bid/ask diversi nelle due serie e quindi aprirà una posizione a prezzi bid/ask diversi. Ciò porterà a (piccole) differenze nei rendimenti.

Questo è un grosso problema? Non credo proprio. Non solo è così che funzionerà il sistema live, ma ci consente anche di sapere quanto sia sensibile la nostra strategia alla velocità di ricezione dei dati. Ad esempio, se calcoliamo la varianza dei rendimenti in tutte i backtest eseguiti con gli stessi dati, avremo un’idea di quanto la strategia sia sensibile alla latenza dei dati.

Idealmente, vogliamo una strategia che abbia una piccola varianza in ciascuna delle nostre serie. Tuttavia, se si ha una varianza elevata, significa che dovremmo fare molta attenzioni a mettere live questa strategia.

Potremmo persino eliminare completamente il problema del determinismo semplicemente utilizzando un thread singolo nel nostro codice di backtest (come per il backtester event-driven per le azioni di DataTrading). Tuttavia, questo ha lo svantaggio di ridurre il realismo con il sistema live. Questi sono i dilemmi di simulazione di trading ad alta frequenza!

Prossimi Passi

Un altro problema del sistema che bisogna risolvere è la gestione di  solo una valuta di base di EUR e una singola coppia di valute, EUR/USD.

Ora che la gestione Position è stata sostanzialmente modificata, sarà molto più semplice estenderla per gestire più coppie di valute. Questo è il passaggio successivo.

A quel punto saremo in grado di provare strategie multi-coppia di valute ed eventualmente introdurre Matplotlib per rappresentare graficamente i risultati.

 

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 #3 – Open Sourcing del Sistema di Trading sul Forex

forex-python-trading-algoritmico-003

In questo articolo della serie sul trading Forex descriviamo il piano a lungo termine per il sistema di trading forex. Inoltre, approfondiamo l’uso del tipo di dati Decimal di Python per rendere i calcoli più accurati.

Ad oggi, abbiamo sperimentato l’ API Rest di OANDA per verificarne il confronto con l’API fornita da Interactive Brokers. Abbiamo anche visto come aggiungere un elemento base di replica del portafoglio come primo passo verso un adeguato sistema di backtesting basato sugli eventi. Ho anche ricevuto alcune email con suggerimenti relativi agli articoli precedenti ( n. 1 e n. 2 ), il che suggerisce che molti di voi desiderano modificare ed estendere il codice da soli.

Codice Open Source per il Sistema di Trading sul Forex

Per i motivi sopra esposti ho deciso di rendere open source il sistema di trading sul forex. Cosa significa questo? Significa che tutto il codice attuale e futuro sarà disponibile gratuitamente, con una licenza open source MIT , sul sito Web di Github al seguente repository: datatrading-info/DTForex.

Per chi ha familiarità con git e Github, è sicuramente in grado di eseguire il git clone del repository ed iniziare a modificarlo per i propri scopi.

Il sistema di trading automatico sul Forex di DataTrading è ora open source con una licenza MIT. Puoi trovare il codice più recente su Github nel repository DTForex su datatrading-info/DTForex.

Per chi non conosce il controllo di versione del codice sorgente, è sicuramente utile leggere come funziona git (e il controllo della versione in generale) con questo ebook gratuito Pro Git . Vale la pena dedicare un po’ di tempo per capire il controllo del codice sorgente in quanto farà risparmiare un’enorme quantità di problemi futuri se si trascorre tempo a programmare e ad aggiornare i progetti!

In Ubuntu si può installare git in modo rapido con il seguente comando:

            sudo apt-get install git-core
        

Si crea una directory per il progetto DTForex e “clonare” il repository dal sito Github, come segue:

            mkdir -p ~/progetti/ 
cd ~/progetti/ 
git clone 
https://github.com/datatrading-info/DTForex.git
        

A questo punto si deve creare un ambiente virtuale in cui eseguire il codice:

            mkdir -p ~/venv/dtforex
cd ~/venv/dtforex
virtualenv .
source ~/venv/dtforex/bin/activate
        

Successivamente è necessario installare le librerie python richieste dal progetto (ci vorrà qualche minuto!):

            pip install -r ~/progetti/dtforex/requirements.txt
        

Come accennato negli articoli precedenti, si deve inoltre creare le variabili di ambiente necessarie per le credenziali di autenticazione OANDA. Si prega di consultare l’articolo DTForex #2 per le istruzioni su come farlo.

Si prega di prestare attenzione al README associato al repository, poiché contiene le istruzioni di installazione, un disclaimer e una garanzia sull’utilizzo del codice.

Poiché il software è in modalità “alpha”, queste istruzioni diventeranno più semplici con il passare del tempo. In particolare cercherò di includere il progetto in un pacchetto Python in modo che possa essere facilmente installato tramite pip.

In caso di domande sulla procedura di installazione, non esitare a scrivermi a [email protected] .

Piano a lungo termine

La “filosofia” del sistema di trading per il forex, come per tutti i progetti del sito DataTrading, è costruire un motore di backtesting che possa imitare il più possibile il trading reale. Ciò significa includere dettagli che sono spesso esclusi da scenari di backtesting più “orientate alla ricerca”. Latenza, interruzioni del server, automazione, monitoraggio e costi di transazione realistici saranno tutti inclusi nei modelli di DataTrading per darci una buona idea della profittabilità di una strategia.

Dato che abbiamo ai dati del tick (timestamp bid/ask) possiamo incorporare lo spread nei costi di transazione. Possiamo anche modellare lo slippage. È invece meno immediato modellare l’impatto del mercato, sebbene ciò sia meno preoccupante per trade con piccoli importi (come per i trader retail) o quando si opera su strumenti molti liquidi.

Oltre ai costi di transazione, vogliamo modellare una solida gestione del portafoglio utilizzando la gestione del rischio e dimensionamento della posizione.

Quindi, cosa è attualmente incluso nel nostro Forex Trading System fino ad oggi?

  • Architettura guidata dagli eventi – Il sistema di trading forex è stato progettato da zero come un sistema guidato dagli eventi, poiché è così che un sistema di trading intraday verrà implementato in un ambiente live.
  • Streaming dei prezzi : abbiamo un oggetto base di streaming dei prezzi di mercato. Questo attualmente gestisce lo streaming di una sola coppia, ma possiamo facilmente modificarlo per gestire più coppie di valute.
  • Generazione del segnale – Possiamo incorporare strategie di trading (basate direttamente sui prezzi tick passati e attuali) utilizzando l’oggetto Strategy, che crea oggetto SignalEvent.
  • Esecuzione degli ordini : disponiamo di un ingenuo sistema di esecuzione degli ordini che invia gli ordini alla cieca da Portfolio averso OANDA. Con “alla cieca” si intende che non viene effettuata alcuna gestione del rischio o dimensionamento della posizione, né alcuna esecuzione algoritmica che potrebbe portare a costi di transazione ridotti.
  • Valuta di base EUR – Per semplificare le cose, abbiamo implementato il sistema solo per la valuta di base EUR. Questo è forse l’aspetto più importante da modificare visto che il mercato forex offre strumenti  denominati in USD, GBP, CAD, JPY, AUD e NZD!
  • Trading EUR / USD – Abbiamo scelto EUR/USD come coppia di valute iniziale con cui testare gli oggetti Position Portfolio. La gestione di più coppie di valute è un importante passaggio successivo. Ciò comporterà modifiche alla posizione e ai calcoli del portafoglio.
  • Gestione dei decimali : qualsiasi sistema di trading live deve gestire correttamente i calcoli di valuta. In particolare, i valori di valuta non dovrebbero essere memorizzati come tipi di dati in virgola mobile, poiché gli errori di arrotondamento si accumulano. Per maggiori dettagli si può consultare questo fantastico articolo sulle rappresentazioni in virgola mobile.
  • Trading Long / Short – Tra l’articolo #2 della serie e l’articolo #3 abbiamo aggiunto la possibilità di shortare una coppia di valute (invece di poter solo andare long). Fondamentalmente, anche questo è oggetto di unit test.
  • Gestione del portafoglio locale – A mio parere, eseguire un backtest che gonfia le prestazioni della strategia a causa di ipotesi non realistiche è inutile nella migliore delle ipotesi ed estremamente poco redditizio nel peggiore dei casi! L’introduzione di un oggetto di portafoglio locale che replica i calcoli di OANDA significa che possiamo controllare i nostri calcoli interni mentre eseguiamo il paper trading, il che ci dà maggiore fiducia quando in seguito utilizzeremo lo stesso oggetto di portafoglio per il backtest sui dati storici.
  • Unit test per Position/Portfolio – Anche se è stato direttamente menzionato direttamente negli articoli #1 e #2, ho effettivamente scritto alcuni unit test per gli oggetti Portfolio Position. Poiché questi sono così cruciali per i calcoli della strategia, bisogna essere estremamente sicuri che funzionino come previsto. Un ulteriore vantaggio di tali test è la possibilità di modificare il calcolo sottostante, in modo tale che se tutti i test continuano ad essere superati, possiamo essere certi che il sistema continuerà a comportarsi come previsto.

In questa fase il Forex Trading System manca delle seguenti funzionalità:

  • Gestione dello slippage – Il sistema sta attualmente generando molti slippage a causa della natura ad alta frequenza dei dati tick forniti da OANDA. Ciò significa che il saldo del portafoglio calcolato localmente non riflette il saldo calcolato da OANDA. Fino a quando non viene eseguita la corretta gestione degli eventi e la regolazione dello slippage, il backtest non rifletterà correttamente la realtà.
  • Valute di base multiple – Attualmente siamo limitati a EUR. Per lo meno dobbiamo includere le principali denominazioni di valuta: USD, GBP, CAD, AUD, JPY e NZD.
  • Coppie di valute multiple – Allo stesso modo dobbiamo supportare le principali coppie di valute oltre EUR / USD). Ci sono due aspetti da considerare. Il primo è gestire correttamente i calcoli quando né la base né la quotazione di una coppia di valute è uguale alla valuta di denominazione del conto. Il secondo aspetto è supportare più posizioni in modo da poter negoziare un portafoglio di coppie di valute.
  • Gestione del rischio – Molti backtest di “ricerca” ignorano completamente la gestione del rischio. Purtroppo questo è generalmente necessario per brevità nel descrivere le regole di una strategia. In realtà dobbiamo prevedere una gestione del rischio durante il trading, altrimenti è estremamente probabile che prima o poi subiremo una pesante perdita. Questo non vuol dire che la gestione del rischio possa prevenirla del tutto, ma certamente la rende meno probabile!
  • Ottimizzazione del portafoglio – In un contesto istituzionale avremo un mandato di investimento, che determinerà un solido sistema di gestione del portafoglio con varie regole di allocazione. In un contesto di trading retail / personale potremmo voler utilizzare un approccio di dimensionamento della posizione come il criterio di Kelly per massimizzare il nostro tasso di crescita composto nel lungo termine.
  • Strategie robuste – Finora abbiamo dimostrato solo alcune semplici strategie “giocattolo” che generano segnali casuali. Ora che stiamo iniziando a creare un sistema di trading forex intraday affidabile, dovremmo iniziare a mettere in atto alcune strategie più interessanti. I prossimi articoli di questa serie si concentreranno su strategie tratte da una combinazione di indicatori / filtri “tecnici”, modelli di serie temporali e tecniche di apprendimento automatico.
  • Distribuzione remota : Dato che siamo potenzialmente interessati al trading 24 ore su 24 (almeno durante la settimana!), Abbiamo bisogno di una configurazione più sofisticata rispetto all’esecuzione del backtest su un computer desktop / laptop locale a casa. È fondamentale creare una solida distribuzione del nostro sistema in un server remoto, con specifici tool di ridondanza e monitoraggio.
  • Backtest storico : Abbiamo costruito l’oggetto Portfolio per poter eseguire un backtest realistico. In questa fase ci manca un sistema di archiviazione dei dati storici dei tick. Negli articoli successivi esamineremo come ottenere i dati storici dei tick e archiviarli in un database appropriato, come HDF5 .
  • Database dei Trade – Inoltre è opportuno memorizzare i nostri trade in tempo reale in un database. Ciò ci consentirà di eseguire le nostre analisi sui dati di live trading. Una buona raccomandazione per un database relazionale è PostgreSQL o MySQL .
  • Monitoraggio e alta disponibilità : Dato che stiamo costruendo un sistema intraday ad alta frequenza, dobbiamo mettere in atto un monitoraggio completo e una ridondanza ad alta disponibilità. Ciò significa generare rapporti sull’utilizzo della CPU, utilizzo del disco, I/O di rete, latenza e verificare che gli script periodici siano impostati per continuare a funzionare. Inoltre abbiamo bisogno di una strategia di backup e ripristino. Chiediti quali piani di backup avresti in atto se avessi grandi posizioni aperte, in un mercato volatile e il tuo server morisse improvvisamente. Credimi, succede!
  • Integrazione Multiple Broker / FIX – Al momento siamo fortemente legati al broker OANDA. Come ho detto in precedenza, perché semplicemente mi sono imbattuto nella loro API e ho trovato che fosse un’ottima infrastruttura per i test. Ci sono molti altri broker là fuori, molti dei quali supportano il protocollo FIX. L’aggiunta di una funzionalità FIX aumenterebbe il numero di broker che potrebbero essere utilizzati nel sistema.
  • Controllo e reportistica GUI – In questo momento il sistema è completamente basato su console / riga di comando. Per lo meno avremo bisogno di alcuni grafici di base per visualizzare i risultati del backtest. Un sistema più sofisticato incorporerà statistiche riassuntive delle operazioni, metriche delle prestazioni a livello di strategia e prestazioni complessive del portafoglio. Questa GUI potrebbe essere implementata utilizzando un sistema di finestre multipiattaforma come Qt o Tkinter . Potrebbe anche essere implementato un front-end basato sul web, utilizzando un framework web come Django .

Come si può vedere, sono rimaste molte funzionalità sulla roadmap! Detto questo, ogni nuovo articolo della serie farà avanzare il progetto.

Tipi di dati decimali

Dopo aver discusso il piano a lungo termine, descriviamo alcune delle modifiche apportate al codice presentato nell’articolo #2 di questa serie. In particolare, descriviamo le modifiche al codice necessarie per gestire il tipo di dati Decimal invece di utilizzare le variabili a virgola mobile. Si tratta di un cambiamento estremamente importante poiché le rappresentazioni in virgola mobile sono una primaria fonte di errori a lungo termine nei sistemi di gestione del portafoglio e degli ordini.

Python supporta nativamente le rappresentazioni decimali con una precisione arbitraria. La funzionalità è contenuta nella libreria decimal.

In particolare abbiamo bisogno di modificare -ogni- valore che appare nei calcoli implementati in Position in un tipo di dato Decimal. Ciò include le unità, l’esposizione, i pip, il profitto e il profitto percentuale. Ciò garantisce che abbiamo il pieno controllo di come vengono gestiti i problemi di arrotondamento quando si tratta di rappresentazioni di valute con una precisione a due cifre decimali. In particolare dobbiamo scegliere il metodo di arrotondamento. Python supporta alcuni tipi diversi, in questo caso usiamo ROUND_HALF_DOWN, che arrotonda all’intero più vicino con legami che vanno verso lo zero.

Ecco un esempio delle modifiche al codice per gestire i tipi di dati Decimal rispetto alle loro precedenti rappresentazioni in virgola mobile. Di seguito è riportato il codice di position.py:

            from decimal import Decimal, getcontext, ROUND_HALF_DOWN

class Position(object):
    def __init__(
        self, side, market, units, 
        exposure, avg_price, cur_price
    ):
        self.side = side
        self.market = market
        self.units = units
        self.exposure = Decimal(str(exposure))
        self.avg_price = Decimal(str(avg_price))
        self.cur_price = Decimal(str(cur_price))
        self.profit_base = self.calculate_profit_base(self.exposure)
        self.profit_perc = self.calculate_profit_perc(self.exposure)

    def calculate_pips(self):
        getcontext.prec = 6
        mult = Decimal("1")
        if self.side == "SHORT":
            mult = Decimal("-1")
        return (mult * (self.cur_price - self.avg_price)).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def calculate_profit_base(self, exposure):
        pips = self.calculate_pips()        
        return (pips * exposure / self.cur_price).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def calculate_profit_perc(self, exposure):
        return (self.profit_base / exposure * Decimal("100.00")).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def update_position_price(self, cur_price, exposure):
        self.cur_price = cur_price
        self.profit_base = self.calculate_profit_base(exposure)
        self.profit_perc = self.calculate_profit_perc(exposure)
        

Da notare come dobbiamo l’argomento di Decimal è una stringa, piuttosto che un argomento in virgola mobile. Questo perché una stringa specifica con esattezza la precisione del valore, mentre non è possibile con un argomento a virgola mobile.

Inoltre bisogna tenere presente che la memorizzazione delle operazioni in un database relazionale (come descritto nella roadmap al paragrafo precedente) prevede di utilizzare il corretto tipo di dati. PostgreSQL e MySQL supportano una rappresentazione decimale. È fondamentale utilizzare questi tipi di dati quando si crea lo schema del database, altrimenti si avranno errori di arrotondamento estremamente difficili da diagnosticare!

Per coloro che sono interessati a una discussione più approfondita di questi problemi, in matematica e informatica, il tema dell’analisi numerica copre i problemi di archiviazione in virgola mobile, tra molti altri argomenti interessanti.

Nei prossimi articoli del diario descriveremo come applicare gli unit test al codice e come estendere il sistema per gestire più coppie di valute, modificando i calcoli di posizione.

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 #2 – Aggiunta di un Portafoglio al Sistema di Trading Automatico sul Forex

forex-python-trading-algoritmico-002

Nel primo articolo della serie sul trading algoritmico sul Forex (link) abbiamo descritto come creare un sistema di trading automatico che si collega all’API del broker OANDA. Abbiamo anche menzionato che i passaggi successivi includevano la costruzione di un portafoglio e una copertura per la gestione del rischio da applicare per tutti i segnali suggeriti e generati dalla componente Strategy. In questo articolo descriviamo come costruire una componente di Portfolio completa e funzionante.

Questa componente è necessaria se vogliamo costruire un motore di backtest per le strategie forex in modo analogo a quanto descritto in precedenza con il mercato azionario tramite il backtester guidato dagli eventi. In particole si vuole un ambiente che presenti una differenza minima tra il trading live e il sistema di backtest. Per questo motivo dobbiamo realizzare una componente di portafoglio che riflettesse (per quanto possibile) lo stato attuale del conto di trading fornito da OANDA.

La logica base prevede che il conto di trading “practice” e le componenti del portafoglio locale dovrebbero avere valori simili, se non uguali, per attributi come il saldo del conto, il profitto e la perdita (P&L) non realizzati, il conto economico realizzato e qualsiasi posizione aperta . Se raggiungiamo questo obiettivo ed eseguiamo alcune strategie di test tramite questa componente di portafoglio, e se gli attributi risultino con valori uguali sia nel portfolio locale che in OANDA, allora potremmo essere fiduciosi nella capacità del backtester di produrre risultati più realistici, molto simili a quelli che si le strategie avrebbero avuto se fossero state in “live”.

Ho passato gli ultimi due giorni a tentare di implementare un tale oggetto Portfolio e credo di esserci quasi riuscito. Vedo ancora alcune differenze tra il saldo del portafoglio locale e il saldo del conto OANDA dopo che sono state effettuate diverse operazioni.

Quali sono i limiti attuali di questa implementazione?

  • La valuta di base, e quindi l’esposizione, è codificata per essere EUR. Deve essere cambiato per consentire la scelta di qualsiasi valuta di base.
  • Attualmente l’ho testato solo per EUR / USD, poiché la mia valuta di base è EUR Successivamente modificherò i calcoli dell’esposizione per consentire qualsiasi coppia di valute.
  • Sebbene alcuni unit test abbiano suggerito che l’aggiunta e la rimozione di posizioni e unità sta funzionando come previsto, non è stato ancora testato.
  • Finora l’ho provato solo con l’apertura e la chiusura di posizioni long, non ho testato posizioni short. Avrò bisogno di scrivere alcuni unit test per gestire le posizioni short.

Ci si potrebbe ragionevolmente chiedere perché sto descrivendo questa componente se presenta tutte queste limitazioni? In questo modo vorrei che qualsiasi lettore di questo articolo possa essere consapevole di come la creazione di sistemi di trading algoritmico è un lavoro duro e richiede molta attenzione ai dettagli! C’è un notevole margine di manovra per introdurre bug e comportamenti scorretti. Voglio delineare come vengono costruiti i sistemi del “mondo reale” e mostrarvi come testare questi errori e correggerli.

Inizieremo descrivendo come ho costruito l’attuale configurazione del portafoglio e poi l’ho integrato nel sistema di trading demo che abbiamo esaminato nel precedente articolo.  Successivamente vedremo i punti in cui penso ci possano essere differenze.

Il seguente codice “così com’è” sotto il disclaimer che ho indicato nel precedente articolo.

Creazione del Portfolio

Per generare un oggetto Portfolio è necessario descrivere come vengono eseguite le negoziazioni in valuta, poiché differiscono in modo sostanziale dalle azioni.

Calcolo di Pips e Unità

In altre classi di attività, il più piccolo incremento di una variazione del prezzo dell’asset è noto come “tick”. Nel trading in Forex è noto come “pip” (Price Interest Point). È l’incremento più piccolo in qualsiasi coppia di valute ed è (di solito) 1/100 di centesimo, noto anche come punto base. Dato che la maggior parte delle principali coppie di valute ha un prezzo di quattro cifre decimali, la variazione più piccola si verifica sull’ultimo punto decimale.

In EUR / USD, ad esempio, un movimento da 1.1184 a 1.1185 è un pip (4 cifre decimali) e quindi un pip è uguale a 0.0001. Qualsiasi valuta basata sullo yen giapponese utilizza due punti decimali, quindi un pip sarebbe uguale a 0,01. 

Una domanda che possiamo ora porci è: A quanto equivale in euro (EUR) un movimento di 20 pips (20 x 0,0001 = 0,002) per una quantità fissa di unità di EUR/USD? Se prendiamo 2.000 unità della valuta di base (ad esempio 2.000 euro), possiamo calcolare il P&L in euro come segue: 

Profitto (EUR) = Pip x Esposizione / EURUSD = 0,002 x 2.000 / 1,1185 = 3,57

Con OANDA siamo liberi di scegliere il numero di quote negoziate (e quindi la generica esposizione). Dal momento che ho un conto in euro (EUR) e sto negoziando EUR/USD (in questo esempio) l’esposizione sarà sempre uguale al numero di unità. Questo è attualmente “codificato” nel sistema sottostante. Nel caso si vuole gestire più coppie di valute, è indispensabile modificare il calcolo dell’esposizione per tenere conto delle diverse valute di base.

Poiché il valore del profitto sopra descritto è piuttosto piccolo e le valute non oscillano molto (tranne quando lo fanno!), di solito è necessario introdurre la leva finanziaria nel conteggio. Discuteremo di questo negli articoli successivi. Per ora, non  dobbiamo preoccuparcene.

Panoramica del sistema di backtesting / trading

Il sistema attuale è costituito dai seguenti componenti:

  • Event – I componenti Evento trasportano i “messaggi” (come tick, segnali e ordini) tra gli oggetti Strategy, Portfolio ed Esecution.
  • Position – La componente Position rappresenta il concetto di una “posizione” Forex, ovvero un “long” o uno “short” in una coppia di valute con associata una quantità di unità.
  • Portfolio: il componente Portfolio contiene più oggetti Position, uno per ciascuna coppia di valute negoziata. Tiene traccia dell’attuale P&L di ciascuna posizione, anche dopo successivi incrementi e riduzioni di unità.
  • Strategy – L’oggetto Strategy prende le informazioni delle serie temporali (tick delle coppie di valute) e quindi calcola e invia gli eventi di segnale al portafoglio, che decide come agire su di essi.
  • Streaming Forex Price: questo componente si collega a OANDA tramite un web-socket in streaming e riceve dati tick-by-tick in tempo reale (ovvero bid / ask) da qualsiasi coppia di valute sottoscritta.
  • Esecution: si prende gli eventi  di tipo “Ordine” e li invia a OANDA per essere eseguiti.
  • Trading Loop – Il trading loop avvolge insieme tutti i componenti descritti sopra ed esegue due thread: uno per i prezzi di streaming e uno per il gestore di eventi.

Per ottenere maggiori informazioni su come il sistema è collegato insieme, vale la pena leggere il precedente articolo di questa serie.

Implementazione in Python

Discuteremo ora come implementare in Python il sistema appena descritto.

Position

Il primo componente è l’oggetto Position. È progettato per replicare il comportamento di una posizione aperta nel sistema fxTrade Practice di OANDA. La scheda Position nel software fxTrade contiene 8 colonne:

  • Type: indica se la posizione è “long” o “short”
  • Market: quale coppia di valute negoziare, ad es. “EUR/USD”
  • Unit: il numero di unità della valuta (vedi sopra)
  • Exposure (BASE) – L’esposizione nella valuta base della posizione
  • Avg. Price: il prezzo medio raggiunto per più acquisti. Se ci sono \(P\) acquisti, il prezzo medio viene calcolato come \(\frac{\sum_{p=1} ^ Pc_pu_p} {\sum_{p=1}^{P}u_p}\), dove \(c_p\) è il costo di acquisto \(p\) e \(u_p\) sono le unità acquisite per l’acquisto \(p\).
  • Current: il prezzo di vendita corrente.
  • Profit (BASE) – L’attuale P&L nella valuta base della posizione.
  • Profit (%): l’attuale percentuale di P&L della posizione.

Come è evidente nel codice seguente, questi attributi sono stati riflessi come membri della classe Position, ad eccezione di “Type”, che ho rinominato “side”, poiché type è una parola riservata in Python!

La classe ha quattro metodi (esclusa l’inizializzazione): calculate_pips, calculate_profit_base, calculate_profit_perc e update_position_price.

Il primo metodo, calculate_pips, determina il numero di pips che sono stati generati dalla posizione da quando è stata aperta (tenendo conto di eventuali nuove unità aggiunte alla posizione). Il secondo metodo, calculate_profit_base, calcola il profitto (o la perdita!) corrente sulla posizione. Il terzo metodo, calculate_profit_perc, determina la percentuale di profitto sulla posizione. Infine, update_position_price aggiorna i due valori precedenti in base ai dati di mercato correnti.

            class Position(object):
    def __init__(
        self, side, market, units,
        exposure, avg_price, cur_price
    ):
        self.side = side
        self.market = market
        self.units = units
        self.exposure = exposure
        self.avg_price = avg_price
        self.cur_price = cur_price
        self.profit_base = self.calculate_profit_base()
        self.profit_perc = self.calculate_profit_perc()

    def calculate_pips(self):
        mult = 1.0
        if self.side == "SHORT":
            mult = -1.0
        return mult * (self.cur_price - self.avg_price)

    def calculate_profit_base(self):
        pips = self.calculate_pips()
        return pips * self.exposure / self.cur_price

    def calculate_profit_perc(self):
        return self.profit_base / self.exposure * 100.0

    def update_position_price(self, cur_price):
        self.cur_price = cur_price
        self.profit_base = self.calculate_profit_base()
        self.profit_perc = self.calculate_profit_perc()
        

Poiché un portafoglio può contenere più posizioni, ci sarà un’istanza di classe per ogni mercato che viene negoziato. Come accennato in precedenza, al momento il Portfoilo gestisce solamente EUR come valuta di base e EUR/USD come strumento di trading. Negli articoli futuri vedremo In articoli futuri estenderò l’oggetto Portfolio per gestire più valute di base e più coppie di valute. Parliamo ora di come configurare un ambiente virtuale di base per Python e quindi di come funziona il Portfolio.

Symlink per l'ambiente virtuale

Nel seguente modulo dell’oggetto Portfolio ho modificato il modo in cui vengono gestite le importazioni. Ho creato un ambiente virtuale, per cui ho aggiunto un collegamento simbolico alla mia directory DTForex. Ciò mi consente di fare riferimento a una gerarchia annidata di file di progetto all’interno di ogni modulo Python. Il codice per realizzare questo in Ubuntu è simile al seguente:

            cd /PATH/TO/YOUR/VIRTUALENV/DIRECTORY/lib/python3/site-packages/
ln -s /PATH/TO/YOUR/DTFOREX/DIRECTORY/ROOT/ DTForex
        

Ovviamente si deve sostituire le posizioni del tuo ambiente virtuale e la posizione del codice sorgente. Normalmente memorizzo i miei ambienti virtuali nella directory home in ~/venv/. Memorizzo i miei progetti nella directory home in ~/sites/. Questo mi consente di fare riferimento, ad esempio, a dtforex.event.event import OrderEvent da qualsiasi file all’interno del progetto.

Portfolio

Il costruttore __init__ del Portfolio richiede i seguenti argomenti:

  • ticker – il gestore del ticker dei prezzi forex in streaming. Viene utilizzato per ottenere gli ultimi prezzi bid / ask.
  • event: la coda degli eventi, in cui il portfolio deve inserire gli eventi.
  • base – la valuta di base, nel mio caso è EUR.
  • leverage – il fattore di leva. Attualmente è 1:20.
  • equity – la quantità di patrimonio netto effettivo nel conto, che ho impostato per default a 100.000.
  • risk_per_trade – la percentuale del patrimonio netto del conto da poter rischiare per ogni operazione, che ho impostato di default al 2%. Ciò significa che le unità di scambio saranno pari a 2.000 per una dimensione del conto iniziale di 100.000.

All’inizializzazione la classe calcola le trade_units, che sono la quantità massima di unità consentite per posizione, oltre a dichiarare il dizionario delle positions (ogni mercato è una chiave) che contiene tutte le posizioni aperte all’interno del portafoglio:

            from copy import deepcopy

from event import OrderEvent
from portfolio import Position


class Portfolio(object):
    def __init__(
        self, ticker, events, base="EUR", leverage=20, 
        equity=100000.0, risk_per_trade=0.02
    ):
        self.ticker = ticker
        self.events = events
        self.base = base
        self.leverage = leverage
        self.equity = equity
        self.balance = deepcopy(self.equity)
        self.risk_per_trade = risk_per_trade
        self.trade_units = self.calc_risk_position_size()
        self.positions = {}
        

In questa fase la “gestione del rischio” è piuttosto semplice! Nel seguente metodo calc_risk_position_size ci assicuriamo solamente che l’esposizione di ciascuna posizione non superi il risk_per_trade% del capitale del conto. Il valore predefinito del risk_per_trade è 2% come argomento della parola chiave, sebbene questo possa ovviamente essere modificato. Quindi per un conto di 100.000 euro, il rischio per operazione non supererà 2.000 euro per posizione.

Nota che questa cifra non si ridimensionerà dinamicamente con la dimensione del saldo del conto, utilizzerà solo il saldo del conto iniziale. Le implementazioni successive incorporeranno logiche più sofisticate di gestione del rischio e dimensionamento della posizione.

             def calc_risk_position_size(self):
        return self.equity * self.risk_per_trade
        

Il successivo metodo, add_new_position, richiede i parametri necessari per aggiungere una nuova posizione al Portfolio. In particolare, richiede add_price e remove_price. Non si utilizza direttamente i prezzi bid/ask perché i prezzi dipenderanno dal fatto che il lato sia “long” o “short”. Quindi dobbiamo specificare correttamente quale prezzo considerare in modo da ottenere un backtest realistico:

                def add_new_position(
        self, side, market, units, exposure,
        add_price, remove_price
    ):
        ps = Position(side, market, units, exposure,
                      add_price, remove_price
                     )
        
        self.positions[market] = ps
        

Abbiamo anche bisogno di un metodo, add_position_units, che consente di aggiungere unità ad una posizione, solamente dopo aver precedentemente creato la posizione. Per fare ciò dobbiamo calcolare il nuovo prezzo medio delle unità acquistate. Ricorda che questo viene calcolato dalla seguente espressione:

\(\begin{eqnarray}\frac{\sum_{p=1}^{P} c_p u_p} {\sum_{p = 1} ^ {P} u_p} \end{eqnarray}\)

Dove \(P\) è il numero di acquisti, \(c_p\) è il costo di acquisto \(p\) e \(u_p\) sono le unità acquistate con l’acquisto \(p\).

Una volta calcolato il nuovo prezzo medio, le unità vengono aggiornate nella posizione e quindi viene ricalcolato il P&L associato alla posizione:

                def add_position_units(
        self, market, units, exposure, 
        add_price, remove_price
    ):
        if market not in self.positions:
            return False
        else:
            ps = self.positions[market]
            new_total_units = ps.units + units
            new_total_cost = ps.avg_price*ps.units + add_price*units
            ps.exposure += exposure
            ps.avg_price = new_total_cost/new_total_units
            ps.units = new_total_units
            ps.update_position_price(remove_price)
            return True
        

Allo stesso modo, abbiamo bisogno di un metodo per rimuovere le unità da una posizione (ma non per chiuderla completamente). Questo è implementato da remove_position_units. Una volta che le unità e l’esposizione sono state ridotte, il conto economico viene calcolato per le unità rimosse e quindi aggiunto (o sottratto!) dal saldo del portafoglio:

                def remove_position_units(
        self, market, units, remove_price
    ):
        if market not in self.positions:
            return False
        else:
            ps = self.positions[market]
            ps.units -= units
            exposure = float(units)
            ps.exposure -= exposure
            ps.update_position_price(remove_price)
            pnl = ps.calculate_pips() * exposure / remove_price 
            self.balance += pnl
            return True
        

Abbiamo anche bisogno di un modo per chiudere completamente una posizione. Questo è implementato in close_position. È simile a remove_position_units tranne per il fatto che la posizione viene eliminata dal dizionario positions:

                def close_position(
            self, market, remove_price
    ):
        if market not in self.positions:
            return False
        else:
            ps = self.positions[market]
            ps.update_position_price(remove_price)
            pnl = ps.calculate_pips() * ps.exposure / remove_price
            self.balance += pnl
            del [self.positions[market]]
            return True
        

La maggior parte del lavoro di questa classe viene eseguita dal metodo execute_signal. Il metodo rende gli oggetti SignalEvent creati dagli oggetti Strategy e li utilizza per generare oggetti OrderEvent da reinserire nella coda degli eventi.

La logica di base è la seguente:

  • Se non esiste una posizione corrente per questa coppia di valute, creane una.
  • Se una posizione esiste già, controlla se sta aggiungendo o sottraendo unità.
  • Se sta aggiungendo unità, aggiungi semplicemente la quantità corretta di unità.
  • Se non sta aggiungendo unità, controlla se la nuova riduzione di unità avversaria chiude lo scambio, in tal caso fallo.
  • Se le unità di riduzione sono inferiori alle unità di posizione, rimuovere semplicemente quella quantità dalla posizione.
  • Tuttavia, se le unità riducenti superano la posizione corrente, è necessario chiudere la posizione corrente dalle unità riducenti e quindi creare una nuova posizione opposta con le unità rimanenti. Non l’ho ancora testato ampiamente, quindi potrebbero esserci ancora dei bug!

Il codice per execute_signal segue:

                def execute_signal(self, signal_event):
        side = signal_event.side
        market = signal_event.instrument
        units = int(self.trade_units)

        # Controlla il lato per il corretto prezzo bid/ask
        # TODO: Supporta solo i long
        add_price = self.ticker.cur_ask
        remove_price = self.ticker.cur_bid
        exposure = float(units)

        # Se non c'è una posizione, si crea una nuova
        if market not in self.positions:
            self.add_new_position(
                side, market, units, exposure,
                add_price, remove_price
            )
            order = OrderEvent(market, units, "market", "buy")
            self.events.put(order)
        # Se la posizione esiste, si aggiunge o rimuove unità
        else:
            ps = self.positions[market]
            # controlla se il lato è coerente con il lato della posizione
            if side == ps.side:
                # aggiunge unità alla posizione
                self.add_position_units(market, units, exposure,
                                        add_price, remove_price
                                        )
            else:
                # Controlla se ci sono unità nella posizione
                if units == ps.units:
                    # Chiude la posizione
                    self.close_position(market, remove_price)
                    order = OrderEvent(market, units, "market", "sell")
                    self.events.put(order)
                elif units < ps.units:
                    # Rimuove unità dalla posizione
                    self.remove_position_units(
                        market, units, remove_price
                    )
                else:  # units > ps.units
                    # Chiude la posizione e crea una nuova posizione
                    # nel lato opposto con le unità rimanenti
                    new_units = units - ps.units
                    self.close_position(market, remove_price)

                    if side == "buy":
                        new_side = "sell"
                    else:
                        new_side = "sell"
                    new_exposure = float(units)
                    self.add_new_position(
                        new_side, market, new_units,
                        new_exposure, add_price, remove_price
                    )
        print
        "Balance: %0.2f" % self.balance
        

Questo conclude il codice per la classe Portfolio. Ora discutiamo della gestione degli eventi.

Event

Affinché questo Portfolio funzioni con le nuove logiche di generazione di segnali e ordini è necessario modificare event.py. In particolare abbiamo aggiunto la componente SignalEvent, che ora è generato dall’oggetto Strategy, invece di un OrderEvent. Indica semplicemente se andare long o short su un particolare “strumento”, cioè una coppia di valute. L’order_type si riferisce al fatto che l’ordine sia un ordine di mercato o un ordine limite. Non ho abbiamo ancora implementato quest’ultimo, quindi per ora sarà sempre valorizzato come “market”:

            class Event(object):
    pass


class TickEvent(Event):
    def __init__(self, instrument, time, bid, ask):
        self.type = 'TICK'
        self.instrument = instrument
        self.time = time
        self.bid = bid
        self.ask = ask


class SignalEvent(Event):
    def __init__(self, instrument, order_type, side):
        self.type = 'SIGNAL'
        self.instrument = instrument
        self.order_type = order_type
        self.side = side        


class OrderEvent(Event):
    def __init__(self, instrument, units, order_type, side):
        self.type = 'ORDER'
        self.instrument = instrument
        self.units = units
        self.order_type = order_type
        self.side = side        
        

Strategy

Dopo aver definito l’oggetto SignalEvent, dobbiamo modificare la logica di funzionamento della classe Strategy. In particolare, ora deve generare eventi SignalEvent invece di OrderEvents.

Dobbiamo effettivamente cambiare la logica base della “strategia”. Invece di creare segnali casuali di acquisto o vendita, ora genera un ordine di acquisto ogni 5 tick e quindi il sistema diventa “investito”. Al 5° tick successivo, se è investito, si effettua una vendita e diventa “non investito”. Questo processo si ripete in un ciclo infinito:

            from event import SignalEvent

class TestStrategy(object):
    def __init__(self, instrument, events):
        self.instrument = instrument
        self.events = events
        self.ticks = 0
        self.invested = False

    def calculate_signals(self, event):
        if event.type == 'TICK':
            self.ticks += 1
            if self.ticks % 5 == 0:
                if self.invested == False:
                    signal = SignalEvent(self.instrument, "market", "buy")
                    self.events.put(signal)
                    self.invested = True
                else:
                    signal = SignalEvent(self.instrument, "market", "sell")
                    self.events.put(signal)
                    self.invested = False
        

StreamingForexPrices

L’oggetto Portfolio richiede un oggetto ticker che contienea i prezzi ask/bid più recenti. Abbiamo semplicemente modificato StreamingForexPrices nel file streaming.py per contenere due attributi extra:

            ..
..
        self.cur_bid = None
        self.cur_ask = None
..
..
        

Questi attributo sono valorizzati nel metodo stream_to_queue:

            ..
..
                if msg.has_key("instrument") or msg.has_key("tick"):
                    print msg
                    instrument = msg["tick"]["instrument"]
                    time = msg["tick"]["time"]
                    bid = msg["tick"]["bid"]
                    ask = msg["tick"]["ask"]
                    self.cur_bid = bid
                    self.cur_ask = ask
                    tev = TickEvent(instrument, time, bid, ask)
                    self.events_queue.put(tev)
        

Come per ogni oggetto di questo articolo, il codice completo può essere trovato nel seguente repository di github: github.com/datatrading-info/DTForex

Trading

L’ultima parte delle modifiche è relativo al file trading.py. Per prima cosa si modificano le importazioni per tenere conto della struttura della directory e del fatto che ora stiamo importando un oggetto Portfolio:

            from execution import Execution
from portfolio import Portfolio
from settings import STREAM_DOMAIN, API_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID
from strategy import TestStrategy
from data import StreamingForexPrices
        

Quindi modifichiamo il gestore della coda degli eventi per indirizzare SignalEvents all’istanza di Portfolio:

            ..
..
    while True:
        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)
..
..
        

Infine modifichiamo la funzione __main__ per creare il Portfolio e aggiustiamo trade_thread per prendere il Portfolio come argomento:

                ..
    ..
    # Crea un oggetto Portfolio che sarà usato per 
    # confrontare le posizioni OANDA con quelle locali
    # in modo da verificare l'integrità del backtesting.
    portfolio = Portfolio(prices, events, equity=100000.0)

    # Crea due threads separati: Uno per il ciclo di trading
    # e l'altro per lo streaming dei prezzi di mercato
    trade_thread = threading.Thread(
        target=trade, args=(
            events, strategy, portfolio, execution
        )
    )
    ..
    ..
        

Variabili d'ambiente nelle impostazioni

Nell’articolo precedente abbiamo menzionato che non è una buona idea memorizzare le password o altre informazioni di autenticazione, inclusi il token API, all’interno del codice sorgente. Quindi si può modificare il file delle impostazioni in questo modo:
            import os

ENVIRONMENTS = { 
    "streaming": {
        "real": "stream-fxtrade.oanda.com",
        "practice": "stream-fxpractice.oanda.com",
        "sandbox": "stream-sandbox.oanda.com"
    },
    "api": {
        "real": "api-fxtrade.oanda.com",
        "practice": "api-fxpractice.oanda.com",
        "sandbox": "api-sandbox.oanda.com"
    }
}

DOMAIN = "practice"
STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN]
API_DOMAIN = ENVIRONMENTS["api"][DOMAIN]
ACCESS_TOKEN = os.environ.get('OANDA_API_ACCESS_TOKEN', None)
ACCOUNT_ID = os.environ.get('OANDA_API_ACCOUNT_ID', None)
        
Nello specifico, le seguenti due righe:
            ACCESS_TOKEN = os.environ.get('OANDA_API_ACCESS_TOKEN', None)
ACCOUNT_ID = os.environ.get('OANDA_API_ACCOUNT_ID', None)
        

Abbiamo utilizzato la libreria os per recuperare due variabili di ambiente (ENVVARS). Il primo è il token di accesso API e il secondo è l’ID account OANDA. Questi possono essere memorizzati in un file di ambiente che viene caricato all’avvio del sistema. In Ubuntu, puoi usare il file .bash_profile nascosto nella tua directory home. Ad esempio, usando l’editor di testo preferito, si può digitare:

            emacs ~/.bash_profile
        
Si aggiungono le seguenti righe, assicurandosi di sostituire le variabili con i dettagli di un account practice:
            export OANDA_API_ACCESS_TOKEN='1234567890abcdef1234567890abcdef1234567890abcdef'
export OANDA_API_ACCOUNT_ID='12345678'
        
Potrebbe essere necessario assicurarsi che il terminale abbia accesso a queste variabili eseguendo quanto segue da riga di comando:
            source ~/.bash_profile
        

Esecuzione del Codice

Per eseguire il codice è necessario assicurarsi che l’ambiente virtuale sia correttamente impostato. Lo si può verificare eseguendo il seguente comando (attenzione e a specificare la directory corretta):
            source ~/venv/qsforex/bin/activate
        

Si dovrà anche installare la libreria requests, se non è statp fatto durante l’articolo precedente:

            pip install requests
        
Infine, si può eseguire il codice (assicurandosi di adattare il percorso al codice sorgente del progetto):
            python dtforex/trading/trading.py
        

A questo punto, stiamo effettuando il nostro primo sismtea di trading! Come affermato nell’articolo precedente, è molto facile perdere denaro con un sistema di questo tipo collegato a un conto di trading live! Assicurati di visualizzare il disclaimer nel post e di essere estremamente attento con gli oggetti Strategy. Consiglio vivamente di provarlo sulla sandbox o sugli account di esercitazione prima di un’implementazione live.

Tuttavia, prima di procedere con l’implementazione di strategie personalizzate, vorrei discutere da dove credo derivino alcune delle differenze tra il saldo del conto OANDA e il saldo calcolato.

Possibili fonti di errore

Man mano che l’implementazione dei sistemi diventa più complessa, aumenta il rischio che siano stati introdotti bug. Si possono utilizzare alcuni unit test per verificare se gli oggetti Position e Portfolio si comportano come previsto, ma ci sono ancora discrepanze tra il portafoglio locale e il saldo del conto OANDA. Le possibili ragioni includono:

  • Bug – Ovviamente i bug possono insinuarsi ovunque. Il modo migliore per eliminarli è definire in anticipo delle solide specifiche su ciò che il programma dovrebbe fare e creare precisi unit test. È necessario prevedere ulteriore lavoro per effettuare gli unit test  di tutte le classi
  • Errori di arrotondamento: poiché si utilizza variabili a virgola mobile per memorizzare tutti i dati finanziari, si verificheranno errori nell’arrotondamento. Il modo per aggirare questo è usare il tipo Decimal di Python. Le implementazioni successive utilizzeranno il valore Decimal.
  • Slippage – Lo slippage è la differenza tra il prezzo che l’oggetto Strategy ha definito quando ha deciso di acquistare o vendere e il prezzo effettivo raggiunto quando il broker esegue un ordine. Data la natura multi-threaded del programma, è estremamente probabile che lo slippage sia una delle cause delle differenze tra il saldo locale e il saldo del conto OANDA.

Studierò questi problemi mentre continuo a lavorare sul sistema forex. Nella prossimo articolo della serie vedremo i miei progressi.

Prossimi Passi

Negli articoli successivi discuteremo i seguenti miglioramenti:

  • Saldi contabili differenti – Il primo compito è determinare perché i saldi contabili differiscono tra OANDA e questa implementazione locale. 
  • Strategie reali: di recente ho letto alcuni articoli su come applicare l’apprendimento automatico ai mercati forex. Convertire alcune di questi teorie in strategie effettive di cui possiamo il bakctest sarebbe interessante (e divertente!).
  • Valute multiple – Aggiunta di più coppie di valute e valute di base alternative.
  • Costi di transazione – Gestione realistica dei costi di transazione, oltre che lo spread denaro-lettera. Ciò includerà una migliore modellazione dello slippage e un impatto sul mercato.

Ci sono anche molti altri miglioramenti da apportare. Questo progetto migliorerà continuamente e spero che possa aiutarti nel tuo trading automatico

 

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: