tests.TestSplitPerformance   A
last analyzed

Complexity

Total Complexity 4

Size/Duplication

Total Lines 101
Duplicated Lines 0 %
Metric Value
dl 0
loc 101
rs 10
wmc 4

2 Methods

Rating   Name   Duplication   Size   Complexity  
A setUp() 0 9 1
B test_split_long_position() 0 90 3
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.transaction import Transaction, create_transaction
39
import zipline.utils.math_utils as zp_math
40
41
from zipline.gens.composites import date_sorted_sources
42
from zipline.finance.trading import SimulationParameters
43
from zipline.finance.blotter import Order
44
from zipline.finance.commission import PerShare, PerTrade, PerDollar
45
from zipline.finance.trading import TradingEnvironment
46
from zipline.utils.factory import create_simulation_parameters
47
from zipline.utils.serialization_utils import (
48
    loads_with_persistent_ids, dumps_with_persistent_ids
49
)
50
import zipline.protocol as zp
51
from zipline.protocol import Event, DATASOURCE_TYPE
52
from zipline.sources.data_frame_source import DataPanelSource
53
54
logger = logging.getLogger('Test Perf Tracking')
55
56
onesec = timedelta(seconds=1)
57
oneday = timedelta(days=1)
58
tradingday = timedelta(hours=6, minutes=30)
59
60
# nose.tools changed name in python 3
61
if not hasattr(nt, 'assert_count_equal'):
62
    nt.assert_count_equal = nt.assert_items_equal
63
64
65
def check_perf_period(pp,
66
                      gross_leverage,
67
                      net_leverage,
68
                      long_exposure,
69
                      longs_count,
70
                      short_exposure,
71
                      shorts_count):
72
73
    perf_data = pp.to_dict()
74
    np.testing.assert_allclose(
75
        gross_leverage, perf_data['gross_leverage'], rtol=1e-3)
76
    np.testing.assert_allclose(
77
        net_leverage, perf_data['net_leverage'], rtol=1e-3)
78
    np.testing.assert_allclose(
79
        long_exposure, perf_data['long_exposure'], rtol=1e-3)
80
    np.testing.assert_allclose(
81
        longs_count, perf_data['longs_count'], rtol=1e-3)
82
    np.testing.assert_allclose(
83
        short_exposure, perf_data['short_exposure'], rtol=1e-3)
84
    np.testing.assert_allclose(
85
        shorts_count, perf_data['shorts_count'], rtol=1e-3)
86
87
88
def check_account(account,
89
                  settled_cash,
90
                  equity_with_loan,
91
                  total_positions_value,
92
                  regt_equity,
93
                  available_funds,
94
                  excess_liquidity,
95
                  cushion,
96
                  leverage,
97
                  net_leverage,
98
                  net_liquidation):
99
    # this is a long only portfolio that is only partially invested
100
    # so net and gross leverage are equal.
101
102
    np.testing.assert_allclose(settled_cash,
103
                               account['settled_cash'], rtol=1e-3)
104
    np.testing.assert_allclose(equity_with_loan,
105
                               account['equity_with_loan'], rtol=1e-3)
106
    np.testing.assert_allclose(total_positions_value,
107
                               account['total_positions_value'], rtol=1e-3)
108
    np.testing.assert_allclose(regt_equity,
109
                               account['regt_equity'], rtol=1e-3)
110
    np.testing.assert_allclose(available_funds,
111
                               account['available_funds'], rtol=1e-3)
112
    np.testing.assert_allclose(excess_liquidity,
113
                               account['excess_liquidity'], rtol=1e-3)
114
    np.testing.assert_allclose(cushion,
115
                               account['cushion'], rtol=1e-3)
116
    np.testing.assert_allclose(leverage, account['leverage'], rtol=1e-3)
117
    np.testing.assert_allclose(net_leverage,
118
                               account['net_leverage'], rtol=1e-3)
119
    np.testing.assert_allclose(net_liquidation,
120
                               account['net_liquidation'], rtol=1e-3)
121
122
123
def create_txn(trade_event, price, amount):
124
    """
125
    Create a fake transaction to be filled and processed prior to the execution
126
    of a given trade event.
127
    """
128
    mock_order = Order(trade_event.dt, trade_event.sid, amount, id=None)
129
    return create_transaction(trade_event, mock_order, price, amount)
130
131
132
def benchmark_events_in_range(sim_params, env):
133
    return [
134
        Event({'dt': dt,
135
               'returns': ret,
136
               'type': zp.DATASOURCE_TYPE.BENCHMARK,
137
               # We explicitly rely on the behavior that benchmarks sort before
138
               # any other events.
139
               'source_id': '1Abenchmarks'})
140
        for dt, ret in env.benchmark_returns.iteritems()
141
        if dt.date() >= sim_params.period_start.date() and
142
        dt.date() <= sim_params.period_end.date()
143
    ]
144
145
146
def calculate_results(sim_params,
147
                      env,
148
                      benchmark_events,
149
                      trade_events,
150
                      dividend_events=None,
151
                      splits=None,
152
                      txns=None):
153
    """
154
    Run the given events through a stripped down version of the loop in
155
    AlgorithmSimulator.transform.
156
157
    IMPORTANT NOTE FOR TEST WRITERS/READERS:
158
159
    This loop has some wonky logic for the order of event processing for
160
    datasource types.  This exists mostly to accomodate legacy tests accomodate
161
    existing tests that were making assumptions about how events would be
162
    sorted.
163
164
    In particular:
165
166
        - Dividends passed for a given date are processed PRIOR to any events
167
          for that date.
168
        - Splits passed for a given date are process AFTER any events for that
169
          date.
170
171
    Tests that use this helper should not be considered useful guarantees of
172
    the behavior of AlgorithmSimulator on a stream containing the same events
173
    unless the subgroups have been explicitly re-sorted in this way.
174
    """
175
176
    txns = txns or []
177
    splits = splits or []
178
179
    perf_tracker = perf.PerformanceTracker(sim_params, env)
180
181
    if dividend_events is not None:
182
        dividend_frame = pd.DataFrame(
183
            [
184
                event.to_series(index=zp.DIVIDEND_FIELDS)
185
                for event in dividend_events
186
            ],
187
        )
188
        perf_tracker.update_dividends(dividend_frame)
189
190
    # Raw trades
191
    trade_events = sorted(trade_events, key=lambda ev: (ev.dt, ev.source_id))
192
193
    # Add a benchmark event for each date.
194
    trades_plus_bm = date_sorted_sources(trade_events, benchmark_events)
195
196
    # Filter out benchmark events that are later than the last trade date.
197
    filtered_trades_plus_bm = (filt_event for filt_event in trades_plus_bm
198
                               if filt_event.dt <= trade_events[-1].dt)
199
200
    grouped_trades_plus_bm = itertools.groupby(filtered_trades_plus_bm,
201
                                               lambda x: x.dt)
202
    results = []
203
204
    bm_updated = False
205
    for date, group in grouped_trades_plus_bm:
206
207
        for txn in filter(lambda txn: txn.dt == date, txns):
208
            # Process txns for this date.
209
            perf_tracker.process_transaction(txn)
210
211
        for event in group:
212
213
            if event.type == zp.DATASOURCE_TYPE.TRADE:
214
                perf_tracker.process_trade(event)
215
            elif event.type == zp.DATASOURCE_TYPE.DIVIDEND:
216
                perf_tracker.process_dividend(event)
217
            elif event.type == zp.DATASOURCE_TYPE.BENCHMARK:
218
                perf_tracker.process_benchmark(event)
219
                bm_updated = True
220
            elif event.type == zp.DATASOURCE_TYPE.COMMISSION:
221
                perf_tracker.process_commission(event)
222
223
        for split in filter(lambda split: split.dt == date, splits):
224
            # Process splits for this date.
225
            perf_tracker.process_split(split)
226
227
        if bm_updated:
228
            msg = perf_tracker.handle_market_close_daily()
229
            msg['account'] = perf_tracker.get_account(True)
230
            results.append(msg)
231
            bm_updated = False
232
    return results
233
234
235
def check_perf_tracker_serialization(perf_tracker):
236
    scalar_keys = [
237
        'emission_rate',
238
        'txn_count',
239
        'market_open',
240
        'last_close',
241
        '_dividend_count',
242
        'period_start',
243
        'day_count',
244
        'capital_base',
245
        'market_close',
246
        'saved_dt',
247
        'period_end',
248
        'total_days',
249
    ]
250
251
    p_string = dumps_with_persistent_ids(perf_tracker)
252
253
    test = loads_with_persistent_ids(p_string, env=perf_tracker.env)
254
255
    for k in scalar_keys:
256
        nt.assert_equal(getattr(test, k), getattr(perf_tracker, k), k)
257
258
    perf_periods = (
259
        test.cumulative_performance,
260
        test.todays_performance
261
    )
262
    for period in perf_periods:
263
        nt.assert_true(hasattr(period, '_position_tracker'))
264
265
266
class TestSplitPerformance(unittest.TestCase):
267
    def setUp(self):
268
        self.env = TradingEnvironment()
269
        self.env.write_data(equities_identifiers=[1])
270
        self.sim_params = create_simulation_parameters(num_days=2)
271
        # start with $10,000
272
        self.sim_params.capital_base = 10e3
273
274
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
275
                                                          self.env)
276
277
    def test_split_long_position(self):
278
        events = factory.create_trade_history(
279
            1,
280
            [20, 20],
281
            [100, 100],
282
            oneday,
283
            self.sim_params,
284
            env=self.env
285
        )
286
287
        # set up a long position in sid 1
288
        # 100 shares at $20 apiece = $2000 position
289
        txns = [create_txn(events[0], 20, 100)]
290
291
        # set up a split with ratio 3 occurring at the start of the second
292
        # day.
293
        splits = [
294
            factory.create_split(
295
                1,
296
                3,
297
                events[1].dt,
298
            ),
299
        ]
300
301
        results = calculate_results(self.sim_params, self.env,
302
                                    self.benchmark_events,
303
                                    events, txns=txns, splits=splits)
304
305
        # should have 33 shares (at $60 apiece) and $20 in cash
306
        self.assertEqual(2, len(results))
307
308
        latest_positions = results[1]['daily_perf']['positions']
309
        self.assertEqual(1, len(latest_positions))
310
311
        # check the last position to make sure it's been updated
312
        position = latest_positions[0]
313
314
        self.assertEqual(1, position['sid'])
