Completed
Pull Request — master (#858)
by Eddie
02:50 queued 01:15
created

tests.finance.SlippageTestCase.test_orders_limit()   F

Complexity

Conditions 9

Size

Total Lines 160

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 9
dl 0
loc 160
rs 3.1304

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
"""
17
Unit tests for finance.slippage
18
"""
19
import datetime
20
21
import pytz
22
23
from unittest import TestCase
24
25
from nose_parameterized import parameterized
26
27
import numpy as np
28
import pandas as pd
29
from testfixtures import TempDirectory
30
31
from zipline.finance.slippage import VolumeShareSlippage
32
from zipline.finance.trading import TradingEnvironment, SimulationParameters
33
34
from zipline.protocol import DATASOURCE_TYPE
35
from zipline.finance.blotter import Order
36
37
from zipline.data.us_equity_minutes import MinuteBarWriterFromDataFrames
38
from zipline.data.us_equity_minutes import BcolzMinuteBarReader
39
from zipline.data.data_portal import DataPortal
40
from zipline.protocol import BarData
41
42
43
class SlippageTestCase(TestCase):
44
45
    @classmethod
46
    def setUpClass(cls):
47
        cls.tempdir = TempDirectory()
48
        cls.env = TradingEnvironment()
49
50
        cls.sim_params = SimulationParameters(
51
            period_start=pd.Timestamp("2006-01-05 14:31", tz="utc"),
52
            period_end=pd.Timestamp("2006-01-05 14:36", tz="utc"),
53
            capital_base=1.0e5,
54
            data_frequency="minute",
55
            emission_rate='daily',
56
            env=cls.env
57
        )
58
59
        cls.sids = [133]
60
61
        cls.minutes = pd.DatetimeIndex(
62
            start=pd.Timestamp("2006-01-05 14:31", tz="utc"),
63
            end=pd.Timestamp("2006-01-05 14:35", tz="utc"),
64
            freq="1min"
65
        )
66
67
        assets = {
68
            133: pd.DataFrame({
69
                "open": np.array([3.0, 3.0, 3.5, 4.0, 3.5]) * 1000,
70
                "high": np.array([3.15, 3.15, 3.15, 3.15, 3.15]) * 1000,
71
                "low": np.array([2.85, 2.85, 2.85, 2.85, 2.85]) * 1000,
72
                "close": np.array([3.0, 3.5, 4.0, 3.5, 3.0]) * 1000,
73
                "volume": [2000, 2000, 2000, 2000, 2000],
74
                "minute": cls.minutes
75
            })
76
        }
77
78
        MinuteBarWriterFromDataFrames(
79
            pd.Timestamp('2002-01-02', tz='UTC')
80
        ).write(cls.tempdir.path, assets)
81
82
        cls.env.write_data(equities_data={
83
            133: {
84
                "start_date": pd.Timestamp("2006-01-05", tz='utc'),
85
                "end_date": pd.Timestamp("2006-01-07", tz='utc')
86
            }
87
        })
88
89
        cls.data_portal = DataPortal(
90
            cls.env,
91
            equity_minute_reader=BcolzMinuteBarReader(cls.tempdir.path),
92
        )
93
94
    @classmethod
95
    def tearDownClass(cls):
96
        cls.tempdir.cleanup()
97
        del cls.env
98
99
    def test_volume_share_slippage(self):
100
        tempdir = TempDirectory()
101
102
        try:
103
            assets = {
104
                133: pd.DataFrame({
105
                    "open": [3000],
106
                    "high": [3150],
107
                    "low": [2850],
108
                    "close": [3000],
109
                    "volume": [200],
110
                    "minute": [self.minutes[0]]
111
                })
112
            }
113
114
            MinuteBarWriterFromDataFrames(
115
                pd.Timestamp('2002-01-02', tz='UTC')
116
            ).write(tempdir.path, assets)
117
118
            equity_minute_reader = BcolzMinuteBarReader(tempdir.path)
119
120
            data_portal = DataPortal(
121
                self.env,
122
                equity_minute_reader=equity_minute_reader,
123
            )
124
125
            slippage_model = VolumeShareSlippage()
126
127
            open_orders = [
128
                Order(
129
                    dt=datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
130
                    amount=100,
131
                    filled=0,
132
                    sid=133
133
                )
134
            ]
135
136
            bar_data = BarData(data_portal,
137
                               lambda: self.minutes[0],
138
                               'minute')
139
140
            orders_txns = list(slippage_model.simulate(
141
                bar_data[133],
142
                open_orders,
143
            ))
144
145
            self.assertEquals(len(orders_txns), 1)
146
            _, txn = orders_txns[0]
147
148
            expected_txn = {
149
                'price': float(3.0001875),
150
                'dt': datetime.datetime(
151
                    2006, 1, 5, 14, 31, tzinfo=pytz.utc),
152
                'amount': int(5),
153
                'sid': int(133),
154
                'commission': None,
155
                'type': DATASOURCE_TYPE.TRANSACTION,
156
                'order_id': open_orders[0].id
157
            }
158
159
            self.assertIsNotNone(txn)
160
161
            # TODO: Make expected_txn an Transaction object and ensure there
162
            # is a __eq__ for that class.
163
            self.assertEquals(expected_txn, txn.__dict__)
164
        finally:
165
            tempdir.cleanup()
166
167
    def test_orders_limit(self):
168
        slippage_model = VolumeShareSlippage()
169
        slippage_model.data_portal = self.data_portal
170
171
        # long, does not trade
172
        open_orders = [
173
            Order(**{
174
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
175
                'amount': 100,
176
                'filled': 0,
177
                'sid': 133,
178
                'limit': 3.5})
179
        ]
180
181
        bar_data = BarData(self.data_portal,
182
                           lambda: self.minutes[3],
183
                           self.sim_params.data_frequency)
184
185
        orders_txns = list(slippage_model.simulate(
186
            bar_data[133],
187
            open_orders,
188
        ))
189
190
        self.assertEquals(len(orders_txns), 0)
191
192
        # long, does not trade - impacted price worse than limit price
193
        open_orders = [
194
            Order(**{
195
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
196
                'amount': 100,
197
                'filled': 0,
198
                'sid': 133,
199
                'limit': 3.5})
200
        ]
201
202
        bar_data = BarData(self.data_portal,
203
                           lambda: self.minutes[3],
204
                           self.sim_params.data_frequency)
205
206
        orders_txns = list(slippage_model.simulate(
207
            bar_data[133],
208
            open_orders,
209
        ))
210
211
        self.assertEquals(len(orders_txns), 0)
212
213
        # long, does trade
214
        open_orders = [
215
            Order(**{
216
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
217
                'amount': 100,
218
                'filled': 0,
219
                'sid': 133,
220
                'limit': 3.6})
221
        ]
222
223
        bar_data = BarData(self.data_portal,
224
                           lambda: self.minutes[3],
225
                           self.sim_params.data_frequency)
226
227
        orders_txns = list(slippage_model.simulate(
228
            bar_data[133],
229
            open_orders,
230
        ))
231
232
        self.assertEquals(len(orders_txns), 1)
233
        txn = orders_txns[0][1]
234
235
        expected_txn = {
236
            'price': float(3.50021875),
237
            'dt': datetime.datetime(
238
                2006, 1, 5, 14, 34, tzinfo=pytz.utc),
239
            # we ordered 100 shares, but default volume slippage only allows
240
            # for 2.5% of the volume.  2.5% * 2000 = 50 shares
241
            'amount': int(50),
242
            'sid': int(133),
243
            'order_id': open_orders[0].id
244
        }
245
246
        self.assertIsNotNone(txn)
247
248
        for key, value in expected_txn.items():
249
            self.assertEquals(value, txn[key])
250
251
        # short, does not trade
252
        open_orders = [
253
            Order(**{
254
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
255
                'amount': -100,
256
                'filled': 0,
257
                'sid': 133,
258
                'limit': 3.5})
259
        ]
260
261
        bar_data = BarData(self.data_portal,
262
                           lambda: self.minutes[0],
263
                           self.sim_params.data_frequency)
264
265
        orders_txns = list(slippage_model.simulate(
266
            bar_data[133],
267
            open_orders,
268
        ))
269
270
        self.assertEquals(len(orders_txns), 0)
271
272
        # short, does not trade - impacted price worse than limit price
273
        open_orders = [
274
            Order(**{
275
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
276
                'amount': -100,
277
                'filled': 0,
278
                'sid': 133,
279
                'limit': 3.5})
280
        ]
281
282
        bar_data = BarData(self.data_portal,
283
                           lambda: self.minutes[0],
284
                           self.sim_params.data_frequency)
285
286
        orders_txns = list(slippage_model.simulate(
287
            bar_data[133],
288
            open_orders,
289
        ))
290
291
        self.assertEquals(len(orders_txns), 0)
292
293
        # short, does trade
294
        open_orders = [
295
            Order(**{
296
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
297
                'amount': -100,
298
                'filled': 0,
299
                'sid': 133,
300
                'limit': 3.4})
301
        ]
302
303
        bar_data = BarData(self.data_portal,
304
                           lambda: self.minutes[1],
305
                           self.sim_params.data_frequency)
306
307
        orders_txns = list(slippage_model.simulate(
308
            bar_data[133],
309
            open_orders,
310
        ))
311
312
        self.assertEquals(len(orders_txns), 1)
313
        _, txn = orders_txns[0]
314
315
        expected_txn = {
316
            'price': float(3.49978125),
317
            'dt': datetime.datetime(
318
                2006, 1, 5, 14, 32, tzinfo=pytz.utc),
319
            'amount': int(-50),
320
            'sid': int(133)
321
        }
322
323
        self.assertIsNotNone(txn)
324
325
        for key, value in expected_txn.items():
326
            self.assertEquals(value, txn[key])
327
328
    STOP_ORDER_CASES = {
329
        # Stop orders can be long/short and have their price greater or
330
        # less than the stop.
331
        #
332
        # A stop being reached is conditional on the order direction.
333
        # Long orders reach the stop when the price is greater than the stop.
334
        # Short orders reach the stop when the price is less than the stop.
335
        #
336
        # Which leads to the following 4 cases:
337
        #
338
        #                    |   long   |   short  |
339
        # | price > stop     |          |          |
340
        # | price < stop     |          |          |
341
        #
342
        # Currently the slippage module acts according to the following table,
343
        # where 'X' represents triggering a transaction
344
        #                    |   long   |   short  |
345
        # | price > stop     |          |     X    |
346
        # | price < stop     |    X     |          |
347
        #
348
        # However, the following behavior *should* be followed.
349
        #
350
        #                    |   long   |   short  |
351
        # | price > stop     |    X     |          |
352
        # | price < stop     |          |     X    |
353
354
        'long | price gt stop': {
355
            'order': {
356
                'dt': pd.Timestamp('2006-01-05 14:30', tz='UTC'),
357
                'amount': 100,
358
                'filled': 0,
359
                'sid': 133,
360
                'stop': 3.5
361
            },
362
            'event': {
363
                'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'),
364
                'volume': 2000,
365
                'price': 4.0,
366
                'high': 3.15,
367
                'low': 2.85,
368
                'sid': 133,
369
                'close': 4.0,
370
                'open': 3.5
371
            },
372
            'expected': {
373
                'transaction': {
374
                    'price': 4.00025,
375
                    'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'),
376
                    'amount': 50,
377
                    'sid': 133,
378
                }
379
            }
380
        },
