bbstrader 0.1.5__py3-none-any.whl → 0.1.7__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 bbstrader might be problematic. Click here for more details.

@@ -1,34 +1,23 @@
1
1
  import pprint
2
2
  import queue
3
3
  import time
4
- import numpy as np
5
- import pandas as pd
6
4
  import yfinance as yf
7
5
  from queue import Queue
8
6
  from datetime import datetime
9
- from seaborn import saturate
10
7
  from bbstrader.btengine.data import *
11
8
  from bbstrader.btengine.execution import *
12
9
  from bbstrader.btengine.portfolio import Portfolio
13
- from bbstrader.btengine.strategy import Strategy
14
10
  from bbstrader.btengine.event import SignalEvent
15
- from bbstrader.models import HMMRiskManager
16
- from filterpy.kalman import KalmanFilter
17
- from bbstrader.strategies import OrnsteinUhlenbeck
18
- from bbstrader.tseries import load_and_prepare_data
19
- from bbstrader.tseries import get_prediction
11
+ from bbstrader.btengine.strategy import Strategy
20
12
  from typing import Literal, Optional, List
13
+ from tabulate import tabulate
21
14
 
22
15
  __all__ = [
23
16
  "Backtest",
24
- "SMAStrategyBacktester",
25
- "KLFStrategyBacktester",
26
- "OUStrategyBacktester",
27
- "ArimaGarchStrategyBacktester",
17
+ "BacktestEngine",
28
18
  "run_backtest"
29
19
  ]
30
20
 
31
-
32
21
  class Backtest(object):
33
22
  """
34
23
  The `Backtest()` object encapsulates the event-handling logic and essentially
@@ -124,7 +113,7 @@ class Backtest(object):
124
113
  self.events, self.symbol_list, **self.kwargs
125
114
  )
126
115
  self.strategy: Strategy = self.strategy_cls(
127
- self.data_handler, self.events, **self.kwargs
116
+ bars=self.data_handler, events=self.events, **self.kwargs
128
117
  )
129
118
  self.portfolio = Portfolio(
130
119
  self.data_handler,
@@ -132,7 +121,8 @@ class Backtest(object):
132
121
  self.start_date,
133
122
  self.initial_capital, **self.kwargs
134
123
  )
135
- self.execution_handler: ExecutionHandler = self.eh_cls(self.events)
124
+ self.execution_handler: ExecutionHandler = self.eh_cls(
125
+ self.events, **self.kwargs)
136
126
 
137
127
  def _run_backtest(self):
138
128
  """
@@ -172,7 +162,7 @@ class Backtest(object):
172
162
  self.fills += 1
173
163
  self.portfolio.update_fill(event)
174
164
 
175
- time.sleep(self.heartbeat)
165
+ time.sleep(self.heartbeat)
176
166
 
177
167
  def _output_performance(self):
178
168
  """
@@ -182,17 +172,30 @@ class Backtest(object):
182
172
 
183
173
  print("\nCreating summary stats...")
184
174
  stats = self.portfolio.output_summary_stats()
185
-
186
- print("\nCreating equity curve...")
187
- print(f"{self.portfolio.equity_curve.tail(10)}\n")
188
- print("==== Summary Stats ====")
189
- pprint.pprint(stats)
175
+ print("[======= Summary Stats =======]")
190
176
  stat2 = {}
191
177
  stat2['Signals'] = self.signals
192
178
  stat2['Orders'] = self.orders
193
179
  stat2['Fills'] = self.fills
194
- pprint.pprint(stat2)
180
+ stats.extend(stat2.items())
181
+ print(
182
+ tabulate(
183
+ stats,
184
+ headers=["Metric", "Value"],
185
+ tablefmt="outline"),
186
+ "\n"
187
+ )
195
188
 
189
+ print("\nCreating equity curve...")
190
+ print("\n[======= EQUITY CURVE =======]")
191
+ print(
192
+ tabulate(
193
+ self.portfolio.equity_curve.tail(10),
194
+ headers="keys",
195
+ tablefmt="outline"),
196
+ "\n"
197
+ )
198
+
196
199
  def simulate_trading(self):
197
200
  """
198
201
  Simulates the backtest and outputs portfolio performance.
@@ -200,701 +203,96 @@ class Backtest(object):
200
203
  self._run_backtest()
201
204
  self._output_performance()
202
205
 