315
        self.assertEqual(33, position['amount'])
316
        self.assertEqual(60, position['cost_basis'])
317
        self.assertEqual(60, position['last_sale_price'])
318
319
        # since we started with $10000, and we spent $2000 on the
320
        # position, but then got $20 back, we should have $8020
321
        # (or close to it) in cash.
322
323
        # we won't get exactly 8020 because sometimes a split is
324
        # denoted as a ratio like 0.3333, and we lose some digits
325
        # of precision.  thus, make sure we're pretty close.
326
        daily_perf = results[1]['daily_perf']
327
328
        self.assertTrue(
329
            zp_math.tolerant_equals(8020,
330
                                    daily_perf['ending_cash'], 1))
331
332
        # Validate that the account attributes were updated.
333
        account = results[1]['account']
334
        self.assertEqual(float('inf'), account['day_trades_remaining'])
335
        # this is a long only portfolio that is only partially invested
336
        # so net and gross leverage are equal.
337
        np.testing.assert_allclose(0.198, account['leverage'], rtol=1e-3)
338
        np.testing.assert_allclose(0.198, account['net_leverage'], rtol=1e-3)
339
        np.testing.assert_allclose(8020, account['regt_equity'], rtol=1e-3)
340
        self.assertEqual(float('inf'), account['regt_margin'])
341
        np.testing.assert_allclose(8020, account['available_funds'], rtol=1e-3)
342
        self.assertEqual(0, account['maintenance_margin_requirement'])
343
        np.testing.assert_allclose(10000,
344
                                   account['equity_with_loan'], rtol=1e-3)
345
        self.assertEqual(float('inf'), account['buying_power'])
346
        self.assertEqual(0, account['initial_margin_requirement'])
347
        np.testing.assert_allclose(8020, account['excess_liquidity'],
348
                                   rtol=1e-3)
349
        np.testing.assert_allclose(8020, account['settled_cash'], rtol=1e-3)
350
        np.testing.assert_allclose(10000, account['net_liquidation'],
351
                                   rtol=1e-3)
352
        np.testing.assert_allclose(0.802, account['cushion'], rtol=1e-3)
353
        np.testing.assert_allclose(1980, account['total_positions_value'],
354
                                   rtol=1e-3)
355
        self.assertEqual(0, account['accrued_interest'])
356
357
        for i, result in enumerate(results):
358
            for perf_kind in ('daily_perf', 'cumulative_perf'):
359
                perf_result = result[perf_kind]
360
                # prices aren't changing, so pnl and returns should be 0.0
361
                self.assertEqual(0.0, perf_result['pnl'],
362
                                 "day %s %s pnl %s instead of 0.0" %
363
                                 (i, perf_kind, perf_result['pnl']))
364
                self.assertEqual(0.0, perf_result['returns'],
365
                                 "day %s %s returns %s instead of 0.0" %
366
                                 (i, perf_kind, perf_result['returns']))
367
368
369
class TestCommissionEvents(unittest.TestCase):
370
371
    def setUp(self):
372
        self.env = TradingEnvironment()
373
        self.env.write_data(
374
            equities_identifiers=[0, 1, 133]
375
        )
376
        self.sim_params = create_simulation_parameters(num_days=5)
377
378
        logger.info("sim_params: %s" % self.sim_params)
379
380
        self.sim_params.capital_base = 10e3
381
382
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
383
                                                          self.env)
384
385
    def test_commission_event(self):
386
        events = factory.create_trade_history(
387
            1,
388
            [10, 10, 10, 10, 10],
389
            [100, 100, 100, 100, 100],
390
            oneday,
391
            self.sim_params,
392
            env=self.env
393
        )
394
395
        # Test commission models and validate result
396
        # Expected commission amounts:
397
        # PerShare commission:  1.00, 1.00, 1.50 = $3.50
398
        # PerTrade commission:  5.00, 5.00, 5.00 = $15.00
399
        # PerDollar commission: 1.50, 3.00, 4.50 = $9.00
400
        # Total commission = $3.50 + $15.00 + $9.00 = $27.50
401
402
        # Create 3 transactions:  50, 100, 150 shares traded @ $20
403
        transactions = [create_txn(events[0], 20, i)
404
                        for i in [50, 100, 150]]
405
406
        # Create commission models and validate that produce expected
407
        # commissions.
408
        models = [PerShare(cost=0.01, min_trade_cost=1.00),
409
                  PerTrade(cost=5.00),
410
                  PerDollar(cost=0.0015)]
411
        expected_results = [3.50, 15.0, 9.0]
412
413
        for model, expected in zip(models, expected_results):
414
            total_commission = 0
415
            for trade in transactions:
416
                total_commission += model.calculate(trade)[1]
417
            self.assertEqual(total_commission, expected)
418
419
        # Verify that commission events are handled correctly by
420
        # PerformanceTracker.
421
        cash_adj_dt = events[0].dt
422
        cash_adjustment = factory.create_commission(1, 300.0, cash_adj_dt)
423
        events.append(cash_adjustment)
424
425
        # Insert a purchase order.
426
        txns = [create_txn(events[0], 20, 1)]
427
        results = calculate_results(self.sim_params,
428
                                    self.env,
429
                                    self.benchmark_events,
430
                                    events,
431
                                    txns=txns)
432
433
        # Validate that we lost 320 dollars from our cash pool.
434
        self.assertEqual(results[-1]['cumulative_perf']['ending_cash'],
435
                         9680)
436
        # Validate that the cost basis of our position changed.
437
        self.assertEqual(results[-1]['daily_perf']['positions']
438
                         [0]['cost_basis'], 320.0)
439
        # Validate that the account attributes were updated.
440
        account = results[1]['account']
441
        self.assertEqual(float('inf'), account['day_trades_remaining'])
442
        np.testing.assert_allclose(0.001, account['leverage'], rtol=1e-3,
443
                                   atol=1e-4)
444
        np.testing.assert_allclose(9680, account['regt_equity'], rtol=1e-3)
445
        self.assertEqual(float('inf'), account['regt_margin'])
446
        np.testing.assert_allclose(9680, account['available_funds'],
447
                                   rtol=1e-3)
448
        self.assertEqual(0, account['maintenance_margin_requirement'])
449
        np.testing.assert_allclose(9690,
450
                                   account['equity_with_loan'], rtol=1e-3)
451
        self.assertEqual(float('inf'), account['buying_power'])
452
        self.assertEqual(0, account['initial_margin_requirement'])
453
        np.testing.assert_allclose(9680, account['excess_liquidity'],
454
                                   rtol=1e-3)
455
        np.testing.assert_allclose(9680, account['settled_cash'],
456
                                   rtol=1e-3)
457
        np.testing.assert_allclose(9690, account['net_liquidation'],
458
                                   rtol=1e-3)
459
        np.testing.assert_allclose(0.999, account['cushion'], rtol=1e-3)
460
        np.testing.assert_allclose(10, account['total_positions_value'],
461
                                   rtol=1e-3)
462
        self.assertEqual(0, account['accrued_interest'])
463
464
    def test_commission_zero_position(self):
465
        """
466
        Ensure no div-by-zero errors.
467
        """
468
        events = factory.create_trade_history(
469
            1,
470
            [10, 10, 10, 10, 10],
471
            [100, 100, 100, 100, 100],
472
            oneday,
473
            self.sim_params,
474
            env=self.env
475
        )
476
477
        # Buy and sell the same sid so that we have a zero position by the
478
        # time of events[3].
479
        txns = [
480
            create_txn(events[0], 20, 1),
481
            create_txn(events[1], 20, -1),
482
        ]
483
484
        # Add a cash adjustment at the time of event[3].
485
        cash_adj_dt = events[3].dt
486
        cash_adjustment = factory.create_commission(1, 300.0, cash_adj_dt)
487
488
        events.append(cash_adjustment)
489
490
        results = calculate_results(self.sim_params,
491
                                    self.env,
492
                                    self.benchmark_events,
493
                                    events,
494
                                    txns=txns)
495
        # Validate that we lost 300 dollars from our cash pool.
496
        self.assertEqual(results[-1]['cumulative_perf']['ending_cash'],
497
                         9700)
498
499
    def test_commission_no_position(self):
500
        """
501
        Ensure no position-not-found or sid-not-found errors.
502
        """
503
        events = factory.create_trade_history(
504
            1,
505
            [10, 10, 10, 10, 10],
506
            [100, 100, 100, 100, 100],
507
            oneday,
508
            self.sim_params,
509
            env=self.env
510
        )
511
512
        # Add a cash adjustment at the time of event[3].
513
        cash_adj_dt = events[3].dt
514
        cash_adjustment = factory.create_commission(1, 300.0, cash_adj_dt)
515
        events.append(cash_adjustment)
516
517
        results = calculate_results(self.sim_params,
518
                                    self.env,
519
                                    self.benchmark_events,
520
                                    events)
521
        # Validate that we lost 300 dollars from our cash pool.
522
        self.assertEqual(results[-1]['cumulative_perf']['ending_cash'],
523
                         9700)
524
525
526
class TestDividendPerformance(unittest.TestCase):
527
528
    @classmethod
529
    def setUpClass(cls):
530
        cls.env = TradingEnvironment()
531
        cls.env.write_data(equities_identifiers=[1, 2])
532
533
    @classmethod
534
    def tearDownClass(cls):
535
        del cls.env
536
537
    def setUp(self):
538
        self.sim_params = create_simulation_parameters(num_days=6)
539
        self.sim_params.capital_base = 10e3
540
541
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
542
                                                          self.env)
543
544
    def test_market_hours_calculations(self):
545
        # DST in US/Eastern began on Sunday March 14, 2010
546
        before = datetime(2010, 3, 12, 14, 31, tzinfo=pytz.utc)
547
        after = factory.get_next_trading_dt(
548
            before,
549
            timedelta(days=1),
550
            self.env,
551
        )
552
        self.assertEqual(after.hour, 13)
553
554
    def test_long_position_receives_dividend(self):
555
        # post some trades in the market
556
        events = factory.create_trade_history(
557
            1,
558
            [10, 10, 10, 10, 10],
559
            [100, 100, 100, 100, 100],
560
            oneday,
561
            self.sim_params,
562
            env=self.env
563
        )
564
        dividend = factory.create_dividend(
565
            1,
566
            10.00,
567
            # declared date, when the algorithm finds out about
568
            # the dividend
569
            events[0].dt,
570
            # ex_date, the date before which the algorithm must hold stock
571
            # to receive the dividend
572
            events[1].dt,
573
            # pay date, when the algorithm receives the dividend.
574
            events[2].dt
575
        )
