Completed
Push — master ( 6106cb...5ecb27 )
by Eddie
01:29
created

check_upcoming_dividends()   B

Complexity

Conditions 4

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 4
dl 0
loc 38
rs 8.5806
1
#
2
# Copyright 2015 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 Tracking
19
====================
20
21
    +-----------------+----------------------------------------------------+
22
    | key             | value                                              |
23
    +=================+====================================================+
24
    | period_start    | The beginning of the period to be tracked. datetime|
25
    |                 | in pytz.utc timezone. Will always be 0:00 on the   |
26
    |                 | date in UTC. The fact that the time may be on the  |
27
    |                 | prior day in the exchange's local time is ignored  |
28
    +-----------------+----------------------------------------------------+
29
    | period_end      | The end of the period to be tracked. datetime      |
30
    |                 | in pytz.utc timezone. Will always be 23:59 on the  |
31
    |                 | date in UTC. The fact that the time may be on the  |
32
    |                 | next day in the exchange's local time is ignored   |
33
    +-----------------+----------------------------------------------------+
34
    | progress        | percentage of test completed                       |
35
    +-----------------+----------------------------------------------------+
36
    | capital_base    | The initial capital assumed for this tracker.      |
37
    +-----------------+----------------------------------------------------+
38
    | cumulative_perf | A dictionary representing the cumulative           |
39
    |                 | performance through all the events delivered to    |
40
    |                 | this tracker. For details see the comments on      |
41
    |                 | :py:meth:`PerformancePeriod.to_dict`               |
42
    +-----------------+----------------------------------------------------+
43
    | todays_perf     | A dictionary representing the cumulative           |
44
    |                 | performance through all the events delivered to    |
45
    |                 | this tracker with datetime stamps between last_open|
46
    |                 | and last_close. For details see the comments on    |
47
    |                 | :py:meth:`PerformancePeriod.to_dict`               |
48
    |                 | TODO: adding this because we calculate it. May be  |
49
    |                 | overkill.                                          |
50
    +-----------------+----------------------------------------------------+
51
    | cumulative_risk | A dictionary representing the risk metrics         |
52
    | _metrics        | calculated based on the positions aggregated       |
53
    |                 | through all the events delivered to this tracker.  |
54
    |                 | For details look at the comments for               |
55
    |                 | :py:meth:`zipline.finance.risk.RiskMetrics.to_dict`|
56
    +-----------------+----------------------------------------------------+
57
58
"""
59
60
from __future__ import division
61
import logbook
62
import pickle
63
from six import iteritems
64
from datetime import datetime
65
66
import numpy as np
67
import pandas as pd
68
from pandas.tseries.tools import normalize_date
69
70
import zipline.finance.risk as risk
71
from . period import PerformancePeriod
72
73
from zipline.utils.serialization_utils import (
74
    VERSION_LABEL
75
)
76
from . position_tracker import PositionTracker
77
78
log = logbook.Logger('Performance')
79
80
81
class PerformanceTracker(object):
82
    """
83
    Tracks the performance of the algorithm.
