Completed
Push — master ( ab3318...80a553 )
by Eddie
02:19
created

positions()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 3
rs 10
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 + payout
256
        total_at_end = self.ending_cash + self.ending_value
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