576
577
        # Simulate a transaction being filled prior to the ex_date.
578
        txns = [create_txn(events[0], 10.0, 100)]
579
        results = calculate_results(
580
            self.sim_params,
581
            self.env,
582
            self.benchmark_events,
583
            events,
584
            dividend_events=[dividend],
585
            txns=txns,
586
        )
587
588
        self.assertEqual(len(results), 5)
589
        cumulative_returns = \
590
            [event['cumulative_perf']['returns'] for event in results]
591
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.1, 0.1, 0.1])
592
        daily_returns = [event['daily_perf']['returns']
593
                         for event in results]
594
        self.assertEqual(daily_returns, [0.0, 0.0, 0.10, 0.0, 0.0])
595
        cash_flows = [event['daily_perf']['capital_used']
596
                      for event in results]
597
        self.assertEqual(cash_flows, [-1000, 0, 1000, 0, 0])
598
        cumulative_cash_flows = \
599
            [event['cumulative_perf']['capital_used'] for event in results]
600
        self.assertEqual(cumulative_cash_flows, [-1000, -1000, 0, 0, 0])
601
        cash_pos = \
602
            [event['cumulative_perf']['ending_cash'] for event in results]
603
        self.assertEqual(cash_pos, [9000, 9000, 10000, 10000, 10000])
604
605
    def test_long_position_receives_stock_dividend(self):
606
        # post some trades in the market
607
        events = []
608
        for sid in (1, 2):
609
            events.extend(
610
                factory.create_trade_history(
611
                    sid,
612
                    [10, 10, 10, 10, 10],
613
                    [100, 100, 100, 100, 100],
614
                    oneday,
615
                    self.sim_params,
616
                    env=self.env)
617
            )
618
619
        dividend = factory.create_stock_dividend(
620
            1,
621
            payment_sid=2,
622
            ratio=2,
623
            # declared date, when the algorithm finds out about
624
            # the dividend
625
            declared_date=events[0].dt,
626
            # ex_date, the date before which the algorithm must hold stock
627
            # to receive the dividend
628
            ex_date=events[1].dt,
629
            # pay date, when the algorithm receives the dividend.
630
            pay_date=events[2].dt
631
        )
632
633
        txns = [create_txn(events[0], 10.0, 100)]
634
635
        results = calculate_results(
636
            self.sim_params,
637
            self.env,
638
            self.benchmark_events,
639
            events,
640
            dividend_events=[dividend],
641
            txns=txns,
642
        )
643
644
        self.assertEqual(len(results), 5)
645
        cumulative_returns = \
646
            [event['cumulative_perf']['returns'] for event in results]
647
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.2, 0.2, 0.2])
648
        daily_returns = [event['daily_perf']['returns']
649
                         for event in results]
650
        self.assertEqual(daily_returns, [0.0, 0.0, 0.2, 0.0, 0.0])
651
        cash_flows = [event['daily_perf']['capital_used']
652
                      for event in results]
653
        self.assertEqual(cash_flows, [-1000, 0, 0, 0, 0])
654
        cumulative_cash_flows = \
655
            [event['cumulative_perf']['capital_used'] for event in results]
656
        self.assertEqual(cumulative_cash_flows, [-1000] * 5)
657
        cash_pos = \
658
            [event['cumulative_perf']['ending_cash'] for event in results]
659
        self.assertEqual(cash_pos, [9000] * 5)
660
661
    def test_long_position_purchased_on_ex_date_receives_no_dividend(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
662
        # post some trades in the market
663
        events = factory.create_trade_history(
664
            1,
665
            [10, 10, 10, 10, 10],
666
            [100, 100, 100, 100, 100],
667
            oneday,
668
            self.sim_params,
669
            env=self.env
670
        )
671
672
        dividend = factory.create_dividend(
673
            1,
674
            10.00,
675
            events[0].dt,  # Declared date
676
            events[1].dt,  # Exclusion date
677
            events[2].dt   # Pay date
678
        )
679
680
        # Simulate a transaction being filled on the ex_date.
681
        txns = [create_txn(events[1], 10.0, 100)]
682
683
        results = calculate_results(
684
            self.sim_params,
685
            self.env,
686
            self.benchmark_events,
687
            events,
688
            dividend_events=[dividend],
689
            txns=txns,
690
        )
691
692
        self.assertEqual(len(results), 5)
693
        cumulative_returns = \
694
            [event['cumulative_perf']['returns'] for event in results]
695
        self.assertEqual(cumulative_returns, [0, 0, 0, 0, 0])
696
        daily_returns = [event['daily_perf']['returns'] for event in results]
697
        self.assertEqual(daily_returns, [0, 0, 0, 0, 0])
698
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
699
        self.assertEqual(cash_flows, [0, -1000, 0, 0, 0])
700
        cumulative_cash_flows = \
701
            [event['cumulative_perf']['capital_used'] for event in results]
702
        self.assertEqual(cumulative_cash_flows,
703
                         [0, -1000, -1000, -1000, -1000])
704
705
    def test_selling_before_dividend_payment_still_gets_paid(self):
706
        # post some trades in the market
707
        events = factory.create_trade_history(
708
            1,
709
            [10, 10, 10, 10, 10],
710
            [100, 100, 100, 100, 100],
711
            oneday,
712
            self.sim_params,
713
            env=self.env
714
        )
715
716
        dividend = factory.create_dividend(
717
            1,
718
            10.00,
719
            events[0].dt,  # Declared date
720
            events[1].dt,  # Exclusion date
721
            events[3].dt   # Pay date
722
        )
723
724
        buy_txn = create_txn(events[0], 10.0, 100)
725
        sell_txn = create_txn(events[2], 10.0, -100)
726
        txns = [buy_txn, sell_txn]
727
728
        results = calculate_results(
729
            self.sim_params,
730
            self.env,
731
            self.benchmark_events,
732
            events,
733
            dividend_events=[dividend],
734
            txns=txns,
735
        )
736
737
        self.assertEqual(len(results), 5)
738
        cumulative_returns = \
739
            [event['cumulative_perf']['returns'] for event in results]
740
        self.assertEqual(cumulative_returns, [0, 0, 0, 0.1, 0.1])
741
        daily_returns = [event['daily_perf']['returns'] for event in results]
742
        self.assertEqual(daily_returns, [0, 0, 0, 0.1, 0])
743
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
744
        self.assertEqual(cash_flows, [-1000, 0, 1000, 1000, 0])
745
        cumulative_cash_flows = \
746
            [event['cumulative_perf']['capital_used'] for event in results]
747
        self.assertEqual(cumulative_cash_flows, [-1000, -1000, 0, 1000, 1000])
748
749
    def test_buy_and_sell_before_ex(self):
750
        # post some trades in the market
751
        events = factory.create_trade_history(
752
            1,
753
            [10, 10, 10, 10, 10, 10],
754
            [100, 100, 100, 100, 100, 100],
755
            oneday,
756
            self.sim_params,
757
            env=self.env
758
        )
759
760
        dividend = factory.create_dividend(
761
            1,
762
            10.00,
763
            events[3].dt,
764
            events[4].dt,
765
            events[5].dt
766
        )
767
768
        buy_txn = create_txn(events[1], 10.0, 100)
769
        sell_txn = create_txn(events[2], 10.0, -100)
770
        txns = [buy_txn, sell_txn]
771
772
        results = calculate_results(
773
            self.sim_params,
774
            self.env,
775
            self.benchmark_events,
776
            events,
777
            dividend_events=[dividend],
778
            txns=txns,
779
        )
780
781
        self.assertEqual(len(results), 6)
782
        cumulative_returns = \
783
            [event['cumulative_perf']['returns'] for event in results]
784
        self.assertEqual(cumulative_returns, [0, 0, 0, 0, 0, 0])
785
        daily_returns = [event['daily_perf']['returns'] for event in results]
786
        self.assertEqual(daily_returns, [0, 0, 0, 0, 0, 0])
787
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
788
        self.assertEqual(cash_flows, [0, -1000, 1000, 0, 0, 0])
789
        cumulative_cash_flows = \
790
            [event['cumulative_perf']['capital_used'] for event in results]
791
        self.assertEqual(cumulative_cash_flows, [0, -1000, 0, 0, 0, 0])
792
793
    def test_ending_before_pay_date(self):
794
        # post some trades in the market
795
        events = factory.create_trade_history(
796
            1,
797
            [10, 10, 10, 10, 10],
798
            [100, 100, 100, 100, 100],
799
            oneday,
800
            self.sim_params,
801
            env=self.env
802
        )
803
804
        pay_date = self.sim_params.first_open
805
        # find pay date that is much later.
806
        for i in range(30):
807
            pay_date = factory.get_next_trading_dt(pay_date, oneday, self.env)
808
        dividend = factory.create_dividend(
809
            1,
810
            10.00,
811
            events[0].dt,
812
            events[0].dt,
813
            pay_date
814
        )
815
816
        txns = [create_txn(events[1], 10.0, 100)]
817
818
        results = calculate_results(
819
            self.sim_params,
820
            self.env,
821
            self.benchmark_events,
822
            events,
823
            dividend_events=[dividend],
824
            txns=txns,
825
        )
826
827
        self.assertEqual(len(results), 5)
828
        cumulative_returns = \
829
            [event['cumulative_perf']['returns'] for event in results]
830
        self.assertEqual(cumulative_returns, [0, 0, 0, 0.0, 0.0])
831
        daily_returns = [event['daily_perf']['returns'] for event in results]
832
        self.assertEqual(daily_returns, [0, 0, 0, 0, 0])
833
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
834
        self.assertEqual(cash_flows, [0, -1000, 0, 0, 0])
835
        cumulative_cash_flows = \
836
            [event['cumulative_perf']['capital_used'] for event in results]
837
        self.assertEqual(
838
            cumulative_cash_flows,
839
            [0, -1000, -1000, -1000, -1000]
840
        )
841
842
    def test_short_position_pays_dividend(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
843
        # post some trades in the market
844
        events = factory.create_trade_history(
845
            1,
846
            [10, 10, 10, 10, 10],
847
            [100, 100, 100, 100, 100],
848
            oneday,
849
            self.sim_params,
850
            env=self.env
851
        )
852
853
        dividend = factory.create_dividend(
854
            1,
855
            10.00,
856
            # declare at open of test
857
            events[0].dt,
858
            # ex_date same as trade 2
859
            events[2].dt,
860
            events[3].dt
861
        )
862
863
        txns = [create_txn(events[1], 10.0, -100)]
864
865
        results = calculate_results(
866
            self.sim_params,
867
            self.env,
868
            self.benchmark_events,
869
            events,
870
            dividend_events=[dividend],
871
            txns=txns,
872
        )
873
874
        self.assertEqual(len(results), 5)
875
        cumulative_returns = \
876
            [event['cumulative_perf']['returns'] for event in results]
877
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, -0.1, -0.1])
878
        daily_returns = [event['daily_perf']['returns'] for event in results]
