Completed
Pull Request — master (#867)
by Joe
02:09
created

tests.TradingEnvironmentTestCase.test_max_date()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 7
rs 9.4286
1
#
2
# Copyright 2013 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
from __future__ import division
17
18
import collections
19
from datetime import (
20
    datetime,
21
    timedelta,
22
)
23
import logging
24
import operator
25
26
import unittest
27
from nose_parameterized import parameterized
28
import nose.tools as nt
29
import pytz
30
import itertools
31
32
import pandas as pd
33
import numpy as np
34
from six.moves import range, zip
35
36
import zipline.utils.factory as factory
37
import zipline.finance.performance as perf
38
from zipline.finance.performance import position_tracker
39
from zipline.finance.transaction import Transaction, create_transaction
40
import zipline.utils.math_utils as zp_math
41
42
from zipline.gens.composites import date_sorted_sources
43
from zipline.finance.trading import SimulationParameters
44
from zipline.finance.blotter import Order
45
from zipline.finance.commission import PerShare, PerTrade, PerDollar
46
from zipline.finance.trading import TradingEnvironment
47
from zipline.utils.factory import create_simulation_parameters
48
from zipline.utils.serialization_utils import (
49
    loads_with_persistent_ids, dumps_with_persistent_ids
50
)
51
import zipline.protocol as zp
52
from zipline.protocol import Event, DATASOURCE_TYPE
53
from zipline.sources.data_frame_source import DataPanelSource
54
55
logger = logging.getLogger('Test Perf Tracking')
56
57
onesec = timedelta(seconds=1)
58
oneday = timedelta(days=1)
59
tradingday = timedelta(hours=6, minutes=30)
60
61
# nose.tools changed name in python 3
62
if not hasattr(nt, 'assert_count_equal'):
63
    nt.assert_count_equal = nt.assert_items_equal
64
65
66
def check_perf_period(pp,
67
                      gross_leverage,
68
                      net_leverage,
69
                      long_exposure,
70
                      longs_count,
71
                      short_exposure,
72
                      shorts_count):
73
74
    perf_data = pp.to_dict()
75
    np.testing.assert_allclose(
76
        gross_leverage, perf_data['gross_leverage'], rtol=1e-3)
77
    np.testing.assert_allclose(
78
        net_leverage, perf_data['net_leverage'], rtol=1e-3)
79
    np.testing.assert_allclose(
80
        long_exposure, perf_data['long_exposure'], rtol=1e-3)
81
    np.testing.assert_allclose(
82
        longs_count, perf_data['longs_count'], rtol=1e-3)
83
    np.testing.assert_allclose(
84
        short_exposure, perf_data['short_exposure'], rtol=1e-3)
85
    np.testing.assert_allclose(
86
        shorts_count, perf_data['shorts_count'], rtol=1e-3)
87
88
89
def check_account(account,
90
                  settled_cash,
91
                  equity_with_loan,
92
                  total_positions_value,
93
                  regt_equity,
94
                  available_funds,
95
                  excess_liquidity,
96
                  cushion,
97
                  leverage,
98
                  net_leverage,
99
                  net_liquidation):
100
    # this is a long only portfolio that is only partially invested
101
    # so net and gross leverage are equal.
102
103
    np.testing.assert_allclose(settled_cash,
104
                               account['settled_cash'], rtol=1e-3)
105
    np.testing.assert_allclose(equity_with_loan,
106
                               account['equity_with_loan'], rtol=1e-3)
107
    np.testing.assert_allclose(total_positions_value,
108
                               account['total_positions_value'], rtol=1e-3)
109
    np.testing.assert_allclose(regt_equity,
110
                               account['regt_equity'], rtol=1e-3)
111
    np.testing.assert_allclose(available_funds,
112
                               account['available_funds'], rtol=1e-3)
113
    np.testing.assert_allclose(excess_liquidity,
114
                               account['excess_liquidity'], rtol=1e-3)
115
    np.testing.assert_allclose(cushion,
116
                               account['cushion'], rtol=1e-3)
117
    np.testing.assert_allclose(leverage, account['leverage'], rtol=1e-3)
118
    np.testing.assert_allclose(net_leverage,
119
                               account['net_leverage'], rtol=1e-3)
120
    np.testing.assert_allclose(net_liquidation,
121
                               account['net_liquidation'], rtol=1e-3)
122
123
124
def create_txn(trade_event, price, amount):
125
    """
126
    Create a fake transaction to be filled and processed prior to the execution
127
    of a given trade event.
128
    """
129
    mock_order = Order(trade_event.dt, trade_event.sid, amount, id=None)
130
    return create_transaction(trade_event, mock_order, price, amount)
131
132
133
def benchmark_events_in_range(sim_params, env):
134
    return [
135
        Event({'dt': dt,
136
               'returns': ret,
137
               'type': zp.DATASOURCE_TYPE.BENCHMARK,
138
               # We explicitly rely on the behavior that benchmarks sort before
139
               # any other events.
140
               'source_id': '1Abenchmarks'})
141
        for dt, ret in env.benchmark_returns.iteritems()
142
        if dt.date() >= sim_params.period_start.date() and
143
        dt.date() <= sim_params.period_end.date()
144
    ]
145
146
147
def calculate_results(sim_params,
148
                      env,
149
                      benchmark_events,
150
                      trade_events,
151
                      dividend_events=None,
152
                      splits=None,
153
                      txns=None):
154
    """
155
    Run the given events through a stripped down version of the loop in
156
    AlgorithmSimulator.transform.
157
158
    IMPORTANT NOTE FOR TEST WRITERS/READERS:
159
160
    This loop has some wonky logic for the order of event processing for
161
    datasource types.  This exists mostly to accomodate legacy tests accomodate
162
    existing tests that were making assumptions about how events would be
163
    sorted.
164
165
    In particular:
166
167
        - Dividends passed for a given date are processed PRIOR to any events
168
          for that date.
169
        - Splits passed for a given date are process AFTER any events for that
170
          date.
171
172
    Tests that use this helper should not be considered useful guarantees of
173
    the behavior of AlgorithmSimulator on a stream containing the same events
174
    unless the subgroups have been explicitly re-sorted in this way.
175
    """
176
177
    txns = txns or []
178
    splits = splits or []
179
180
    perf_tracker = perf.PerformanceTracker(sim_params, env)
181
182
    if dividend_events is not None:
183
        dividend_frame = pd.DataFrame(
184
            [
185
                event.to_series(index=zp.DIVIDEND_FIELDS)
186
                for event in dividend_events
187
            ],
188
        )
189
        perf_tracker.update_dividends(dividend_frame)
190
191
    # Raw trades
192
    trade_events = sorted(trade_events, key=lambda ev: (ev.dt, ev.source_id))
193
194
    # Add a benchmark event for each date.
195
    trades_plus_bm = date_sorted_sources(trade_events, benchmark_events)
196
197
    # Filter out benchmark events that are later than the last trade date.
198
    filtered_trades_plus_bm = (filt_event for filt_event in trades_plus_bm
199
                               if filt_event.dt <= trade_events[-1].dt)
200
201
    grouped_trades_plus_bm = itertools.groupby(filtered_trades_plus_bm,
202
                                               lambda x: x.dt)
203
    results = []
204
205
    bm_updated = False
206
    for date, group in grouped_trades_plus_bm:
207
208
        for txn in filter(lambda txn: txn.dt == date, txns):
209
            # Process txns for this date.
210
            perf_tracker.process_transaction(txn)
211
212
        for event in group:
213
214
            if event.type == zp.DATASOURCE_TYPE.TRADE:
215
                perf_tracker.process_trade(event)
216
            elif event.type == zp.DATASOURCE_TYPE.DIVIDEND:
217
                perf_tracker.process_dividend(event)
218
            elif event.type == zp.DATASOURCE_TYPE.BENCHMARK:
219
                perf_tracker.process_benchmark(event)
220
                bm_updated = True
221
            elif event.type == zp.DATASOURCE_TYPE.COMMISSION:
222
                perf_tracker.process_commission(event)
223
224
        for split in filter(lambda split: split.dt == date, splits):
225
            # Process splits for this date.
226
            perf_tracker.process_split(split)
227
228
        if bm_updated:
229
            msg = perf_tracker.handle_market_close_daily()
230
            msg['account'] = perf_tracker.get_account(True)
231
            results.append(msg)
232
            bm_updated = False
233
    return results
234
235
236
def check_perf_tracker_serialization(perf_tracker):
237
    scalar_keys = [
238
        'emission_rate',
239
        'txn_count',
240
        'market_open',
241
        'last_close',
242
        '_dividend_count',
243
        'period_start',
244
        'day_count',
245
        'capital_base',
246
        'market_close',
247
        'saved_dt',
248
        'period_end',
249
        'total_days',
250
    ]
251
252
    p_string = dumps_with_persistent_ids(perf_tracker)
253
254
    test = loads_with_persistent_ids(p_string, env=perf_tracker.env)
255
256
    for k in scalar_keys:
257
        nt.assert_equal(getattr(test, k), getattr(perf_tracker, k), k)
258
259
    perf_periods = (
260
        test.cumulative_performance,
261
        test.todays_performance
262
    )
263
    for period in perf_periods:
264
        nt.assert_true(hasattr(period, '_position_tracker'))
265
266
267
class TestSplitPerformance(unittest.TestCase):
268
    def setUp(self):
269
        self.env = TradingEnvironment()
270
        self.env.write_data(equities_identifiers=[1])
271
        self.sim_params = create_simulation_parameters(num_days=2)
272
        # start with $10,000
273
        self.sim_params.capital_base = 10e3
274
275
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
276
                                                          self.env)
277
278
    def test_split_long_position(self):
279
        events = factory.create_trade_history(
280
            1,
281
            [20, 20],
282
            [100, 100],
283
            oneday,
284
            self.sim_params,
285
            env=self.env
286
        )
287
288
        # set up a long position in sid 1
289
        # 100 shares at $20 apiece = $2000 position
290
        txns = [create_txn(events[0], 20, 100)]
291
292
        # set up a split with ratio 3 occurring at the start of the second
293
        # day.
294
        splits = [
295
            factory.create_split(
296
                1,
297
                3,
298
                events[1].dt,
299
            ),
300
        ]
301
302
        results = calculate_results(self.sim_params, self.env,
303
                                    self.benchmark_events,
304
                                    events, txns=txns, splits=splits)
305
306
        # should have 33 shares (at $60 apiece) and $20 in cash
307
        self.assertEqual(2, len(results))
308
309
        latest_positions = results[1]['daily_perf']['positions']