84
    """
85
    def __init__(self, sim_params, env):
86
87
        self.sim_params = sim_params
88
        self.env = env
89
90
        self.period_start = self.sim_params.period_start
91
        self.period_end = self.sim_params.period_end
92
        self.last_close = self.sim_params.last_close
93
        first_open = self.sim_params.first_open.tz_convert(
94
            self.env.exchange_tz
95
        )
96
        self.day = pd.Timestamp(datetime(first_open.year, first_open.month,
97
                                         first_open.day), tz='UTC')
98
        self.market_open, self.market_close = env.get_open_and_close(self.day)
99
        self.total_days = self.sim_params.days_in_period
100
        self.capital_base = self.sim_params.capital_base
101
        self.emission_rate = sim_params.emission_rate
102
103
        all_trading_days = env.trading_days
104
        mask = ((all_trading_days >= normalize_date(self.period_start)) &
105
                (all_trading_days <= normalize_date(self.period_end)))
106
107
        self.trading_days = all_trading_days[mask]
108
109
        self.dividend_frame = pd.DataFrame()
110
        self._dividend_count = 0
111
112
        self.position_tracker = PositionTracker(asset_finder=env.asset_finder)
113
114
        if self.emission_rate == 'daily':
115
            self.all_benchmark_returns = pd.Series(
116
                index=self.trading_days)
117
            self.cumulative_risk_metrics = \
118
                risk.RiskMetricsCumulative(self.sim_params, self.env)
119
120
        elif self.emission_rate == 'minute':
121
            self.all_benchmark_returns = pd.Series(index=pd.date_range(
122
                self.sim_params.first_open, self.sim_params.last_close,
123
                freq='Min'))
124
125
            self.cumulative_risk_metrics = \
126
                risk.RiskMetricsCumulative(self.sim_params, self.env,
127
                                           create_first_day_stats=True)
128
129
        # this performance period will span the entire simulation from
130
        # inception.
131
        self.cumulative_performance = PerformancePeriod(
132
            # initial cash is your capital base.
133
            starting_cash=self.capital_base,
134
            # the cumulative period will be calculated over the entire test.
135
            period_open=self.period_start,
136
            period_close=self.period_end,
137
            # don't save the transactions for the cumulative
138
            # period
139
            keep_transactions=False,
140
            keep_orders=False,
141
            # don't serialize positions for cumualtive period
142
            serialize_positions=False,
143
            asset_finder=self.env.asset_finder,
144
        )
145
        self.cumulative_performance.position_tracker = self.position_tracker
146
147
        # this performance period will span just the current market day
148
        self.todays_performance = PerformancePeriod(
149
            # initial cash is your capital base.
150
            starting_cash=self.capital_base,
151
            # the daily period will be calculated for the market day
152
            period_open=self.market_open,
153
            period_close=self.market_close,
154
            keep_transactions=True,
155
            keep_orders=True,
156
            serialize_positions=True,
157
            asset_finder=self.env.asset_finder,
158
        )
159
        self.todays_performance.position_tracker = self.position_tracker
160
161
        self.saved_dt = self.period_start
162
        # one indexed so that we reach 100%
163
        self.day_count = 0.0
164
        self.txn_count = 0
165
166
        self.account_needs_update = True
167
        self._account = None
168
169
    def __repr__(self):
170
        return "%s(%r)" % (
171
            self.__class__.__name__,
172
            {'simulation parameters': self.sim_params})
173
174
    @property
175
    def progress(self):
176
        if self.emission_rate == 'minute':
177
            # Fake a value
178
            return 1.0
179
        elif self.emission_rate == 'daily':
180
            return self.day_count / self.total_days
181
182
    def set_date(self, date):
183
        if self.emission_rate == 'minute':
184
            self.saved_dt = date
185
            self.todays_performance.period_close = self.saved_dt
186
187
    def update_dividends(self, new_dividends):
188
        """
189
        Update our dividend frame with new dividends.  @new_dividends should be
190
        a DataFrame with columns containing at least the entries in
191
        zipline.protocol.DIVIDEND_FIELDS.
192
        """
193
194
        # Mark each new dividend with a unique integer id.  This ensures that
195
        # we can differentiate dividends whose date/sid fields are otherwise
196
        # identical.
197
        new_dividends['id'] = np.arange(
198
            self._dividend_count,
199
            self._dividend_count + len(new_dividends),
200
        )
201
        self._dividend_count += len(new_dividends)
202
203
        self.dividend_frame = pd.concat(
204
            [self.dividend_frame, new_dividends]
205
        ).sort(['pay_date', 'ex_date']).set_index('id', drop=False)
206
207
    def initialize_dividends_from_other(self, other):
208
        """
209
        Helper for copying dividends to a new PerformanceTracker while
210
        preserving dividend count.  Useful if a simulation needs to create a
211
        new PerformanceTracker mid-stream and wants to preserve stored dividend
212
        info.
213
214
        Note that this does not copy unpaid dividends.
215
        """
216
        self.dividend_frame = other.dividend_frame
217
        self._dividend_count = other._dividend_count
218
219
    def handle_sid_removed_from_universe(self, sid):
220
        """
221
        This method handles any behaviors that must occur when a SID leaves the
222
        universe of the TradingAlgorithm.
223
224
        Parameters
225
        __________
226
        sid : int
227
            The sid of the Asset being removed from the universe.
