Completed
Pull Request — master (#938)
by Eddie
03:26
created

handle_execution()   B

Complexity

Conditions 6

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 6
dl 0
loc 24
rs 7.6129
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