381
        'long | price lt stop': {
382
            'order': {
383
                'dt': pd.Timestamp('2006-01-05 14:30', tz='UTC'),
384
                'amount': 100,
385
                'filled': 0,
386
                'sid': 133,
387
                'stop': 3.6
388
            },
389
            'event': {
390
                'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'),
391
                'volume': 2000,
392
                'price': 3.5,
393
                'high': 3.15,
394
                'low': 2.85,
395
                'sid': 133,
396
                'close': 3.5,
397
                'open': 4.0
398
            },
399
            'expected': {
400
                'transaction': None
401
            }
402
        },
403
        'short | price gt stop': {
404
            'order': {
405
                'dt': pd.Timestamp('2006-01-05 14:30', tz='UTC'),
406
                'amount': -100,
407
                'filled': 0,
408
                'sid': 133,
409
                'stop': 3.4
410
            },
411
            'event': {
412
                'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'),
413
                'volume': 2000,
414
                'price': 3.5,
415
                'high': 3.15,
416
                'low': 2.85,
417
                'sid': 133,
418
                'close': 3.5,
419
                'open': 3.0
420
            },
421
            'expected': {
422
                'transaction': None
423
            }
424
        },
425
        'short | price lt stop': {
426
            'order': {
427
                'dt': pd.Timestamp('2006-01-05 14:30', tz='UTC'),
428
                'amount': -100,
429
                'filled': 0,
430
                'sid': 133,
431
                'stop': 3.5
432
            },
433
            'event': {
434
                'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'),
435
                'volume': 2000,
436
                'price': 3.0,
437
                'high': 3.15,
438
                'low': 2.85,
439
                'sid': 133,
440
                'close': 3.0,
441
                'open': 3.0
442
            },