879
        self.assertEqual(daily_returns, [0.0, 0.0, 0.0, -0.1, 0.0])
880
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
881
        self.assertEqual(cash_flows, [0, 1000, 0, -1000, 0])
882
        cumulative_cash_flows = \
883
            [event['cumulative_perf']['capital_used'] for event in results]
884
        self.assertEqual(cumulative_cash_flows, [0, 1000, 1000, 0, 0])
885
886
    def test_no_position_receives_no_dividend(self):
887
        # post some trades in the market
888
        events = factory.create_trade_history(
889
            1,
890
            [10, 10, 10, 10, 10],
891
            [100, 100, 100, 100, 100],
892
            oneday,
893
            self.sim_params,
894
            env=self.env
895
        )
896
897
        dividend = factory.create_dividend(
898
            1,
899
            10.00,
900
            events[0].dt,
901
            events[1].dt,
902
            events[2].dt
903
        )
904
905
        results = calculate_results(
906
            self.sim_params,
907
            self.env,
908
            self.benchmark_events,
909
            events,
910
            dividend_events=[dividend],
911
        )
912
913
        self.assertEqual(len(results), 5)
914
        cumulative_returns = \
915
            [event['cumulative_perf']['returns'] for event in results]
916
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, 0.0, 0.0])
917
        daily_returns = [event['daily_perf']['returns'] for event in results]
918
        self.assertEqual(daily_returns, [0.0, 0.0, 0.0, 0.0, 0.0])
919
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
920
        self.assertEqual(cash_flows, [0, 0, 0, 0, 0])
921
        cumulative_cash_flows = \
922
            [event['cumulative_perf']['capital_used'] for event in results]
923
        self.assertEqual(cumulative_cash_flows, [0, 0, 0, 0, 0])
924
925
    def test_no_dividend_at_simulation_end(self):
926
        # post some trades in the market
927
        events = factory.create_trade_history(
928
            1,
929
            [10, 10, 10, 10, 10],
930
            [100, 100, 100, 100, 100],
931
            oneday,
932
            self.sim_params,
933
            env=self.env
934
        )
935
        dividend = factory.create_dividend(
936
            1,
937
            10.00,
938
            # declared date, when the algorithm finds out about
939
            # the dividend
940
            events[-3].dt,
941
            # ex_date, the date before which the algorithm must hold stock
942
            # to receive the dividend
943
            events[-2].dt,
944
            # pay date, when the algorithm receives the dividend.
945
            # This pays out on the day after the last event
946
            self.env.next_trading_day(events[-1].dt)
947
        )
948
949
        # Set the last day to be the last event
950
        self.sim_params.period_end = events[-1].dt
951
        self.sim_params.update_internal_from_env(self.env)
952
953
        # Simulate a transaction being filled prior to the ex_date.
954
        txns = [create_txn(events[0], 10.0, 100)]
955
        results = calculate_results(
956
            self.sim_params,
957
            self.env,
958
            self.benchmark_events,
959
            events,
960
            dividend_events=[dividend],
961
            txns=txns,
962
        )
963
964
        self.assertEqual(len(results), 5)
965
        cumulative_returns = \
966
            [event['cumulative_perf']['returns'] for event in results]
967
        self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, 0.0, 0.0])
968
        daily_returns = [event['daily_perf']['returns'] for event in results]
969
        self.assertEqual(daily_returns, [0.0, 0.0, 0.0, 0.0, 0.0])
970
        cash_flows = [event['daily_perf']['capital_used'] for event in results]
971
        self.assertEqual(cash_flows, [-1000, 0, 0, 0, 0])
972
        cumulative_cash_flows = \
973
            [event['cumulative_perf']['capital_used'] for event in results]
974
        self.assertEqual(cumulative_cash_flows,
975
                         [-1000, -1000, -1000, -1000, -1000])
976
977
978
class TestDividendPerformanceHolidayStyle(TestDividendPerformance):
979
980
    # The holiday tests begins the simulation on the day
981
    # before Thanksgiving, so that the next trading day is
982
    # two days ahead. Any tests that hard code events
983
    # to be start + oneday will fail, since those events will
984
    # be skipped by the simulation.
985
986
    def setUp(self):
987
        self.dt = datetime(2003, 11, 30, tzinfo=pytz.utc)
988
        self.end_dt = datetime(2004, 11, 25, tzinfo=pytz.utc)
989
        self.sim_params = SimulationParameters(
990
            self.dt,
991
            self.end_dt,
992
            env=self.env)
993
994
        self.sim_params.capital_base = 10e3
995
996
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
997
                                                          self.env)
998
999
1000
class TestPositionPerformance(unittest.TestCase):
1001
1002
    @classmethod
1003
    def setUpClass(cls):
1004
        cls.env = TradingEnvironment()
1005
        cls.env.write_data(equities_identifiers=[1, 2])
1006
1007
    @classmethod
1008
    def tearDownClass(cls):
1009
        del cls.env
1010
1011
    def setUp(self):
1012
        self.sim_params = create_simulation_parameters(num_days=4)
1013
1014
        self.finder = self.env.asset_finder
1015
        self.benchmark_events = benchmark_events_in_range(self.sim_params,
1016
                                                          self.env)
1017
1018
    def test_long_short_positions(self):
1019
        """
1020
        start with $1000
1021
        buy 100 stock1 shares at $10
1022
        sell short 100 stock2 shares at $10
1023
        stock1 then goes down to $9
1024
        stock2 goes to $11
1025
        """
1026
1027
        trades_1 = factory.create_trade_history(
1028
            1,
1029
            [10, 10, 10, 9],
1030
            [100, 100, 100, 100],
1031
            onesec,
1032
            self.sim_params,
1033
            env=self.env
1034
        )
1035
1036
        trades_2 = factory.create_trade_history(
1037
            2,
1038
            [10, 10, 10, 11],
1039
            [100, 100, 100, 100],
1040
            onesec,
1041
            self.sim_params,
1042
            env=self.env
1043
        )
1044
1045
        txn1 = create_txn(trades_1[1], 10.0, 100)
1046
        txn2 = create_txn(trades_2[1], 10.0, -100)
1047
        pt = perf.PositionTracker(self.env.asset_finder)
1048
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1049
        pp.position_tracker = pt
1050
        pt.execute_transaction(txn1)
1051
        pp.handle_execution(txn1)
1052
        pt.execute_transaction(txn2)
1053
        pp.handle_execution(txn2)
1054
1055
        for trade in itertools.chain(trades_1[:-2], trades_2[:-2]):
1056
            pt.update_last_sale(trade)
1057
1058
        pp.calculate_performance()
1059
1060
        check_perf_period(
1061
            pp,
1062
            gross_leverage=2.0,
1063
            net_leverage=0.0,
1064
            long_exposure=1000.0,
1065
            longs_count=1,
1066
            short_exposure=-1000.0,
1067
            shorts_count=1)
1068
        # Validate that the account attributes were updated.
1069
        account = pp.as_account()
1070
        check_account(account,
1071
                      settled_cash=1000.0,
1072
                      equity_with_loan=1000.0,
1073
                      total_positions_value=0.0,
1074
                      regt_equity=1000.0,
1075
                      available_funds=1000.0,
1076
                      excess_liquidity=1000.0,
1077
                      cushion=1.0,
1078
                      leverage=2.0,
1079
                      net_leverage=0.0,
1080
                      net_liquidation=1000.0)
1081
1082
        # now simulate stock1 going to $9
1083
        pt.update_last_sale(trades_1[-1])
1084
        # and stock2 going to $11
1085
        pt.update_last_sale(trades_2[-1])
1086
1087
        pp.calculate_performance()
1088
1089
        # Validate that the account attributes were updated.
1090
        account = pp.as_account()
1091
1092
        check_perf_period(
1093
            pp,
1094
            gross_leverage=2.5,
1095
            net_leverage=-0.25,
1096
            long_exposure=900.0,
1097
            longs_count=1,
1098
            short_exposure=-1100.0,
1099
            shorts_count=1)
1100
1101
        check_account(account,
1102
                      settled_cash=1000.0,
1103
                      equity_with_loan=800.0,
1104
                      total_positions_value=-200.0,
1105
                      regt_equity=1000.0,
1106
                      available_funds=1000.0,
1107
                      excess_liquidity=1000.0,
1108
                      cushion=1.25,
1109
                      leverage=2.5,
1110
                      net_leverage=-0.25,
1111
                      net_liquidation=800.0)
1112
1113
    def test_levered_long_position(self):
1114
        """
1115
            start with $1,000, then buy 1000 shares at $10.
1116
            price goes to $11
1117
        """
1118
        # post some trades in the market
1119
        trades = factory.create_trade_history(
1120
            1,
1121
            [10, 10, 10, 11],
1122
            [100, 100, 100, 100],
1123
            onesec,
1124
            self.sim_params,
1125
            env=self.env
1126
        )
1127
1128
        txn = create_txn(trades[1], 10.0, 1000)
1129
        pt = perf.PositionTracker(self.env.asset_finder)
1130
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1131
        pp.position_tracker = pt
1132
1133
        pt.execute_transaction(txn)
1134
        pp.handle_execution(txn)
1135
1136
        for trade in trades[:-2]:
1137
            pt.update_last_sale(trade)
1138
1139
        pp.calculate_performance()
1140
1141
        check_perf_period(
1142
            pp,
1143
            gross_leverage=10.0,
1144
            net_leverage=10.0,
1145
            long_exposure=10000.0,
1146
            longs_count=1,
1147
            short_exposure=0.0,
1148
            shorts_count=0)
1149
1150
        # Validate that the account attributes were updated.
1151
        account = pp.as_account()
