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

handle_cash_payment()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

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