443
            'expected': {
444
                'transaction': {
445
                    'price': 2.9998125,
446
                    'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'),
447
                    'amount': -50,
448
                    'sid': 133,
449
                }
450
            }
451
        },
452
    }
453
454
    @parameterized.expand([
455
        (name, case['order'], case['event'], case['expected'])
456
        for name, case in STOP_ORDER_CASES.items()
457
    ])
458
    def test_orders_stop(self, name, order_data, event_data, expected):
459
        tempdir = TempDirectory()
460
        try:
461
            order = Order(**order_data)
462
463
            assets = {
464
                133: pd.DataFrame({
465
                    "open": [event_data["open"] * 1000],
466
                    "high": [event_data["high"] * 1000],
467
                    "low": [event_data["low"] * 1000],
468
                    "close": [event_data["close"] * 1000],
469
                    "volume": [event_data["volume"]],
470
                    "minute": [pd.Timestamp('2006-01-05 14:31', tz='UTC')]
471
                })
472
            }
473
474
            MinuteBarWriterFromDataFrames(
475
                pd.Timestamp('2002-01-02', tz='UTC')
476
            ).write(tempdir.path, assets)
477
478
            equity_minute_reader = BcolzMinuteBarReader(tempdir.path)
479
480
            data_portal = DataPortal(
481
                self.env,
482
                equity_minute_reader=equity_minute_reader,
483
            )
