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

bbstrader/__main__.py CHANGED
@@ -12,7 +12,7 @@ from bbstrader.metatrader.scripts import copy_trades
12
12
  from bbstrader.trading.scripts import execute_strategy
13
13
 
14
14
 
15
- class Module(Enum):
15
+ class _Module(Enum):
16
16
  COPIER = "copier"
17
17
  BACKTEST = "backtest"
18
18
  EXECUTION = "execution"
@@ -52,13 +52,13 @@ def main():
52
52
  sys.exit(0)
53
53
  try:
54
54
  match args.run:
55
- case Module.COPIER.value:
55
+ case _Module.COPIER.value:
56
56
  copy_trades(unknown)
57
- case Module.BACKTEST.value:
57
+ case _Module.BACKTEST.value:
58
58
  backtest(unknown)
59
- case Module.EXECUTION.value:
59
+ case _Module.EXECUTION.value:
60
60
  execute_strategy(unknown)
61
- case Module.NEWS_FEED.value:
61
+ case _Module.NEWS_FEED.value:
62
62
  send_news_feed(unknown)
63
63
  case _:
64
64
  print(Fore.RED + f"Unknown module: {args.run}")
@@ -361,7 +361,7 @@ class CSVDataHandler(BaseCSVDataHandler):
361
361
  csv_dir (str): Absolute directory path to the CSV files.
362
362
 
363
363
  NOTE:
364
- All csv fille can be strored in 'Home/.bbstrader/csv_data'
364
+ All csv fille can be stored in 'Home/.bbstrader/data/csv_data'
365
365
 
366
366
  """
367
367
  csv_dir = kwargs.get("csv_dir")
@@ -580,7 +580,7 @@ class EODHDataHandler(BaseCSVDataHandler):
580
580
  to_unix_time=unix_end,
581
581
  )
582
582
 
583
- def _forma_data(self, data: List[Dict] | pd.DataFrame) -> pd.DataFrame:
583
+ def _format_data(self, data: List[Dict] | pd.DataFrame) -> pd.DataFrame:
584
584
  if isinstance(data, pd.DataFrame):
585
585
  if data.empty or len(data) == 0:
586
586
  raise ValueError("No data found.")
@@ -608,7 +608,7 @@ class EODHDataHandler(BaseCSVDataHandler):
608
608
  filepath = os.path.join(cache_dir, f"{symbol}.csv")
609
609
  try:
610
610
  data = self._get_data(symbol, self.period)
611
- data = self._forma_data(data)
611
+ data = self._format_data(data)
612
612
  data.to_csv(filepath)
613
613
  except Exception as e:
614
614
  raise ValueError(f"Error downloading {symbol}: {e}")
@@ -797,17 +797,19 @@ class MT5Strategy(Strategy):
797
797
  return False
798
798
  tick_info = self.account.get_tick_info(asset)
799
799
  bid, ask = tick_info.bid, tick_info.ask
800
+ price = None
800
801
  if len(prices) == 1:
801
802
  price = prices[0]
802
803
  elif len(prices) in range(2, self.max_trades[asset] + 1):
803
804
  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
805
+ if price is not None:
806
+ if (
807
+ position == 0
808
+ and self.calculate_pct_change(ask, price) >= th
809
+ or position == 1
810
+ and abs(self.calculate_pct_change(bid, price)) >= th
811
+ ):
812
+ return True
811
813
  return False
812
814
 
813
815
  @staticmethod
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)
@@ -5,8 +5,6 @@ from datetime import datetime, timedelta
5
5
  from typing import Any, Dict, List, Literal, Optional, Tuple, Union
6
6
 
7
7
  import pandas as pd
8
- from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
9
-
10
8
  from bbstrader.metatrader.utils import (
11
9
  AccountInfo,
12
10
  BookInfo,
@@ -23,6 +21,7 @@ from bbstrader.metatrader.utils import (
23
21
  TradeRequest,
24
22
  raise_mt5_error,
25
23
  )
24
+ from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
26
25
 
27
26
  try:
28
27
  import MetaTrader5 as mt5
@@ -33,6 +32,7 @@ except ImportError:
33
32
  __all__ = [
34
33
  "Account",
35
34
  "Broker",
35
+ "MetaQuotes",
36
36
  "AdmiralMarktsGroup",
37
37
  "JustGlobalMarkets",
38
38
  "PepperstoneGroupLimited",
@@ -41,6 +41,7 @@ __all__ = [
41
41
  ]
42
42
 
43
43
  __BROKERS__ = {
44
+ "MQL": "MetaQuotes Ltd.",
44
45
  "AMG": "Admirals Group AS",
45
46
  "JGM": "Just Global Markets Ltd.",
46
47
  "FTMO": "FTMO S.R.O.",
@@ -67,14 +68,13 @@ _ADMIRAL_MARKETS_PRODUCTS_ = [
67
68
  ]
68
69
  _JUST_MARKETS_PRODUCTS_ = ["Stocks", "Crypto", "indices", "Commodities", "Forex"]
69
70
 
70
- SUPPORTED_BROKERS = [__BROKERS__[b] for b in {"AMG", "JGM", "FTMO"}]
71
+ SUPPORTED_BROKERS = [__BROKERS__[b] for b in {"MQL", "AMG", "JGM", "FTMO"}]
71
72
  INIT_MSG = (
72
- f"\n* Ensure you have a good and stable internet connexion\n"
73
- f"* Ensure you have an activete MT5 terminal install on your machine\n"
74
- f"* Ensure you have an active MT5 Account with {' or '.join(SUPPORTED_BROKERS)}\n"
75
- f"* If you want to trade {', '.join(_ADMIRAL_MARKETS_PRODUCTS_)}, See [{_ADMIRAL_MARKETS_URL_}]\n"
76
- f"* If you want to trade {', '.join(_JUST_MARKETS_PRODUCTS_)}, See [{_JUST_MARKETS_URL_}]\n"
77
- f"* If you are looking for a prop firm, See [{_FTMO_URL_}]\n"
73
+ f"\n* Check your internet connection\n"
74
+ f"* Make sure MT5 is installed and active\n"
75
+ f"* Looking for a boker? See [{_ADMIRAL_MARKETS_URL_}] "
76
+ f"or [{_JUST_MARKETS_URL_}]\n"
77
+ f"* Looking for a prop firm? See [{_FTMO_URL_}]\n"
78
78
  )
79
79
 
80
80
  amg_url = _ADMIRAL_MARKETS_URL_
@@ -136,7 +136,16 @@ AMG_EXCHANGES = {
136
136
  }
137
137
 
138
138
 
139
- def check_mt5_connection(**kwargs) -> bool:
139
+ def check_mt5_connection(
140
+ *,
141
+ path=None,
142
+ login=None,
143
+ password=None,
144
+ server=None,
145
+ timeout=60_000,
146
+ portable=False,
147
+ **kwargs,
148
+ ) -> bool:
140
149
  """
