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.

Tradingview: Indicatore di forza di una valuta

trading-algoritmico-tradingview-Currency-Strength-Indicator

Quando effettuiamo un’operazione nel mercato Forex, stiamo applicando la nostra analisi contemporaneamente su due economie. In altre parole, stiamo scommettendo che un’economia è, o sarà, più forte dell’altra. Prima di effettuare un’operazione sarebbe utile validare la nostra analisi osservando la forza complessiva di entrambe le valute nella coppia che stiamo per negoziare. Dopo tutto, se una valuta nella coppia è debole rispetto a tutte le principali valute, è probabile che sia debole nella coppia che si vuole negoziare. Con questo approccio, un indicatore della forza della valuta potrebbe essere uno strumento utile per la nostra analisi. Per il dollaro USA potrebbe non essere necessario dato che possiamo usare l’indice USDOLLAR per misurare la forza dell’USD, ma come fare per le altre valute?

Indicatore di forza della valuta

L’indicatore della forza valutaria prende in considerazione fino a 4 coppie di valute e calcola la variazione percentuale media rispetto al timeframe giornaliero. Quindi traccerà il grafico di tutte le coppie insieme a una linea aggregata che indica la forza complessiva. È disponibile un’opzione per contrassegnare una valuta come controvaluta in modo che la variazione percentuale sia invertita. 

Si consideri il seguente esempio. Si vuole conoscere la forza della sterlina britannica. Nella maggior parte delle coppie la sterlina è la valuta di base tranne che contro l’euro. Ciò significa che quando l’EURGBP sale, la sterlina si sta indebolendo e viceversa. Pertanto, per ottenere una lettura accurata, bisogna di contrassegnare la sterlina come controvaluta e invertire il risultato della variazione percentuale.

Codice Pinescript

            //@version=3
study(title="Currency Strength", shorttitle="CUR-STR", precision=4, scale=scale.left)
 
sym1 = input(title='FX Pair 1', type=symbol, defval='OANDA:GBPUSD')
sym1_counter = input(title='Counter Currency?', type=bool, defval=false)
sym2 = input(title='FX Pair 2', type=symbol, defval='OANDA:EURGBP')
sym2_counter = input(title='Counter Currency?', type=bool, defval=true)
sym3 = input(title='FX Pair 3', type=symbol, defval='OANDA:GBPJPY')
sym3_counter = input(title='Counter Currency?', type=bool, defval=false)
sym4 = input(title='FX Pair 4', type=symbol, defval='OANDA:GBPCHF')
sym4_counter = input(title='Counter Currency?', type=bool, defval=false)
 
 
inst1_daily = security(sym1, "D", close[1], lookahead=barmerge.lookahead_on)
inst2_daily = security(sym2, "D", close[1], lookahead=barmerge.lookahead_on)
inst3_daily = security(sym3, "D", close[1], lookahead=barmerge.lookahead_on)
inst4_daily = security(sym4, "D", close[1], lookahead=barmerge.lookahead_on)
 
 
inst1_current = security(sym1, period, close)
inst2_current = security(sym2, period, close)
inst3_current = security(sym3, period, close)
inst4_current = security(sym4, period, close)
 
inst1_change = ((inst1_current - inst1_daily) / inst1_daily) * 100
inst2_change = ((inst2_current - inst2_daily) / inst2_daily) * 100
inst3_change = ((inst3_current - inst3_daily) / inst3_daily) * 100
inst4_change = ((inst4_current - inst4_daily) / inst4_daily) * 100
 
 
inst1_change := sym1_counter == true ? inst1_change * -1 : inst1_change
inst2_change := sym2_counter == true ? inst2_change * -1 : inst2_change
inst3_change := sym3_counter == true ? inst3_change * -1 : inst3_change
inst4_change := sym4_counter == true ? inst4_change * -1 : inst4_change
    
overall_strength = (inst1_change + inst2_change + inst3_change + inst4_change) / 4
 
plot(inst1_change,title='Sym1', color=lime, style=area, transp=80)
plot(inst2_change,title='Sym2', color=blue, style=area, transp=90)
plot(inst3_change,title='Sym3', color=red, style=area, transp=90)
plot(inst4_change,title='Sym4', color=purple, style=area, transp=90)
 
plot(overall_strength,title='Overall Strength', color=black, linewidth=3, style=line)
 
// Test Plots
// ------------------
// plot(inst1_daily)
// plot(inst2_daily)
// plot(inst3_daily)
// plot(inst4_daily)
 
// plot(inst1_current)
// plot(inst2_current)
// plot(inst3_current)
// plot(inst4_current)
        

Breve commento

Se ti stai chiedendo perché ho usato il lookahead e vuoi saperne di più, ho scritto un post che descrive il lookhead in modo più dettagliato: Tradingview: Comprensione di lookahead, dati storici e in tempo reale

Inoltre, l’utilizzo della funzione security() per ottenere dati di un diverso timeframe o un diverso strumento è descritto in modo approfondito in questo articolo: Tradingview: creare un indicatore

Il resto del codice calcola solo la variazione percentuale e la forza complessiva della valuta. Se sei interessato alla matematica alla base del calcolo di una variazione percentuale, posso indicarti questo tutorial.

