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