310
        self.assertEqual(1, len(latest_positions))
311
312
        # check the last position to make sure it's been updated
313
        position = latest_positions[0]
314
315
        self.assertEqual(1, position['sid'])
316
        self.assertEqual(33, position['amount'])
317
        self.assertEqual(60, position['cost_basis'])
318
        self.assertEqual(60, position['last_sale_price'])
319
320
        # since we started with $10000, and we spent $2000 on the
321
        # position, but then got $20 back, we should have $8020
322
        # (or close to it) in cash.
323
324
        # we won't get exactly 8020 because sometimes a split is
325
        # denoted as a ratio like 0.3333, and we lose some digits
326
        # of precision.  thus, make sure we're pretty close.
327
        daily_perf = results[1]['daily_perf']
328
329
        self.assertTrue(
330
            zp_math.tolerant_equals(8020,
331
                                    daily_perf['ending_cash'], 1))
332
333
        # Validate that the account attributes were updated.
334
        account = results[1]['account']
335
        self.assertEqual(float('inf'), account['day_trades_remaining'])
336
        # this is a long only portfolio that is only partially invested
337
        # so net and gross leverage are equal.
338
        np.testing.assert_allclose(0.198, account['leverage'], rtol=1e-3)
339
        np.testing.assert_allclose(0.198, account['net_leverage'], rtol=1e-3)
340
        np.testing.assert_allclose(8020, account['regt_equity'], rtol=1e-3)
341
        self.assertEqual(float('inf'), account['regt_margin'])
342
        np.testing.assert_allclose(8020, account['available_funds'], rtol=1e-3)
343
        self.assertEqual(0, account['maintenance_margin_requirement'])
344
        np.testing.assert_allclose(10000,
345
                                   account['equity_with_loan'], rtol=1e-3)
346
        self.assertEqual(float('inf'), account['buying_power'])
347
        self.assertEqual(0, account['initial_margin_requirement'])
348
        np.testing.assert_allclose(8020, account['excess_liquidity'],
349
                                   rtol=1e-3)
350
        np.testing.assert_allclose(8020, account['settled_cash'], rtol=1e-3)
351
        np.testing.assert_allclose(10000, account['net_liquidation'],
352
                                   rtol=1e-3)
353
        np.testing.assert_allclose(0.802, account['cushion'], rtol=1e-3)
354
        np.testing.assert_allclose(1980, account['total_positions_value'],
355
                                   rtol=1e-3)
356
        self.assertEqual(0, account['accrued_interest'])
357
358
        for i, result in enumerate(results):
359
            for perf_kind in ('daily_perf', 'cumulative_perf'):
360
                perf_result = result[perf_kind]
361
                # prices aren't changing, so pnl and returns should be 0.0
362
                self.assertEqual(0.0, perf_result['pnl'],
363
                                 "day %s %s pnl %s instead of 0.0" %
364
                                 (i, perf_kind, perf_result['pnl']))
365
                self.assertEqual(0.0, perf_result['returns'],
366
                                 "day %s %s returns %s instead of 0.0" %
367
                                 (i, perf_kind, perf_result['returns']))
368
369
370
class TestCommissionEvents(unittest.TestCase):
371
372
    def setUp(self):
373
        self.env = TradingEnvironment()
374
        self.env.write_data(
375
            equities_identifiers=[0, 1, 133]
376
        )
377
        self.sim_params = create_simulation_parameters(num_days=5)
378
379
        logger.info("sim_params: %s" % self.sim_params)
380
381
        self.sim_params.capital_base = 10e3
382
383
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
384
                                                          self.env)
385
386
    def test_commission_event(self):
387
        events = factory.create_trade_history(
388
            1,
389
            [10, 10, 10, 10, 10],
390
            [100, 100, 100, 100, 100],
391
            oneday,
392
            self.sim_params,
393
            env=self.env
394
        )
395
396
        # Test commission models and validate result
397
        # Expected commission amounts:
398
        # PerShare commission:  1.00, 1.00, 1.50 = $3.50
399
        # PerTrade commission:  5.00, 5.00, 5.00 = $15.00
400
        # PerDollar commission: 1.50, 3.00, 4.50 = $9.00
401
        # Total commission = $3.50 + $15.00 + $9.00 = $27.50
402
403
        # Create 3 transactions:  50, 100, 150 shares traded @ $20
404
        transactions = [create_txn(events[0], 20, i)
405
                        for i in [50, 100, 150]]
406
407
        # Create commission models and validate that produce expected
408
        # commissions.
409
        models = [PerShare(cost=0.01, min_trade_cost=1.00),
410
                  PerTrade(cost=5.00),
411
                  PerDollar(cost=0.0015)]
412
        expected_results = [3.50, 15.0, 9.0]
413
414
        for model, expected in zip(models, expected_results):
415
            total_commission = 0
416
            for trade in transactions:
417
                total_commission += model.calculate(trade)[1]
418
            self.assertEqual(total_commission, expected)
419
420
        # Verify that commission events are handled correctly by
421
        # PerformanceTracker.
422
        cash_adj_dt = events[0].dt
423
        cash_adjustment = factory.create_commission(1, 300.0, cash_adj_dt)
424
        events.append(cash_adjustment)
425
426
        # Insert a purchase order.
427
        txns = [create_txn(events[0], 20, 1)]
428
        results = calculate_results(self.sim_params,
429
                                    self.env,
430
                                    self.benchmark_events,
431
                                    events,
432
                                    txns=txns)
433
434
        # Validate that we lost 320 dollars from our cash pool.
435
        self.assertEqual(results[-1]['cumulative_perf']['ending_cash'],
436
                         9680)
437
        # Validate that the cost basis of our position changed.
438
        self.assertEqual(results[-1]['daily_perf']['positions']
439
                         [0]['cost_basis'], 320.0)
440
        # Validate that the account attributes were updated.
441
        account = results[1]['account']
442
        self.assertEqual(float('inf'), account['day_trades_remaining'])
443
        np.testing.assert_allclose(0.001, account['leverage'], rtol=1e-3,
444
                                   atol=1e-4)
445
        np.testing.assert_allclose(9680, account['regt_equity'], rtol=1e-3)
446
        self.assertEqual(float('inf'), account['regt_margin'])
447
        np.testing.assert_allclose(9680, account['available_funds'],
448
                                   rtol=1e-3)
449
        self.assertEqual(0, account['maintenance_margin_requirement'])
450
        np.testing.assert_allclose(9690,
451
                                   account['equity_with_loan'], rtol=1e-3)
452
        self.assertEqual(float('inf'), account['buying_power'])
453
        self.assertEqual(0, account['initial_margin_requirement'])
454
        np.testing.assert_allclose(9680, account['excess_liquidity'],
455
                                   rtol=1e-3)
456
        np.testing.assert_allclose(9680, account['settled_cash'],
457
                                   rtol=1e-3)
458
        np.testing.assert_allclose(9690, account['net_liquidation'],
459
                                   rtol=1e-3)
460
        np.testing.assert_allclose(0.999, account['cushion'], rtol=1e-3)
461
        np.testing.assert_allclose(10, account['total_positions_value'],
462
                                   rtol=1e-3)
463
        self.assertEqual(0, account['accrued_interest'])
464
465
    def test_commission_zero_position(self):
466
        """
467
        Ensure no div-by-zero errors.
468
        """
469
        events = factory.create_trade_history(
470
            1,
471
            [10, 10, 10, 10, 10],
472
            [100, 100, 100, 100, 100],
473
            oneday,
474
            self.sim_params,
475
            env=self.env
476
        )
477
478
        # Buy and sell the same sid so that we have a zero position by the
479
        # time of events[3].
480
        txns = [
481
            create_txn(events[0], 20, 1),
482
            create_txn(events[1], 20, -1),
483
        ]
484
485
        # Add a cash adjustment at the time of event[3].
486
        cash_adj_dt = events[3].dt
487
        cash_adjustment = factory.create_commission(1, 300.0, cash_adj_dt)
488
489
        events.append(cash_adjustment)
490
491
        results = calculate_results(self.sim_params,
492
                                    self.env,
493
                                    self.benchmark_events,
494
                                    events,
495
                                    txns=txns)
496
        # Validate that we lost 300 dollars from our cash pool.
497
        self.assertEqual(results[-1]['cumulative_perf']['ending_cash'],
498
                         9700)
499
500
    def test_commission_no_position(self):
501
        """
502
        Ensure no position-not-found or sid-not-found errors.
503
        """
504
        events = factory.create_trade_history(
505
            1,
506
            [10, 10, 10, 10, 10],
507
            [100, 100, 100, 100, 100],
508
            oneday,
509
            self.sim_params,
510
            env=self.env
511
        )
512
513
        # Add a cash adjustment at the time of event[3].
514
        cash_adj_dt = events[3].dt
515
        cash_adjustment = factory.create_commission(1, 300.0, cash_adj_dt)
516
        events.append(cash_adjustment)
517
518
        results = calculate_results(self.sim_params,
519
                                    self.env,
520
                                    self.benchmark_events,
521
                                    events)
522
        # Validate that we lost 300 dollars from our cash pool.
523
        self.assertEqual(results[-1]['cumulative_perf']['ending_cash'],
524
                         9700)
525
526
527
class TestDividendPerformance(unittest.TestCase):
528
529
    @classmethod
530
    def setUpClass(cls):
531
        cls.env = TradingEnvironment()
532
        cls.env.write_data(equities_identifiers=[1, 2])
533
534
    @classmethod
535
    def tearDownClass(cls):
536
        del cls.env
537
538
    def setUp(self):
539
        self.sim_params = create_simulation_parameters(num_days=6)
540
        self.sim_params.capital_base = 10e3
541
542
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
543
                                                          self.env)
544
545
    def test_market_hours_calculations(self):
546
        # DST in US/Eastern began on Sunday March 14, 2010
547
        before = datetime(2010, 3, 12, 14, 31, tzinfo=pytz.utc)
548
        after = factory.get_next_trading_dt(
549
            before,
550
            timedelta(days=1),
551
            self.env,
552
        )
553
        self.assertEqual(after.hour, 13)
554
555
    def test_long_position_receives_dividend(self):
556
        # post some trades in the market
557
        events = factory.create_trade_history(
558
            1,
559
            [10, 10, 10, 10, 10],
560
            [100, 100, 100, 100, 100],
561
            oneday,
562
            self.sim_params,
563
            env=self.env
564
        )
