bbstrader 0.2.991__py3-none-any.whl → 0.3.1__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/__init__.py CHANGED
@@ -7,7 +7,7 @@ __author__ = "Bertin Balouki SIMYELI"
7
7
  __copyright__ = "2023-2025 Bertin Balouki SIMYELI"
8
8
  __email__ = "bertin@bbstrader.com"
9
9
  __license__ = "MIT"
10
- __version__ = "0.2.0"
10
+ __version__ = "0.3.1"
11
11
 
12
12
  from bbstrader import compat # noqa: F401
13
13
  from bbstrader import core # noqa: F401
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,23 @@ 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
+ try:
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)
65
+ except KeyboardInterrupt:
66
+ sys.exit(0)
67
+ except Exception:
68
+ sys.exit(1)
48
69
 
49
70
 
50
71
  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
@@ -149,7 +149,7 @@ class FmpNews(object):
149
149
  try:
150
150
  articles = pd.read_csv("latest_fmp_articles.csv")
151
151
  articles = articles.to_dict(orient="records")
152
- if self._last_date(articles[0]["date"]) < end_date:
152
+ if self._last_date(articles[0]["date"]).hour < end_date.hour:
153
153
  articles = self.get_articles(**kwargs)
154
154
  else:
155
155
  return articles
@@ -161,7 +161,9 @@ class FmpNews(object):
161
161
  df.to_csv("latest_fmp_articles.csv", index=False)
162
162
  return articles
163
163
 
164
- def get_news(self, query, source="articles", articles=None, symbol=None, **kwargs):
164
+ def get_news(
165
+ self, query, source="articles", articles=None, symbol: str = None, **kwargs
166
+ ):
165
167
  """
166
168
  Retrieves relevant financial news based on the specified source.
167
169
 
@@ -183,6 +185,10 @@ class FmpNews(object):
183
185
  Returns an empty list if no relevant news is found.
184
186
  """
185
187
  query = _get_search_query(query)
188
+ if symbol is not None:
189
+ symbol = symbol.replace("-", "").split("=")[
190
+ 0
191
+ ] # if symbol is a yahoo finance ticker
186
192
  source_methods = {
187
193
  "articles": lambda: self.get_latest_articles(articles=articles, save=True),
188
194
  "releases": lambda: self.get_releases(symbol=symbol, **kwargs),
@@ -191,6 +197,8 @@ class FmpNews(object):
191
197
  "forex": lambda: self.get_forex_news(symbol=symbol, **kwargs),
192
198
  }
193
199
  news_source = source_methods.get(source, lambda: [])()
200
+ if source == "articles":
201
+ symbol = None # Articles do not require a symbol filter
194
202
  news = self.parse_news(news_source, symbol=symbol)
195
203
  return _filter_news(news, query)
196
204
 
@@ -198,12 +206,12 @@ class FmpNews(object):
198
206
  class FinancialNews(object):
199
207
  """
200
208
  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.
209
+ from various sources such as Yahoo Finance, Google Finance, Reddit, Coindesk and Twitter.
202
210
  It also supports retrieving news using Financial Modeling Prep (FMP).
203
211
 
204
212
  """
205
213
 
206
- def _fetch_news(self, url, query, asset_type, n_news, headline_tag) -> List[str]:
214
+ def _fetch_news(self, url, query, n_news, headline_tag) -> List[str]:
207
215
  headers = {"User-Agent": "Mozilla/5.0"}
208
216
  try:
209
217
  response = requests.get(url, headers=headers)
@@ -224,32 +232,60 @@ class FinancialNews(object):
224
232
  ]
225
233
  return headlines[:n_news]
226
234
 
