BackcastPro 0.0.1__py3-none-any.whl → 0.0.2__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.

@@ -0,0 +1,1174 @@
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])