565
        dividend = factory.create_dividend(
566
            1,
567
            10.00,
568
            # declared date, when the algorithm finds out about
569
            # the dividend
570
            events[0].dt,
571
            # ex_date, the date before which the algorithm must hold stock
572
            # to receive the dividend
573
            events[1].dt,
574
            # pay date, when the algorithm receives the dividend.
575
            events[2].dt
576
        )
577
578
        # Simulate a transaction being filled prior to the ex_date.
579
        txns = [create_txn(events[0], 10.0, 100)]
580
        results = calculate_results(
581
            self.sim_params,
582
            self.env,
583
            self.benchmark_events,
584
            events,
585
            dividend_events=[dividend],
586
            txns=txns,
587
        )
588
589
        self.assertEqual(len(results), 5)
590
        cumulative_returns = \
591
            [event['cumulative_perf']['returns'] for event in results]
592
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.1, 0.1, 0.1])
593
        daily_returns = [event['daily_perf']['returns']
594
                         for event in results]
595
        self.assertEqual(daily_returns, [0.0, 0.0, 0.10, 0.0, 0.0])
596
        cash_flows = [event['daily_perf']['capital_used']
597
                      for event in results]
598
        self.assertEqual(cash_flows, [-1000, 0, 1000, 0, 0])
599
        cumulative_cash_flows = \
600
            [event['cumulative_perf']['capital_used'] for event in results]
601
        self.assertEqual(cumulative_cash_flows, [-1000, -1000, 0, 0, 0])
602
        cash_pos = \
603
            [event['cumulative_perf']['ending_cash'] for event in results]
604
        self.assertEqual(cash_pos, [9000, 9000, 10000, 10000, 10000])
605
606
    def test_long_position_receives_stock_dividend(self):
607
        # post some trades in the market
608
        events = []
609
        for sid in (1, 2):
610
            events.extend(
611
                factory.create_trade_history(
612
                    sid,
613
                    [10, 10, 10, 10, 10],
614
                    [100, 100, 100, 100, 100],
615
                    oneday,
616
                    self.sim_params,
617
                    env=self.env)
618
            )
619
620
        dividend = factory.create_stock_dividend(
621
            1,
622
            payment_sid=2,
623
            ratio=2,
624
            # declared date, when the algorithm finds out about
625
            # the dividend
626
            declared_date=events[0].dt,
627
            # ex_date, the date before which the algorithm must hold stock
628
            # to receive the dividend
629
            ex_date=events[1].dt,
630
            # pay date, when the algorithm receives the dividend.
631
            pay_date=events[2].dt
632
        )
633
634
        txns = [create_txn(events[0], 10.0, 100)]
635
636
        results = calculate_results(
637
            self.sim_params,
638
            self.env,
639
            self.benchmark_events,
640
            events,
641
            dividend_events=[dividend],
642
            txns=txns,
643
        )
644
645
        self.assertEqual(len(results), 5)
646
        cumulative_returns = \
647
            [event['cumulative_perf']['returns'] for event in results]
648
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.2, 0.2, 0.2])
649
        daily_returns = [event['daily_perf']['returns']
650
                         for event in results]
651
        self.assertEqual(daily_returns, [0.0, 0.0, 0.2, 0.0, 0.0])
652
        cash_flows = [event['daily_perf']['capital_used']
653
                      for event in results]
654
        self.assertEqual(cash_flows, [-1000, 0, 0, 0, 0])
655
        cumulative_cash_flows = \
656
            [event['cumulative_perf']['capital_used'] for event in results]
657
        self.assertEqual(cumulative_cash_flows, [-1000] * 5)
658
        cash_pos = \
659
            [event['cumulative_perf']['ending_cash'] for event in results]
660
        self.assertEqual(cash_pos, [9000] * 5)
661
662
    def test_long_position_purchased_on_ex_date_receives_no_dividend(self):
663
        # post some trades in the market
664
        events = factory.create_trade_history(
665
            1,
666
            [10, 10, 10, 10, 10],
667
            [100, 100, 100, 100, 100],
668
            oneday,
669
            self.sim_params,
670
            env=self.env
671
        )
672
673
        dividend = factory.create_dividend(
674
            1,
675
            10.00,
676
            events[0].dt,  # Declared date
677
            events[1].dt,  # Exclusion date
678
            events[2].dt   # Pay date
679
        )
680
681
        # Simulate a transaction being filled on the ex_date.
682
        txns = [create_txn(events[1], 10.0, 100)]
683
684
        results = calculate_results(
685
            self.sim_params,
686
            self.env,
687
            self.benchmark_events,
688
            events,
689
            dividend_events=[dividend],
690
            txns=txns,
691
        )
692
693
        self.assertEqual(len(results), 5)
694
        cumulative_returns = \
695
            [event['cumulative_perf']['returns'] for event in results]
696
        self.assertEqual(cumulative_returns, [0, 0, 0, 0, 0])
697
        daily_returns = [event['daily_perf']['returns'] for event in results]
698
        self.assertEqual(daily_returns, [0, 0, 0, 0, 0])
699
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
700
        self.assertEqual(cash_flows, [0, -1000, 0, 0, 0])
701
        cumulative_cash_flows = \
702
            [event['cumulative_perf']['capital_used'] for event in results]
703
        self.assertEqual(cumulative_cash_flows,
704
                         [0, -1000, -1000, -1000, -1000])
705
706
    def test_selling_before_dividend_payment_still_gets_paid(self):
707
        # post some trades in the market
708
        events = factory.create_trade_history(
709
            1,
710
            [10, 10, 10, 10, 10],
711
            [100, 100, 100, 100, 100],
712
            oneday,
713
            self.sim_params,
714
            env=self.env
715
        )
716
717
        dividend = factory.create_dividend(
718
            1,
719
            10.00,
720
            events[0].dt,  # Declared date
721
            events[1].dt,  # Exclusion date
722
            events[3].dt   # Pay date
723
        )
724
725
        buy_txn = create_txn(events[0], 10.0, 100)
726
        sell_txn = create_txn(events[2], 10.0, -100)
727
        txns = [buy_txn, sell_txn]
728
729
        results = calculate_results(
730
            self.sim_params,
731
            self.env,
732
            self.benchmark_events,
733
            events,
734
            dividend_events=[dividend],
735
            txns=txns,
736
        )
737
738
        self.assertEqual(len(results), 5)
739
        cumulative_returns = \
740
            [event['cumulative_perf']['returns'] for event in results]
741
        self.assertEqual(cumulative_returns, [0, 0, 0, 0.1, 0.1])
742
        daily_returns = [event['daily_perf']['returns'] for event in results]
743
        self.assertEqual(daily_returns, [0, 0, 0, 0.1, 0])
744
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
745
        self.assertEqual(cash_flows, [-1000, 0, 1000, 1000, 0])
746
        cumulative_cash_flows = \
747
            [event['cumulative_perf']['capital_used'] for event in results]
748
        self.assertEqual(cumulative_cash_flows, [-1000, -1000, 0, 1000, 1000])
749
750
    def test_buy_and_sell_before_ex(self):
751
        # post some trades in the market
752
        events = factory.create_trade_history(
753
            1,
754
            [10, 10, 10, 10, 10, 10],
755
            [100, 100, 100, 100, 100, 100],
756
            oneday,
757
            self.sim_params,
758
            env=self.env
759
        )
760
761
        dividend = factory.create_dividend(
762
            1,
763
            10.00,
764
            events[3].dt,
765
            events[4].dt,
766
            events[5].dt
767
        )
768
769
        buy_txn = create_txn(events[1], 10.0, 100)
770
        sell_txn = create_txn(events[2], 10.0, -100)
771
        txns = [buy_txn, sell_txn]
772
773
        results = calculate_results(
774
            self.sim_params,
775
            self.env,
776
            self.benchmark_events,
777
            events,
778
            dividend_events=[dividend],
779
            txns=txns,
780
        )
781
782
        self.assertEqual(len(results), 6)
783
        cumulative_returns = \
784
            [event['cumulative_perf']['returns'] for event in results]
785
        self.assertEqual(cumulative_returns, [0, 0, 0, 0, 0, 0])
786
        daily_returns = [event['daily_perf']['returns'] for event in results]
787
        self.assertEqual(daily_returns, [0, 0, 0, 0, 0, 0])
788
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
789
        self.assertEqual(cash_flows, [0, -1000, 1000, 0, 0, 0])
790
        cumulative_cash_flows = \
791
            [event['cumulative_perf']['capital_used'] for event in results]
792
        self.assertEqual(cumulative_cash_flows, [0, -1000, 0, 0, 0, 0])
793
794
    def test_ending_before_pay_date(self):
795
        # post some trades in the market
796
        events = factory.create_trade_history(
797
            1,
798
            [10, 10, 10, 10, 10],
799
            [100, 100, 100, 100, 100],
800
            oneday,
801
            self.sim_params,
802
            env=self.env
803
        )
804
805
        pay_date = self.sim_params.first_open
806
        # find pay date that is much later.
807
        for i in range(30):
808
            pay_date = factory.get_next_trading_dt(pay_date, oneday, self.env)
809
        dividend = factory.create_dividend(
810
            1,
811
            10.00,
812
            events[0].dt,
813
            events[0].dt,
814
            pay_date
815
        )
816
817
        txns = [create_txn(events[1], 10.0, 100)]
818
819
        results = calculate_results(
820
            self.sim_params,
821
            self.env,
822
            self.benchmark_events,
823
            events,
824
            dividend_events=[dividend],
825
            txns=txns,
826
        )
827
828
        self.assertEqual(len(results), 5)
829
        cumulative_returns = \
830
            [event['cumulative_perf']['returns'] for event in results]
831
        self.assertEqual(cumulative_returns, [0, 0, 0, 0.0, 0.0])
832
        daily_returns = [event['daily_perf']['returns'] for event in results]
833
        self.assertEqual(daily_returns, [0, 0, 0, 0, 0])
834
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
835
        self.assertEqual(cash_flows, [0, -1000, 0, 0, 0])
836
        cumulative_cash_flows = \
837
            [event['cumulative_perf']['capital_used'] for event in results]
838
        self.assertEqual(
839
            cumulative_cash_flows,
840
            [0, -1000, -1000, -1000, -1000]
841
        )
842
843
    def test_short_position_pays_dividend(self):
844
        # post some trades in the market
845
        events = factory.create_trade_history(
846
            1,
847
            [10, 10, 10, 10, 10],
848
            [100, 100, 100, 100, 100],
849
            oneday,
850
            self.sim_params,
851
            env=self.env
852
        )
853
854
        dividend = factory.create_dividend(
855
            1,
856
            10.00,
857
            # declare at open of test
858
            events[0].dt,
859
            # ex_date same as trade 2
860
            events[2].dt,
861
            events[3].dt
862
        )
