bbstrader 0.1.94__py3-none-any.whl → 0.2.1__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.

Files changed (38) hide show
  1. bbstrader/__ini__.py +9 -9
  2. bbstrader/btengine/__init__.py +7 -7
  3. bbstrader/btengine/backtest.py +30 -26
  4. bbstrader/btengine/data.py +100 -79
  5. bbstrader/btengine/event.py +2 -1
  6. bbstrader/btengine/execution.py +18 -16
  7. bbstrader/btengine/performance.py +11 -7
  8. bbstrader/btengine/portfolio.py +35 -36
  9. bbstrader/btengine/strategy.py +119 -94
  10. bbstrader/config.py +14 -8
  11. bbstrader/core/__init__.py +0 -0
  12. bbstrader/core/data.py +22 -0
  13. bbstrader/core/utils.py +57 -0
  14. bbstrader/ibkr/__init__.py +0 -0
  15. bbstrader/ibkr/utils.py +0 -0
  16. bbstrader/metatrader/__init__.py +5 -5
  17. bbstrader/metatrader/account.py +117 -121
  18. bbstrader/metatrader/rates.py +83 -80
  19. bbstrader/metatrader/risk.py +23 -37
  20. bbstrader/metatrader/trade.py +169 -140
  21. bbstrader/metatrader/utils.py +3 -3
  22. bbstrader/models/__init__.py +5 -5
  23. bbstrader/models/factors.py +280 -0
  24. bbstrader/models/ml.py +1092 -0
  25. bbstrader/models/optimization.py +31 -28
  26. bbstrader/models/{portfolios.py → portfolio.py} +64 -46
  27. bbstrader/models/risk.py +15 -9
  28. bbstrader/trading/__init__.py +2 -2
  29. bbstrader/trading/execution.py +252 -164
  30. bbstrader/trading/scripts.py +8 -4
  31. bbstrader/trading/strategies.py +79 -66
  32. bbstrader/tseries.py +482 -107
  33. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
  34. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/METADATA +6 -1
  35. bbstrader-0.2.1.dist-info/RECORD +37 -0
  36. bbstrader-0.1.94.dist-info/RECORD +0 -32
  37. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
  38. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/top_level.txt +0 -0
@@ -2,28 +2,27 @@
2
2
  Strategies module for trading strategies backtesting and execution.
3
3
  """
4
4
 
5
+ from datetime import datetime
6
+ from queue import Queue
7
+ from typing import Dict, List, Literal, Optional, Union
8
+
5
9
  import numpy as np
6
10
  import pandas as pd
7
- from queue import Queue
8
11
  import yfinance as yf
9
- from datetime import datetime
10
- from bbstrader.metatrader.rates import Rates
11
- from bbstrader.metatrader.account import Account
12
- from bbstrader.btengine.event import SignalEvent
13
- from bbstrader.btengine.data import DataHandler
14
- from bbstrader.models.risk import HMMRiskManager
15
- from bbstrader.models.risk import build_hmm_models
12
+
16
13
  from bbstrader.btengine.backtest import BacktestEngine
14
+ from bbstrader.btengine.data import DataHandler, MT5DataHandler, YFDataHandler
15
+ from bbstrader.btengine.event import SignalEvent
16
+ from bbstrader.btengine.execution import MT5ExecutionHandler, SimExecutionHandler
17
17
  from bbstrader.btengine.strategy import Strategy
18
- from bbstrader.btengine.strategy import MT5Strategy
19
- from bbstrader.btengine.execution import *
20
- from bbstrader.btengine.data import *
21
- from bbstrader.tseries import KalmanFilterModel, ArimaGarchModel
22
- from typing import Union, Optional, Literal, Dict, List
18
+ from bbstrader.metatrader.account import Account
19
+ from bbstrader.metatrader.rates import Rates
20
+ from bbstrader.models.risk import build_hmm_models
21
+ from bbstrader.tseries import ArimaGarchModel, KalmanFilterModel
23
22
 
24
23
  __all__ = [
25
- 'SMAStrategy',
26
- 'ArimaGarchStrategy',
24
+ 'SMAStrategy',
25
+ 'ArimaGarchStrategy',
27
26
  'KalmanFilterStrategy',
28
27
  'StockIndexSTBOTrading',
29
28
  'test_strategy',
@@ -36,7 +35,7 @@ def get_quantities(quantities, symbol_list):
36
35
  return quantities
37
36
  elif isinstance(quantities, int):
38
37
  return {symbol: quantities for symbol in symbol_list}
39
-
38
+
40
39
 
41
40
  class SMAStrategy(Strategy):
42
41
  """
