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

@@ -0,0 +1,57 @@
1
+ import asyncio
2
+ from telegram import Bot
3
+ from notifypy import Notify
4
+ from telegram.error import TelegramError
5
+
6
+ __all__ = ['send_telegram_message', 'send_notification', 'send_message']
7
+
8
+ async def send_telegram_message(token, chat_id, text=''):
9
+ """
10
+ Send a message to a telegram chat
11
+
12
+ Args:
13
+ token: str: Telegram bot token
14
+ chat_id: int or str or list: Chat id or list of chat ids
15
+ text: str: Message to send
16
+ """
17
+ try:
18
+ bot = Bot(token=token)
19
+ if isinstance(chat_id, (int, str)):
20
+ chat_id = [chat_id]
21
+ for id in chat_id:
22
+ await bot.send_message(chat_id=id, text=text)
23
+ except TelegramError as e:
24
+ print(f"Error sending message: {e}")
25
+
26
+ def send_notification(title, message=''):
27
+ """
28
+ Send a desktop notification
29
+
30
+ Args:
31
+ title: str: Title of the notification
32
+ message: str: Message of the notification
33
+ """
34
+ notification = Notify(default_notification_application_name='bbstrading')
35
+ notification.title = title
36
+ notification.message = message
37
+ notification.send()
38
+
39
+ def send_message(title='SIGNAL', message='New signal',
40
+ notify_me=False, telegram=False, token=None, chat_id=None):
41
+ """
42
+ Send a message to the user
43
+
44
+ Args:
45
+ title: str: Title of the message
46
+ message: str: Message of the message
47
+ notify_me: bool: Send a desktop notification
48
+ telegram: bool: Send a telegram message
49
+ token: str: Telegram bot token
50
+ chat_id: int or str or list: Chat id or list of chat ids
51
+ """
52
+ if notify_me:
53
+ send_notification(title, message=message)
54
+ if telegram:
55
+ if token is None or chat_id is None:
56
+ raise ValueError('Token and chat_id must be provided')
57
+ asyncio.run(send_telegram_message(token, chat_id, text=message))
@@ -15,10 +15,10 @@ from bbstrader.models.risk import HMMRiskManager
15
15
  from bbstrader.models.risk import build_hmm_models
16
16
  from bbstrader.btengine.backtest import BacktestEngine
17
17
  from bbstrader.btengine.strategy import Strategy
18
+ from bbstrader.btengine.strategy import MT5Strategy
18
19
  from bbstrader.btengine.execution import *
19
20
  from bbstrader.btengine.data import *
20
- from bbstrader.tseries import (
21
- KalmanFilterModel, ArimaGarchModel)
21
+ from bbstrader.tseries import KalmanFilterModel, ArimaGarchModel
22
22
  from typing import Union, Optional, Literal, Dict, List
23
23
 
