bbstrader 0.2.99__py3-none-any.whl → 0.3.0__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,6 +1,5 @@
1
1
  """
2
2
  Simplified Investment & Trading Toolkit
3
- =======================================
4
3
 
5
4
  """
6
5
 
bbstrader/__main__.py CHANGED
@@ -1,15 +1,29 @@
1
1
  import argparse
2
2
  import sys
3
+ from enum import Enum
3
4
 
4
5
  import pyfiglet
5
6
  from colorama import Fore
6
7
 
7
8
  from bbstrader.btengine.scripts import backtest
9
+ from bbstrader.core.scripts import send_news_feed
8
10
  from bbstrader.metatrader.scripts import copy_trades
9
11
  from bbstrader.trading.scripts import execute_strategy
10
12
 
11
- DESCRIPTION = "BBSTRADER"
12
- USAGE_TEXT = """
13
+
14
+ class Module(Enum):
15
+ COPIER = "copier"
16
+ BACKTEST = "backtest"
17
+ EXECUTION = "execution"
18
+ NEWS_FEED = "news_feed"
19
+
20
+
21
+ FONT = pyfiglet.figlet_format("BBSTRADER", font="big")
22
+
23
+
24
+ def main():
25
+ DESCRIPTION = "BBSTRADER"
26
+ USAGE_TEXT = """
13
27
  Usage:
14
28
  python -m bbstrader --run <module> [options]
15
29
 
@@ -17,14 +31,10 @@ USAGE_TEXT = """
17
31
  copier: Copy trades from one MetaTrader account to another or multiple accounts
18
32
  backtest: Backtest a strategy, see bbstrader.btengine.backtest.run_backtest
19
33
  execution: Execute a strategy, see bbstrader.trading.execution.Mt5ExecutionEngine
34
+ news_feed: Send news feed from Coindesk to Telegram channel
20
35
 
21
36
  python -m bbstrader --run <module> --help for more information on the module
22
- """
23
-
24
- FONT = pyfiglet.figlet_format("BBSTRADER", font="big")
25
-
26
-
27
- def main():
37
+ """
28
38
  print(Fore.BLUE + FONT)
29
39
  print(Fore.WHITE + "")
30
40
  parser = argparse.ArgumentParser(
@@ -39,12 +49,19 @@ def main():
39
49
  if ("-h" in sys.argv or "--help" in sys.argv) and args.run is None:
40
50
  print(Fore.WHITE + USAGE_TEXT)
41
51
  sys.exit(0)
42
- if args.run == "copier":
43
- copy_trades(unknown)
44
- elif args.run == "backtest":
45
- backtest(unknown)
46
- elif args.run == "execution":
47
- execute_strategy(unknown)
52
+
53
+ match args.run:
54
+ case Module.COPIER.value:
55
+ copy_trades(unknown)
56
+ case Module.BACKTEST.value:
57
+ backtest(unknown)
58
+ case Module.EXECUTION.value:
59
+ execute_strategy(unknown)
60
+ case Module.NEWS_FEED.value:
61
+ send_news_feed(unknown)
62
+ case _:
63
+ print(Fore.RED + f"Unknown module: {args.run}")
64
+ sys.exit(1)
48
65
 
49
66
 
50
67
  if __name__ == "__main__":
@@ -5,6 +5,7 @@ from queue import Queue
5
5
  from typing import Dict, List
6
6
 
7
7
  import numpy as np
8
+ from numpy.typing import NDArray
8
9
  import pandas as pd
9
10
  import yfinance as yf
10
11
  from eodhd import APIClient
@@ -91,7 +92,7 @@ class DataHandler(metaclass=ABCMeta):
91
92
  pass
92
93
 
93
94
  @abstractmethod
94
- def get_latest_bars_values(self, symbol, val_type, N=1) -> np.ndarray:
95
+ def get_latest_bars_values(self, symbol, val_type, N=1) -> NDArray:
95
96
  """
96
97
  Returns the last N bar values from the
97
98
  latest_symbol list, or N-k if less available.
@@ -301,7 +302,7 @@ class BaseCSVDataHandler(DataHandler):
301
302
  )
302
303
  raise
303
304
 
