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): |
|
|
|
|
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): |
|
|
|
|
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: |
|
|
|
|
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: |
|
|
|
|
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
|
|
|
|
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.