863
864
        txns = [create_txn(events[1], 10.0, -100)]
865
866
        results = calculate_results(
867
            self.sim_params,
868
            self.env,
869
            self.benchmark_events,
870
            events,
871
            dividend_events=[dividend],
872
            txns=txns,
873
        )
874
875
        self.assertEqual(len(results), 5)
876
        cumulative_returns = \
877
            [event['cumulative_perf']['returns'] for event in results]
878
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, -0.1, -0.1])
879
        daily_returns = [event['daily_perf']['returns'] for event in results]
880
        self.assertEqual(daily_returns, [0.0, 0.0, 0.0, -0.1, 0.0])
881
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
882
        self.assertEqual(cash_flows, [0, 1000, 0, -1000, 0])
883
        cumulative_cash_flows = \
884
            [event['cumulative_perf']['capital_used'] for event in results]
885
        self.assertEqual(cumulative_cash_flows, [0, 1000, 1000, 0, 0])
886
887
    def test_no_position_receives_no_dividend(self):
888
        # post some trades in the market
889
        events = factory.create_trade_history(
890
            1,
891
            [10, 10, 10, 10, 10],
892
            [100, 100, 100, 100, 100],
893
            oneday,
894
            self.sim_params,
895
            env=self.env
896
        )
897
898
        dividend = factory.create_dividend(
899
            1,
900
            10.00,
901
            events[0].dt,
902
            events[1].dt,
903
            events[2].dt
904
        )
905
906
        results = calculate_results(
907
            self.sim_params,
908
            self.env,
909
            self.benchmark_events,
910
            events,
911
            dividend_events=[dividend],
912
        )
913
914
        self.assertEqual(len(results), 5)
915
        cumulative_returns = \
916
            [event['cumulative_perf']['returns'] for event in results]
917
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, 0.0, 0.0])
918
        daily_returns = [event['daily_perf']['returns'] for event in results]
919
        self.assertEqual(daily_returns, [0.0, 0.0, 0.0, 0.0, 0.0])
920
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
921
        self.assertEqual(cash_flows, [0, 0, 0, 0, 0])
922
        cumulative_cash_flows = \
923
            [event['cumulative_perf']['capital_used'] for event in results]
924
        self.assertEqual(cumulative_cash_flows, [0, 0, 0, 0, 0])
925
926
    def test_no_dividend_at_simulation_end(self):
927
        # post some trades in the market
928
        events = factory.create_trade_history(
929
            1,
930
            [10, 10, 10, 10, 10],
931
            [100, 100, 100, 100, 100],
932
            oneday,
933
            self.sim_params,
934
            env=self.env
935
        )
936
        dividend = factory.create_dividend(
937
            1,
938
            10.00,
939
            # declared date, when the algorithm finds out about
940
            # the dividend
941
            events[-3].dt,
942
            # ex_date, the date before which the algorithm must hold stock
943
            # to receive the dividend
944
            events[-2].dt,
945
            # pay date, when the algorithm receives the dividend.
946
            # This pays out on the day after the last event
947
            self.env.next_trading_day(events[-1].dt)
948
        )
949
950
        # Set the last day to be the last event
951
        self.sim_params.period_end = events[-1].dt
952
        self.sim_params.update_internal_from_env(self.env)
953
954
        # Simulate a transaction being filled prior to the ex_date.
955
        txns = [create_txn(events[0], 10.0, 100)]
956
        results = calculate_results(
957
            self.sim_params,
958
            self.env,
959
            self.benchmark_events,
960
            events,
961
            dividend_events=[dividend],
962
            txns=txns,
963
        )
964
965
        self.assertEqual(len(results), 5)
966
        cumulative_returns = \
967
            [event['cumulative_perf']['returns'] for event in results]
968
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, 0.0, 0.0])
969
        daily_returns = [event['daily_perf']['returns'] for event in results]
970
        self.assertEqual(daily_returns, [0.0, 0.0, 0.0, 0.0, 0.0])
971
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
972
        self.assertEqual(cash_flows, [-1000, 0, 0, 0, 0])
973
        cumulative_cash_flows = \
974
            [event['cumulative_perf']['capital_used'] for event in results]
975
        self.assertEqual(cumulative_cash_flows,
976
                         [-1000, -1000, -1000, -1000, -1000])
977
978
979
class TestDividendPerformanceHolidayStyle(TestDividendPerformance):
980
981
    # The holiday tests begins the simulation on the day
982
    # before Thanksgiving, so that the next trading day is
983
    # two days ahead. Any tests that hard code events
984
    # to be start + oneday will fail, since those events will
985
    # be skipped by the simulation.
986
987
    def setUp(self):
988
        self.dt = datetime(2003, 11, 30, tzinfo=pytz.utc)
989
        self.end_dt = datetime(2004, 11, 25, tzinfo=pytz.utc)
990
        self.sim_params = SimulationParameters(
991
            self.dt,
992
            self.end_dt,
993
            env=self.env)
994
995
        self.sim_params.capital_base = 10e3
996
997
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
998
                                                          self.env)
999
1000
1001
class TestPositionPerformance(unittest.TestCase):
1002
1003
    @classmethod
1004
    def setUpClass(cls):
1005
        cls.env = TradingEnvironment()
1006
        cls.env.write_data(equities_identifiers=[1, 2])
1007
1008
    @classmethod
1009
    def tearDownClass(cls):
1010
        del cls.env
1011
1012
    def setUp(self):
1013
        self.sim_params = create_simulation_parameters(num_days=4)
1014
1015
        self.finder = self.env.asset_finder
1016
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
1017
                                                          self.env)
1018
1019
    def test_long_short_positions(self):
1020
        """
1021
        start with $1000
1022
        buy 100 stock1 shares at $10
1023
        sell short 100 stock2 shares at $10
1024
        stock1 then goes down to $9
1025
        stock2 goes to $11
1026
        """
1027
1028
        trades_1 = factory.create_trade_history(
1029
            1,
1030
            [10, 10, 10, 9],
1031
            [100, 100, 100, 100],
1032
            onesec,
1033
            self.sim_params,
1034
            env=self.env
1035
        )
1036
1037
        trades_2 = factory.create_trade_history(
1038
            2,
1039
            [10, 10, 10, 11],
1040
            [100, 100, 100, 100],
1041
            onesec,
1042
            self.sim_params,
1043
            env=self.env
1044
        )
1045
1046
        txn1 = create_txn(trades_1[1], 10.0, 100)
1047
        txn2 = create_txn(trades_2[1], 10.0, -100)
1048
        pt = perf.PositionTracker(self.env.asset_finder)
1049
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1050
        pp.position_tracker = pt
1051
        pt.execute_transaction(txn1)
1052
        pp.handle_execution(txn1)
1053
        pt.execute_transaction(txn2)
1054
        pp.handle_execution(txn2)
1055
1056
        for trade in itertools.chain(trades_1[:-2], trades_2[:-2]):
1057
            pt.update_last_sale(trade)
1058
1059
        pp.calculate_performance()
1060
1061
        check_perf_period(
1062
            pp,
1063
            gross_leverage=2.0,
1064
            net_leverage=0.0,
1065
            long_exposure=1000.0,
1066
            longs_count=1,
1067
            short_exposure=-1000.0,
1068
            shorts_count=1)
1069
        # Validate that the account attributes were updated.
1070
        account = pp.as_account()
1071
        check_account(account,
1072
                      settled_cash=1000.0,
1073
                      equity_with_loan=1000.0,
1074
                      total_positions_value=0.0,
1075
                      regt_equity=1000.0,
1076
                      available_funds=1000.0,
1077
                      excess_liquidity=1000.0,
1078
                      cushion=1.0,
1079
                      leverage=2.0,
1080
                      net_leverage=0.0,
1081
                      net_liquidation=1000.0)
1082
1083
        # now simulate stock1 going to $9
1084
        pt.update_last_sale(trades_1[-1])
1085
        # and stock2 going to $11
1086
        pt.update_last_sale(trades_2[-1])
1087
1088
        pp.calculate_performance()
1089
1090
        # Validate that the account attributes were updated.
1091
        account = pp.as_account()
1092
1093
        check_perf_period(
1094
            pp,
1095
            gross_leverage=2.5,
1096
            net_leverage=-0.25,
1097
            long_exposure=900.0,
1098
            longs_count=1,
1099
            short_exposure=-1100.0,
1100
            shorts_count=1)
1101
1102
        check_account(account,
1103
                      settled_cash=1000.0,
1104
                      equity_with_loan=800.0,
1105
                      total_positions_value=-200.0,
1106
                      regt_equity=1000.0,
1107
                      available_funds=1000.0,
1108
                      excess_liquidity=1000.0,
1109
                      cushion=1.25,
1110
                      leverage=2.5,
1111
                      net_leverage=-0.25,
1112
                      net_liquidation=800.0)
1113
1114
    def test_levered_long_position(self):
1115
        """
1116
            start with $1,000, then buy 1000 shares at $10.
1117
            price goes to $11
1118
        """
1119
        # post some trades in the market
1120
        trades = factory.create_trade_history(
1121
            1,
1122
            [10, 10, 10, 11],
1123
            [100, 100, 100, 100],
1124
            onesec,
1125
            self.sim_params,
1126
            env=self.env
1127
        )
1128
1129
        txn = create_txn(trades[1], 10.0, 1000)
1130
        pt = perf.PositionTracker(self.env.asset_finder)
1131
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1132
        pp.position_tracker = pt
1133
1134
        pt.execute_transaction(txn)
1135
        pp.handle_execution(txn)
1136
1137
        for trade in trades[:-2]:
1138
            pt.update_last_sale(trade)
1139
1140
        pp.calculate_performance()
1141
1142
        check_perf_period(
1143
            pp,
1144
            gross_leverage=10.0,
1145
            net_leverage=10.0,
1146
            long_exposure=10000.0,
1147
            longs_count=1,
1148
            short_exposure=0.0,
1149
            shorts_count=0)
1150
1151
        # Validate that the account attributes were updated.
1152
        account = pp.as_account()
1153
        check_account(account,
1154
                      settled_cash=-9000.0,
1155
                      equity_with_loan=1000.0,
1156
                      total_positions_value=10000.0,
1157
                      regt_equity=-9000.0,
1158
                      available_funds=-9000.0,
1159
                      excess_liquidity=-9000.0,
1160
                      cushion=-9.0,
1161
                      leverage=10.0,
1162
                      net_leverage=10.0,
1163
                      net_liquidation=1000.0)
