bbstrader 2.0.3__cp312-cp312-macosx_11_0_arm64.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.
Files changed (45) hide show
  1. bbstrader/__init__.py +27 -0
  2. bbstrader/__main__.py +92 -0
  3. bbstrader/api/__init__.py +96 -0
  4. bbstrader/api/handlers.py +245 -0
  5. bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
  6. bbstrader/api/metatrader_client.pyi +624 -0
  7. bbstrader/assets/bbs_.png +0 -0
  8. bbstrader/assets/bbstrader.ico +0 -0
  9. bbstrader/assets/bbstrader.png +0 -0
  10. bbstrader/assets/qs_metrics_1.png +0 -0
  11. bbstrader/btengine/__init__.py +54 -0
  12. bbstrader/btengine/backtest.py +358 -0
  13. bbstrader/btengine/data.py +737 -0
  14. bbstrader/btengine/event.py +229 -0
  15. bbstrader/btengine/execution.py +287 -0
  16. bbstrader/btengine/performance.py +408 -0
  17. bbstrader/btengine/portfolio.py +393 -0
  18. bbstrader/btengine/strategy.py +588 -0
  19. bbstrader/compat.py +28 -0
  20. bbstrader/config.py +100 -0
  21. bbstrader/core/__init__.py +27 -0
  22. bbstrader/core/data.py +628 -0
  23. bbstrader/core/strategy.py +466 -0
  24. bbstrader/metatrader/__init__.py +48 -0
  25. bbstrader/metatrader/_copier.py +720 -0
  26. bbstrader/metatrader/account.py +865 -0
  27. bbstrader/metatrader/broker.py +418 -0
  28. bbstrader/metatrader/copier.py +1487 -0
  29. bbstrader/metatrader/rates.py +495 -0
  30. bbstrader/metatrader/risk.py +667 -0
  31. bbstrader/metatrader/trade.py +1692 -0
  32. bbstrader/metatrader/utils.py +402 -0
  33. bbstrader/models/__init__.py +39 -0
  34. bbstrader/models/nlp.py +932 -0
  35. bbstrader/models/optimization.py +182 -0
  36. bbstrader/scripts.py +665 -0
  37. bbstrader/trading/__init__.py +33 -0
  38. bbstrader/trading/execution.py +1159 -0
  39. bbstrader/trading/strategy.py +362 -0
  40. bbstrader/trading/utils.py +69 -0
  41. bbstrader-2.0.3.dist-info/METADATA +396 -0
  42. bbstrader-2.0.3.dist-info/RECORD +45 -0
  43. bbstrader-2.0.3.dist-info/WHEEL +5 -0
  44. bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
  45. bbstrader-2.0.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,27 @@
