Gestione delle Posizioni nel Trading Quantitativo con DataTrader

In questo articolo presentiamo i primi risultati nell’implementazione dell’infrastruttura DataTrader per il trading quantitativo avanzato. In particolare, spieghiamo come gestiamo le posizioni nel framework DataTrader, seguendo le linee guida pubblicate nel precedente articolo.

Introduzione

Per costruire questo framework riutilizziamo e miglioriamo quanto più possibile il codice base di TQTradingSystem e TQForex. Così evitiamo di duplicare sforzi e lavoro. Dopo vari tentativi di usare gran parte del codice esistente, decidiamo di ripartire da zero. Rivediamo la gestione e il dimensionamento delle posizioni, la gestione del rischio e la costruzione del portafoglio.

Modifichiamo in particolare il design di DTForex e DataBacktest per integrare il corretto dimensionamento della posizione e dei livelli di rischio. Le versioni precedenti non includevano questi aspetti. Questi due componenti offrono strumenti solidi per costruire portafogli professionali, non semplici simulazioni vettoriali da backtest.

In questa fase non inseriamo logiche specifiche per la gestione del rischio o del dimensionamento della posizione. Ogni trader ha esigenze altamente personalizzate. Inseriamo però alcuni esempi per scenari semplici, come il criterio di Kelly o il rischio basato sulla volatilità. Non sviluppiamo invece logiche complesse per il rischio o le posizioni. Così possiamo usare versioni standard dei componenti, ma anche sostituirli con logiche personalizzate. Non imponiamo alcuna metodologia fissa per la gestione del rischio.

Abbiamo codificato le basi per la gestione del portafoglio. Il nostro strumento di backtest e trading, chiamato DataTrader, è ancora lontano dalla produzione. In questa fase lavoriamo in uno stato “pre-alfa”! Anche se siamo agli inizi, dedichiamo grande impegno agli unit test delle prime componenti e ai valori del broker esterno. Siamo abbastanza fiduciosi nella gestione delle posizioni. Quando emergono nuovi casi limite, aggiungiamo nuovi unit test per migliorare la robustezza.

Il progetto è ora disponibile (in uno stato estremamente precoce!) su github.com/tradingquant-it/DataTrader con licenza MIT open source. In questa fase manca ancora la documentazione. Inseriamo comunque il link per chi desidera esplorare il codice.

Progettazione dei componenti

Nel precedente articolo di questa serie sul Trading Quantitativo Avanzato abbiamo descritto la roadmap per sviluppare l’infrastruttura software. In questo articolo approfondiamo uno degli aspetti più importanti del sistema, cioè la componente Position, utile per gestire le posizioni. Questa componente rappresenta la base del Portfolio e in seguito del PortfolioHandler.

Quando progettiamo un sistema di questo tipo, suddividiamo i comportamenti in diversi sottomoduli separati. Questo approccio ci aiuta a ridurre l’accoppiamento tra i componenti e a rendere i test più semplici. Infatti, possiamo testare ogni singolo modulo in modo indipendente.

Abbiamo scelto un design specifico per l’infrastruttura di gestione del portafoglio, che include i seguenti componenti:

  • Position – Questa classe gestisce tutti i dati relativi a una posizione aperta su un asset. In pratica, monitoriamo profitti e perdite (PnL) realizzati e non realizzati. Calcoliamo la media delle gambe multiple della transazione, considerando anche i costi di transazione.
  • Portfolio – La classe Portfolio contiene l’elenco delle posizioni. Include anche il saldo del conto, l’equity e il PnL complessivo.
  • PositionSizer – Con questa classe forniamo al PortfolioHandler indicazioni su come dimensionare le posizioni, una volta ricevuto un segnale dalla strategia. Ad esempio, possiamo implementare un criterio basato sul Kelly.
  • RiskManager – Usiamo il RiskManager all’interno del PortfolioHandler per approvare, modificare o rifiutare le operazioni proposte dal PositionSizer. Valutiamo sempre la composizione attuale del portafoglio e il rischio esterno, come correlazioni o volatilità.
  • PortfolioHandler – La classe PortfolioHandler gestisce il portafoglio corrente. Comunichiamo con RiskManager e PositionSizer, e inviamo gli ordini da eseguire al gestore di esecuzione.