1164
1165
        # now simulate a price jump to $11
1166
        pt.update_last_sale(trades[-1])
1167
1168
        pp.calculate_performance()
1169
1170
        check_perf_period(
1171
            pp,
1172
            gross_leverage=5.5,
1173
            net_leverage=5.5,
1174
            long_exposure=11000.0,
1175
            longs_count=1,
1176
            short_exposure=0.0,
1177
            shorts_count=0)
1178
1179
        # Validate that the account attributes were updated.
1180
        account = pp.as_account()
1181
1182
        check_account(account,
1183
                      settled_cash=-9000.0,
1184
                      equity_with_loan=2000.0,
1185
                      total_positions_value=11000.0,
1186
                      regt_equity=-9000.0,
1187
                      available_funds=-9000.0,
1188
                      excess_liquidity=-9000.0,
1189
                      cushion=-4.5,
1190
                      leverage=5.5,
1191
                      net_leverage=5.5,
1192
                      net_liquidation=2000.0)
1193
1194
    def test_long_position(self):
1195
        """
1196
            verify that the performance period calculates properly for a
1197
            single buy transaction
1198
        """
1199
        # post some trades in the market
1200
        trades = factory.create_trade_history(
1201
            1,
1202
            [10, 10, 10, 11],
1203
            [100, 100, 100, 100],
1204
            onesec,
1205
            self.sim_params,
1206
            env=self.env
1207
        )
1208
1209
        txn = create_txn(trades[1], 10.0, 100)
1210
        pt = perf.PositionTracker(self.env.asset_finder)
1211
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1212
        pp.position_tracker = pt
1213
1214
        pt.execute_transaction(txn)
1215
        pp.handle_execution(txn)
1216
1217
        # This verifies that the last sale price is being correctly
1218
        # set in the positions. If this is not the case then returns can
1219
        # incorrectly show as sharply dipping if a transaction arrives
1220
        # before a trade. This is caused by returns being based on holding
1221
        # stocks with a last sale price of 0.
1222
        self.assertEqual(pp.positions[1].last_sale_price, 10.0)
1223
1224
        for trade in trades:
1225
            pt.update_last_sale(trade)
1226
1227
        pp.calculate_performance()
1228
1229
        self.assertEqual(
1230
            pp.period_cash_flow,
1231
            -1 * txn.price * txn.amount,
1232
            "capital used should be equal to the opposite of the transaction \
1233
            cost of sole txn in test"
1234
        )
1235
1236
        self.assertEqual(
1237
            len(pp.positions),
1238
            1,
1239
            "should be just one position")
1240
1241
        self.assertEqual(
1242
            pp.positions[1].sid,
1243
            txn.sid,
1244
            "position should be in security with id 1")
1245
1246
        self.assertEqual(
1247
            pp.positions[1].amount,
1248
            txn.amount,
1249
            "should have a position of {sharecount} shares".format(
1250
                sharecount=txn.amount
1251
            )
1252
        )
1253
1254
        self.assertEqual(
1255
            pp.positions[1].cost_basis,
1256
            txn.price,
1257
            "should have a cost basis of 10"
1258
        )
1259
1260
        self.assertEqual(
1261
            pp.positions[1].last_sale_price,
1262
            trades[-1]['price'],
1263
            "last sale should be same as last trade. \
1264
            expected {exp} actual {act}".format(
1265
                exp=trades[-1]['price'],
1266
                act=pp.positions[1].last_sale_price)
1267
        )
1268
1269
        self.assertEqual(
1270
            pp.ending_value,
1271
            1100,
1272
            "ending value should be price of last trade times number of \
1273
            shares in position"
1274
        )
1275
1276
        self.assertEqual(pp.pnl, 100, "gain of 1 on 100 shares should be 100")
1277
1278
        check_perf_period(
1279
            pp,
1280
            gross_leverage=1.0,
1281
            net_leverage=1.0,
1282
            long_exposure=1100.0,
1283
            longs_count=1,
1284
            short_exposure=0.0,
1285
            shorts_count=0)
1286
1287
        # Validate that the account attributes were updated.
1288
        account = pp.as_account()
1289
        check_account(account,
1290
                      settled_cash=0.0,
1291
                      equity_with_loan=1100.0,
1292
                      total_positions_value=1100.0,
1293
                      regt_equity=0.0,
1294
                      available_funds=0.0,
1295
                      excess_liquidity=0.0,
1296
                      cushion=0.0,
1297
                      leverage=1.0,
1298
                      net_leverage=1.0,
1299
                      net_liquidation=1100.0)
1300
1301
    def test_short_position(self):
1302
        """verify that the performance period calculates properly for a \
1303
single short-sale transaction"""
1304
        trades = factory.create_trade_history(
1305
            1,
1306
            [10, 10, 10, 11, 10, 9],
1307
            [100, 100, 100, 100, 100, 100],
1308
            onesec,
1309
            self.sim_params,
1310
            env=self.env
1311
        )
1312
1313
        trades_1 = trades[:-2]
1314
1315
        txn = create_txn(trades[1], 10.0, -100)
1316
        pt = perf.PositionTracker(self.env.asset_finder)
1317
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1318
        pp.position_tracker = pt
1319
1320
        pt.execute_transaction(txn)
1321
        pp.handle_execution(txn)
1322
        for trade in trades_1:
1323
            pt.update_last_sale(trade)
1324
1325
        pp.calculate_performance()
1326
1327
        self.assertEqual(
1328
            pp.period_cash_flow,
1329
            -1 * txn.price * txn.amount,
1330
            "capital used should be equal to the opposite of the transaction\
1331
             cost of sole txn in test"
1332
        )
1333
1334
        self.assertEqual(
1335
            len(pp.positions),
1336
            1,
1337
            "should be just one position")
1338
1339
        self.assertEqual(
1340
            pp.positions[1].sid,
1341
            txn.sid,
1342
            "position should be in security from the transaction"
1343
        )
1344
1345
        self.assertEqual(
1346
            pp.positions[1].amount,
1347
            -100,
1348
            "should have a position of -100 shares"
1349
        )
1350
1351
        self.assertEqual(
1352
            pp.positions[1].cost_basis,
1353
            txn.price,
1354
            "should have a cost basis of 10"
1355
        )
1356
1357
        self.assertEqual(
1358
            pp.positions[1].last_sale_price,
1359
            trades_1[-1]['price'],
1360
            "last sale should be price of last trade"
1361
        )
1362
1363
        self.assertEqual(
1364
            pp.ending_value,
1365
            -1100,
1366
            "ending value should be price of last trade times number of \
1367
            shares in position"
1368
        )
1369
1370
        self.assertEqual(pp.pnl, -100, "gain of 1 on 100 shares should be 100")
1371
1372
        # simulate additional trades, and ensure that the position value
1373
        # reflects the new price
1374
        trades_2 = trades[-2:]
1375
1376
        # simulate a rollover to a new period
1377
        pp.rollover()
1378
1379
        for trade in trades_2:
1380
            pt.update_last_sale(trade)
1381
1382
        pp.calculate_performance()
1383
1384
        self.assertEqual(
1385
            pp.period_cash_flow,
1386
            0,
1387
            "capital used should be zero, there were no transactions in \
1388
            performance period"
1389
        )
1390
1391
        self.assertEqual(
1392
            len(pp.positions),
1393
            1,
1394
            "should be just one position"
1395
        )
1396
1397
        self.assertEqual(
1398
            pp.positions[1].sid,
1399
            txn.sid,
1400
            "position should be in security from the transaction"
1401
        )
1402
1403
        self.assertEqual(
1404
            pp.positions[1].amount,
1405
            -100,
1406
            "should have a position of -100 shares"
1407
        )
1408
1409
        self.assertEqual(
1410
            pp.positions[1].cost_basis,
1411
            txn.price,
1412
            "should have a cost basis of 10"
1413
        )
1414
1415
        self.assertEqual(
1416
            pp.positions[1].last_sale_price,
1417
            trades_2[-1].price,
1418
            "last sale should be price of last trade"
1419
        )
