1
|
|
|
# |
2
|
|
|
# Copyright 2014 Quantopian, Inc. |
3
|
|
|
# |
4
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
5
|
|
|
# you may not use this file except in compliance with the License. |
6
|
|
|
# You may obtain a copy of the License at |
7
|
|
|
# |
8
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0 |
9
|
|
|
# |
10
|
|
|
# Unless required by applicable law or agreed to in writing, software |
11
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS, |
12
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13
|
|
|
# See the License for the specific language governing permissions and |
14
|
|
|
# limitations under the License. |
15
|
|
|
|
16
|
|
|
""" |
17
|
|
|
|
18
|
|
|
Performance Period |
19
|
|
|
================== |
20
|
|
|
|
21
|
|
|
Performance Periods are updated with every trade. When calling |
22
|
|
|
code needs a portfolio object that fulfills the algorithm |
23
|
|
|
protocol, use the PerformancePeriod.as_portfolio method. See that |
24
|
|
|
method for comments on the specific fields provided (and |
25
|
|
|
omitted). |
26
|
|
|
|
27
|
|
|
+---------------+------------------------------------------------------+ |
28
|
|
|
| key | value | |
29
|
|
|
+===============+======================================================+ |
30
|
|
|
| ending_value | the total market value of the positions held at the | |
31
|
|
|
| | end of the period | |
32
|
|
|
+---------------+------------------------------------------------------+ |
33
|
|
|
| cash_flow | the cash flow in the period (negative means spent) | |
34
|
|
|
| | from buying and selling assets in the period. | |
35
|
|
|
| | Includes dividend payments in the period as well. | |
36
|
|
|
+---------------+------------------------------------------------------+ |
37
|
|
|
| starting_value| the total market value of the positions held at the | |
38
|
|
|
| | start of the period | |
39
|
|
|
+---------------+------------------------------------------------------+ |
40
|
|
|
| starting_cash | cash on hand at the beginning of the period | |
41
|
|
|
+---------------+------------------------------------------------------+ |
42
|
|
|
| ending_cash | cash on hand at the end of the period | |
43
|
|
|
+---------------+------------------------------------------------------+ |
44
|
|
|
| positions | a list of dicts representing positions, see | |
45
|
|
|
| | :py:meth:`Position.to_dict()` | |
46
|
|
|
| | for details on the contents of the dict | |
47
|
|
|
+---------------+------------------------------------------------------+ |
48
|
|
|
| pnl | Dollar value profit and loss, for both realized and | |
49
|
|
|
| | unrealized gains. | |
50
|
|
|
+---------------+------------------------------------------------------+ |
51
|
|
|
| returns | percentage returns for the entire portfolio over the | |
52
|
|
|
| | period | |
53
|
|
|
+---------------+------------------------------------------------------+ |
54
|
|
|
| cumulative\ | The net capital used (positive is spent) during | |
55
|
|
|
| _capital_used | the period | |
56
|
|
|
+---------------+------------------------------------------------------+ |
57
|
|
|
| max_capital\ | The maximum amount of capital deployed during the | |
58
|
|
|
| _used | period. | |
59
|
|
|
+---------------+------------------------------------------------------+ |
60
|
|
|
| period_close | The last close of the market in period. datetime in | |
61
|
|
|
| | pytz.utc timezone. | |
62
|
|
|
+---------------+------------------------------------------------------+ |
63
|
|
|
| period_open | The first open of the market in period. datetime in | |
64
|
|
|
| | pytz.utc timezone. | |
65
|
|
|
+---------------+------------------------------------------------------+ |
66
|
|
|
| transactions | all the transactions that were acrued during this | |
67
|
|
|
| | period. Unset/missing for cumulative periods. | |
68
|
|
|
+---------------+------------------------------------------------------+ |
69
|
|
|
|
70
|
|
|
|
71
|
|
|
""" |
72
|
|
|
|
73
|
|
|
from __future__ import division |
74
|
|
|
import logbook |
75
|
|
|
|
76
|
|
|
import numpy as np |
77
|
|
|
|
78
|
|
|
from collections import namedtuple |
79
|
|
|
from zipline.assets import Future |
80
|
|
|
|
81
|
|
|
try: |
82
|
|
|
# optional cython based OrderedDict |
83
|
|
|
from cyordereddict import OrderedDict |
84
|
|
|
except ImportError: |
85
|
|
|
from collections import OrderedDict |
86
|
|
|
|
87
|
|
|
from six import itervalues, iteritems |
88
|
|
|
|
89
|
|
|
import zipline.protocol as zp |
90
|
|
|
|
91
|
|
|
from zipline.utils.serialization_utils import ( |
92
|
|
|
VERSION_LABEL |
93
|
|
|
) |
94
|
|
|
|
95
|
|
|
log = logbook.Logger('Performance') |
96
|
|
|
TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE |
97
|
|
|
|
98
|
|
|
|
99
|
|
|
PeriodStats = namedtuple('PeriodStats', |
100
|
|
|
['net_liquidation', |
101
|
|
|
'gross_leverage', |
102
|
|
|
'net_leverage']) |
103
|
|
|
|
104
|
|
|
|
105
|
|
|
def calc_net_liquidation(ending_cash, long_value, short_value): |
106
|
|
|
return ending_cash + long_value + short_value |
107
|
|
|
|
108
|
|
|
|
109
|
|
|
def calc_leverage(exposure, net_liq): |
110
|
|
|
if net_liq != 0: |
111
|
|
|
return exposure / net_liq |
112
|
|
|
|
113
|
|
|
return np.inf |
114
|
|
|
|
115
|
|
|
|
116
|
|
|
def calc_period_stats(pos_stats, ending_cash): |
117
|
|
|
net_liq = calc_net_liquidation(ending_cash, |
118
|
|
|
pos_stats.long_value, |
119
|
|
|
pos_stats.short_value) |
120
|
|
|
gross_leverage = calc_leverage(pos_stats.gross_exposure, net_liq) |
121
|
|
|
net_leverage = calc_leverage(pos_stats.net_exposure, net_liq) |
122
|
|
|
|
123
|
|
|
return PeriodStats( |
124
|
|
|
net_liquidation=net_liq, |
125
|
|
|
gross_leverage=gross_leverage, |
126
|
|
|
net_leverage=net_leverage) |
127
|
|
|
|
128
|
|
|
|
129
|
|
|
def calc_payout(contract_multiplier, amount, old_price, price): |
130
|
|
|
return (price - old_price) * contract_multiplier * amount |
131
|
|
|
|
132
|
|
|
|
133
|
|
|
class PerformancePeriod(object): |
134
|
|
|
|
135
|
|
|
def __init__( |
136
|
|
|
self, |
137
|
|
|
starting_cash, |
138
|
|
|
asset_finder, |
139
|
|
|
period_open=None, |
140
|
|
|
period_close=None, |
141
|
|
|
keep_transactions=True, |
142
|
|
|
keep_orders=False, |
143
|
|
|
serialize_positions=True): |
144
|
|
|
|
145
|
|
|
self.asset_finder = asset_finder |
146
|
|
|
|
147
|
|
|
self.period_open = period_open |
148
|
|
|
self.period_close = period_close |
149
|
|
|
|
150
|
|
|
self.ending_value = 0.0 |
151
|
|
|
self.ending_exposure = 0.0 |
152
|
|
|
self.period_cash_flow = 0.0 |
153
|
|
|
self.pnl = 0.0 |
154
|
|
|
|
155
|
|
|
self.ending_cash = starting_cash |
156
|
|
|
|
157
|
|
|
# Keyed by asset, the previous last sale price of positions with |
158
|
|
|
# payouts on price differences, e.g. Futures. |
159
|
|
|
# |
160
|
|
|
# This dt is not the previous minute to the minute for which the |
161
|
|
|
# calculation is done, but the last sale price either before the period |
162
|
|
|
# start, or when the price at execution. |
163
|
|
|
self._payout_last_sale_prices = {} |
164
|
|
|
|
165
|
|
|
# rollover initializes a number of self's attributes: |
166
|
|
|
self.rollover() |
167
|
|
|
self.keep_transactions = keep_transactions |
168
|
|
|
self.keep_orders = keep_orders |
169
|
|
|
|
170
|
|
|
# An object to recycle via assigning new values |
171
|
|
|
# when returning portfolio information. |
172
|
|
|
# So as not to avoid creating a new object for each event |
173
|
|
|
self._portfolio_store = zp.Portfolio() |
174
|
|
|
self._account_store = zp.Account() |
175
|
|
|
self.serialize_positions = serialize_positions |
176
|
|
|
|
177
|
|
|
# This dict contains the known cash flow multipliers for sids and is |
178
|
|
|
# keyed on sid |
179
|
|
|
self._execution_cash_flow_multipliers = {} |
180
|
|
|
|
181
|
|
|
_position_tracker = None |
182
|
|
|
|
183
|
|
|
@property |
184
|
|
|
def position_tracker(self): |
185
|
|
|
return self._position_tracker |
186
|
|
|
|
187
|
|
|
@position_tracker.setter |
188
|
|
|
def position_tracker(self, obj): |
189
|
|
|
if obj is None: |
190
|
|
|
raise ValueError("position_tracker can not be None") |
191
|
|
|
self._position_tracker = obj |
192
|
|
|
# we only calculate perf once we inject PositionTracker |
193
|
|
|
self.calculate_performance() |
194
|
|
|
|
195
|
|
|
def rollover(self): |
196
|
|
|
self.starting_value = self.ending_value |
197
|
|
|
self.starting_exposure = self.ending_exposure |
198
|
|
|
self.starting_cash = self.ending_cash |
199
|
|
|
self.period_cash_flow = 0.0 |
200
|
|
|
self.pnl = 0.0 |
201
|
|
|
self.processed_transactions = {} |
202
|
|
|
self.orders_by_modified = {} |
203
|
|
|
self.orders_by_id = OrderedDict() |
204
|
|
|
|
205
|
|
|
payout_assets = self._payout_last_sale_prices.keys() |
206
|
|
|
|
207
|
|
|
for asset in payout_assets: |
208
|
|
|
if asset in self._payout_last_sale_prices: |
209
|
|
|
self._payout_last_sale_prices[asset] = \ |
210
|
|
|
self.position_tracker.positions[asset].last_sale_price |
211
|
|
|
else: |
212
|
|
|
del self._payout_last_sale_prices[asset] |
213
|
|
|
|
214
|
|
|
def handle_dividends_paid(self, net_cash_payment): |
215
|
|
|
if net_cash_payment: |
216
|
|
|
self.handle_cash_payment(net_cash_payment) |
217
|
|
|
self.calculate_performance() |
218
|
|
|
|
219
|
|
|
def handle_cash_payment(self, payment_amount): |
220
|
|
|
self.adjust_cash(payment_amount) |
221
|
|
|
|
222
|
|
|
def handle_commission(self, cost): |
223
|
|
|
# Deduct from our total cash pool. |
224
|
|
|
self.adjust_cash(-cost) |
225
|
|
|
|
226
|
|
|
def adjust_cash(self, amount): |
227
|
|
|
self.period_cash_flow += amount |
228
|
|
|
|
229
|
|
|
def adjust_field(self, field, value): |
230
|
|
|
setattr(self, field, value) |
231
|
|
|
|
232
|
|
|
def _get_payout_total(self, positions): |
233
|
|
|
payouts = [] |
234
|
|
|
for asset, old_price in iteritems(self._payout_last_sale_prices): |
235
|
|
|
pos = positions[asset] |
236
|
|
|
amount = pos.amount |
237
|
|
|
payout = calc_payout( |
238
|
|
|
asset.contract_multiplier, |
239
|
|
|
amount, |
240
|
|
|
old_price, |
241
|
|
|
pos.last_sale_price) |
242
|
|
|
payouts.append(payout) |
243
|
|
|
|
244
|
|
|
return sum(payouts) |
245
|
|
|
|
246
|
|
|
def calculate_performance(self): |
247
|
|
|
pt = self.position_tracker |
248
|
|
|
pos_stats = pt.stats() |
249
|
|
|
self.ending_value = pos_stats.net_value |
250
|
|
|
self.ending_exposure = pos_stats.net_exposure |
251
|
|
|
|
252
|
|
|
payout = self._get_payout_total(pt.positions) |
253
|
|
|
|
254
|
|
|
total_at_start = self.starting_cash + self.starting_value |
255
|
|
|
self.ending_cash = self.starting_cash + self.period_cash_flow |
256
|
|
|
total_at_end = self.ending_cash + self.ending_value + payout |
257
|
|
|
|
258
|
|
|
self.pnl = total_at_end - total_at_start |
259
|
|
|
if total_at_start != 0: |
260
|
|
|
self.returns = self.pnl / total_at_start |
261
|
|
|
else: |
262
|
|
|
self.returns = 0.0 |
263
|
|
|
|
264
|
|
|
def record_order(self, order): |
265
|
|
|
if self.keep_orders: |
266
|
|
|
try: |
267
|
|
|
dt_orders = self.orders_by_modified[order.dt] |
268
|
|
|
if order.id in dt_orders: |
269
|
|
|
del dt_orders[order.id] |
270
|
|
|
except KeyError: |
271
|
|
|
self.orders_by_modified[order.dt] = dt_orders = OrderedDict() |
272
|
|
|
dt_orders[order.id] = order |
273
|
|
|
# to preserve the order of the orders by modified date |
274
|
|
|
# we delete and add back. (ordered dictionary is sorted by |
275
|
|
|
# first insertion date). |
276
|
|
|
if order.id in self.orders_by_id: |
277
|
|
|
del self.orders_by_id[order.id] |
278
|
|
|
self.orders_by_id[order.id] = order |
279
|
|
|
|
280
|
|
|
def handle_execution(self, txn): |
281
|
|
|
self.period_cash_flow += self._calculate_execution_cash_flow(txn) |
282
|
|
|
|
283
|
|
|
asset = self.asset_finder.retrieve_asset(txn.sid) |
284
|
|
|
if isinstance(asset, Future): |
285
|
|
|
try: |
286
|
|
|
old_price = self._payout_last_sale_prices[asset] |
287
|
|
|
pos = self.position_tracker.positions[asset] |
288
|
|
|
amount = pos.amount |
289
|
|
|
price = txn.price |
290
|
|
|
cash_adj = calc_payout( |
291
|
|
|
asset.contract_multiplier, amount, old_price, price) |
292
|
|
|
self.adjust_cash(cash_adj) |
293
|
|
|
if amount + txn.amount == 0: |
294
|
|
|
del self._payout_last_sale_prices[asset] |
295
|
|
|
else: |
296
|
|
|
self._payout_last_sale_prices[asset] = price |
297
|
|
|
except KeyError: |
298
|
|
|
self._payout_last_sale_prices[asset] = txn.price |
299
|
|
|
|
300
|
|
|
if self.keep_transactions: |
301
|
|
|
try: |
302
|
|
|
self.processed_transactions[txn.dt].append(txn) |
303
|
|
|
except KeyError: |
304
|
|
|
self.processed_transactions[txn.dt] = [txn] |
305
|
|
|
|
306
|
|
|
def _calculate_execution_cash_flow(self, txn): |
307
|
|
|
""" |
308
|
|
|
Calculates the cash flow from executing the given transaction |
309
|
|
|
""" |
310
|
|
|
# Check if the multiplier is cached. If it is not, look up the asset |
311
|
|
|
# and cache the multiplier. |
312
|
|
|
try: |
313
|
|
|
multiplier = self._execution_cash_flow_multipliers[txn.sid] |
314
|
|
|
except KeyError: |
315
|
|
|
asset = self.asset_finder.retrieve_asset(txn.sid) |
316
|
|
|
# Futures experience no cash flow on transactions |
317
|
|
|
if isinstance(asset, Future): |
318
|
|
|
multiplier = 0 |
319
|
|
|
else: |
320
|
|
|
multiplier = 1 |
321
|
|
|
self._execution_cash_flow_multipliers[txn.sid] = multiplier |
322
|
|
|
|
323
|
|
|
# Calculate and return the cash flow given the multiplier |
324
|
|
|
return -1 * txn.price * txn.amount * multiplier |
325
|
|
|
|
326
|
|
|
# backwards compat. TODO: remove? |
327
|
|
|
@property |
328
|
|
|
def positions(self): |
329
|
|
|
return self.position_tracker.positions |
330
|
|
|
|
331
|
|
|
@property |
332
|
|
|
def position_amounts(self): |
333
|
|
|
return self.position_tracker.position_amounts |
334
|
|
|
|
335
|
|
|
def __core_dict(self): |
336
|
|
|
pos_stats = self.position_tracker.stats() |
337
|
|
|
period_stats = calc_period_stats(pos_stats, self.ending_cash) |
338
|
|
|
|
339
|
|
|
rval = { |
340
|
|
|
'ending_value': self.ending_value, |
341
|
|
|
'ending_exposure': self.ending_exposure, |
342
|
|
|
# this field is renamed to capital_used for backward |
343
|
|
|
# compatibility. |
344
|
|
|
'capital_used': self.period_cash_flow, |
345
|
|
|
'starting_value': self.starting_value, |
346
|
|
|
'starting_exposure': self.starting_exposure, |
347
|
|
|
'starting_cash': self.starting_cash, |
348
|
|
|
'ending_cash': self.ending_cash, |
349
|
|
|
'portfolio_value': self.ending_cash + self.ending_value, |
350
|
|
|
'pnl': self.pnl, |
351
|
|
|
'returns': self.returns, |
352
|
|
|
'period_open': self.period_open, |
353
|
|
|
'period_close': self.period_close, |
354
|
|
|
'gross_leverage': period_stats.gross_leverage, |
355
|
|
|
'net_leverage': period_stats.net_leverage, |
356
|
|
|
'short_exposure': pos_stats.short_exposure, |
357
|
|
|
'long_exposure': pos_stats.long_exposure, |
358
|
|
|
'short_value': pos_stats.short_value, |
359
|
|
|
'long_value': pos_stats.long_value, |
360
|
|
|
'longs_count': pos_stats.longs_count, |
361
|
|
|
'shorts_count': pos_stats.shorts_count, |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
return rval |
365
|
|
|
|
366
|
|
|
def to_dict(self, dt=None): |
367
|
|
|
""" |
368
|
|
|
Creates a dictionary representing the state of this performance |
369
|
|
|
period. See header comments for a detailed description. |
370
|
|
|
|
371
|
|
|
Kwargs: |
372
|
|
|
dt (datetime): If present, only return transactions for the dt. |
373
|
|
|
""" |
374
|
|
|
rval = self.__core_dict() |
375
|
|
|
|
376
|
|
|
if self.serialize_positions: |
377
|
|
|
positions = self.position_tracker.get_positions_list() |
378
|
|
|
rval['positions'] = positions |
379
|
|
|
|
380
|
|
|
# we want the key to be absent, not just empty |
381
|
|
|
if self.keep_transactions: |
382
|
|
|
if dt: |
383
|
|
|
# Only include transactions for given dt |
384
|
|
|
try: |
385
|
|
|
transactions = [x.to_dict() |
386
|
|
|
for x in self.processed_transactions[dt]] |
387
|
|
|
except KeyError: |
388
|
|
|
transactions = [] |
389
|
|
|
else: |
390
|
|
|
transactions = \ |
391
|
|
|
[y.to_dict() |
392
|
|
|
for x in itervalues(self.processed_transactions) |
393
|
|
|
for y in x] |
394
|
|
|
rval['transactions'] = transactions |
395
|
|
|
|
396
|
|
|
if self.keep_orders: |
397
|
|
|
if dt: |
398
|
|
|
# only include orders modified as of the given dt. |
399
|
|
|
try: |
400
|
|
|
orders = [x.to_dict() |
401
|
|
|
for x in itervalues(self.orders_by_modified[dt])] |
402
|
|
|
except KeyError: |
403
|
|
|
orders = [] |
404
|
|
|
else: |
405
|
|
|
orders = [x.to_dict() for x in itervalues(self.orders_by_id)] |
406
|
|
|
rval['orders'] = orders |
407
|
|
|
|
408
|
|
|
return rval |
409
|
|
|
|
410
|
|
|
def as_portfolio(self): |
411
|
|
|
""" |
412
|
|
|
The purpose of this method is to provide a portfolio |
413
|
|
|
object to algorithms running inside the same trading |
414
|
|
|
client. The data needed is captured raw in a |
415
|
|
|
PerformancePeriod, and in this method we rename some |
416
|
|
|
fields for usability and remove extraneous fields. |
417
|
|
|
""" |
418
|
|
|
# Recycles containing objects' Portfolio object |
419
|
|
|
# which is used for returning values. |
420
|
|
|
# as_portfolio is called in an inner loop, |
421
|
|
|
# so repeated object creation becomes too expensive |
422
|
|
|
portfolio = self._portfolio_store |
423
|
|
|
# maintaining the old name for the portfolio field for |
424
|
|
|
# backward compatibility |
425
|
|
|
portfolio.capital_used = self.period_cash_flow |
426
|
|
|
portfolio.starting_cash = self.starting_cash |
427
|
|
|
portfolio.portfolio_value = self.ending_cash + self.ending_value |
428
|
|
|
portfolio.pnl = self.pnl |
429
|
|
|
portfolio.returns = self.returns |
430
|
|
|
portfolio.cash = self.ending_cash |
431
|
|
|
portfolio.start_date = self.period_open |
432
|
|
|
portfolio.positions = self.position_tracker.get_positions() |
433
|
|
|
portfolio.positions_value = self.ending_value |
434
|
|
|
portfolio.positions_exposure = self.ending_exposure |
435
|
|
|
return portfolio |
436
|
|
|
|
437
|
|
|
def as_account(self): |
438
|
|
|
account = self._account_store |
439
|
|
|
|
440
|
|
|
pt = self.position_tracker |
441
|
|
|
pos_stats = pt.stats() |
442
|
|
|
period_stats = calc_period_stats(pos_stats, self.ending_cash) |
443
|
|
|
|
444
|
|
|
# If no attribute is found on the PerformancePeriod resort to the |
445
|
|
|
# following default values. If an attribute is found use the existing |
446
|
|
|
# value. For instance, a broker may provide updates to these |
447
|
|
|
# attributes. In this case we do not want to over write the broker |
448
|
|
|
# values with the default values. |
449
|
|
|
account.settled_cash = \ |
450
|
|
|
getattr(self, 'settled_cash', self.ending_cash) |
451
|
|
|
account.accrued_interest = \ |
452
|
|
|
getattr(self, 'accrued_interest', 0.0) |
453
|
|
|
account.buying_power = \ |
454
|
|
|
getattr(self, 'buying_power', float('inf')) |
455
|
|
|
account.equity_with_loan = \ |
456
|
|
|
getattr(self, 'equity_with_loan', |
457
|
|
|
self.ending_cash + self.ending_value) |
458
|
|
|
account.total_positions_value = \ |
459
|
|
|
getattr(self, 'total_positions_value', self.ending_value) |
460
|
|
|
account.total_positions_value = \ |
461
|
|
|
getattr(self, 'total_positions_exposure', self.ending_exposure) |
462
|
|
|
account.regt_equity = \ |
463
|
|
|
getattr(self, 'regt_equity', self.ending_cash) |
464
|
|
|
account.regt_margin = \ |
465
|
|
|
getattr(self, 'regt_margin', float('inf')) |
466
|
|
|
account.initial_margin_requirement = \ |
467
|
|
|
getattr(self, 'initial_margin_requirement', 0.0) |
468
|
|
|
account.maintenance_margin_requirement = \ |
469
|
|
|
getattr(self, 'maintenance_margin_requirement', 0.0) |
470
|
|
|
account.available_funds = \ |
471
|
|
|
getattr(self, 'available_funds', self.ending_cash) |
472
|
|
|
account.excess_liquidity = \ |
473
|
|
|
getattr(self, 'excess_liquidity', self.ending_cash) |
474
|
|
|
account.cushion = \ |
475
|
|
|
getattr(self, 'cushion', |
476
|
|
|
self.ending_cash / (self.ending_cash + self.ending_value)) |
477
|
|
|
account.day_trades_remaining = \ |
478
|
|
|
getattr(self, 'day_trades_remaining', float('inf')) |
479
|
|
|
account.leverage = getattr(self, 'leverage', |
480
|
|
|
period_stats.gross_leverage) |
481
|
|
|
account.net_leverage = period_stats.net_leverage |
482
|
|
|
|
483
|
|
|
account.net_liquidation = getattr(self, 'net_liquidation', |
484
|
|
|
period_stats.net_liquidation) |
485
|
|
|
return account |
486
|
|
|
|
487
|
|
|
def __getstate__(self): |
488
|
|
|
state_dict = {k: v for k, v in iteritems(self.__dict__) |
489
|
|
|
if not k.startswith('_')} |
490
|
|
|
|
491
|
|
|
state_dict['_portfolio_store'] = self._portfolio_store |
492
|
|
|
state_dict['_account_store'] = self._account_store |
493
|
|
|
|
494
|
|
|
state_dict['processed_transactions'] = \ |
495
|
|
|
dict(self.processed_transactions) |
496
|
|
|
state_dict['orders_by_id'] = \ |
497
|
|
|
dict(self.orders_by_id) |
498
|
|
|
state_dict['orders_by_modified'] = \ |
499
|
|
|
dict(self.orders_by_modified) |
500
|
|
|
state_dict['_payout_last_sale_prices'] = \ |
501
|
|
|
self._payout_last_sale_prices |
502
|
|
|
|
503
|
|
|
STATE_VERSION = 3 |
504
|
|
|
state_dict[VERSION_LABEL] = STATE_VERSION |
505
|
|
|
return state_dict |
506
|
|
|
|
507
|
|
|
def __setstate__(self, state): |
508
|
|
|
|
509
|
|
|
OLDEST_SUPPORTED_STATE = 3 |
510
|
|
|
version = state.pop(VERSION_LABEL) |
511
|
|
|
|
512
|
|
|
if version < OLDEST_SUPPORTED_STATE: |
513
|
|
|
raise BaseException("PerformancePeriod saved state is too old.") |
514
|
|
|
|
515
|
|
|
processed_transactions = {} |
516
|
|
|
processed_transactions.update(state.pop('processed_transactions')) |
517
|
|
|
|
518
|
|
|
orders_by_id = OrderedDict() |
519
|
|
|
orders_by_id.update(state.pop('orders_by_id')) |
520
|
|
|
|
521
|
|
|
orders_by_modified = {} |
522
|
|
|
orders_by_modified.update(state.pop('orders_by_modified')) |
523
|
|
|
self.processed_transactions = processed_transactions |
524
|
|
|
self.orders_by_id = orders_by_id |
525
|
|
|
self.orders_by_modified = orders_by_modified |
526
|
|
|
|
527
|
|
|
self._execution_cash_flow_multipliers = {} |
528
|
|
|
|
529
|
|
|
self.__dict__.update(state) |
530
|
|
|
|