304
- def get_latest_bars_values(self, symbol: str, val_type: str, N=1) -> np.ndarray:
305
+ def get_latest_bars_values(self, symbol: str, val_type: str, N=1) -> NDArray:
305
306
  """
306
307
  Returns the last N bar values from the
307
308
  latest_symbol list, or N-k if less available.
@@ -413,6 +414,7 @@ class MT5DataHandler(BaseCSVDataHandler):
413
414
  self.data_dir = kwargs.get("data_dir")
414
415
  self.symbol_list = symbol_list
415
416
  self.kwargs = kwargs
417
+ self.kwargs["backtest"] = True # Ensure backtest mode is set to avoid InvalidBroker errors
416
418
 
417
419
  csv_dir = self._download_and_cache_data(self.data_dir)
418
420
  super().__init__(
@@ -2,7 +2,7 @@ from datetime import datetime
2
2
  from enum import Enum
3
3
  from typing import Literal
4
4
 
5
- __all__ = ["Event", "MarketEvent", "SignalEvent", "OrderEvent", "FillEvent"]
5
+ __all__ = ["Event", "Events", "MarketEvent", "SignalEvent", "OrderEvent", "FillEvent"]
6
6
 
7
7
 
8
8
  class Event(object):
@@ -134,20 +134,21 @@ class OrderEvent(Event):
134
134
  self.price = price
135
135
  self.signal = signal
136
136
 
137
- def print_order(self):
138
- """
139
- Outputs the values within the Order.
140
- """
141
- print(
142
- "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s, Price=%s"
143
- % (
144
- self.symbol,
145
- self.order_type,
146
- self.quantity,
147
- self.direction,
148
- self.price,
149
- )
137
+ def print_order(self):
138
+ """
139
+ Outputs the values within the Order.
140
+ """
141
+ print(
142
+ "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s, Price=%s"
143
+ % (
144
+ self.symbol,
145
+ self.order_type,
146
+ self.quantity,
147
+ self.direction,
148
+ self.price,
150
149
  )
150
+ )
151
+
151
152
 
152
153
 
153
154
  class FillEvent(Event):
@@ -7,6 +7,7 @@ from bbstrader.btengine.data import DataHandler
7
7
  from bbstrader.btengine.event import Events, FillEvent, OrderEvent
8
8
  from bbstrader.config import BBSTRADER_DIR
9
9
  from bbstrader.metatrader.account import Account
10
+ from bbstrader.metatrader.utils import SymbolType
10
11
 
11
12
  __all__ = ["ExecutionHandler", "SimExecutionHandler", "MT5ExecutionHandler"]
12
13
 
@@ -71,6 +72,8 @@ class SimExecutionHandler(ExecutionHandler):
71
72
  self.events = events
72
73
  self.bardata = data
73
74
  self.logger = kwargs.get("logger") or logger
75
+ self.commissions = kwargs.get("commission")
76
+ self.exchange = kwargs.get("exchange", "ARCA")
74
77
 
75
78
  def execute_order(self, event: OrderEvent):
76
79
  """
@@ -85,11 +88,11 @@ class SimExecutionHandler(ExecutionHandler):
85
88
  fill_event = FillEvent(
86
89
  timeindex=dtime,
87
90
  symbol=event.symbol,
88
- exchange="ARCA",
91
+ exchange=self.exchange,
89
92
  quantity=event.quantity,
90
93
  direction=event.direction,
91
94
  fill_cost=None,
92
- commission=None,
95
+ commission=self.commissions,
93
96
  order=event.signal,
94
97
  )
95
98
  self.events.put(fill_event)
@@ -135,6 +138,8 @@ class MT5ExecutionHandler(ExecutionHandler):
135
138
  self.events = events
136
139
  self.bardata = data
137
140
  self.logger = kwargs.get("logger") or logger
141
+ self.commissions = kwargs.get("commission")
142
+ self.exchange = kwargs.get("exchange", "MT5")
138
143
  self.__account = Account(**kwargs)
139
144
 
140
145
  def _calculate_lot(self, symbol, quantity, price):
@@ -145,9 +150,13 @@ class MT5ExecutionHandler(ExecutionHandler):
145
150
  lot = (quantity * price) / (contract_size * price)