141
150
  Initialize the connection to the MetaTrader 5 terminal.
142
151
 
@@ -156,13 +165,12 @@ def check_mt5_connection(**kwargs) -> bool:
156
165
  - Follow these instructions to lunch each terminal in portable mode first:
157
166
  https://www.metatrader5.com/en/terminal/help/start_advanced/start#configuration_file
158
167
  """
159
- path = kwargs.get("path", None)
160
- login = kwargs.get("login", None)
161
- password = kwargs.get("password", None)
162
- server = kwargs.get("server", None)
163
- timeout = kwargs.get("timeout", 60_000)
164
- portable = kwargs.get("portable", False)
165
-
168
+ if login is not None and server is not None:
169
+ account_info = mt5.account_info()
170
+ if account_info is not None:
171
+ if account_info.login == login and account_info.server == server:
172
+ return True
173
+
166
174
  init = False
167
175
  if path is None and (login or password or server):
168
176
  raise ValueError(
@@ -221,6 +229,11 @@ class Broker(object):
221
229
  return f"{self.__class__.__name__}({self.name})"
222
230
 
223
231
 
232
+ class MetaQuotes(Broker):
233
+ def __init__(self, **kwargs):
234
+ super().__init__(__BROKERS__["MQL"], **kwargs)
235
+
236
+
224
237
  class AdmiralMarktsGroup(Broker):
225
238
  def __init__(self, **kwargs):
226
239
  super().__init__(__BROKERS__["AMG"], **kwargs)
@@ -262,6 +275,7 @@ class AMP(Broker): ...
262
275
 
263
276
  BROKERS: Dict[str, Broker] = {
264
277
  "FTMO": FTMO(),
278
+ "MQL": MetaQuotes(),
265
279
  "AMG": AdmiralMarktsGroup(),
266
280
  "JGM": JustGlobalMarkets(),
267
281
  "PGL": PepperstoneGroupLimited(),
@@ -1372,7 +1386,7 @@ class Account(object):
1372
1386
  group: Optional[str] = None,
1373
1387
  ticket: Optional[int] = None,
1374
1388
  to_df: bool = False,
1375
- ) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
1389
+ ) -> Union[pd.DataFrame, Tuple[TradeOrder]]:
1376
1390
  """
1377
1391
  Get active orders with the ability to filter by symbol or ticket.
1378
1392
  There are four call options: