@@ -60,8 +60,10 @@ def init(self):
60
60
61
61
def next (self ):
62
62
if crossover (self .sma1 , self .sma2 ):
63
+ self .position .close ()
63
64
self .buy ()
64
65
elif crossover (self .sma2 , self .sma1 ):
66
+ self .position .close ()
65
67
self .sell ()
66
68
67
69
@@ -154,47 +156,55 @@ def next(self, FIVE_DAYS=pd.Timedelta('3 days')):
154
156
assert not np .isnan (self .sma [- 1 ])
155
157
assert self .data .index [- 1 ]
156
158
157
- self .orders .is_long
158
- self .orders .is_short
159
- self .orders .entry
160
- self .orders .sl
161
- self .orders .tp
162
-
163
159
self .position
164
160
self .position .size
165
161
self .position .pl
166
162
self .position .pl_pct
167
- self .position .open_price
168
- self .position .open_time
169
163
self .position .is_long
170
164
171
165
if crossover (self .sma , self .data .Close ):
172
- self .orders .cancel ()
173
- self .sell ()
174
- assert not self .orders .is_long
175
- assert self .orders .is_short
176
- assert self .orders .entry
177
- assert not self .orders .sl
178
- assert not self .orders .tp
166
+ self .orders .cancel () # cancels only non-contingent
179
167
price = self .data .Close [- 1 ]
180
168
sl , tp = 1.05 * price , .9 * price
181
- self .sell (price , sl = sl , tp = tp )
182
- self .orders .set_entry (price )
183
- self .orders .set_sl (sl )
184
- self .orders .set_tp (tp )
185
- assert self .orders .entry == price
186
- assert self .orders .sl == sl
187
- assert self .orders .tp == tp
169
+
170
+ n_orders = len (self .orders )
171
+ self .sell (size = .21 , limit = price , stop = price , sl = sl , tp = tp )
172
+ assert len (self .orders ) == n_orders + 1
173
+
174
+ order = self .orders [- 1 ]
175
+ assert order .limit == price
176
+ assert order .stop == price
177
+ assert order .size == - .21
178
+ assert order .sl == sl
179
+ assert order .tp == tp
180
+ assert not order .is_contingent
188
181
189
182
elif self .position :
190
- assert not self .orders .entry
191
183
assert not self .position .is_long
192
- assert not not self .position .is_short
193
- assert self .position .open_price
184
+ assert self .position .is_short
194
185
assert self .position .pl
195
186
assert self .position .pl_pct
196
187
assert self .position .size < 0
197
- if self .data .index [- 1 ] - self .position .open_time > FIVE_DAYS :
188
+
189
+ trade = self .trades [0 ]
190
+ if self .data .index [- 1 ] - self .data .index [trade .entry_bar ] > FIVE_DAYS :
191
+ assert not trade .is_long
192
+ assert trade .is_short
193
+ assert trade .size < 0
194
+ assert trade .entry_bar > 0
195
+ assert trade .exit_bar is None
196
+ assert trade .entry_price > 0
197
+ assert trade .exit_price is None
198
+ assert trade .pl / 1
199
+ assert trade .pl_pct / 1
200
+ assert trade .value > 0
201
+ assert trade .sl
202
+ assert trade .tp
203
+ # Close multiple times
204
+ self .position .close (.5 )
205
+ self .position .close (.5 )
206
+ self .position .close (.5 )
207
+ self .position .close ()
198
208
self .position .close ()
199
209
200
210
bt = Backtest (GOOG , Assertive )
@@ -241,36 +251,42 @@ def test_compute_stats(self):
241
251
# NOTE: These values are also used on the website!
242
252
'# Trades' : 65 ,
243
253
'Avg. Drawdown Duration' : pd .Timedelta ('41 days 00:00:00' ),
244
- 'Avg. Drawdown [%]' : - 6.087158560194047 ,
254
+ 'Avg. Drawdown [%]' : - 5.925851581948801 ,
245
255
'Avg. Trade Duration' : pd .Timedelta ('46 days 00:00:00' ),
246
- 'Avg. Trade [%]' : 3.0404430275631444 ,
247
- 'Best Trade [%]' : 54.05363186670138 ,
256
+ 'Avg. Trade [%]' : 3.097629974370268 ,
257
+ 'Best Trade [%]' : 53.59595229490424 ,
248
258
'Buy & Hold Return [%]' : 703.4582419772772 ,
249
- 'Calmar Ratio' : 0.0631443286380662 ,
259
+ 'Calmar Ratio' : 0.06456068720154355 ,
250
260
'Duration' : pd .Timedelta ('3116 days 00:00:00' ),
251
261
'End' : pd .Timestamp ('2013-03-01 00:00:00' ),
252
- 'Equity Final [$]' : 52624.29346696951 ,
253
- 'Equity Peak [$]' : 76908.27001642012 ,
254
- 'Expectancy [%]' : 8.774692825628644 ,
255
- 'Exposure Time [%]' : 93.93453145057767 ,
262
+ 'Equity Final [$]' : 51959.94999999997 ,
263
+ 'Equity Peak [$]' : 75787.44 ,
264
+ 'Expectancy [%]' : 8.791710931051735 ,
265
+ 'Exposure Time [%]' : 93.99441340782123 ,
256
266
'Max. Drawdown Duration' : pd .Timedelta ('584 days 00:00:00' ),
257
- 'Max. Drawdown [%]' : - 48.15069053929621 ,
267
+ 'Max. Drawdown [%]' : - 47.98012705007589 ,
258
268
'Max. Trade Duration' : pd .Timedelta ('183 days 00:00:00' ),
259
- 'Profit Factor' : 2.060450149412349 ,
260
- 'Return [%]' : 426.2429346696951 ,
261
- 'SQN' : 0.91553210127173 ,
262
- 'Sharpe Ratio' : 0.23169782960690408 ,
263
- 'Sortino Ratio' : 0.7096713270577958 ,
269
+ 'Profit Factor' : 2.0880175388920286 ,
270
+ 'Return [%]' : 419.59949999999964 ,
271
+ 'SQN' : 0.916892986080858 ,
272
+ 'Sharpe Ratio' : 0.2357610034211845 ,
273
+ 'Sortino Ratio' : 0.7355072888872161 ,
264
274
'Start' : pd .Timestamp ('2004-08-19 00:00:00' ),
265
275
'Win Rate [%]' : 46.15384615384615 ,
266
- 'Worst Trade [%]' : - 18.85561318387153 ,
276
+ 'Worst Trade [%]' : - 18.39887353835481 ,
267
277
}).sort_index ()
268
278
)
269
- self .assertTrue (
270
- stats ._trade_data .columns .equals (
271
- pd .Index (['Equity' , 'Exit Entry' , 'Exit Position' ,
272
- 'Entry Price' , 'Exit Price' , 'P/L' , 'Returns' ,
273
- 'Drawdown' , 'Drawdown Duration' ])))
279
+
280
+ self .assertSequenceEqual (
281
+ sorted (stats ['_equity_curve' ].columns ),
282
+ sorted (['Equity' , 'DrawdownPct' , 'DrawdownDuration' ]))
283
+
284
+ self .assertEqual (len (stats ['_trades' ]), 65 )
285
+
286
+ self .assertSequenceEqual (
287
+ sorted (stats ['_trades' ].columns ),
288
+ sorted (['Size' , 'EntryBar' , 'ExitBar' , 'EntryPrice' , 'ExitPrice' ,
289
+ 'PnL' , 'ReturnPct' , 'EntryTime' , 'ExitTime' , 'Duration' ]))
274
290
275
291
def test_compute_stats_bordercase (self ):
276
292
@@ -308,7 +324,7 @@ def next(self):
308
324
stats = Backtest (GOOG .iloc [:100 ], strategy ).run ()
309
325
310
326
self .assertFalse (np .isnan (stats ['Equity Final [$]' ]))
311
- self .assertFalse (stats . _trade_data ['Equity' ].isnull ().any ())
327
+ self .assertFalse (stats [ '_equity_curve' ] ['Equity' ].isnull ().any ())
312
328
self .assertEqual (stats ['_strategy' ].__class__ , strategy )
313
329
314
330
@@ -323,8 +339,6 @@ def coroutine(self):
323
339
assert self .position .size > 0
324
340
assert self .position .pl
325
341
assert self .position .pl_pct
326
- assert self .position .open_price > 0
327
- assert self .position .open_time
328
342
329
343
yield self .position .close ()
330
344
@@ -334,8 +348,6 @@ def coroutine(self):
334
348
assert not self .position .size
335
349
assert not self .position .pl
336
350
assert not self .position .pl_pct
337
- assert not self .position .open_price
338
- assert not self .position .open_time
339
351
340
352
class S (Strategy ):
341
353
def init (self ):
@@ -363,8 +375,8 @@ def test_optimize(self):
363
375
self .assertIsInstance (res , pd .Series )
364
376
365
377
res2 = bt .optimize (** OPT_PARAMS , maximize = lambda s : s ['SQN' ])
366
- self .assertSequenceEqual (res .filter (regex = '^[^_]' ).to_dict (),
367
- res2 .filter (regex = '^[^_]' ).to_dict ())
378
+ self .assertDictEqual (res .filter (regex = '^[^_]' ). fillna ( - 1 ).to_dict (),
379
+ res2 .filter (regex = '^[^_]' ). fillna ( - 1 ).to_dict ())
368
380
369
381
res3 , heatmap = bt .optimize (** OPT_PARAMS , return_heatmap = True ,
370
382
constraint = lambda d : d .slow > 2 * d .fast )
@@ -589,7 +601,7 @@ def init(self):
589
601
self .data .Close < sma )
590
602
591
603
stats = Backtest (GOOG , S ).run ()
592
- self .assertGreater (stats ['# Trades' ], 1000 )
604
+ self .assertEqual (stats ['# Trades' ], 1180 )
593
605
594
606
def test_TrailingStrategy (self ):
595
607
class S (TrailingStrategy ):
@@ -605,7 +617,7 @@ def next(self):
605
617
self .buy ()
606
618
607
619
stats = Backtest (GOOG , S ).run ()
608
- self .assertGreater (stats ['# Trades' ], 6 )
620
+ self .assertEqual (stats ['# Trades' ], 50 )
609
621
610
622
611
623
class TestUtil (TestCase ):
0 commit comments