146
151
  if contract_size == 1:
147
152
  lot = quantity
148
- if symbol_type in ["COMD", "FUT", "CRYPTO"] and contract_size > 1:
153
+ if (
154
+ symbol_type
155
+ in (SymbolType.COMMODITIES, SymbolType.FUTURES, SymbolType.CRYPTO)
156
+ and contract_size > 1
157
+ ):
149
158
  lot = quantity / contract_size
150
- if symbol_type == "FX":
159
+ if symbol_type == SymbolType.FOREX:
151
160
  lot = quantity * price / contract_size
152
161
  return self._check_lot(symbol, lot)
153
162
 
@@ -161,17 +170,17 @@ class MT5ExecutionHandler(ExecutionHandler):
161
170
 
162
171
  def _estimate_total_fees(self, symbol, lot, qty, price):
163
172
  symbol_type = self.__account.get_symbol_type(symbol)
164
- if symbol_type in ["STK", "ETF"]:
173
+ if symbol_type in (SymbolType.STOCKS, SymbolType.ETFs):
165
174
  return self._estimate_stock_commission(symbol, qty, price)
166
- elif symbol_type == "FX":
175
+ elif symbol_type == SymbolType.FOREX:
167
176
  return self._estimate_forex_commission(lot)
168
- elif symbol_type == "COMD":
177
+ elif symbol_type == SymbolType.COMMODITIES:
169
178
  return self._estimate_commodity_commission(lot)
170
- elif symbol_type == "IDX":
179
+ elif symbol_type == SymbolType.INDICES:
171
180
  return self._estimate_index_commission(lot)
172
- elif symbol_type == "FUT":
181
+ elif symbol_type == SymbolType.FUTURES:
173
182
  return self._estimate_futures_commission()
174
- elif symbol_type == "CRYPTO":
183
+ elif symbol_type == SymbolType.CRYPTO:
175
184
  return self._estimate_crypto_commission()
176
185
  else:
177
186
  return 0.0
@@ -187,7 +196,7 @@ class MT5ExecutionHandler(ExecutionHandler):
187
196
  eu_asia_cm = 0.0015 # percent
188
197
  if (
189
198
  symbol in self.__account.get_stocks_from_country("USA")
190
- or self.__account.get_symbol_type(symbol) == "ETF"
199
+ or self.__account.get_symbol_type(symbol) == SymbolType.ETFs
191
200
  and self.__account.get_currency_rates(symbol)["mc"] == "USD"
192
201
  ):
193
202
  return max(min_com, qty * us_com)
@@ -195,7 +204,7 @@ class MT5ExecutionHandler(ExecutionHandler):
195
204
  symbol in self.__account.get_stocks_from_country("GBR")
196
205
  or symbol in self.__account.get_stocks_from_country("FRA")
197
206
  or symbol in self.__account.get_stocks_from_country("DEU")
198
- or self.__account.get_symbol_type(symbol) == "ETF"
207
+ or self.__account.get_symbol_type(symbol) == SymbolType.ETFs
199
208
  and self.__account.get_currency_rates(symbol)["mc"] in ["GBP", "EUR"]
200
209
  ):
201
210
  return max(min_com, qty * price * ger_fr_uk_cm)
@@ -241,14 +250,15 @@ class MT5ExecutionHandler(ExecutionHandler):
241
250
  lot = self._calculate_lot(symbol, quantity, price)
242
251
  fees = self._estimate_total_fees(symbol, lot, quantity, price)
243
252
  dtime = self.bardata.get_latest_bar_datetime(symbol)
253
+ commission = self.commissions or fees
244
254
  fill_event = FillEvent(
245
255
  timeindex=dtime,
246
256
  symbol=symbol,
247
- exchange="MT5",
257
+ exchange=self.exchange,
248
258
  quantity=quantity,
249
259
  direction=direction,
250
260
  fill_cost=None,
251
- commission=fees,
261
+ commission=commission,
252
262
  order=event.signal,
253
263
  )
254
264
  self.events.put(fill_event)
@@ -17,10 +17,12 @@ from bbstrader.metatrader.account import (
17
17
  AdmiralMarktsGroup,
18
18
  PepperstoneGroupLimited,
19
19
  )