Prima di andare avanti, penso che valga la pena sottolineare le motivazioni dell’uso dei parametri “style” e “transp” per tutti i grafici tranne che per “forza complessiva”. In questo modo si evita che l’indicatore sia troppo “affollato”. Inoltre, secondo me, la linea della “forza complessiva” è la trama più importante ed ero preoccupato che potesse essere mascherata o di difficile da leggere.

Aggiunta di un’altra valuta

Se si ritiene che 4 coppie di valute non siano sufficienti, si può facilmente aggiungerne altre. Come si può vedere dal codice, abbiamo 6 passaggi fondamentali:

  1. Selezione delle coppie di valute.
  2. Ricavare la chiusura giornaliera per quelle coppie
  3. Ricavare l’ultimo prezzo di chiusura nel timeframe considerato
  4. Calcolare la differenza tra il prezzo di chiusura corrente e la chiusura daily
  5. Mediare le differenze tra loro
  6. Tracciare i grafici dei risultati.

Pertanto per aggiungere un’altra coppia di valute all’indicatore si prevedono i seguenti passi:

  • Innanzitutto, aggiungere i nuovi input per la coppia da considerare. Assicurarsi di creare nuovi nomi di variabili e valori predefiniti. Ad esempio sym5 come segue 
            sym5 = input(title='FX Pair 5', type=symbol, defval='OANDA:GBPAUD')
sym5_counter = input(title='Counter Currency?', type=bool, defval=false)
        
  • Ricavare i dati per il timeframe giornaliero e per il timeframe corrente per la coppia. Anche in questo caso bisogna fare attenzione ad aggiornare i nomi delle variabili e i parametri security()
            inst5_daily = security(sym5, "D", close[1], lookahead=barmerge.lookahead_on)
inst5_current = security(sym5, period, close)
        
  • Calcolare la variazione e invertire il ​​valore se si sta utilizzando una controvaluta. Se si ottiene un errore dove si segnala che inst5_change è già dichiarato, bisogna osservare l’operatore “:= “. Per aggiornare una variabile già dichiarata, è necessario aggiungere i due punti “:” davanti all’operatore di uguale.
            inst5_change = ((inst5_current - inst5_daily) / inst5_daily) * 100
 
inst5_change := if sym5_counter == true
    inst5_change * -1
        
  • Infine si aggiunge la nuova coppia al calcolo della forza complessiva e si traccia il grafico.
            overall_strength = (inst1_change + inst2_change + inst3_change + inst4_change + inst5_change) / 5

plot(inst5_change,title='Sym5', color=yellow, style=area, transp=80)
        

Risultati

Di seguito si riporta il risultato dello script descritto in precedenza

trading-algoritmico-tradingview-Default-Currency-Strength-Indicator

Esempi di utilizzo e suggerimenti

Impostazione di 2 indicatori per confrontare la forza

L’esempio mostra la coppia GBPJPY con un indicatore della forza della valuta GBP e un indicatore della forza della valuta JPY. Entrambe le valute si stanno rafforzando. Tuttavia, lo JPY sta mostrando più forza e di conseguenza GBPJPY è ribassista.

trading-algoritmico-tradingview-JPY-forza-GBP-forza

Identificare le tendenze opposte

Il seguente esempio mostra come tendenze opposte possono accelerare i movimenti dei prezzi. In questo caso, il GBP inizia a diventare più forte, mentre allo stesso tempo il USD inizia a indebolirsi. Ciò provoca un aumento costante dei prezzi seguito da un balzo finale

trading-algoritmico-tradingview-Opposing-Trends

Impostazione per JPY e verifica dell'output

Infine, ecco uno screenshot di come impostare l’indicatore per monitorare la forza dello yen giapponese. In tutti i casi lo JPY è la controvaluta quindi per impostarlo correttamente è necessario aprire la pagina delle impostazioni dell’indicatore e compilare con le coppie JPY da monitorare, come segue. Per salvare le impostazioni bisogna premere sulla freccia in basso accanto al pulsante dei valori predefiniti e selezionare ” Salva come predefinito “.

trading-algoritmico-tradingview-JPY-Setup

Tradingview: Ricalcolo dopo esecuzione di un ordine

trading-algoritmico-tradingview-ricalcolo-dopo-esecuzione

Questo post è stato ispirato da una domanda che ho visto di recente su reddit su / r / Forex / . L’utente chiedeva semplicemente:

What does the “Recalculate After Order Filled flag” actually do?

In un primo momento, ho pensato che un rapido sguardo all’help di Tradingview sarebbe sufficiente per trovare la risposta. In questo caso si scopre che la documentazione non è così chiara:

You can set the strategy to perform additional calculation after an order is filled. For this you need to check off “Recalculate After Order filled” in settings or do it in script itself:

Questa documentazione  è un po’ ambigua e lascia spazio ad alcune domande: Di quale calcolo aggiuntivo stiamo parlando? Devo specificare qualcosa da calcolare? È un calcolo speciale documentato altrove?

Ricalcola dopo l'esecuzione dell'ordine

Prima di dare la mia interpretazione, devo evidenziare che “ricalcola dopo l’esecuzione dell’ordine” è il nome del flag che si vede nella schermata delle impostazioni della strategia. In realtà nel codice è indicato come “calc_on_order_fills ” ed è impostato come argomento nella chiamata alla funzione strategy().