1
+ """
2
+ Overview
3
+ ========
4
+
5
+ The Core Module provides the fundamental building blocks and abstract base classes for the
6
+ trading system. It defines the essential components that are extended by other modules to
7
+ create a complete trading application, ensuring a consistent and modular architecture.
8
+
9
+ Features
10
+ ========
11
+
12
+ - **Abstract Base Classes**: Defines the interfaces for key components like data handlers and strategies, promoting a standardized approach to development.
13
+ - **Modularity**: Enforces a modular design by providing a clear separation of concerns between data handling, strategy logic, and execution.
14
+ - **Extensibility**: Designed to be easily extended with concrete implementations, allowing for the creation of custom data sources and trading strategies.
15
+
16
+ Components
17
+ ==========
18
+
19
+ - **Data**: Contains the abstract base class `DataHandler`, which defines the interface for managing market data from various sources.
20
+ - **Strategy**: Contains the abstract base class `Strategy`, which provides the framework for developing trading strategies.
21
+
22
+ This module contains the abstract classes that form the foundation of the trading system.
23
+ Implementations of these classes can be found in other modules like `btengine` and `trading`.
24
+ """
25
+
26
+ from bbstrader.core.data import * # noqa: F403
27
+ from bbstrader.core.strategy import * # noqa: F403
bbstrader/core/data.py ADDED
@@ -0,0 +1,628 @@
1
+ import json
2
+ import re
3
+ import ssl
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Literal, Optional, Union
6
+ from urllib.request import urlopen
7
+
8
+ import certifi
9
+ import pandas as pd
10
+ import praw
11
+ import requests
12
+ import tweepy
13
+ import yfinance as yf
14
+ from bs4 import BeautifulSoup
15
+ from financetoolkit import Toolkit
16
+
17
+ __all__ = ["FmpData", "FmpNews", "FinancialNews"]
18
+
19
+
20
+ def _get_search_query(query: str) -> str:
21
+ if " " in query or query == "":
22
+ return query
23
+ try:
24
+ name = yf.Ticker(query).info["shortName"]
25
+ return query + " " + name
26
+ except Exception:
27
+ return query
28
+
29
+
30
+ def _find_news(query: Union[str, List[str]], text: str) -> bool:
31
+ if isinstance(query, str):
32
+ query = query.split(" ")
33
+ pattern = r"\b(?:" + "|".join(map(re.escape, query)) + r")\b"
34
+ if re.search(pattern, text, re.IGNORECASE):
35
+ return True
36
+ return False
37
+
38
+
39
+ def _filter_news(news: List[str], query: Union[str, List[str]]) -> List[str]:
40
+ return [text for text in news if _find_news(query, text)]
41
+
42
+
43
+ class FmpNews(object):
44
+ """
45
+ ``FmpNews`` is responsible for retrieving financial news, press releases, and articles from Financial Modeling Prep (FMP).
46
+
47
+ ``FmpNews`` provides methods to fetch the latest stock, crypto, forex, and general financial news,
48
+ as well as financial articles and press releases.
49
+ """
50
+
51
+ def __init__(self, api: str) -> None:
52
+ """
53
+ Args:
54
+ api (str): The API key for accessing FMP's news data.
55
+
56
+ Example:
57
+ fmp_news = FmpNews(api="your_api_key_here")
58
+ """
59
+ if api is None:
60
+ raise ValueError("API key is required For FmpNews")
61
+ self.__api = api
62
+
63
+ def _jsonparsed_data(self, url: str) -> Any:
64
+ context = ssl.create_default_context(cafile=certifi.where())
65
+ with urlopen(url, context=context) as response:
66
+ data = response.read().decode("utf-8")
67
+ return json.loads(data)
68
+
69
+ def _load_news(
70
+ self, news_type: str, symbol: Optional[str] = None, **kwargs: Any
71
+ ) -> List[Dict[str, Any]]:
72
+ params = {"start": "from", "end": "to", "page": "page", "limit": "limit"}
73
+ base_url = f"https://financialmodelingprep.com/stable/news/{news_type}-latest?apikey={self.__api}"
74
+ if news_type == "articles":
75
+ assert symbol is None, ValueError("symbol not supported for articles")
76
+ base_url = f"https://financialmodelingprep.com/stable/fmp-articles?apikey={self.__api}"
77
+ elif symbol is not None:
78
+ base_url = f"https://financialmodelingprep.com/stable/news/{news_type}?symbols={symbol}&apikey={self.__api}"
79
+
80
+ for param, value in params.items():
81
+ if kwargs.get(param) is not None:
82
+ base_url += f"&{value.strip()}={kwargs.get(param)}"
83
+
84
+ return self._jsonparsed_data(base_url)
85
+
86
+ def get_articles(self, **kwargs: Any) -> List[Dict[str, Any]]:
87
+ def html_parser(content: str) -> str:
88
+ soup = BeautifulSoup(content, "html.parser")
89
+ text = soup.get_text(separator="\n")
90
+ return text.replace("\n", "")
91
+
92
+ articles = self._load_news("articles", **kwargs)
93
+ df = pd.DataFrame(articles)
94
+ df = df[["title", "date", "content", "tickers"]]
95
+ df["content"] = df["content"].apply(html_parser)
96
+ return df.to_dict(orient="records") # type: ignore
97
+
98
+ def get_releases(
99
+ self, symbol: Optional[str] = None, **kwargs: Any
100
+ ) -> List[Dict[str, Any]]:
101
+ return self._load_news("press-releases", symbol, **kwargs)
102
+
103
+ def get_stock_news(
104
+ self, symbol: Optional[str] = None, **kwargs: Any
105
+ ) -> List[Dict[str, Any]]:
106
+ return self._load_news("stock", symbol, **kwargs)
107
+
108
+ def get_crypto_news(
109
+ self, symbol: Optional[str] = None, **kwargs: Any
110
+ ) -> List[Dict[str, Any]]:
111
+ return self._load_news("crypto", symbol, **kwargs)
112
+
113
+ def get_forex_news(
114
+ self, symbol: Optional[str] = None, **kwargs: Any
115
+ ) -> List[Dict[str, Any]]:
116
+ return self._load_news("forex", symbol, **kwargs)
117
+
118
+ def _last_date(self, date: str) -> datetime:
119
+ return datetime.strptime(date, "%Y-%m-%d %H:%M:%S")
120
+
121
+ def parse_news(
122
+ self, news: List[Dict[str, Any]], symbol: Optional[str] = None, **kwargs: Any
123
+ ) -> List[str]:
124
+ start = kwargs.get("start")
125
+ end = kwargs.get("end")
126
+ end_date = self._last_date(end) if end is not None else datetime.now().date()
127
+
128
+ def parse_record(record: Dict[str, Any]) -> str:
129
+ return " ".join(
130
+ [
131
+ record.pop("symbol", ""),
132
+ record.pop("title", ""),
133
+ record.pop("text", ""),
134
+ record.pop("content", ""),
135
+ record.pop("tickers", ""),
136
+ ]
137
+ )
138
+
139
+ parsed_news = []
140
+ for record in news:
141
+ date = record.get("publishedDate")
142
+ published_date = self._last_date(record.get("date", date)).date() # type: ignore
143
+ start_date = (
144
+ self._last_date(start).date() if start is not None else published_date
145
+ )
146
+ if published_date >= start_date and published_date <= end_date:
147
+ if symbol is not None:
148
+ if record.get("symbol", "") == symbol or symbol in record.get(
149
+ "tickers", ""
150
+ ):
151
+ parsed_news.append(parse_record(record))
152
+ else:
153
+ parsed_news.append(parse_record(record))
154
+ return parsed_news
155
+
156
+ def get_latest_articles(
157
+ self,
158
+ articles: Optional[List[Dict[str, Any]]] = None,
159
+ save: bool = False,
160
+ **kwargs: Any,
161
+ ) -> List[Dict[str, Any]]:
162
+ end = kwargs.get("end")
163
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
164
+ end_date = self._last_date(end) if end is not None else self._last_date(now)
165
+ if articles is None:
166
+ try:
167
+ articles = pd.read_csv("latest_fmp_articles.csv") # type: ignore
168
+ articles = articles.to_dict(orient="records") # type: ignore
169
+ if self._last_date(articles[0]["date"]).hour < end_date.hour: # type: ignore
170
+ articles = self.get_articles(**kwargs)
171
+ else:
172
+ return articles # type: ignore
173
+ except FileNotFoundError:
174
+ articles = self.get_articles(**kwargs)
175
+
176
+ if save and len(articles) > 0:
177
+ df = pd.DataFrame(articles)
178
+ df.to_csv("latest_fmp_articles.csv", index=False)
179
+ return articles
180
+
181
+ def get_news(
182
+ self,
183
+ query: str,
184
+ source: str = "articles",
185
+ articles: Optional[List[Dict[str, Any]]] = None,
186
+ symbol: Optional[str] = None,
187
+ **kwargs: Any,
188
+ ) -> List[str]:
189
+ """
190
+ Retrieves relevant financial news based on the specified source.
191
+
192
+ Args:
193
+ query (str): The search query or keyword for filtering news, may also be a ticker.
194
+ source (str, optional): The news source to retrieve from. Defaults to "articles".
195
+ Available options: "articles", "releases", "stock", "crypto", "forex".
196
+ articles (list, optional): List of pre-fetched articles to use when source="articles". Defaults to None.
197
+ symbol (str, optional): The financial asset symbol (e.g., "AAPL" for stocks, "BTC" for crypto). Defaults to None.
198
+ **kwargs (dict):
199
+ Additional arguments required for fetching news data. May include:
200
+ - start (str): The start period for news retrieval (YYY-MM-DD)
201
+ - end (str): The end period for news retrieval (YYY-MM-DD)
202
+ - page (int): The number of page to load for each news
203
+ - limit (int): Maximum Responses per API Call
204
+
205
+ Returns:
206
+ list[dict]: A list of filtered news articles relevant to the query.
207
+ Returns an empty list if no relevant news is found.
208
+ """
209
+ query = _get_search_query(query)
210
+ if symbol is not None:
211
+ symbol = symbol.replace("-", "").split("=")[
212
+ 0
213
+ ] # if symbol is a yahoo finance ticker
214
+ source_methods = {
215
+ "articles": lambda: self.get_latest_articles(
216
+ articles=articles, save=True, **kwargs
217
+ ),
218
+ "releases": lambda: self.get_releases(symbol=symbol, **kwargs),
219
+ "stock": lambda: self.get_stock_news(symbol=symbol, **kwargs),
220
+ "crypto": lambda: self.get_crypto_news(symbol=symbol, **kwargs),
221
+ "forex": lambda: self.get_forex_news(symbol=symbol, **kwargs),
222
+ }
223
+ news_source = source_methods.get(source, lambda: [])()
224
+ if source == "articles":
225
+ symbol = None # Articles do not require a symbol filter
226
+ news = self.parse_news(news_source, symbol=symbol, **kwargs)
227
+ return _filter_news(news, query)
228
+
229
+
230
+ class FinancialNews(object):
231
+ """
232
+ The FinancialNews class provides methods to fetch financial news, articles, and discussions
233
+ from various sources such as Yahoo Finance, Google Finance, Reddit, Coindesk and Twitter.
234
+ It also supports retrieving news using Financial Modeling Prep (FMP).
235
+
236
+ """
237
+
238
+ def _fetch_news(
239
+ self, url: str, query: str, n_news: int, headline_tag: str
240
+ ) -> List[str]:
241
+ headers = {"User-Agent": "Mozilla/5.0"}
242
+ try:
243
+ response = requests.get(url, headers=headers)
244
+ response.raise_for_status()
245
+ except requests.exceptions.RequestException:
246
+ response = None
247
+
248
+ if response is None or response.status_code != 200:
249
+ return []
250
+
251
+ query = _get_search_query(query)
252
+ soup = BeautifulSoup(response.text, "html.parser")
253
+
254
+ headlines = [
255
+ h.text.strip()
256
+ for h in soup.find_all(headline_tag)
257
+ if h.text and _find_news(query, h.text)
258
+ ]
259
+ return headlines[:n_news]
260
+
261
+ def get_yahoo_finance_news(
262
+ self, query: str, asset_type: str = "stock", n_news: int = 10
263
+ ) -> List[str]:
264
+ """
265
+ Fetches recent Yahoo Finance news headlines for a given financial asset.
266
+
267
+ Args:
268
+ query (str): The asset symbol or name (e.g., "AAPL").
269
+ asset_type (str, optional): The type of asset (e.g., "stock", "etf"). Defaults to "stock",
270
+ supported types include:
271
+ - "stock": Stock symbols (e.g., AAPL, MSFT)
272
+ - "etf": Exchange-traded funds (e.g., SPY, QQQ)
273
+ - "future": Futures contracts (e.g., CL=F for crude oil)
274
+ - "forex": Forex pairs (e.g., EURUSD=X, USDJPY=X)
275
+ - "crypto": Cryptocurrency pairs (e.g., BTC-USD, ETH-USD)
276
+ - "index": Stock market indices (e.g., ^GSPC for S&P 500)
277
+ n_news (int, optional): The number of news headlines to return. Defaults to 10.
278
+
279
+ Note:
280
+ For commotities and bonds, use the "Future" asset type.
281
+
282
+ Returns:
283
+ list[str]: A list of Yahoo Finance news headlines relevant to the query.
284
+ """
285
+ if asset_type == "forex" or asset_type == "future":
286
+ assert "=" in query, (
287
+ "Forex query must contain '=' for currency pairs (e.g., EURUSD=X, CL=F)"
288
+ )
289
+ if asset_type == "crypto":
290
+ assert "-" in query, (
291
+ "Crypto query must contain '-' for crypto pairs (e.g., BTC-USD, ETH-USD)"
292
+ )
293
+ if asset_type == "index":
294
+ assert query.startswith("^"), (
295
+ "Index query must start with '^' (e.g., ^GSPC for S&P 500)"
296
+ )
297
+ url = (
298
+ f"https://finance.yahoo.com/quote/{query}/news"
299
+ if asset_type in ["stock", "etf", "index", "future", "forex"]
300
+ else "https://finance.yahoo.com/news"
301
+ )
302
+ return self._fetch_news(url, query, n_news, "h3")
303
+
304
+ def get_google_finance_news(
305
+ self, query: str, asset_type: str = "stock", n_news: int = 10
306
+ ) -> List[str]:
307
+ """
308
+ Fetches recent Google Finance news headlines for a given financial asset.
309
+
310
+ Args:
311
+ query (str): The asset symbol or name (e.g., "AAPL").
312
+ asset_type (str, optional): The type of asset (e.g., "stock", "crypto"). Defaults to "stock".
313
+ Supported types include:
314
+ - "stock": Stock symbols (e.g., AAPL, MSFT)
315
+ - "etf": Exchange-traded funds (e.g., SPY, QQQ)
316
+ - "future": Futures contracts (e.g., CL=F or crude oil)
317
+ - "forex": Forex pairs (e.g., EURUSD, USDJPY)
318
+ - "crypto": Cryptocurrency pairs (e.g., BTCUSD, ETHUSD)
319
+ n_news (int, optional): The number of news headlines to return. Defaults to 10.
320
+
321
+ Returns:
322
+ list[str]: A list of Google Finance news headlines relevant to the query.
323
+ """
324
+ search_terms = {
325
+ "stock": f"{query} stock OR {query} shares OR {query} market",
326
+ "etf": f"{query} ETF OR {query} fund OR {query} exchange-traded fund",
327
+ "future": f"{query} futures OR {query} price OR {query} market",
328
+ "forex": f"{query} forex OR {query} exchange rate OR {query} market",
329
+ "crypto": f"{query} cryptocurrency OR {query} price OR {query} market",
330
+ "index": f"{query} index OR {query} stock market OR {query} performance",
331
+ }
332
+ search_query = search_terms.get(asset_type, query)
333
+ url = f"https://news.google.com/search?q={search_query.replace(' ', '+')}"
334
+ return self._fetch_news(url, query, n_news, "a")
335
+
336
+ def get_reddit_posts(
337
+ self,
338
+ symbol: str,
339
+ client_id=None,
340
+ client_secret=None,
341
+ user_agent=None,
342
+ asset_class="stock",
343
+ n_posts=10,
344
+ ) -> List[str]:
345
+ """
346
+ Fetches recent Reddit posts related to a financial asset.
347
+
348
+ This method queries relevant subreddits for posts mentioning the specified symbol
349
+ and returns posts based on the selected asset class (e.g., stock, forex, crypto).
350
+ The function uses the PRAW library to interact with Reddit's API.
351
+
352
+ Args:
353
+ symbol (str): The financial asset's symbol or name to search for.
354
+ client_id (str, optional): Reddit API client ID for authentication.
355
+ client_secret (str, optional): Reddit API client secret.
356
+ user_agent (str, optional): Reddit API user agent.
357
+ asset_class (str, optional): The type of financial asset. Defaults to "stock".
358
+ - "stock": Searches in stock-related subreddits (e.g., wallstreetbets, stocks).
359
+ - "forex": Searches in forex-related subreddits.
360
+ - "commodities": Searches in commodity-related subreddits (e.g., gold, oil).
361
+ - "etf": Searches in ETF-related subreddits.
362
+ - "future": Searches in futures and options trading subreddits.
363
+ - "crypto": Searches in cryptocurrency-related subreddits.
364
+ - If an unrecognized asset class is provided, defaults to stock-related subreddits.
365
+ n_posts (int, optional): The number of posts to return per subreddit. Defaults to 10.
366
+
367
+ Returns:
368
+ list[str]: A list of Reddit post contents matching the query.
369
+ Each entry contains the post title and body.
370
+ If no posts are found or an error occurs, returns an empty list.
371
+
372
+ Raises:
373
+ praw.exceptions.PRAWException: If an error occurs while interacting with Reddit's API.
374
+
375
+ Example:
376
+ >>> get_reddit_posts(symbol="AAPL", client_id="your_id", client_secret="your_secret", user_agent="your_agent", asset_class="stock", n_posts=5)
377
+ ["Apple stock is rallying today due to strong earnings.", "Should I buy $AAPL now?", ...]
378
+
379
+ Notes:
380
+ - Requires valid Reddit API credentials.
381
+ """
382
+
383
+ reddit = praw.Reddit(
384
+ client_id=client_id,
385
+ client_secret=client_secret,
386
+ user_agent=user_agent,
387
+ check_for_updates=False,
388
+ comment_kind="t1",
389
+ message_kind="t4",
390
+ redditor_kind="t2",
391
+ submission_kind="t3",
392
+ subreddit_kind="t5",
393
+ trophy_kind="t6",
394
+ oauth_url="https://oauth.reddit.com",
395
+ reddit_url="https://www.reddit.com",
396
+ short_url="https://redd.it",
397
+ timeout=16,
398
+ ratelimit_seconds=5,
399
+ )
400
+ assert reddit.read_only
401
+ subreddit_mapping = {
402
+ "stock": ["wallstreetbets", "stocks", "investing", "StockMarket"],
403
+ "forex": ["Forex", "ForexTrading", "DayTrading"],
404
+ "etfs": ["ETFs", "investing"],
405
+ "futures": [
406
+ "FuturesTrading",
407
+ "OptionsTrading",
408
+ "DayTrading",
409
+ "Commodities",
410
+ "Gold",
411
+ "Silverbugs",
412
+ "oil",
413
+ ],
414
+ "crypto": ["CryptoCurrency", "Bitcoin", "ethereum", "altcoin"],
415
+ }
416
+ try:
417
+ subreddits = subreddit_mapping.get(asset_class.lower(), ["stocks"])
418
+ except Exception:
419
+ return []
420
+
421
+ posts = []
422
+ for sub in subreddits:
423
+ subreddit = reddit.subreddit(sub)
424
+ query = _get_search_query(symbol)
425
+ all_posts = subreddit.search(query, limit=n_posts)
426
+ for post in all_posts:
427
+ text = post.title + " " + post.selftext
428
+ if _find_news(query, text):
429
+ posts.append(text)
430
+ return posts
431
+
432
+ def get_twitter_posts(
433
+ self,
434
+ query: str,
435
+ asset_type: str = "stock",
436
+ bearer: Optional[str] = None,
437
+ api_key: Optional[str] = None,
438
+ api_secret: Optional[str] = None,
439
+ access_token: Optional[str] = None,
440
+ access_secret: Optional[str] = None,
441
+ n_posts: int = 10,
442
+ ) -> List[str]:
443
+ """
444
+ Fetches recent tweets related to a financial asset.
445
+
446
+ This method queries Twitter for recent posts mentioning the specified asset
447
+ and filters the results based on the asset type (e.g., stock, forex, crypto).
448
+ The function uses the Tweepy API to fetch tweets and returns a list of tweet texts.
449
+
450
+ Args:
451
+ query (str): The main keyword to search for (e.g., a stock ticker or asset name).
452
+ asset_type (str, optional): The type of financial asset. Defaults to "stock".
453
+ - "stock": Searches for tweets mentioning the stock or shares.
454
+ - "forex": Searches for tweets mentioning foreign exchange (forex) or currency.
455
+ - "crypto": Searches for tweets mentioning cryptocurrency or related terms.
456
+ - "commodity": Searches for tweets mentioning commodities or futures trading.
457
+ - "index": Searches for tweets mentioning stock market indices.
458
+ - "bond": Searches for tweets mentioning bonds or fixed income securities.
459
+ - If an unrecognized asset type is provided, defaults to general finance-related tweets.
460
+ bearer (str, optional): Twitter API bearer token for authentication.
461
+ api_key (str, optional): Twitter API consumer key.
462
+ api_secret (str, optional): Twitter API consumer secret.
463
+ access_token (str, optional): Twitter API access token.
464
+ access_secret (str, optional): Twitter API access token secret.
465
+ n_posts (int, optional): The number of tweets to return. Defaults to 10.
466
+
467
+ Returns:
468
+ list[str]: A list of up to `n_posts` tweet texts matching the query.
469
+ If no tweets are found or an API error occurs, returns an empty list.
470
+
471
+ Raises:
472
+ tweepy.TweepyException: If an error occurs while making the Twitter API request.
473
+
474
+ Example:
475
+ >>> get_twitter_posts(query="AAPL", asset_type="stock", bearer="YOUR_BEARER_TOKEN", n_posts=5)
476
+ ["Apple stock surges after strong earnings!", "Is $AAPL a buy at this price?", ...]
477
+ """
478
+ client = tweepy.Client(
479
+ bearer_token=bearer,
480
+ consumer_key=api_key,
481
+ consumer_secret=api_secret,
482
+ access_token=access_token,
483
+ access_token_secret=access_secret,
484
+ )
485
+ asset_queries = {
486
+ "stock": f"{query} stock OR {query} shares -is:retweet lang:en",
487
+ "forex": f"{query} forex OR {query} currency -is:retweet lang:en",
488
+ "crypto": f"{query} cryptocurrency OR {query} crypto OR #{query} -is:retweet lang:en",
489
+ "commodity": f"{query} commodity OR {query} futures OR {query} trading -is:retweet lang:en",
490
+ "index": f"{query} index OR {query} market -is:retweet lang:en",
491
+ "bond": f"{query} bonds OR {query} fixed income -is:retweet lang:en",
492
+ }
493
+ # Get the correct query based on the asset type
494
+ search = asset_queries.get(
495
+ asset_type.lower(), f"{query} finance -is:retweet lang:en"
496
+ )
497
+ try:
498
+ tweets = client.search_recent_tweets(
499
+ query=search, max_results=100, tweet_fields=["text"]
500
+ )
501
+ query = _get_search_query(query)
502
+ news = [tweet.text for tweet in tweets.data] if tweets.data else [] # type: ignore
503
+ return _filter_news(news, query)[:n_posts]
504
+ except tweepy.TweepyException:
505
+ return []
506
+
507
+ def get_fmp_news(self, api: str | None = None) -> FmpNews:
508
+ return FmpNews(api=api) # type: ignore
509
+
510
+ def get_coindesk_news(
511
+ self,
512
+ query="",
513
+ lang: Literal["EN", "ES", "TR", "FR", "JP", "PT"] = "EN",
514
+ limit=10,
515
+ list_of_str=False,
516
+ ) -> List[str] | List[dict]:
517
+ """
518
+ Fetches and filters recent news articles from CoinDesk's News API.
519
+
520
+ Args:
521
+ query : str, optional
522
+ A search term to filter articles by title, body, or keywords.
523
+ If empty, all articles are returned without filtering (default is "").
524
+
525
+ lang : Literal["EN", "ES", "TR", "FR", "JP", "PT"], optional
526
+ Language in which to fetch news articles. Supported languages:
527
+ English (EN), Spanish (ES), Turkish (TR), French (FR), Japanese (JP), and Portuguese (PT).
528
+ Default is "EN".
529
+
530
+ limit : int, optional
531
+ Maximum number of articles to retrieve. Default is 50.
532
+
533
+ list_of_str : bool, optional
534
+ If True, returns a list of strings (concatenated article content).
535
+ If False, returns a list of filtered article dictionaries.
536
+ Default is False.
537
+
538
+ Returns:
539
+ List[str] | List[dict]
540
+ - If `query` is empty: returns a list of filtered article dictionaries.
541
+ - If `query` is provided:
542
+ - Returns a list of strings if `list_of_str=True`.
543
+ - Returns a list of filtered article dictionaries otherwise.
544
+
545
+ Each article dictionary contains the following fields:
546
+ - 'published_on': datetime of publication
547
+ - 'title': article headline
548
+ - 'subtitle': secondary headline
549
+ - 'url': direct link to the article
550
+ - 'body': article content
551
+ - 'keywords': associated tags
552
+ - 'sentiment': sentiment label
553
+ - 'status': publication status
554
+
555
+ Notes:
556
+ - Articles marked as sponsored are automatically excluded.
557
+ """
558
+ maximum = 100
559
+ if limit > maximum:
560
+ raise ValueError(f"Number of total news articles allowed is {maximum}")
561
+ try:
562
+ response = requests.get(
563
+ "https://data-api.coindesk.com/news/v1/article/list",
564
+ params={"lang": lang, "limit": limit},
565
+ headers={"Content-type": "application/json; charset=UTF-8"},
566
+ )
567
+ response.raise_for_status()
568
+ json_response = response.json()
569
+ except requests.exceptions.RequestException:
570
+ return []
571
+ if (
572
+ response.status_code != 200
573
+ or "Data" not in json_response
574
+ or len(json_response["Data"]) == 0
575
+ ):
576
+ return []
577
+ articles = json_response["Data"]
578
+ to_keep = [
579
+ "PUBLISHED_ON",
580
+ "TITLE",
581
+ "SUBTITLE",
582
+ "URL",
583
+ "BODY",
584
+ "KEYWORDS",
585
+ "SENTIMENT",
586
+ "STATUS",
587
+ ]
588
+ filtered_articles = []
589
+ for article in articles:
590
+ keys = article.keys()
591
+ filtered_articles.append(
592
+ {
593
+ k.lower(): article[k]
594
+ if k in keys and k != "PUBLISHED_ON"
595
+ else datetime.fromtimestamp(article[k])
596
+ for k in to_keep
597
+ if article[k] is not None and "sponsored" not in str(article[k])
598
+ }
599
+ )
600
+ if query == "" or len(filtered_articles) == 0:
601
+ return filtered_articles
602
+ to_return = []
603
+ query = _get_search_query(query)
604
+ for article in filtered_articles:
605
+ if not all(k in article for k in ("title", "body", "keywords")):
606
+ continue
607
+ text = article["title"] + " " + article["body"] + " " + article["keywords"]
608
+ if list_of_str and _find_news(query, text=text):
609
+ to_return.append(text)
610
+ if not list_of_str and _find_news(query, text=text):
611
+ to_return.append(article)
612
+ return to_return
613
+
614
+
615
+ class FmpData(Toolkit):
616
+ """
617
+ FMPData class for fetching data from Financial Modeling Prep API
618
+ using the Toolkit class from financetoolkit package.
619
+
620
+ See `financetoolkit` for more details.
621
+
622
+ """
623
+
624
+ def __init__(self, api_key: str = "", symbols: str | list = "AAPL"):
625
+ super().__init__(tickers=symbols, api_key=api_key)
626
+
627
+
628
+ class DataBendo: ...