20
+ from bbstrader.metatrader.utils import SymbolType
20
21
  from bbstrader.metatrader.rates import Rates
21
- from bbstrader.metatrader.trade import TradeSignal
22
+ from bbstrader.metatrader.trade import TradeSignal, TradingMode
22
23
  from bbstrader.models.optimization import optimized_weights
23
24
 
25
+
24
26
  __all__ = ["Strategy", "MT5Strategy"]
25
27
 
26
28
  logger.add(
@@ -76,7 +78,7 @@ class MT5Strategy(Strategy):
76
78
  events: Queue = None,
77
79
  symbol_list: List[str] = None,
78
80
  bars: DataHandler = None,
79
- mode: str = None,
81
+ mode: TradingMode = None,
80
82
  **kwargs,
81
83
  ):
82
84
  """
@@ -86,7 +88,7 @@ class MT5Strategy(Strategy):
86
88
  events : The event queue.
87
89
  symbol_list : The list of symbols for the strategy.
88
90
  bars : The data handler object.
89
- mode : The mode of operation for the strategy (backtest or live).
91
+ mode (TradingMode): The mode of operation for the strategy.
90
92
  **kwargs : Additional keyword arguments for other classes (e.g, Portfolio, ExecutionHandler).
91
93
  - max_trades : The maximum number of trades allowed per symbol.
92
94
  - time_frame : The time frame for the strategy.
@@ -101,33 +103,49 @@ class MT5Strategy(Strategy):
101
103
  self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
102
104
  self.tf = kwargs.get("time_frame", "D1")
103
105
  self.logger = kwargs.get("logger") or logger
104
- if self.mode == "backtest":
106
+ if self.mode == TradingMode.BACKTEST:
105
107
  self._initialize_portfolio()
106
108
  self.kwargs = kwargs
107
109
  self.periodes = 0
110
+
111
+ @property
112
+ def account(self):
113
+ return Account(**self.kwargs)
108
114
 
109
115
  @property
110
116
  def cash(self) -> float:
117
+ if self.mode == TradingMode.LIVE:
118
+ return self.account.balance
111
119
  return self._porfolio_value
112
120
 
113
121
  @cash.setter
114
122
  def cash(self, value):
123
+ if self.mode == TradingMode.LIVE:
124
+ raise ValueError("Cannot set the account cash in live mode")
115
125
  self._porfolio_value = value
116
126
 
117
127
  @property
118
- def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
128
+ def orders(self):
129
+ if self.mode == TradingMode.LIVE:
130
+ return self.account.get_orders()
119
131
  return self._orders
120
132
 
121
133
  @property
122
134
  def trades(self) -> Dict[str, Dict[str, int]]:
135
+ if self.mode == TradingMode.LIVE:
136
+ raise ValueError("Cannot call this methode in live mode")
123
137
  return self._trades
124
138
 
125
139
  @property
126
- def positions(self) -> Dict[str, Dict[str, int | float]]:
140
+ def positions(self):
141
+ if self.mode == TradingMode.LIVE:
142
+ return self.account.get_positions()
127
143
  return self._positions
128
144
 
129
145
  @property
130
146
  def holdings(self) -> Dict[str, float]:
147
+ if self.mode == TradingMode.LIVE:
148
+ raise ValueError("Cannot call this methode in live mode")
131
149
  return self._holdings
132
150
 
133
151
  def _check_risk_budget(self, **kwargs):
@@ -606,7 +624,7 @@ class MT5Strategy(Strategy):
606
624
  value_type: str = "returns",
607
625
  array: bool = True,
608
626
  bars: DataHandler = None,
609
- mode: Literal["backtest", "live"] = "backtest",
627
+ mode: TradingMode = TradingMode.BACKTEST,
610
628
  tf: str = "D1",
611
629
  error: Literal["ignore", "raise"] = None,
612
630
  ) -> Dict[str, np.ndarray | pd.Series] | None:
@@ -634,7 +652,7 @@ class MT5Strategy(Strategy):
634
652
  if mode not in ["backtest", "live"]:
635
653
  raise ValueError("Mode must be either backtest or live.")
636
654
  asset_values = {}
637
- if mode == "backtest":
655
+ if mode == TradingMode.BACKTEST:
638
656
  if bars is None:
639
657
  raise ValueError("DataHandler is required for backtest mode.")
640
658
  for asset in symbol_list:
@@ -644,11 +662,11 @@ class MT5Strategy(Strategy):
644
662
  else:
645
663
  values = bars.get_latest_bars(asset, N=window)
646
664
  asset_values[asset] = getattr(values, value_type)
647
- elif mode == "live":
665
+ elif mode == TradingMode.LIVE:
648
666
  for asset in symbol_list:
649
667
  rates = Rates(asset, timeframe=tf, count=window + 1, **self.kwargs)
650
668
  if array:
651
- values = getattr(rates, value_type).values
669
+ values = getattr(rates, value_type).to_numpy()
652
670
  asset_values[asset] = values[~np.isnan(values)]
653
671
  else:
654
672
  values = getattr(rates, value_type)
@@ -704,7 +722,7 @@ class MT5Strategy(Strategy):
704
722
  Returns:
705
723
  bool : True if there are open positions, False otherwise
706
724
  """