228
        """
229
230
        # Drop any dividends for the sid from the dividends frame
231
        self.dividend_frame = self.dividend_frame[
232
            self.dividend_frame.sid != sid
233
        ]
234
235
    def update_performance(self):
236
        # calculate performance as of last trade
237
        self.cumulative_performance.calculate_performance()
238
        self.todays_performance.calculate_performance()
239
240
    def get_portfolio(self, performance_needs_update):
241
        if performance_needs_update:
242
            self.update_performance()
243
            self.account_needs_update = True
244
        return self.cumulative_performance.as_portfolio()
245
246
    def get_account(self, performance_needs_update):
247
        if performance_needs_update:
248
            self.update_performance()
249
            self.account_needs_update = True
250
        if self.account_needs_update:
251
            self._update_account()
252
        return self._account
253
254
    def _update_account(self):
255
        self._account = self.cumulative_performance.as_account()
256
        self.account_needs_update = False
257
258
    def to_dict(self, emission_type=None):
259
        """
260
        Creates a dictionary representing the state of this tracker.
261
        Returns a dict object of the form described in header comments.
262
        """
263
264
        # Default to the emission rate of this tracker if no type is provided
265
        if emission_type is None:
266
            emission_type = self.emission_rate
267
268
        _dict = {
269
            'period_start': self.period_start,
270
            'period_end': self.period_end,
271
            'capital_base': self.capital_base,
272
            'cumulative_perf': self.cumulative_performance.to_dict(),
273
            'progress': self.progress,
274
            'cumulative_risk_metrics': self.cumulative_risk_metrics.to_dict()
275
        }
276
        if emission_type == 'daily':
277
            _dict['daily_perf'] = self.todays_performance.to_dict()
278
        elif emission_type == 'minute':
279
            _dict['minute_perf'] = self.todays_performance.to_dict(
280
                self.saved_dt)
281
        else:
282
            raise ValueError("Invalid emission type: %s" % emission_type)
283
284
        return _dict
285
286
    def _handle_event_price(self, event):
287
        # updates last sale, and pays out a cash adjustment if applicable
288
        cash_adjustment = self.position_tracker.update_last_sale(event)
289
        if cash_adjustment != 0:
290
            self.cumulative_performance.handle_cash_payment(cash_adjustment)
291
            self.todays_performance.handle_cash_payment(cash_adjustment)
292
293
    def process_trade(self, event):
294
        self._handle_event_price(event)
295
296
    def process_transaction(self, event):
297
        self._handle_event_price(event)
298
        self.txn_count += 1
299
        self.position_tracker.execute_transaction(event)
300
        self.cumulative_performance.handle_execution(event)
301
        self.todays_performance.handle_execution(event)
302
303
    def process_dividend(self, dividend):
304
305
        log.info("Ignoring DIVIDEND event.")
306
307
    def process_split(self, event):
308
        leftover_cash = self.position_tracker.handle_split(event)
309
        if leftover_cash > 0:
310
            self.cumulative_performance.handle_cash_payment(leftover_cash)
311
            self.todays_performance.handle_cash_payment(leftover_cash)
312
313
    def process_order(self, event):
314
        self.cumulative_performance.record_order(event)
315
        self.todays_performance.record_order(event)
316
317
    def process_commission(self, event):
318
319
        self.position_tracker.handle_commission(event)
320
        self.cumulative_performance.handle_commission(event)
321
        self.todays_performance.handle_commission(event)
322
323
    def process_benchmark(self, event):
324
        if self.sim_params.data_frequency == 'minute' and \
325
           self.sim_params.emission_rate == 'daily':
326
            # Minute data benchmarks should have a timestamp of market
327
            # close, so that calculations are triggered at the right time.
328
            # However, risk module uses midnight as the 'day'
329
            # marker for returns, so adjust back to midnight.
330
            midnight = pd.tseries.tools.normalize_date(event.dt)
331
        else:
332
            midnight = event.dt
333
334
        if midnight not in self.all_benchmark_returns.index:
335
            raise AssertionError(
336
                ("Date %s not allocated in all_benchmark_returns. "
337
                 "Calendar seems to mismatch with benchmark. "
338
                 "Benchmark container is=%s" %
339
                 (midnight,
340
                  self.all_benchmark_returns.index)))
341
342
        self.all_benchmark_returns[midnight] = event.returns
343
344
    def process_close_position(self, event):
345
346
        # CLOSE_POSITION events that contain prices that must be handled as
347
        # a final trade event
348
        if 'price' in event:
349
            self.process_trade(event)
350
351
        txn = self.position_tracker.\
352
            maybe_create_close_position_transaction(event)
353
        if txn:
354
            self.process_transaction(txn)
355
356
    def check_upcoming_dividends(self, next_trading_day):
357
        """