1420
1421
        self.assertEqual(
1422
            pp.ending_value,
1423
            -900,
1424
            "ending value should be price of last trade times number of \
1425
            shares in position")
1426
1427
        self.assertEqual(
1428
            pp.pnl,
1429
            200,
1430
            "drop of 2 on -100 shares should be 200"
1431
        )
1432
1433
        # now run a performance period encompassing the entire trade sample.
1434
        ptTotal = perf.PositionTracker(self.env.asset_finder)
1435
        ppTotal = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1436
        ppTotal.position_tracker = pt
1437
1438
        for trade in trades_1:
1439
            ptTotal.update_last_sale(trade)
1440
1441
        ptTotal.execute_transaction(txn)
1442
        ppTotal.handle_execution(txn)
1443
1444
        for trade in trades_2:
1445
            ptTotal.update_last_sale(trade)
1446
1447
        ppTotal.calculate_performance()
1448
1449
        self.assertEqual(
1450
            ppTotal.period_cash_flow,
1451
            -1 * txn.price * txn.amount,
1452
            "capital used should be equal to the opposite of the transaction \
1453
cost of sole txn in test"
1454
        )
1455
1456
        self.assertEqual(
1457
            len(ppTotal.positions),
1458
            1,
1459
            "should be just one position"
1460
        )
1461
        self.assertEqual(
1462
            ppTotal.positions[1].sid,
1463
            txn.sid,
1464
            "position should be in security from the transaction"
1465
        )
1466
1467
        self.assertEqual(
1468
            ppTotal.positions[1].amount,
1469
            -100,
1470
            "should have a position of -100 shares"
1471
        )
1472
1473
        self.assertEqual(
1474
            ppTotal.positions[1].cost_basis,
1475
            txn.price,
1476
            "should have a cost basis of 10"
1477
        )
1478
1479
        self.assertEqual(
1480
            ppTotal.positions[1].last_sale_price,
1481
            trades_2[-1].price,
1482
            "last sale should be price of last trade"
1483
        )
1484
1485
        self.assertEqual(
1486
            ppTotal.ending_value,
1487
            -900,
1488
            "ending value should be price of last trade times number of \
1489
            shares in position")
1490
1491
        self.assertEqual(
1492
            ppTotal.pnl,
1493
            100,
1494
            "drop of 1 on -100 shares should be 100"
1495
        )
1496
1497
        check_perf_period(
1498
            pp,
1499
            gross_leverage=0.8181,
1500
            net_leverage=-0.8181,
1501
            long_exposure=0.0,
1502
            longs_count=0,
1503
            short_exposure=-900.0,
1504
            shorts_count=1)
1505
1506
        # Validate that the account attributes.
1507
        account = ppTotal.as_account()
1508
        check_account(account,
1509
                      settled_cash=2000.0,
1510
                      equity_with_loan=1100.0,
1511
                      total_positions_value=-900.0,
1512
                      regt_equity=2000.0,
1513
                      available_funds=2000.0,
1514
                      excess_liquidity=2000.0,
1515
                      cushion=1.8181,
1516
                      leverage=0.8181,
1517
                      net_leverage=-0.8181,
1518
                      net_liquidation=1100.0)
1519
1520
    def test_covering_short(self):
1521
        """verify performance where short is bought and covered, and shares \
1522
trade after cover"""
1523
1524
        trades = factory.create_trade_history(
1525
            1,
1526
            [10, 10, 10, 11, 9, 8, 7, 8, 9, 10],
1527
            [100, 100, 100, 100, 100, 100, 100, 100, 100, 100],
1528
            onesec,
1529
            self.sim_params,
1530
            env=self.env
1531
        )
1532
1533
        short_txn = create_txn(
1534
            trades[1],
1535
            10.0,
1536
            -100,
1537
        )
1538
1539
        cover_txn = create_txn(trades[6], 7.0, 100)
1540
        pt = perf.PositionTracker(self.env.asset_finder)
1541
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1542
        pp.position_tracker = pt
1543
1544
        pt.execute_transaction(short_txn)
1545
        pp.handle_execution(short_txn)
1546
        pt.execute_transaction(cover_txn)
1547
        pp.handle_execution(cover_txn)
1548
1549
        for trade in trades:
1550
            pt.update_last_sale(trade)
1551
1552
        pp.calculate_performance()
1553
1554
        short_txn_cost = short_txn.price * short_txn.amount
1555
        cover_txn_cost = cover_txn.price * cover_txn.amount
1556
1557
        self.assertEqual(
1558
            pp.period_cash_flow,
1559
            -1 * short_txn_cost - cover_txn_cost,
1560
            "capital used should be equal to the net transaction costs"
1561
        )
1562
1563
        self.assertEqual(
1564
            len(pp.positions),
1565
            1,
1566
            "should be just one position"
1567
        )
1568
1569
        self.assertEqual(
1570
            pp.positions[1].sid,
1571
            short_txn.sid,
1572
            "position should be in security from the transaction"
1573
        )
1574
1575
        self.assertEqual(
1576
            pp.positions[1].amount,
1577
            0,
1578
            "should have a position of -100 shares"
1579
        )
1580
1581
        self.assertEqual(
1582
            pp.positions[1].cost_basis,
1583
            0,
1584
            "a covered position should have a cost basis of 0"
1585
        )
1586
1587
        self.assertEqual(
1588
            pp.positions[1].last_sale_price,
1589
            trades[-1].price,
1590
            "last sale should be price of last trade"
1591
        )
1592
1593
        self.assertEqual(
1594
            pp.ending_value,
1595
            0,
1596
            "ending value should be price of last trade times number of \
1597
shares in position"
1598
        )
1599
1600
        self.assertEqual(
1601
            pp.pnl,
1602
            300,
1603
            "gain of 1 on 100 shares should be 300"
1604
        )
1605
1606
        check_perf_period(
1607
            pp,
1608
            gross_leverage=0.0,
1609
            net_leverage=0.0,
1610
            long_exposure=0.0,
1611
            longs_count=0,
1612
            short_exposure=0.0,
1613
            shorts_count=0)
1614
1615
        account = pp.as_account()
1616
        check_account(account,
1617
                      settled_cash=1300.0,
1618
                      equity_with_loan=1300.0,
1619
                      total_positions_value=0.0,
1620
                      regt_equity=1300.0,
1621
                      available_funds=1300.0,
1622
                      excess_liquidity=1300.0,
1623
                      cushion=1.0,
1624
                      leverage=0.0,
1625
                      net_leverage=0.0,
1626
                      net_liquidation=1300.0)
1627
1628
    def test_cost_basis_calc(self):
1629
        history_args = (
1630
            1,
1631
            [10, 11, 11, 12],
1632
            [100, 100, 100, 100],
1633
            onesec,
1634
            self.sim_params,
1635
            self.env
1636
        )
1637
        trades = factory.create_trade_history(*history_args)
1638
        transactions = factory.create_txn_history(*history_args)
1639
1640
        pt = perf.PositionTracker(self.env.asset_finder)
1641
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1642
        pp.position_tracker = pt
1643
1644
        average_cost = 0
1645
        for i, txn in enumerate(transactions):
1646
            pt.execute_transaction(txn)
1647
            pp.handle_execution(txn)
1648
            average_cost = (average_cost * i + txn.price) / (i + 1)
1649
            self.assertEqual(pp.positions[1].cost_basis, average_cost)
1650
1651
        for trade in trades:
1652
            pt.update_last_sale(trade)
1653
1654
        pp.calculate_performance()
1655
1656
        self.assertEqual(
1657
            pp.positions[1].last_sale_price,
1658
            trades[-1].price,
1659
            "should have a last sale of 12, got {val}".format(
1660
                val=pp.positions[1].last_sale_price)
1661
        )
1662
1663
        self.assertEqual(
1664
            pp.positions[1].cost_basis,
1665
            11,
1666
            "should have a cost basis of 11"
1667
        )
1668
1669
        self.assertEqual(
1670
            pp.pnl,
1671
            400
1672
        )
1673
1674
        down_tick = factory.create_trade(
1675
            1,
1676
            10.0,
1677
            100,
1678
            trades[-1].dt + onesec)
1679
1680
        sale_txn = create_txn(
1681
            down_tick,
1682
            10.0,
1683
            -100)
1684
1685
        pp.rollover()
1686
1687
        pt.execute_transaction(sale_txn)
1688
        pp.handle_execution(sale_txn)
1689
        pt.update_last_sale(down_tick)
1690
1691
        pp.calculate_performance()
1692
        self.assertEqual(
1693
            pp.positions[1].last_sale_price,
1694
            10,
1695
            "should have a last sale of 10, was {val}".format(
1696
                val=pp.positions[1].last_sale_price)
1697
        )
1698
1699
        self.assertEqual(
1700
            pp.positions[1].cost_basis,
1701
            11,
1702
            "should have a cost basis of 11"
1703
        )
1704
1705
        self.assertEqual(pp.pnl, -800, "this period goes from +400 to -400")
1706
1707
        pt3 = perf.PositionTracker(self.env.asset_finder)
1708
        pp3 = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1709
        pp3.position_tracker = pt3
1710
1711
        average_cost = 0
1712
        for i, txn in enumerate(transactions):
1713
            pt3.execute_transaction(txn)
1714
            pp3.handle_execution(txn)
1715
            average_cost = (average_cost * i + txn.price) / (i + 1)
1716
            self.assertEqual(pp3.positions[1].cost_basis, average_cost)
1717
1718
        pt3.execute_transaction(sale_txn)
1719
        pp3.handle_execution(sale_txn)
1720
1721
        trades.append(down_tick)
1722
        for trade in trades:
1723
            pt3.update_last_sale(trade)
1724
1725
        pp3.calculate_performance()
1726
        self.assertEqual(
1727
            pp3.positions[1].last_sale_price,
1728
            10,
1729
            "should have a last sale of 10"
1730
        )
1731
1732
        self.assertEqual(
1733
            pp3.positions[1].cost_basis,
1734
            11,
1735
            "should have a cost basis of 11"
1736
        )
1737
1738
        self.assertEqual(
1739
            pp3.pnl,
1740
            -400,
1741
            "should be -400 for all trades and transactions in period"
1742
        )
1743
1744
    def test_cost_basis_calc_close_pos(self):
1745
        history_args = (
1746
            1,
1747
            [10, 9, 11, 8, 9, 12, 13, 14],
1748
            [200, -100, -100, 100, -300, 100, 500, 400],
1749
            onesec,
1750
            self.sim_params,
1751
            self.env
1752
        )
1753
        cost_bases = [10, 10, 0, 8, 9, 9, 13, 13.5]
1754
1755
        trades = factory.create_trade_history(*history_args)
1756
        transactions = factory.create_txn_history(*history_args)
1757
1758
        pt = perf.PositionTracker(self.env.asset_finder)
1759
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1760
        pp.position_tracker = pt
1761
1762
        for txn, cb in zip(transactions, cost_bases):
1763
            pt.execute_transaction(txn)
1764
            pp.handle_execution(txn)
1765
            self.assertEqual(pp.positions[1].cost_basis, cb)
1766
1767
        for trade in trades:
1768
            pt.update_last_sale(trade)
1769
1770
        pp.calculate_performance()
1771
1772
        self.assertEqual(pp.positions[1].cost_basis, cost_bases[-1])
1773
1774
1775
class TestPerformanceTracker(unittest.TestCase):
1776
1777
    @classmethod
1778
    def setUpClass(cls):
1779
        cls.env = TradingEnvironment()
1780
        cls.env.write_data(equities_identifiers=[1, 2, 133, 134])
1781
1782
    @classmethod
1783
    def tearDownClass(cls):
1784
        del cls.env
1785
1786
    NumDaysToDelete = collections.namedtuple(
1787
        'NumDaysToDelete', ('start', 'middle', 'end'))
1788
1789
    @parameterized.expand([
1790
        ("Don't delete any events",
1791
         NumDaysToDelete(start=0, middle=0, end=0)),
1792
        ("Delete first day of events",
1793
         NumDaysToDelete(start=1, middle=0, end=0)),
1794
        ("Delete first two days of events",
1795
         NumDaysToDelete(start=2, middle=0, end=0)),
1796
        ("Delete one day of events from the middle",
1797
         NumDaysToDelete(start=0, middle=1, end=0)),
1798
        ("Delete two events from the middle",
1799
         NumDaysToDelete(start=0, middle=2, end=0)),
1800
        ("Delete last day of events",
1801
         NumDaysToDelete(start=0, middle=0, end=1)),
1802
        ("Delete last two days of events",
1803
         NumDaysToDelete(start=0, middle=0, end=2)),
1804
        ("Delete all but one event.",
1805
         NumDaysToDelete(start=2, middle=1, end=2)),
1806
    ])
1807
    def test_tracker(self, parameter_comment, days_to_delete):
1808
        """
1809
        @days_to_delete - configures which days in the data set we should
1810
        remove, used for ensuring that we still return performance messages
1811
        even when there is no data.
1812
        """
1813
        # This date range covers Columbus day,
1814
        # however Columbus day is not a market holiday
1815
        #
1816
        #     October 2008
1817
        # Su Mo Tu We Th Fr Sa
1818
        #           1  2  3  4
1819
        #  5  6  7  8  9 10 11
1820
        # 12 13 14 15 16 17 18
1821
        # 19 20 21 22 23 24 25
1822
        # 26 27 28 29 30 31
1823
        start_dt = datetime(year=2008,
1824
                            month=10,
1825
                            day=9,
1826
                            tzinfo=pytz.utc)
1827
        end_dt = datetime(year=2008,
1828
                          month=10,
1829
                          day=16,
1830
                          tzinfo=pytz.utc)
1831
1832
        trade_count = 6
1833
        sid = 133
1834
        price = 10.1
1835
        price_list = [price] * trade_count
1836
        volume = [100] * trade_count
1837
        trade_time_increment = timedelta(days=1)
1838
1839
        sim_params = SimulationParameters(
1840
            period_start=start_dt,
1841
            period_end=end_dt,
1842
            env=self.env,
1843
        )
1844
1845
        benchmark_events = benchmark_events_in_range(sim_params, self.env)
1846
1847
        trade_history = factory.create_trade_history(
1848
            sid,
1849
            price_list,
1850
            volume,
1851
            trade_time_increment,
1852
            sim_params,
1853
            source_id="factory1",
1854
            env=self.env
1855
        )
1856
1857
        sid2 = 134
1858
        price2 = 12.12
1859
        price2_list = [price2] * trade_count
1860
        trade_history2 = factory.create_trade_history(
1861
            sid2,
1862
            price2_list,
1863
            volume,
1864
            trade_time_increment,
1865
            sim_params,
1866
            source_id="factory2",
1867
            env=self.env
1868
        )
1869
        # 'middle' start of 3 depends on number of days == 7
1870
        middle = 3
1871
1872
        # First delete from middle
1873
        if days_to_delete.middle:
1874
            del trade_history[middle:(middle + days_to_delete.middle)]
1875
            del trade_history2[middle:(middle + days_to_delete.middle)]
1876
1877
        # Delete start
1878
        if days_to_delete.start:
1879
            del trade_history[:days_to_delete.start]
1880
            del trade_history2[:days_to_delete.start]
1881
1882
        # Delete from end
1883
        if days_to_delete.end:
1884
            del trade_history[-days_to_delete.end:]
1885
            del trade_history2[-days_to_delete.end:]
1886
1887
        sim_params.capital_base = 1000.0
1888
        sim_params.frame_index = [
1889
            'sid',
1890
            'volume',
1891
            'dt',
1892
            'price',
1893
            'changed']
1894
        perf_tracker = perf.PerformanceTracker(
1895
            sim_params, self.env
1896
        )
1897
1898
        events = date_sorted_sources(trade_history, trade_history2)
1899
1900
        events = [event for event in
1901
                  self.trades_with_txns(events, trade_history[0].dt)]
1902
1903
        # Extract events with transactions to use for verification.
1904
        txns = [event for event in
1905
                events if event.type == zp.DATASOURCE_TYPE.TRANSACTION]
1906
1907
        orders = [event for event in
1908
                  events if event.type == zp.DATASOURCE_TYPE.ORDER]
1909
1910
        all_events = date_sorted_sources(events, benchmark_events)
1911
1912
        filtered_events = [filt_event for filt_event
1913
                           in all_events if filt_event.dt <= end_dt]
1914
        filtered_events.sort(key=lambda x: x.dt)
1915
        grouped_events = itertools.groupby(filtered_events, lambda x: x.dt)
1916
        perf_messages = []
1917
1918
        for date, group in grouped_events:
1919
            for event in group:
1920
                if event.type == zp.DATASOURCE_TYPE.TRADE:
1921
                    perf_tracker.process_trade(event)
1922
                elif event.type == zp.DATASOURCE_TYPE.ORDER:
1923
                    perf_tracker.process_order(event)
1924
                elif event.type == zp.DATASOURCE_TYPE.BENCHMARK:
1925
                    perf_tracker.process_benchmark(event)
1926
                elif event.type == zp.DATASOURCE_TYPE.TRANSACTION:
1927
                    perf_tracker.process_transaction(event)
1928
            msg = perf_tracker.handle_market_close_daily()
1929
            perf_messages.append(msg)
1930
1931
        self.assertEqual(perf_tracker.txn_count, len(txns))
1932
        self.assertEqual(perf_tracker.txn_count, len(orders))
1933
1934
        positions = perf_tracker.cumulative_performance.positions
1935
        if len(txns) == 0:
1936
            self.assertNotIn(sid, positions)
1937
        else:
1938
            expected_size = len(txns) / 2 * -25
1939
            cumulative_pos = positions[sid]
1940
            self.assertEqual(cumulative_pos.amount, expected_size)
1941
1942
            self.assertEqual(len(perf_messages),
1943
                             sim_params.days_in_period)
1944
1945
        check_perf_tracker_serialization(perf_tracker)
1946
1947
    def trades_with_txns(self, events, no_txn_dt):
1948
        for event in events:
1949
1950
            # create a transaction for all but
1951
            # first trade in each sid, to simulate None transaction
1952
            if event.dt != no_txn_dt:
1953
                order = Order(
1954
                    sid=event.sid,
1955
                    amount=-25,
1956
                    dt=event.dt
1957
                )
1958
                order.source_id = 'MockOrderSource'
1959
                yield order
1960
                yield event
1961
                txn = Transaction(
1962
                    sid=event.sid,
1963
                    amount=-25,
1964
                    dt=event.dt,
1965
                    price=10.0,
1966
                    commission=0.50,
1967
                    order_id=order.id
1968
                )
1969
                txn.source_id = 'MockTransactionSource'
1970
                yield txn
1971
            else:
1972
                yield event
1973
1974
    def test_minute_tracker(self):
1975
        """ Tests minute performance tracking."""
1976
        start_dt = self.env.exchange_dt_in_utc(datetime(2013, 3, 1, 9, 31))
1977
        end_dt = self.env.exchange_dt_in_utc(datetime(2013, 3, 1, 16, 0))
1978
1979
        foosid = 1
1980
        barsid = 2
1981
1982
        sim_params = SimulationParameters(
1983
            period_start=start_dt,
1984
            period_end=end_dt,
1985
            emission_rate='minute',
1986
            env=self.env,
1987
        )
1988
        tracker = perf.PerformanceTracker(sim_params, env=self.env)
1989
1990
        foo_event_1 = factory.create_trade(foosid, 10.0, 20, start_dt)
1991
        order_event_1 = Order(sid=foo_event_1.sid,
1992
                              amount=-25,
1993
                              dt=foo_event_1.dt)
1994
        bar_event_1 = factory.create_trade(barsid, 100.0, 200, start_dt)
1995
        txn_event_1 = Transaction(sid=foo_event_1.sid,
1996
                                  amount=-25,
1997
                                  dt=foo_event_1.dt,
1998
                                  price=10.0,
1999
                                  commission=0.50,
2000
                                  order_id=order_event_1.id)
2001
        benchmark_event_1 = Event({
2002
            'dt': start_dt,
2003
            'returns': 0.01,
2004
            'type': zp.DATASOURCE_TYPE.BENCHMARK
2005
        })
2006
2007
        foo_event_2 = factory.create_trade(
2008
            foosid, 11.0, 20, start_dt + timedelta(minutes=1))
2009
        bar_event_2 = factory.create_trade(
2010
            barsid, 11.0, 20, start_dt + timedelta(minutes=1))
2011
        benchmark_event_2 = Event({
2012
            'dt': start_dt + timedelta(minutes=1),
2013
            'returns': 0.02,
2014
            'type': zp.DATASOURCE_TYPE.BENCHMARK
2015
        })
2016
2017
        events = [
2018
            foo_event_1,
2019
            order_event_1,
2020
            benchmark_event_1,
2021
            txn_event_1,
2022
            bar_event_1,
2023
            foo_event_2,
2024
            benchmark_event_2,
2025
            bar_event_2,
2026
        ]
2027
2028
        grouped_events = itertools.groupby(
2029
            events, operator.attrgetter('dt'))
2030
2031
        messages = {}
2032
        for date, group in grouped_events:
2033
            tracker.set_date(date)
2034
            for event in group:
2035
                if event.type == zp.DATASOURCE_TYPE.TRADE:
2036
                    tracker.process_trade(event)
2037
                elif event.type == zp.DATASOURCE_TYPE.BENCHMARK:
2038
                    tracker.process_benchmark(event)
2039
                elif event.type == zp.DATASOURCE_TYPE.ORDER:
2040
                    tracker.process_order(event)
2041
                elif event.type == zp.DATASOURCE_TYPE.TRANSACTION:
2042
                    tracker.process_transaction(event)
2043
            msg, _ = tracker.handle_minute_close(date)
2044
            messages[date] = msg
2045
2046
        self.assertEquals(2, len(messages))
2047
2048
        msg_1 = messages[foo_event_1.dt]
2049
        msg_2 = messages[foo_event_2.dt]
2050
2051
        self.assertEquals(1, len(msg_1['minute_perf']['transactions']),
2052
                          "The first message should contain one "
2053
                          "transaction.")
2054
        # Check that transactions aren't emitted for previous events.
2055
        self.assertEquals(0, len(msg_2['minute_perf']['transactions']),
2056
                          "The second message should have no "
2057
                          "transactions.")
2058
2059
        self.assertEquals(1, len(msg_1['minute_perf']['orders']),
2060
                          "The first message should contain one orders.")
2061
        # Check that orders aren't emitted for previous events.
2062
        self.assertEquals(0, len(msg_2['minute_perf']['orders']),
2063
                          "The second message should have no orders.")
2064
2065
        # Ensure that period_close moves through time.
2066
        # Also, ensure that the period_closes are the expected dts.
2067
        self.assertEquals(foo_event_1.dt,
2068
                          msg_1['minute_perf']['period_close'])
2069
        self.assertEquals(foo_event_2.dt,
2070
                          msg_2['minute_perf']['period_close'])
2071
2072
        # In this test event1 transactions arrive on the first bar.
2073
        # This leads to no returns as the price is constant.
2074
        # Sharpe ratio cannot be computed and is None.
2075
        # In the second bar we can start establishing a sharpe ratio.
2076
        self.assertIsNone(msg_1['cumulative_risk_metrics']['sharpe'])
2077
        self.assertIsNotNone(msg_2['cumulative_risk_metrics']['sharpe'])
2078
2079
        check_perf_tracker_serialization(tracker)
2080
2081
    def test_close_position_event(self):
2082
        pt = perf.PositionTracker(asset_finder=self.env.asset_finder)
2083
        dt = pd.Timestamp("1984/03/06 3:00PM")
2084
        pos1 = perf.Position(1, amount=np.float64(120.0),
2085
                             last_sale_date=dt, last_sale_price=3.4)
2086
        pos2 = perf.Position(2, amount=np.float64(-100.0),
2087
                             last_sale_date=dt, last_sale_price=3.4)
2088
        pt.update_positions({1: pos1, 2: pos2})
2089
2090
        event_type = DATASOURCE_TYPE.CLOSE_POSITION
2091
        index = [dt + timedelta(days=1)]
2092
        pan = pd.Panel({1: pd.DataFrame({'price': 1, 'volume': 0,
2093
                                         'type': event_type}, index=index),
2094
                        2: pd.DataFrame({'price': 1, 'volume': 0,
2095
                                         'type': event_type}, index=index),
2096
                        3: pd.DataFrame({'price': 1, 'volume': 0,
2097
                                         'type': event_type}, index=index)})
2098
2099
        source = DataPanelSource(pan)
2100
        for i, event in enumerate(source):
2101
            txn = pt.maybe_create_close_position_transaction(event)
2102
            if event.sid == 1:
2103
                # Test owned long
2104
                self.assertEqual(-120, txn.amount)
2105
            elif event.sid == 2:
2106
                # Test owned short
2107
                self.assertEqual(100, txn.amount)
2108
            elif event.sid == 3:
2109
                # Test not-owned SID
2110
                self.assertIsNone(txn)
2111
2112
    def test_handle_sid_removed_from_universe(self):
2113
        # post some trades in the market
2114
        sim_params = create_simulation_parameters(num_days=5)
2115
        events = factory.create_trade_history(
2116
            1,
2117
            [10, 10, 10, 10, 10],
2118
            [100, 100, 100, 100, 100],
2119
            oneday,
2120
            sim_params,
2121
            env=self.env
2122
        )
2123
2124
        # Create a tracker and a dividend
2125
        perf_tracker = perf.PerformanceTracker(sim_params, env=self.env)
2126
        dividend = factory.create_dividend(
2127
            1,
2128
            10.00,
2129
            # declared date, when the algorithm finds out about
2130
            # the dividend
2131
            events[0].dt,
2132
            # ex_date, the date before which the algorithm must hold stock
2133
            # to receive the dividend
2134
            events[1].dt,
2135
            # pay date, when the algorithm receives the dividend.
2136
            events[2].dt
2137
        )
2138
        dividend_frame = pd.DataFrame(
2139
            [dividend.to_series(index=zp.DIVIDEND_FIELDS)],
2140
        )
2141
        perf_tracker.update_dividends(dividend_frame)
2142
2143
        # Ensure that the dividend is in the tracker
2144
        self.assertIn(1, perf_tracker.dividend_frame['sid'].values)
2145
2146
        # Inform the tracker that sid 1 has been removed from the universe
2147
        perf_tracker.handle_sid_removed_from_universe(1)
2148
2149
        # Ensure that the dividend for sid 1 has been removed from dividend
2150
        # frame
2151
        self.assertNotIn(1, perf_tracker.dividend_frame['sid'].values)
2152
2153
    def test_serialization(self):
2154
        start_dt = datetime(year=2008,
2155
                            month=10,
2156
                            day=9,
2157
                            tzinfo=pytz.utc)
2158
        end_dt = datetime(year=2008,
2159
                          month=10,
2160
                          day=16,
2161
                          tzinfo=pytz.utc)
2162
2163
        sim_params = SimulationParameters(
2164
            period_start=start_dt,
2165
            period_end=end_dt,
2166
            env=self.env,
2167
        )
2168
2169
        perf_tracker = perf.PerformanceTracker(
2170
            sim_params, env=self.env
2171
        )
2172
        check_perf_tracker_serialization(perf_tracker)
2173
2174
2175
class TestPosition(unittest.TestCase):
2176
    def setUp(self):
2177
        pass
2178
2179
    def test_serialization(self):
2180
        dt = pd.Timestamp("1984/03/06 3:00PM")
2181
        pos = perf.Position(10, amount=np.float64(120.0), last_sale_date=dt,
2182
                            last_sale_price=3.4)
2183
2184
        p_string = dumps_with_persistent_ids(pos)
2185
2186
        test = loads_with_persistent_ids(p_string, env=None)
2187
        nt.assert_dict_equal(test.__dict__, pos.__dict__)
2188
2189
2190
class TestPositionTracker(unittest.TestCase):
2191
2192
    @classmethod
2193
    def setUpClass(cls):
2194
        cls.env = TradingEnvironment()
2195
        futures_metadata = {3: {'contract_multiplier': 1000},
2196
                            4: {'contract_multiplier': 1000}}
2197
        cls.env.write_data(equities_identifiers=[1, 2],
2198
                           futures_data=futures_metadata)
2199
2200
    @classmethod
2201
    def tearDownClass(cls):
2202
        del cls.env
2203
2204
    def test_empty_positions(self):
2205
        """
2206
        make sure all the empty position stats return a numeric 0
2207
2208
        Originally this bug was due to np.dot([], []) returning
2209
        np.bool_(False)
2210
        """
2211
        pt = perf.PositionTracker(self.env.asset_finder)
2212
        pos_stats = position_tracker.calc_position_stats(pt)
2213
2214
        stats = [
2215
            'net_value',
2216
            'net_exposure',
2217
            'gross_value',
2218
            'gross_exposure',
2219
            'short_value',
2220
            'short_exposure',
2221
            'shorts_count',
2222
            'long_value',
2223
            'long_exposure',
2224
            'longs_count',
2225
        ]
2226
        for name in stats:
2227
            val = getattr(pos_stats, name)
2228
            self.assertEquals(val, 0)
2229
            self.assertNotIsInstance(val, (bool, np.bool_))
2230
2231
    def test_update_last_sale(self):
2232
        pt = perf.PositionTracker(self.env.asset_finder)
2233
        dt = pd.Timestamp("1984/03/06 3:00PM")
2234
        pos1 = perf.Position(1, amount=np.float64(100.0),
2235
                             last_sale_date=dt, last_sale_price=10)
2236
        pos3 = perf.Position(3, amount=np.float64(100.0),
2237
                             last_sale_date=dt, last_sale_price=10)
2238
        pt.update_positions({1: pos1, 3: pos3})
2239
2240
        event1 = Event({'sid': 1,
2241
                        'price': 11,
2242
                        'dt': dt})
2243
        event3 = Event({'sid': 3,
2244
                        'price': 11,
2245
                        'dt': dt})
2246
2247
        # Check cash-adjustment return value
2248
        self.assertEqual(0, pt.update_last_sale(event1))
2249
        self.assertEqual(100000, pt.update_last_sale(event3))
2250
2251
    def test_position_values_and_exposures(self):
2252
        pt = perf.PositionTracker(self.env.asset_finder)
2253
        dt = pd.Timestamp("1984/03/06 3:00PM")
2254
        pos1 = perf.Position(1, amount=np.float64(10.0),
2255
                             last_sale_date=dt, last_sale_price=10)
2256
        pos2 = perf.Position(2, amount=np.float64(-20.0),
2257
                             last_sale_date=dt, last_sale_price=10)
2258
        pos3 = perf.Position(3, amount=np.float64(30.0),
2259
                             last_sale_date=dt, last_sale_price=10)
2260
        pos4 = perf.Position(4, amount=np.float64(-40.0),
2261
                             last_sale_date=dt, last_sale_price=10)
2262
        pt.update_positions({1: pos1, 2: pos2, 3: pos3, 4: pos4})
2263
2264
        # Test long-only methods
2265
2266
        pos_stats = position_tracker.calc_position_stats(pt)
2267
        self.assertEqual(100, pos_stats.long_value)
2268
        self.assertEqual(100 + 300000, pos_stats.long_exposure)
2269
        self.assertEqual(2, pos_stats.longs_count)
2270
2271
        # Test short-only methods
2272
        self.assertEqual(-200, pos_stats.short_value)
2273
        self.assertEqual(-200 - 400000, pos_stats.short_exposure)
2274
        self.assertEqual(2, pos_stats.shorts_count)
2275
2276
        # Test gross and net values
2277
        self.assertEqual(100 + 200, pos_stats.gross_value)
2278
        self.assertEqual(100 - 200, pos_stats.net_value)
2279
2280
        # Test gross and net exposures
2281
        self.assertEqual(100 + 200 + 300000 + 400000, pos_stats.gross_exposure)
2282
        self.assertEqual(100 - 200 + 300000 - 400000, pos_stats.net_exposure)
2283
2284
    def test_serialization(self):
2285
        pt = perf.PositionTracker(self.env.asset_finder)
2286
        dt = pd.Timestamp("1984/03/06 3:00PM")
2287
        pos1 = perf.Position(1, amount=np.float64(120.0),
2288
                             last_sale_date=dt, last_sale_price=3.4)
2289
        pos3 = perf.Position(3, amount=np.float64(100.0),
2290
                             last_sale_date=dt, last_sale_price=3.4)
2291
2292
        pt.update_positions({1: pos1, 3: pos3})
2293
        p_string = dumps_with_persistent_ids(pt)
2294
        test = loads_with_persistent_ids(p_string, env=self.env)
2295
        nt.assert_count_equal(test.positions.keys(), pt.positions.keys())
2296
        for sid in pt.positions:
2297
            nt.assert_dict_equal(test.positions[sid].__dict__,
2298
                                 pt.positions[sid].__dict__)
2299
2300
2301
class TestPerformancePeriod(unittest.TestCase):
2302
2303
    def test_serialization(self):
2304
        env = TradingEnvironment()
2305
        pt = perf.PositionTracker(env.asset_finder)
2306
        pp = perf.PerformancePeriod(100, env.asset_finder)
2307
        pp.position_tracker = pt
2308
2309
        p_string = dumps_with_persistent_ids(pp)
2310
        test = loads_with_persistent_ids(p_string, env=env)
2311
2312
        correct = pp.__dict__.copy()
2313
        del correct['_position_tracker']
2314
2315
        nt.assert_count_equal(test.__dict__.keys(), correct.keys())
2316
2317
        equal_keys = list(correct.keys())
2318
        equal_keys.remove('_account_store')
2319
        equal_keys.remove('_portfolio_store')
2320
2321
        for k in equal_keys:
2322
            nt.assert_equal(test.__dict__[k], correct[k])
2323