484
485
            slippage_model = VolumeShareSlippage()
486
487
            try:
488
                dt = pd.Timestamp('2006-01-05 14:31', tz='UTC')
489
                bar_data = BarData(data_portal,
490
                                   lambda: dt,
491
                                   'minute')
492
                _, txn = next(slippage_model.simulate(
493
                    bar_data[133],
494
                    [order],
495
                ))
496
            except StopIteration:
497
                txn = None
498
499
            if expected['transaction'] is None:
500
                self.assertIsNone(txn)
501
            else:
502
                self.assertIsNotNone(txn)
503
504
                for key, value in expected['transaction'].items():
505
                    self.assertEquals(value, txn[key])
506
        finally:
507
            tempdir.cleanup()
508
509
    def test_orders_stop_limit(self):
510
        slippage_model = VolumeShareSlippage()
511
        slippage_model.data_portal = self.data_portal
512
513
        # long, does not trade
514
        open_orders = [
515
            Order(**{
516
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
517
                'amount': 100,
518
                'filled': 0,
519
                'sid': 133,
520
                'stop': 4.0,
521
                'limit': 3.0})
522
        ]
523
524
        bar_data = BarData(self.data_portal,
525
                           lambda: self.minutes[2],
526
                           self.sim_params.data_frequency)
527
528
        orders_txns = list(slippage_model.simulate(
529
            bar_data[133],
530
            open_orders,
531
        ))
532
533
        self.assertEquals(len(orders_txns), 0)
534
535
        bar_data = BarData(self.data_portal,
536
                           lambda: self.minutes[3],
537
                           self.sim_params.data_frequency)
538
539
        orders_txns = list(slippage_model.simulate(
540
            bar_data[133],
541
            open_orders,
542
        ))
543
544
        self.assertEquals(len(orders_txns), 0)
545
546
        # long, does not trade - impacted price worse than limit price
547
        open_orders = [
548
            Order(**{
549
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
550
                'amount': 100,
551
                'filled': 0,
552
                'sid': 133,
553
                'stop': 4.0,
554
                'limit': 3.5})
555
        ]
556
557
        bar_data = BarData(self.data_portal,
558
                           lambda: self.minutes[2],
559
                           self.sim_params.data_frequency)
560
561
        orders_txns = list(slippage_model.simulate(
562
            bar_data[133],
563
            open_orders,
564
        ))
565
566
        self.assertEquals(len(orders_txns), 0)
567
568
        bar_data = BarData(self.data_portal,
569
                           lambda: self.minutes[3],
570
                           self.sim_params.data_frequency)
571
572
        orders_txns = list(slippage_model.simulate(
573
            bar_data[133],
574
            open_orders,
575
        ))
576
577
        self.assertEquals(len(orders_txns), 0)
578
579
        # long, does trade
