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

@@ -11,10 +11,12 @@ from loguru import logger
11
11
 
12
12
  from bbstrader.btengine.data import DataHandler
13
13
  from bbstrader.btengine.event import Events, FillEvent, SignalEvent
14
+ from bbstrader.metatrader.trade import generate_signal, TradeAction
14
15
  from bbstrader.config import BBSTRADER_DIR
15
16
  from bbstrader.metatrader import (
16
17
  Account,
17
18
  AdmiralMarktsGroup,
19
+ MetaQuotes,
18
20
  PepperstoneGroupLimited,
19
21
  TradeOrder,
20
22
  Rates,
@@ -78,7 +80,16 @@ class MT5Strategy(Strategy):
78
80
  in order to avoid naming collusion.
79
81
  """
80
82
  tf: str
83
+ id: int
84
+ ID: int
85
+
81
86
  max_trades: Dict[str, int]
87
+ risk_budget: Dict[str, float] | str | None
88
+
89
+ _orders: Dict[str, Dict[str, List[SignalEvent]]]
90
+ _positions: Dict[str, Dict[str, int | float]]
91
+ _trades: Dict[str, Dict[str, int]]
92
+
82
93
  def __init__(
83
94
  self,
84
95
  events: Queue = None,
@@ -104,13 +115,19 @@ class MT5Strategy(Strategy):
104
115
  self.data = bars
105
116
  self.symbols = symbol_list
106
117
  self.mode = mode
107
- self._porfolio_value = None
118
+ if self.mode not in [TradingMode.BACKTEST, TradingMode.LIVE]:
119
+ raise ValueError(f"Mode must be an instance of {type(TradingMode)} not {type(self.mode)}")
120
+
108
121
  self.risk_budget = self._check_risk_budget(**kwargs)
122
+
109
123
  self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
110
124
  self.tf = kwargs.get("time_frame", "D1")
111
125
  self.logger = kwargs.get("logger") or logger
126
+
112
127
  if self.mode == TradingMode.BACKTEST:
128
+ self._porfolio_value = None
113
129
  self._initialize_portfolio()
130
+
114
131
  self.kwargs = kwargs
115
132
  self.periodes = 0
116
133
 
@@ -170,19 +187,17 @@ class MT5Strategy(Strategy):
170
187
  return weights
171
188
 
172
189
  def _initialize_portfolio(self):
173
- positions = ["LONG", "SHORT"]
174
- orders = ["BLMT", "BSTP", "BSTPLMT", "SLMT", "SSTP", "SSTPLMT"]
175
- self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
176
- self._positions: Dict[str, Dict[str, int | float]] = {}
177
- self._trades: Dict[str, Dict[str, int]] = {}
190
+ self._orders = {}
191
+ self._positions = {}
192
+ self._trades = {}
178
193
  for symbol in self.symbols:
179
194
  self._positions[symbol] = {}
180
195
  self._orders[symbol] = {}
181
196
  self._trades[symbol] = {}
182
- for position in positions:
197
+ for position in ["LONG", "SHORT"]:
183
198
  self._trades[symbol][position] = 0
184
199
  self._positions[symbol][position] = 0.0
185
- for order in orders:
200
+ for order in ["BLMT", "BSTP", "BSTPLMT", "SLMT", "SSTP", "SSTPLMT"]:
186
201
  self._orders[symbol][order] = []
187
202
  self._holdings = {s: 0.0 for s in self.symbols}
188
203
 
@@ -236,6 +251,54 @@ class MT5Strategy(Strategy):
236
251
  """
237
252
  pass
238
253
 
254
+ def signal(self, signal: int, symbol: str) -> TradeSignal:
255
+ """
256
+ Generate a ``TradeSignal`` object based on the signal value.
257
+ Args:
258
+ signal : An integer value representing the signal type:
259
+ 0: BUY
260
+ 1: SELL
261
+ 2: EXIT_LONG
262
+ 3: EXIT_SHORT
263
+ 4: EXIT_ALL_POSITIONS
264
+ 5: EXIT_ALL_ORDERS
265
+ 6: EXIT_STOP
266
+ 7: EXIT_LIMIT
267
+
268
+ symbol : The symbol for the trade.
269
+
270
+ Returns:
271
+ TradeSignal : A ``TradeSignal`` object representing the trade signal.
272
+
273
+ Note:
274
+ This generate only common signals. For more complex signals, use `generate_signal` directly.
275
+
276
+ Raises:
277
+ ValueError : If the signal value is not between 0 and 7.
278
+ """
279
+ signal_id = getattr(self, "id", None) or getattr(self, "ID")
280
+
281
+ match signal:
282
+ case 0:
283
+ return generate_signal(signal_id, symbol, TradeAction.BUY)
284
+ case 1:
285
+ return generate_signal(signal_id, symbol, TradeAction.SELL)
286
+ case 2:
287
+ return generate_signal(signal_id, symbol, TradeAction.EXIT_LONG)
288
+ case 3:
289
+ return generate_signal(signal_id, symbol, TradeAction.EXIT_SHORT)
290
+ case 4:
291
+ return generate_signal(signal_id, symbol, TradeAction.EXIT_ALL_POSITIONS)
292
+ case 5:
293
+ return generate_signal(signal_id, symbol, TradeAction.EXIT_ALL_ORDERS)
294
+ case 6:
295
+ return generate_signal(signal_id, symbol, TradeAction.EXIT_STOP)
296
+ case 7:
297
+ return generate_signal(signal_id, symbol, TradeAction.EXIT_LIMIT)
298
+ case _:
299
+ raise ValueError(f"Invalid signal value: {signal}. Must be an integer between 0 and 7.")
300
+
301
+
239
302
  def perform_period_end_checks(self, *args, **kwargs):
240
303
  """
241
304
  Some strategies may require additional checks at the end of the period,
@@ -551,75 +614,75 @@ class MT5Strategy(Strategy):
551
614
  ]
552
615
  logmsg(order, log_label, symbol, dtime)
553
616
 
554
- for symbol in self.symbols:
555
- dtime = self.data.get_latest_bar_datetime(symbol)
556
- latest_close = self.data.get_latest_bar_value(symbol, "close")
557
-
558
- process_orders(
559
- "BLMT",
560
- lambda o: latest_close <= o.price,
561
- lambda o: self.buy_mkt(
562
- o.strategy_id, symbol, o.price, o.quantity, dtime
563
- ),
564
- "BUY LIMIT",
565
- symbol,
566
- dtime,
567
- )
568
-
569
- process_orders(
570
- "SLMT",
571
- lambda o: latest_close >= o.price,
572
- lambda o: self.sell_mkt(
573
- o.strategy_id, symbol, o.price, o.quantity, dtime
574
- ),
575
- "SELL LIMIT",
576
- symbol,
577
- dtime,
578
- )
579
-
580
- process_orders(
581
- "BSTP",
582
- lambda o: latest_close >= o.price,
583
- lambda o: self.buy_mkt(
584
- o.strategy_id, symbol, o.price, o.quantity, dtime
585
- ),
586
- "BUY STOP",
587
- symbol,
588
- dtime,
589
- )
590
-
591
- process_orders(
592
- "SSTP",
593
- lambda o: latest_close <= o.price,
594
- lambda o: self.sell_mkt(
595
- o.strategy_id, symbol, o.price, o.quantity, dtime
596
- ),
597
- "SELL STOP",
598
- symbol,
599
- dtime,
600
- )
601
-
602
- process_orders(
603
- "BSTPLMT",
604
- lambda o: latest_close >= o.price,
605
- lambda o: self.buy_limit(
606
- o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
607
- ),
608
- "BUY STOP LIMIT",
609
- symbol,
610
- dtime,
611
- )
612
-
613
- process_orders(
614
- "SSTPLMT",
615
- lambda o: latest_close <= o.price,
616
- lambda o: self.sell_limit(
617
- o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
618
- ),
619
- "SELL STOP LIMIT",
620
- symbol,
621
- dtime,
622
- )
617
+ for symbol in self.symbols:
618
+ dtime = self.data.get_latest_bar_datetime(symbol)
619
+ latest_close = self.data.get_latest_bar_value(symbol, "close")
620
+
621
+ process_orders(
622
+ "BLMT",
623
+ lambda o: latest_close <= o.price,
624
+ lambda o: self.buy_mkt(
625
+ o.strategy_id, symbol, o.price, o.quantity, dtime
626
+ ),
627
+ "BUY LIMIT",
628
+ symbol,
629
+ dtime,
630
+ )
631
+
632
+ process_orders(
633
+ "SLMT",
634
+ lambda o: latest_close >= o.price,
635
+ lambda o: self.sell_mkt(
636
+ o.strategy_id, symbol, o.price, o.quantity, dtime
637
+ ),
638
+ "SELL LIMIT",
639
+ symbol,
640
+ dtime,
641
+ )
642
+
643
+ process_orders(
644
+ "BSTP",
645
+ lambda o: latest_close >= o.price,
646
+ lambda o: self.buy_mkt(
647
+ o.strategy_id, symbol, o.price, o.quantity, dtime
648
+ ),
649
+ "BUY STOP",
650
+ symbol,
651
+ dtime,
652
+ )
653
+
654
+ process_orders(
655
+ "SSTP",
656
+ lambda o: latest_close <= o.price,
657
+ lambda o: self.sell_mkt(
658
+ o.strategy_id, symbol, o.price, o.quantity, dtime
659
+ ),
660
+ "SELL STOP",
661
+ symbol,
662
+ dtime,
663
+ )
664
+
665
+ process_orders(
666
+ "BSTPLMT",
667
+ lambda o: latest_close >= o.price,
668
+ lambda o: self.buy_limit(
669
+ o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
670
+ ),
671
+ "BUY STOP LIMIT",
672
+ symbol,
673
+ dtime,
674
+ )
675
+
676
+ process_orders(
677
+ "SSTPLMT",
678
+ lambda o: latest_close <= o.price,
679
+ lambda o: self.sell_limit(
680
+ o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
681
+ ),
682
+ "SELL STOP LIMIT",
683
+ symbol,
684
+ dtime,
685
+ )
623
686
 
624
687
  @staticmethod
625
688
  def calculate_pct_change(current_price, lh_price) -> float:
@@ -797,17 +860,16 @@ class MT5Strategy(Strategy):
797
860
  return False
798
861
  tick_info = self.account.get_tick_info(asset)
799
862
  bid, ask = tick_info.bid, tick_info.ask
863
+ price = None
800
864
  if len(prices) == 1:
801
865
  price = prices[0]
802
866
  elif len(prices) in range(2, self.max_trades[asset] + 1):
803
867
  price = np.mean(prices)
804
- if (
805
- position == 0
806
- and self.calculate_pct_change(ask, price) >= th
807
- or position == 1
808
- and abs(self.calculate_pct_change(bid, price)) >= th
809
- ):
810
- return True
868
+ if price is not None:
869
+ if position == 0:
870
+ return self.calculate_pct_change(ask, price) >= th
871
+ elif position == 1:
872
+ return self.calculate_pct_change(bid, price) <= -th
811
873
  return False
812
874
 
813
875
  @staticmethod
@@ -832,9 +894,7 @@ class MT5Strategy(Strategy):
832
894
  dt_to : The converted datetime.
833
895
  """
834
896
  from_tz = pytz.timezone(from_tz)
835
- if isinstance(dt, datetime):
836
- dt = pd.to_datetime(dt, unit="s")
837
- elif isinstance(dt, int):
897
+ if isinstance(dt, (datetime, int)):
838
898
  dt = pd.to_datetime(dt, unit="s")
839
899
  if dt.tzinfo is None:
840
900
  dt = dt.tz_localize(from_tz)
@@ -860,20 +920,35 @@ class MT5Strategy(Strategy):
860
920
  Returns:
861
921
  mt5_equivalent : The MetaTrader 5 equivalent symbols for the symbols in the list.
862
922
  """
923
+
863
924
  account = Account(**kwargs)
864
925
  mt5_symbols = account.get_symbols(symbol_type=symbol_type)
865
926
  mt5_equivalent = []
866
- if account.broker == AdmiralMarktsGroup():
927
+
928
+ def _get_admiral_symbols():
867
929
  for s in mt5_symbols:
868
930
  _s = s[1:] if s[0] in string.punctuation else s
869
931
  for symbol in symbols:
870
932
  if _s.split(".")[0] == symbol or _s.split("_")[0] == symbol:
871
933
  mt5_equivalent.append(s)
872
- elif account.broker == PepperstoneGroupLimited():
873
- for s in mt5_symbols:
934
+
935
+ def _get_pepperstone_symbols():
936
+ for s in mt5_symbols:
874
937
  for symbol in symbols:
875
938
  if s.split(".")[0] == symbol:
876
939
  mt5_equivalent.append(s)
940
+
941
+ if account.broker == MetaQuotes():
942
+ if "Admiral" in account.server:
943
+ _get_admiral_symbols()
944
+ elif "Pepperstone" in account.server:
945
+ _get_pepperstone_symbols()
946
+
947
+ elif account.broker == AdmiralMarktsGroup():
948
+ _get_admiral_symbols()
949
+ elif account.broker == PepperstoneGroupLimited():
950
+ _get_pepperstone_symbols()
951
+
877
952
  return mt5_equivalent
878
953
 
879
954
 
bbstrader/core/data.py CHANGED
@@ -190,7 +190,9 @@ class FmpNews(object):
190
190
  0
191
191
  ] # if symbol is a yahoo finance ticker
192
192
  source_methods = {
193
- "articles": lambda: self.get_latest_articles(articles=articles, save=True),
193
+ "articles": lambda: self.get_latest_articles(
194
+ articles=articles, save=True, **kwargs
195
+ ),
194
196
  "releases": lambda: self.get_releases(symbol=symbol, **kwargs),
195
197
  "stock": lambda: self.get_stock_news(symbol=symbol, **kwargs),
196
198
  "crypto": lambda: self.get_crypto_news(symbol=symbol, **kwargs),
bbstrader/core/scripts.py CHANGED
@@ -4,6 +4,7 @@ import sys
4
4
  import textwrap
5
5
  import time
6
6
  from datetime import datetime, timedelta
7
+ from typing import List, Literal
7
8
 
8
9
  import nltk
9
10
  from loguru import logger
@@ -25,7 +26,7 @@ def summarize_text(text, sentences_count=5):
25
26
  return " ".join(str(sentence) for sentence in summary)
26
27
 
27
28
 
28
- def format_article_for_telegram(article: dict) -> str:
29
+ def format_coindesk_article(article: dict) -> str:
29
30
  if not all(
30
31
  k in article
31
32
  for k in (
@@ -53,16 +54,40 @@ def format_article_for_telegram(article: dict) -> str:
53
54
  return text
54
55
 
55
56
 
56
- async def send_articles(articles: dict, token: str, id: str, interval=15):
57
+ def format_fmp_article(article: dict) -> str:
58
+ if not all(k in article for k in ("title", "date", "content", "tickers")):
59
+ return ""
60
+ summary = summarize_text(article["content"])
61
+ text = (
62
+ f"📰 {article['title']}\n"
63
+ f"Published Date: {article['date']}\n"
64
+ f"Keywords: {article['tickers']}\n\n"
65
+ f"🔍 Summary\n"
66
+ f"{textwrap.fill(summary, width=80)}"
67
+ )
68
+ return text
69
+
70
+
71
+ async def send_articles(
72
+ articles: List[dict],
73
+ token: str,
74
+ id: str,
75
+ source: Literal["coindesk", "fmp"],
76
+ interval=15,
77
+ ):
57
78
  for article in articles:
58
- if article["published_on"] >= datetime.now() - timedelta(minutes=interval):
59
- article["published_on"] = article.get("published_on").strftime(
60
- "%Y-%m-%d %H:%M:%S"
61
- )
62
- message = format_article_for_telegram(article)
63
- if message == "":
64
- return
65
- await send_telegram_message(token, id, text=message)
79
+ message = ""
80
+ if source == "coindesk":
81
+ if article["published_on"] >= datetime.now() - timedelta(minutes=interval):
82
+ article["published_on"] = article.get("published_on").strftime(
83
+ "%Y-%m-%d %H:%M:%S"
84
+ )
85
+ message = format_coindesk_article(article)
86
+ else:
87
+ message = format_fmp_article(article)
88
+ if message == "":
89
+ return
90
+ await send_telegram_message(token, id, text=message)
66
91
 
67
92
 
68
93
  def send_news_feed(unknown):
@@ -78,6 +103,7 @@ def send_news_feed(unknown):
78
103
  -q, --query: The news to look for (default: "")
79
104
  -t, --token: Telegram bot token
80
105
  -I, --id: Telegram Chat id
106
+ --fmp: Financial Modeling Prop Api Key
81
107
  -i, --interval: Interval in minutes to fetch news (default: 15)
82
108
 
83
109
  Note:
@@ -101,6 +127,9 @@ def send_news_feed(unknown):
101
127
  help="Telegram bot token",
102
128
  )
103
129
  parser.add_argument("-I", "--id", type=str, required=True, help="Telegram Chat id")
130
+ parser.add_argument(
131
+ "--fmp", type=str, default="", help="Financial Modeling Prop Api Key"
132
+ )
104
133
  parser.add_argument(
105
134
  "-i",
106
135
  "--interval",
@@ -112,19 +141,33 @@ def send_news_feed(unknown):
112
141
 
113
142
  nltk.download("punkt", quiet=True)
114
143
  news = FinancialNews()
115
- logger.info(
116
- f"Starting the News Feed on {args.interval} minutes"
117
- )
144
+ logger.info(f"Starting the News Feed on {args.interval} minutes")
118
145
  while True:
119
146
  try:
120
- articles = news.get_coindesk_news(query=args.query)
121
- if len(articles) == 0:
122
- time.sleep(args.interval * 60)
123
- continue
124
- asyncio.run(send_articles(articles, args.token, args.id))
147
+ fmp_articles = []
148
+ coindesk_articles = news.get_coindesk_news(query=args.query)
149
+ if args.fmp:
150
+ start = datetime.now() - timedelta(minutes=args.interval)
151
+ start = start.strftime("%Y-%m-%d %H:%M:%S")
152
+ end = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
153
+ fmp_articles = news.get_fmp_news(api=args.fmp).get_latest_articles(
154
+ save=True, start=start, end=end
155
+ )
156
+ if len(coindesk_articles) != 0:
157
+ asyncio.run(
158
+ send_articles(
159
+ coindesk_articles,
160
+ args.token,
161
+ args.id,
162
+ "coindesk",
163
+ interval=args.interval,
164
+ )
165
+ )
166
+ if len(fmp_articles) != 0:
167
+ asyncio.run(send_articles(fmp_articles, args.token, args.id, "fmp"))
125
168
  time.sleep(args.interval * 60)
126
169
  except KeyboardInterrupt:
127
170
  logger.info("Stopping the News Feed ...")
128
- exit(0)
171
+ sys.exit(0)
129
172
  except Exception as e:
130
173
  logger.error(e)
bbstrader/core/utils.py CHANGED
@@ -66,9 +66,11 @@ def dict_from_ini(file_path, sections: str | List[str] = None) -> Dict[str, Any]
66
66
  Returns:
67
67
  A dictionary containing the INI file contents with proper data types.
68
68
  """
69
- config = configparser.ConfigParser(interpolation=None)
70
- config.read(file_path)
71
-
69
+ try:
70
+ config = configparser.ConfigParser(interpolation=None)
71
+ config.read(file_path)
72
+ except Exception:
73
+ raise
72
74
  ini_dict = {}
73
75
  for section in config.sections():
74
76
  ini_dict[section] = {