203
-
204
- class SMAStrategyBacktester(Strategy):
205
- """
206
- Carries out a basic Moving Average Crossover strategy bactesting with a
207
- short/long simple weighted moving average. Default short/long
208
- windows are 50/200 periods respectively and uses Hiden Markov Model
209
- as risk Managment system for filteering signals.
210
-
211
- The trading strategy for this class is exceedingly simple and is used to bettter
212
- understood. The important issue is the risk management aspect (the Hmm model)
213
-
214
- The Long-term trend following strategy is of the classic moving average crossover type.
215
- The rules are simple:
216
- - At every bar calculate the 50-day and 200-day simple moving averages (SMA)
217
- - If the 50-day SMA exceeds the 200-day SMA and the strategy is not invested, then go long
218
- - If the 200-day SMA exceeds the 50-day SMA and the strategy is invested, then close the position
219
- """
220
-
221
- def __init__(
222
- self, bars: DataHandler, events: Queue, /, **kwargs
223
- ):
224
- """
225
- Args:
226
- bars (DataHandler): A data handler object that provides market data.
227
- events (Queue): An event queue object where generated signals are placed.
228
- short_window (int, optional): The period for the short moving average.
229
- long_window (int, optional): The period for the long moving average.
230
- hmm_model (optional): The risk management model to be used.
231
- quantity (int, optional): The default quantity of assets to trade.
232
- """
233
- self.bars = bars
234
- self.symbol_list = self.bars.symbol_list
235
- self.events = events
236
-
237
- self.short_window = kwargs.get("short_window", 50)
238
- self.long_window = kwargs.get("long_window", 200)
239
- self.hmm_model = kwargs.get("hmm_model")
240
- self.qty = kwargs.get("quantity", 100)
241
-
242
- self.bought = self._calculate_initial_bought()
243
-
244
- def _calculate_initial_bought(self):
245
- bought = {}
246
- for s in self.symbol_list:
247
- bought[s] = 'OUT'
248
- return bought
249
-
250
- def get_data(self):
251
- for s in self.symbol_list:
252
- bar_date = self.bars.get_latest_bar_datetime(s)
253
- bars = self.bars.get_latest_bars_values(
254
- s, "Adj Close", N=self.long_window
255
- )
256
- returns_val = self.bars.get_latest_bars_values(
257
- s, "Returns", N=self.long_window
258
- )
259
- if len(bars) >= self.long_window and len(returns_val) >= self.long_window:
260
- regime = self.hmm_model.which_trade_allowed(returns_val)
261
-
262
- short_sma = np.mean(bars[-self.short_window:])
263
- long_sma = np.mean(bars[-self.long_window:])
264
-
265
- return short_sma, long_sma, regime, s, bar_date
266
- else:
267
- return None
268
-
269
- def create_signal(self):
270
- signal = None
271
- data = self.get_data()
272
- if data is not None:
273
- short_sma, long_sma, regime, s, bar_date = data
274
- dt = bar_date
275
- if regime == "LONG":
276
- # Bulliqh regime
277
- if short_sma < long_sma and self.bought[s] == "LONG":
278
- print(f"EXIT: {bar_date}")
279
- signal = SignalEvent(1, s, dt, 'EXIT')
280
- self.bought[s] = 'OUT'
281
-
282
- elif short_sma > long_sma and self.bought[s] == "OUT":
283
- print(f"LONG: {bar_date}")
284
- signal = SignalEvent(
285
- 1, s, dt, 'LONG', quantity=self.qty)
286
- self.bought[s] = 'LONG'
287
-
288
- elif regime == "SHORT":
289
- # Bearish regime
290
- if short_sma > long_sma and self.bought[s] == "SHORT":
291
- print(f"EXIT: {bar_date}")
292
- signal = SignalEvent(1, s, dt, 'EXIT')
293
- self.bought[s] = 'OUT'
294
-
295
- elif short_sma < long_sma and self.bought[s] == "OUT":
296
- print(f"SHORT: {bar_date}")
297
- signal = SignalEvent(
298
- 1, s, dt, 'SHORT', quantity=self.qty)
299
- self.bought[s] = 'SHORT'
300
- return signal
301
-
302
- def calculate_signals(self, event):
303
- if event.type == 'MARKET':
304
- signal = self.create_signal()
305
- if signal is not None:
306
- self.events.put(signal)
307
-
308
-
309
- class KLFStrategyBacktester(Strategy):
310
- """
311
- The `KLFStrategyBacktester` class implements a backtesting framework for a
312
- [pairs trading](https://en.wikipedia.org/wiki/Pairs_trade) strategy using
313
- Kalman Filter for signals and Hidden Markov Models (HMM) for risk management.
314
- This document outlines the structure and usage of the `KLFStrategyBacktester`,
315
- including initialization parameters, main functions, and an example of how to run a backtest.
316
- """
317
-
318
- def __init__(
319
- self,
320
- bars: DataHandler, events_queue: Queue, **kwargs
321
- ):
322
- """
323
- Args:
324
- `bars`: `DataHandler` for market data handling.
325
- `events_queue`: A queue for managing events.
326
- kwargs : Additional keyword arguments including
327
- - `tickers`: List of ticker symbols involved in the pairs trading strategy.
328
- - `quantity`: Quantity of assets to trade. Default is 100.
329
- - `delta`: Delta parameter for the Kalman Filter. Default is `1e-4`.
330
- - `vt`: Measurement noise covariance for the Kalman Filter. Default is `1e-3`.
331
- - `hmm_model`: Instance of `HMMRiskManager` for managing trading risks.
332
- - `hmm_window`: Window size for calculating returns for the HMM. Default is 50.
333
- - `hmm_tiker`: Ticker symbol used by the HMM for risk management.
334
- """
335
- self.bars = bars
336
- self.symbol_list = self.bars.symbol_list
337
- self.events_queue = events_queue
338
-
339
- self.tickers = kwargs.get("tickers")
340
- self.hmm_tiker = kwargs.get("hmm_tiker")
341
- self.hmm_model = kwargs.get("hmm_model")
342
- self._assert_tikers()
343
- self.hmm_window = kwargs.get("hmm_window", 50)
344
- self.qty = kwargs.get("quantity", 100)
345
-
346
- self.latest_prices = np.array([-1.0, -1.0])
347
- self.delta = kwargs.get("delta", 1e-4)
348
- self.wt = self.delta/(1-self.delta) * np.eye(2)
349
- self.vt = kwargs.get("vt", 1e-3)
350
- self.theta = np.zeros(2)
351
- self.P = np.zeros((2, 2))
352
- self.R = None
353
- self.kf = self._init_kalman()
354
-
355
- self.long_market = False
356
- self.short_market = False
357
-
358
- def _assert_tikers(self):
359
- if self.tickers is None:
360
- raise ValueError(
361
- "A list of 2 Tickers must be provide for this strategy")
362
- if self.hmm_tiker is None:
363
- raise ValueError(
364
- "You need to provide a ticker used by the HMM for risk management")
365
-
366
- def _init_kalman(self):
367
- kf = KalmanFilter(dim_x=2, dim_z=1)
368
- kf.x = np.zeros((2, 1)) # Initial state
369
- kf.P = self.P # Initial covariance
370
- kf.F = np.eye(2) # State transition matrix
371
- kf.Q = self.wt # Process noise covariance
372
- kf.R = 1. # Scalar measurement noise covariance
373
-
374
- return kf
375
-
376
- def calc_slope_intercep(self, prices):
377
- kf = self.kf
378
- kf.H = np.array([[prices[1], 1.0]])
379
- kf.predict()
380
- kf.update(prices[0])
381
- slope = kf.x.copy().flatten()[0]
382
- intercept = kf.x.copy().flatten()[1]
383
-
384
- return slope, intercept
385
-
386
- def calculate_xy_signals(self, et, sqrt_Qt, regime, dt):
387
- # Make sure there is no position open
388
- if et >= -sqrt_Qt and self.long_market:
389
- print("CLOSING LONG: %s" % dt)
390
- y_signal = SignalEvent(1, self.tickers[1], dt, "EXIT")
391
- x_signal = SignalEvent(1, self.tickers[0], dt, "EXIT")
392
- self.events_queue.put(y_signal)
393
- self.events_queue.put(x_signal)
394
- self.long_market = False
395
-
396
- elif et <= sqrt_Qt and self.short_market:
397
- print("CLOSING SHORT: %s" % dt)
398
- y_signal = SignalEvent(1, self.tickers[1], dt, "EXIT")
399
- x_signal = SignalEvent(1, self.tickers[0], dt, "EXIT")
400
- self.events_queue.put(y_signal)
401
- self.events_queue.put(x_signal)
402
- self.short_market = False
403
-
404
- # Long Entry
405
- if regime == "LONG":
406
- if et <= -sqrt_Qt and not self.long_market:
407
- print("LONG: %s" % dt)
408
- y_signal = SignalEvent(
409
- 1, self.tickers[1], dt, "LONG", self.qty, 1.0)
410
- x_signal = SignalEvent(
411
- 1, self.tickers[0], dt, "SHORT", self.qty, self.theta[0])
412
- self.events_queue.put(y_signal)
413
- self.events_queue.put(x_signal)
414
- self.long_market = True
415
-
416
- # Short Entry
417
- elif regime == "SHORT":
418
- if et >= sqrt_Qt and not self.short_market:
419
- print("SHORT: %s" % dt)
420
- y_signal = SignalEvent(
421
- 1, self.tickers[1], dt, "SHORT", self.qty, 1.0)
422
- x_signal = SignalEvent(
423
- 1, self.tickers[0], "LONG", self.qty, self.theta[0])
424
- self.events_queue.put(y_signal)
425
- self.events_queue.put(x_signal)
426
- self.short_market = True
427
-
428
- def calculate_signals_for_pairs(self):
429
- p0, p1 = self.tickers[0], self.tickers[1]
430
- dt = self.bars.get_latest_bar_datetime(p0)
431
- _x = self.bars.get_latest_bars_values(
432
- p0, "Close", N=1
433
- )
434
- _y = self.bars.get_latest_bars_values(
435
- p1, "Close", N=1
436
- )
437
- returns = self.bars.get_latest_bars_values(
438
- self.hmm_tiker, "Returns", N=self.hmm_window
439
- )
440
- if len(returns) >= self.hmm_window:
441
- self.latest_prices[0] = _x[-1]
442
- self.latest_prices[1] = _y[-1]
443
-
444
- if all(self.latest_prices > -1.0):
445
- slope, intercept = self.calc_slope_intercep(self.latest_prices)
446
-
447
- self.theta[0] = slope
448
- self.theta[1] = intercept
449
-
450
- # Create the observation matrix of the latest prices
451
- # of Y and the intercept value (1.0) as well as the
452
- # scalar value of the latest price from X
453
- F = np.asarray([self.latest_prices[0], 1.0]).reshape((1, 2))
454
- y = self.latest_prices[1]
455
-
456
- # The prior value of the states \theta_t is
457
- # distributed as a multivariate Gaussian with
458
- # mean a_t and variance-covariance R_t
459
- if self.R is not None:
460
- self.R = self.C + self.wt
461
- else:
462
- self.R = np.zeros((2, 2))
463
-
464
- # Calculate the Kalman Filter update
465
- # ---------------------------------
466
- # Calculate prediction of new observation
467
- # as well as forecast error of that prediction
468
- yhat = F.dot(self.theta)
469
- et = y - yhat
470
-
471
- # Q_t is the variance of the prediction of
472
- # observations and hence sqrt_Qt is the
473
- # standard deviation of the predictions
474
- Qt = F.dot(self.R).dot(F.T) + self.vt
475
- sqrt_Qt = np.sqrt(Qt)
476
-
477
- # The posterior value of the states \theta_t is
478
- # distributed as a multivariate Gaussian with mean
479
- # m_t and variance-covariance C_t
480
- At = self.R.dot(F.T) / Qt
481
- self.theta = self.theta + At.flatten() * et
482
- self.C = self.R - At * F.dot(self.R)
483
- regime = self.hmm_model.which_trade_allowed(returns)
484
-
485
- self.calculate_xy_signals(et, sqrt_Qt, regime, dt)
486
-
487
- def calculate_signals(self, event):
488
- """
489
- Calculate the Kalman Filter strategy.
490
- """
491
- if event.type == "MARKET":
492
- self.calculate_signals_for_pairs()
493
-
494
-
495
- class OUStrategyBacktester(Strategy):
496
- """
497
- The `OUBacktester` class is a specialized trading strategy that implements
498
- the Ornstein-Uhlenbeck (OU) process for mean-reverting financial time series.
499
- This class extends the generic `Strategy` class provided by a backtesting framework
500
- allowing it to integrate seamlessly with the ecosystem of data handling, signal generation
501
- event management, and execution handling components of the framework.
502
- The strategy is designed to operate on historical market data, specifically
503
- targeting a single financial instrument (or a list of instruments)
504
- for which the trading signals are to be generated.
505
-
506
- Note:
507
- This strategy is based on a stochastic process, so it is normal that every time
508
- you run the backtest you get different results.
509
- """
510
-
511
- def __init__(self, bars: DataHandler, events: Queue, **kwargs):
512
- """
513
- Args:
514
- `bars`: DataHandler
515
- `events`: event queue
516
- `ticker`: Symbol of the financial instrument.
517
- `p`: Lookback period for the OU process.
518
- `n`: Minimum number of observations for signal generation.
519
- `quantity`: Quantity of assets to trade.
520
- `ou_data`: DataFrame used to estimate Ornstein-Uhlenbeck process params
521
- (drift `(θ)`, volatility `(σ)`, and long-term mean `(μ)`).
522
- `hmm_model`: HMM risk management model.
523
- `hmm_window`: Lookback period for HMM.
524
- """
525
- self.bars = bars
526
- self.symbol_list = self.bars.symbol_list
527
- self.events = events
528
-
529
- self.ticker = kwargs.get('tiker')
530
- self.p = kwargs.get('p', 20)
531
- self.n = kwargs.get('n', 10)
532
- self.qty = kwargs.get('quantity', 1000)
533
-
534
- self.data = kwargs.get("ou_data")
535
- self.ou_data = self._get_data(self.data)
536
- self.ou = OrnsteinUhlenbeck(self.ou_data["Close"].values)
537
-
538
- self.hmm = kwargs.get('hmm_model')
539
- self.window = kwargs.get('hmm_window', 50)
540
-
541
- self.LONG = False
542
- self.SHORT = False
543
-
544
- def _get_data(self, data):
545
- if isinstance(data, pd.DataFrame):
546
- return data
547
- if isinstance(data, str):
548
- return self._read_csv(data)
549
-
550
- def _read_csv(self, csv_file):
551
- df = pd.read_csv(csv_file, header=0,
552
- names=["Date", "Open", "High", "Low",
553
- "Close", "Adj Close", "Volume"],
554
- index_col="Date", parse_dates=True)
555
- return df
556
-
557
- def create_signal(self):
558
- returns = self.bars.get_latest_bars_values(
559
- self.ticker, "Returns", N=self.p
560
- )
561
- hmm_returns = self.bars.get_latest_bars_values(
562
- self.ticker, "Returns", N=self.window
563
- )
564
- dt = self.bars.get_latest_bar_datetime(self.ticker)
565
- if len(returns) >= self.p and len(hmm_returns) >= self.window:
566
- action = self.ou.calculate_signals(
567
- rts=returns, p=self.p, n=self.n, th=1)
568
- regime = self.hmm.which_trade_allowed(hmm_returns)
569
-
570
- if action == "SHORT" and self.LONG:
571
- signal = SignalEvent(1, self.ticker, dt, "EXIT")
572
- self.events.put(signal)
573
- print(dt, "EXIT LONG")
574
- self.LONG = False
575
-
576
- elif action == "LONG" and self.SHORT:
577
- signal = SignalEvent(1, self.ticker, dt, "EXIT")
578
- self.events.put(signal)
579
- print(dt, "EXIT SHORT")
580
- self.SHORT = False
581
-
582
- if regime == "LONG":
583
- if action == "LONG" and not self.LONG:
584
- self.LONG = True
585
- signal = SignalEvent(1, self.ticker, dt, "LONG", self.qty)
586
- self.events.put(signal)
587
- print(dt, "LONG")
588
-
589
- elif regime == 'SHORT':
590
- if action == "SHORT" and not self.SHORT:
591
- self.SHORT = True
592
- signal = SignalEvent(1, self.ticker, dt, "SHORT", self.qty)
593
- self.events.put(signal)
594
- print(dt, "SHORT")
595
-
596
- def calculate_signals(self, event):
597
- if event.type == "MARKET":
598
- self.create_signal()
599
-
600
-
601
- class ArimaGarchStrategyBacktester(Strategy):
602
- """
603
- The `ArimaGarchStrategyBacktester` class extends the `Strategy`
604
- class to implement a backtesting framework for trading strategies based on
605
- ARIMA-GARCH models, incorporating a Hidden Markov Model (HMM) for risk management.
606
-
607
- Features
608
- ========
609
- - **ARIMA-GARCH Model**: Utilizes ARIMA for time series forecasting and GARCH for volatility forecasting, aimed at predicting market movements.
610
-
611
- - **HMM Risk Management**: Employs a Hidden Markov Model to manage risks, determining safe trading regimes.
612
-
613
- - **Event-Driven Backtesting**: Capable of simulating real-time trading conditions by processing market data and signals sequentially.
614
-
615
- Key Methods
616
- ===========
617
- - `get_data()`: Retrieves and prepares the data required for ARIMA-GARCH model predictions.
618
- - `create_signal()`: Generates trading signals based on model predictions and current market positions.
619
- - `calculate_signals(event)`: Listens for market events and triggers signal creation and event placement.
620
-
621
- """
622
-
623
- def __init__(self, bars: DataHandler, events: Queue, **kwargs):
624
- """
625
- Args:
626
- `bars`: DataHandler
627
- `events`: event queue
628
- `ticker`: Symbol of the financial instrument.
629
- `arima_window`: The window size for rolling prediction in backtesting.
630
- `quantity`: Quantity of assets to trade.
631
- `hmm_window`: Lookback period for HMM.
632
- `hmm_model`: HMM risk management model.
633
-
634
- """
635
- self.bars = bars
636
- self.symbol_list = self.bars.symbol_list
637
- self.events = events
638
-
639
- self.tiker = kwargs.get('tiker')
640
- self.arima_window = kwargs.get('arima_window', 252)
641
-
642
- self.qty = kwargs.get('qauntity', 100)
643
- self.hmm_window = kwargs.get("hmm_window", 50)
644
- self.hmm_model = kwargs.get("hmm_model")
645
-
646
- self.long_market = False
647
- self.short_market = False
648
-
649
- def get_data(self):
650
- symbol = self.tiker
651
- M = self.arima_window
652
- N = self.hmm_window
653
- dt = self.bars.get_latest_bar_datetime(self.tiker)
654
- bars = self.bars.get_latest_bars_values(
655
- symbol, "Close", N=self.arima_window
656
- )
657
- returns = self.bars.get_latest_bars_values(
658
- symbol, 'Returns', N=self.hmm_window
659
- )
660
- df = pd.DataFrame()
661
- df['Close'] = bars[-M:]
662
- df = df.dropna()
663
- data = load_and_prepare_data(df)
664
- if len(data) >= M and len(returns) >= N:
665
- return data, returns[-N:], dt
666
- return None, None, None
667
-
668
- def create_signal(self):
669
- data, returns, dt = self.get_data()
670
- signal = None
671
- if data is not None and returns is not None:
672
- prediction = get_prediction(data['diff_log_return'])
673
- regime = self.hmm_model.which_trade_allowed(returns)
674
-
675
- # If we are short the market, check for an exit
676
- if prediction > 0 and self.short_market:
677
- signal = SignalEvent(1, self.tiker, dt, "EXIT")
678
- print(f"{dt}: EXIT SHORT")
679
- self.short_market = False
680
-
681
- # If we are long the market, check for an exit
682
- elif prediction < 0 and self.long_market:
683
- signal = SignalEvent(1, self.tiker, dt, "EXIT")
684
- print(f"{dt}: EXIT LONG")
685
- self.long_market = False
686
-
687
- if regime == "LONG":
688
- # If we are not in the market, go long
689
- if prediction > 0 and not self.long_market:
690
- signal = SignalEvent(
691
- 1, self.tiker, dt, "LONG", quantity=self.qty)
692
- print(f"{dt}: LONG")
693
- self.long_market = True
694
-
695
- elif regime == "SHORT":
696
- # If we are not in the market, go short
697
- if prediction < 0 and not self.short_market:
698
- signal = SignalEvent(
699
- 1, self.tiker, dt, "SHORT", quantity=self.qty)
700
- print(f"{dt}: SHORT")
701
- self.short_market = True
702
-
703
- return signal
704
-
705
- def calculate_signals(self, event):
706
- if event.type == 'MARKET':
707
- signal = self.create_signal()
708
- if signal is not None:
709
- self.events.put(signal)
710
-
711
-
712
- def _run_backtest(
713
- strategy_name: str,
714
- capital: float, symbol_list: list, kwargs: dict):
715
- """
716
- Executes a backtest of the specified strategy
717
- integrating a Hidden Markov Model (HMM) for risk management.
718
- """
719
- hmm_data = yf.download(
720
- kwargs.get("hmm_tiker", symbol_list[0]),
721
- start=kwargs.get("hmm_start"), end=kwargs.get("hmm_end")
722
- )
723
- kwargs["hmm_model"] = HMMRiskManager(data=hmm_data, verbose=True)
724
- kwargs["strategy_name"] = strategy_name
725
-
726
- engine = Backtest(
727
- symbol_list, capital, 0.0, datetime.strptime(
728
- kwargs['yf_start'], "%Y-%m-%d"),
729
- kwargs.get("data_handler", YFHistoricDataHandler),
730
- SimulatedExecutionHandler, kwargs.pop('backtester_class'), **kwargs
731
- )
732
- engine.simulate_trading()
733
-
734
-
735
- def _run_arch_backtest(
736
- capital: float = 100000.0,
737
- quantity: int = 1000
738
- ):
739
- kwargs = {
740
- 'tiker': 'SPY',
741
- 'quantity': quantity,
742
- 'yf_start': "2004-01-02",
743
- 'backtester_class': ArimaGarchStrategyBacktester
744
- }
745
- _run_backtest("ARIMA+GARCH & HMM", capital, ["SPY"], kwargs)
746
-
747
-
748
- def _run_ou_backtest(
749
- capital: float = 100000.0,
750
- quantity: int = 2000
751
- ):
752
- kwargs = {
753
- "tiker": 'GLD',
754
- 'quantity': quantity,
755
- "n": 5,
756
- "p": 5,
757
- "hmm_window": 50,
758
- "yf_start": "2015-01-02",
759
- 'backtester_class': OUStrategyBacktester,
760
- 'ou_data': yf.download("GLD", start="2010-01-04", end="2014-12-31"),
761
- 'hmm_end': "2014-12-31"
762
- }
763
- _run_backtest("Ornstein-Uhlenbeck & HMM", capital, ['GLD'], kwargs)
764
-
765
-
766
- def _run_kf_backtest(
767
- capital: float = 100000.0,
768
- quantity: int = 2000
769
- ):
770
- symbol_list = ["IEI", "TLT"]
771
- kwargs = {
772
- "tickers": symbol_list,
773
- "quantity": quantity,
774
- "benchmark": "TLT",
775
- "yf_start": "2009-08-03",
776
- "hmm_tiker": "TLT",
777
- 'backtester_class': KLFStrategyBacktester,
778
- 'hmm_end': "2009-07-28"
779
- }
780
- _run_backtest("Kalman Filter & HMM", capital, symbol_list, kwargs)
781
-
782
-
783
- def _run_sma_backtest(
784
- capital: float = 100000.0,
785
- quantity: int = 100
786
- ):
787
- kwargs = {
788
- "quantity": quantity,
789
- "hmm_end": "2009-12-31",
790
- "hmm_tiker": "^GSPC",
791
- "yf_start": "2010-01-01",
792
- "hmm_start": "1990-01-01",
793
- "start_pos": "2023-01-01",
794
- "session_duration": 23.0,
795
- "backtester_class": SMAStrategyBacktester,
796
- "data_handler": MT5HistoricDataHandler
797
- }
798
- _run_backtest("SMA & HMM", capital, ["[SP500]"], kwargs)
799
-
800
-
801
- _BACKTESTS = {
802
- 'ou': _run_ou_backtest,
803
- 'sma': _run_sma_backtest,
804
- 'klf': _run_kf_backtest,
805
- 'arch': _run_arch_backtest
806
- }
807
-
206
+ BacktestEngine = Backtest
808
207
 
