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.
- bbstrader/__init__.py +27 -0
- bbstrader/__main__.py +92 -0
- bbstrader/api/__init__.py +96 -0
- bbstrader/api/handlers.py +245 -0
- bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
- bbstrader/api/metatrader_client.pyi +624 -0
- bbstrader/assets/bbs_.png +0 -0
- bbstrader/assets/bbstrader.ico +0 -0
- bbstrader/assets/bbstrader.png +0 -0
- bbstrader/assets/qs_metrics_1.png +0 -0
- bbstrader/btengine/__init__.py +54 -0
- bbstrader/btengine/backtest.py +358 -0
- bbstrader/btengine/data.py +737 -0
- bbstrader/btengine/event.py +229 -0
- bbstrader/btengine/execution.py +287 -0
- bbstrader/btengine/performance.py +408 -0
- bbstrader/btengine/portfolio.py +393 -0
- bbstrader/btengine/strategy.py +588 -0
- bbstrader/compat.py +28 -0
- bbstrader/config.py +100 -0
- bbstrader/core/__init__.py +27 -0
- bbstrader/core/data.py +628 -0
- bbstrader/core/strategy.py +466 -0
- bbstrader/metatrader/__init__.py +48 -0
- bbstrader/metatrader/_copier.py +720 -0
- bbstrader/metatrader/account.py +865 -0
- bbstrader/metatrader/broker.py +418 -0
- bbstrader/metatrader/copier.py +1487 -0
- bbstrader/metatrader/rates.py +495 -0
- bbstrader/metatrader/risk.py +667 -0
- bbstrader/metatrader/trade.py +1692 -0
- bbstrader/metatrader/utils.py +402 -0
- bbstrader/models/__init__.py +39 -0
- bbstrader/models/nlp.py +932 -0
- bbstrader/models/optimization.py +182 -0
- bbstrader/scripts.py +665 -0
- bbstrader/trading/__init__.py +33 -0
- bbstrader/trading/execution.py +1159 -0
- bbstrader/trading/strategy.py +362 -0
- bbstrader/trading/utils.py +69 -0
- bbstrader-2.0.3.dist-info/METADATA +396 -0
- bbstrader-2.0.3.dist-info/RECORD +45 -0
- bbstrader-2.0.3.dist-info/WHEEL +5 -0
- bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
- 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: ...
|