Completed
Pull Request — master (#858)
by Eddie
01:43
created

as_account()   B

Complexity

Conditions 1

Size

Total Lines 44

Duplication

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