809
208
  def run_backtest(
810
- symbol_list: List[str] = ...,
811
- start_date: datetime = ...,
812
- data_handler: DataHandler = ...,
813
- strategy: Strategy = ...,
814
- exc_handler: Optional[ExecutionHandler] = None,
815
- initial_capital: Optional[float] = 100000.0,
816
- heartbeat: Optional[float] = 0.0,
817
- test_mode: Optional[bool] = True,
818
- test_strategy: Literal['ou', 'sma', 'klf', 'arch'] = 'sma',
819
- test_quantity: Optional[int] = 1000,
209
+ symbol_list: List[str],
210
+ start_date: datetime,
211
+ data_handler: DataHandler,
212
+ strategy: Strategy,
213
+ exc_handler: Optional[ExecutionHandler] = None,
214
+ initial_capital: float = 100000.0,
215
+ heartbeat: float = 0.0,
820
216
  **kwargs
821
217
  ):
822
218
  """
823
- Runs a backtest simulation based on a `DataHandler`, `Strategy` and `ExecutionHandler`.
219
+ Runs a backtest simulation based on a `DataHandler`, `Strategy`, and `ExecutionHandler`.
824
220
 
825
221
  Args:
826
- symbol_list (List[str]): List of symbol strings for the assets to be backtested.
827
- This is required when `test_mode` is set to False.
828
-
829
- start_date (datetime): Start date of the backtest. This is required when `test_mode` is False.
830
-
222
+ symbol_list (List[str]): List of symbol strings for the assets to be backtested.
223
+
224
+ start_date (datetime): Start date of the backtest.
225
+
831
226
  data_handler (DataHandler): An instance of the `DataHandler` class, responsible for managing
832
- and processing market data. Required when `test_mode` is False.
833
- There are three DataHandler classes implemented in btengine module
834
- `HistoricCSVDataHandler`, `MT5HistoricDataHandler` and `YFHistoricDataHandler`
835
- See each of this class documentation for more details.
836
- You can create your `CustumDataHandler` but it must be a subclass of `DataHandler`.
837
-
838
- strategy (Strategy): The trading strategy to be employed during the backtest. Required when `test_mode` is False.
839
- The strategy must be an instance of `Strategy` and it must have `DataHandler` and `event queue`
840
- as required positional arguments to be used int the `Backtest` class. All other argument needed to be pass
841
- to the strategy class must be in `**kwargs`. The strategy class must have `calculate_signals`
842
- methods to generate `SignalEvent` in the backtest class.
843
-
844
- exc_handler (ExecutionHandler): The execution handler for managing order executions. If not provided,
845
- a `SimulatedExecutionHandler` will be used by default. Required when `test_mode` is False.
846
- The `exc_handler` must be an instance of `ExecutionHandler` and must have `execute_order`method
847
- used to handle `OrderEvent` in events queue in the `Backtest` class.
227
+ and processing market data. Available options include `HistoricCSVDataHandler`,
228
+ `MT5HistoricDataHandler`, and `YFHistoricDataHandler`. Ensure that the `DataHandler`
229
+ instance is initialized before passing it to the function.
848
230
 
849
- initial_capital (float, optional): The initial capital for the portfolio in the backtest. Default is 100,000.
231
+ strategy (Strategy): The trading strategy to be employed during the backtest.
232
+ The strategy must be an instance of `Strategy` and should include the following attributes:
233
+ - `bars` (DataHandler): The `DataHandler` instance for the strategy.
234
+ - `events` (Queue): Queue instance for managing events.
235
+ - `symbol_list` (List[str]): List of symbols to trade.
236
+ - `mode` (str): 'live' or 'backtest'.
850
237
 
851
- heartbeat (float, optional): Time delay (in seconds) between iterations of the event-driven backtest loop.
852
- Default is 0.0, allowing the backtest to run as fast as possible.
853
- It could be also used as time frame in live trading engine (e.g 1m, 5m, 15m etc.) when listening
854
- to a live market `DataHandler`.
238
+ Additional parameters specific to the strategy should be passed in `**kwargs`.
239
+ The strategy class must implement a `calculate_signals` method to generate `SignalEvent`.
855
240
 
856
- test_mode (bool, optional): If set to True, the function runs a predefined backtest using a selected strategy
857
- (`ou`, `sma`, `klf`, `arch`). Default is True.
241
+ exc_handler (ExecutionHandler, optional): The execution handler for managing order executions.
242
+ If not provided, a `SimulatedExecutionHandler` will be used by default. This handler must
243
+ implement an `execute_order` method to process `OrderEvent` in the `Backtest` class.
858
244
 
859
- test_strategy (Literal['ou', 'sma', 'klf', 'arch'], optional): The strategy to use in test mode. Default is `sma`.
860
- - `ou` Execute `OUStrategyBacktester`, for more detail see this class documentation.
861
- - `sma` Execute `SMAStrategyBacktester`, for more detail see this class documentation.
862
- - `klf` Execute `KLFStrategyBacktester`, for more detail see this class documentation.
863
- - `arch` Execute `ArimaGarchStrategyBacktester`, for more detail see this class documentation.
245
+ initial_capital (float, optional): The initial capital for the portfolio in the backtest.
246
+ Default is 100,000.
864
247
 
865
- test_quantity (int, optional): The quantity of assets to be used in the test backtest. Default is 1000.
248
+ heartbeat (float, optional): Time delay (in seconds) between iterations of the event-driven
249
+ backtest loop. Default is 0.0, allowing the backtest to run as fast as possible. This could
250
+ also be used as a time frame in live trading (e.g., 1m, 5m, 15m) with a live `DataHandler`.
866
251
 
867
252
  **kwargs: Additional parameters passed to the `Backtest` instance, which may include strategy-specific,
868
- data handler, protfolio or execution handler options.
869
-
870
- Usage:
871
- - To run a predefined test backtest, set `test_mode=True` and select a strategy using `test_strategy`.
872
- - To customize the backtest, set `test_mode=False` and provide the required parameters (`symbol_list`,
873
- `start_date`, `data_handler`, `strategy`, `exc_handler`).
874
-
875
- Examples:
876
- >>> from bbstrader.btengine import run_backtest
877
- >>> run_backtest(test_mode=True, test_strategy='ou', test_quantity=2000)
878
-
253
+ data handler, portfolio, or execution handler options.
254
+
255
+ Notes:
256
+ This function generates three outputs:
257
+ - A performance summary saved as an HTML file.
258
+ - An equity curve of the portfolio saved as a CSV file.
259
+ - Monthly returns saved as a PNG image.
260
+
261
+ Example:
262
+ >>> from bbstrader.trading.strategies import StockIndexSTBOTrading
263
+ >>> from bbstrader.metatrader.utils import config_logger
264
+ >>> from bbstrader.datahandlers import MT5HistoricDataHandler
265
+ >>> from bbstrader.execution import MT5ExecutionHandler
266
+ >>> from datetime import datetime
267
+ >>>
268
+ >>> logger = config_logger('index_trade.log', console_log=True)
269
+ >>> symbol_list = ['[SP500]', 'GERMANY40', '[DJI30]', '[NQ100]']
270
+ >>> start = datetime(2010, 6, 1, 2, 0, 0)
271
+ >>> kwargs = {
272
+ ... 'expected_returns': {'[NQ100]': 1.5, '[SP500]': 1.5, '[DJI30]': 1.0, 'GERMANY40': 1.0},
273
+ ... 'quantities': {'[NQ100]': 15, '[SP500]': 30, '[DJI30]': 5, 'GERMANY40': 10},
274
+ ... 'max_trades': {'[NQ100]': 3, '[SP500]': 3, '[DJI30]': 3, 'GERMANY40': 3},
275
+ ... 'mt5_start': start,
276
+ ... 'time_frame': '15m',
277
+ ... 'strategy_name': 'SISTBO',
278
+ ... }
879
279
  >>> run_backtest(
880
- ... symbol_list=['AAPL', 'GOOG'],
881
- ... start_date=datetime(2020, 1, 1),
882
- ... data_handler=CustomDataHandler(),
883
- ... strategy=MovingAverageStrategy(),
884
- ... exc_handler=CustomExecutionHandler(),
885
- ... initial_capital=500000.0,
886
- ... heartbeat=1.0
280
+ ... symbol_list=symbol_list,
281
+ ... start_date=start,
282
+ ... data_handler=MT5HistoricDataHandler(),
283
+ ... strategy=StockIndexSTBOTrading(),
284
+ ... exc_handler=MT5ExecutionHandler(),
285
+ ... initial_capital=100000.0,
286
+ ... heartbeat=0.0,
287
+ ... **kwargs
887
288
  ... )
888
289
  """
889
- if test_mode:
890
- _BACKTESTS[test_strategy](
891
- capital=initial_capital,
892
- quantity=test_quantity
893
- )
290
+ if exc_handler is None:
291
+ execution_handler = SimulatedExecutionHandler
894
292
  else:
895
- execution_handler = kwargs.get("exc_handler", SimulatedExecutionHandler)
896
- engine = Backtest(
897
- symbol_list, initial_capital, heartbeat, start_date,
898
- data_handler, execution_handler, strategy, **kwargs
899
- )
900
- engine.simulate_trading()
293
+ execution_handler = exc_handler
294
+ engine = BacktestEngine(
295
+ symbol_list, initial_capital, heartbeat, start_date,
296
+ data_handler, execution_handler, strategy, **kwargs
297
+ )
298
+ engine.simulate_trading()