Questa organizzazione dei componenti differisce in parte dal funzionamento del sistema di backtest con TQForex. In quel caso, l’oggetto Portfolio svolge il ruolo della vecchia classe PortfolioHandler. In questo nuovo progetto separiamo le responsabilità in due classi distinte. Così possiamo gestire meglio il rischio e il dimensionamento delle posizioni grazie alle classi RiskManager e PositionSizer.

Ora concentriamo la nostra attenzione sulla classe Position. Nei prossimi articoli analizzeremo Portfolio, PortfolioHandler e le componenti per dimensionare rischio e posizioni.

La gestione delle posizioni

Creiamo la classe Position per implementare la gestione delle posizioni. Questa classe è molto simile all’omonima classe nel progetto DTForex,. 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

				
			

Calcolo dei profitti e perdite 

Per una corretta gestione delle posizione 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.

Inizializzazione di una posizione 

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)
				
			

Utilizziamo questo metodo per eseguire i calcoli iniziali quando apriamo una nuova posizione. Per le azioni “BOT” (acquisti), incrementiamo il numero di acquisti.
Calcoliamo i valori medi e totali acquistati, insieme al prezzo medio della posizione. Otteniamo il costo base moltiplicando la quantità corrente per il prezzo medio pagato.
Questo equivale al “prezzo totale pagato finora”. Inoltre, calcoliamo anche la quantità netta, il totale netto e la commissione netta.

Attualizzazione al valore di mercato

Un elemento chiave nella gestione delle posizioni consiste nell’attualizzare il valore al prezzo corrente dell’asset. Applichiamo questa logica tramite il metodo update_market_value.
Questo metodo richiede attenzione, perché seguiamo un approccio specifico per calcolare il “valore di mercato”. Non esiste una scelta univoca, dato che manca un valore oggettivo.
Tuttavia, possiamo seguire diversi approcci utili:

  • Punto medio – Adottiamo spesso il punto medio dello spread denaro-lettera. Usiamo i prezzi più alti di offerta e domanda del portafoglio ordini e ne calcoliamo la media.
  • Ultimo prezzo negoziato – Consideriamo l’ultimo prezzo di scambio. Il calcolo può risultare complesso se il titolo è stato scambiato su più livelli di prezzo.
    In quel caso, possiamo usare una media ponderata.
  • Bid o Ask – In base alla direzione della transazione (acquisto o vendita), possiamo scegliere di utilizzare il prezzo bid o ask più alto come riferimento.

Teniamo presente che nessuno di questi prezzi rappresenta esattamente il prezzo finale applicato. Le dinamiche del portafoglio ordini, lo slippage e l’impatto del mercato influenzano il prezzo reale.

Nel nostro metodo scegliamo di calcolare il punto medio per rappresentare il “valore di mercato”. Tuttavia, questo valore entra nei calcoli del rischio e d

				
					
    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
        )
				
			

Gestione delle transazioni

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 .

Come effettuare i test

Nell’implementazione della gestione delle posizioni dobbiamo prevedere una fase di testing del codice implementato al fine di evitare bug ed errori di calcolo che possono causare perdita di denaro. 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
				
			

Unit Test con Python

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')
        )
				
			

Notiamo 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"))
				
			

I codice richiama il metodo transact_shares  quattro volte e infine aggiorna il valore di mercato con il metodo 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

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"))
				
			

Risultati dei test

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

				
					
(datatrader)tradingquant@desktop:~/sites/datatrader/approot/position$ python position_test.py 
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
				
			

Conclusioni

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.

In questo articolo abbiamo descritto la logica implementata in DataTrader per la gestione delle posizioni. Nei seguenti articoli esamineremo le classi Portfolio PortfolioHandler. Entrambi sono già stati codificati e testati.

Il codice completo presentato in questo articolo, basato sul framework di trading quantitativo event-driven DataTrader, è disponibile nel seguente repository GitHub: https://github.com/tradingquant-it/DataTrader.

Gli altri tutorial di questa serie

Benvenuto su TradingQuant!

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

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

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

SCRIVIMI SU TELEGRAM

Per informazioni, suggerimenti, collaborazioni...

Torna in alto