@@ -56,9 +55,9 @@ class SMAStrategy(Strategy):
56
55
  """
57
56
 
58
57
  def __init__(
59
- self, bars: DataHandler = None,
58
+ self, bars: DataHandler = None,
60
59
  events: Queue = None,
61
- symbol_list: List[str] = None,
60
+ symbol_list: List[str] = None,
62
61
  mode: Literal['backtest', 'live'] = 'backtest',
63
62
  **kwargs
64
63
  ):
@@ -100,7 +99,6 @@ class SMAStrategy(Strategy):
100
99
  def get_backtest_data(self):
101
100
  symbol_data = {symbol: None for symbol in self.symbol_list}
102
101
  for s in self.symbol_list:
103
- latest_bars = self.bars.get_latest_bars(s, N=self.long_window)
104
102
  bar_date = self.bars.get_latest_bar_datetime(s)
105
103
  bars = self.bars.get_latest_bars_values(
106
104
  s, "adj_close", N=self.long_window
@@ -157,17 +155,20 @@ class SMAStrategy(Strategy):
157
155
  def get_live_data(self):
158
156
  symbol_data = {symbol: None for symbol in self.symbol_list}
159
157
  for symbol in self.symbol_list:
160
- sig_rate = Rates(symbol, self.tf, 0, self.risk_window+2, **self.kwargs)
158
+ sig_rate = Rates(symbol, self.tf, 0,
159
+ self.risk_window+2, **self.kwargs)
161
160
  hmm_data = sig_rate.returns.values
162
161
  prices = sig_rate.close.values
163
- current_regime = self.risk_models[symbol].which_trade_allowed(hmm_data)
164
- assert len(prices) >= self.long_window and len(hmm_data) >= self.risk_window
162
+ current_regime = self.risk_models[symbol].which_trade_allowed(
163
+ hmm_data)
164
+ assert len(prices) >= self.long_window and len(
165
+ hmm_data) >= self.risk_window
165
166
  short_sma = np.mean(prices[-self.short_window:])
166
167
  long_sma = np.mean(prices[-self.long_window:])
167
168
  short_sma, long_sma, current_regime
168
169
  symbol_data[symbol] = (short_sma, long_sma, current_regime)
169
170
  return symbol_data
170
-
171
+
171
172
  def create_live_signals(self):
172
173
  signals = {symbol: None for symbol in self.symbol_list}
173
174
  symbol_data = self.get_live_data()
@@ -182,7 +183,7 @@ class SMAStrategy(Strategy):
182
183
  signal = 'SHORT'
183
184
  signals[symbol] = signal
184
185
  return signals
185
-
186
+
186
187
  def calculate_signals(self, event=None):
187
188
  if self.mode == 'backtest' and event is not None:
188
189
  if event.type == 'MARKET':
@@ -204,9 +205,9 @@ class ArimaGarchStrategy(Strategy):
204
205
  Features
205
206
  ========
206
207
  - **ARIMA-GARCH Model**: Utilizes ARIMA for time series forecasting and GARCH for volatility forecasting, aimed at predicting market movements.
207
-
208
+
208
209
  - **HMM Risk Management**: Employs a Hidden Markov Model to manage risks, determining safe trading regimes.
209
-
210
+
210
211
  - **Event-Driven Backtesting**: Capable of simulating real-time trading conditions by processing market data and signals sequentially.
211
212
 
212
213
  - **Live Trading**: Supports real-time trading by generating signals based on live ARIMA-GARCH predictions and HMM risk management.
@@ -218,15 +219,15 @@ class ArimaGarchStrategy(Strategy):
218
219
  - `get_live_data()`: Retrieves live data for real-time trading.
219
220
  - `create_live_signals()`: Generates trading signals based on live ARIMA-GARCH predictions and HMM risk management.
220
221
  - `calculate_signals()`: Determines the trading signals based on the mode of operation (backtest or live).
221
-
222
+
222
223
  """
223
224
 