tradingview-ricalcolo-dopo-esecuzione-trading-algoritmico

In questo modo si effettua il il ricalcolo in questione per l’intero script, cioè il tuo script viene “calcolato” per ogni barra dei dati storici disponibili. Quando si attiva il “ricalcola dopo che l’ordine evaso“, si ricalcola nuovamente lo script ogni volta che un ordine sia stato completamente eseguito. Nell’emulatore di broker reso disponibile da Tradingview, questo avviene prima della barra successiva.

 

Può sembrare un dettaglio trascurabile (o può aumentare notevolmente il rischio)

A seconda del contenuto dello script, l’impostazione “ricalcola dopo l’esecuzione dell’ordine” (calc_on_order_fills) potrebbe non avere alcun evidente effetto sul risultato dello script, ad esempio le strategie semplici che non prevedono la “piramidazione”.

Dall’altra parte, può causare un’esposizione eccessiva perchè si inserendo più ordini sulla stessa barra. Con la piramidazione, viene piazzato un secondo ordine sulla stessa barra se le condizioni di ingresso sono ancora soddisfatte dopo il “ricalcolo”. Secondo me questo annulla l’effetto della piramidazione.

Codice Pinescript

Per effettuare i primi test possiamo usare una strategia molto, molto semplice. E’ cosi semplice che ho limitato l’intervallo di backtest a soli 100 giorni in modo che il grafico non venga sovraccaricato da troppi ordini di acquisto e vendita. (si può fare tramite la  parola chiave max_bars_back)

            //@version=4
strategy("calc_on_order_fills testing",  overlay=true, calc_on_order_fills=true, pyramiding=2, max_bars_back=100)

//setup ma
ma = sma(close, 50)

//plotting
plot(ma, linewidth=2)

//Strat
longCondition = (open > ma)
if (longCondition)
    strategy.entry("MA Long Sig", strategy.long)

shortCondition = (open < ma)
if (shortCondition)
    strategy.entry("MA Short Sig", strategy.short)
        

Risultati

Vediamo di seguito i risultati dopo aver eseguito lo script con le impostazioni predefinite calc_on_order_fills = false, pyramiding = 2:

La cosa fondamentale da notare su questa immagine è che ogni entrata / uscita si trova su una barra diversa. 

Di seguito abbiamo un esempio dello stesso script ma con le impostazioni calc_on_order_fills = true, pyramiding = 2:

trading-algoritmico-tradingview-pinescript-Recalc-On

Nel secondo esempio possiamo vedere come 2 ordini di entrata vengono entrambi eseguiti sulla stessa barra. Ciò accade perché l’intero script viene ricalcolato e le condizioni per l’ingresso sono ancora valide.

Penso che vedere le due immagini affiancate renda facile vedere capire l’effetto di questa impostazione. Inoltre la seconda immagine evidenzia i pericoli della piramidazione quando si ricalcola dopo l’esecuzione dell’ordine. Questa impostazione potrebbe essere più utile nei test a termine in cui le condizioni non sono identiche dopo che l’ordine è stato eseguito o quando si vuole fare trading automatico con broker reali su Tradingview.


PS. Se sei nuovo su pinescript assicurati di controllare alcuni dei miei post precedenti su Tradingview:

  1. Tradingview: primo script
  2. Tradingview: crea un indicatore
  3. Tradingview: comprensione dei dati anticipati, storici e in tempo reale

Introduzione a QuantConnect

Il contenuto di questo sito è stato inizialmente focalizzato su due piattaforme. Per essere più specifici, tali piattaforme sono Backtrader e Tradingview. Il motivo è semplicemente che queste si sono rivelate per me le soluzioni migliori quando ho iniziato questo viaggio. Come la maggior parte delle persone, prima di decidere di investire il mio tempo in queste piattaforme, ho dato un’occhiata a diverse altre opzioni. QuantConnect era solo una delle tante altre piattaforme disponibili. Tuttavia, dopo aver trascorso un po’ di tempo a frugare nel sito, in quel momento ho deciso che QuantConnect non faceva per me. Di recente, ho capito che era tempo di dare un’altra occhiata. Dopotutto, la tecnologia si muove a un ritmo rapidissimo ed immaginavo che la piattaforma avrebbe potuto fare molta strada dopo la mia prima visita. Inoltre, dopo aver dedicato molto tempo allo sviluppo nelle altre piattaforme, ora ho una nuova prospettiva e una visione diversa di ciò che è importante per me. Quindi iniziamo un nuovo viaggio come principianti di questa piattaforma. Vediamo se l’erba è davvero più verde da quelli parti.

QuantConnect

Per quelli di voi che non ne hanno mai sentito parlare, QuantConnect è una piattaforma online che consente agli utenti di scrivere, collaborare e persino ottenere finanziamenti per algoritmi di trading. Il codice viene generalmente scritto nel browser e backtestato online utilizzando i dati e la potenza di calcolo di QuantConnects.

Link: https://www.quantconnect.com/

Introduzione

