Portfolio
object must be able to process SignalEvent
objects, generate OrderEvent
objects OrderEvent
and interpret FillEvent
objects to update positions. Thus, it is not surprising that Portfolio objects are usually the most voluminous elements of the backtesting system in terms of lines of code.FillEvent
and OrderEvent
objects FillEvent
also OrderEvent
— a Portfolio
object handles each of them. # portfolio.py import datetime import numpy as np import pandas as pd import Queue from abc import ABCMeta, abstractmethod from math import floor from event import FillEvent, OrderEvent
update_signal
and update_fill
. The first handles new trading signals that are taken from the event queue, and the last one works with information about the executed orders received from the engine handler object. # portfolio.py class Portfolio(object): """ Portfolio : , , 5 , 30 , 60 . """ __metaclass__ = ABCMeta @abstractmethod def update_signal(self, event): """ SignalEvent . """ raise NotImplementedError("Should implement update_signal()") @abstractmethod def update_fill(self, event): """ FillEvent. """ raise NotImplementedError("Should implement update_fill()")
NaivePortfolio
class. It is designed to calculate the size of positions and reserved funds and process trade orders in a simple manner — simply by sending them to a brokerage trading system with a specified number of shares. In the real world, everything is more complicated, but such simplifications help to understand how the portfolio order processing system should function in event-oriented products.NaivePortfolio
requires the amount of initial capital - in the example it is set at $ 100,000. You must also set the day and time to start work.all_positions
and current_positions
. The first item stores a list of all previous positions recorded by the time stamp of a market event. Position is simply the amount of a financial instrument. Negative positions mean that shares are sold short. The second element stores a dictionary containing the current positions for the last update of the bars.all_holdings
element stores a historical list of the cost of all positions, and current_holdings
stores the most recent dictionary of meanings: # portfolio.py class NaivePortfolio(Portfolio): """ NaivePortfolio (.. -) / , . BuyAndHoldStrategy. """ def __init__(self, bars, events, start_date, initial_capital=100000.0): """ . ( , ). 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()
# portfolio.py def construct_all_positions(self): """ , start_date , . """ d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] ) d['datetime'] = self.start_date return [d]
construct_all_hldings
is similar to the one described above, but adds some additional keys for free funds, commissions and the balance of the account after the transactions, the total commission paid and the total amount of existing assets (open positions and money). Short positions are considered as "negative". The values ​​of starting cash
and total account
equal to the initial capital: # portfolio.py def construct_all_holdings(self): """ , start_date , . """ 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]
construct_current_holdings
method is almost identical to the previous one, except that it does not “wrap” the dictionary in the list: # portfolio.py def construct_current_holdings(self): """ , . """ 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
DataHandler
object, the portfolio must update the current market value of the held positions. In the real trading scenario, this information can be downloaded and parsed directly from the brokerage system, but for the backtesting system it is necessary to calculate these values ​​separately.update_timeindex
method update_timeindex
responsible for processing the current value of new positions. It receives the latest prices from the market data processor and creates a new dictionary of tools that represent current positions, and equating “new” positions to “current” positions. This scheme only changes when a FillEvent
. After this, the method all_positions
current position set to the all_positions
list. Then, the present value values ​​are updated in a similar way, except that the market value is calculated by multiplying the number of current positions by the closing price of the last bar ( self.current_positions[s] * bars[s][0][5]
). New values ​​obtained are added to the all_holdings
list: # portfolio.py def update_timeindex(self, event): """ . , .. (OLHCVI). MarketEvent . """ 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)
update_positions_from_fill
method determines exactly what FillEvent
(buy or sell) was, and then updates the current_positions dictionary by adding or removing the appropriate number of stocks: # portfolio.py def update_positions_from_fill(self, fill): """ FillEvent , . 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 # self.current_positions[fill.symbol] += fill_dir*fill.quantity
update_holdings_from_fill
method update_holdings_from_fill
similar to the one described above, but it updates the holdings value. To simulate the cost of execution, the method does not use the price associated with the FillEvent
. Why so prosithodit? In the backtesting environment, the execution price is actually unknown, which means it must be assumed. Thus, the exercise price is set as the “current market price” (closing price of the last bar). The value of the current positions for a particular instrument is then equated to the price of execution multiplied by the number of securities in the order. # portfolio.py def update_holdings_from_fill(self, fill): """ FillEvent holdings . : fill - FillEvent, . """ # 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)
update_fill
from the abstract base Portfolio
class is implemented. It simply executes the two previous methods update_positions_from_fill
and update_holdings_from_fill
: # portfolio.py def update_fill(self, event): """ FillEvent. """ if event.type == 'FILL': self.update_positions_from_fill(event) self.update_holdings_from_fill(event)
OrderEvent
when receiving SignalEvent
signal events. The generate_naive_order method uses a signal to open a long or short position, the target financial instrument and then sends the corresponding order with 100 shares of the desired asset. 100 here is an arbitrary value. In the course of real trading, it would be determined by a risk management system or a module for calculating the value of positions. However, in NaivePortfolio
you can “naively” send orders right after receiving signals without any risk management.OrderEvent
object is OrderEvent
: # portfolio.py def generate_naive_order(self, signal): """ OrderEvent . : signal - SignalEvent. """ 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
update_signal
method simply calls the method described above and adds the generated order to the event queue. # portfolio.py def update_signal(self, event): """ SignalEvent . """ if event.type == 'SIGNAL': order_event = self.generate_naive_order(event) self.events.put(order_event)
NaivePortfolio
is to generate a capital curve. It creates a stream of information about profits, which is useful for calculating the performance of the strategy, then the curve is normalized on a percentage basis. The initial account size is set to wound 1.0: # portfolio.py def create_equity_curve_dataframe(self): """ pandas DataFrame all_holdings. """ 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
Portfolio
object is the most complex aspect of the entire event-oriented back tester. Despite the complexity, the processing of positions here is implemented at a very simple level.ExecutionHandler
object, which uses OrderEvent
objects to create a FillEvent
from them.Source: https://habr.com/ru/post/266623/
All Articles