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 diPosition
, 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 daPortfolioHandler
per verificare, modificare o porre il veto a qualsiasi ipotetico trade che passa dalPositionSizer
, 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 delPortfolio
, dell’interazione conRiskManager
ePositionSizer
nonché dell’invio degli ordini che devono essere eseguiti da unExecutionHandler
. - 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 includonoTickEvent
,OrderEvent
,SignalEvent
eFillEvent
. - Strategy – La classe
Strategy
gestisce la logica di generazione dei segnali di trading in base alle informazioni sui prezzi. Invia questi segnali alPortfolioHandler
. - ExecutionHandler : La classe
ExecutionHandler
legge gliOrderEvent
e produceFillEvent
, 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 unPriceHandler
eExecutionHandler
, 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 PositionSizer
e RiskManager
.
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 Portfolio
e 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 RiskManager
e PositionSizer
, 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.py
e position_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
e _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
e _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 Position
e Portfolio
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
e Portfolio
dobbiamo approfondire il PortfolioHandler
. Questa è la classe che interagisce con PositionSizer
e RiskManager
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.