707
- account = account or Account(**self.kwargs)
725
+ account = account or self.account
708
726
  positions = account.get_positions(symbol=symbol)
709
727
  if positions is not None:
710
728
  open_positions = [
@@ -730,7 +748,7 @@ class MT5Strategy(Strategy):
730
748
  Returns:
731
749
  prices : numpy array of buy or sell prices for open positions if any or an empty array.
732
750
  """
733
- account = account or Account(**self.kwargs)
751
+ account = account or self.account
734
752
  positions = account.get_positions(symbol=symbol)
735
753
  if positions is not None:
736
754
  prices = np.array(
@@ -778,21 +796,21 @@ class MT5Strategy(Strategy):
778
796
  return dt_to
779
797
 
780
798
  @staticmethod
781
- def get_mt5_equivalent(symbols, type="STK", **kwargs) -> List[str]:
799
+ def get_mt5_equivalent(symbols, symbol_type: str | SymbolType = SymbolType.STOCKS, **kwargs) -> List[str]:
782
800
  """
783
801
  Get the MetaTrader 5 equivalent symbols for the symbols in the list.
784
802
  This method is used to get the symbols that are available on the MetaTrader 5 platform.
785
803
 
786
804
  Args:
787
805
  symbols : The list of symbols to get the MetaTrader 5 equivalent symbols for.
788
- type : The type of symbols to get (e.g., STK, CFD, etc.).
806
+ symbol_type : The type of symbols to get (See `bbstrader.metatrader.utils.SymbolType`).
789
807
  **kwargs : Additional keyword arguments for the `bbstrader.metatrader.Account` object.
790
808
 
791
809
  Returns:
792
810
  mt5_equivalent : The MetaTrader 5 equivalent symbols for the symbols in the list.
793
811
  """
794
812
  account = Account(**kwargs)
795
- mt5_symbols = account.get_symbols(symbol_type=type)
813
+ mt5_symbols = account.get_symbols(symbol_type=symbol_type)
796
814
  mt5_equivalent = []
797
815
  if account.broker == AdmiralMarktsGroup():
798
816
  for s in mt5_symbols:
bbstrader/core/data.py CHANGED
@@ -2,7 +2,7 @@ import json
2
2
  import re
3
3
  import ssl
4
4
  from datetime import datetime
5
- from typing import List
5
+ from typing import List, Literal
6
6
  from urllib.request import urlopen
7
7
 
8
8
  import certifi
@@ -18,7 +18,7 @@ __all__ = ["FmpData", "FmpNews", "FinancialNews"]
18
18
 
19
19
 
20
20
  def _get_search_query(query: str) -> str:
21
- if " " in query:
21
+ if " " in query or query == "":
22
22
  return query
23
23
  try:
24
24
  name = yf.Ticker(query).info["shortName"]
@@ -198,7 +198,7 @@ class FmpNews(object):
198
198
  class FinancialNews(object):
199
199
  """
200
200
  The FinancialNews class provides methods to fetch financial news, articles, and discussions
201
- from various sources such as Yahoo Finance, Google Finance, Reddit, and Twitter.
201
+ from various sources such as Yahoo Finance, Google Finance, Reddit, Coindesk and Twitter.
202
202
  It also supports retrieving news using Financial Modeling Prep (FMP).
203
203
 
204
204
  """
@@ -422,6 +422,102 @@ class FinancialNews(object):
422
422
  def get_fmp_news(self, api=None) -> FmpNews:
423
423
  return FmpNews(api=api)
424
424
 
425
+ def get_coindesk_news(
426
+ self,
427
+ query="",
428
+ lang: Literal["EN", "ES", "TR", "FR", "JP", "PT"] = "EN",
429
+ limit=50,
430
+ list_of_str=False,
431
+ ) -> List[str] | List[dict]:
432
+ """
433
+ Fetches and filters recent news articles from CoinDesk's News API.
434
+
435
+ Args:
436
+ query : str, optional
437
+ A search term to filter articles by title, body, or keywords.
438
+ If empty, all articles are returned without filtering (default is "").
439
+
440
+ lang : Literal["EN", "ES", "TR", "FR", "JP", "PT"], optional
441
+ Language in which to fetch news articles. Supported languages:
442
+ English (EN), Spanish (ES), Turkish (TR), French (FR), Japanese (JP), and Portuguese (PT).
443
+ Default is "EN".
444
+
445
+ limit : int, optional
446
+ Maximum number of articles to retrieve. Default is 50.
447
+
448
+ list_of_str : bool, optional
449
+ If True, returns a list of strings (concatenated article content).
450
+ If False, returns a list of filtered article dictionaries.
451
+ Default is False.
452
+
453
+ Returns:
454
+ List[str] | List[dict]
455
+ - If `query` is empty: returns a list of filtered article dictionaries.
456
+ - If `query` is provided:
457
+ - Returns a list of strings if `list_of_str=True`.
458
+ - Returns a list of filtered article dictionaries otherwise.
459
+
460
+ Each article dictionary contains the following fields:
461
+ - 'published_on': datetime of publication
462
+ - 'title': article headline
463
+ - 'subtitle': secondary headline
464
+ - 'url': direct link to the article
465
+ - 'body': article content
466
+ - 'keywords': associated tags
467
+ - 'sentiment': sentiment label
468
+ - 'status': publication status
469
+
470
+ Notes:
471
+ - Articles marked as sponsored are automatically excluded.
472
+ """
473
+ maximum = 100
474
+ if limit > maximum:
475
+ raise ValueError(f"Number of total news articles allowed is {maximum}")
476
+
477
+ response = requests.get(
478
+ "https://data-api.coindesk.com/news/v1/article/list",
479
+ params={"lang": lang, "limit": limit},
480
+ headers={"Content-type": "application/json; charset=UTF-8"},
481
+ )
482
+ json_response = response.json()
483
+ articles = json_response["Data"]
484
+ if len(articles) == 0:
485
+ return []
486
+ to_keep = [
487
+ "PUBLISHED_ON",
488
+ "TITLE",
489
+ "SUBTITLE",
490
+ "URL",
491
+ "BODY",
492
+ "KEYWORDS",
493
+ "SENTIMENT",
494
+ "STATUS",
495
+ ]
496
+ filtered_articles = []
497
+ for article in articles:
498
+ filtered_articles.append(
499
+ {
500
+ k.lower(): article[k]
501
+ if k in article and k != "PUBLISHED_ON"
502
+ else datetime.fromtimestamp(article[k])
503
+ for k in to_keep
504
+ if article[k] is not None and "sponsored" not in str(article[k])
505
+ }
506
+ )
507
+ if query == "" or len(filtered_articles) == 0:
508
+ return filtered_articles
509
+ to_return = []
510
+ query = _get_search_query(query)
511
+ for article in filtered_articles:
512
+ if not all(k in article for k in ("title", "body", "keywords")):
513
+ continue
514
+ text = article["title"] + " " + article["body"] + " " + article["keywords"]
515
+ if list_of_str and _find_news(query, text=text):
516
+ to_return.append(text)
517
+ if not list_of_str and _find_news(query, text=text):
518
+ to_return.append(article)
519
+ return to_return
520
+
425
521
 
426
522
  class FmpData(Toolkit):
427
523
  """