DataTrader - trading algoritmico - Parte 3

DataTrader – Trading Algoritmico Avanzato: la Classe Portfolio

Sommario

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.

Benvenuto su DataTrading!

Sono Gianluca, ingegnere software e data scientist. Sono appassionato di coding, finanza e trading. Leggi la mia storia.

Ho creato DataTrading per aiutare le altre persone ad utilizzare nuovi approcci e nuovi strumenti, ed applicarli correttamente al mondo del trading.

DataTrading vuole essere un punto di ritrovo per scambiare esperienze, opinioni ed idee.

SCRIVIMI SU TELEGRAM

Per informazioni, suggerimenti, collaborazioni...

Torna in alto
Scroll to Top