224
- def __init__(self,
225
- bars: DataHandler = None,
225
+ def __init__(self,
226
+ bars: DataHandler = None,
226
227
  events: Queue = None,
227
228
  symbol_list: List[str] = None,
228
229
  mode: Literal['backtest', 'live'] = 'backtest',
229
- **kwargs):
230
+ **kwargs):
230
231
  """
231
232
  Args:
232
233
  `bars`: A data handler object that provides market data.
@@ -242,8 +243,8 @@ class ArimaGarchStrategy(Strategy):
242
243
  self.events = events
243
244
  self.symbol_list = symbol_list or self.bars.symbol_list
244
245
  self.mode = mode
245
-
246
- self.qty = get_quantities(
246
+
247
+ self.qty = get_quantities(
247
248
  kwargs.get('quantities', 100), self.symbol_list)
248
249
  self.arima_window = kwargs.get('arima_window', 252)
249
250
  self.tf = kwargs.get('time_frame', 'D1')
@@ -251,9 +252,9 @@ class ArimaGarchStrategy(Strategy):
251
252
  self.risk_window = kwargs.get("hmm_window", 50)
252
253
  self.risk_models = build_hmm_models(self.symbol_list, **kwargs)
253
254
  self.arima_models = self._build_arch_models(**kwargs)
254
-
255
- self.long_market = {s : False for s in self.symbol_list}
256
- self.short_market = {s : False for s in self.symbol_list}
255
+
256
+ self.long_market = {s: False for s in self.symbol_list}
257
+ self.short_market = {s: False for s in self.symbol_list}
257
258
 
258
259
  def _build_arch_models(self, **kwargs) -> Dict[str, ArimaGarchModel]:
259
260
  arch_models = {symbol: None for symbol in self.symbol_list}
@@ -263,11 +264,11 @@ class ArimaGarchStrategy(Strategy):
263
264
  data = rates.get_rates_from_pos()
264
265
  assert data is not None, f"No data for {symbol}"
265
266
  except AssertionError:
266
- data = yf.download(symbol, start=kwargs.get('yf_start'))
267
+ data = yf.download(symbol, start=kwargs.get('yf_start'))
267
268
  arch = ArimaGarchModel(symbol, data, k=self.arima_window)
268
269
  arch_models[symbol] = arch
269
270
  return arch_models
270
-
271
+
271
272
  def get_backtest_data(self):
272
273
  symbol_data = {symbol: None for symbol in self.symbol_list}
273
274
  for symbol in self.symbol_list:
@@ -335,12 +336,13 @@ class ArimaGarchStrategy(Strategy):
335
336
  for symbol in self.symbol_list:
336
337
  arch_data = Rates(symbol, self.tf, 0, self.arima_window)
337
338
  rates = arch_data.get_rates_from_pos()
338
- arch_returns = self.arima_models[symbol].load_and_prepare_data(rates)
339
+ arch_returns = self.arima_models[symbol].load_and_prepare_data(
340
+ rates)
339
341
  window_data = arch_returns['diff_log_return'].iloc[-self.arima_window:]
340
342
  hmm_returns = arch_data.returns.values[-self.risk_window:]
341
343
  symbol_data[symbol] = (window_data, hmm_returns)
342
344
  return symbol_data
343
-
345
+
344
346
  def create_live_signals(self):
345
347
  signals = {symbol: None for symbol in self.symbol_list}
346
348
  data = self.get_live_data()
@@ -348,8 +350,10 @@ class ArimaGarchStrategy(Strategy):
348
350
  symbol_data = data[symbol]
349
351
  if symbol_data is not None:
350
352
  window_data, hmm_returns = symbol_data
351
- prediction = self.arima_models[symbol].get_prediction(window_data)
352
- regime = self.risk_models[symbol].which_trade_allowed(hmm_returns)
353
+ prediction = self.arima_models[symbol].get_prediction(
354
+ window_data)
355
+ regime = self.risk_models[symbol].which_trade_allowed(
356
+ hmm_returns)
353
357
  if regime == "LONG":
354
358
  if prediction > 0:
355
359
  signals[symbol] = "LONG"
@@ -357,7 +361,7 @@ class ArimaGarchStrategy(Strategy):
357
361
  if prediction < 0:
358
362
  signals[symbol] = "SHORT"
359
363
  return signals
360
-
364
+
361
365
  def calculate_signals(self, event=None):
362
366
  if self.mode == 'backtest' and event is not None:
363
367
  if event.type == 'MARKET':
@@ -369,7 +373,7 @@ class ArimaGarchStrategy(Strategy):
369
373
  return self.create_live_signals()
370
374
 
371
375
 
372
- class KalmanFilterStrategy(Strategy):
376
+ class KalmanFilterStrategy(Strategy):
373
377
  """
374
378
  The `KalmanFilterStrategy` class implements a backtesting framework for a
