bbstrader 0.3.6__py3-none-any.whl → 0.3.7__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 +1 -1
- bbstrader/__main__.py +2 -2
- bbstrader/apps/_copier.py +40 -37
- bbstrader/btengine/backtest.py +33 -28
- bbstrader/btengine/data.py +105 -81
- bbstrader/btengine/event.py +21 -22
- bbstrader/btengine/execution.py +51 -24
- bbstrader/btengine/performance.py +23 -12
- bbstrader/btengine/portfolio.py +40 -30
- bbstrader/btengine/scripts.py +13 -12
- bbstrader/btengine/strategy.py +288 -101
- bbstrader/compat.py +4 -3
- bbstrader/config.py +20 -36
- bbstrader/core/data.py +76 -48
- bbstrader/core/scripts.py +22 -21
- bbstrader/core/utils.py +13 -12
- bbstrader/trading/execution.py +6 -0
- bbstrader/tseries.py +55 -39
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/METADATA +3 -3
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/RECORD +25 -25
- tests/engine/test_portfolio.py +3 -2
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/top_level.txt +0 -0
bbstrader/compat.py
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import platform
|
|
2
2
|
import sys
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
def setup_mock_modules():
|
|
6
|
+
def setup_mock_modules() -> None:
|
|
6
7
|
"""Mock some modules not available on some OS to prevent import errors."""
|
|
7
8
|
from unittest.mock import MagicMock
|
|
8
9
|
|
|
9
10
|
class Mock(MagicMock):
|
|
10
11
|
@classmethod
|
|
11
|
-
def __getattr__(cls, name):
|
|
12
|
+
def __getattr__(cls, name: str) -> Any:
|
|
12
13
|
return MagicMock()
|
|
13
|
-
|
|
14
|
+
|
|
14
15
|
MOCK_MODULES = []
|
|
15
16
|
|
|
16
17
|
# Mock Metatrader5 on Linux and MacOS
|
bbstrader/config.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
from pathlib import Path
|
|
3
|
-
from typing import List
|
|
4
|
+
from typing import Any, List, Optional
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def get_config_dir(name: str = ".bbstrader") -> Path:
|
|
@@ -23,7 +24,7 @@ BBSTRADER_DIR = get_config_dir()
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class LogLevelFilter(logging.Filter):
|
|
26
|
-
def __init__(self, levels: List[int]):
|
|
27
|
+
def __init__(self, levels: List[int]) -> None:
|
|
27
28
|
"""
|
|
28
29
|
Initializes the filter with specific logging levels.
|
|
29
30
|
|
|
@@ -47,52 +48,35 @@ class LogLevelFilter(logging.Filter):
|
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
class CustomFormatter(logging.Formatter):
|
|
50
|
-
def formatTime(
|
|
51
|
+
def formatTime(
|
|
52
|
+
self, record: logging.LogRecord, datefmt: Optional[str] = None
|
|
53
|
+
) -> str:
|
|
51
54
|
if hasattr(record, "custom_time"):
|
|
52
55
|
# Use the custom time if provided
|
|
53
|
-
record.created = record.custom_time.timestamp()
|
|
56
|
+
record.created = record.custom_time.timestamp() # type: ignore
|
|
54
57
|
return super().formatTime(record, datefmt)
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
class CustomLogger(logging.Logger):
|
|
58
|
-
def __init__(self, name, level=logging.NOTSET):
|
|
61
|
+
def __init__(self, name: str, level: int = logging.NOTSET) -> None:
|
|
59
62
|
super().__init__(name, level)
|
|
60
63
|
|
|
61
|
-
def
|
|
64
|
+
def log(
|
|
62
65
|
self,
|
|
63
|
-
level,
|
|
64
|
-
msg,
|
|
65
|
-
args,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
stacklevel=1,
|
|
70
|
-
custom_time=None,
|
|
71
|
-
):
|
|
72
|
-
if extra is None:
|
|
73
|
-
extra = {}
|
|
74
|
-
# Add custom_time to the extra dictionary if provided
|
|
66
|
+
level: int,
|
|
67
|
+
msg: object,
|
|
68
|
+
*args: object,
|
|
69
|
+
custom_time: Optional[datetime] = None,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> None:
|
|
75
72
|
if custom_time:
|
|
76
|
-
extra["
|
|
77
|
-
|
|
73
|
+
if "extra" not in kwargs or kwargs["extra"] is None:
|
|
74
|
+
kwargs["extra"] = {}
|
|
75
|
+
kwargs["extra"]["custom_time"] = custom_time
|
|
76
|
+
super().log(level, msg, *args, **kwargs)
|
|
78
77
|
|
|
79
|
-
def info(self, msg, *args, custom_time=None, **kwargs):
|
|
80
|
-
self._log(logging.INFO, msg, args, custom_time=custom_time, **kwargs)
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
self._log(logging.DEBUG, msg, args, custom_time=custom_time, **kwargs)
|
|
84
|
-
|
|
85
|
-
def warning(self, msg, *args, custom_time=None, **kwargs):
|
|
86
|
-
self._log(logging.WARNING, msg, args, custom_time=custom_time, **kwargs)
|
|
87
|
-
|
|
88
|
-
def error(self, msg, *args, custom_time=None, **kwargs):
|
|
89
|
-
self._log(logging.ERROR, msg, args, custom_time=custom_time, **kwargs)
|
|
90
|
-
|
|
91
|
-
def critical(self, msg, *args, custom_time=None, **kwargs):
|
|
92
|
-
self._log(logging.CRITICAL, msg, args, custom_time=custom_time, **kwargs)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def config_logger(log_file: str, console_log=True):
|
|
79
|
+
def config_logger(log_file: str, console_log: bool = True) -> logging.Logger:
|
|
96
80
|
# Use the CustomLogger
|
|
97
81
|
logging.setLoggerClass(CustomLogger)
|
|
98
82
|
logger = logging.getLogger(__name__)
|
bbstrader/core/data.py
CHANGED
|
@@ -2,7 +2,7 @@ import json
|
|
|
2
2
|
import re
|
|
3
3
|
import ssl
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from typing import List, Literal
|
|
5
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
6
6
|
from urllib.request import urlopen
|
|
7
7
|
|
|
8
8
|
import certifi
|
|
@@ -27,7 +27,7 @@ def _get_search_query(query: str) -> str:
|
|
|
27
27
|
return query
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def _find_news(query: str
|
|
30
|
+
def _find_news(query: Union[str, List[str]], text: str) -> bool:
|
|
31
31
|
if isinstance(query, str):
|
|
32
32
|
query = query.split(" ")
|
|
33
33
|
pattern = r"\b(?:" + "|".join(map(re.escape, query)) + r")\b"
|
|
@@ -36,7 +36,7 @@ def _find_news(query: str | List[str], text):
|
|
|
36
36
|
return False
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def _filter_news(news: List[str], query: str
|
|
39
|
+
def _filter_news(news: List[str], query: Union[str, List[str]]) -> List[str]:
|
|
40
40
|
return [text for text in news if _find_news(query, text)]
|
|
41
41
|
|
|
42
42
|
|
|
@@ -48,7 +48,7 @@ class FmpNews(object):
|
|
|
48
48
|
as well as financial articles and press releases.
|
|
49
49
|
"""
|
|
50
50
|
|
|
51
|
-
def __init__(self, api: str):
|
|
51
|
+
def __init__(self, api: str) -> None:
|
|
52
52
|
"""
|
|
53
53
|
Args:
|
|
54
54
|
api (str): The API key for accessing FMP's news data.
|
|
@@ -60,13 +60,15 @@ class FmpNews(object):
|
|
|
60
60
|
raise ValueError("API key is required For FmpNews")
|
|
61
61
|
self.__api = api
|
|
62
62
|
|
|
63
|
-
def _jsonparsed_data(self, url):
|
|
63
|
+
def _jsonparsed_data(self, url: str) -> Any:
|
|
64
64
|
context = ssl.create_default_context(cafile=certifi.where())
|
|
65
65
|
with urlopen(url, context=context) as response:
|
|
66
66
|
data = response.read().decode("utf-8")
|
|
67
67
|
return json.loads(data)
|
|
68
68
|
|
|
69
|
-
def _load_news(
|
|
69
|
+
def _load_news(
|
|
70
|
+
self, news_type: str, symbol: Optional[str] = None, **kwargs: Any
|
|
71
|
+
) -> List[Dict[str, Any]]:
|
|
70
72
|
params = {"start": "from", "end": "to", "page": "page", "limit": "limit"}
|
|
71
73
|
base_url = f"https://financialmodelingprep.com/stable/news/{news_type}-latest?apikey={self.__api}"
|
|
72
74
|
if news_type == "articles":
|
|
@@ -81,8 +83,8 @@ class FmpNews(object):
|
|
|
81
83
|
|
|
82
84
|
return self._jsonparsed_data(base_url)
|
|
83
85
|
|
|
84
|
-
def get_articles(self, **kwargs) -> List[
|
|
85
|
-
def html_parser(content):
|
|
86
|
+
def get_articles(self, **kwargs: Any) -> List[Dict[str, Any]]:
|
|
87
|
+
def html_parser(content: str) -> str:
|
|
86
88
|
soup = BeautifulSoup(content, "html.parser")
|
|
87
89
|
text = soup.get_text(separator="\n")
|
|
88
90
|
return text.replace("\n", "")
|
|
@@ -91,29 +93,39 @@ class FmpNews(object):
|
|
|
91
93
|
df = pd.DataFrame(articles)
|
|
92
94
|
df = df[["title", "date", "content", "tickers"]]
|
|
93
95
|
df["content"] = df["content"].apply(html_parser)
|
|
94
|
-
return df.to_dict(orient="records")
|
|
96
|
+
return df.to_dict(orient="records") # type: ignore
|
|
95
97
|
|
|
96
|
-
def get_releases(
|
|
98
|
+
def get_releases(
|
|
99
|
+
self, symbol: Optional[str] = None, **kwargs: Any
|
|
100
|
+
) -> List[Dict[str, Any]]:
|
|
97
101
|
return self._load_news("press-releases", symbol, **kwargs)
|
|
98
102
|
|
|
99
|
-
def get_stock_news(
|
|
103
|
+
def get_stock_news(
|
|
104
|
+
self, symbol: Optional[str] = None, **kwargs: Any
|
|
105
|
+
) -> List[Dict[str, Any]]:
|
|
100
106
|
return self._load_news("stock", symbol, **kwargs)
|
|
101
107
|
|
|
102
|
-
def get_crypto_news(
|
|
108
|
+
def get_crypto_news(
|
|
109
|
+
self, symbol: Optional[str] = None, **kwargs: Any
|
|
110
|
+
) -> List[Dict[str, Any]]:
|
|
103
111
|
return self._load_news("crypto", symbol, **kwargs)
|
|
104
112
|
|
|
105
|
-
def get_forex_news(
|
|
113
|
+
def get_forex_news(
|
|
114
|
+
self, symbol: Optional[str] = None, **kwargs: Any
|
|
115
|
+
) -> List[Dict[str, Any]]:
|
|
106
116
|
return self._load_news("forex", symbol, **kwargs)
|
|
107
117
|
|
|
108
|
-
def _last_date(self, date):
|
|
118
|
+
def _last_date(self, date: str) -> datetime:
|
|
109
119
|
return datetime.strptime(date, "%Y-%m-%d %H:%M:%S")
|
|
110
120
|
|
|
111
|
-
def parse_news(
|
|
121
|
+
def parse_news(
|
|
122
|
+
self, news: List[Dict[str, Any]], symbol: Optional[str] = None, **kwargs: Any
|
|
123
|
+
) -> List[str]:
|
|
112
124
|
start = kwargs.get("start")
|
|
113
125
|
end = kwargs.get("end")
|
|
114
126
|
end_date = self._last_date(end) if end is not None else datetime.now().date()
|
|
115
127
|
|
|
116
|
-
def parse_record(record):
|
|
128
|
+
def parse_record(record: Dict[str, Any]) -> str:
|
|
117
129
|
return " ".join(
|
|
118
130
|
[
|
|
119
131
|
record.pop("symbol", ""),
|
|
@@ -127,7 +139,7 @@ class FmpNews(object):
|
|
|
127
139
|
parsed_news = []
|
|
128
140
|
for record in news:
|
|
129
141
|
date = record.get("publishedDate")
|
|
130
|
-
published_date = self._last_date(record.get("date", date)).date()
|
|
142
|
+
published_date = self._last_date(record.get("date", date)).date() # type: ignore
|
|
131
143
|
start_date = (
|
|
132
144
|
self._last_date(start).date() if start is not None else published_date
|
|
133
145
|
)
|
|
@@ -141,18 +153,23 @@ class FmpNews(object):
|
|
|
141
153
|
parsed_news.append(parse_record(record))
|
|
142
154
|
return parsed_news
|
|
143
155
|
|
|
144
|
-
def get_latest_articles(
|
|
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]]:
|
|
145
162
|
end = kwargs.get("end")
|
|
146
163
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
147
164
|
end_date = self._last_date(end) if end is not None else self._last_date(now)
|
|
148
165
|
if articles is None:
|
|
149
166
|
try:
|
|
150
|
-
articles = pd.read_csv("latest_fmp_articles.csv")
|
|
151
|
-
articles = articles.to_dict(orient="records")
|
|
152
|
-
if self._last_date(articles[0]["date"]).hour < end_date.hour:
|
|
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
|
|
153
170
|
articles = self.get_articles(**kwargs)
|
|
154
171
|
else:
|
|
155
|
-
return articles
|
|
172
|
+
return articles # type: ignore
|
|
156
173
|
except FileNotFoundError:
|
|
157
174
|
articles = self.get_articles(**kwargs)
|
|
158
175
|
|
|
@@ -162,8 +179,13 @@ class FmpNews(object):
|
|
|
162
179
|
return articles
|
|
163
180
|
|
|
164
181
|
def get_news(
|
|
165
|
-
self,
|
|
166
|
-
|
|
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]:
|
|
167
189
|
"""
|
|
168
190
|
Retrieves relevant financial news based on the specified source.
|
|
169
191
|
|
|
@@ -201,7 +223,7 @@ class FmpNews(object):
|
|
|
201
223
|
news_source = source_methods.get(source, lambda: [])()
|
|
202
224
|
if source == "articles":
|
|
203
225
|
symbol = None # Articles do not require a symbol filter
|
|
204
|
-
news = self.parse_news(news_source, symbol=symbol)
|
|
226
|
+
news = self.parse_news(news_source, symbol=symbol, **kwargs)
|
|
205
227
|
return _filter_news(news, query)
|
|
206
228
|
|
|
207
229
|
|
|
@@ -213,7 +235,9 @@ class FinancialNews(object):
|
|
|
213
235
|
|
|
214
236
|
"""
|
|
215
237
|
|
|
216
|
-
def _fetch_news(
|
|
238
|
+
def _fetch_news(
|
|
239
|
+
self, url: str, query: str, n_news: int, headline_tag: str
|
|
240
|
+
) -> List[str]:
|
|
217
241
|
headers = {"User-Agent": "Mozilla/5.0"}
|
|
218
242
|
try:
|
|
219
243
|
response = requests.get(url, headers=headers)
|
|
@@ -234,7 +258,9 @@ class FinancialNews(object):
|
|
|
234
258
|
]
|
|
235
259
|
return headlines[:n_news]
|
|
236
260
|
|
|
237
|
-
def get_yahoo_finance_news(
|
|
261
|
+
def get_yahoo_finance_news(
|
|
262
|
+
self, query: str, asset_type: str = "stock", n_news: int = 10
|
|
263
|
+
) -> List[str]:
|
|
238
264
|
"""
|
|
239
265
|
Fetches recent Yahoo Finance news headlines for a given financial asset.
|
|
240
266
|
|
|
@@ -257,17 +283,17 @@ class FinancialNews(object):
|
|
|
257
283
|
list[str]: A list of Yahoo Finance news headlines relevant to the query.
|
|
258
284
|
"""
|
|
259
285
|
if asset_type == "forex" or asset_type == "future":
|
|
260
|
-
assert (
|
|
261
|
-
"=
|
|
262
|
-
)
|
|
286
|
+
assert "=" in query, (
|
|
287
|
+
"Forex query must contain '=' for currency pairs (e.g., EURUSD=X, CL=F)"
|
|
288
|
+
)
|
|
263
289
|
if asset_type == "crypto":
|
|
264
|
-
assert (
|
|
265
|
-
"-
|
|
266
|
-
)
|
|
290
|
+
assert "-" in query, (
|
|
291
|
+
"Crypto query must contain '-' for crypto pairs (e.g., BTC-USD, ETH-USD)"
|
|
292
|
+
)
|
|
267
293
|
if asset_type == "index":
|
|
268
|
-
assert query.startswith(
|
|
269
|
-
"^"
|
|
270
|
-
)
|
|
294
|
+
assert query.startswith("^"), (
|
|
295
|
+
"Index query must start with '^' (e.g., ^GSPC for S&P 500)"
|
|
296
|
+
)
|
|
271
297
|
url = (
|
|
272
298
|
f"https://finance.yahoo.com/quote/{query}/news"
|
|
273
299
|
if asset_type in ["stock", "etf", "index", "future", "forex"]
|
|
@@ -275,7 +301,9 @@ class FinancialNews(object):
|
|
|
275
301
|
)
|
|
276
302
|
return self._fetch_news(url, query, n_news, "h3")
|
|
277
303
|
|
|
278
|
-
def get_google_finance_news(
|
|
304
|
+
def get_google_finance_news(
|
|
305
|
+
self, query: str, asset_type: str = "stock", n_news: int = 10
|
|
306
|
+
) -> List[str]:
|
|
279
307
|
"""
|
|
280
308
|
Fetches recent Google Finance news headlines for a given financial asset.
|
|
281
309
|
|
|
@@ -403,14 +431,14 @@ class FinancialNews(object):
|
|
|
403
431
|
|
|
404
432
|
def get_twitter_posts(
|
|
405
433
|
self,
|
|
406
|
-
query,
|
|
407
|
-
asset_type="stock",
|
|
408
|
-
bearer=None,
|
|
409
|
-
api_key=None,
|
|
410
|
-
api_secret=None,
|
|
411
|
-
access_token=None,
|
|
412
|
-
access_secret=None,
|
|
413
|
-
n_posts=10,
|
|
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,
|
|
414
442
|
) -> List[str]:
|
|
415
443
|
"""
|
|
416
444
|
Fetches recent tweets related to a financial asset.
|
|
@@ -471,13 +499,13 @@ class FinancialNews(object):
|
|
|
471
499
|
query=search, max_results=100, tweet_fields=["text"]
|
|
472
500
|
)
|
|
473
501
|
query = _get_search_query(query)
|
|
474
|
-
news = [tweet.text for tweet in tweets.data] if tweets.data else []
|
|
502
|
+
news = [tweet.text for tweet in tweets.data] if tweets.data else [] # type: ignore
|
|
475
503
|
return _filter_news(news, query)[:n_posts]
|
|
476
504
|
except tweepy.TweepyException:
|
|
477
505
|
return []
|
|
478
506
|
|
|
479
|
-
def get_fmp_news(self, api=None) -> FmpNews:
|
|
480
|
-
return FmpNews(api=api)
|
|
507
|
+
def get_fmp_news(self, api:str |None =None) -> FmpNews:
|
|
508
|
+
return FmpNews(api=api) # type: ignore
|
|
481
509
|
|
|
482
510
|
def get_coindesk_news(
|
|
483
511
|
self,
|
bbstrader/core/scripts.py
CHANGED
|
@@ -4,7 +4,7 @@ import sys
|
|
|
4
4
|
import textwrap
|
|
5
5
|
import time
|
|
6
6
|
from datetime import datetime, timedelta
|
|
7
|
-
from typing import List, Literal
|
|
7
|
+
from typing import Any, Coroutine, Dict, List, Literal
|
|
8
8
|
|
|
9
9
|
import nltk
|
|
10
10
|
from loguru import logger
|
|
@@ -16,7 +16,7 @@ from bbstrader.core.data import FinancialNews
|
|
|
16
16
|
from bbstrader.trading.utils import send_telegram_message
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def summarize_text(text, sentences_count=5):
|
|
19
|
+
def summarize_text(text: str, sentences_count: int = 5) -> str:
|
|
20
20
|
"""
|
|
21
21
|
Generate a summary using TextRank algorithm.
|
|
22
22
|
"""
|
|
@@ -26,7 +26,7 @@ def summarize_text(text, sentences_count=5):
|
|
|
26
26
|
return " ".join(str(sentence) for sentence in summary)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def format_coindesk_article(article:
|
|
29
|
+
def format_coindesk_article(article: Dict[str, Any]) -> str:
|
|
30
30
|
if not all(
|
|
31
31
|
k in article
|
|
32
32
|
for k in (
|
|
@@ -35,7 +35,7 @@ def format_coindesk_article(article: dict) -> str:
|
|
|
35
35
|
"published_on",
|
|
36
36
|
"sentiment",
|
|
37
37
|
"keywords",
|
|
38
|
-
"
|
|
38
|
+
"status",
|
|
39
39
|
"url",
|
|
40
40
|
)
|
|
41
41
|
):
|
|
@@ -54,7 +54,7 @@ def format_coindesk_article(article: dict) -> str:
|
|
|
54
54
|
return text
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def format_fmp_article(article:
|
|
57
|
+
def format_fmp_article(article: Dict[str, Any]) -> str:
|
|
58
58
|
if not all(k in article for k in ("title", "date", "content", "tickers")):
|
|
59
59
|
return ""
|
|
60
60
|
summary = summarize_text(article["content"])
|
|
@@ -69,28 +69,29 @@ def format_fmp_article(article: dict) -> str:
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
async def send_articles(
|
|
72
|
-
articles: List[
|
|
72
|
+
articles: List[Dict[str, Any]],
|
|
73
73
|
token: str,
|
|
74
74
|
id: str,
|
|
75
75
|
source: Literal["coindesk", "fmp"],
|
|
76
|
-
interval=15,
|
|
77
|
-
):
|
|
76
|
+
interval: int = 15,
|
|
77
|
+
) -> None:
|
|
78
78
|
for article in articles:
|
|
79
79
|
message = ""
|
|
80
80
|
if source == "coindesk":
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
published_on = article.get("published_on")
|
|
82
|
+
if isinstance(
|
|
83
|
+
published_on, datetime
|
|
84
|
+
) and published_on >= datetime.now() - timedelta(minutes=interval):
|
|
85
|
+
article["published_on"] = published_on.strftime("%Y-%m-%d %H:%M:%S")
|
|
86
|
+
message = format_coindesk_article(article)
|
|
86
87
|
else:
|
|
87
88
|
message = format_fmp_article(article)
|
|
88
89
|
if message == "":
|
|
89
|
-
|
|
90
|
+
continue
|
|
90
91
|
await send_telegram_message(token, id, text=message)
|
|
91
92
|
|
|
92
93
|
|
|
93
|
-
def send_news_feed(unknown):
|
|
94
|
+
def send_news_feed(unknown: List[str]) -> None:
|
|
94
95
|
HELP_MSG = """
|
|
95
96
|
Send news feed from Coindesk to Telegram channel.
|
|
96
97
|
This script fetches the latest news articles from Coindesk, summarizes them,
|
|
@@ -145,19 +146,19 @@ def send_news_feed(unknown):
|
|
|
145
146
|
logger.info(f"Starting the News Feed on {args.interval} minutes")
|
|
146
147
|
while True:
|
|
147
148
|
try:
|
|
148
|
-
fmp_articles = []
|
|
149
|
+
fmp_articles: List[Dict[str, Any]] = []
|
|
149
150
|
if fmp_news is not None:
|
|
150
151
|
start = datetime.now() - timedelta(minutes=args.interval)
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
start_str = start.strftime("%Y-%m-%d %H:%M:%S")
|
|
153
|
+
end_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
153
154
|
fmp_articles = fmp_news.get_latest_articles(
|
|
154
|
-
save=True, start=
|
|
155
|
+
save=True, start=start_str, end=end_str
|
|
155
156
|
)
|
|
156
157
|
coindesk_articles = news.get_coindesk_news(query=args.query)
|
|
157
|
-
if
|
|
158
|
+
if coindesk_articles and isinstance(coindesk_articles, list):
|
|
158
159
|
asyncio.run(
|
|
159
160
|
send_articles(
|
|
160
|
-
coindesk_articles,
|
|
161
|
+
coindesk_articles, # type: ignore
|
|
161
162
|
args.token,
|
|
162
163
|
args.id,
|
|
163
164
|
"coindesk",
|
bbstrader/core/utils.py
CHANGED
|
@@ -2,17 +2,16 @@ import configparser
|
|
|
2
2
|
import importlib
|
|
3
3
|
import importlib.util
|
|
4
4
|
import os
|
|
5
|
-
from
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
from typing import Any, Dict, List, Optional, Type, Union
|
|
6
7
|
|
|
7
8
|
__all__ = ["load_module", "load_class"]
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
def load_module(file_path):
|
|
11
|
+
def load_module(file_path: str) -> ModuleType:
|
|
11
12
|
"""Load a module from a file path.
|
|
12
|
-
|
|
13
13
|
Args:
|
|
14
14
|
file_path: Path to the file to load.
|
|
15
|
-
|
|
16
15
|
Returns:
|
|
17
16
|
The loaded module.
|
|
18
17
|
"""
|
|
@@ -21,14 +20,15 @@ def load_module(file_path):
|
|
|
21
20
|
f"Strategy file {file_path} not found. Please create it."
|
|
22
21
|
)
|
|
23
22
|
spec = importlib.util.spec_from_file_location("bbstrader.cli", file_path)
|
|
23
|
+
if spec is None or spec.loader is None:
|
|
24
|
+
raise ImportError(f"Could not load spec for module at {file_path}")
|
|
24
25
|
module = importlib.util.module_from_spec(spec)
|
|
25
26
|
spec.loader.exec_module(module)
|
|
26
27
|
return module
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def load_class(module, class_name, base_class):
|
|
30
|
+
def load_class(module: ModuleType, class_name: str, base_class: Type) -> Type:
|
|
30
31
|
"""Load a class from a module.
|
|
31
|
-
|
|
32
32
|
Args:
|
|
33
33
|
module: The module to load the class from.
|
|
34
34
|
class_name: The name of the class to load.
|
|
@@ -42,7 +42,7 @@ def load_class(module, class_name, base_class):
|
|
|
42
42
|
return class_
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def auto_convert(value):
|
|
45
|
+
def auto_convert(value: str) -> Union[bool, None, int, float, str]:
|
|
46
46
|
"""Convert string values to appropriate data types"""
|
|
47
47
|
if value.lower() in {"true", "false"}: # Boolean
|
|
48
48
|
return value.lower() == "true"
|
|
@@ -56,13 +56,13 @@ def auto_convert(value):
|
|
|
56
56
|
return value
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def dict_from_ini(
|
|
59
|
+
def dict_from_ini(
|
|
60
|
+
file_path: str, sections: Optional[Union[str, List[str]]] = None
|
|
61
|
+
) -> Dict[str, Any]:
|
|
60
62
|
"""Reads an INI file and converts it to a dictionary with proper data types.
|
|
61
|
-
|
|
62
63
|
Args:
|
|
63
64
|
file_path: Path to the INI file to read.
|
|
64
65
|
sections: Optional list of sections to read from the INI file.
|
|
65
|
-
|
|
66
66
|
Returns:
|
|
67
67
|
A dictionary containing the INI file contents with proper data types.
|
|
68
68
|
"""
|
|
@@ -71,7 +71,7 @@ def dict_from_ini(file_path, sections: str | List[str] = None) -> Dict[str, Any]
|
|
|
71
71
|
config.read(file_path)
|
|
72
72
|
except Exception:
|
|
73
73
|
raise
|
|
74
|
-
ini_dict = {}
|
|
74
|
+
ini_dict: Dict[str, Any] = {}
|
|
75
75
|
for section in config.sections():
|
|
76
76
|
ini_dict[section] = {
|
|
77
77
|
key: auto_convert(value) for key, value in config.items(section)
|
|
@@ -83,10 +83,11 @@ def dict_from_ini(file_path, sections: str | List[str] = None) -> Dict[str, Any]
|
|
|
83
83
|
except KeyError:
|
|
84
84
|
raise KeyError(f"{sections} not found in the {file_path} file")
|
|
85
85
|
if isinstance(sections, list):
|
|
86
|
-
sect_dict = {}
|
|
86
|
+
sect_dict: Dict[str, Any] = {}
|
|
87
87
|
for section in sections:
|
|
88
88
|
try:
|
|
89
89
|
sect_dict[section] = ini_dict[section]
|
|
90
90
|
except KeyError:
|
|
91
91
|
raise KeyError(f"{section} not found in the {file_path} file")
|
|
92
|
+
return sect_dict
|
|
92
93
|
return ini_dict
|
bbstrader/trading/execution.py
CHANGED
|
@@ -971,11 +971,17 @@ class Mt5ExecutionEngine:
|
|
|
971
971
|
msg = f"Handling period end actions, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
972
972
|
self._print_exc(msg, e)
|
|
973
973
|
pass
|
|
974
|
+
|
|
975
|
+
def select_symbols(self):
|
|
976
|
+
for symbol in self.symbols:
|
|
977
|
+
if not MT5.symbol_select(symbol, True):
|
|
978
|
+
logger.error(f"Failed to select symbol {symbol} error = {MT5.last_error()}")
|
|
974
979
|
|
|
975
980
|
def run(self):
|
|
976
981
|
while self._running and not self.shutdown_event.is_set():
|
|
977
982
|
try:
|
|
978
983
|
check_mt5_connection(**self.kwargs)
|
|
984
|
+
self.select_symbols()
|
|
979
985
|
positions_orders = self._check_positions_orders()
|
|
980
986
|
if self.show_positions_orders:
|
|
981
987
|
self._display_positions_orders(positions_orders)
|