358
        Check if we currently own any stocks with dividends whose ex_date is
359
        the next trading day.  Track how much we should be payed on those
360
        dividends' pay dates.
361
362
        Then check if we are owed cash/stock for any dividends whose pay date
363
        is the next trading day.  Apply all such benefits, then recalculate
364
        performance.
365
        """
366
        if len(self.dividend_frame) == 0:
367
            # We don't currently know about any dividends for this simulation
368
            # period, so bail.
369
            return
370
371
        # Dividends whose ex_date is the next trading day.  We need to check if
372
        # we own any of these stocks so we know to pay them out when the pay
373
        # date comes.
374
        ex_date_mask = (self.dividend_frame['ex_date'] == next_trading_day)
375
        dividends_earnable = self.dividend_frame[ex_date_mask]
376
377
        # Dividends whose pay date is the next trading day.  If we held any of
378
        # these stocks on midnight before the ex_date, we need to pay these out
379
        # now.
380
        pay_date_mask = (self.dividend_frame['pay_date'] == next_trading_day)
381
        dividends_payable = self.dividend_frame[pay_date_mask]
382
383
        position_tracker = self.position_tracker
384
        if len(dividends_earnable):
385
            position_tracker.earn_dividends(dividends_earnable)
386
387
        if not len(dividends_payable):
388
            return
389
390
        net_cash_payment = position_tracker.pay_dividends(dividends_payable)
391
392
        self.cumulative_performance.handle_dividends_paid(net_cash_payment)
393
        self.todays_performance.handle_dividends_paid(net_cash_payment)
394
395
    def check_asset_auto_closes(self, next_trading_day):
396
        """
397
        Check if the position tracker currently owns any Assets with an
398
        auto-close date that is the next trading day.  Close those positions.
399
400
        Parameters
401
        ----------
402
        next_trading_day : pandas.Timestamp
403
            The next trading day of the simulation
404
        """
405
        auto_close_events = self.position_tracker.auto_close_position_events(
406
            next_trading_day=next_trading_day
407
        )
408
        for event in auto_close_events:
409
            self.process_close_position(event)
410
411
    def handle_minute_close(self, dt):
412
        """
413
        Handles the close of the given minute. This includes handling
414
        market-close functions if the given minute is the end of the market
415
        day.
416
417
        Parameters
418
        __________
419
        dt : Timestamp
420
            The minute that is ending
421
422
        Returns
423
        _______
424
        (dict, dict/None)
425
            A tuple of the minute perf packet and daily perf packet.
426
            If the market day has not ended, the daily perf packet is None.
427
        """
428
        self.update_performance()
429
        todays_date = normalize_date(dt)
430
        account = self.get_account(False)
431
432
        bench_returns = self.all_benchmark_returns.loc[todays_date:dt]
433
        # cumulative returns
434
        bench_since_open = (1. + bench_returns).prod() - 1
435
436
        self.cumulative_risk_metrics.update(todays_date,
437
                                            self.todays_performance.returns,
438
                                            bench_since_open,
439
                                            account)
440
441
        minute_packet = self.to_dict(emission_type='minute')
442
443
        # if this is the close, update dividends for the next day.
444
        # Return the performance tuple
445
        if dt == self.market_close:
446
            return (minute_packet, self._handle_market_close(todays_date))
447
        else:
448
            return (minute_packet, None)
449
450
    def handle_market_close_daily(self):
451
        """
452
        Function called after handle_data when running with daily emission
453
        rate.