24
24
  __all__ = [
@@ -37,6 +37,7 @@ def get_quantities(quantities, symbol_list):
37
37
  elif isinstance(quantities, int):
38
38
  return {symbol: quantities for symbol in symbol_list}
39
39
 
40
+
40
41
  class SMAStrategy(Strategy):
41
42
  """
42
43
  Carries out a basic Moving Average Crossover strategy bactesting with a
@@ -76,10 +77,7 @@ class SMAStrategy(Strategy):
76
77
  """
77
78
  self.bars = bars
78
79
  self.events = events
79
- if symbol_list is not None:
80
- self.symbol_list = symbol_list
81
- else:
82
- self.symbol_list = self.bars.symbol_list
80
+ self.symbol_list = symbol_list or self.bars.symbol_list
83
81
  self.mode = mode
84
82
 
85
83
  self.short_window = kwargs.get("short_window", 50)
@@ -102,12 +100,13 @@ class SMAStrategy(Strategy):
102
100
  def get_backtest_data(self):
103
101
  symbol_data = {symbol: None for symbol in self.symbol_list}
104
102
  for s in self.symbol_list:
103
+ latest_bars = self.bars.get_latest_bars(s, N=self.long_window)
105
104
  bar_date = self.bars.get_latest_bar_datetime(s)
106
105
  bars = self.bars.get_latest_bars_values(
107
- s, "Adj Close", N=self.long_window
106
+ s, "adj_close", N=self.long_window
108
107
  )
109
108
  returns_val = self.bars.get_latest_bars_values(
110
- s, "Returns", N=self.risk_window
109
+ s, "returns", N=self.risk_window
111
110
  )
112
111
  if len(bars) >= self.long_window and len(returns_val) >= self.risk_window:
113
112
  regime = self.risk_models[s].which_trade_allowed(returns_val)
@@ -124,7 +123,7 @@ class SMAStrategy(Strategy):
124
123
  for s, data in symbol_data.items():
125
124
  signal = None
126
125
  if data is not None:
127
- price = self.bars.get_latest_bar_value(s, "Adj Close")
126
+ price = self.bars.get_latest_bar_value(s, "adj_close")
128
127
  short_sma, long_sma, regime, bar_date = data
129
128
  dt = bar_date
130
129
  if regime == "LONG":
@@ -159,8 +158,8 @@ class SMAStrategy(Strategy):
159
158
  symbol_data = {symbol: None for symbol in self.symbol_list}
160
159
  for symbol in self.symbol_list:
161
160
  sig_rate = Rates(symbol, self.tf, 0, self.risk_window)
162
- hmm_data = sig_rate.get_returns.values
163
- prices = sig_rate.get_close.values
161
+ hmm_data = sig_rate.returns.values
162
+ prices = sig_rate.close.values
164
163
  current_regime = self.risk_models[symbol].which_trade_allowed(hmm_data)
165
164
  assert len(prices) >= self.long_window and len(hmm_data) >= self.risk_window
166
165
  short_sma = np.mean(prices[-self.short_window:])
@@ -241,10 +240,7 @@ class ArimaGarchStrategy(Strategy):
241
240
  """
242
241
  self.bars = bars
243
242
  self.events = events
244
- if symbol_list is not None:
245
- self.symbol_list = symbol_list
246
- else:
247
- self.symbol_list = self.bars.symbol_list
243
+ self.symbol_list = symbol_list or self.bars.symbol_list
248
244
  self.mode = mode
249
245
 
250
246
  self.qty = get_quantities(
@@ -259,7 +255,6 @@ class ArimaGarchStrategy(Strategy):
259
255
  self.long_market = {s : False for s in self.symbol_list}
260
256
  self.short_market = {s : False for s in self.symbol_list}
261
257
 
262
-
263
258
  def _build_arch_models(self, **kwargs) -> Dict[str, ArimaGarchModel]:
264
259
  arch_models = {symbol: None for symbol in self.symbol_list}
265
260
  for symbol in self.symbol_list:
@@ -280,10 +275,10 @@ class ArimaGarchStrategy(Strategy):
280
275
  N = self.risk_window
281
276
  dt = self.bars.get_latest_bar_datetime(symbol)
282
277
  bars = self.bars.get_latest_bars_values(
283
- symbol, "Close", N=self.arima_window
278
+ symbol, "close", N=self.arima_window
284
279
  )
285
280
  returns = self.bars.get_latest_bars_values(
286
- symbol, 'Returns', N=self.risk_window
281
+ symbol, 'returns', N=self.risk_window
287
282
  )
288
283
  df = pd.DataFrame()
289
284
  df['Close'] = bars[-M:]
@@ -303,7 +298,7 @@ class ArimaGarchStrategy(Strategy):
303
298
  signal = None
304
299
  prediction = self.arima_models[symbol].get_prediction(data)
305
300
  regime = self.risk_models[symbol].which_trade_allowed(returns)
306
- price = self.bars.get_latest_bar_value(symbol, "Adj Close")
301
+ price = self.bars.get_latest_bar_value(symbol, "adj_close")
307
302
 
308
303
  # If we are short the market, check for an exit
309
304
  if prediction > 0 and self.short_market[symbol]:
@@ -342,14 +337,15 @@ class ArimaGarchStrategy(Strategy):
342
337
  rates = arch_data.get_rates_from_pos()
343
338
  arch_returns = self.arima_models[symbol].load_and_prepare_data(rates)
344
339
  window_data = arch_returns['diff_log_return'].iloc[-self.arima_window:]
345
- hmm_returns = arch_data.get_returns.values[-self.risk_window:]
340
+ hmm_returns = arch_data.returns.values[-self.risk_window:]
346
341
  symbol_data[symbol] = (window_data, hmm_returns)
347
342
  return symbol_data
348
343
 
349
344
  def create_live_signals(self):
350
345
  signals = {symbol: None for symbol in self.symbol_list}
346
+ data = self.get_live_data()
351
347
  for symbol in self.symbol_list:
352
- symbol_data = self.get_live_data()[symbol]
348
+ symbol_data = data[symbol]
353
349
  if symbol_data is not None:
354
350
  window_data, hmm_returns = symbol_data
355
351
  prediction = self.arima_models[symbol].get_prediction(window_data)
@@ -403,14 +399,12 @@ class KalmanFilterStrategy(Strategy):
403
399
  """
404
400
  self.bars = bars
405
401
  self.events_queue = events
406
- if symbol_list is not None:
407
- self.symbol_list = symbol_list
408
- else:
409
- self.symbol_list = self.bars.symbol_list
402
+ self.symbol_list = symbol_list or self.bars.symbol_list
410
403
  self.mode = mode
411
404
 
412
405
  self.hmm_tiker = kwargs.get("hmm_tiker")
413
406
  self._assert_tikers()
407
+ self.account = Account()
414
408
  self.hmm_window = kwargs.get("hmm_window", 50)
415
409
  self.qty = kwargs.get("quantity", 100)
416
410
  self.tf = kwargs.get("time_frame", "D1")
@@ -437,8 +431,8 @@ class KalmanFilterStrategy(Strategy):
437
431
  return
438
432
  et, sqrt_Qt = etqt
439
433
  theta = self.kl_model.theta
440
- p1 = self.bars.get_latest_bar_value(self.tickers[1], "Adj Close")
441
- p0 = self.bars.get_latest_bar_value(self.tickers[0], "Adj Close")
434
+ p1 = self.bars.get_latest_bar_value(self.tickers[1], "adj_close")
435
+ p0 = self.bars.get_latest_bar_value(self.tickers[0], "adj_close")
442
436
  if et >= -sqrt_Qt and self.long_market:
443
437
  print("CLOSING LONG: %s" % dt)
444
438
  y_signal = SignalEvent(1, self.tickers[1], dt, "EXIT", price=p1)
@@ -481,14 +475,9 @@ class KalmanFilterStrategy(Strategy):
481
475
 
482
476
  def calculate_livexy(self):
483
477
  signals = {symbol: None for symbol in self.symbol_list}
484
- p0_ = Rates(self.tickers[0], self.tf, 0, 10)
485
- p1_ = Rates(self.tickers[1], self.tf, 0, 10)
486
-
487
- p0_data = p0_.get_close
488
- p1_data = p1_.get_close
489
- prices = np.array(
490
- [p0_data.values[-1], p1_data.values[-1]]
491
- )
478
+ p0_price = self.account.get_tick_info(self.tickers[0]).ask
479
+ p1_price = self.account.get_tick_info(self.tickers[1]).ask
480
+ prices = np.array([p0_price, p1_price])
492
481
  et_std = self.kl_model.calculate_etqt(prices)
493
482
  if et_std is not None:
494
483
  et, std = et_std
@@ -514,19 +503,15 @@ class KalmanFilterStrategy(Strategy):
514
503
  def calculate_backtest_signals(self):
515
504
  p0, p1 = self.tickers[0], self.tickers[1]
516
505
  dt = self.bars.get_latest_bar_datetime(p0)
517
- _x = self.bars.get_latest_bars_values(
518
- p0, "Close", N=1
519
- )
520
- _y = self.bars.get_latest_bars_values(
521
- p1, "Close", N=1
522
- )
506
+ x = self.bars.get_latest_bar_value(p0, "close")
507
+ y = self.bars.get_latest_bar_value(p1, "close")
523
508
  returns = self.bars.get_latest_bars_values(
524
- self.hmm_tiker, "Returns", N=self.hmm_window
509
+ self.hmm_tiker, "returns", N=self.hmm_window
525
510
  )
526
511
  latest_prices = np.array([-1.0, -1.0])
527
512
  if len(returns) >= self.hmm_window:
528
- latest_prices[0] = _x[-1]
529
- latest_prices[1] = _y[-1]
513
+ latest_prices[0] = x
514
+ latest_prices[1] = y
530
515
  et_qt = self.kl_model.calculate_etqt(latest_prices)
531
516
  regime = self.risk_model[
532
517
  self.hmm_tiker].which_trade_allowed(returns)
@@ -537,7 +522,7 @@ class KalmanFilterStrategy(Strategy):
537
522
  signals = {symbol: None for symbol in self.symbol_list}
538
523
  initial_signals = self.calculate_livexy()
539
524
  hmm_data = Rates(self.hmm_ticker, self.tf, 0, self.hmm_window)
540
- returns = hmm_data.get_returns.values
525
+ returns = hmm_data.returns.values
541
526
  current_regime = self.risk_model[
542
527
  self.hmm_tiker].which_trade_allowed(returns)
543
528
  for symbol in self.symbol_list:
@@ -592,10 +577,7 @@ class StockIndexSTBOTrading(Strategy):
592
577
  """
593
578
  self.bars = bars
594
579
  self.events = events
595
- if symbol_list is not None:
596
- self.symbol_list = symbol_list
597
- else:
598
- self.symbol_list = self.bars.symbol_list
580
+ self.symbol_list = symbol_list or self.bars.symbol_list
599
581
  self.mode = mode
600
582
 
601
583
  self.account = Account()
@@ -670,7 +652,7 @@ class StockIndexSTBOTrading(Strategy):
670
652
  def calculate_backtest_signals(self):
671
653
  for index in self.symbol_list.copy():
672
654
  dt = self.bars.get_latest_bar_datetime(index)
673
- last_price = self.bars.get_latest_bars_values(index, 'Close', N=1)
655
+ last_price = self.bars.get_latest_bars_values(index, 'close', N=1)
674
656
 
675
657
  current_price = last_price[-1]
676
658
  if self.last_price[index] is None:
@@ -693,7 +675,6 @@ class StockIndexSTBOTrading(Strategy):
693
675
  and self.num_buys[index] <= self.max_trades[index]):
694
676
  signal = SignalEvent(100, index, dt, 'LONG',
695
677
  quantity=self.qty[index], price=current_price)
696
- print(f'{dt}: LONG {self.qty[index]} units of {index} at {current_price}')
697
678
  self.events.put(signal)
698
679
  self.num_buys[index] += 1
699
680
  self.buy_prices[index].append(current_price)
@@ -705,12 +686,10 @@ class StockIndexSTBOTrading(Strategy):
705
686
  if self._calculate_pct_change(
706
687
  current_price, av_price) >= (self.expeted_return[index]):
707
688
  signal = SignalEvent(100, index, dt, 'EXIT', quantity=qty, price=current_price)
708
- print(f'{dt}: EXIT {qty} units of {index} at {current_price}')
709
689
  self.events.put(signal)
710
690
  self.num_buys[index] = 0
711
691
  self.buy_prices[index] = []
712
692
 
713
-
714
693
  def calculate_signals(self, event=None) -> Dict[str, Union[str, None]]:
715
694
  if self.mode == 'backtest' and event is not None:
716
695
  if event.type == 'MARKET':
@@ -730,13 +709,12 @@ def _run_backtest(
730
709
  engine = BacktestEngine(
731
710
  symbol_list, capital, 0.0, datetime.strptime(
732
711
  kwargs['yf_start'], "%Y-%m-%d"),
733
- kwargs.get("data_handler", YFHistoricDataHandler),
734
- kwargs.get("exc_handler", SimulatedExecutionHandler),
712
+ kwargs.get("data_handler", YFDataHandler),
713
+ kwargs.get("exc_handler", SimExecutionHandler),
735
714
  kwargs.pop('backtester_class'), **kwargs
736
715
  )
737
716
  engine.simulate_trading()
738
717
 
739
-
740
718
  def _run_arch_backtest(
741
719
  capital: float = 100000.0,
742
720
  quantity: int = 1000
@@ -748,11 +726,10 @@ def _run_arch_backtest(
748
726
  "yf_start": "2010-01-04",
749
727
  "hmm_data": hmm_data,
750
728
  'backtester_class': ArimaGarchStrategy,
751
- "data_handler": YFHistoricDataHandler,
729
+ "data_handler": YFDataHandler,
752
730
  }
753
731
  _run_backtest("ARIMA+GARCH & HMM", capital, ["^GSPC"], kwargs)
754
732
 
755
-
756
733
  def _run_kf_backtest(
757
734
  capital: float = 100000.0,
758
735
  quantity: int = 2000
@@ -767,11 +744,10 @@ def _run_kf_backtest(
767
744
  "hmm_tiker": "TLT",
768
745
  "session_duration": 6.5,
769
746
  'backtester_class': KalmanFilterStrategy,
770
- "data_handler": YFHistoricDataHandler
747
+ "data_handler": YFDataHandler
771
748
  }
772
749
  _run_backtest("Kalman Filter & HMM", capital, symbol_list, kwargs)
773
750
 
774
-
775
751
  def _run_sma_backtest(
776
752
  capital: float = 100000.0,
777
753
  quantity: int = 1
@@ -782,10 +758,10 @@ def _run_sma_backtest(
782
758
  "hmm_end": "2009-12-31",
783
759
  "yf_start": "2010-01-04",
784
760
  "hmm_data": spx_data,
785
- "mt5_start": datetime(2010, 1, 4),
786
- "mt5_end": datetime(2023, 1, 1),
761
+ "mt5_start": datetime(2010,1,1),
762
+ "mt5_end": datetime(2023,1,1),
787
763
  "backtester_class": SMAStrategy,
788
- "data_handler": MT5HistoricDataHandler,
764
+ "data_handler": MT5DataHandler,
789
765
  "exc_handler": MT5ExecutionHandler
790
766
  }
791
767
  _run_backtest("SMA & HMM", capital, ["[SP500]"], kwargs)
@@ -810,7 +786,7 @@ def _run_sistbo_backtest(
810
786
  'yf_start': start.strftime('%Y-%m-%d'),
811
787
  'time_frame': '15m',
812
788
  "backtester_class": StockIndexSTBOTrading,
813
- "data_handler": MT5HistoricDataHandler,
789
+ "data_handler": MT5DataHandler,
814
790
  "exc_handler": MT5ExecutionHandler
815
791
  }
816
792
  _run_backtest("Stock Index Short Term Buy Only ", capital, symbol_list, kwargs)
@@ -839,4 +815,4 @@ def test_strategy(strategy: Literal['sma', 'klf', 'arch', 'sistbo'] = 'sma',
839
815
  if strategy in _BACKTESTS:
840
816
  _BACKTESTS[strategy](quantity=quantity)
841
817
  else:
842
- raise ValueError(f"Unknown strategy: {strategy}")
818
+ raise ValueError(f"Unknown strategy: {strategy}")