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
|
|
|
class PerformancePeriod(object): |
130
|
|
|
|
131
|
|
|
def __init__( |
132
|
|
|
self, |
133
|
|
|
starting_cash, |
134
|
|
|
asset_finder, |
135
|
|
|
period_open=None, |
136
|
|
|
period_close=None, |
137
|
|
|
keep_transactions=True, |
138
|
|
|
keep_orders=False, |
139
|
|
|
serialize_positions=True): |
140
|
|
|
|
141
|
|
|
self.asset_finder = asset_finder |
142
|
|
|
|
143
|
|
|
self.period_open = period_open |
144
|
|
|
self.period_close = period_close |
145
|
|
|
|
146
|
|
|
self.ending_value = 0.0 |
147
|
|
|
self.ending_exposure = 0.0 |
148
|
|
|
self.period_cash_flow = 0.0 |
149
|
|
|
self.pnl = 0.0 |
150
|
|
|
|
151
|
|
|
self.ending_cash = starting_cash |
152
|
|
|
|
153
|
|
|
# Keyed by asset, the previous last sale price of positions with |
154
|
|
|
# payouts on price differences, e.g. Futures. |
155
|
|
|
# |
156
|
|
|
# This dt is not the previous minute to the minute for which the |
157
|
|
|
# calculation is done, but the last sale price either before the period |
158
|
|
|
# start, or when the price at execution. |
159
|
|
|
self._payout_last_sale_prices = {} |
160
|
|
|
|
161
|
|
|
# rollover initializes a number of self's attributes: |
162
|
|
|
self.rollover() |
163
|
|
|
self.keep_transactions = keep_transactions |
164
|
|
|
self.keep_orders = keep_orders |
165
|
|
|
|
166
|
|
|
# An object to recycle via assigning new values |
167
|
|
|
# when returning portfolio information. |
168
|
|
|
# So as not to avoid creating a new object for each event |
169
|
|
|
self._portfolio_store = zp.Portfolio() |
170
|
|
|
self._account_store = zp.Account() |
171
|
|
|
self.serialize_positions = serialize_positions |
172
|
|
|
|
173
|
|
|
# This dict contains the known cash flow multipliers for sids and is |
174
|
|
|
# keyed on sid |
175
|
|
|
self._execution_cash_flow_multipliers = {} |
176
|
|
|
|
177
|
|
|
_position_tracker = None |
178
|
|
|
|
179
|
|
|
@property |
180
|
|
|
def position_tracker(self): |
181
|
|
|
return self._position_tracker |
182
|
|
|
|
183
|
|
|
@position_tracker.setter |
184
|
|
|
def position_tracker(self, obj): |
185
|
|
|
if obj is None: |
186
|
|
|
raise ValueError("position_tracker can not be None") |
187
|
|
|
self._position_tracker = obj |
188
|
|
|
# we only calculate perf once we inject PositionTracker |
189
|
|
|
self.calculate_performance() |
190
|
|
|
|
191
|
|
|
def rollover(self): |
192
|
|
|
self.starting_value = self.ending_value |
193
|
|
|
self.starting_exposure = self.ending_exposure |
194
|
|
|
self.starting_cash = self.ending_cash |
195
|
|
|
self.period_cash_flow = 0.0 |
196
|
|
|
self.pnl = 0.0 |
197
|
|
|
self.processed_transactions = {} |
198
|
|
|
self.orders_by_modified = {} |
199
|
|
|
self.orders_by_id = OrderedDict() |
200
|
|
|
|
201
|
|
|
payout_assets = self._payout_last_sale_prices.keys() |
202
|
|
|
|
203
|
|
|
for asset in payout_assets: |
204
|
|
|
if asset in self._payout_last_sale_prices: |
205
|
|
|
self._payout_last_sale_prices[asset] = \ |
206
|
|
|
self.position_tracker.positions[asset].last_sale_price |
207
|
|
|
else: |
208
|
|
|
del self._payout_last_sale_prices[asset] |
209
|
|
|
|
210
|
|
|
def handle_dividends_paid(self, net_cash_payment): |
211
|
|
|
if net_cash_payment: |
212
|
|
|
self.handle_cash_payment(net_cash_payment) |
213
|
|
|
self.calculate_performance() |
214
|
|
|
|
215
|
|
|
def handle_cash_payment(self, payment_amount): |
216
|
|
|
self.adjust_cash(payment_amount) |
217
|
|
|
|
218
|
|
|
def handle_commission(self, cost): |
219
|
|
|
# Deduct from our total cash pool. |
220
|
|
|
self.adjust_cash(-cost) |
221
|
|
|
|
222
|
|
|
def adjust_cash(self, amount): |
223
|
|
|
self.period_cash_flow += amount |
224
|
|
|
|
225
|
|
|
def adjust_field(self, field, value): |
226
|
|
|
setattr(self, field, value) |
227
|
|
|
|
228
|
|
|
def _get_payout_total(self, positions): |
229
|
|
|
payouts = [] |
230
|
|
|
for asset, old_price in iteritems(self._payout_last_sale_prices): |
231
|
|
|
pos = positions[asset] |
232
|
|
|
price = pos.last_sale_price |
233
|
|
|
|
234
|
|
|
payout = ( |
235
|
|
|
(price - old_price) |
236
|
|
|
* |
237
|
|
|
asset.contract_multiplier |
238
|
|
|
* |
239
|
|
|
pos.amount |
240
|
|
|
) |
241
|
|
|
payouts.append(payout) |
242
|
|
|
|
243
|
|
|
return sum(payouts) |
244
|
|
|
|
245
|
|
|
def calculate_performance(self): |
246
|
|
|
pt = self.position_tracker |
247
|
|
|
pos_stats = pt.stats() |
248
|
|
|
self.ending_value = pos_stats.net_value |
249
|
|
|
self.ending_exposure = pos_stats.net_exposure |
250
|
|
|
|
251
|
|
|
payout = self._get_payout_total(pt.positions) |
252
|
|
|
|
253
|
|
|
total_at_start = self.starting_cash + self.starting_value |
254
|
|
|
self.ending_cash = self.starting_cash + self.period_cash_flow |
255
|
|
|
total_at_end = self.ending_cash + self.ending_value + payout |
256
|
|
|
|
257
|
|
|
self.pnl = total_at_end - total_at_start |
258
|
|
|
if total_at_start != 0: |
259
|
|
|
self.returns = self.pnl / total_at_start |
260
|
|
|
else: |
261
|
|
|
self.returns = 0.0 |
262
|
|
|
|
263
|
|
|
def record_order(self, order): |
264
|
|
|
if self.keep_orders: |
265
|
|
|
try: |
266
|
|
|
dt_orders = self.orders_by_modified[order.dt] |
267
|
|
|
if order.id in dt_orders: |
268
|
|
|
del dt_orders[order.id] |
269
|
|
|
except KeyError: |
270
|
|
|
self.orders_by_modified[order.dt] = dt_orders = OrderedDict() |
271
|
|
|
dt_orders[order.id] = order |
272
|
|
|
# to preserve the order of the orders by modified date |
273
|
|
|
# we delete and add back. (ordered dictionary is sorted by |
274
|
|
|
# first insertion date). |
275
|
|
|
if order.id in self.orders_by_id: |
276
|
|
|
del self.orders_by_id[order.id] |
277
|
|
|
self.orders_by_id[order.id] = order |
278
|
|
|
|
279
|
|
|
def handle_execution(self, txn): |
280
|
|
|
self.period_cash_flow += self._calculate_execution_cash_flow(txn) |
281
|
|
|
|
282
|
|
|
asset = self.asset_finder.retrieve_asset(txn.sid) |
283
|
|
|
if isinstance(asset, Future): |
284
|
|
|
try: |
285
|
|
|
old_price = self._payout_last_sale_prices[asset] |
286
|
|
|
amount = self.position_tracker.positions[asset].amount |
287
|
|
|
price = txn.price |
288
|
|
|
cash_adj = (price - old_price) * asset.contract_multiplier * \ |
289
|
|
|
amount |
290
|
|
|
self.adjust_cash(cash_adj) |
291
|
|
|
if amount + txn.amount == 0: |
292
|
|
|
del self._payout_last_sale_prices[asset] |
293
|
|
|
else: |
294
|
|
|
self._payout_last_sale_prices[asset] = price |
295
|
|
|
except KeyError: |
296
|
|
|
self._payout_last_sale_prices[asset] = txn.price |
297
|
|
|
|
298
|
|
|
if self.keep_transactions: |
299
|
|
|
try: |
300
|
|
|
self.processed_transactions[txn.dt].append(txn) |
301
|
|
|
except KeyError: |
302
|
|
|
self.processed_transactions[txn.dt] = [txn] |
303
|
|
|
|
304
|
|
|
def _calculate_execution_cash_flow(self, txn): |
305
|
|
|
""" |
306
|
|
|
Calculates the cash flow from executing the given transaction |
307
|
|
|
""" |
308
|
|
|
# Check if the multiplier is cached. If it is not, look up the asset |
309
|
|
|
# and cache the multiplier. |
310
|
|
|
try: |
311
|
|
|
multiplier = self._execution_cash_flow_multipliers[txn.sid] |
312
|
|
|
except KeyError: |
313
|
|
|
asset = self.asset_finder.retrieve_asset(txn.sid) |
314
|
|
|
# Futures experience no cash flow on transactions |
315
|
|
|
if isinstance(asset, Future): |
316
|
|
|
multiplier = 0 |
317
|
|
|
else: |
318
|
|
|
multiplier = 1 |
319
|
|
|
self._execution_cash_flow_multipliers[txn.sid] = multiplier |
320
|
|
|
|
321
|
|
|
# Calculate and return the cash flow given the multiplier |
322
|
|
|
return -1 * txn.price * txn.amount * multiplier |
323
|
|
|
|
324
|
|
|
# backwards compat. TODO: remove? |
325
|
|
|
@property |
326
|
|
|
def positions(self): |
327
|
|
|
return self.position_tracker.positions |
328
|
|
|
|
329
|
|
|
@property |
330
|
|
|
def position_amounts(self): |
331
|
|
|
return self.position_tracker.position_amounts |
332
|
|
|
|
333
|
|
|
def __core_dict(self): |
334
|
|
|
pos_stats = self.position_tracker.stats() |
335
|
|
|
period_stats = calc_period_stats(pos_stats, self.ending_cash) |
336
|
|
|
|
337
|
|
|
rval = { |
338
|
|
|
'ending_value': self.ending_value, |
339
|
|
|
'ending_exposure': self.ending_exposure, |
340
|
|
|
# this field is renamed to capital_used for backward |
341
|
|
|
# compatibility. |
342
|
|
|
'capital_used': self.period_cash_flow, |
343
|
|
|
'starting_value': self.starting_value, |
344
|
|
|
'starting_exposure': self.starting_exposure, |
345
|
|
|
'starting_cash': self.starting_cash, |
346
|
|
|
'ending_cash': self.ending_cash, |
347
|
|
|
'portfolio_value': self.ending_cash + self.ending_value, |
348
|
|
|
'pnl': self.pnl, |
349
|
|
|
'returns': self.returns, |
350
|
|
|
'period_open': self.period_open, |
351
|
|
|
'period_close': self.period_close, |
352
|
|
|
'gross_leverage': period_stats.gross_leverage, |
353
|
|
|
'net_leverage': period_stats.net_leverage, |
354
|
|
|
'short_exposure': pos_stats.short_exposure, |
355
|
|
|
'long_exposure': pos_stats.long_exposure, |
356
|
|
|
'short_value': pos_stats.short_value, |
357
|
|
|
'long_value': pos_stats.long_value, |
358
|
|
|
'longs_count': pos_stats.longs_count, |
359
|
|
|
'shorts_count': pos_stats.shorts_count, |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
return rval |
363
|
|
|
|
364
|
|
|
def to_dict(self, dt=None): |
365
|
|
|
""" |
366
|
|
|
Creates a dictionary representing the state of this performance |
367
|
|
|
period. See header comments for a detailed description. |
368
|
|
|
|
369
|
|
|
Kwargs: |
370
|
|
|
dt (datetime): If present, only return transactions for the dt. |
371
|
|
|
""" |
372
|
|
|
rval = self.__core_dict() |
373
|
|
|
|
374
|
|
|
if self.serialize_positions: |
375
|
|
|
positions = self.position_tracker.get_positions_list() |
376
|
|
|
rval['positions'] = positions |
377
|
|
|
|
378
|
|
|
# we want the key to be absent, not just empty |
379
|
|
|
if self.keep_transactions: |
380
|
|
|
if dt: |
381
|
|
|
# Only include transactions for given dt |
382
|
|
|
try: |
383
|
|
|
transactions = [x.to_dict() |
384
|
|
|
for x in self.processed_transactions[dt]] |
385
|
|
|
except KeyError: |
386
|
|
|
transactions = [] |
387
|
|
|
else: |
388
|
|
|
transactions = \ |
389
|
|
|
[y.to_dict() |
390
|
|
|
for x in itervalues(self.processed_transactions) |
391
|
|
|
for y in x] |
392
|
|
|
rval['transactions'] = transactions |
393
|
|
|
|
394
|
|
|
if self.keep_orders: |
395
|
|
|
if dt: |
396
|
|
|
# only include orders modified as of the given dt. |
397
|
|
|
try: |
398
|
|
|
orders = [x.to_dict() |
399
|
|
|
for x in itervalues(self.orders_by_modified[dt])] |
400
|
|
|
except KeyError: |
401
|
|
|
orders = [] |
402
|
|
|
else: |
403
|
|
|
orders = [x.to_dict() for x in itervalues(self.orders_by_id)] |
404
|
|
|
rval['orders'] = orders |
405
|
|
|
|
406
|
|
|
return rval |
407
|
|
|
|
408
|
|
|
def as_portfolio(self): |
409
|
|
|
""" |
410
|
|
|
The purpose of this method is to provide a portfolio |
411
|
|
|
object to algorithms running inside the same trading |
412
|
|
|
client. The data needed is captured raw in a |
413
|
|
|
PerformancePeriod, and in this method we rename some |
414
|
|
|
fields for usability and remove extraneous fields. |
415
|
|
|
""" |
416
|
|
|
# Recycles containing objects' Portfolio object |
417
|
|
|
# which is used for returning values. |
418
|
|
|
# as_portfolio is called in an inner loop, |
419
|
|
|
# so repeated object creation becomes too expensive |
420
|
|
|
portfolio = self._portfolio_store |
421
|
|
|
# maintaining the old name for the portfolio field for |
422
|
|
|
# backward compatibility |
423
|
|
|
portfolio.capital_used = self.period_cash_flow |
424
|
|
|
portfolio.starting_cash = self.starting_cash |
425
|
|
|
portfolio.portfolio_value = self.ending_cash + self.ending_value |
426
|
|
|
portfolio.pnl = self.pnl |
427
|
|
|
portfolio.returns = self.returns |
428
|
|
|
portfolio.cash = self.ending_cash |
429
|
|
|
portfolio.start_date = self.period_open |
430
|
|
|
portfolio.positions = self.position_tracker.get_positions() |
431
|
|
|
portfolio.positions_value = self.ending_value |
432
|
|
|
portfolio.positions_exposure = self.ending_exposure |
433
|
|
|
return portfolio |
434
|
|
|
|
435
|
|
|
def as_account(self): |
436
|
|
|
account = self._account_store |
437
|
|
|
|
438
|
|
|
pt = self.position_tracker |
439
|
|
|
pos_stats = pt.stats() |
440
|
|
|
period_stats = calc_period_stats(pos_stats, self.ending_cash) |
441
|
|
|
|
442
|
|
|
# If no attribute is found on the PerformancePeriod resort to the |
443
|
|
|
# following default values. If an attribute is found use the existing |
444
|
|
|
# value. For instance, a broker may provide updates to these |
445
|
|
|
# attributes. In this case we do not want to over write the broker |
446
|
|
|
# values with the default values. |
447
|
|
|
account.settled_cash = \ |
448
|
|
|
getattr(self, 'settled_cash', self.ending_cash) |
449
|
|
|
account.accrued_interest = \ |
450
|
|
|
getattr(self, 'accrued_interest', 0.0) |
451
|
|
|
account.buying_power = \ |
452
|
|
|
getattr(self, 'buying_power', float('inf')) |
453
|
|
|
account.equity_with_loan = \ |
454
|
|
|
getattr(self, 'equity_with_loan', |
455
|
|
|
self.ending_cash + self.ending_value) |
456
|
|
|
account.total_positions_value = \ |
457
|
|
|
getattr(self, 'total_positions_value', self.ending_value) |
458
|
|
|
account.total_positions_value = \ |
459
|
|
|
getattr(self, 'total_positions_exposure', self.ending_exposure) |
460
|
|
|
account.regt_equity = \ |
461
|
|
|
getattr(self, 'regt_equity', self.ending_cash) |
462
|
|
|
account.regt_margin = \ |
463
|
|
|
getattr(self, 'regt_margin', float('inf')) |
464
|
|
|
account.initial_margin_requirement = \ |
465
|
|
|
getattr(self, 'initial_margin_requirement', 0.0) |
466
|
|
|
account.maintenance_margin_requirement = \ |
467
|
|
|
getattr(self, 'maintenance_margin_requirement', 0.0) |
468
|
|
|
account.available_funds = \ |
469
|
|
|
getattr(self, 'available_funds', self.ending_cash) |
470
|
|
|
account.excess_liquidity = \ |
471
|
|
|
getattr(self, 'excess_liquidity', self.ending_cash) |
472
|
|
|
account.cushion = \ |
473
|
|
|
getattr(self, 'cushion', |
474
|
|
|
self.ending_cash / (self.ending_cash + self.ending_value)) |
475
|
|
|
account.day_trades_remaining = \ |
476
|
|
|
getattr(self, 'day_trades_remaining', float('inf')) |
477
|
|
|
account.leverage = getattr(self, 'leverage', |
478
|
|
|
period_stats.gross_leverage) |
479
|
|
|
account.net_leverage = period_stats.net_leverage |
480
|
|
|
|
481
|
|
|
account.net_liquidation = getattr(self, 'net_liquidation', |
482
|
|
|
period_stats.net_liquidation) |
483
|
|
|
return account |
484
|
|
|
|
485
|
|
|
def __getstate__(self): |
486
|
|
|
state_dict = {k: v for k, v in iteritems(self.__dict__) |
487
|
|
|
if not k.startswith('_')} |
488
|
|
|
|
489
|
|
|
state_dict['_portfolio_store'] = self._portfolio_store |
490
|
|
|
state_dict['_account_store'] = self._account_store |
491
|
|
|
|
492
|
|
|
state_dict['processed_transactions'] = \ |
493
|
|
|
dict(self.processed_transactions) |
494
|
|
|
state_dict['orders_by_id'] = \ |
495
|
|
|
dict(self.orders_by_id) |
496
|
|
|
state_dict['orders_by_modified'] = \ |
497
|
|
|
dict(self.orders_by_modified) |
498
|
|
|
state_dict['_payout_last_sale_prices'] = \ |
499
|
|
|
self._payout_last_sale_prices |
500
|
|
|
|
501
|
|
|
STATE_VERSION = 3 |
502
|
|
|
state_dict[VERSION_LABEL] = STATE_VERSION |
503
|
|
|
return state_dict |
504
|
|
|
|
505
|
|
|
def __setstate__(self, state): |
506
|
|
|
|
507
|
|
|
OLDEST_SUPPORTED_STATE = 3 |
508
|
|
|
version = state.pop(VERSION_LABEL) |
509
|
|
|
|
510
|
|
|
if version < OLDEST_SUPPORTED_STATE: |
511
|
|
|
raise BaseException("PerformancePeriod saved state is too old.") |
512
|
|
|
|
513
|
|
|
processed_transactions = {} |
514
|
|
|
processed_transactions.update(state.pop('processed_transactions')) |
515
|
|
|
|
516
|
|
|
orders_by_id = OrderedDict() |
517
|
|
|
orders_by_id.update(state.pop('orders_by_id')) |
518
|
|
|
|
519
|
|
|
orders_by_modified = {} |
520
|
|
|
orders_by_modified.update(state.pop('orders_by_modified')) |
521
|
|
|
self.processed_transactions = processed_transactions |
522
|
|
|
self.orders_by_id = orders_by_id |
523
|
|
|
self.orders_by_modified = orders_by_modified |
524
|
|
|
|
525
|
|
|
self._execution_cash_flow_multipliers = {} |
526
|
|
|
|
527
|
|
|
self.__dict__.update(state) |
528
|
|
|
|