375
379
  [pairs trading](https://en.wikipedia.org/wiki/Pairs_trade) strategy using
@@ -378,12 +382,12 @@ class KalmanFilterStrategy(Strategy):
378
382
  including initialization parameters, main functions, and an example of how to run a backtest.
379
383
  """
380
384
 
381
- def __init__(self,
382
- bars: DataHandler = None,
385
+ def __init__(self,
386
+ bars: DataHandler = None,
383
387
  events: Queue = None,
384
388
  symbol_list: List[str] = None,
385
389
  mode: Literal['backtest', 'live'] = 'backtest',
386
- **kwargs):
390
+ **kwargs):
387
391
  """
388
392
  Args:
389
393
  `bars`: `DataHandler` for market data handling.
@@ -404,7 +408,7 @@ class KalmanFilterStrategy(Strategy):
404
408
 
405
409
  self.hmm_tiker = kwargs.get("hmm_tiker")
406
410
  self._assert_tikers()
407
- self.account = Account()
411
+ self.account = Account(**kwargs)
408
412
  self.hmm_window = kwargs.get("hmm_window", 50)
409
413
  self.qty = kwargs.get("quantity", 100)
410
414
  self.tf = kwargs.get("time_frame", "D1")
@@ -424,7 +428,7 @@ class KalmanFilterStrategy(Strategy):
424
428
  if self.hmm_tiker is None:
425
429
  raise ValueError(
426
430
  "You need to provide a ticker used by the HMM for risk management")
427
-
431
+
428
432
  def calculate_btxy(self, etqt, regime, dt):
429
433
  # Make sure there is no position open
430
434
  if etqt is None:
@@ -499,7 +503,7 @@ class KalmanFilterStrategy(Strategy):
499
503
  signals[self.tickers[0]] = x_signal
500
504
  signals[self.tickers[1]] = y_signal
501
505
  return signals
502
-
506
+
503
507
  def calculate_backtest_signals(self):
504
508
  p0, p1 = self.tickers[0], self.tickers[1]
505
509
  dt = self.bars.get_latest_bar_datetime(p0)
@@ -543,7 +547,7 @@ class KalmanFilterStrategy(Strategy):
543
547
  self.calculate_backtest_signals()
544
548
  elif self.mode == 'live':
545
549
  return self.calculate_live_signals()
546
-
550
+
547
551
 
548
552
  class StockIndexSTBOTrading(Strategy):
549
553
  """
@@ -554,12 +558,13 @@ class StockIndexSTBOTrading(Strategy):
554
558
  to recover. It operates in two modes: backtest and live, and it is particularly
555
559
  tailored to index trading.
556
560
  """
557
- def __init__(self,
558
- bars: DataHandler = None,
561
+
562
+ def __init__(self,
563
+ bars: DataHandler = None,
559
564
  events: Queue = None,
560
565
  symbol_list: List[str] = None,
561
566
  mode: Literal['backtest', 'live'] = 'backtest',
562
- **kwargs):
567
+ **kwargs):
563
568
  """
564
569
  Args:
565
570
  `bars`: `DataHandler` for market data handling.
@@ -599,7 +604,7 @@ class StockIndexSTBOTrading(Strategy):
599
604
  self.last_price = {index: None for index in symbols}
600
605
  self.heightest_price = {index: None for index in symbols}
601
606
  self.lowerst_price = {index: None for index in symbols}
602
-
607
+
603
608
  if self.mode == 'backtest':
604
609
  self.qty = get_quantities(quantities, symbols)
605
610
  self.num_buys = {index: 0 for index in symbols}
@@ -644,7 +649,7 @@ class StockIndexSTBOTrading(Strategy):
644
649
  current_price, avg_price) >= (self.expeted_return[index]):
645
650
  signals[index] = 'EXIT'
646
651
  self.logger.info(
647
- f"SYMBOL={index} - Hp={self.heightest_price[index]} - "
652
+ f"SYMBOL={index} - Hp={self.heightest_price[index]} - "
648
653
  f"Lp={self.lowerst_price[index]} - Cp={current_price} - %chg={round(down_change, 2)}"
649
654
  )
650
655
  return signals
@@ -672,8 +677,8 @@ class StockIndexSTBOTrading(Strategy):
672
677
  current_price, self.heightest_price[index])
673
678
 
674
679
  if (down_change <= - (self.expeted_return[index]/self.rr)
675
- and self.num_buys[index] <= self.max_trades[index]):
676
- signal = SignalEvent(100, index, dt, 'LONG',
680
+ and self.num_buys[index] <= self.max_trades[index]):
681
+ signal = SignalEvent(100, index, dt, 'LONG',
677
682
  quantity=self.qty[index], price=current_price)
678
683
  self.events.put(signal)
679
684
  self.num_buys[index] += 1
@@ -685,7 +690,8 @@ class StockIndexSTBOTrading(Strategy):
685
690
  qty = self.qty[index] * self.num_buys[index]
686
691
  if self._calculate_pct_change(
687
692
  current_price, av_price) >= (self.expeted_return[index]):
688
- signal = SignalEvent(100, index, dt, 'EXIT', quantity=qty, price=current_price)
693
+ signal = SignalEvent(
694
+ 100, index, dt, 'EXIT', quantity=qty, price=current_price)
689
695
  self.events.put(signal)
690
696
  self.num_buys[index] = 0
691
697
  self.buy_prices[index] = []
@@ -709,12 +715,13 @@ def _run_backtest(
709
715
  engine = BacktestEngine(
710
716
  symbol_list, capital, 0.0, datetime.strptime(
711
717
  kwargs['yf_start'], "%Y-%m-%d"),
712
- kwargs.get("data_handler", YFDataHandler),
713
- kwargs.get("exc_handler", SimExecutionHandler),
718
+ kwargs.get("data_handler", YFDataHandler),
719
+ kwargs.get("exc_handler", SimExecutionHandler),
714
720
  kwargs.pop('backtester_class'), **kwargs
715
721
  )
716
722
  engine.simulate_trading()
717
723
 
724
+
718
725
  def _run_arch_backtest(
719
726
  capital: float = 100000.0,
720
727
  quantity: int = 1000
@@ -730,6 +737,7 @@ def _run_arch_backtest(
730
737
  }
731
738
  _run_backtest("ARIMA+GARCH & HMM", capital, ["^GSPC"], kwargs)
732
739
 
740
+
733
741
  def _run_kf_backtest(
734
742
  capital: float = 100000.0,
735
743
  quantity: int = 2000
@@ -748,24 +756,26 @@ def _run_kf_backtest(
748
756
  }
749
757
  _run_backtest("Kalman Filter & HMM", capital, symbol_list, kwargs)
750
758
 
759
+
751
760
  def _run_sma_backtest(
752
761
  capital: float = 100000.0,
753
762
  quantity: int = 1
754
- ):
755
- spx_data = yf.download("^GSPC",start="1990-01-01", end="2009-12-31")
763
+ ):
764
+ spx_data = yf.download("^GSPC", start="1990-01-01", end="2009-12-31")
756
765
  kwargs = {
757
766
  "quantities": quantity,
758
767
  "hmm_end": "2009-12-31",
759
768
  "yf_start": "2010-01-04",
760
769
  "hmm_data": spx_data,
761
- "mt5_start": datetime(2010,1,1),
762
- "mt5_end": datetime(2023,1,1),
770
+ "mt5_start": datetime(2010, 1, 1),
771
+ "mt5_end": datetime(2023, 1, 1),
763
772
  "backtester_class": SMAStrategy,
764
773
  "data_handler": MT5DataHandler,
765
774
  "exc_handler": MT5ExecutionHandler
766
775
  }
767
776
  _run_backtest("SMA & HMM", capital, ["[SP500]"], kwargs)
768
777
 
778
+
769
779
  def _run_sistbo_backtest(
770
780
  capital: float = 100000.0,
771
781
  quantity: int = None
@@ -789,7 +799,9 @@ def _run_sistbo_backtest(
789
799
  "data_handler": MT5DataHandler,
790
800
  "exc_handler": MT5ExecutionHandler
791
801
  }
792
- _run_backtest("Stock Index Short Term Buy Only ", capital, symbol_list, kwargs)
802
+ _run_backtest("Stock Index Short Term Buy Only ",
803
+ capital, symbol_list, kwargs)
804
+
793
805
 
794
806
  _BACKTESTS = {
795
807
  'sma': _run_sma_backtest,
@@ -798,8 +810,9 @@ _BACKTESTS = {
798
810
  'sistbo': _run_sistbo_backtest
799
811
  }
800
812
 
813
+
801
814
  def test_strategy(strategy: Literal['sma', 'klf', 'arch', 'sistbo'] = 'sma',
802
- quantity: Optional[int] = 100):
815
+ quantity: Optional[int] = 100):
803
816
  """
804
817
  Executes a backtest of the specified strategy
805
818