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 Portfolio
e 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 RiskManager
e PositionSizer
.
Rivolgeremo ora la nostra attenzione alla classe Position
. Negli articoli successivi vedremo le classi Portfolio
, PortfolioHandler
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
e 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)datatrading@desktop:~/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
e 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