454
        """
455
        self.update_performance()
456
        completed_date = self.day
457
        account = self.get_account(False)
458
459
        # update risk metrics for cumulative performance
460
        self.cumulative_risk_metrics.update(
461
            completed_date,
462
            self.todays_performance.returns,
463
            self.all_benchmark_returns[completed_date],
464
            account)
465
466
        return self._handle_market_close(completed_date)
467
468
    def _handle_market_close(self, completed_date):
469
470
        # increment the day counter before we move markers forward.
471
        self.day_count += 1.0
472
473
        # Get the next trading day and, if it is past the bounds of this
474
        # simulation, return the daily perf packet
475
        next_trading_day = self.env.next_trading_day(completed_date)
476
477
        # Check if any assets need to be auto-closed before generating today's
478
        # perf period
479
        if next_trading_day:
480
            self.check_asset_auto_closes(next_trading_day=next_trading_day)
481
482
        # Take a snapshot of our current performance to return to the
483
        # browser.
484
        daily_update = self.to_dict(emission_type='daily')
485
486
        # On the last day of the test, don't create tomorrow's performance
487
        # period.  We may not be able to find the next trading day if we're at
488
        # the end of our historical data
489
        if self.market_close >= self.last_close:
490
            return daily_update
491
492
        # move the market day markers forward
493
        self.market_open, self.market_close = \
494
            self.env.next_open_and_close(self.day)
495
        self.day = self.env.next_trading_day(self.day)
496
497
        # Roll over positions to current day.
498
        self.todays_performance.rollover()
499
        self.todays_performance.period_open = self.market_open
500
        self.todays_performance.period_close = self.market_close
501
502
        # If the next trading day is irrelevant, then return the daily packet
503
        if (next_trading_day is None) or (next_trading_day >= self.last_close):
504
            return daily_update
505
506
        # Check for any dividends and auto-closes, then return the daily perf
507
        # packet
508
        self.check_upcoming_dividends(next_trading_day=next_trading_day)
509
        return daily_update
510
511
    def handle_simulation_end(self):
512
        """
513
        When the simulation is complete, run the full period risk report
514
        and send it out on the results socket.
515
        """
516
517
        log_msg = "Simulated {n} trading days out of {m}."
518
        log.info(log_msg.format(n=int(self.day_count), m=self.total_days))
519
        log.info("first open: {d}".format(
520
            d=self.sim_params.first_open))
521
        log.info("last close: {d}".format(
522
            d=self.sim_params.last_close))
523
524
        bms = pd.Series(
525
            index=self.cumulative_risk_metrics.cont_index,
526
            data=self.cumulative_risk_metrics.benchmark_returns_cont)
527
        ars = pd.Series(
528
            index=self.cumulative_risk_metrics.cont_index,
529
            data=self.cumulative_risk_metrics.algorithm_returns_cont)
530
        acl = self.cumulative_risk_metrics.algorithm_cumulative_leverages
531
        self.risk_report = risk.RiskReport(
532
            ars,
533
            self.sim_params,
534
            benchmark_returns=bms,
535
            algorithm_leverages=acl,
536
            env=self.env)
537
538
        risk_dict = self.risk_report.to_dict()
539
        return risk_dict
540
541
    def __getstate__(self):
542
        state_dict = \
543
            {k: v for k, v in iteritems(self.__dict__)
544
                if not k.startswith('_')}
545
546
        state_dict['dividend_frame'] = pickle.dumps(self.dividend_frame)
547
548
        state_dict['_dividend_count'] = self._dividend_count
549
550
        STATE_VERSION = 4
551
        state_dict[VERSION_LABEL] = STATE_VERSION
552
553
        return state_dict
554
555
    def __setstate__(self, state):
556
557
        OLDEST_SUPPORTED_STATE = 4
558
        version = state.pop(VERSION_LABEL)
559
560
        if version < OLDEST_SUPPORTED_STATE:
561
            raise BaseException("PerformanceTracker saved state is too old.")
562
563
        self.__dict__.update(state)
564
565
        # Handle the dividend frame specially
566
        self.dividend_frame = pickle.loads(state['dividend_frame'])
567
568
        # properly setup the perf periods
569
        p_types = ['cumulative', 'todays']
570
        for p_type in p_types:
571
            name = p_type + '_performance'
572
            period = getattr(self, name, None)
573
            if period is None:
574
                continue
575
            period._position_tracker = self.position_tracker
576