580
        open_orders = [
581
            Order(**{
582
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
583
                'amount': 100,
584
                'filled': 0,
585
                'sid': 133,
586
                'stop': 4.0,
587
                'limit': 3.6})
588
        ]
589
590
        bar_data = BarData(self.data_portal,
591
                           lambda: self.minutes[2],
592
                           self.sim_params.data_frequency)
593
594
        orders_txns = list(slippage_model.simulate(
595
            bar_data[133],
596
            open_orders,
597
        ))
598
599
        self.assertEquals(len(orders_txns), 0)
600
601
        bar_data = BarData(self.data_portal,
602
                           lambda: self.minutes[3],
603
                           self.sim_params.data_frequency)
604
605
        orders_txns = list(slippage_model.simulate(
606
            bar_data[133],
607
            open_orders,
608
        ))
609
610
        self.assertEquals(len(orders_txns), 1)
611
        _, txn = orders_txns[0]
612
613
        expected_txn = {
614
            'price': float(3.50021875),
615
            'dt': datetime.datetime(
616
                2006, 1, 5, 14, 34, tzinfo=pytz.utc),
617
            'amount': int(50),
618
            'sid': int(133)
619
        }
620
621
        for key, value in expected_txn.items():
622
            self.assertEquals(value, txn[key])
623
624
        # short, does not trade
625
626
        open_orders = [
627
            Order(**{
628
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
629
                'amount': -100,
630
                'filled': 0,
631
                'sid': 133,
632
                'stop': 3.0,
633
                'limit': 4.0})
634
        ]
635
636
        bar_data = BarData(self.data_portal,
637
                           lambda: self.minutes[0],
638
                           self.sim_params.data_frequency)
639
640
        orders_txns = list(slippage_model.simulate(
641
            bar_data[133],
642
            open_orders,
643
        ))
644
645
        self.assertEquals(len(orders_txns), 0)
646
647
        bar_data = BarData(self.data_portal,
648
                           lambda: self.minutes[1],
649
                           self.sim_params.data_frequency)
650
651
        orders_txns = list(slippage_model.simulate(
652
            bar_data[133],
653
            open_orders,
654
        ))
655
656
        self.assertEquals(len(orders_txns), 0)
657
658
        # short, does not trade - impacted price worse than limit price
659
        open_orders = [
660
            Order(**{
661
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
662
                'amount': -100,
663
                'filled': 0,
664
                'sid': 133,
665
                'stop': 3.0,
666
                'limit': 3.5})
667
        ]
668
669
        bar_data = BarData(self.data_portal,
670
                           lambda: self.minutes[0],
671
                           self.sim_params.data_frequency)
672
673
        orders_txns = list(slippage_model.simulate(
674
            bar_data[133],
675
            open_orders,
676
        ))
677
678
        self.assertEquals(len(orders_txns), 0)
679
680
        bar_data = BarData(self.data_portal,
681
                           lambda: self.minutes[1],
682
                           self.sim_params.data_frequency)
683
684
        orders_txns = list(slippage_model.simulate(
685
            bar_data[133],
686
            open_orders,
687
        ))
688
689
        self.assertEquals(len(orders_txns), 0)
690
691
        # short, does trade
692
        open_orders = [
693
            Order(**{
694
                'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
695
                'amount': -100,
696
                'filled': 0,
697
                'sid': 133,
698
                'stop': 3.0,
699
                'limit': 3.4})
700
        ]
701
702
        bar_data = BarData(self.data_portal,
703
                           lambda: self.minutes[0],
704
                           self.sim_params.data_frequency)
705
706
        orders_txns = list(slippage_model.simulate(
707
            bar_data[133],
708
            open_orders,
709
        ))
710
711
        self.assertEquals(len(orders_txns), 0)
712
713
        bar_data = BarData(self.data_portal,
714
                           lambda: self.minutes[1],
715
                           self.sim_params.data_frequency)
716
717
        orders_txns = list(slippage_model.simulate(
718
            bar_data[133],
719
            open_orders,
720
        ))
721
722
        self.assertEquals(len(orders_txns), 1)
723
        _, txn = orders_txns[0]
724
725
        expected_txn = {
726
            'price': float(3.49978125),
727
            'dt': datetime.datetime(
728
                2006, 1, 5, 14, 32, tzinfo=pytz.utc),
729
            'amount': int(-50),
730
            'sid': int(133)
731
        }
732
733
        for key, value in expected_txn.items():
734
            self.assertEquals(value, txn[key])
735