bbstrader 0.2.991__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.

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__(
@@ -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
@@ -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
  """
@@ -0,0 +1,130 @@
1
+ import argparse
2
+ import asyncio
3
+ import sys
4
+ import textwrap
5
+ import time
6
+ from datetime import datetime, timedelta
7
+
8
+ import nltk
9
+ from loguru import logger
10
+ from sumy.nlp.tokenizers import Tokenizer
11
+ from sumy.parsers.plaintext import PlaintextParser
12
+ from sumy.summarizers.text_rank import TextRankSummarizer
13
+
14
+ from bbstrader.core.data import FinancialNews
15
+ from bbstrader.trading.utils import send_telegram_message
16
+
17
+
18
+ def summarize_text(text, sentences_count=5):
19
+ """
20
+ Generate a summary using TextRank algorithm.
21
+ """
22
+ parser = PlaintextParser.from_string(text, Tokenizer("english"))
23
+ summarizer = TextRankSummarizer()
24
+ summary = summarizer(parser.document, sentences_count)
25
+ return " ".join(str(sentence) for sentence in summary)
26
+
27
+
28
+ def format_article_for_telegram(article: dict) -> str:
29
+ if not all(
30
+ k in article
31
+ for k in (
32
+ "body",
33
+ "title",
34
+ "published_on",
35
+ "sentiment",
36
+ "keywords",
37
+ "keywords",
38
+ "url",
39
+ )
40
+ ):
41
+ return ""
42
+ summary = summarize_text(article["body"])
43
+ text = (
44
+ f"📰 {article['title']}\n"
45
+ f"Published Date: {article['published_on']}\n"
46
+ f"Sentiment: {article['sentiment']}\n"
47
+ f"Status: {article['status']}\n"
48
+ f"Keywords: {article['keywords']}\n\n"
49
+ f"🔍 Summary\n"
50
+ f"{textwrap.fill(summary, width=80)}"
51
+ f"\n\n👉 Visit {article['url']} for full article."
52
+ )
53
+ return text
54
+
55
+
56
+ async def send_articles(articles: dict, token: str, id: str, interval=15):
57
+ 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)
66
+
67
+
68
+ def send_news_feed(unknown):
69
+ HELP_MSG = """
70
+ Send news feed from Coindesk to Telegram channel.
71
+ This script fetches the latest news articles from Coindesk, summarizes them,
72
+ and sends them to a specified Telegram channel at regular intervals.
73
+
74
+ Usage:
75
+ python -m bbstrader --run news_feed [options]
76
+
77
+ Options:
78
+ -q, --query: The news to look for (default: "")
79
+ -t, --token: Telegram bot token
80
+ -I, --id: Telegram Chat id
81
+ -i, --interval: Interval in minutes to fetch news (default: 15)
82
+
83
+ Note:
84
+ The script will run indefinitely, fetching news every 15 minutes.
85
+ Use Ctrl+C to stop the script.
86
+ """
87
+
88
+ if "-h" in unknown or "--help" in unknown:
89
+ print(HELP_MSG)
90
+ sys.exit(0)
91
+
92
+ parser = argparse.ArgumentParser()
93
+ parser.add_argument(
94
+ "-q", "--query", type=str, default="", help="The news to look for"
95
+ )
96
+ parser.add_argument(
97
+ "-t",
98
+ "--token",
99
+ type=str,
100
+ required=True,
101
+ help="Telegram bot token",
102
+ )
103
+ parser.add_argument("-I", "--id", type=str, required=True, help="Telegram Chat id")
104
+ parser.add_argument(
105
+ "-i",
106
+ "--interval",
107
+ type=int,
108
+ default=15,
109
+ help="Interval in minutes to fetch news (default: 15)",
110
+ )
111
+ args = parser.parse_args(unknown)
112
+
113
+ nltk.download("punkt", quiet=True)
114
+ news = FinancialNews()
115
+ logger.info(
116
+ f"Starting the News Feed on {args.interval} minutes"
117
+ )
118
+ while True:
119
+ 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))
125
+ time.sleep(args.interval * 60)
126
+ except KeyboardInterrupt:
127
+ logger.info("Stopping the News Feed ...")
128
+ exit(0)
129
+ except Exception as e:
130
+ logger.error(e)