227
- def get_yahoo_finance_news(self, query, asset_type="stock", n_news=10):
235
+ def get_yahoo_finance_news(self, query: str, asset_type="stock", n_news=10):
228
236
  """
229
237
  Fetches recent Yahoo Finance news headlines for a given financial asset.
230
238
 
231
239
  Args:
232
240
  query (str): The asset symbol or name (e.g., "AAPL").
233
- asset_type (str, optional): The type of asset (e.g., "stock", "etf"). Defaults to "stock".
241
+ asset_type (str, optional): The type of asset (e.g., "stock", "etf"). Defaults to "stock",
242
+ supported types include:
243
+ - "stock": Stock symbols (e.g., AAPL, MSFT)
244
+ - "etf": Exchange-traded funds (e.g., SPY, QQQ)
245
+ - "future": Futures contracts (e.g., CL=F for crude oil)
246
+ - "forex": Forex pairs (e.g., EURUSD=X, USDJPY=X)
247
+ - "crypto": Cryptocurrency pairs (e.g., BTC-USD, ETH-USD)
248
+ - "index": Stock market indices (e.g., ^GSPC for S&P 500)
234
249
  n_news (int, optional): The number of news headlines to return. Defaults to 10.
235
250
 
251
+ Note:
252
+ For commotities and bonds, use the "Future" asset type.
253
+
236
254
  Returns:
237
255
  list[str]: A list of Yahoo Finance news headlines relevant to the query.
238
256
  """
257
+ if asset_type == "forex" or asset_type == "future":
258
+ assert (
259
+ "=" in query
260
+ ), "Forex query must contain '=' for currency pairs (e.g., EURUSD=X, CL=F)"
261
+ if asset_type == "crypto":
262
+ assert (
263
+ "-" in query
264
+ ), "Crypto query must contain '-' for crypto pairs (e.g., BTC-USD, ETH-USD)"
265
+ if asset_type == "index":
266
+ assert query.startswith(
267
+ "^"
268
+ ), "Index query must start with '^' (e.g., ^GSPC for S&P 500)"
239
269
  url = (
240
270
  f"https://finance.yahoo.com/quote/{query}/news"
241
- if asset_type in ["stock", "etf"]
271
+ if asset_type in ["stock", "etf", "index", "future", "forex"]
242
272
  else "https://finance.yahoo.com/news"
243
273
  )
244
- return self._fetch_news(url, query, asset_type, n_news, "h3")
274
+ return self._fetch_news(url, query, n_news, "h3")
245
275
 
246
- def get_google_finance_news(self, query, asset_type="stock", n_news=10):
276
+ def get_google_finance_news(self, query: str, asset_type="stock", n_news=10):
247
277
  """
248
278
  Fetches recent Google Finance news headlines for a given financial asset.
249
279
 
250
280
  Args:
251
281
  query (str): The asset symbol or name (e.g., "AAPL").
252
282
  asset_type (str, optional): The type of asset (e.g., "stock", "crypto"). Defaults to "stock".
283
+ Supported types include:
284
+ - "stock": Stock symbols (e.g., AAPL, MSFT)
285
+ - "etf": Exchange-traded funds (e.g., SPY, QQQ)
286
+ - "future": Futures contracts (e.g., CL=F or crude oil)
287
+ - "forex": Forex pairs (e.g., EURUSD, USDJPY)
288
+ - "crypto": Cryptocurrency pairs (e.g., BTCUSD, ETHUSD)
253
289
  n_news (int, optional): The number of news headlines to return. Defaults to 10.
254
290
 
255
291
  Returns:
@@ -258,20 +294,18 @@ class FinancialNews(object):
258
294
  search_terms = {
259
295
  "stock": f"{query} stock OR {query} shares OR {query} market",
260
296
  "etf": f"{query} ETF OR {query} fund OR {query} exchange-traded fund",
261
- "futures": f"{query} futures OR {query} price OR {query} market",
262
- "commodity": f"{query} price OR {query} futures OR {query} market",
297
+ "future": f"{query} futures OR {query} price OR {query} market",
263
298
  "forex": f"{query} forex OR {query} exchange rate OR {query} market",
264
299
  "crypto": f"{query} cryptocurrency OR {query} price OR {query} market",
265
- "bond": f"{query} bond OR {query} yield OR {query} interest rate",
266
300
  "index": f"{query} index OR {query} stock market OR {query} performance",
267
301
  }
268
302
  search_query = search_terms.get(asset_type, query)
269
303
  url = f"https://news.google.com/search?q={search_query.replace(' ', '+')}"
270
- return self._fetch_news(url, query, asset_type, n_news, "a")
304
+ return self._fetch_news(url, query, n_news, "a")
271
305
 
272
306
  def get_reddit_posts(
273
307
  self,
274
- symbol,
308
+ symbol: str,
275
309
  client_id=None,
276
310
  client_secret=None,
277
311
  user_agent=None,
@@ -294,8 +328,8 @@ class FinancialNews(object):
294
328
  - "stock": Searches in stock-related subreddits (e.g., wallstreetbets, stocks).
295
329
  - "forex": Searches in forex-related subreddits.
296
330
  - "commodities": Searches in commodity-related subreddits (e.g., gold, oil).
297
- - "etfs": Searches in ETF-related subreddits.
298
- - "futures": Searches in futures and options trading subreddits.
331
+ - "etf": Searches in ETF-related subreddits.
332
+ - "future": Searches in futures and options trading subreddits.
299
333
  - "crypto": Searches in cryptocurrency-related subreddits.
300
334
  - If an unrecognized asset class is provided, defaults to stock-related subreddits.
301
335
  n_posts (int, optional): The number of posts to return per subreddit. Defaults to 10.
@@ -317,15 +351,36 @@ class FinancialNews(object):
317
351
  """
