bbstrader 0.3.2__py3-none-any.whl → 0.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bbstrader might be problematic. Click here for more details.
- bbstrader/__main__.py +15 -7
- bbstrader/apps/__init__.py +0 -0
- bbstrader/apps/_copier.py +664 -0
- bbstrader/btengine/data.py +3 -3
- bbstrader/btengine/strategy.py +165 -90
- bbstrader/core/data.py +3 -1
- bbstrader/core/scripts.py +62 -19
- bbstrader/core/utils.py +5 -3
- bbstrader/metatrader/account.py +196 -42
- bbstrader/metatrader/analysis.py +7 -5
- bbstrader/metatrader/copier.py +325 -171
- bbstrader/metatrader/rates.py +2 -2
- bbstrader/metatrader/scripts.py +15 -2
- bbstrader/metatrader/trade.py +2 -2
- bbstrader/metatrader/utils.py +65 -5
- bbstrader/models/ml.py +8 -5
- bbstrader/models/nlp.py +16 -11
- bbstrader/trading/execution.py +100 -48
- bbstrader/tseries.py +0 -2
- {bbstrader-0.3.2.dist-info → bbstrader-0.3.4.dist-info}/METADATA +5 -5
- {bbstrader-0.3.2.dist-info → bbstrader-0.3.4.dist-info}/RECORD +25 -23
- {bbstrader-0.3.2.dist-info → bbstrader-0.3.4.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.2.dist-info → bbstrader-0.3.4.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.2.dist-info → bbstrader-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.2.dist-info → bbstrader-0.3.4.dist-info}/top_level.txt +0 -0
bbstrader/btengine/strategy.py
CHANGED
|
@@ -11,10 +11,12 @@ from loguru import logger
|
|
|
11
11
|
|
|
12
12
|
from bbstrader.btengine.data import DataHandler
|
|
13
13
|
from bbstrader.btengine.event import Events, FillEvent, SignalEvent
|
|
14
|
+
from bbstrader.metatrader.trade import generate_signal, TradeAction
|
|
14
15
|
from bbstrader.config import BBSTRADER_DIR
|
|
15
16
|
from bbstrader.metatrader import (
|
|
16
17
|
Account,
|
|
17
18
|
AdmiralMarktsGroup,
|
|
19
|
+
MetaQuotes,
|
|
18
20
|
PepperstoneGroupLimited,
|
|
19
21
|
TradeOrder,
|
|
20
22
|
Rates,
|
|
@@ -78,7 +80,16 @@ class MT5Strategy(Strategy):
|
|
|
78
80
|
in order to avoid naming collusion.
|
|
79
81
|
"""
|
|
80
82
|
tf: str
|
|
83
|
+
id: int
|
|
84
|
+
ID: int
|
|
85
|
+
|
|
81
86
|
max_trades: Dict[str, int]
|
|
87
|
+
risk_budget: Dict[str, float] | str | None
|
|
88
|
+
|
|
89
|
+
_orders: Dict[str, Dict[str, List[SignalEvent]]]
|
|
90
|
+
_positions: Dict[str, Dict[str, int | float]]
|
|
91
|
+
_trades: Dict[str, Dict[str, int]]
|
|
92
|
+
|
|
82
93
|
def __init__(
|
|
83
94
|
self,
|
|
84
95
|
events: Queue = None,
|
|
@@ -104,13 +115,19 @@ class MT5Strategy(Strategy):
|
|
|
104
115
|
self.data = bars
|
|
105
116
|
self.symbols = symbol_list
|
|
106
117
|
self.mode = mode
|
|
107
|
-
self.
|
|
118
|
+
if self.mode not in [TradingMode.BACKTEST, TradingMode.LIVE]:
|
|
119
|
+
raise ValueError(f"Mode must be an instance of {type(TradingMode)} not {type(self.mode)}")
|
|
120
|
+
|
|
108
121
|
self.risk_budget = self._check_risk_budget(**kwargs)
|
|
122
|
+
|
|
109
123
|
self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
|
|
110
124
|
self.tf = kwargs.get("time_frame", "D1")
|
|
111
125
|
self.logger = kwargs.get("logger") or logger
|
|
126
|
+
|
|
112
127
|
if self.mode == TradingMode.BACKTEST:
|
|
128
|
+
self._porfolio_value = None
|
|
113
129
|
self._initialize_portfolio()
|
|
130
|
+
|
|
114
131
|
self.kwargs = kwargs
|
|
115
132
|
self.periodes = 0
|
|
116
133
|
|
|
@@ -170,19 +187,17 @@ class MT5Strategy(Strategy):
|
|
|
170
187
|
return weights
|
|
171
188
|
|
|
172
189
|
def _initialize_portfolio(self):
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
self.
|
|
176
|
-
self._positions: Dict[str, Dict[str, int | float]] = {}
|
|
177
|
-
self._trades: Dict[str, Dict[str, int]] = {}
|
|
190
|
+
self._orders = {}
|
|
191
|
+
self._positions = {}
|
|
192
|
+
self._trades = {}
|
|
178
193
|
for symbol in self.symbols:
|
|
179
194
|
self._positions[symbol] = {}
|
|
180
195
|
self._orders[symbol] = {}
|
|
181
196
|
self._trades[symbol] = {}
|
|
182
|
-
for position in
|
|
197
|
+
for position in ["LONG", "SHORT"]:
|
|
183
198
|
self._trades[symbol][position] = 0
|
|
184
199
|
self._positions[symbol][position] = 0.0
|
|
185
|
-
for order in
|
|
200
|
+
for order in ["BLMT", "BSTP", "BSTPLMT", "SLMT", "SSTP", "SSTPLMT"]:
|
|
186
201
|
self._orders[symbol][order] = []
|
|
187
202
|
self._holdings = {s: 0.0 for s in self.symbols}
|
|
188
203
|
|
|
@@ -236,6 +251,54 @@ class MT5Strategy(Strategy):
|
|
|
236
251
|
"""
|
|
237
252
|
pass
|
|
238
253
|
|
|
254
|
+
def signal(self, signal: int, symbol: str) -> TradeSignal:
|
|
255
|
+
"""
|
|
256
|
+
Generate a ``TradeSignal`` object based on the signal value.
|
|
257
|
+
Args:
|
|
258
|
+
signal : An integer value representing the signal type:
|
|
259
|
+
0: BUY
|
|
260
|
+
1: SELL
|
|
261
|
+
2: EXIT_LONG
|
|
262
|
+
3: EXIT_SHORT
|
|
263
|
+
4: EXIT_ALL_POSITIONS
|
|
264
|
+
5: EXIT_ALL_ORDERS
|
|
265
|
+
6: EXIT_STOP
|
|
266
|
+
7: EXIT_LIMIT
|
|
267
|
+
|
|
268
|
+
symbol : The symbol for the trade.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
TradeSignal : A ``TradeSignal`` object representing the trade signal.
|
|
272
|
+
|
|
273
|
+
Note:
|
|
274
|
+
This generate only common signals. For more complex signals, use `generate_signal` directly.
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
ValueError : If the signal value is not between 0 and 7.
|
|
278
|
+
"""
|
|
279
|
+
signal_id = getattr(self, "id", None) or getattr(self, "ID")
|
|
280
|
+
|
|
281
|
+
match signal:
|
|
282
|
+
case 0:
|
|
283
|
+
return generate_signal(signal_id, symbol, TradeAction.BUY)
|
|
284
|
+
case 1:
|
|
285
|
+
return generate_signal(signal_id, symbol, TradeAction.SELL)
|
|
286
|
+
case 2:
|
|
287
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_LONG)
|
|
288
|
+
case 3:
|
|
289
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_SHORT)
|
|
290
|
+
case 4:
|
|
291
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_ALL_POSITIONS)
|
|
292
|
+
case 5:
|
|
293
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_ALL_ORDERS)
|
|
294
|
+
case 6:
|
|
295
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_STOP)
|
|
296
|
+
case 7:
|
|
297
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_LIMIT)
|
|
298
|
+
case _:
|
|
299
|
+
raise ValueError(f"Invalid signal value: {signal}. Must be an integer between 0 and 7.")
|
|
300
|
+
|
|
301
|
+
|
|
239
302
|
def perform_period_end_checks(self, *args, **kwargs):
|
|
240
303
|
"""
|
|
241
304
|
Some strategies may require additional checks at the end of the period,
|
|
@@ -551,75 +614,75 @@ class MT5Strategy(Strategy):
|
|
|
551
614
|
]
|
|
552
615
|
logmsg(order, log_label, symbol, dtime)
|
|
553
616
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
617
|
+
for symbol in self.symbols:
|
|
618
|
+
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
619
|
+
latest_close = self.data.get_latest_bar_value(symbol, "close")
|
|
620
|
+
|
|
621
|
+
process_orders(
|
|
622
|
+
"BLMT",
|
|
623
|
+
lambda o: latest_close <= o.price,
|
|
624
|
+
lambda o: self.buy_mkt(
|
|
625
|
+
o.strategy_id, symbol, o.price, o.quantity, dtime
|
|
626
|
+
),
|
|
627
|
+
"BUY LIMIT",
|
|
628
|
+
symbol,
|
|
629
|
+
dtime,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
process_orders(
|
|
633
|
+
"SLMT",
|
|
634
|
+
lambda o: latest_close >= o.price,
|
|
635
|
+
lambda o: self.sell_mkt(
|
|
636
|
+
o.strategy_id, symbol, o.price, o.quantity, dtime
|
|
637
|
+
),
|
|
638
|
+
"SELL LIMIT",
|
|
639
|
+
symbol,
|
|
640
|
+
dtime,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
process_orders(
|
|
644
|
+
"BSTP",
|
|
645
|
+
lambda o: latest_close >= o.price,
|
|
646
|
+
lambda o: self.buy_mkt(
|
|
647
|
+
o.strategy_id, symbol, o.price, o.quantity, dtime
|
|
648
|
+
),
|
|
649
|
+
"BUY STOP",
|
|
650
|
+
symbol,
|
|
651
|
+
dtime,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
process_orders(
|
|
655
|
+
"SSTP",
|
|
656
|
+
lambda o: latest_close <= o.price,
|
|
657
|
+
lambda o: self.sell_mkt(
|
|
658
|
+
o.strategy_id, symbol, o.price, o.quantity, dtime
|
|
659
|
+
),
|
|
660
|
+
"SELL STOP",
|
|
661
|
+
symbol,
|
|
662
|
+
dtime,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
process_orders(
|
|
666
|
+
"BSTPLMT",
|
|
667
|
+
lambda o: latest_close >= o.price,
|
|
668
|
+
lambda o: self.buy_limit(
|
|
669
|
+
o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
|
|
670
|
+
),
|
|
671
|
+
"BUY STOP LIMIT",
|
|
672
|
+
symbol,
|
|
673
|
+
dtime,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
process_orders(
|
|
677
|
+
"SSTPLMT",
|
|
678
|
+
lambda o: latest_close <= o.price,
|
|
679
|
+
lambda o: self.sell_limit(
|
|
680
|
+
o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
|
|
681
|
+
),
|
|
682
|
+
"SELL STOP LIMIT",
|
|
683
|
+
symbol,
|
|
684
|
+
dtime,
|
|
685
|
+
)
|
|
623
686
|
|
|
624
687
|
@staticmethod
|
|
625
688
|
def calculate_pct_change(current_price, lh_price) -> float:
|
|
@@ -797,17 +860,16 @@ class MT5Strategy(Strategy):
|
|
|
797
860
|
return False
|
|
798
861
|
tick_info = self.account.get_tick_info(asset)
|
|
799
862
|
bid, ask = tick_info.bid, tick_info.ask
|
|
863
|
+
price = None
|
|
800
864
|
if len(prices) == 1:
|
|
801
865
|
price = prices[0]
|
|
802
866
|
elif len(prices) in range(2, self.max_trades[asset] + 1):
|
|
803
867
|
price = np.mean(prices)
|
|
804
|
-
if
|
|
805
|
-
position == 0
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
):
|
|
810
|
-
return True
|
|
868
|
+
if price is not None:
|
|
869
|
+
if position == 0:
|
|
870
|
+
return self.calculate_pct_change(ask, price) >= th
|
|
871
|
+
elif position == 1:
|
|
872
|
+
return self.calculate_pct_change(bid, price) <= -th
|
|
811
873
|
return False
|
|
812
874
|
|
|
813
875
|
@staticmethod
|
|
@@ -832,9 +894,7 @@ class MT5Strategy(Strategy):
|
|
|
832
894
|
dt_to : The converted datetime.
|
|
833
895
|
"""
|
|
834
896
|
from_tz = pytz.timezone(from_tz)
|
|
835
|
-
if isinstance(dt, datetime):
|
|
836
|
-
dt = pd.to_datetime(dt, unit="s")
|
|
837
|
-
elif isinstance(dt, int):
|
|
897
|
+
if isinstance(dt, (datetime, int)):
|
|
838
898
|
dt = pd.to_datetime(dt, unit="s")
|
|
839
899
|
if dt.tzinfo is None:
|
|
840
900
|
dt = dt.tz_localize(from_tz)
|
|
@@ -860,20 +920,35 @@ class MT5Strategy(Strategy):
|
|
|
860
920
|
Returns:
|
|
861
921
|
mt5_equivalent : The MetaTrader 5 equivalent symbols for the symbols in the list.
|
|
862
922
|
"""
|
|
923
|
+
|
|
863
924
|
account = Account(**kwargs)
|
|
864
925
|
mt5_symbols = account.get_symbols(symbol_type=symbol_type)
|
|
865
926
|
mt5_equivalent = []
|
|
866
|
-
|
|
927
|
+
|
|
928
|
+
def _get_admiral_symbols():
|
|
867
929
|
for s in mt5_symbols:
|
|
868
930
|
_s = s[1:] if s[0] in string.punctuation else s
|
|
869
931
|
for symbol in symbols:
|
|
870
932
|
if _s.split(".")[0] == symbol or _s.split("_")[0] == symbol:
|
|
871
933
|
mt5_equivalent.append(s)
|
|
872
|
-
|
|
873
|
-
|
|
934
|
+
|
|
935
|
+
def _get_pepperstone_symbols():
|
|
936
|
+
for s in mt5_symbols:
|
|
874
937
|
for symbol in symbols:
|
|
875
938
|
if s.split(".")[0] == symbol:
|
|
876
939
|
mt5_equivalent.append(s)
|
|
940
|
+
|
|
941
|
+
if account.broker == MetaQuotes():
|
|
942
|
+
if "Admiral" in account.server:
|
|
943
|
+
_get_admiral_symbols()
|
|
944
|
+
elif "Pepperstone" in account.server:
|
|
945
|
+
_get_pepperstone_symbols()
|
|
946
|
+
|
|
947
|
+
elif account.broker == AdmiralMarktsGroup():
|
|
948
|
+
_get_admiral_symbols()
|
|
949
|
+
elif account.broker == PepperstoneGroupLimited():
|
|
950
|
+
_get_pepperstone_symbols()
|
|
951
|
+
|
|
877
952
|
return mt5_equivalent
|
|
878
953
|
|
|
879
954
|
|
bbstrader/core/data.py
CHANGED
|
@@ -190,7 +190,9 @@ class FmpNews(object):
|
|
|
190
190
|
0
|
|
191
191
|
] # if symbol is a yahoo finance ticker
|
|
192
192
|
source_methods = {
|
|
193
|
-
"articles": lambda: self.get_latest_articles(
|
|
193
|
+
"articles": lambda: self.get_latest_articles(
|
|
194
|
+
articles=articles, save=True, **kwargs
|
|
195
|
+
),
|
|
194
196
|
"releases": lambda: self.get_releases(symbol=symbol, **kwargs),
|
|
195
197
|
"stock": lambda: self.get_stock_news(symbol=symbol, **kwargs),
|
|
196
198
|
"crypto": lambda: self.get_crypto_news(symbol=symbol, **kwargs),
|
bbstrader/core/scripts.py
CHANGED
|
@@ -4,6 +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
8
|
|
|
8
9
|
import nltk
|
|
9
10
|
from loguru import logger
|
|
@@ -25,7 +26,7 @@ def summarize_text(text, sentences_count=5):
|
|
|
25
26
|
return " ".join(str(sentence) for sentence in summary)
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
def
|
|
29
|
+
def format_coindesk_article(article: dict) -> str:
|
|
29
30
|
if not all(
|
|
30
31
|
k in article
|
|
31
32
|
for k in (
|
|
@@ -53,16 +54,40 @@ def format_article_for_telegram(article: dict) -> str:
|
|
|
53
54
|
return text
|
|
54
55
|
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
def format_fmp_article(article: dict) -> str:
|
|
58
|
+
if not all(k in article for k in ("title", "date", "content", "tickers")):
|
|
59
|
+
return ""
|
|
60
|
+
summary = summarize_text(article["content"])
|
|
61
|
+
text = (
|
|
62
|
+
f"📰 {article['title']}\n"
|
|
63
|
+
f"Published Date: {article['date']}\n"
|
|
64
|
+
f"Keywords: {article['tickers']}\n\n"
|
|
65
|
+
f"🔍 Summary\n"
|
|
66
|
+
f"{textwrap.fill(summary, width=80)}"
|
|
67
|
+
)
|
|
68
|
+
return text
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def send_articles(
|
|
72
|
+
articles: List[dict],
|
|
73
|
+
token: str,
|
|
74
|
+
id: str,
|
|
75
|
+
source: Literal["coindesk", "fmp"],
|
|
76
|
+
interval=15,
|
|
77
|
+
):
|
|
57
78
|
for article in articles:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
message = ""
|
|
80
|
+
if source == "coindesk":
|
|
81
|
+
if article["published_on"] >= datetime.now() - timedelta(minutes=interval):
|
|
82
|
+
article["published_on"] = article.get("published_on").strftime(
|
|
83
|
+
"%Y-%m-%d %H:%M:%S"
|
|
84
|
+
)
|
|
85
|
+
message = format_coindesk_article(article)
|
|
86
|
+
else:
|
|
87
|
+
message = format_fmp_article(article)
|
|
88
|
+
if message == "":
|
|
89
|
+
return
|
|
90
|
+
await send_telegram_message(token, id, text=message)
|
|
66
91
|
|
|
67
92
|
|
|
68
93
|
def send_news_feed(unknown):
|
|
@@ -78,6 +103,7 @@ def send_news_feed(unknown):
|
|
|
78
103
|
-q, --query: The news to look for (default: "")
|
|
79
104
|
-t, --token: Telegram bot token
|
|
80
105
|
-I, --id: Telegram Chat id
|
|
106
|
+
--fmp: Financial Modeling Prop Api Key
|
|
81
107
|
-i, --interval: Interval in minutes to fetch news (default: 15)
|
|
82
108
|
|
|
83
109
|
Note:
|
|
@@ -101,6 +127,9 @@ def send_news_feed(unknown):
|
|
|
101
127
|
help="Telegram bot token",
|
|
102
128
|
)
|
|
103
129
|
parser.add_argument("-I", "--id", type=str, required=True, help="Telegram Chat id")
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--fmp", type=str, default="", help="Financial Modeling Prop Api Key"
|
|
132
|
+
)
|
|
104
133
|
parser.add_argument(
|
|
105
134
|
"-i",
|
|
106
135
|
"--interval",
|
|
@@ -112,19 +141,33 @@ def send_news_feed(unknown):
|
|
|
112
141
|
|
|
113
142
|
nltk.download("punkt", quiet=True)
|
|
114
143
|
news = FinancialNews()
|
|
115
|
-
logger.info(
|
|
116
|
-
f"Starting the News Feed on {args.interval} minutes"
|
|
117
|
-
)
|
|
144
|
+
logger.info(f"Starting the News Feed on {args.interval} minutes")
|
|
118
145
|
while True:
|
|
119
146
|
try:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
147
|
+
fmp_articles = []
|
|
148
|
+
coindesk_articles = news.get_coindesk_news(query=args.query)
|
|
149
|
+
if args.fmp:
|
|
150
|
+
start = datetime.now() - timedelta(minutes=args.interval)
|
|
151
|
+
start = start.strftime("%Y-%m-%d %H:%M:%S")
|
|
152
|
+
end = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
153
|
+
fmp_articles = news.get_fmp_news(api=args.fmp).get_latest_articles(
|
|
154
|
+
save=True, start=start, end=end
|
|
155
|
+
)
|
|
156
|
+
if len(coindesk_articles) != 0:
|
|
157
|
+
asyncio.run(
|
|
158
|
+
send_articles(
|
|
159
|
+
coindesk_articles,
|
|
160
|
+
args.token,
|
|
161
|
+
args.id,
|
|
162
|
+
"coindesk",
|
|
163
|
+
interval=args.interval,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
if len(fmp_articles) != 0:
|
|
167
|
+
asyncio.run(send_articles(fmp_articles, args.token, args.id, "fmp"))
|
|
125
168
|
time.sleep(args.interval * 60)
|
|
126
169
|
except KeyboardInterrupt:
|
|
127
170
|
logger.info("Stopping the News Feed ...")
|
|
128
|
-
exit(0)
|
|
171
|
+
sys.exit(0)
|
|
129
172
|
except Exception as e:
|
|
130
173
|
logger.error(e)
|
bbstrader/core/utils.py
CHANGED
|
@@ -66,9 +66,11 @@ def dict_from_ini(file_path, sections: str | List[str] = None) -> Dict[str, Any]
|
|
|
66
66
|
Returns:
|
|
67
67
|
A dictionary containing the INI file contents with proper data types.
|
|
68
68
|
"""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
try:
|
|
70
|
+
config = configparser.ConfigParser(interpolation=None)
|
|
71
|
+
config.read(file_path)
|
|
72
|
+
except Exception:
|
|
73
|
+
raise
|
|
72
74
|
ini_dict = {}
|
|
73
75
|
for section in config.sections():
|
|
74
76
|
ini_dict[section] = {
|