Prima di sviluppare il nostro primo script, fornirò alcune considerazioni iniziali (sia positive che negative) sulle caratteristiche generali della piattaforma. Ciò può aiutare i lettori a valutare rapidamente se continuare o meno questo viaggio. Innanzitutto, parliamo delle cose buone! Ci sono alcuni aspetti di QuantConnect davvero interessanti e verranno evidenziati man mano lungo questa serie di articoli. Ma quello che è veramente unico la grande quantità di dati storici che sono disponibili per gli utenti. È di facile accesso e quindi non c’è bisogno di lunghe, fatiche e costose ricerche per reperire e conservare i dati di cui abbiamo bisogno. Chiunque abbia mai provato, saprà bene che è un dispendio di tempo e di denaro! QuantConnect supporta anche numerosi linguaggi di programmazione (Python, C # e F). Questo è ottimo per una piattaforma online con perimetro semi-murato. Puoi programmare nel linguaggio che conosci, invece di dover imparare un linguaggio di scripting come Pine-Script di Tradingview.

Integrazione con il Broker

Un altro enorme vantaggio è la stretta integrazione con i maggiori broker per i trader retail. Ovviamente, anche Backtrader supporta l’integrazione con i broker, ma QuantConnect ha creato partnership ufficiali. Inoltre, forniscono macchine virtuali per facilitare la distribuzione di un algoritmo e garantire un solido tempo di attività. L’implementazione di un algoritmo live è semplice, basta premere un pulsante nella parte superiore dell’interfaccia utente e selezionare il proprio broker. Non sono necessarie modifiche al codice.
Se sei interessato al live trading, QuantConnect offre diversi tipi di abbonamenti per i server e gli account (il Backtesting è gratuito). Il prezzo per aprire un server di live trading è abbastanza ragionevole, solo $20 al mese. Questo ti dà pieno accesso all’infrastruttura e agli strumenti che impiegherebbero molto tempo per essere sviluppati in altri framework come Backtrader. Inoltre, se sei un cliente di Oanda, in realtà ti sovvenzionano il costo del server, rendendolo GRATUITO!

Una Visione Oggettiva

Se vogliamo dare un giudizio oggettivo, dobbiamo essere critici e onesti riguardo alle carenze della piattaforma (e ce ne sono alcune).

  • Lo sviluppo, il debug e il testing è un processo lungo e lento nel caso si utilizza la piattaforma online. L’esecuzione del codice, ogni volta, può richiedere fino a 30 secondi per compilare, analizzare ed eseguire il codice. L’analisi da sola richiede in media 15 secondi e questo deve essere fatto prima di vedere qualsiasi tipo di errore. Quindi, se stai esplorando o eseguendo il debug e provando cose diverse, preparati a lenti progressi. Ad essere onesti, sembra esserci un’opzione per lo sviluppo locale. Gli utenti possono scaricare il motore “lean” ed eseguirlo localmente. Tuttavia, credo che la maggior parte degli utenti occasionali / al dettaglio non seguirà questa strada.
  • Documentazione, anche se ce n’è molta, personalmente ho trovato difficile trovare quello che mi interessava. I programmatori C# hanno un’eccellente funzione di copilota che fornisce suggerimenti sulla documentazione e fornisce frammenti ci codice delle varie risorse online. Sfortunatamente, il copilota non è disponibile in Python, il mio linguaggio preferito.
  • USA – Centrico: i dati disponibili sono davvero spettacolari ma sono molto focalizzati sui mercati statunitensi. Se si desidera eseguire il backtest delle azioni nel resto del mondo, è comunque necessario procurarsi altri fornitori di dati. Detto questo, i dati del Forex, Crypto e CFD sono disponibili per le persone che non sono interessate alle azioni statunitensi.

Risorse Limitate

Poiché si sta usando le risorse di altre persone, ci sono alcune sensibili limitazioni. Ovviamente, QuantConnect non vuole che un utente blocchi accidentalmente il sistema o prosciughi tutte le risorse. Pertanto, sono state previste le seguenti limitazioni:

  • Limiti di stampa: ogni grafico è limitato a un certo numero di punti. Se si supera il numero massimo di punti, la stampa viene interrotta. Pertanto, se si desidera eseguire un backtest con molti dati, non sarà possibile tracciare completamente tutti i dati. Pertanto, la stampa deve essere eseguita durante periodi di test più brevi per verificare che l’algoritmo funzioni come previsto.
  • Limite di logging giornaliero. Ogni utente può creare solo x Kb nei file di log. Ciò significa che devi essere un po’ attento ai dati che decidi di stampare nei log e chiederti se sono veramente utili.
  • Pacchetti: poiché siamo in esecuzione in un sistema con un perimetro ben limitato, solo una manciata di pacchetti sono ufficialmente disponibili per l’importazione. Fortunatamente, i pacchetti che possiamo importare sono quelli più utilizzati e che probabilmente sono esenziali,  come Panda, Numpy, Sci-kit learn, statsmodels ecc.

Iniziamo

Il prossimo post relativo a QuantConnect riguarderà un’introduzione di base per iniziare a utilizzare Python su QuantConnect. Segue un formato simile (ove possibile) agli articoli introduttivi delle altre piattaforme descritte su DataTrading.info

Creare un Indicatore con Tradingview

Questo articolo fa anche parte della serie introduttiva su Tradingview. In questo tutorial, seguiremo passaggi simili a quelli descritti nell’articolo Tradingview: il primo Script, con la differenza che in questo caso si vuole creare un indicatore invece che una strategia.

Se non sai come aprire l’editor di pine-script, ti suggerisco di leggere l’articolo menzionato sopra. Ti guiderà attraverso l’apertura di un grafico e l’accesso all’editor.

L'indicatore

Prima di iniziare a sporcarci le mani, è opportuno descrivere la logica dell’indicatore che si vuole creare. In questo articolo si descrive come creare un indicatore RSI con un doppio intervallo di tempo. L’indicatore traccia i valori RSI relativi a 2 timeframe. La prima linea tracciata corrisponde al timeframe corrente del grafico, mentre la seconda è relativa ad un timeframe selezionato dall’utente. L’idea base consiste nel ritenere probabile un’inversione del trend (o almeno un ritorno verso la media) di uno strumento quando il prezzo è ipercomprato / ipervenduto contemporaneamente su due diversi timeframe. Questo indicatore è adatto in condizioni di mercato laterale.

Gli argomenti di base trattati in questo tutorial sono:

  • Comprensione della funzione di studio
  • Aggiunta degli ingressi dell’indicatore
  • Importazione dei dati relativi ad un diverso timeframe
  • Chiamata ad una funzione RSI
  • Mostrare graficamente le linee orizzontali e le linee RSI

Ok, vediamo in dettaglio l’implementazione di questo indicatore

Il Codice

Di seguito puoi trovare il codice completo. A seguire si descrive in dettaglio la logica di ogni sezione di questo codice.
            //@version=3
study("Dual Strength RSI", "DS-RSI")
 
// Inputs
otf = input(defval="D", title="Second Momentum Timeframe", type=resolution)
otf_period = input(defval=14, title="Look Back Period (2nd Timeframe)", type=integer)
ctf_period = input(defval=14, title="Look Back Period (Chart Timeframe)", type=integer)
ob = input(defval=70, title="Overbought Area", type=integer)
os = input(defval=30, title="Oversold Area", type=integer)
 
//Get the data
otf_rsi = security(tickerid, otf, rsi(close, otf_period))
 
//Calculate RSI Values
ctf_rsi = rsi(close, ctf_period)
 
//Plot
hline(ob, title='Overbought Line', color=black, linestyle=dashed, linewidth=1)
hline(os, title='Oversold Line', color=black, linestyle=dashed, linewidth=1)
plot(otf_rsi, title='OTF RSI', color=blue, style=line, linewidth=3)
plot(ctf_rsi, title='CTF RSI', color=green, style=line, linewidth=3)
        

Spiegazione del Codice

Iniziato dall’intestazione del codice con la funzione study().
            study("Dual Strength RSI", "DS-RSI")
        

La funzione study deve essere inclusa in ogni script. Tradingview classifica questo tipo di funzione come una “funzione di annotazione“. Come suggerisce il nome, queste funzioni annotano le informazioni che appaiono sul grafico. Ad esempio, assegnando un titolo all’indicatore presente nel grafico. Nell’esempio precedente si assegna all’indicatore un titolo completo e una notazione breve.

 

Inputs

            otf = input(defval="D", title="Second Momentum Timeframe", type=resolution)
otf_period = input(defval=14, title="Look Back Period (2nd Timeframe)", type=integer)
ctf_period = input(defval=14, title="Look Back Period (Chart Timeframe)", type=integer)
ob = input(defval=80, title="Overbought Area", type=integer)
os = input(defval=20, title="Oversold Area", type=integer)
        

A seguire si ha la sezione degli input. Gli input forniscono i parametri dell’indicatore che possono essere modificati dopo aver aggiunto l’indicatore al grafico. Offrono agli altri utenti la possibilità di modificare le impostazioni dell’indicatore nel caso non condividino le impostazioni di default. Inoltre, gli utenti possono salvare la propria configurazione per qualsiasi indicatore. Non c’è bisogno di codificare queste logiche perchè sono integrate nelle funzionalità stardand della piattaforma.

  • defval = valore predefinito per il parametro
  • title = Il testo che appare nella casella di input
  • type = il tipo di valore che deve essere inserito. Si noti che un parametro è di tipo “resolution“. Questa è una variabile speciale che fornisce l’elenco di tutti i timeframe supportati.

Di seguito si mostra un esempio della rappresentazione grafica di questi input.

 

Import di altri dati

            //Get the data
otf_rsi = security(tickerid, otf, rsi(close, otf_period))
        
Vediamo ora l’importazione dei dati. Questa linea di codice usa una funzione chiamata security() per importare i dati all’interno dello script. I dati importati potrebbero provenire da un diverso timeframe dello stesso strumento, dallo stesso timeframe ma di uno strumento diverso oppure da un timeframe e uno strumento completamente diversi da quelli rappresentati nel grafico principale.
  • tickerid = È un’altra variabile speciale che si riferisce allo strumento nel grafico principale. Questo ci consente di ottenere facilmente un altro timeframe dello stesso strumento. Ancora più importante, dato che non si specifica il nome di uno strumento (ad esempio LON: VOD), questo indicatore può essere applicato a qualsiasi strumento senza bisogno di modificare il codice.
  • otf = È la variabile restituita dalla funzione di input che abbiamo aggiunto sopra.
  • rsi (close, otf_period) = Restituisce una serie RSI per il timeframe selezionato. In questo caso l’RSI è calcolato  a partire dai valori close di ogni barra di quel timeframe.
Plotting
            //Plot
hline(ob, title='Overbought Line', color=black, linestyle=dashed, linewidth=1)
hline(os, title='Oversold Line', color=black, linestyle=dashed, linewidth=1)
plot(otf_rsi, title='OTF RSI', color=blue, style=line, linewidth=3)
plot(ctf_rsi, title='CTF RSI', color=green, style=line, linewidth=3)
        
Infine si tracciano tutte le linee sul grafico. La funzione hline() traccia le linee orizzontali. La funzione plot() traccia i valori della serie RSI. Per concludere penso che gli argomenti delle keyword siano abbastanza autoesplicativi.

Il Risultato

Per vedere il risultato finale del nostro indicatore è sufficiente:

  1. Aprire Tradingview
  2. Scrivere il codice nell’editor di pine-script.
  3. Premere il pulsante “Aggiungi al grafico”
Questo è tutto quello che c’è da fare per creare un indicatore con pine-script di tradingview. Con questo codice di partenza prova a giocare con il setup dei dati importando diversi strumenti, prova un mix di indicatori, gioca con le funzioni di disegno e vedi se riesci a trovare qualche combinazione interessante.

Tradingview: lookahead dei dati realtime e dei dati storici

Per quanto la documentazione di pine-script sia ben fatta, ci sono ancora parti non spiegate sufficientemente bene, oppure è rivolta ad un target di pubblico con competenze molto avanzate. Ho quindi deciso di scrivere questo articolo dopo aver speso un po’ di tempo a studiare le differenze tra i dati “in tempo reale” e i dati storici e come l’utilizzo di un tipo o dell’altro possa modificare i calcoli prodotti da un indicatore. Questo è particolarmente vero quando si inizia ad utilizzare la keyword lookahead. Ne parleremo più avanti …

Nota: se hai appena iniziato ad utilizzare pine-script, ho scritto un’introduzione nell’articolo “Trading View: il primo Script”

Dati Storici vs Dati Real-time

I dati “real-time” di Tradingview non devono essere confusi con i dati in tempo reale forniti dalle borse e dagli exchange. In pine-script i dati in tempo reale sono tutti i dati acquisiti durante la creazione di una candela (anche se i dati stessi sono in ritardo). Al contrario, i dati storici si riferiscono a qualsiasi candela chiusa prima di aggiungere qualsiasi indicatore sul grafico. Sembra logico vero? E’ necessario però fare attenzione a un paio di cose:

  • I calcoli effettuati su dati in tempo reale quasi sempre hanno risultati diversi rispetto a quelli effettuati sui dati storici. Questo può sembrare ovvio, ma negli esempi seguenti si evidenzia come questo può provocare effetti indesiderati.
  • I dati in tempo reale non diventano dati storici una volta completata la candela. In altre parole, tali dati non sono elaborati nello stesso modo in cui sono elaborati i dati storici quando si aggiunge l’indicatore per la prima volta. Anche in questo caso, i seguenti esempi evidenziano queste criticità.

Andiamo avanti con un esempio pratico e creiamo un indicatore.

L'indicatore

L’indicatore che si vuol utilizzare è un semplice indicatore che sovrappone i valori giornalieri di High e Low su un grafico infragiornaliero. Questo potrebbe essere utile quando si cercano breakout sopra / sotto il massimo / minimo del giorno precedente.

Esempio 1 - Livelli Base

Il seguente codice è l’esempio più semplice di questo articolo. L’indicatore raccoglie semplicemente valori alti e bassi per lo stesso strumento usando un timeframe giornaliero. Quindi traccia questi valori nel timeframe corrente.

            //@version=3
study("Barmerge Tests", overlay=true)
 
daily_high = security(tickerid, "D", high)
daily_low = security(tickerid, "D", low)
 
plot(daily_high, style=cross, color=green)
plot(daily_low, style=cross, color=red)
        

Come al solito, puoi copiare ed incollare il codice precedente direttamente nell’editor di pine-script di Tradingview ed aggiungire l’indicatore al tuo grafico.

Risultati dell'esempio 1

Nell’immagine precedente si può notare come i dati storici sono calcolati e tracciati come previsto. Si ha una bella linea retta dall’inizio alla fine di ogni giorno. Tuttavia, guarda cosa succede quando arrivano i dati in tempo reale. Le barre si spostano verso l’alto nel bel mezzo della giornata! Dopo aver esaminato più da vicino le linee generate dopo l’aggiunta dell’indicatore (dati in tempo reale), si può notare come le linee hanno iniziato a far riferimento ai valori High / Low del giorno corrente.

Lookahead

Il Lookahead permette di includere i dati futuri nei calcoli di un indicatore. Se si dichiara "lookahead_on", l’indicatore guarderà avanti per trovare alti e bassi per il resto della giornata. Li userà quindi per tracciare ogni barra infragiornaliera quel giorno, indipendentemente dal fatto che siamo prima o dopo il massimo o il minimo di quel giorno. L’esempio seguente mostra questo in modo più dettagliato.

Example 2 – lookahead attivo

Nell’esempio seguente si utilizza attivamente la funzionalità di lookahead. Se non si fornisce un argomento per la kyword “lookaheda”, il valore di default è OFF (barmerge.lookahead_off).

            //@version=3
study("Barmerge Tests", overlay=true)
 
daily_high = security(tickerid, "D", high, lookahead=barmerge.lookahead_on)
daily_low = security(tickerid, "D", low, lookahead=barmerge.lookahead_on)
 
plot(daily_high, style=cross, color=green)
plot(daily_low, style=cross, color=red)
        


Risultati dell'esempio 2

Nella figura precedente si mostra l’indicatore appena stato aggiunto al grafico. Per i dati storici, il massimo e il minimo della giornata sono tracciati prima che questi avvengano effettivamente durante la giornata!

Non utilizzare mai questo tipo di indicatore per il backtest. Ad esempio, è possibile overfittare i risultati aprendo una posizione short all’inizio della giornata e chiudendola prima della fine della stessa giornata. Si avrebbero ottime prestazioni ma non sarebbero applicabili nel mondo reale.

Per i dati in tempo reale, il calcolo restituisce valori diversi all’interno della stessa giornata, quando il prezzo si sposta oltre il massimo o il minimo corrente. Questo è ovvio perché non possiamo conoscere in anticipo il valore delle candele che si devono ancora formare e che potrebbero avere high più alti e low più bassi.

Di seguito è riportato il grafico dell’indicatore che utilizza i dati in tempo reale dove il prezzo si sposta oltre i dati storici High e Low già noti.

Esempio 3 - Lookahead on e l'index.

In questo caso si può vedere che le linee High e Low rappresentano i valori relativi ai giorni precedenti. Inoltre, i dati in tempo reale forniscono lo stesso valore.
            //@version=3
study("Barmerge Tests", overlay=true)
 
daily_high = security(tickerid, "D", high[1], lookahead=barmerge.lookahead_on)
daily_low = security(tickerid, "D", low[1], lookahead=barmerge.lookahead_on)
 
plot(daily_high, style=cross, color=green)
plot(daily_low, style=cross, color=red)
        


Risultati dell'esempio 3

Qui possiamo vedere che le linee High e Low rappresentanto i valori relativi ai giorni precedenti. Inoltre, i dati in tempo reale forniscono lo stesso valore.

Indicatore di Trendline per BackTrader

Questo è un frammento di codice per un indicatore di Trendline. Come suggerisce il nome, calcola il valore del prezzo in diversi punti di una trendline e, di conseguenza, genera segnali di acquisto e vendita. In alcuni casi mi piace poter adottare un approccio semi-automatico al trading algoritmico. Si potrebbe individuare una bella trendline su Tradingview ma si vuole eseguire operazioni in un ambiente di forward testing, utilizzando Backtrader, quando il prezzo raggiunge la trendline. In quanto tale, considero questo un indicatore a breve termine che può essere utilizzato fino a quando la trendline non viene rotta. L’indicatore di trendline descritto in questo articolo è destinato a essere utilizzato nel framework di Backtrader, ma i calcoli utilizzati per calcolare la trendline possono essere facilmente trasferiti su altri framework.

Background Matematico

Se come me, non sei un mago della matematica, potrebbe essere necessario un po’ di teoria per capire le equazioni contenute nel codice. E’ necessario quindi introdurre quelle che nel mondo della matematica sono conosciute come “equazioni lineari“, al fine di poter sviluppare questo indicatore.

Un tutorial ben scritto e molto utile, anche se in inglese, è il seguente:
mathplanet.com/education/algebra-1/formulating-linear-equations/writing-linear-equations-using-the-slope-intercept-form

Il concetto principale è che per poter calcolare il prezzo della trendline in qualsiasi momento, è necessario calcolare la velocità con cui cambia la pendenza tra due punti temporali. Questi punti temporali sono i due prezzi inseriti come input per l’indicatore. Come identificare questi due punti dipende da te. Come accennato in precedenza, identifico e traccio in modo discrezionale la trendline su Tradingview e quindi prendo nota del punto iniziale e del punto finale da utilizzare con questo indicatore.

L’equazione a cui dobbiamo arrivare è:

\(
\begin{eqnarray}
y = mx + b
\end{eqnarray}
\)

Dove:

y = prezzo

m = pendenza

x = data / ora

b = intersezione con l’asse y

A questo punto, una cosa che ho trovato difficile da spiegare è come la maggior parte dei tutorial presume che tu possa vedere visivamente l’intersezione con l’asse y, cioè il valore di y quando attraversa l’asse y per x=0. In questo modo:

Come sappiamo, i grafici dei prezzi sono leggermente diversi perchè difficilmente si può tornare indietro nel tempo (dove si potrebbe attraversare l’asse y). Non possiamo vedere dove è l’intersezione dell’asse y ma è possibile calcolarla! Per fare questo è necessario capovolgere un po’ l’equazione.

\( \begin{eqnarray} y = mx + b \end{eqnarray} \)
diventa
\( \begin{eqnarray} b = y – m*x \end{eqnarray} \)

Dopo aver risolto l’equazione, si può utilizzare i valori di x e y del punto iniziale e finale della trendline che hai indiviuato. Vedrai come l’ho implementato nel codice seguente.

Le Regole dell'Indicatore

L’indicatore della trendline genera un segnale di acquisto se il prezzo attraversa la trendline verso l’alto, mentre genera un segnale di vendita se il prezzo la attraversa dal basso. Poiché la trendline funge da supporto o resistenza, sarà sempre inferiore al prezzo quando si cercano segnali di acquisto mentre sarà superiore al prezzo quando si cercano segnali di vendita.

Il Codice

            class TrendLine(bt.Indicator):
 
    lines = ('signal','trend')
    params = (
        ('x1', None),
        ('y1', None),
        ('x2', None),
        ('y2', None)
    )
 
    def __init__(self):
        self.p.x1 = datetime.datetime.strptime(self.p.x1, "%Y-%m-%d %H:%M:%S")
        self.p.x2 = datetime.datetime.strptime(self.p.x2, "%Y-%m-%d %H:%M:%S")
        x1_time_stamp = time.mktime(self.p.x1.timetuple())
        x2_time_stamp = time.mktime(self.p.x2.timetuple())
        self.m = self.get_slope(x1_time_stamp,x2_time_stamp,self.p.y1,self.p.y2)
        self.B = self.get_y_intercept(self.m, x1_time_stamp, self.p.y1)
        self.plotlines.trend._plotskip = True
 
    def next(self):
        date = self.data0.datetime.datetime()
        date_timestamp = time.mktime(date.timetuple())
        Y = self.get_y(date_timestamp)
        self.lines.trend[0] = Y
 
        #Check if price has crossed up / down into it.
        if self.data0.high[-1] < Y and self.data0.high[0] > Y:
            self.lines.signal[0] = -1
            return
 
        #Check for cross downs (Into support)
        elif self.data0.low[-1] > Y and self.data0.low[0] < Y:
            self.lines.signal[0] = 1
            return
 
        else:
            self.lines.signal[0] = 0
 
    def get_slope(self, x1,x2,y1,y2):
        m = (y2-y1)/(x2-x1)
        return m
 
    def get_y_intercept(self, m, x1, y1):
        b=y1-m*x1
        return b
 
    def get_y(self,ts):
        Y = self.m * ts + self.B
        return Y
        


Spiegazione del codice

Si generano due lines per questo indicatore. Una line è la trendline e l’altra line è il segnale. Da notare che la trendline non viene tracciata nel grafico perché in backtrader non è possibile tracciare una line sul grafico principale e un’altra sul grafico secondario. Si deve scegliere l’uno o l’altro. Tecnicamente si potrebbe tracciare entrambi sul grafico secondario, ma risulterebbe un po’ strano poiché la trendline può essere qualsiasi numero (dato che il valore dipende dal prezzo dello strumento) mentre il segnale oscilla solamente tra -1 e 1. Una soluzione alternativa è presentata nella sezione dei risultati. Un’altra cosa da notare sono le date, che devono essere convertite in un timestamp in modo che i calcoli possano essere fatti su un numero e non su un oggetto data. Infine, ci si potrebbe domandare perché si sta costruendo la trendline e richiamando get_y() nel metodo next() invece che nel metodo __init__()? Questo perché backtrader genera un IndexError se si prova ad ottenere le informazioni sulla data all’interno di __init__(). Se qualcuno sa come risolvere il problema, lasciatemi un commento! Probabilmente ho trascurato qualcosa all’interno della documentazione.

I metodi chiave

Se si desidera implementare questo codice in un altro framewoek, i tre metodi fondamentali da implementare sono i seguenti:
                def get_slope(self, x1,x2,y1,y2):
        m = (y2-y1)/(x2-x1)
        return m
 
    def get_y_intercept(self, m, x1, y1):
        b=y1-m*x1
        return b
 
    def get_y(self,ts):
        Y = self.m * ts + self.B
        return Y
        

Come in precedenza, è necessario utilizzare i timestamp per i parametri x e richiamare le funzioni nel seguente ordine:

  1. get_slope()
  2. get_y_intercept(), perché ha bisogno del valore di m, risultato di get_slope()
  3. get_y, per ottenere il prezzo finale per qualsiasi data e ora.

Risultati

Come accennato in precedenza, se si esegue questo codice così com’è, l’indicatore non traccia automaticamente nessuna trendline sul grafico di output. Poiché l’indicatore è progettato per produrre segnali di acquisto e vendita, la priorità è data alla stampa della line del segnale.

Come soluzione alternativa, per creare una certa verifica visiva del corretto funzionamento dell’indicatore, è possibile inizializzare un secondo indicatore all’interno della strategia che traccia una media mobile semplice della trendline, con un periodo pari a 1.

            self.sma = bt.indicators.MovingAverageSimple(self.ind1.lines.trend, 
                                             period = 1,
                                             plotmaster = self.data0
                                            )
        
Da notare che, nel codice precedenete, è necessario inserire la riga all’interno del metodo __init__() della strategia, supponendo di aver già inizializzato l’indicatore con il nome ind1 (self.ind1.lines.trend).
Come si può vedere, si ha una trendline che attraversa il grafico e i segnali vengono generati secondo le regole implementate nel codice, come rappresentato nel grafico secondario.