1152
        check_account(account,
1153
                      settled_cash=-9000.0,
1154
                      equity_with_loan=1000.0,
1155
                      total_positions_value=10000.0,
1156
                      regt_equity=-9000.0,
1157
                      available_funds=-9000.0,
1158
                      excess_liquidity=-9000.0,
1159
                      cushion=-9.0,
1160
                      leverage=10.0,
1161
                      net_leverage=10.0,
1162
                      net_liquidation=1000.0)
1163
1164
        # now simulate a price jump to $11
1165
        pt.update_last_sale(trades[-1])
1166
1167
        pp.calculate_performance()
1168
1169
        check_perf_period(
1170
            pp,
1171
            gross_leverage=5.5,
1172
            net_leverage=5.5,
1173
            long_exposure=11000.0,
1174
            longs_count=1,
1175
            short_exposure=0.0,
1176
            shorts_count=0)
1177
1178
        # Validate that the account attributes were updated.
1179
        account = pp.as_account()
1180
1181
        check_account(account,
1182
                      settled_cash=-9000.0,
1183
                      equity_with_loan=2000.0,
1184
                      total_positions_value=11000.0,
1185
                      regt_equity=-9000.0,
1186
                      available_funds=-9000.0,
1187
                      excess_liquidity=-9000.0,
1188
                      cushion=-4.5,
1189
                      leverage=5.5,
1190
                      net_leverage=5.5,
1191
                      net_liquidation=2000.0)
1192
1193
    def test_long_position(self):
1194
        """
1195
            verify that the performance period calculates properly for a
1196
            single buy transaction
1197
        """
1198
        # post some trades in the market
1199
        trades = factory.create_trade_history(
1200
            1,
1201
            [10, 10, 10, 11],
1202
            [100, 100, 100, 100],
1203
            onesec,
1204
            self.sim_params,
1205
            env=self.env
1206
        )
1207
1208
        txn = create_txn(trades[1], 10.0, 100)
1209
        pt = perf.PositionTracker(self.env.asset_finder)
1210
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1211
        pp.position_tracker = pt
1212
1213
        pt.execute_transaction(txn)
1214
        pp.handle_execution(txn)
1215
1216
        # This verifies that the last sale price is being correctly
1217
        # set in the positions. If this is not the case then returns can
1218
        # incorrectly show as sharply dipping if a transaction arrives
1219
        # before a trade. This is caused by returns being based on holding
1220
        # stocks with a last sale price of 0.
1221
        self.assertEqual(pp.positions[1].last_sale_price, 10.0)
1222
1223
        for trade in trades:
1224
            pt.update_last_sale(trade)
1225
1226
        pp.calculate_performance()
1227
1228
        self.assertEqual(
1229
            pp.period_cash_flow,
1230
            -1 * txn.price * txn.amount,
1231
            "capital used should be equal to the opposite of the transaction \
1232
            cost of sole txn in test"
1233
        )
1234
1235
        self.assertEqual(
1236
            len(pp.positions),
1237
            1,
1238
            "should be just one position")
1239
1240
        self.assertEqual(
1241
            pp.positions[1].sid,
1242
            txn.sid,
1243
            "position should be in security with id 1")
1244
1245
        self.assertEqual(
1246
            pp.positions[1].amount,
1247
            txn.amount,
1248
            "should have a position of {sharecount} shares".format(
1249
                sharecount=txn.amount
1250
            )
1251
        )
1252
1253
        self.assertEqual(
1254
            pp.positions[1].cost_basis,
1255
            txn.price,
1256
            "should have a cost basis of 10"
1257
        )
1258
1259
        self.assertEqual(
1260
            pp.positions[1].last_sale_price,
1261
            trades[-1]['price'],
1262
            "last sale should be same as last trade. \
1263
            expected {exp} actual {act}".format(
1264
                exp=trades[-1]['price'],
1265
                act=pp.positions[1].last_sale_price)
1266
        )
1267
1268
        self.assertEqual(
1269
            pp.ending_value,
1270
            1100,
1271
            "ending value should be price of last trade times number of \
1272
            shares in position"
1273
        )
1274
1275
        self.assertEqual(pp.pnl, 100, "gain of 1 on 100 shares should be 100")
1276
1277
        check_perf_period(
1278
            pp,
1279
            gross_leverage=1.0,
1280
            net_leverage=1.0,
1281
            long_exposure=1100.0,
1282
            longs_count=1,
1283
            short_exposure=0.0,
1284
            shorts_count=0)
1285
1286
        # Validate that the account attributes were updated.
1287
        account = pp.as_account()
1288
        check_account(account,
1289
                      settled_cash=0.0,
1290
                      equity_with_loan=1100.0,
1291
                      total_positions_value=1100.0,
1292
                      regt_equity=0.0,
1293
                      available_funds=0.0,
1294
                      excess_liquidity=0.0,
1295
                      cushion=0.0,
1296
                      leverage=1.0,
1297
                      net_leverage=1.0,
1298
                      net_liquidation=1100.0)
1299
1300
    def test_short_position(self):
1301
        """verify that the performance period calculates properly for a \
1302
single short-sale transaction"""
1303
        trades = factory.create_trade_history(
1304
            1,
1305
            [10, 10, 10, 11, 10, 9],
1306
            [100, 100, 100, 100, 100, 100],
1307
            onesec,
1308
            self.sim_params,
1309
            env=self.env
1310
        )
1311
1312
        trades_1 = trades[:-2]
1313
1314
        txn = create_txn(trades[1], 10.0, -100)
1315
        pt = perf.PositionTracker(self.env.asset_finder)
1316
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1317
        pp.position_tracker = pt
1318
1319
        pt.execute_transaction(txn)
1320
        pp.handle_execution(txn)
1321
        for trade in trades_1:
1322
            pt.update_last_sale(trade)
1323
1324
        pp.calculate_performance()
1325
1326
        self.assertEqual(
1327
            pp.period_cash_flow,
1328
            -1 * txn.price * txn.amount,
1329
            "capital used should be equal to the opposite of the transaction\
1330
             cost of sole txn in test"
1331
        )
1332
1333
        self.assertEqual(
1334
            len(pp.positions),
1335
            1,
1336
            "should be just one position")
1337
1338
        self.assertEqual(
1339
            pp.positions[1].sid,
1340
            txn.sid,
1341
            "position should be in security from the transaction"
1342
        )
1343
1344
        self.assertEqual(
1345
            pp.positions[1].amount,
1346
            -100,
1347
            "should have a position of -100 shares"
1348
        )
1349
1350
        self.assertEqual(
1351
            pp.positions[1].cost_basis,
1352
            txn.price,
1353
            "should have a cost basis of 10"
1354
        )
1355
1356
        self.assertEqual(
1357
            pp.positions[1].last_sale_price,
1358
            trades_1[-1]['price'],
1359
            "last sale should be price of last trade"
1360
        )
1361
1362
        self.assertEqual(
1363
            pp.ending_value,
1364
            -1100,
1365
            "ending value should be price of last trade times number of \
1366
            shares in position"
1367
        )
1368
1369
        self.assertEqual(pp.pnl, -100, "gain of 1 on 100 shares should be 100")
1370
1371
        # simulate additional trades, and ensure that the position value
1372
        # reflects the new price
1373
        trades_2 = trades[-2:]
1374
1375
        # simulate a rollover to a new period
1376
        pp.rollover()
1377
1378
        for trade in trades_2:
1379
            pt.update_last_sale(trade)
1380
1381
        pp.calculate_performance()
1382
1383
        self.assertEqual(
1384
            pp.period_cash_flow,
1385
            0,
1386
            "capital used should be zero, there were no transactions in \
1387
            performance period"
1388
        )
1389
1390
        self.assertEqual(
1391
            len(pp.positions),
1392
            1,
1393
            "should be just one position"
1394
        )
1395
1396
        self.assertEqual(
1397
            pp.positions[1].sid,
1398
            txn.sid,
1399
            "position should be in security from the transaction"
1400
        )
1401
1402
        self.assertEqual(
1403
            pp.positions[1].amount,
1404
            -100,
1405
            "should have a position of -100 shares"
1406
        )
1407
1408
        self.assertEqual(
1409
            pp.positions[1].cost_basis,
1410
            txn.price,
1411
            "should have a cost basis of 10"
1412
        )
1413
1414
        self.assertEqual(
1415
            pp.positions[1].last_sale_price,
1416
            trades_2[-1].price,
1417
            "last sale should be price of last trade"
1418
        )