318
352
 
319
353
  reddit = praw.Reddit(
320
- client_id=client_id, client_secret=client_secret, user_agent=user_agent
354
+ client_id=client_id,
355
+ client_secret=client_secret,
356
+ user_agent=user_agent,
357
+ check_for_updates=False,
358
+ comment_kind="t1",
359
+ message_kind="t4",
360
+ redditor_kind="t2",
361
+ submission_kind="t3",
362
+ subreddit_kind="t5",
363
+ trophy_kind="t6",
364
+ oauth_url="https://oauth.reddit.com",
365
+ reddit_url="https://www.reddit.com",
366
+ short_url="https://redd.it",
367
+ timeout=16,
368
+ ratelimit_seconds=5,
321
369
  )
322
-
370
+ assert reddit.read_only
323
371
  subreddit_mapping = {
324
372
  "stock": ["wallstreetbets", "stocks", "investing", "StockMarket"],
325
373
  "forex": ["Forex", "ForexTrading", "DayTrading"],
326
- "commodities": ["Commodities", "Gold", "Silverbugs", "oil"],
327
374
  "etfs": ["ETFs", "investing"],
328
- "futures": ["FuturesTrading", "OptionsTrading", "DayTrading"],
375
+ "futures": [
376
+ "FuturesTrading",
377
+ "OptionsTrading",
378
+ "DayTrading",
379
+ "Commodities",
380
+ "Gold",
381
+ "Silverbugs",
382
+ "oil",
383
+ ],
329
384
  "crypto": ["CryptoCurrency", "Bitcoin", "ethereum", "altcoin"],
330
385
  }
331
386
  try:
@@ -473,16 +528,23 @@ class FinancialNews(object):
473
528
  maximum = 100
474
529
  if limit > maximum:
475
530
  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:
531
+ try:
532
+ response = requests.get(
533
+ "https://data-api.coindesk.com/news/v1/article/list",
534
+ params={"lang": lang, "limit": limit},
535
+ headers={"Content-type": "application/json; charset=UTF-8"},
536
+ )
537
+ response.raise_for_status()
538
+ json_response = response.json()
539
+ except requests.exceptions.RequestException:
485
540
  return []
541
+ if (
542
+ response.status_code != 200
543
+ or "Data" not in json_response
544
+ or len(json_response["Data"]) == 0
545
+ ):
546
+ return []
547
+ articles = json_response["Data"]
486
548
  to_keep = [
487
549
  "PUBLISHED_ON",
488
550
  "TITLE",
@@ -495,10 +557,11 @@ class FinancialNews(object):
495
557
  ]
496
558
  filtered_articles = []
497
559
  for article in articles:
560
+ keys = article.keys()
498
561
  filtered_articles.append(
499
562
  {
500
563
  k.lower(): article[k]
501
- if k in article and k != "PUBLISHED_ON"
564
+ if k in keys and k != "PUBLISHED_ON"
502
565
  else datetime.fromtimestamp(article[k])
503
566
  for k in to_keep
504
567
  if article[k] is not None and "sponsored" not in str(article[k])