Dalam artikel sebelumnya tentang backtesting event-driven, kami membahas bagaimana membangun hierarki kelas Strategi. Strategi, sebagaimana didefinisikan di sini, digunakan untuk menghasilkan sinyal, yang digunakan oleh objek portofolio untuk membuat keputusan apakah akan mengirim pesanan. Seperti sebelumnya, wajar untuk membuat kelas dasar abstrak portofolio (ABC) yang semua subkelas berikutnya mewarisi.
Artikel ini menjelaskan objek NaivePortfolio yang melacak posisi dalam portofolio dan menghasilkan pesanan jumlah saham tetap berdasarkan sinyal. Objek portofolio selanjutnya akan mencakup alat manajemen risiko yang lebih canggih dan akan menjadi subjek artikel selanjutnya.
Sistem manajemen pesanan portofolio mungkin merupakan komponen yang paling kompleks dari backtester event-driven. Perannya adalah untuk melacak semua posisi pasar saat ini serta nilai pasar posisi (dikenal sebagai
Selain manajemen posisi dan kepemilikan, portofolio juga harus menyadari faktor risiko dan teknik ukuran posisi untuk mengoptimalkan pesanan yang dikirim ke broker atau bentuk akses pasar lainnya.
Berlanjut dalam hirarki kelas Event, objek Portfolio harus dapat menangani objek SignalEvent, menghasilkan objek OrderEvent dan menafsirkan objek FillEvent untuk memperbarui posisi.
Kami membuat file baruportfolio.pydan mengimpor perpustakaan yang diperlukan. ini sama dengan sebagian besar implementasi kelas dasar abstrak lainnya. kita perlu mengimpor fungsi lantai dari perpustakaan matematika untuk menghasilkan ukuran urutan dengan nilai bulat. kita juga membutuhkan objek FillEvent dan OrderEvent karena Portfolio menangani keduanya.
# portfolio.py
import datetime
import numpy as np
import pandas as pd
import Queue
dari abc import ABCMeta, metode abstrak dari lantai impor matematika
dari event import FillEvent, OrderEvent Seperti sebelumnya kita membuat ABC untuk Portfolio dan memiliki dua metode virtual murni update_signal dan update_fill. Yang pertama menangani sinyal perdagangan baru yang diambil dari antrian acara dan yang terakhir menangani mengisi yang diterima dari objek pengendali eksekusi.
# portfolio.py
class Portfolio(object):
"""
The Portfolio class handles the positions and market
value of all instruments at a resolution of a "bar",
i.e. secondly, minutely, 5-min, 30-min, 60 min or EOD.
"""
__metaclass__ = ABCMeta
@abstractmethod
def update_signal(self, event):
"""
Acts on a SignalEvent to generate new orders
based on the portfolio logic.
"""
raise NotImplementedError("Should implement update_signal()")
@abstractmethod
def update_fill(self, event):
"""
Updates the portfolio current positions and holdings
from a FillEvent.
"""
raise NotImplementedError("Should implement update_fill()")
Subyek utama dari artikel ini adalah kelas NaivePortfolio. Ini dirancang untuk menangani ukuran posisi dan kepemilikan saat ini, tetapi akan melakukan pesanan perdagangan dengan cara
The NaivePortfolio membutuhkan nilai modal awal, yang telah saya atur secara default sebesar 100.000 USD.
Portfolio berisi semua_posisi dan anggota_posisi saat ini. Yang pertama menyimpan daftar semua posisi sebelumnya yang tercatat pada saat timestamp dari peristiwa data pasar. Posisi hanyalah jumlah aset. Posisi negatif berarti aset telah diringkas. Anggota terakhir menyimpan kamus yang berisi posisi saat ini untuk pembaruan bar pasar terakhir.
Selain anggota posisi, portofolio menyimpan kepemilikan, yang menggambarkan nilai pasar saat ini dari posisi yang dipegang.
# portfolio.py
class NaivePortfolio(Portfolio):
"""
The NaivePortfolio object is designed to send orders to
a brokerage object with a constant quantity size blindly,
i.e. without any risk management or position sizing. It is
used to test simpler strategies such as BuyAndHoldStrategy.
"""
def __init__(self, bars, events, start_date, initial_capital=100000.0):
"""
Initialises the portfolio with bars and an event queue.
Also includes a starting datetime index and initial capital
(USD unless otherwise stated).
Parameters:
bars - The DataHandler object with current market data.
events - The Event Queue object.
start_date - The start date (bar) of the portfolio.
initial_capital - The starting capital in USD.
"""
self.bars = bars
self.events = events
self.symbol_list = self.bars.symbol_list
self.start_date = start_date
self.initial_capital = initial_capital
self.all_positions = self.construct_all_positions()
self.current_positions = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
self.all_holdings = self.construct_all_holdings()
self.current_holdings = self.construct_current_holdings()
Metode berikut, construct_all_positions, hanya membuat kamus untuk setiap simbol, menetapkan nilai menjadi nol untuk masing-masing dan kemudian menambahkan kunci waktu tanggal, akhirnya menambahkannya ke daftar.
# portfolio.py
def construct_all_positions(self):
"""
Constructs the positions list using the start_date
to determine when the time index will begin.
"""
d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
d['datetime'] = self.start_date
return [d]
Metode construct_all_holdings mirip dengan yang di atas tetapi menambahkan kunci tambahan untuk kas, komisi dan total, yang masing-masing mewakili kas cadangan di akun setelah pembelian, komisi kumulatif yang terkumpul dan total ekuitas akun termasuk kas dan posisi terbuka. Posisi pendek diperlakukan sebagai negatif.
# portfolio.py
def construct_all_holdings(self):
"""
Constructs the holdings list using the start_date
to determine when the time index will begin.
"""
d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
d['datetime'] = self.start_date
d['cash'] = self.initial_capital
d['commission'] = 0.0
d['total'] = self.initial_capital
return [d]
Metode berikut, construct_current_holdings hampir sama dengan metode di atas kecuali bahwa tidak membungkus kamus dalam daftar:
# portfolio.py
def construct_current_holdings(self):
"""
This constructs the dictionary which will hold the instantaneous
value of the portfolio across all symbols.
"""
d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
d['cash'] = self.initial_capital
d['commission'] = 0.0
d['total'] = self.initial_capital
return d
Pada setiap
Sayangnya tidak ada hal seperti
Metode update_timeindex menangani pelacakan kepemilikan baru. Pertama-tama memperoleh harga terbaru dari pengendali data pasar dan membuat kamus simbol baru untuk mewakili posisi saat ini, dengan menetapkan posisi
# portfolio.py
def update_timeindex(self, event):
"""
Adds a new record to the positions matrix for the current
market data bar. This reflects the PREVIOUS bar, i.e. all
current market data at this stage is known (OLHCVI).
Makes use of a MarketEvent from the events queue.
"""
bars = {}
for sym in self.symbol_list:
bars[sym] = self.bars.get_latest_bars(sym, N=1)
# Update positions
dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
dp['datetime'] = bars[self.symbol_list[0]][0][1]
for s in self.symbol_list:
dp[s] = self.current_positions[s]
# Append the current positions
self.all_positions.append(dp)
# Update holdings
dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
dh['datetime'] = bars[self.symbol_list[0]][0][1]
dh['cash'] = self.current_holdings['cash']
dh['commission'] = self.current_holdings['commission']
dh['total'] = self.current_holdings['cash']
for s in self.symbol_list:
# Approximation to the real value
market_value = self.current_positions[s] * bars[s][0][5]
dh[s] = market_value
dh['total'] += market_value
# Append the current holdings
self.all_holdings.append(dh)
Metode update_positions_from_fill menentukan apakah FillEvent adalah Buy atau Sell dan kemudian memperbarui kamus current_positions dengan menambahkan/mengurangi jumlah saham yang benar:
# portfolio.py
def update_positions_from_fill(self, fill):
"""
Takes a FilltEvent object and updates the position matrix
to reflect the new position.
Parameters:
fill - The FillEvent object to update the positions with.
"""
# Check whether the fill is a buy or sell
fill_dir = 0
if fill.direction == 'BUY':
fill_dir = 1
if fill.direction == 'SELL':
fill_dir = -1
# Update positions list with new quantities
self.current_positions[fill.symbol] += fill_dir*fill.quantity
Update_holdings_from_fill adalah metode yang sama dengan metode di atas tetapi memperbarui nilai kepemilikan sebagai gantinya. Untuk mensimulasikan biaya pengisian, metode berikut tidak menggunakan biaya yang terkait dari FillEvent. Mengapa ini? Sederhananya, dalam lingkungan backtesting biaya pengisian sebenarnya tidak diketahui dan karenanya harus diperkirakan. Dengan demikian biaya pengisian ditetapkan menjadi
Setelah biaya pengisian diketahui, saham saat ini, uang tunai dan nilai total dapat diperbarui.
# portfolio.py
def update_holdings_from_fill(self, fill):
"""
Takes a FillEvent object and updates the holdings matrix
to reflect the holdings value.
Parameters:
fill - The FillEvent object to update the holdings with.
"""
# Check whether the fill is a buy or sell
fill_dir = 0
if fill.direction == 'BUY':
fill_dir = 1
if fill.direction == 'SELL':
fill_dir = -1
# Update holdings list with new quantities
fill_cost = self.bars.get_latest_bars(fill.symbol)[0][5] # Close price
cost = fill_dir * fill_cost * fill.quantity
self.current_holdings[fill.symbol] += cost
self.current_holdings['commission'] += fill.commission
self.current_holdings['cash'] -= (cost + fill.commission)
self.current_holdings['total'] -= (cost + fill.commission)
Metode update_fill virtual murni dari Portfolio ABC diimplementasikan di sini. Ini hanya mengeksekusi dua metode sebelumnya, update_positions_from_fill dan update_holdings_from_fill yang telah dibahas di atas:
# portfolio.py
def update_fill(self, event):
"""
Updates the portfolio current positions and holdings
from a FillEvent.
"""
if event.type == 'FILL':
self.update_positions_from_fill(event)
self.update_holdings_from_fill(event)
Sementara objek Portfolio harus menangani FillEvents, ia juga harus mengurus untuk menghasilkan OrderEvents pada saat penerimaan satu atau lebih SignalEvents. Metode generate_naive_order hanya mengambil sinyal untuk panjang atau pendek aset dan kemudian mengirim order untuk melakukannya untuk 100 saham aset tersebut. Jelas 100 adalah nilai sewenang-wenang. Dalam implementasi yang realistis nilai ini akan ditentukan oleh manajemen risiko atau overlay ukuran posisi. Namun, ini adalah NaivePortfolio dan jadi
Metode ini menangani keinginan, shorting dan keluar dari posisi, berdasarkan kuantitas saat ini dan simbol tertentu.
# portfolio.py
def generate_naive_order(self, signal):
"""
Simply transacts an OrderEvent object as a constant quantity
sizing of the signal object, without risk management or
position sizing considerations.
Parameters:
signal - The SignalEvent signal information.
"""
order = None
symbol = signal.symbol
direction = signal.signal_type
strength = signal.strength
mkt_quantity = floor(100 * strength)
cur_quantity = self.current_positions[symbol]
order_type = 'MKT'
if direction == 'LONG' and cur_quantity == 0:
order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
if direction == 'SHORT' and cur_quantity == 0:
order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')
if direction == 'EXIT' and cur_quantity > 0:
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
if direction == 'EXIT' and cur_quantity < 0:
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
return order
Metode update_signal hanya memanggil metode di atas dan menambahkan urutan yang dihasilkan ke antrian acara:
# portfolio.py
def update_signal(self, event):
"""
Acts on a SignalEvent to generate new orders
based on the portfolio logic.
"""
if event.type == 'SIGNAL':
order_event = self.generate_naive_order(event)
self.events.put(order_event)
Metode terakhir dalam NaivePortfolio adalah generasi kurva ekuitas. Ini hanya menciptakan aliran pengembalian, berguna untuk perhitungan kinerja dan kemudian menormalkan kurva ekuitas untuk menjadi persentase berbasis. Dengan demikian ukuran awal akun sama dengan 1.0:
# portfolio.py
def create_equity_curve_dataframe(self):
"""
Creates a pandas DataFrame from the all_holdings
list of dictionaries.
"""
curve = pd.DataFrame(self.all_holdings)
curve.set_index('datetime', inplace=True)
curve['returns'] = curve['total'].pct_change()
curve['equity_curve'] = (1.0+curve['returns']).cumprod()
self.equity_curve = curve
Objek portofolio adalah aspek yang paling kompleks dari seluruh sistem backtest event-driven. Implementasi di sini, meskipun rumit, relatif sederhana dalam penanganan posisi. Versi berikutnya akan mempertimbangkan manajemen risiko dan ukuran posisi, yang akan mengarah pada ide yang jauh lebih realistis dari kinerja strategi.
Dalam artikel berikutnya kita akan mempertimbangkan bagian terakhir dari backtester event-driven, yaitu objek ExecutionHandler, yang digunakan untuk mengambil objek OrderEvent dan membuat objek FillEvent dari mereka.