-
Notifications
You must be signed in to change notification settings - Fork 19
/
Portfolio.py
233 lines (200 loc) · 8.86 KB
/
Portfolio.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
from __future__ import print_function
import datetime
try:
import Queue as queue
except ImportError:
import queue
import numpy as np
import pandas as pd
from Events import FillEvent, OrderEvent, SignalEvent
from Performance import create_sharpe_ratio, create_drawdowns
from math import floor
class Portfolio(object):
"""
The Portfolio class handles the positions and market
value of all instruments at a resolution of a "bar",
The positions DataFrame stores a time-index of the
quantity of positions held.
"""
def __init__(self, bars, events, start_date, initial_capital=100000.0):
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.define_all_positions()
self.current_positions = {symbol: 0 for symbol in self.symbol_list}
self.all_holdings = self.define_all_holdings()
self.current_holdings = self.define_current_holdings()
def define_all_positions(self):
"""
Creates a list of positions of all symbols at start_date time index
"""
positions = {symbol: 0 for symbol in self.symbol_list}
positions['datetime'] = self.start_date
return [positions]
def define_all_holdings(self):
"""
Similar to positions, creates the list of holdings using
start_date as initial time index.
Holdings should consider the time, cash, commission and the total
"""
holdings = {symbol: 0 for symbol in self.symbol_list}
holdings['datetime'] = self.start_date
holdings['cash'] = self.initial_capital
holdings['commission'] = 0.0
holdings['total'] = self.initial_capital
return [holdings]
def define_current_holdings(self):
"""
This builds the dictionary which will hold the instantaneous
value of the portfolio across all symbols.
"""
holdings = {symbol: 0.0 for symbol in self.symbol_list}
holdings["cash"] = self.initial_capital
holdings["commission"] = 0.0
holdings["total"] = self.initial_capital
return holdings
"""
This is the update of the portfolio value at each new datafeed coming from a MarketEvent
"""
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 (OHLCV).
Makes use of a MarketEvent from the events queue.
"""
latest_datetime = self.bars.get_latest_bar_datetime(self.symbol_list[0])
# Update positions
# ================
# Dictionary comprehension list with all symbol keys updated by current_positions values
positions = {symbol: self.current_positions[symbol] for symbol in self.symbol_list}
positions["datetime"] = latest_datetime
# Append the current positions
self.all_positions.append(positions)
# Update holdings
# ===============
holdings = {symbol: 0.0 for symbol in self.symbol_list}
holdings["datetime"] = latest_datetime
holdings["cash"] = self.current_holdings["cash"]
holdings["commission"] = self.current_holdings["commission"]
holdings["total"] = self.current_holdings["cash"]
# Update market value and pnl for all symbols
# ==============
for symbol in self.symbol_list:
# Approximation to the real value --> market_value = adj close price * position_size
# TODO --> This needs to be better represented in real life, depending on the frequency of the strategy
market_value = self.current_positions[symbol] * self.bars.get_latest_bar_value(symbol, "adj_close")
holdings[symbol] = market_value
holdings["total"] += market_value
# Append the current holdings
self.all_holdings.append(holdings)
"""
Check if a SignalEvent has been generated from the strategy to place an Order event in the queue
and create a naive order
"""
# TODO --> To consider a Risk Management class for position sizing, between strategies
def update_signal(self, event):
"""
Acts on a SignalEvent to generate new orders
based on the portfolio logic.
"""
if isinstance(event, SignalEvent):
order_event = self.generate_naive_order(event)
self.events.put(order_event)
def generate_naive_order(self, signal):
"""
Simply files an Order object as a constant quantity
sizing of the signal object
Parameters:
signal - The tuple containing Signal information.
"""
order = None
symbol = signal.symbol
direction = signal.signal_type
strength = signal.strength
mkt_quantity = floor(100 * strength)
current_quantity = self.current_positions[symbol]
order_type = "MKT"
if direction == "LONG" and current_quantity == 0:
order = OrderEvent(symbol, order_type, mkt_quantity, "BUY")
if direction == "SHORT" and current_quantity == 0:
order = OrderEvent(symbol, order_type, mkt_quantity, "SELL")
if direction == "EXIT" and current_quantity > 0:
order = OrderEvent(symbol, order_type, abs(current_quantity), "SELL")
if direction == "EXIT" and current_quantity < 0:
order = OrderEvent(symbol, order_type, abs(current_quantity), "BUY")
return order
"""
The functions below update positions, and holdings of the portfolio after a Fill event
"""
def update_fill(self, event):
"""
Updates the portfolio current positions and holdings
from a FillEvent.
"""
if isinstance(event, FillEvent):
self.update_positions_after_fill(event)
self.update_holdings_after_fill(event)
def update_positions_after_fill(self, fill):
"""
Takes a Fill object and updates the position matrix to
reflect the new position.
Parameters:
fill - The Fill 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
def update_holdings_after_fill(self, fill):
"""
Takes a Fill object and updates the holdings matrix to
reflect the holdings value.
Parameters:
fill - The Fill 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_bar_value(fill.symbol, "adj_close") # unknown so set to the market 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)
def create_equity_curve_dataframe(self):
"""
Creates a pandas DataFrame from the all_holdings
list of dictionaries.
"""
equity_curve = pd.DataFrame(self.all_holdings)
equity_curve.set_index("datetime", inplace=True)
equity_curve["returns"] = equity_curve["total"].pct_change()
equity_curve["equity_curve"] = (1.0 + equity_curve["returns"]).cumprod()
self.equity_curve = equity_curve
def output_summary_stats(self):
"""
Creates a list of summary statistics for the portfolio.
"""
total_return = self.equity_curve["equity_curve"][-1]
returns = self.equity_curve["returns"]
pnl = self.equity_curve["equity_curve"]
sharpe_ratio = create_sharpe_ratio(returns, periods=252)
drawdown, max_dd, max_dd_duration = create_drawdowns(pnl)
self.equity_curve["drawdown"] = drawdown
stats = [("Total Return", "%0.2f%%" % ((total_return - 1.0) * 100.0)),
("Sharpe Ratio", "%0.2f" % sharpe_ratio),
("Max Drawdown", "%0.2f%%" % (max_dd * 100.0)),
("Max Drawdown Duration", "%d" % max_dd_duration)]
self.equity_curve.to_csv("equity.csv")
return stats