1419
1420
        self.assertEqual(
1421
            pp.ending_value,
1422
            -900,
1423
            "ending value should be price of last trade times number of \
1424
            shares in position")
1425
1426
        self.assertEqual(
1427
            pp.pnl,
1428
            200,
1429
            "drop of 2 on -100 shares should be 200"
1430
        )
1431
1432
        # now run a performance period encompassing the entire trade sample.
1433
        ptTotal = perf.PositionTracker(self.env.asset_finder)
1434
        ppTotal = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1435
        ppTotal.position_tracker = pt
1436
1437
        for trade in trades_1:
1438
            ptTotal.update_last_sale(trade)
1439
1440
        ptTotal.execute_transaction(txn)
1441
        ppTotal.handle_execution(txn)
1442
1443
        for trade in trades_2:
1444
            ptTotal.update_last_sale(trade)
1445
1446
        ppTotal.calculate_performance()
1447
1448
        self.assertEqual(
1449
            ppTotal.period_cash_flow,
1450
            -1 * txn.price * txn.amount,
1451
            "capital used should be equal to the opposite of the transaction \
1452
cost of sole txn in test"
1453
        )
1454
1455
        self.assertEqual(
1456
            len(ppTotal.positions),
1457
            1,
1458
            "should be just one position"
1459
        )
1460
        self.assertEqual(
1461
            ppTotal.positions[1].sid,
1462
            txn.sid,
1463
            "position should be in security from the transaction"
1464
        )
1465
1466
        self.assertEqual(
1467
            ppTotal.positions[1].amount,
1468
            -100,
1469
            "should have a position of -100 shares"
1470
        )
1471
1472
        self.assertEqual(
1473
            ppTotal.positions[1].cost_basis,
1474
            txn.price,
1475
            "should have a cost basis of 10"
1476
        )
1477
1478
        self.assertEqual(
1479
            ppTotal.positions[1].last_sale_price,
1480
            trades_2[-1].price,
1481
            "last sale should be price of last trade"
1482
        )
1483
1484
        self.assertEqual(
1485
            ppTotal.ending_value,
1486
            -900,
1487
            "ending value should be price of last trade times number of \
1488
            shares in position")
1489
1490
        self.assertEqual(
1491
            ppTotal.pnl,
1492
            100,
1493
            "drop of 1 on -100 shares should be 100"
1494
        )
1495
1496
        check_perf_period(
1497
            pp,
1498
            gross_leverage=0.8181,
1499
            net_leverage=-0.8181,
1500
            long_exposure=0.0,
1501
            longs_count=0,
1502
            short_exposure=-900.0,
1503
            shorts_count=1)
1504
1505
        # Validate that the account attributes.
1506
        account = ppTotal.as_account()
1507
        check_account(account,
1508
                      settled_cash=2000.0,
1509
                      equity_with_loan=1100.0,
1510
                      total_positions_value=-900.0,
1511
                      regt_equity=2000.0,
1512
                      available_funds=2000.0,
1513
                      excess_liquidity=2000.0,
1514
                      cushion=1.8181,
1515
                      leverage=0.8181,
1516
                      net_leverage=-0.8181,
1517
                      net_liquidation=1100.0)
1518
1519
    def test_covering_short(self):
1520
        """verify performance where short is bought and covered, and shares \
1521
trade after cover"""
1522
1523
        trades = factory.create_trade_history(
1524
            1,
1525
            [10, 10, 10, 11, 9, 8, 7, 8, 9, 10],
1526
            [100, 100, 100, 100, 100, 100, 100, 100, 100, 100],
1527
            onesec,
1528
            self.sim_params,
1529
            env=self.env
1530
        )
1531
1532
        short_txn = create_txn(
1533
            trades[1],
1534
            10.0,
1535
            -100,
1536
        )
1537
1538
        cover_txn = create_txn(trades[6], 7.0, 100)
1539
        pt = perf.PositionTracker(self.env.asset_finder)
1540
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1541
        pp.position_tracker = pt
1542
1543
        pt.execute_transaction(short_txn)
1544
        pp.handle_execution(short_txn)
1545
        pt.execute_transaction(cover_txn)
1546
        pp.handle_execution(cover_txn)
1547
1548
        for trade in trades:
1549
            pt.update_last_sale(trade)
1550
1551
        pp.calculate_performance()
1552
1553
        short_txn_cost = short_txn.price * short_txn.amount
1554
        cover_txn_cost = cover_txn.price * cover_txn.amount
1555
1556
        self.assertEqual(
1557
            pp.period_cash_flow,
1558
            -1 * short_txn_cost - cover_txn_cost,
1559
            "capital used should be equal to the net transaction costs"
1560
        )
1561
1562
        self.assertEqual(
1563
            len(pp.positions),
1564
            1,
1565
            "should be just one position"
1566
        )
1567
1568
        self.assertEqual(
1569
            pp.positions[1].sid,
1570
            short_txn.sid,
1571
            "position should be in security from the transaction"
1572
        )
1573
1574
        self.assertEqual(
1575
            pp.positions[1].amount,
1576
            0,
1577
            "should have a position of -100 shares"
1578
        )
1579
1580
        self.assertEqual(
1581
            pp.positions[1].cost_basis,
1582
            0,
1583
            "a covered position should have a cost basis of 0"
1584
        )
1585
1586
        self.assertEqual(
1587
            pp.positions[1].last_sale_price,
1588
            trades[-1].price,
1589
            "last sale should be price of last trade"
1590
        )
1591
1592
        self.assertEqual(
1593
            pp.ending_value,
1594
            0,
1595
            "ending value should be price of last trade times number of \
1596
shares in position"
1597
        )
1598
1599
        self.assertEqual(
1600
            pp.pnl,
1601
            300,
1602
            "gain of 1 on 100 shares should be 300"
1603
        )
1604
1605
        check_perf_period(
1606
            pp,
1607
            gross_leverage=0.0,
1608
            net_leverage=0.0,
1609
            long_exposure=0.0,
1610
            longs_count=0,
1611
            short_exposure=0.0,
1612
            shorts_count=0)
1613
1614
        account = pp.as_account()
1615
        check_account(account,
1616
                      settled_cash=1300.0,
1617
                      equity_with_loan=1300.0,
1618
                      total_positions_value=0.0,
1619
                      regt_equity=1300.0,
1620
                      available_funds=1300.0,
1621
                      excess_liquidity=1300.0,
1622
                      cushion=1.0,
1623
                      leverage=0.0,
1624
                      net_leverage=0.0,
1625
                      net_liquidation=1300.0)
1626
1627
    def test_cost_basis_calc(self):
1628
        history_args = (
1629
            1,
1630
            [10, 11, 11, 12],
1631
            [100, 100, 100, 100],
1632
            onesec,
1633
            self.sim_params,
1634
            self.env
1635
        )
1636
        trades = factory.create_trade_history(*history_args)
1637
        transactions = factory.create_txn_history(*history_args)
1638
1639
        pt = perf.PositionTracker(self.env.asset_finder)
1640
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1641
        pp.position_tracker = pt
1642
1643
        average_cost = 0
1644
        for i, txn in enumerate(transactions):
1645
            pt.execute_transaction(txn)
1646
            pp.handle_execution(txn)
1647
            average_cost = (average_cost * i + txn.price) / (i + 1)
1648
            self.assertEqual(pp.positions[1].cost_basis, average_cost)
1649
1650
        for trade in trades:
1651
            pt.update_last_sale(trade)
1652
1653
        pp.calculate_performance()
1654
1655
        self.assertEqual(
1656
            pp.positions[1].last_sale_price,
1657
            trades[-1].price,
1658
            "should have a last sale of 12, got {val}".format(
1659
                val=pp.positions[1].last_sale_price)
1660
        )
1661
1662
        self.assertEqual(
1663
            pp.positions[1].cost_basis,
1664
            11,
1665
            "should have a cost basis of 11"
1666
        )
1667
1668
        self.assertEqual(
1669
            pp.pnl,
1670
            400
1671
        )
1672
1673
        down_tick = factory.create_trade(
1674
            1,
1675
            10.0,
1676
            100,
1677
            trades[-1].dt + onesec)
1678
1679
        sale_txn = create_txn(
1680
            down_tick,
1681
            10.0,
1682
            -100)
1683
1684
        pp.rollover()
1685
1686
        pt.execute_transaction(sale_txn)
1687
        pp.handle_execution(sale_txn)
1688
        pt.update_last_sale(down_tick)
1689
1690
        pp.calculate_performance()
1691
        self.assertEqual(
1692
            pp.positions[1].last_sale_price,
1693
            10,
1694
            "should have a last sale of 10, was {val}".format(
1695
                val=pp.positions[1].last_sale_price)
1696
        )
1697
1698
        self.assertEqual(
1699
            pp.positions[1].cost_basis,
1700
            11,
1701
            "should have a cost basis of 11"
1702
        )
1703
1704
        self.assertEqual(pp.pnl, -800, "this period goes from +400 to -400")
1705
1706
        pt3 = perf.PositionTracker(self.env.asset_finder)
1707
        pp3 = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1708
        pp3.position_tracker = pt3
1709
1710
        average_cost = 0
1711
        for i, txn in enumerate(transactions):
1712
            pt3.execute_transaction(txn)
1713
            pp3.handle_execution(txn)
1714
            average_cost = (average_cost * i + txn.price) / (i + 1)
1715
            self.assertEqual(pp3.positions[1].cost_basis, average_cost)
1716
1717
        pt3.execute_transaction(sale_txn)
1718
        pp3.handle_execution(sale_txn)
1719
1720
        trades.append(down_tick)
1721
        for trade in trades:
1722
            pt3.update_last_sale(trade)
1723
1724
        pp3.calculate_performance()
1725
        self.assertEqual(
1726
            pp3.positions[1].last_sale_price,
1727
            10,
1728
            "should have a last sale of 10"
1729
        )
1730
1731
        self.assertEqual(
1732
            pp3.positions[1].cost_basis,
1733
            11,
1734
            "should have a cost basis of 11"
1735
        )
1736
1737
        self.assertEqual(
1738
            pp3.pnl,
1739
            -400,
1740
            "should be -400 for all trades and transactions in period"
1741
        )
1742
1743
    def test_cost_basis_calc_close_pos(self):
1744
        history_args = (
1745
            1,
1746
            [10, 9, 11, 8, 9, 12, 13, 14],
1747
            [200, -100, -100, 100, -300, 100, 500, 400],
1748
            onesec,
1749
            self.sim_params,
1750
            self.env
1751
        )
1752
        cost_bases = [10, 10, 0, 8, 9, 9, 13, 13.5]
1753
1754
        trades = factory.create_trade_history(*history_args)
1755
        transactions = factory.create_txn_history(*history_args)
1756
1757
        pt = perf.PositionTracker(self.env.asset_finder)
1758
        pp = perf.PerformancePeriod(1000.0, self.env.asset_finder)
1759
        pp.position_tracker = pt
1760
1761
        for txn, cb in zip(transactions, cost_bases):
1762
            pt.execute_transaction(txn)
1763
            pp.handle_execution(txn)
1764
            self.assertEqual(pp.positions[1].cost_basis, cb)
1765
1766
        for trade in trades:
1767
            pt.update_last_sale(trade)
1768
1769
        pp.calculate_performance()
1770
1771
        self.assertEqual(pp.positions[1].cost_basis, cost_bases[-1])
1772
1773
1774
class TestPerformanceTracker(unittest.TestCase):
1775
1776
    @classmethod
1777
    def setUpClass(cls):
1778
        cls.env = TradingEnvironment()
1779
        cls.env.write_data(equities_identifiers=[1, 2, 133, 134])
1780
1781
    @classmethod
1782
    def tearDownClass(cls):
1783
        del cls.env
1784
1785
    NumDaysToDelete = collections.namedtuple(
1786
        'NumDaysToDelete', ('start', 'middle', 'end'))
1787
1788
    @parameterized.expand([
1789
        ("Don't delete any events",
1790
         NumDaysToDelete(start=0, middle=0, end=0)),
1791
        ("Delete first day of events",
1792
         NumDaysToDelete(start=1, middle=0, end=0)),
1793
        ("Delete first two days of events",
1794
         NumDaysToDelete(start=2, middle=0, end=0)),
1795
        ("Delete one day of events from the middle",
1796
         NumDaysToDelete(start=0, middle=1, end=0)),
1797
        ("Delete two events from the middle",
1798
         NumDaysToDelete(start=0, middle=2, end=0)),
1799
        ("Delete last day of events",
1800
         NumDaysToDelete(start=0, middle=0, end=1)),
1801
        ("Delete last two days of events",
1802
         NumDaysToDelete(start=0, middle=0, end=2)),
1803
        ("Delete all but one event.",
1804
         NumDaysToDelete(start=2, middle=1, end=2)),
1805
    ])
1806
    def test_tracker(self, parameter_comment, days_to_delete):
1807
        """
1808
        @days_to_delete - configures which days in the data set we should
1809
        remove, used for ensuring that we still return performance messages
1810
        even when there is no data.
1811
        """
1812
        # This date range covers Columbus day,
1813
        # however Columbus day is not a market holiday
1814
        #
1815
        #     October 2008
1816
        # Su Mo Tu We Th Fr Sa
1817
        #           1  2  3  4
1818
        #  5  6  7  8  9 10 11
1819
        # 12 13 14 15 16 17 18
1820
        # 19 20 21 22 23 24 25
1821
        # 26 27 28 29 30 31
1822
        start_dt = datetime(year=2008,
1823
                            month=10,
1824
                            day=9,
1825
                            tzinfo=pytz.utc)
1826
        end_dt = datetime(year=2008,
1827
                          month=10,
1828
                          day=16,
1829
                          tzinfo=pytz.utc)
1830
1831
        trade_count = 6
1832
        sid = 133
1833
        price = 10.1
1834
        price_list = [price] * trade_count
1835
        volume = [100] * trade_count
1836
        trade_time_increment = timedelta(days=1)
1837
1838
        sim_params = SimulationParameters(
1839
            period_start=start_dt,
1840
            period_end=end_dt,
1841
            env=self.env,
1842
        )
1843
1844
        benchmark_events = benchmark_events_in_range(sim_params, self.env)
1845
1846
        trade_history = factory.create_trade_history(
1847
            sid,
1848
            price_list,
1849
            volume,
1850
            trade_time_increment,
1851
            sim_params,
1852
            source_id="factory1",
1853
            env=self.env
1854
        )
1855
1856
        sid2 = 134
1857
        price2 = 12.12
1858
        price2_list = [price2] * trade_count
1859
        trade_history2 = factory.create_trade_history(
1860
            sid2,
1861
            price2_list,
1862
            volume,
1863
            trade_time_increment,
1864
            sim_params,
1865
            source_id="factory2",
1866
            env=self.env
1867
        )
1868
        # 'middle' start of 3 depends on number of days == 7
1869
        middle = 3
1870
1871
        # First delete from middle
1872
        if days_to_delete.middle:
1873
            del trade_history[middle:(middle + days_to_delete.middle)]
1874
            del trade_history2[middle:(middle + days_to_delete.middle)]
1875
1876
        # Delete start
1877
        if days_to_delete.start:
1878
            del trade_history[:days_to_delete.start]
1879
            del trade_history2[:days_to_delete.start]
1880
1881
        # Delete from end
1882
        if days_to_delete.end:
1883
            del trade_history[-days_to_delete.end:]
1884
            del trade_history2[-days_to_delete.end:]
1885
1886
        sim_params.capital_base = 1000.0
1887
        sim_params.frame_index = [
1888
            'sid',
1889
            'volume',
1890
            'dt',
1891
            'price',
1892
            'changed']
1893
        perf_tracker = perf.PerformanceTracker(
1894
            sim_params, self.env
1895
        )
1896
1897
        events = date_sorted_sources(trade_history, trade_history2)
1898
1899
        events = [event for event in
1900
                  self.trades_with_txns(events, trade_history[0].dt)]
1901
1902
        # Extract events with transactions to use for verification.
1903
        txns = [event for event in
1904
                events if event.type == zp.DATASOURCE_TYPE.TRANSACTION]
1905
1906
        orders = [event for event in
1907
                  events if event.type == zp.DATASOURCE_TYPE.ORDER]
1908
1909
        all_events = date_sorted_sources(events, benchmark_events)
1910
1911
        filtered_events = [filt_event for filt_event
1912
                           in all_events if filt_event.dt <= end_dt]
1913
        filtered_events.sort(key=lambda x: x.dt)
1914
        grouped_events = itertools.groupby(filtered_events, lambda x: x.dt)
1915
        perf_messages = []
1916
1917
        for date, group in grouped_events:
1918
            for event in group:
1919
                if event.type == zp.DATASOURCE_TYPE.TRADE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1920
                    perf_tracker.process_trade(event)
1921
                elif event.type == zp.DATASOURCE_TYPE.ORDER:
1922
                    perf_tracker.process_order(event)
1923
                elif event.type == zp.DATASOURCE_TYPE.BENCHMARK:
1924
                    perf_tracker.process_benchmark(event)
1925
                elif event.type == zp.DATASOURCE_TYPE.TRANSACTION:
1926
                    perf_tracker.process_transaction(event)
1927
            msg = perf_tracker.handle_market_close_daily()
1928
            perf_messages.append(msg)
1929
1930
        self.assertEqual(perf_tracker.txn_count, len(txns))
1931
        self.assertEqual(perf_tracker.txn_count, len(orders))
1932
1933
        positions = perf_tracker.cumulative_performance.positions
1934
        if len(txns) == 0:
1935
            self.assertNotIn(sid, positions)
1936
        else:
1937
            expected_size = len(txns) / 2 * -25
1938
            cumulative_pos = positions[sid]
1939
            self.assertEqual(cumulative_pos.amount, expected_size)
1940
1941
            self.assertEqual(len(perf_messages),
1942
                             sim_params.days_in_period)
1943
1944
        check_perf_tracker_serialization(perf_tracker)
1945
1946
    def trades_with_txns(self, events, no_txn_dt):
1947
        for event in events:
1948
1949
            # create a transaction for all but
1950
            # first trade in each sid, to simulate None transaction
1951
            if event.dt != no_txn_dt:
1952
                order = Order(
1953
                    sid=event.sid,
1954
                    amount=-25,
1955
                    dt=event.dt
1956
                )
1957
                order.source_id = 'MockOrderSource'
1958
                yield order
1959
                yield event
1960
                txn = Transaction(
1961
                    sid=event.sid,
1962
                    amount=-25,
1963
                    dt=event.dt,
1964
                    price=10.0,
1965
                    commission=0.50,
1966
                    order_id=order.id
1967
                )
1968
                txn.source_id = 'MockTransactionSource'
1969
                yield txn
1970
            else:
1971
                yield event
1972
1973
    def test_minute_tracker(self):
1974
        """ Tests minute performance tracking."""
1975
        start_dt = self.env.exchange_dt_in_utc(datetime(2013, 3, 1, 9, 31))
1976
        end_dt = self.env.exchange_dt_in_utc(datetime(2013, 3, 1, 16, 0))
1977
1978
        foosid = 1
1979
        barsid = 2
1980
1981
        sim_params = SimulationParameters(
1982
            period_start=start_dt,
1983
            period_end=end_dt,
1984
            emission_rate='minute',
1985
            env=self.env,
1986
        )
1987
        tracker = perf.PerformanceTracker(sim_params, env=self.env)
1988
1989
        foo_event_1 = factory.create_trade(foosid, 10.0, 20, start_dt)
1990
        order_event_1 = Order(sid=foo_event_1.sid,
1991
                              amount=-25,
1992
                              dt=foo_event_1.dt)
1993
        bar_event_1 = factory.create_trade(barsid, 100.0, 200, start_dt)
1994
        txn_event_1 = Transaction(sid=foo_event_1.sid,
1995
                                  amount=-25,
1996
                                  dt=foo_event_1.dt,
1997
                                  price=10.0,
1998
                                  commission=0.50,
1999
                                  order_id=order_event_1.id)
2000
        benchmark_event_1 = Event({
2001
            'dt': start_dt,
2002
            'returns': 0.01,
2003
            'type': zp.DATASOURCE_TYPE.BENCHMARK
2004
        })
2005
2006
        foo_event_2 = factory.create_trade(
2007
            foosid, 11.0, 20, start_dt + timedelta(minutes=1))
2008
        bar_event_2 = factory.create_trade(
2009
            barsid, 11.0, 20, start_dt + timedelta(minutes=1))
2010
        benchmark_event_2 = Event({
2011
            'dt': start_dt + timedelta(minutes=1),
2012
            'returns': 0.02,
2013
            'type': zp.DATASOURCE_TYPE.BENCHMARK
2014
        })
2015
2016
        events = [
2017
            foo_event_1,
2018
            order_event_1,
2019
            benchmark_event_1,
2020
            txn_event_1,
2021
            bar_event_1,
2022
            foo_event_2,
2023
            benchmark_event_2,
2024
            bar_event_2,
2025
        ]
2026
2027
        grouped_events = itertools.groupby(
2028
            events, operator.attrgetter('dt'))
2029
2030
        messages = {}
2031
        for date, group in grouped_events:
2032
            tracker.set_date(date)
2033
            for event in group:
2034
                if event.type == zp.DATASOURCE_TYPE.TRADE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2035
                    tracker.process_trade(event)
2036
                elif event.type == zp.DATASOURCE_TYPE.BENCHMARK:
2037
                    tracker.process_benchmark(event)
2038
                elif event.type == zp.DATASOURCE_TYPE.ORDER:
2039
                    tracker.process_order(event)
2040
                elif event.type == zp.DATASOURCE_TYPE.TRANSACTION:
2041
                    tracker.process_transaction(event)
2042
            msg, _ = tracker.handle_minute_close(date)
2043
            messages[date] = msg
2044
2045
        self.assertEquals(2, len(messages))
2046
2047
        msg_1 = messages[foo_event_1.dt]
2048
        msg_2 = messages[foo_event_2.dt]
2049
2050
        self.assertEquals(1, len(msg_1['minute_perf']['transactions']),
2051
                          "The first message should contain one "
2052
                          "transaction.")
2053
        # Check that transactions aren't emitted for previous events.
2054
        self.assertEquals(0, len(msg_2['minute_perf']['transactions']),
2055
                          "The second message should have no "
2056
                          "transactions.")
2057
2058
        self.assertEquals(1, len(msg_1['minute_perf']['orders']),
2059
                          "The first message should contain one orders.")
2060
        # Check that orders aren't emitted for previous events.
2061
        self.assertEquals(0, len(msg_2['minute_perf']['orders']),
2062
                          "The second message should have no orders.")
2063
2064
        # Ensure that period_close moves through time.
2065
        # Also, ensure that the period_closes are the expected dts.
2066
        self.assertEquals(foo_event_1.dt,
2067
                          msg_1['minute_perf']['period_close'])
2068
        self.assertEquals(foo_event_2.dt,
2069
                          msg_2['minute_perf']['period_close'])
2070
2071
        # In this test event1 transactions arrive on the first bar.
2072
        # This leads to no returns as the price is constant.
2073
        # Sharpe ratio cannot be computed and is None.
2074
        # In the second bar we can start establishing a sharpe ratio.
2075
        self.assertIsNone(msg_1['cumulative_risk_metrics']['sharpe'])
2076
        self.assertIsNotNone(msg_2['cumulative_risk_metrics']['sharpe'])
2077
2078
        check_perf_tracker_serialization(tracker)
2079
2080
    def test_close_position_event(self):
2081
        pt = perf.PositionTracker(asset_finder=self.env.asset_finder)
2082
        dt = pd.Timestamp("1984/03/06 3:00PM")
2083
        pos1 = perf.Position(1, amount=np.float64(120.0),
2084
                             last_sale_date=dt, last_sale_price=3.4)
2085
        pos2 = perf.Position(2, amount=np.float64(-100.0),
2086
                             last_sale_date=dt, last_sale_price=3.4)
2087
        pt.update_positions({1: pos1, 2: pos2})
2088
2089
        event_type = DATASOURCE_TYPE.CLOSE_POSITION
2090
        index = [dt + timedelta(days=1)]
2091
        pan = pd.Panel({1: pd.DataFrame({'price': 1, 'volume': 0,
2092
                                         'type': event_type}, index=index),
2093
                        2: pd.DataFrame({'price': 1, 'volume': 0,
2094
                                         'type': event_type}, index=index),
2095
                        3: pd.DataFrame({'price': 1, 'volume': 0,
2096
                                         'type': event_type}, index=index)})
2097
2098
        source = DataPanelSource(pan)
2099
        for i, event in enumerate(source):
2100
            txn = pt.maybe_create_close_position_transaction(event)
2101
            if event.sid == 1:
2102
                # Test owned long
2103
                self.assertEqual(-120, txn.amount)
2104
            elif event.sid == 2:
2105
                # Test owned short
2106
                self.assertEqual(100, txn.amount)
2107
            elif event.sid == 3:
2108
                # Test not-owned SID
2109
                self.assertIsNone(txn)
2110
2111
    def test_handle_sid_removed_from_universe(self):
2112
        # post some trades in the market
2113
        sim_params = create_simulation_parameters(num_days=5)
2114
        events = factory.create_trade_history(
2115
            1,
2116
            [10, 10, 10, 10, 10],
2117
            [100, 100, 100, 100, 100],
2118
            oneday,
2119
            sim_params,
2120
            env=self.env
2121
        )
2122
2123
        # Create a tracker and a dividend
2124
        perf_tracker = perf.PerformanceTracker(sim_params, env=self.env)
2125
        dividend = factory.create_dividend(
2126
            1,
2127
            10.00,
2128
            # declared date, when the algorithm finds out about
2129
            # the dividend
2130
            events[0].dt,
2131
            # ex_date, the date before which the algorithm must hold stock
2132
            # to receive the dividend
2133
            events[1].dt,
2134
            # pay date, when the algorithm receives the dividend.
2135
            events[2].dt
2136
        )
2137
        dividend_frame = pd.DataFrame(
2138
            [dividend.to_series(index=zp.DIVIDEND_FIELDS)],
2139
        )
2140
        perf_tracker.update_dividends(dividend_frame)
2141
2142
        # Ensure that the dividend is in the tracker
2143
        self.assertIn(1, perf_tracker.dividend_frame['sid'].values)
2144
2145
        # Inform the tracker that sid 1 has been removed from the universe
2146
        perf_tracker.handle_sid_removed_from_universe(1)
2147
2148
        # Ensure that the dividend for sid 1 has been removed from dividend
2149
        # frame
2150
        self.assertNotIn(1, perf_tracker.dividend_frame['sid'].values)
2151
2152
    def test_serialization(self):
2153
        start_dt = datetime(year=2008,
2154
                            month=10,
2155
                            day=9,
2156
                            tzinfo=pytz.utc)
2157
        end_dt = datetime(year=2008,
2158
                          month=10,
2159
                          day=16,
2160
                          tzinfo=pytz.utc)
2161
2162
        sim_params = SimulationParameters(
2163
            period_start=start_dt,
2164
            period_end=end_dt,
2165
            env=self.env,
2166
        )
2167
2168
        perf_tracker = perf.PerformanceTracker(
2169
            sim_params, env=self.env
2170
        )
2171
        check_perf_tracker_serialization(perf_tracker)
2172
2173
2174
class TestPosition(unittest.TestCase):
2175
    def setUp(self):
2176
        pass
2177
2178
    def test_serialization(self):
2179
        dt = pd.Timestamp("1984/03/06 3:00PM")
2180
        pos = perf.Position(10, amount=np.float64(120.0), last_sale_date=dt,
2181
                            last_sale_price=3.4)
2182
2183
        p_string = dumps_with_persistent_ids(pos)
2184
2185
        test = loads_with_persistent_ids(p_string, env=None)
2186
        nt.assert_dict_equal(test.__dict__, pos.__dict__)
2187
2188
2189
class TestPositionTracker(unittest.TestCase):
2190
2191
    @classmethod
2192
    def setUpClass(cls):
2193
        cls.env = TradingEnvironment()
2194
        futures_metadata = {3: {'contract_multiplier': 1000},
2195
                            4: {'contract_multiplier': 1000}}
2196
        cls.env.write_data(equities_identifiers=[1, 2],
2197
                           futures_data=futures_metadata)
2198
2199
    @classmethod
2200
    def tearDownClass(cls):
2201
        del cls.env
2202
2203
    def test_empty_positions(self):
2204
        """
2205
        make sure all the empty position stats return a numeric 0
2206
2207
        Originally this bug was due to np.dot([], []) returning
2208
        np.bool_(False)
2209
        """
2210
        pt = perf.PositionTracker(self.env.asset_finder)
2211
        pos_stats = pt.stats()
2212
2213
        stats = [
2214
            'net_value',
2215
            'net_exposure',
2216
            'gross_value',
2217
            'gross_exposure',
2218
            'short_value',
2219
            'short_exposure',
2220
            'shorts_count',
2221
            'long_value',
2222
            'long_exposure',
2223
            'longs_count',
2224
        ]
2225
        for name in stats:
2226
            val = getattr(pos_stats, name)
2227
            self.assertEquals(val, 0)
2228
            self.assertNotIsInstance(val, (bool, np.bool_))
2229
2230
    def test_position_values_and_exposures(self):
2231
        pt = perf.PositionTracker(self.env.asset_finder)
2232
        dt = pd.Timestamp("1984/03/06 3:00PM")
2233
        pos1 = perf.Position(1, amount=np.float64(10.0),
2234
                             last_sale_date=dt, last_sale_price=10)
2235
        pos2 = perf.Position(2, amount=np.float64(-20.0),
2236
                             last_sale_date=dt, last_sale_price=10)
2237
        pos3 = perf.Position(3, amount=np.float64(30.0),
2238
                             last_sale_date=dt, last_sale_price=10)
2239
        pos4 = perf.Position(4, amount=np.float64(-40.0),
2240
                             last_sale_date=dt, last_sale_price=10)
2241
        pt.update_positions({1: pos1, 2: pos2, 3: pos3, 4: pos4})
2242
2243
        # Test long-only methods
2244
2245
        pos_stats = pt.stats()
2246
        self.assertEqual(100, pos_stats.long_value)
2247
        self.assertEqual(100 + 300000, pos_stats.long_exposure)
2248
        self.assertEqual(2, pos_stats.longs_count)
2249
2250
        # Test short-only methods
2251
        self.assertEqual(-200, pos_stats.short_value)
2252
        self.assertEqual(-200 - 400000, pos_stats.short_exposure)
2253
        self.assertEqual(2, pos_stats.shorts_count)
2254
2255
        # Test gross and net values
2256
        self.assertEqual(100 + 200, pos_stats.gross_value)
2257
        self.assertEqual(100 - 200, pos_stats.net_value)
2258
2259
        # Test gross and net exposures
2260
        self.assertEqual(100 + 200 + 300000 + 400000, pos_stats.gross_exposure)
2261
        self.assertEqual(100 - 200 + 300000 - 400000, pos_stats.net_exposure)
2262
2263
    def test_serialization(self):
2264
        pt = perf.PositionTracker(self.env.asset_finder)
2265
        dt = pd.Timestamp("1984/03/06 3:00PM")
2266
        pos1 = perf.Position(1, amount=np.float64(120.0),
2267
                             last_sale_date=dt, last_sale_price=3.4)
2268
        pos3 = perf.Position(3, amount=np.float64(100.0),
2269
                             last_sale_date=dt, last_sale_price=3.4)
2270
2271
        pt.update_positions({1: pos1, 3: pos3})
2272
        p_string = dumps_with_persistent_ids(pt)
2273
        test = loads_with_persistent_ids(p_string, env=self.env)
2274
        nt.assert_count_equal(test.positions.keys(), pt.positions.keys())
2275
        for sid in pt.positions:
2276
            nt.assert_dict_equal(test.positions[sid].__dict__,
2277
                                 pt.positions[sid].__dict__)
2278
2279
2280
class TestPerformancePeriod(unittest.TestCase):
2281
2282
    def test_serialization(self):
2283
        env = TradingEnvironment()
2284
        pt = perf.PositionTracker(env.asset_finder)
2285
        pp = perf.PerformancePeriod(100, env.asset_finder)
2286
        pp.position_tracker = pt
2287
2288
        p_string = dumps_with_persistent_ids(pp)
2289
        test = loads_with_persistent_ids(p_string, env=env)
2290
2291
        correct = pp.__dict__.copy()
2292
        del correct['_position_tracker']
2293
2294
        nt.assert_count_equal(test.__dict__.keys(), correct.keys())
2295
2296
        equal_keys = list(correct.keys())
2297
        equal_keys.remove('_account_store')
2298
        equal_keys.remove('_portfolio_store')
2299
2300
        for k in equal_keys:
2301
            nt.assert_equal(test.__dict__[k], correct[k])
2302