Trong bài viết trước về kiểm tra ngược dựa trên sự kiện, chúng tôi đã xem xét cách xây dựng một hệ thống phân cấp lớp Chiến lược. Chiến lược, như được định nghĩa ở đây, được sử dụng để tạo ra các tín hiệu, được sử dụng bởi một đối tượng danh mục đầu tư để đưa ra quyết định về việc có nên gửi lệnh hay không. Như trước đây, nó là tự nhiên để tạo ra một lớp cơ sở trừu tượng danh mục đầu tư (ABC) mà tất cả các lớp con tiếp theo được thừa hưởng.
Bài viết này mô tả một đối tượng NaivePortfolio theo dõi các vị trí trong một danh mục đầu tư và tạo ra các đơn đặt hàng của một lượng cổ phiếu cố định dựa trên tín hiệu.
Hệ thống quản lý đơn đặt hàng danh mục đầu tư có lẽ là thành phần phức tạp nhất của một backtester dựa trên sự kiện. Vai trò của nó là theo dõi tất cả các vị trí thị trường hiện tại cũng như giá trị thị trường của các vị trí (được gọi là
Ngoài việc quản lý các vị trí và cổ phần, danh mục đầu tư cũng phải nhận thức được các yếu tố rủi ro và các kỹ thuật kích thước vị trí để tối ưu hóa các lệnh được gửi đến môi giới hoặc các hình thức tiếp cận thị trường khác.
Tiếp tục theo hệ thống phân cấp lớp Event, một đối tượng Portfolio phải có khả năng xử lý các đối tượng SignalEvent, tạo các đối tượng OrderEvent và giải thích các đối tượng FillEvent để cập nhật vị trí.
Chúng tôi tạo một tệp mớiportfolio.pyvà nhập các thư viện cần thiết. Chúng giống như hầu hết các thực hiện lớp cơ sở trừu tượng khác. Chúng tôi cần nhập hàm sàn từ thư viện toán học để tạo ra kích thước thứ tự có giá trị nguyên. Chúng tôi cũng cần các đối tượng FillEvent và OrderEvent vì Portfolio xử lý cả hai.
# portfolio.py
import datetime
import numpy as np
import pandas as pd
import Queue
từ abc nhập khẩu ABCMeta, abstractmethod từ sàn nhập toán
từ nhập sự kiện FillEvent, OrderEvent Như trước đây, chúng ta tạo ra một ABC cho Portfolio và có hai phương thức ảo tinh khiết update_signal và update_fill.
# 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()")
Chủ đề chính của bài viết này là lớp NaivePortfolio. Nó được thiết kế để xử lý kích thước vị trí và cổ phần hiện tại, nhưng sẽ thực hiện lệnh giao dịch theo cách "ngốc" bằng cách chỉ đơn giản gửi trực tiếp đến môi giới với kích thước số lượng cố định được xác định trước, bất kể số tiền mặt được nắm giữ.
NaivePortfolio đòi hỏi một giá trị vốn ban đầu, mà tôi đã thiết lập mặc định là 100.000 USD. Nó cũng đòi hỏi một thời gian bắt đầu.
Các danh mục đầu tư chứa các thành viên all_positions và current_positions. Thành viên đầu tiên lưu trữ danh sách tất cả các vị trí trước đó được ghi lại tại thời điểm đánh dấu thời gian của một sự kiện dữ liệu thị trường. Một vị trí chỉ đơn giản là số lượng của tài sản. Các vị trí âm có nghĩa là tài sản đã được bán ngắn. Thành viên sau lưu trữ một từ điển chứa các vị trí hiện tại cho bản cập nhật thanh thị trường cuối cùng.
Ngoài các thành viên vị trí, danh mục đầu tư lưu trữ cổ phần, mô tả giá trị thị trường hiện tại của các vị trí nắm giữ.
# 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()
Phương pháp sau, construct_all_positions, chỉ đơn giản tạo một từ điển cho mỗi ký hiệu, đặt giá trị thành 0 cho mỗi ký hiệu và sau đó thêm một khóa thời gian ngày, cuối cùng thêm nó vào danh sách.
# 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]
Phương pháp construct_all_holdings tương tự như trên nhưng thêm các khóa bổ sung cho tiền mặt, hoa hồng và tổng số, tương ứng đại diện cho số tiền mặt dư thừa trong tài khoản sau khi mua hàng, hoa hồng tích lũy và tổng vốn tài khoản bao gồm tiền mặt và bất kỳ vị trí mở nào. Các vị trí ngắn được coi là âm. Tiền mặt bắt đầu và tổng vốn tài khoản đều được đặt theo vốn ban đầu:
# 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]
Phương pháp sau, construct_current_holdings gần như giống với phương pháp trên ngoại trừ việc nó không gói từ điển trong một danh sách:
# 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
Trong một kịch bản giao dịch trực tiếp, thông tin này có thể được tải xuống và phân tích trực tiếp từ môi giới, nhưng để thực hiện backtesting, cần phải tính toán các giá trị này theo cách thủ công.
Thật không may, không có thứ như
Phương pháp update_timeindex xử lý việc theo dõi các cổ phiếu mới. Đầu tiên nó lấy giá mới nhất từ trình xử lý dữ liệu thị trường và tạo ra một từ điển biểu tượng mới để đại diện cho các vị trí hiện tại, bằng cách đặt các vị trí
# 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)
Phương pháp update_positions_from_fill xác định xem FillEvent là mua hay bán và sau đó cập nhật từ điển current_positions phù hợp bằng cách thêm/từ trừ số lượng cổ phần chính xác:
# 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 tương tự như phương pháp trên nhưng thay vào đó cập nhật giá trị nắm giữ. Để mô phỏng chi phí của một FillEvent, phương pháp sau không sử dụng chi phí liên quan từ FillEvent. Tại sao vậy? Nói đơn giản, trong môi trường backtesting chi phí chứa thực sự không được biết và do đó phải được ước tính. Do đó chi phí chứa được đặt thành
Một khi chi phí lấp đầy được biết, các cổ phần hiện tại, tiền mặt và tổng giá trị đều có thể được cập nhật.
# 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)
Phương pháp update_fill ảo thuần túy từ Portfolio ABC được thực hiện ở đây. Nó chỉ đơn giản là thực hiện hai phương thức trước đó, update_positions_from_fill và update_holdings_from_fill, đã được thảo luận ở trên:
# 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)
Trong khi đối tượng Portfolio phải xử lý FillEvents, nó cũng phải chăm sóc việc tạo OrderEvents khi nhận được một hoặc nhiều SignalEvents. Phương pháp generate_naive_order chỉ đơn giản là lấy một tín hiệu để mua hoặc bán một tài sản và sau đó gửi một lệnh để làm như vậy cho 100 cổ phiếu của tài sản đó. Rõ ràng 100 là một giá trị tùy ý. Trong một thực hiện thực tế, giá trị này sẽ được xác định bởi quản lý rủi ro hoặc lớp phủ kích thước vị trí. Tuy nhiên, đây là một NaivePortfolio và vì vậy nó
Phương pháp xử lý mong muốn, mua ngắn và thoát khỏi một vị trí, dựa trên số lượng hiện tại và biểu tượng cụ thể.
# 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
Phương thức update_signal chỉ đơn giản gọi phương thức trên và thêm thứ tự được tạo vào hàng đợi sự kiện:
# 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)
Phương pháp cuối cùng trong NaivePortfolio là tạo ra đường cong vốn chủ sở hữu. Điều này chỉ đơn giản tạo ra một dòng lợi nhuận, hữu ích cho tính toán hiệu suất và sau đó bình thường hóa đường cong vốn chủ sở hữu để dựa trên tỷ lệ phần trăm. Do đó, kích thước ban đầu của tài khoản bằng 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
Đối tượng danh mục đầu tư là khía cạnh phức tạp nhất của toàn bộ hệ thống backtest dựa trên sự kiện. Việc thực hiện ở đây, mặc dù phức tạp, tương đối cơ bản trong việc xử lý các vị trí. Các phiên bản sau sẽ xem xét quản lý rủi ro và kích thước vị trí, điều này sẽ dẫn đến một ý tưởng thực tế hơn về hiệu suất chiến lược.
Trong bài viết tiếp theo chúng ta sẽ xem xét phần cuối cùng của backtester điều khiển bởi sự kiện, cụ thể là đối tượng ExecutionHandler, được sử dụng để lấy các đối tượng OrderEvent và tạo các đối tượng FillEvent từ chúng.