BackcastPro 0.0.2__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of BackcastPro might be problematic. Click here for more details.
- BackcastPro/__init__.py +4 -86
- BackcastPro/_broker.py +390 -0
- BackcastPro/_stats.py +169 -212
- BackcastPro/backtest.py +269 -0
- BackcastPro/data/__init__.py +7 -0
- BackcastPro/data/datareader.py +168 -0
- BackcastPro/order.py +154 -0
- BackcastPro/position.py +61 -0
- BackcastPro/strategy.py +174 -0
- BackcastPro/trade.py +195 -0
- backcastpro-0.0.3.dist-info/METADATA +59 -0
- backcastpro-0.0.3.dist-info/RECORD +14 -0
- BackcastPro/_plotting.py +0 -785
- BackcastPro/_util.py +0 -337
- BackcastPro/backtesting.py +0 -1763
- BackcastPro/lib.py +0 -646
- BackcastPro/test/__init__.py +0 -29
- BackcastPro/test/__main__.py +0 -7
- BackcastPro/test/_test.py +0 -1174
- backcastpro-0.0.2.dist-info/METADATA +0 -53
- backcastpro-0.0.2.dist-info/RECORD +0 -13
- {backcastpro-0.0.2.dist-info → backcastpro-0.0.3.dist-info}/WHEEL +0 -0
- {backcastpro-0.0.2.dist-info → backcastpro-0.0.3.dist-info}/top_level.txt +0 -0
BackcastPro/test/_test.py
DELETED
|
@@ -1,1174 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
import multiprocessing as mp
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import time
|
|
6
|
-
import unittest
|
|
7
|
-
from concurrent.futures.process import ProcessPoolExecutor
|
|
8
|
-
from contextlib import contextmanager
|
|
9
|
-
from glob import glob
|
|
10
|
-
from runpy import run_path
|
|
11
|
-
from tempfile import NamedTemporaryFile, gettempdir
|
|
12
|
-
from unittest import TestCase
|
|
13
|
-
|
|
14
|
-
import numpy as np
|
|
15
|
-
import pandas as pd
|
|
16
|
-
from pandas.testing import assert_frame_equal
|
|
17
|
-
|
|
18
|
-
from BackcastPro import Backtest, Strategy
|
|
19
|
-
from BackcastPro._stats import compute_drawdown_duration_peaks
|
|
20
|
-
from BackcastPro._util import _Array, _as_str, _Indicator, patch, try_
|
|
21
|
-
from BackcastPro.lib import (
|
|
22
|
-
FractionalBacktest, MultiBacktest, OHLCV_AGG,
|
|
23
|
-
SignalStrategy,
|
|
24
|
-
TrailingStrategy,
|
|
25
|
-
barssince,
|
|
26
|
-
compute_stats,
|
|
27
|
-
cross,
|
|
28
|
-
crossover,
|
|
29
|
-
plot_heatmaps,
|
|
30
|
-
quantile,
|
|
31
|
-
random_ohlc_data,
|
|
32
|
-
resample_apply,
|
|
33
|
-
)
|
|
34
|
-
from BackcastPro.test import BTCUSD, EURUSD, GOOG, SMA
|
|
35
|
-
|
|
36
|
-
SHORT_DATA = GOOG.iloc[:20] # Short data for fast tests with no indicator lag
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@contextmanager
|
|
40
|
-
def _tempfile():
|
|
41
|
-
with NamedTemporaryFile(suffix='.html') as f:
|
|
42
|
-
if sys.platform.startswith('win'):
|
|
43
|
-
f.close()
|
|
44
|
-
yield f.name
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@contextmanager
|
|
48
|
-
def chdir(path):
|
|
49
|
-
cwd = os.getcwd()
|
|
50
|
-
os.chdir(path)
|
|
51
|
-
try:
|
|
52
|
-
yield
|
|
53
|
-
finally:
|
|
54
|
-
os.chdir(cwd)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class SmaCross(Strategy):
|
|
58
|
-
# NOTE: These values are also used on the website!
|
|
59
|
-
fast = 10
|
|
60
|
-
slow = 30
|
|
61
|
-
|
|
62
|
-
def init(self):
|
|
63
|
-
self.sma1 = self.I(SMA, self.data.Close, self.fast)
|
|
64
|
-
self.sma2 = self.I(SMA, self.data.Close, self.slow)
|
|
65
|
-
|
|
66
|
-
def next(self):
|
|
67
|
-
if crossover(self.sma1, self.sma2):
|
|
68
|
-
self.position.close()
|
|
69
|
-
self.buy()
|
|
70
|
-
elif crossover(self.sma2, self.sma1):
|
|
71
|
-
self.position.close()
|
|
72
|
-
self.sell()
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class _S(Strategy):
|
|
76
|
-
def init(self):
|
|
77
|
-
super().init()
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class TestBacktest(TestCase):
|
|
81
|
-
def test_run(self):
|
|
82
|
-
bt = Backtest(EURUSD, SmaCross)
|
|
83
|
-
bt.run()
|
|
84
|
-
|
|
85
|
-
def test_run_invalid_param(self):
|
|
86
|
-
bt = Backtest(GOOG, SmaCross)
|
|
87
|
-
self.assertRaises(AttributeError, bt.run, foo=3)
|
|
88
|
-
|
|
89
|
-
def test_run_speed(self):
|
|
90
|
-
bt = Backtest(GOOG, SmaCross)
|
|
91
|
-
start = time.process_time()
|
|
92
|
-
bt.run()
|
|
93
|
-
end = time.process_time()
|
|
94
|
-
self.assertLess(end - start, .3)
|
|
95
|
-
|
|
96
|
-
def test_data_missing_columns(self):
|
|
97
|
-
df = GOOG.copy(deep=False)
|
|
98
|
-
del df['Open']
|
|
99
|
-
with self.assertRaises(ValueError):
|
|
100
|
-
Backtest(df, SmaCross).run()
|
|
101
|
-
|
|
102
|
-
def test_data_nan_columns(self):
|
|
103
|
-
df = GOOG.copy()
|
|
104
|
-
df['Open'] = np.nan
|
|
105
|
-
with self.assertRaises(ValueError):
|
|
106
|
-
Backtest(df, SmaCross).run()
|
|
107
|
-
|
|
108
|
-
def test_data_extra_columns(self):
|
|
109
|
-
df = GOOG.copy(deep=False)
|
|
110
|
-
df['P/E'] = np.arange(len(df))
|
|
111
|
-
df['MCap'] = np.arange(len(df))
|
|
112
|
-
|
|
113
|
-
class S(Strategy):
|
|
114
|
-
def init(self):
|
|
115
|
-
assert len(self.data.MCap) == len(self.data.Close)
|
|
116
|
-
assert len(self.data['P/E']) == len(self.data.Close)
|
|
117
|
-
|
|
118
|
-
def next(self):
|
|
119
|
-
assert len(self.data.MCap) == len(self.data.Close)
|
|
120
|
-
assert len(self.data['P/E']) == len(self.data.Close)
|
|
121
|
-
|
|
122
|
-
Backtest(df, S).run()
|
|
123
|
-
|
|
124
|
-
def test_data_invalid(self):
|
|
125
|
-
with self.assertRaises(TypeError):
|
|
126
|
-
Backtest(GOOG.index, SmaCross).run()
|
|
127
|
-
with self.assertRaises(ValueError):
|
|
128
|
-
Backtest(GOOG.iloc[:0], SmaCross).run()
|
|
129
|
-
|
|
130
|
-
def test_assertions(self):
|
|
131
|
-
class Assertive(Strategy):
|
|
132
|
-
def init(self):
|
|
133
|
-
self.sma = self.I(SMA, self.data.Close, 10)
|
|
134
|
-
self.remains_indicator = np.r_[2] * np.cumsum(self.sma * 5 + 1) * np.r_[2]
|
|
135
|
-
|
|
136
|
-
self.transpose_invalid = self.I(lambda: np.column_stack((self.data.Open,
|
|
137
|
-
self.data.Close)))
|
|
138
|
-
|
|
139
|
-
resampled = resample_apply('W', SMA, self.data.Close, 3)
|
|
140
|
-
resampled_ind = resample_apply('W', SMA, self.sma, 3)
|
|
141
|
-
assert np.unique(resampled[-5:]).size == 1
|
|
142
|
-
assert np.unique(resampled[-6:]).size == 2
|
|
143
|
-
assert resampled in self._indicators, "Strategy.I not called"
|
|
144
|
-
assert resampled_ind in self._indicators, "Strategy.I not called"
|
|
145
|
-
|
|
146
|
-
assert 1 == try_(lambda: self.data.X, 1, AttributeError)
|
|
147
|
-
assert 1 == try_(lambda: self.data['X'], 1, KeyError)
|
|
148
|
-
|
|
149
|
-
assert self.data.pip == .01
|
|
150
|
-
|
|
151
|
-
assert float(self.data.Close) == self.data.Close[-1]
|
|
152
|
-
|
|
153
|
-
def next(self, _FEW_DAYS=pd.Timedelta('3 days')): # noqa: N803
|
|
154
|
-
assert self.equity >= 0
|
|
155
|
-
|
|
156
|
-
assert isinstance(self.sma, _Indicator)
|
|
157
|
-
assert isinstance(self.remains_indicator, _Indicator)
|
|
158
|
-
assert self.remains_indicator.name
|
|
159
|
-
assert isinstance(self.remains_indicator._opts, dict)
|
|
160
|
-
|
|
161
|
-
assert not np.isnan(self.data.Open[-1])
|
|
162
|
-
assert not np.isnan(self.data.High[-1])
|
|
163
|
-
assert not np.isnan(self.data.Low[-1])
|
|
164
|
-
assert not np.isnan(self.data.Close[-1])
|
|
165
|
-
assert not np.isnan(self.data.Volume[-1])
|
|
166
|
-
assert not np.isnan(self.sma[-1])
|
|
167
|
-
assert self.data.index[-1]
|
|
168
|
-
|
|
169
|
-
self.position
|
|
170
|
-
self.position.size
|
|
171
|
-
self.position.pl
|
|
172
|
-
self.position.pl_pct
|
|
173
|
-
self.position.is_long
|
|
174
|
-
|
|
175
|
-
if crossover(self.sma, self.data.Close):
|
|
176
|
-
self.orders.cancel() # cancels only non-contingent
|
|
177
|
-
price = self.data.Close[-1]
|
|
178
|
-
sl, tp = 1.05 * price, .9 * price
|
|
179
|
-
|
|
180
|
-
n_orders = len(self.orders)
|
|
181
|
-
self.sell(size=.21, limit=price, stop=price, sl=sl, tp=tp)
|
|
182
|
-
assert len(self.orders) == n_orders + 1
|
|
183
|
-
|
|
184
|
-
order = self.orders[-1]
|
|
185
|
-
assert order.limit == price
|
|
186
|
-
assert order.stop == price
|
|
187
|
-
assert order.size == -.21
|
|
188
|
-
assert order.sl == sl
|
|
189
|
-
assert order.tp == tp
|
|
190
|
-
assert not order.is_contingent
|
|
191
|
-
|
|
192
|
-
elif self.position:
|
|
193
|
-
assert not self.position.is_long
|
|
194
|
-
assert self.position.is_short
|
|
195
|
-
assert self.position.pl
|
|
196
|
-
assert self.position.pl_pct
|
|
197
|
-
assert self.position.size < 0
|
|
198
|
-
|
|
199
|
-
trade = self.trades[0]
|
|
200
|
-
if self.data.index[-1] - self.data.index[trade.entry_bar] > _FEW_DAYS:
|
|
201
|
-
assert not trade.is_long
|
|
202
|
-
assert trade.is_short
|
|
203
|
-
assert trade.size < 0
|
|
204
|
-
assert trade.entry_bar > 0
|
|
205
|
-
assert isinstance(trade.entry_time, pd.Timestamp)
|
|
206
|
-
assert trade.exit_bar is None
|
|
207
|
-
assert trade.exit_time is None
|
|
208
|
-
assert trade.entry_price > 0
|
|
209
|
-
assert trade.exit_price is None
|
|
210
|
-
assert trade.pl / 1
|
|
211
|
-
assert trade.pl_pct / 1
|
|
212
|
-
assert trade.value > 0
|
|
213
|
-
assert trade.sl
|
|
214
|
-
assert trade.tp
|
|
215
|
-
# Close multiple times
|
|
216
|
-
self.position.close(.5)
|
|
217
|
-
self.position.close(.5)
|
|
218
|
-
self.position.close(.5)
|
|
219
|
-
self.position.close()
|
|
220
|
-
self.position.close()
|
|
221
|
-
|
|
222
|
-
bt = Backtest(GOOG, Assertive)
|
|
223
|
-
with self.assertWarns(UserWarning):
|
|
224
|
-
stats = bt.run()
|
|
225
|
-
self.assertEqual(stats['# Trades'], 131)
|
|
226
|
-
|
|
227
|
-
def test_broker_params(self):
|
|
228
|
-
bt = Backtest(GOOG.iloc[:100], SmaCross,
|
|
229
|
-
cash=1000, spread=.01, margin=.1, trade_on_close=True)
|
|
230
|
-
bt.run()
|
|
231
|
-
|
|
232
|
-
def test_spread_commission(self):
|
|
233
|
-
class S(Strategy):
|
|
234
|
-
def init(self):
|
|
235
|
-
self.done = False
|
|
236
|
-
|
|
237
|
-
def next(self):
|
|
238
|
-
if not self.position:
|
|
239
|
-
self.buy()
|
|
240
|
-
else:
|
|
241
|
-
self.position.close()
|
|
242
|
-
self.next = lambda: None # Done
|
|
243
|
-
|
|
244
|
-
SPREAD = .01
|
|
245
|
-
COMMISSION = .01
|
|
246
|
-
CASH = 10_000
|
|
247
|
-
ORDER_BAR = 2
|
|
248
|
-
stats = Backtest(SHORT_DATA, S, cash=CASH, spread=SPREAD, commission=COMMISSION).run()
|
|
249
|
-
trade_open_price = SHORT_DATA['Open'].iloc[ORDER_BAR]
|
|
250
|
-
self.assertEqual(stats['_trades']['EntryPrice'].iloc[0], trade_open_price * (1 + SPREAD))
|
|
251
|
-
self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(),
|
|
252
|
-
[9685.31, 9749.33])
|
|
253
|
-
|
|
254
|
-
stats = Backtest(SHORT_DATA, S, cash=CASH, commission=(100, COMMISSION)).run()
|
|
255
|
-
self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(),
|
|
256
|
-
[9784.50, 9718.69])
|
|
257
|
-
|
|
258
|
-
commission_func = lambda size, price: size * price * COMMISSION # noqa: E731
|
|
259
|
-
stats = Backtest(SHORT_DATA, S, cash=CASH, commission=commission_func).run()
|
|
260
|
-
self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(),
|
|
261
|
-
[9781.28, 9846.04])
|
|
262
|
-
|
|
263
|
-
def test_commissions(self):
|
|
264
|
-
class S(_S):
|
|
265
|
-
def next(self):
|
|
266
|
-
if len(self.data) == 2:
|
|
267
|
-
self.buy(size=SIZE, tp=3)
|
|
268
|
-
|
|
269
|
-
FIXED_COMMISSION, COMMISSION = 10, .01
|
|
270
|
-
CASH, SIZE, PRICE_ENTRY, PRICE_EXIT = 5000, 100, 1, 4
|
|
271
|
-
arr = np.r_[1, PRICE_ENTRY, 1, 2, PRICE_EXIT, 1, 2]
|
|
272
|
-
df = pd.DataFrame({'Open': arr, 'High': arr, 'Low': arr, 'Close': arr})
|
|
273
|
-
with self.assertWarnsRegex(UserWarning, 'index is not datetime'):
|
|
274
|
-
stats = Backtest(df, S, cash=CASH, commission=(FIXED_COMMISSION, COMMISSION)).run()
|
|
275
|
-
EXPECTED_PAID_COMMISSION = (
|
|
276
|
-
FIXED_COMMISSION + COMMISSION * SIZE * PRICE_ENTRY +
|
|
277
|
-
FIXED_COMMISSION + COMMISSION * SIZE * PRICE_EXIT)
|
|
278
|
-
self.assertEqual(stats['Commissions [$]'], EXPECTED_PAID_COMMISSION)
|
|
279
|
-
self.assertEqual(stats._trades['Commission'][0], EXPECTED_PAID_COMMISSION)
|
|
280
|
-
self.assertEqual(
|
|
281
|
-
stats['Equity Final [$]'],
|
|
282
|
-
CASH + (PRICE_EXIT - PRICE_ENTRY) * SIZE - EXPECTED_PAID_COMMISSION)
|
|
283
|
-
|
|
284
|
-
def test_dont_overwrite_data(self):
|
|
285
|
-
df = EURUSD.copy()
|
|
286
|
-
bt = Backtest(df, SmaCross)
|
|
287
|
-
bt.run()
|
|
288
|
-
bt.optimize(fast=4, slow=[6, 8])
|
|
289
|
-
bt.plot(plot_drawdown=True, open_browser=False)
|
|
290
|
-
self.assertTrue(df.equals(EURUSD))
|
|
291
|
-
|
|
292
|
-
def test_strategy_abstract(self):
|
|
293
|
-
class MyStrategy(Strategy):
|
|
294
|
-
pass
|
|
295
|
-
|
|
296
|
-
self.assertRaises(TypeError, MyStrategy, None, None)
|
|
297
|
-
|
|
298
|
-
def test_strategy_str(self):
|
|
299
|
-
bt = Backtest(GOOG.iloc[:100], SmaCross)
|
|
300
|
-
self.assertEqual(str(bt.run()._strategy), SmaCross.__name__)
|
|
301
|
-
self.assertEqual(str(bt.run(fast=11)._strategy), SmaCross.__name__ + '(fast=11)')
|
|
302
|
-
|
|
303
|
-
def test_compute_drawdown(self):
|
|
304
|
-
dd = pd.Series([0, 1, 7, 0, 4, 0, 0])
|
|
305
|
-
durations, peaks = compute_drawdown_duration_peaks(dd)
|
|
306
|
-
np.testing.assert_array_equal(durations, pd.Series([3, 2], index=[3, 5]).reindex(dd.index))
|
|
307
|
-
np.testing.assert_array_equal(peaks, pd.Series([7, 4], index=[3, 5]).reindex(dd.index))
|
|
308
|
-
|
|
309
|
-
def test_compute_stats(self):
|
|
310
|
-
stats = Backtest(GOOG, SmaCross, finalize_trades=True).run()
|
|
311
|
-
expected = pd.Series({
|
|
312
|
-
# NOTE: These values are also used on the website! # noqa: E126
|
|
313
|
-
'# Trades': 66,
|
|
314
|
-
'Avg. Drawdown Duration': pd.Timedelta('41 days 00:00:00'),
|
|
315
|
-
'Avg. Drawdown [%]': -5.925851581948801,
|
|
316
|
-
'Avg. Trade Duration': pd.Timedelta('46 days 00:00:00'),
|
|
317
|
-
'Avg. Trade [%]': 2.531715975158555,
|
|
318
|
-
'Best Trade [%]': 53.59595229490424,
|
|
319
|
-
'Buy & Hold Return [%]': 522.0601851851852,
|
|
320
|
-
'Calmar Ratio': 0.4414380935608377,
|
|
321
|
-
'Duration': pd.Timedelta('3116 days 00:00:00'),
|
|
322
|
-
'End': pd.Timestamp('2013-03-01 00:00:00'),
|
|
323
|
-
'Equity Final [$]': 51422.98999999996,
|
|
324
|
-
'Equity Peak [$]': 75787.44,
|
|
325
|
-
'Expectancy [%]': 3.2748078066748834,
|
|
326
|
-
'Exposure Time [%]': 96.74115456238361,
|
|
327
|
-
'Max. Drawdown Duration': pd.Timedelta('584 days 00:00:00'),
|
|
328
|
-
'Max. Drawdown [%]': -47.98012705007589,
|
|
329
|
-
'Max. Trade Duration': pd.Timedelta('183 days 00:00:00'),
|
|
330
|
-
'Profit Factor': 2.167945974262033,
|
|
331
|
-
'Return (Ann.) [%]': 21.180255813792282,
|
|
332
|
-
'Return [%]': 414.2298999999996,
|
|
333
|
-
'Volatility (Ann.) [%]': 36.49390889140787,
|
|
334
|
-
'CAGR [%]': 14.159843619607383,
|
|
335
|
-
'SQN': 1.0766187356697705,
|
|
336
|
-
'Kelly Criterion': 0.1518705127029717,
|
|
337
|
-
'Sharpe Ratio': 0.5803778344714113,
|
|
338
|
-
'Sortino Ratio': 1.0847880675854096,
|
|
339
|
-
'Start': pd.Timestamp('2004-08-19 00:00:00'),
|
|
340
|
-
'Win Rate [%]': 46.96969696969697,
|
|
341
|
-
'Worst Trade [%]': -18.39887353835481,
|
|
342
|
-
'Alpha [%]': 394.37391142027462,
|
|
343
|
-
'Beta': 0.03803390709192,
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
def almost_equal(a, b):
|
|
347
|
-
try:
|
|
348
|
-
return np.isclose(a, b, rtol=1.e-8)
|
|
349
|
-
except TypeError:
|
|
350
|
-
return a == b
|
|
351
|
-
|
|
352
|
-
diff = {key: print(key) or value # noqa: T201
|
|
353
|
-
for key, value in stats.filter(regex='^[^_]').items()
|
|
354
|
-
if not almost_equal(value, expected[key])}
|
|
355
|
-
self.assertDictEqual(diff, {})
|
|
356
|
-
|
|
357
|
-
self.assertSequenceEqual(
|
|
358
|
-
sorted(stats['_equity_curve'].columns),
|
|
359
|
-
sorted(['Equity', 'DrawdownPct', 'DrawdownDuration']))
|
|
360
|
-
|
|
361
|
-
self.assertEqual(len(stats['_trades']), 66)
|
|
362
|
-
|
|
363
|
-
indicator_columns = [
|
|
364
|
-
f'{entry}_SMA(C,{n})'
|
|
365
|
-
for entry in ('Entry', 'Exit')
|
|
366
|
-
for n in (SmaCross.fast, SmaCross.slow)]
|
|
367
|
-
self.assertSequenceEqual(
|
|
368
|
-
sorted(stats['_trades'].columns),
|
|
369
|
-
sorted(['Size', 'EntryBar', 'ExitBar', 'EntryPrice', 'ExitPrice',
|
|
370
|
-
'SL', 'TP', 'PnL', 'ReturnPct', 'EntryTime', 'ExitTime',
|
|
371
|
-
'Duration', 'Tag', 'Commission',
|
|
372
|
-
*indicator_columns]))
|
|
373
|
-
|
|
374
|
-
def test_compute_stats_bordercase(self):
|
|
375
|
-
|
|
376
|
-
class SingleTrade(Strategy):
|
|
377
|
-
def init(self):
|
|
378
|
-
self._done = False
|
|
379
|
-
|
|
380
|
-
def next(self):
|
|
381
|
-
if not self._done:
|
|
382
|
-
self.buy()
|
|
383
|
-
self._done = True
|
|
384
|
-
if self.position:
|
|
385
|
-
self.position.close()
|
|
386
|
-
|
|
387
|
-
class SinglePosition(_S):
|
|
388
|
-
def next(self):
|
|
389
|
-
if not self.position:
|
|
390
|
-
self.buy()
|
|
391
|
-
|
|
392
|
-
class NoTrade(_S):
|
|
393
|
-
def next(self):
|
|
394
|
-
pass
|
|
395
|
-
|
|
396
|
-
for strategy in (SmaCross,
|
|
397
|
-
SingleTrade,
|
|
398
|
-
SinglePosition,
|
|
399
|
-
NoTrade):
|
|
400
|
-
with self.subTest(strategy=strategy.__name__):
|
|
401
|
-
stats = Backtest(GOOG.iloc[:100], strategy).run()
|
|
402
|
-
|
|
403
|
-
self.assertFalse(np.isnan(stats['Equity Final [$]']))
|
|
404
|
-
self.assertFalse(stats['_equity_curve']['Equity'].isnull().any())
|
|
405
|
-
self.assertEqual(stats['_strategy'].__class__, strategy)
|
|
406
|
-
|
|
407
|
-
def test_trade_enter_hit_sl_on_same_day(self):
|
|
408
|
-
the_day = pd.Timestamp("2012-10-17 00:00:00")
|
|
409
|
-
|
|
410
|
-
class S(_S):
|
|
411
|
-
def next(self):
|
|
412
|
-
if self.data.index[-1] == the_day:
|
|
413
|
-
self.buy(sl=720)
|
|
414
|
-
|
|
415
|
-
self.assertEqual(Backtest(GOOG, S).run()._trades.iloc[0].ExitPrice, 720)
|
|
416
|
-
|
|
417
|
-
class S(_S):
|
|
418
|
-
def next(self):
|
|
419
|
-
if self.data.index[-1] == the_day:
|
|
420
|
-
self.buy(stop=758, sl=720)
|
|
421
|
-
|
|
422
|
-
with self.assertWarns(UserWarning):
|
|
423
|
-
self.assertEqual(Backtest(GOOG, S).run()._trades.iloc[0].ExitPrice, 705.58)
|
|
424
|
-
|
|
425
|
-
def test_stop_price_between_sl_tp(self):
|
|
426
|
-
class S(_S):
|
|
427
|
-
def next(self):
|
|
428
|
-
if self.data.index[-1] == pd.Timestamp("2004-09-09 00:00:00"):
|
|
429
|
-
self.buy(stop=104, sl=103, tp=110)
|
|
430
|
-
|
|
431
|
-
with self.assertWarns(UserWarning):
|
|
432
|
-
self.assertEqual(Backtest(GOOG, S).run()._trades.iloc[0].EntryPrice, 104)
|
|
433
|
-
|
|
434
|
-
def test_position_close_portion(self):
|
|
435
|
-
class SmaCross(Strategy):
|
|
436
|
-
def init(self):
|
|
437
|
-
self.sma1 = self.I(SMA, self.data.Close, 10)
|
|
438
|
-
self.sma2 = self.I(SMA, self.data.Close, 20)
|
|
439
|
-
|
|
440
|
-
def next(self):
|
|
441
|
-
if not self.position and crossover(self.sma1, self.sma2):
|
|
442
|
-
self.buy(size=10)
|
|
443
|
-
if self.position and crossover(self.sma2, self.sma1):
|
|
444
|
-
self.position.close(portion=.5)
|
|
445
|
-
|
|
446
|
-
bt = Backtest(GOOG, SmaCross, spread=.002)
|
|
447
|
-
bt.run()
|
|
448
|
-
|
|
449
|
-
def test_close_orders_from_last_strategy_iteration(self):
|
|
450
|
-
class S(_S):
|
|
451
|
-
def next(self):
|
|
452
|
-
if not self.position:
|
|
453
|
-
self.buy()
|
|
454
|
-
elif len(self.data) == len(SHORT_DATA):
|
|
455
|
-
self.position.close()
|
|
456
|
-
|
|
457
|
-
with self.assertWarnsRegex(UserWarning, 'finalize_trades'):
|
|
458
|
-
self.assertTrue(Backtest(SHORT_DATA, S, finalize_trades=False).run()._trades.empty)
|
|
459
|
-
self.assertFalse(Backtest(SHORT_DATA, S, finalize_trades=True).run()._trades.empty)
|
|
460
|
-
|
|
461
|
-
def test_check_adjusted_price_when_placing_order(self):
|
|
462
|
-
class S(_S):
|
|
463
|
-
def next(self):
|
|
464
|
-
self.buy(tp=self.data.Close * 1.01)
|
|
465
|
-
|
|
466
|
-
self.assertRaises(ValueError, Backtest(SHORT_DATA, S, spread=.02).run)
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
class TestStrategy(TestCase):
|
|
470
|
-
@staticmethod
|
|
471
|
-
def _Backtest(strategy_coroutine, data=SHORT_DATA, **kwargs):
|
|
472
|
-
class S(Strategy):
|
|
473
|
-
def init(self):
|
|
474
|
-
self.step = strategy_coroutine(self)
|
|
475
|
-
|
|
476
|
-
def next(self):
|
|
477
|
-
try_(self.step.__next__, None, StopIteration)
|
|
478
|
-
|
|
479
|
-
return Backtest(data, S, **kwargs)
|
|
480
|
-
|
|
481
|
-
def test_position(self):
|
|
482
|
-
def coroutine(self):
|
|
483
|
-
yield self.buy()
|
|
484
|
-
|
|
485
|
-
assert self.position
|
|
486
|
-
assert self.position.is_long
|
|
487
|
-
assert not self.position.is_short
|
|
488
|
-
assert self.position.size > 0
|
|
489
|
-
assert self.position.pl
|
|
490
|
-
assert self.position.pl_pct
|
|
491
|
-
|
|
492
|
-
yield self.position.close()
|
|
493
|
-
|
|
494
|
-
assert not self.position
|
|
495
|
-
assert not self.position.is_long
|
|
496
|
-
assert not self.position.is_short
|
|
497
|
-
assert not self.position.size
|
|
498
|
-
assert not self.position.pl
|
|
499
|
-
assert not self.position.pl_pct
|
|
500
|
-
|
|
501
|
-
self._Backtest(coroutine).run()
|
|
502
|
-
|
|
503
|
-
def test_broker_hedging(self):
|
|
504
|
-
def coroutine(self):
|
|
505
|
-
yield self.buy(size=2)
|
|
506
|
-
|
|
507
|
-
assert len(self.trades) == 1
|
|
508
|
-
yield self.sell(size=1)
|
|
509
|
-
|
|
510
|
-
assert len(self.trades) == 2
|
|
511
|
-
|
|
512
|
-
self._Backtest(coroutine, hedging=True).run()
|
|
513
|
-
|
|
514
|
-
def test_broker_exclusive_orders(self):
|
|
515
|
-
def coroutine(self):
|
|
516
|
-
yield self.buy(size=2)
|
|
517
|
-
|
|
518
|
-
assert len(self.trades) == 1
|
|
519
|
-
yield self.sell(size=3)
|
|
520
|
-
|
|
521
|
-
assert len(self.trades) == 1
|
|
522
|
-
assert self.trades[0].size == -3
|
|
523
|
-
|
|
524
|
-
self._Backtest(coroutine, exclusive_orders=True).run()
|
|
525
|
-
|
|
526
|
-
def test_trade_multiple_close(self):
|
|
527
|
-
def coroutine(self):
|
|
528
|
-
yield self.buy()
|
|
529
|
-
|
|
530
|
-
assert self.trades
|
|
531
|
-
self.trades[-1].close(1)
|
|
532
|
-
self.trades[-1].close(.1)
|
|
533
|
-
yield
|
|
534
|
-
|
|
535
|
-
self._Backtest(coroutine).run()
|
|
536
|
-
|
|
537
|
-
def test_close_trade_leaves_needsize_0(self):
|
|
538
|
-
def coroutine(self):
|
|
539
|
-
self.buy(size=1)
|
|
540
|
-
self.buy(size=1)
|
|
541
|
-
yield
|
|
542
|
-
if self.position:
|
|
543
|
-
self.sell(size=1)
|
|
544
|
-
|
|
545
|
-
self._Backtest(coroutine).run()
|
|
546
|
-
|
|
547
|
-
def test_stop_limit_order_price_is_stop_price(self):
|
|
548
|
-
def coroutine(self):
|
|
549
|
-
self.buy(stop=112, limit=113, size=1)
|
|
550
|
-
self.sell(stop=107, limit=105, size=1)
|
|
551
|
-
yield
|
|
552
|
-
|
|
553
|
-
stats = self._Backtest(coroutine).run()
|
|
554
|
-
self.assertListEqual(stats._trades.filter(like='Price').stack().tolist(), [112, 107])
|
|
555
|
-
|
|
556
|
-
def test_autoclose_trades_on_finish(self):
|
|
557
|
-
def coroutine(self):
|
|
558
|
-
yield self.buy()
|
|
559
|
-
|
|
560
|
-
stats = self._Backtest(coroutine, finalize_trades=True).run()
|
|
561
|
-
self.assertEqual(len(stats._trades), 1)
|
|
562
|
-
|
|
563
|
-
def test_order_tag(self):
|
|
564
|
-
def coroutine(self):
|
|
565
|
-
yield self.buy(size=2, tag=1)
|
|
566
|
-
yield self.sell(size=1, tag='s')
|
|
567
|
-
yield self.sell(size=1)
|
|
568
|
-
|
|
569
|
-
yield self.buy(tag=2)
|
|
570
|
-
yield self.position.close()
|
|
571
|
-
|
|
572
|
-
stats = self._Backtest(coroutine).run()
|
|
573
|
-
self.assertEqual(list(stats._trades.Tag), [1, 1, 2])
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
class TestOptimize(TestCase):
|
|
577
|
-
def test_optimize(self):
|
|
578
|
-
bt = Backtest(GOOG.iloc[:100], SmaCross)
|
|
579
|
-
OPT_PARAMS = {'fast': range(2, 5, 2), 'slow': [2, 5, 7, 9]}
|
|
580
|
-
|
|
581
|
-
self.assertRaises(ValueError, bt.optimize)
|
|
582
|
-
self.assertRaises(ValueError, bt.optimize, maximize='missing key', **OPT_PARAMS)
|
|
583
|
-
self.assertRaises(ValueError, bt.optimize, maximize='missing key', **OPT_PARAMS)
|
|
584
|
-
self.assertRaises(TypeError, bt.optimize, maximize=15, **OPT_PARAMS)
|
|
585
|
-
self.assertRaises(TypeError, bt.optimize, constraint=15, **OPT_PARAMS)
|
|
586
|
-
self.assertRaises(ValueError, bt.optimize, constraint=lambda d: False, **OPT_PARAMS)
|
|
587
|
-
self.assertRaises(ValueError, bt.optimize, return_optimization=True, **OPT_PARAMS)
|
|
588
|
-
|
|
589
|
-
res = bt.optimize(**OPT_PARAMS)
|
|
590
|
-
self.assertIsInstance(res, pd.Series)
|
|
591
|
-
|
|
592
|
-
default_maximize = inspect.signature(Backtest.optimize).parameters['maximize'].default
|
|
593
|
-
res2 = bt.optimize(**OPT_PARAMS, maximize=lambda s: s[default_maximize])
|
|
594
|
-
self.assertDictEqual(res.filter(regex='^[^_]').fillna(-1).to_dict(),
|
|
595
|
-
res2.filter(regex='^[^_]').fillna(-1).to_dict())
|
|
596
|
-
|
|
597
|
-
res3, heatmap = bt.optimize(**OPT_PARAMS, return_heatmap=True,
|
|
598
|
-
constraint=lambda d: d.slow > 2 * d.fast)
|
|
599
|
-
self.assertIsInstance(heatmap, pd.Series)
|
|
600
|
-
self.assertEqual(len(heatmap), 4)
|
|
601
|
-
self.assertEqual(heatmap.name, default_maximize)
|
|
602
|
-
|
|
603
|
-
with _tempfile() as f:
|
|
604
|
-
bt.plot(filename=f, open_browser=False)
|
|
605
|
-
|
|
606
|
-
def test_method_sambo(self):
|
|
607
|
-
bt = Backtest(GOOG.iloc[:100], SmaCross, finalize_trades=True)
|
|
608
|
-
res, heatmap, sambo_results = bt.optimize(
|
|
609
|
-
fast=range(2, 20), slow=np.arange(2, 20, dtype=object),
|
|
610
|
-
constraint=lambda p: p.fast < p.slow,
|
|
611
|
-
max_tries=30,
|
|
612
|
-
method='sambo',
|
|
613
|
-
return_optimization=True,
|
|
614
|
-
return_heatmap=True,
|
|
615
|
-
random_state=2)
|
|
616
|
-
self.assertIsInstance(res, pd.Series)
|
|
617
|
-
self.assertIsInstance(heatmap, pd.Series)
|
|
618
|
-
self.assertGreater(heatmap.max(), 1.1)
|
|
619
|
-
self.assertGreater(heatmap.min(), -2)
|
|
620
|
-
self.assertEqual(-sambo_results.fun, heatmap.max())
|
|
621
|
-
self.assertEqual(heatmap.index.tolist(), heatmap.dropna().index.unique().tolist())
|
|
622
|
-
|
|
623
|
-
def test_max_tries(self):
|
|
624
|
-
bt = Backtest(GOOG.iloc[:100], SmaCross)
|
|
625
|
-
OPT_PARAMS = {'fast': range(2, 10, 2), 'slow': [2, 5, 7, 9]}
|
|
626
|
-
for method, max_tries, random_state in (('grid', 5, 0),
|
|
627
|
-
('grid', .3, 0),
|
|
628
|
-
('sambo', 6, 0),
|
|
629
|
-
('sambo', .42, 0)):
|
|
630
|
-
with self.subTest(method=method,
|
|
631
|
-
max_tries=max_tries,
|
|
632
|
-
random_state=random_state):
|
|
633
|
-
_, heatmap = bt.optimize(max_tries=max_tries,
|
|
634
|
-
method=method,
|
|
635
|
-
random_state=random_state,
|
|
636
|
-
return_heatmap=True,
|
|
637
|
-
**OPT_PARAMS)
|
|
638
|
-
self.assertEqual(len(heatmap), 6)
|
|
639
|
-
|
|
640
|
-
def test_optimize_invalid_param(self):
|
|
641
|
-
bt = Backtest(GOOG.iloc[:100], SmaCross)
|
|
642
|
-
self.assertRaises(AttributeError, bt.optimize, foo=range(3))
|
|
643
|
-
self.assertRaises(ValueError, bt.optimize, fast=[])
|
|
644
|
-
|
|
645
|
-
def test_optimize_no_trades(self):
|
|
646
|
-
bt = Backtest(GOOG, SmaCross)
|
|
647
|
-
stats = bt.optimize(fast=[3], slow=[3])
|
|
648
|
-
self.assertTrue(stats.isnull().any())
|
|
649
|
-
|
|
650
|
-
def test_optimize_speed(self):
|
|
651
|
-
bt = Backtest(GOOG.iloc[:100], SmaCross)
|
|
652
|
-
start = time.process_time()
|
|
653
|
-
bt.optimize(fast=range(2, 20, 2), slow=range(10, 40, 2))
|
|
654
|
-
end = time.process_time()
|
|
655
|
-
print(end - start)
|
|
656
|
-
handicap = 5 if 'win' in sys.platform else .1
|
|
657
|
-
self.assertLess(end - start, .3 + handicap)
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
class TestPlot(TestCase):
|
|
661
|
-
def test_plot_before_run(self):
|
|
662
|
-
bt = Backtest(GOOG, SmaCross)
|
|
663
|
-
self.assertRaises(RuntimeError, bt.plot)
|
|
664
|
-
|
|
665
|
-
def test_file_size(self):
|
|
666
|
-
bt = Backtest(GOOG, SmaCross)
|
|
667
|
-
bt.run()
|
|
668
|
-
with _tempfile() as f:
|
|
669
|
-
bt.plot(filename=f[:-len('.html')], open_browser=False)
|
|
670
|
-
self.assertLess(os.path.getsize(f), 500000)
|
|
671
|
-
|
|
672
|
-
def test_params(self):
|
|
673
|
-
bt = Backtest(GOOG.iloc[:100], SmaCross)
|
|
674
|
-
bt.run()
|
|
675
|
-
with _tempfile() as f:
|
|
676
|
-
for p in dict(plot_volume=False, # noqa: C408
|
|
677
|
-
plot_equity=False,
|
|
678
|
-
plot_return=True,
|
|
679
|
-
plot_pl=False,
|
|
680
|
-
plot_drawdown=True,
|
|
681
|
-
plot_trades=False,
|
|
682
|
-
superimpose=False,
|
|
683
|
-
resample='1W',
|
|
684
|
-
smooth_equity=False,
|
|
685
|
-
relative_equity=False,
|
|
686
|
-
reverse_indicators=True,
|
|
687
|
-
show_legend=False).items():
|
|
688
|
-
with self.subTest(param=p[0]):
|
|
689
|
-
bt.plot(**dict([p]), filename=f, open_browser=False)
|
|
690
|
-
|
|
691
|
-
def test_hide_legend(self):
|
|
692
|
-
bt = Backtest(GOOG.iloc[:100], SmaCross)
|
|
693
|
-
bt.run()
|
|
694
|
-
with _tempfile() as f:
|
|
695
|
-
bt.plot(filename=f, show_legend=False)
|
|
696
|
-
# Give browser time to open before tempfile is removed
|
|
697
|
-
time.sleep(5)
|
|
698
|
-
|
|
699
|
-
def test_resolutions(self):
|
|
700
|
-
with _tempfile() as f:
|
|
701
|
-
for rule in 'ms s min h D W ME'.split():
|
|
702
|
-
with self.subTest(rule=rule):
|
|
703
|
-
df = EURUSD.iloc[:2].resample(rule).agg(OHLCV_AGG).dropna().iloc[:1100]
|
|
704
|
-
bt = Backtest(df, SmaCross)
|
|
705
|
-
bt.run()
|
|
706
|
-
bt.plot(filename=f, open_browser=False)
|
|
707
|
-
|
|
708
|
-
def test_range_axis(self):
|
|
709
|
-
df = GOOG.iloc[:100].reset_index(drop=True)
|
|
710
|
-
|
|
711
|
-
# Warm-up. CPython bug bpo-29620.
|
|
712
|
-
try:
|
|
713
|
-
with self.assertWarns(UserWarning):
|
|
714
|
-
Backtest(df, SmaCross)
|
|
715
|
-
except RuntimeError:
|
|
716
|
-
pass
|
|
717
|
-
|
|
718
|
-
with self.assertWarns(UserWarning):
|
|
719
|
-
bt = Backtest(df, SmaCross)
|
|
720
|
-
bt.run()
|
|
721
|
-
with _tempfile() as f:
|
|
722
|
-
bt.plot(filename=f, open_browser=False)
|
|
723
|
-
|
|
724
|
-
def test_preview(self):
|
|
725
|
-
class Strategy(SmaCross):
|
|
726
|
-
def init(self):
|
|
727
|
-
super().init()
|
|
728
|
-
|
|
729
|
-
def ok(x):
|
|
730
|
-
return x
|
|
731
|
-
|
|
732
|
-
self.a = self.I(SMA, self.data.Open, 5, overlay=False, name='ok')
|
|
733
|
-
self.b = self.I(ok, np.random.random(len(self.data.Open)))
|
|
734
|
-
|
|
735
|
-
bt = Backtest(GOOG, Strategy)
|
|
736
|
-
bt.run()
|
|
737
|
-
with _tempfile() as f:
|
|
738
|
-
bt.plot(filename=f, plot_drawdown=True, smooth_equity=True)
|
|
739
|
-
# Give browser time to open before tempfile is removed
|
|
740
|
-
time.sleep(5)
|
|
741
|
-
|
|
742
|
-
def test_wellknown(self):
|
|
743
|
-
class S(_S):
|
|
744
|
-
def next(self):
|
|
745
|
-
date = self.data.index[-1]
|
|
746
|
-
if date == pd.Timestamp('Thu 19 Oct 2006'):
|
|
747
|
-
self.buy(stop=484, limit=466, size=100)
|
|
748
|
-
elif date == pd.Timestamp('Thu 30 Oct 2007'):
|
|
749
|
-
self.position.close()
|
|
750
|
-
elif date == pd.Timestamp('Tue 11 Nov 2008'):
|
|
751
|
-
self.sell(stop=self.data.Low,
|
|
752
|
-
limit=324.90, # High from 14 Nov
|
|
753
|
-
size=200)
|
|
754
|
-
|
|
755
|
-
bt = Backtest(GOOG, S, margin=.1)
|
|
756
|
-
stats = bt.run()
|
|
757
|
-
trades = stats['_trades']
|
|
758
|
-
|
|
759
|
-
self.assertAlmostEqual(stats['Equity Peak [$]'], 46961)
|
|
760
|
-
self.assertEqual(stats['Equity Final [$]'], 0)
|
|
761
|
-
self.assertEqual(len(trades), 2)
|
|
762
|
-
assert trades[['EntryTime', 'ExitTime']].equals(
|
|
763
|
-
pd.DataFrame({'EntryTime': pd.to_datetime(['2006-11-01', '2008-11-14']),
|
|
764
|
-
'ExitTime': pd.to_datetime(['2007-10-31', '2009-09-21'])}))
|
|
765
|
-
assert trades['PnL'].round().equals(pd.Series([23469., -34420.]))
|
|
766
|
-
|
|
767
|
-
with _tempfile() as f:
|
|
768
|
-
bt.plot(filename=f, plot_drawdown=True, smooth_equity=False)
|
|
769
|
-
# Give browser time to open before tempfile is removed
|
|
770
|
-
time.sleep(1)
|
|
771
|
-
|
|
772
|
-
def test_resample(self):
|
|
773
|
-
class S(SmaCross):
|
|
774
|
-
def init(self):
|
|
775
|
-
self.I(lambda: ['x'] * len(self.data)) # categorical indicator, GH-309
|
|
776
|
-
super().init()
|
|
777
|
-
|
|
778
|
-
bt = Backtest(GOOG, S)
|
|
779
|
-
bt.run()
|
|
780
|
-
import backtesting._plotting
|
|
781
|
-
with _tempfile() as f, \
|
|
782
|
-
patch(backtesting._plotting, '_MAX_CANDLES', 10), \
|
|
783
|
-
self.assertWarns(UserWarning):
|
|
784
|
-
bt.plot(filename=f, resample=True)
|
|
785
|
-
# Give browser time to open before tempfile is removed
|
|
786
|
-
time.sleep(1)
|
|
787
|
-
|
|
788
|
-
def test_indicator_name(self):
|
|
789
|
-
test_self = self
|
|
790
|
-
|
|
791
|
-
class S(Strategy):
|
|
792
|
-
def init(self):
|
|
793
|
-
def _SMA():
|
|
794
|
-
return SMA(self.data.Close, 5), SMA(self.data.Close, 10)
|
|
795
|
-
|
|
796
|
-
test_self.assertRaises(TypeError, self.I, _SMA, name=42)
|
|
797
|
-
test_self.assertRaises(ValueError, self.I, _SMA, name=("SMA One", ))
|
|
798
|
-
test_self.assertRaises(
|
|
799
|
-
ValueError, self.I, _SMA, name=("SMA One", "SMA Two", "SMA Three"))
|
|
800
|
-
|
|
801
|
-
for overlay in (True, False):
|
|
802
|
-
self.I(SMA, self.data.Close, 5, overlay=overlay)
|
|
803
|
-
self.I(SMA, self.data.Close, 5, name="My SMA", overlay=overlay)
|
|
804
|
-
self.I(SMA, self.data.Close, 5, name=("My SMA", ), overlay=overlay)
|
|
805
|
-
self.I(_SMA, overlay=overlay)
|
|
806
|
-
self.I(_SMA, name="My SMA", overlay=overlay)
|
|
807
|
-
self.I(_SMA, name=("SMA One", "SMA Two"), overlay=overlay)
|
|
808
|
-
|
|
809
|
-
def next(self):
|
|
810
|
-
pass
|
|
811
|
-
|
|
812
|
-
bt = Backtest(GOOG, S)
|
|
813
|
-
bt.run()
|
|
814
|
-
with _tempfile() as f:
|
|
815
|
-
bt.plot(filename=f,
|
|
816
|
-
plot_drawdown=False, plot_equity=False, plot_pl=False, plot_volume=False,
|
|
817
|
-
open_browser=False)
|
|
818
|
-
|
|
819
|
-
def test_indicator_color(self):
|
|
820
|
-
class S(Strategy):
|
|
821
|
-
def init(self):
|
|
822
|
-
a = self.I(SMA, self.data.Close, 5, overlay=True, color='red')
|
|
823
|
-
b = self.I(SMA, self.data.Close, 10, overlay=False, color='blue')
|
|
824
|
-
self.I(lambda: (a, b), overlay=False, color=('green', 'orange'))
|
|
825
|
-
|
|
826
|
-
def next(self):
|
|
827
|
-
pass
|
|
828
|
-
|
|
829
|
-
bt = Backtest(GOOG, S)
|
|
830
|
-
bt.run()
|
|
831
|
-
with _tempfile() as f:
|
|
832
|
-
bt.plot(filename=f,
|
|
833
|
-
plot_drawdown=False, plot_equity=False, plot_pl=False, plot_volume=False,
|
|
834
|
-
open_browser=False)
|
|
835
|
-
|
|
836
|
-
def test_indicator_scatter(self):
|
|
837
|
-
class S(Strategy):
|
|
838
|
-
def init(self):
|
|
839
|
-
self.I(SMA, self.data.Close, 5, overlay=True, scatter=True)
|
|
840
|
-
self.I(SMA, self.data.Close, 10, overlay=False, scatter=True)
|
|
841
|
-
|
|
842
|
-
def next(self):
|
|
843
|
-
pass
|
|
844
|
-
|
|
845
|
-
bt = Backtest(GOOG, S)
|
|
846
|
-
bt.run()
|
|
847
|
-
with _tempfile() as f:
|
|
848
|
-
bt.plot(filename=f,
|
|
849
|
-
plot_drawdown=False, plot_equity=False, plot_pl=False, plot_volume=False,
|
|
850
|
-
open_browser=False)
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
class TestLib(TestCase):
|
|
854
|
-
def test_barssince(self):
|
|
855
|
-
self.assertEqual(barssince(np.r_[1, 0, 0]), 2)
|
|
856
|
-
self.assertEqual(barssince(np.r_[0, 0, 0]), np.inf)
|
|
857
|
-
self.assertEqual(barssince(np.r_[0, 0, 0], 0), 0)
|
|
858
|
-
|
|
859
|
-
def test_cross(self):
|
|
860
|
-
self.assertTrue(cross([0, 1], [1, 0]))
|
|
861
|
-
self.assertTrue(cross([1, 0], [0, 1]))
|
|
862
|
-
self.assertFalse(cross([1, 0], [1, 0]))
|
|
863
|
-
|
|
864
|
-
def test_crossover(self):
|
|
865
|
-
self.assertTrue(crossover([0, 1], [1, 0]))
|
|
866
|
-
self.assertTrue(crossover([0, 1], .5))
|
|
867
|
-
self.assertTrue(crossover([0, 1], pd.Series([.5, .5], index=[5, 6])))
|
|
868
|
-
self.assertFalse(crossover([1, 0], [1, 0]))
|
|
869
|
-
self.assertFalse(crossover([0], [1]))
|
|
870
|
-
|
|
871
|
-
def test_quantile(self):
|
|
872
|
-
self.assertEqual(quantile(np.r_[1, 3, 2], .5), 2)
|
|
873
|
-
self.assertEqual(quantile(np.r_[1, 3, 2]), .5)
|
|
874
|
-
|
|
875
|
-
def test_resample_apply(self):
|
|
876
|
-
res = resample_apply('D', SMA, EURUSD.Close, 10)
|
|
877
|
-
self.assertEqual(res.name, 'C[D]')
|
|
878
|
-
self.assertEqual(res.count() / res.size, .9634)
|
|
879
|
-
np.testing.assert_almost_equal(res.iloc[-48:].unique().tolist(),
|
|
880
|
-
[1.242643, 1.242381, 1.242275],
|
|
881
|
-
decimal=6)
|
|
882
|
-
|
|
883
|
-
def resets_index(*args):
|
|
884
|
-
return pd.Series(SMA(*args).values)
|
|
885
|
-
|
|
886
|
-
res2 = resample_apply('D', resets_index, EURUSD.Close, 10)
|
|
887
|
-
self.assertTrue((res.dropna() == res2.dropna()).all())
|
|
888
|
-
self.assertTrue((res.index == res2.index).all())
|
|
889
|
-
|
|
890
|
-
res3 = resample_apply('D', None, EURUSD)
|
|
891
|
-
self.assertIn('Volume', res3)
|
|
892
|
-
|
|
893
|
-
res3 = resample_apply('D', lambda df: (df.Close, df.Close), EURUSD)
|
|
894
|
-
self.assertIsInstance(res3, pd.DataFrame)
|
|
895
|
-
|
|
896
|
-
def test_plot_heatmaps(self):
|
|
897
|
-
bt = Backtest(GOOG, SmaCross)
|
|
898
|
-
stats, heatmap = bt.optimize(fast=range(2, 7, 2),
|
|
899
|
-
slow=range(7, 15, 2),
|
|
900
|
-
return_heatmap=True)
|
|
901
|
-
with _tempfile() as f:
|
|
902
|
-
for agg in ('mean',
|
|
903
|
-
lambda x: np.percentile(x, 75)):
|
|
904
|
-
plot_heatmaps(heatmap, agg, filename=f, open_browser=False)
|
|
905
|
-
|
|
906
|
-
# Preview
|
|
907
|
-
plot_heatmaps(heatmap, filename=f)
|
|
908
|
-
time.sleep(5)
|
|
909
|
-
|
|
910
|
-
def test_random_ohlc_data(self):
|
|
911
|
-
generator = random_ohlc_data(GOOG, frac=1)
|
|
912
|
-
new_data = next(generator)
|
|
913
|
-
self.assertEqual(list(new_data.index), list(GOOG.index))
|
|
914
|
-
self.assertEqual(new_data.shape, GOOG.shape)
|
|
915
|
-
self.assertEqual(list(new_data.columns), list(GOOG.columns))
|
|
916
|
-
|
|
917
|
-
def test_compute_stats(self):
|
|
918
|
-
stats = Backtest(GOOG, SmaCross).run()
|
|
919
|
-
only_long_trades = stats._trades[stats._trades.Size > 0]
|
|
920
|
-
long_stats = compute_stats(stats=stats, trades=only_long_trades,
|
|
921
|
-
data=GOOG, risk_free_rate=.02)
|
|
922
|
-
self.assertNotEqual(list(stats._equity_curve.Equity),
|
|
923
|
-
list(long_stats._equity_curve.Equity))
|
|
924
|
-
self.assertNotEqual(stats['Sharpe Ratio'], long_stats['Sharpe Ratio'])
|
|
925
|
-
self.assertEqual(long_stats['# Trades'], len(only_long_trades))
|
|
926
|
-
self.assertEqual(stats._strategy, long_stats._strategy)
|
|
927
|
-
assert_frame_equal(long_stats._trades, only_long_trades)
|
|
928
|
-
|
|
929
|
-
def test_SignalStrategy(self):
|
|
930
|
-
class S(SignalStrategy):
|
|
931
|
-
def init(self):
|
|
932
|
-
sma = self.data.Close.s.rolling(10).mean()
|
|
933
|
-
self.set_signal(self.data.Close > sma,
|
|
934
|
-
self.data.Close < sma)
|
|
935
|
-
|
|
936
|
-
stats = Backtest(GOOG, S).run()
|
|
937
|
-
self.assertIn(stats['# Trades'], (1179, 1180)) # varies on different archs?
|
|
938
|
-
|
|
939
|
-
def test_TrailingStrategy(self):
|
|
940
|
-
class S(TrailingStrategy):
|
|
941
|
-
def init(self):
|
|
942
|
-
super().init()
|
|
943
|
-
self.set_atr_periods(40)
|
|
944
|
-
self.set_trailing_pct(.1)
|
|
945
|
-
self.set_trailing_sl(3)
|
|
946
|
-
self.sma = self.I(lambda: self.data.Close.s.rolling(10).mean())
|
|
947
|
-
|
|
948
|
-
def next(self):
|
|
949
|
-
super().next()
|
|
950
|
-
if not self.position and self.data.Close > self.sma:
|
|
951
|
-
self.buy()
|
|
952
|
-
|
|
953
|
-
stats = Backtest(GOOG, S).run()
|
|
954
|
-
self.assertEqual(stats['# Trades'], 56)
|
|
955
|
-
|
|
956
|
-
def test_FractionalBacktest(self):
|
|
957
|
-
ubtc_bt = FractionalBacktest(BTCUSD['2015':], SmaCross, fractional_unit=1 / 1e6, cash=100)
|
|
958
|
-
stats = ubtc_bt.run(fast=2, slow=3)
|
|
959
|
-
self.assertEqual(stats['# Trades'], 41)
|
|
960
|
-
trades = stats['_trades']
|
|
961
|
-
self.assertEqual(len(trades), 41)
|
|
962
|
-
trade = trades.iloc[0]
|
|
963
|
-
self.assertAlmostEqual(trade['EntryPrice'], 236.69)
|
|
964
|
-
self.assertAlmostEqual(stats['_strategy']._indicators[0][trade['EntryBar']], 234.14)
|
|
965
|
-
|
|
966
|
-
def test_MultiBacktest(self):
|
|
967
|
-
import backtesting
|
|
968
|
-
assert callable(getattr(backtesting, 'Pool', None)), backtesting.__dict__
|
|
969
|
-
for start_method in mp.get_all_start_methods():
|
|
970
|
-
with self.subTest(start_method=start_method), \
|
|
971
|
-
patch(backtesting, 'Pool', mp.get_context(start_method).Pool):
|
|
972
|
-
start_time = time.monotonic()
|
|
973
|
-
btm = MultiBacktest([GOOG, EURUSD, BTCUSD], SmaCross, cash=100_000)
|
|
974
|
-
res = btm.run(fast=2)
|
|
975
|
-
self.assertIsInstance(res, pd.DataFrame)
|
|
976
|
-
self.assertEqual(res.columns.tolist(), [0, 1, 2])
|
|
977
|
-
heatmap = btm.optimize(fast=[2, 4], slow=[10, 20])
|
|
978
|
-
self.assertIsInstance(heatmap, pd.DataFrame)
|
|
979
|
-
self.assertEqual(heatmap.columns.tolist(), [0, 1, 2])
|
|
980
|
-
print(start_method, time.monotonic() - start_time)
|
|
981
|
-
plot_heatmaps(heatmap.mean(axis=1), open_browser=False)
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
class TestUtil(TestCase):
|
|
985
|
-
def test_as_str(self):
|
|
986
|
-
def func():
|
|
987
|
-
pass
|
|
988
|
-
|
|
989
|
-
class Class:
|
|
990
|
-
def __call__(self):
|
|
991
|
-
pass
|
|
992
|
-
|
|
993
|
-
self.assertEqual(_as_str('4'), '4')
|
|
994
|
-
self.assertEqual(_as_str(4), '4')
|
|
995
|
-
self.assertEqual(_as_str(_Indicator([1, 2], name='x')), 'x')
|
|
996
|
-
self.assertEqual(_as_str(func), 'func')
|
|
997
|
-
self.assertEqual(_as_str(Class), 'Class')
|
|
998
|
-
self.assertEqual(_as_str(Class()), 'Class')
|
|
999
|
-
self.assertEqual(_as_str(pd.Series([1, 2], name='x')), 'x')
|
|
1000
|
-
self.assertEqual(_as_str(pd.DataFrame()), 'df')
|
|
1001
|
-
self.assertEqual(_as_str(lambda x: x), 'λ')
|
|
1002
|
-
for s in ('Open', 'High', 'Low', 'Close', 'Volume'):
|
|
1003
|
-
self.assertEqual(_as_str(_Array([1], name=s)), s[0])
|
|
1004
|
-
|
|
1005
|
-
def test_patch(self):
|
|
1006
|
-
class Object:
|
|
1007
|
-
pass
|
|
1008
|
-
o = Object()
|
|
1009
|
-
o.attr = False
|
|
1010
|
-
with patch(o, 'attr', True):
|
|
1011
|
-
self.assertTrue(o.attr)
|
|
1012
|
-
self.assertFalse(o.attr)
|
|
1013
|
-
|
|
1014
|
-
def test_pandas_accessors(self):
|
|
1015
|
-
class S(Strategy):
|
|
1016
|
-
def init(self):
|
|
1017
|
-
close, index = self.data.Close, self.data.index
|
|
1018
|
-
assert close.s.equals(pd.Series(close, index=index))
|
|
1019
|
-
assert self.data.df['Close'].equals(pd.Series(close, index=index))
|
|
1020
|
-
self.data.df['new_key'] = 2 * close
|
|
1021
|
-
|
|
1022
|
-
def next(self):
|
|
1023
|
-
close, index = self.data.Close, self.data.index
|
|
1024
|
-
assert close.s.equals(pd.Series(close, index=index))
|
|
1025
|
-
assert self.data.df['Close'].equals(pd.Series(close, index=index))
|
|
1026
|
-
assert self.data.df['new_key'].equals(pd.Series(self.data.new_key, index=index))
|
|
1027
|
-
|
|
1028
|
-
Backtest(GOOG.iloc[:20], S).run()
|
|
1029
|
-
|
|
1030
|
-
def test_indicators_picklable(self):
|
|
1031
|
-
bt = Backtest(SHORT_DATA, SmaCross)
|
|
1032
|
-
with ProcessPoolExecutor() as executor:
|
|
1033
|
-
stats = executor.submit(Backtest.run, bt).result()
|
|
1034
|
-
assert stats._strategy._indicators[0]._opts, '._opts and .name were not unpickled'
|
|
1035
|
-
bt.plot(results=stats, resample='2d', open_browser=False)
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
class TestDocs(TestCase):
|
|
1039
|
-
DOCS_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'doc')
|
|
1040
|
-
|
|
1041
|
-
@unittest.skipUnless(os.path.isdir(DOCS_DIR), "docs dir doesn't exist")
|
|
1042
|
-
def test_examples(self):
|
|
1043
|
-
examples = glob(os.path.join(self.DOCS_DIR, 'examples', '*.py'))
|
|
1044
|
-
self.assertGreaterEqual(len(examples), 4)
|
|
1045
|
-
with chdir(gettempdir()):
|
|
1046
|
-
for file in examples:
|
|
1047
|
-
with self.subTest(example=os.path.basename(file)):
|
|
1048
|
-
run_path(file)
|
|
1049
|
-
|
|
1050
|
-
def test_backtest_run_docstring_contains_stats_keys(self):
|
|
1051
|
-
stats = Backtest(SHORT_DATA, SmaCross).run()
|
|
1052
|
-
for key in stats.index:
|
|
1053
|
-
self.assertIn(key, Backtest.run.__doc__)
|
|
1054
|
-
|
|
1055
|
-
def test_readme_contains_stats_keys(self):
|
|
1056
|
-
with open(os.path.join(os.path.dirname(__file__),
|
|
1057
|
-
'..', '..', 'README.md')) as f:
|
|
1058
|
-
readme = f.read()
|
|
1059
|
-
stats = Backtest(SHORT_DATA, SmaCross).run()
|
|
1060
|
-
for key in stats.index:
|
|
1061
|
-
self.assertIn(key, readme)
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
class TestRegressions(TestCase):
|
|
1065
|
-
def test_gh_521(self):
|
|
1066
|
-
class S(_S):
|
|
1067
|
-
def next(self):
|
|
1068
|
-
if self.data.Close[-1] == 100:
|
|
1069
|
-
self.buy(size=1, sl=90)
|
|
1070
|
-
|
|
1071
|
-
arr = np.r_[100, 100, 100, 50, 50]
|
|
1072
|
-
df = pd.DataFrame({'Open': arr, 'High': arr, 'Low': arr, 'Close': arr})
|
|
1073
|
-
with self.assertWarnsRegex(UserWarning, 'index is not datetime'):
|
|
1074
|
-
bt = Backtest(df, S, cash=100, trade_on_close=True)
|
|
1075
|
-
self.assertEqual(bt.run()._trades['ExitPrice'][0], 50)
|
|
1076
|
-
|
|
1077
|
-
def test_stats_annualized(self):
|
|
1078
|
-
stats = Backtest(GOOG.resample('W').agg(OHLCV_AGG), SmaCross).run()
|
|
1079
|
-
self.assertFalse(np.isnan(stats['Return (Ann.) [%]']))
|
|
1080
|
-
self.assertEqual(round(stats['Return (Ann.) [%]']), -3)
|
|
1081
|
-
|
|
1082
|
-
def test_cancel_orders(self):
|
|
1083
|
-
class S(_S):
|
|
1084
|
-
def next(self):
|
|
1085
|
-
self.buy(sl=1, tp=1e3)
|
|
1086
|
-
if self.position:
|
|
1087
|
-
self.position.close()
|
|
1088
|
-
for order in self.orders:
|
|
1089
|
-
order.cancel()
|
|
1090
|
-
|
|
1091
|
-
Backtest(SHORT_DATA, S).run()
|
|
1092
|
-
|
|
1093
|
-
def test_trade_on_close_closes_trades_on_close(self):
|
|
1094
|
-
def coro(strat):
|
|
1095
|
-
yield strat.buy(size=1, sl=90) and strat.buy(size=1, sl=80)
|
|
1096
|
-
assert len(strat.trades) == 2
|
|
1097
|
-
yield strat.trades[0].close()
|
|
1098
|
-
yield
|
|
1099
|
-
|
|
1100
|
-
arr = np.r_[100, 101, 102, 50, 51]
|
|
1101
|
-
df = pd.DataFrame({
|
|
1102
|
-
'Open': arr - 10,
|
|
1103
|
-
'Close': arr, 'High': arr, 'Low': arr})
|
|
1104
|
-
with self.assertWarnsRegex(UserWarning, 'index is not datetime'):
|
|
1105
|
-
trades = TestStrategy._Backtest(coro, df, cash=250, trade_on_close=True).run()._trades
|
|
1106
|
-
# trades = Backtest(df, S, cash=250, trade_on_close=True).run()._trades
|
|
1107
|
-
self.assertEqual(trades['EntryBar'][0], 1)
|
|
1108
|
-
self.assertEqual(trades['ExitBar'][0], 2)
|
|
1109
|
-
self.assertEqual(trades['EntryPrice'][0], 101)
|
|
1110
|
-
self.assertEqual(trades['ExitPrice'][0], 102)
|
|
1111
|
-
self.assertEqual(trades['EntryBar'][1], 1)
|
|
1112
|
-
self.assertEqual(trades['ExitBar'][1], 3)
|
|
1113
|
-
self.assertEqual(trades['EntryPrice'][1], 101)
|
|
1114
|
-
self.assertEqual(trades['ExitPrice'][1], 40)
|
|
1115
|
-
|
|
1116
|
-
with self.assertWarnsRegex(UserWarning, 'index is not datetime'):
|
|
1117
|
-
trades = TestStrategy._Backtest(coro, df, cash=250, trade_on_close=False).run()._trades
|
|
1118
|
-
# trades = Backtest(df, S, cash=250, trade_on_close=False).run()._trades
|
|
1119
|
-
self.assertEqual(trades['EntryBar'][0], 2)
|
|
1120
|
-
self.assertEqual(trades['ExitBar'][0], 3)
|
|
1121
|
-
self.assertEqual(trades['EntryPrice'][0], 92)
|
|
1122
|
-
self.assertEqual(trades['ExitPrice'][0], 40)
|
|
1123
|
-
self.assertEqual(trades['EntryBar'][1], 2)
|
|
1124
|
-
self.assertEqual(trades['ExitBar'][1], 3)
|
|
1125
|
-
self.assertEqual(trades['EntryPrice'][1], 92)
|
|
1126
|
-
self.assertEqual(trades['ExitPrice'][1], 40)
|
|
1127
|
-
|
|
1128
|
-
def test_trades_dates_match_prices(self):
|
|
1129
|
-
bt = Backtest(EURUSD, SmaCross, trade_on_close=True)
|
|
1130
|
-
trades = bt.run()._trades
|
|
1131
|
-
self.assertEqual(EURUSD.Close[trades['ExitTime']].tolist(),
|
|
1132
|
-
trades['ExitPrice'].tolist())
|
|
1133
|
-
|
|
1134
|
-
def test_sl_always_before_tp(self):
|
|
1135
|
-
class S(_S):
|
|
1136
|
-
def next(self):
|
|
1137
|
-
i = len(self.data.index)
|
|
1138
|
-
if i == 4:
|
|
1139
|
-
self.buy()
|
|
1140
|
-
if i == 5:
|
|
1141
|
-
t = self.trades[0]
|
|
1142
|
-
t.sl = 105
|
|
1143
|
-
t.tp = 107.9
|
|
1144
|
-
|
|
1145
|
-
trades = Backtest(SHORT_DATA, S).run()._trades
|
|
1146
|
-
self.assertEqual(trades['ExitPrice'].iloc[0], 104.95)
|
|
1147
|
-
|
|
1148
|
-
def test_stop_entry_and_tp_in_same_bar(self):
|
|
1149
|
-
class S(_S):
|
|
1150
|
-
def next(self):
|
|
1151
|
-
i = len(self.data.index)
|
|
1152
|
-
if i == 3:
|
|
1153
|
-
self.sell(stop=108, tp=105, sl=113)
|
|
1154
|
-
|
|
1155
|
-
trades = Backtest(SHORT_DATA, S).run()._trades
|
|
1156
|
-
self.assertEqual(trades['ExitBar'].iloc[0], 3)
|
|
1157
|
-
self.assertEqual(trades['ExitPrice'].iloc[0], 105)
|
|
1158
|
-
|
|
1159
|
-
def test_optimize_datetime_index_with_timezone(self):
|
|
1160
|
-
data: pd.DataFrame = GOOG.iloc[:100]
|
|
1161
|
-
data.index = data.index.tz_localize('Asia/Kolkata')
|
|
1162
|
-
res = Backtest(data, SmaCross).optimize(fast=range(2, 3), slow=range(4, 5))
|
|
1163
|
-
self.assertGreater(res['# Trades'], 0)
|
|
1164
|
-
|
|
1165
|
-
def test_sl_tp_values_in_trades_df(self):
|
|
1166
|
-
class S(_S):
|
|
1167
|
-
def next(self):
|
|
1168
|
-
self.next = lambda: None
|
|
1169
|
-
self.buy(size=1, tp=111)
|
|
1170
|
-
self.buy(size=1, sl=99)
|
|
1171
|
-
|
|
1172
|
-
trades = Backtest(SHORT_DATA, S).run()._trades
|
|
1173
|
-
self.assertEqual(trades['SL'].fillna(0).tolist(), [0, 99])
|
|
1174
|
-
self.assertEqual(trades['TP'].fillna(0).tolist(), [111, 0])
|