bbstrader 0.2.99__py3-none-any.whl → 0.3.0__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/{__ini__.py → __init__.py} +0 -1
- bbstrader/__main__.py +31 -14
- bbstrader/btengine/data.py +4 -2
- bbstrader/btengine/event.py +15 -14
- bbstrader/btengine/execution.py +24 -14
- bbstrader/btengine/strategy.py +33 -15
- bbstrader/core/data.py +99 -3
- bbstrader/core/scripts.py +130 -0
- bbstrader/metatrader/account.py +124 -129
- bbstrader/metatrader/copier.py +5 -6
- bbstrader/metatrader/rates.py +14 -13
- bbstrader/metatrader/risk.py +13 -11
- bbstrader/metatrader/trade.py +34 -23
- bbstrader/metatrader/utils.py +79 -26
- bbstrader/models/factors.py +3 -1
- bbstrader/models/ml.py +2 -1
- bbstrader/models/nlp.py +21 -4
- bbstrader/trading/execution.py +36 -19
- bbstrader/trading/strategies.py +15 -14
- bbstrader/tseries.py +8 -9
- bbstrader-0.3.0.dist-info/METADATA +469 -0
- bbstrader-0.3.0.dist-info/RECORD +47 -0
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/WHEEL +1 -1
- bbstrader-0.2.99.dist-info/METADATA +0 -193
- bbstrader-0.2.99.dist-info/RECORD +0 -46
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/top_level.txt +0 -0
bbstrader/__main__.py
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import sys
|
|
3
|
+
from enum import Enum
|
|
3
4
|
|
|
4
5
|
import pyfiglet
|
|
5
6
|
from colorama import Fore
|
|
6
7
|
|
|
7
8
|
from bbstrader.btengine.scripts import backtest
|
|
9
|
+
from bbstrader.core.scripts import send_news_feed
|
|
8
10
|
from bbstrader.metatrader.scripts import copy_trades
|
|
9
11
|
from bbstrader.trading.scripts import execute_strategy
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
|
|
14
|
+
class Module(Enum):
|
|
15
|
+
COPIER = "copier"
|
|
16
|
+
BACKTEST = "backtest"
|
|
17
|
+
EXECUTION = "execution"
|
|
18
|
+
NEWS_FEED = "news_feed"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
FONT = pyfiglet.figlet_format("BBSTRADER", font="big")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
DESCRIPTION = "BBSTRADER"
|
|
26
|
+
USAGE_TEXT = """
|
|
13
27
|
Usage:
|
|
14
28
|
python -m bbstrader --run <module> [options]
|
|
15
29
|
|
|
@@ -17,14 +31,10 @@ USAGE_TEXT = """
|
|
|
17
31
|
copier: Copy trades from one MetaTrader account to another or multiple accounts
|
|
18
32
|
backtest: Backtest a strategy, see bbstrader.btengine.backtest.run_backtest
|
|
19
33
|
execution: Execute a strategy, see bbstrader.trading.execution.Mt5ExecutionEngine
|
|
34
|
+
news_feed: Send news feed from Coindesk to Telegram channel
|
|
20
35
|
|
|
21
36
|
python -m bbstrader --run <module> --help for more information on the module
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
FONT = pyfiglet.figlet_format("BBSTRADER", font="big")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def main():
|
|
37
|
+
"""
|
|
28
38
|
print(Fore.BLUE + FONT)
|
|
29
39
|
print(Fore.WHITE + "")
|
|
30
40
|
parser = argparse.ArgumentParser(
|
|
@@ -39,12 +49,19 @@ def main():
|
|
|
39
49
|
if ("-h" in sys.argv or "--help" in sys.argv) and args.run is None:
|
|
40
50
|
print(Fore.WHITE + USAGE_TEXT)
|
|
41
51
|
sys.exit(0)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
|
|
53
|
+
match args.run:
|
|
54
|
+
case Module.COPIER.value:
|
|
55
|
+
copy_trades(unknown)
|
|
56
|
+
case Module.BACKTEST.value:
|
|
57
|
+
backtest(unknown)
|
|
58
|
+
case Module.EXECUTION.value:
|
|
59
|
+
execute_strategy(unknown)
|
|
60
|
+
case Module.NEWS_FEED.value:
|
|
61
|
+
send_news_feed(unknown)
|
|
62
|
+
case _:
|
|
63
|
+
print(Fore.RED + f"Unknown module: {args.run}")
|
|
64
|
+
sys.exit(1)
|
|
48
65
|
|
|
49
66
|
|
|
50
67
|
if __name__ == "__main__":
|
bbstrader/btengine/data.py
CHANGED
|
@@ -5,6 +5,7 @@ from queue import Queue
|
|
|
5
5
|
from typing import Dict, List
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
|
+
from numpy.typing import NDArray
|
|
8
9
|
import pandas as pd
|
|
9
10
|
import yfinance as yf
|
|
10
11
|
from eodhd import APIClient
|
|
@@ -91,7 +92,7 @@ class DataHandler(metaclass=ABCMeta):
|
|
|
91
92
|
pass
|
|
92
93
|
|
|
93
94
|
@abstractmethod
|
|
94
|
-
def get_latest_bars_values(self, symbol, val_type, N=1) ->
|
|
95
|
+
def get_latest_bars_values(self, symbol, val_type, N=1) -> NDArray:
|
|
95
96
|
"""
|
|
96
97
|
Returns the last N bar values from the
|
|
97
98
|
latest_symbol list, or N-k if less available.
|
|
@@ -301,7 +302,7 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
301
302
|
)
|
|
302
303
|
raise
|
|
303
304
|
|
|
304
|
-
def get_latest_bars_values(self, symbol: str, val_type: str, N=1) ->
|
|
305
|
+
def get_latest_bars_values(self, symbol: str, val_type: str, N=1) -> NDArray:
|
|
305
306
|
"""
|
|
306
307
|
Returns the last N bar values from the
|
|
307
308
|
latest_symbol list, or N-k if less available.
|
|
@@ -413,6 +414,7 @@ class MT5DataHandler(BaseCSVDataHandler):
|
|
|
413
414
|
self.data_dir = kwargs.get("data_dir")
|
|
414
415
|
self.symbol_list = symbol_list
|
|
415
416
|
self.kwargs = kwargs
|
|
417
|
+
self.kwargs["backtest"] = True # Ensure backtest mode is set to avoid InvalidBroker errors
|
|
416
418
|
|
|
417
419
|
csv_dir = self._download_and_cache_data(self.data_dir)
|
|
418
420
|
super().__init__(
|
bbstrader/btengine/event.py
CHANGED
|
@@ -2,7 +2,7 @@ from datetime import datetime
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
|
-
__all__ = ["Event", "MarketEvent", "SignalEvent", "OrderEvent", "FillEvent"]
|
|
5
|
+
__all__ = ["Event", "Events", "MarketEvent", "SignalEvent", "OrderEvent", "FillEvent"]
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Event(object):
|
|
@@ -134,20 +134,21 @@ class OrderEvent(Event):
|
|
|
134
134
|
self.price = price
|
|
135
135
|
self.signal = signal
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
)
|
|
137
|
+
def print_order(self):
|
|
138
|
+
"""
|
|
139
|
+
Outputs the values within the Order.
|
|
140
|
+
"""
|
|
141
|
+
print(
|
|
142
|
+
"Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s, Price=%s"
|
|
143
|
+
% (
|
|
144
|
+
self.symbol,
|
|
145
|
+
self.order_type,
|
|
146
|
+
self.quantity,
|
|
147
|
+
self.direction,
|
|
148
|
+
self.price,
|
|
150
149
|
)
|
|
150
|
+
)
|
|
151
|
+
|
|
151
152
|
|
|
152
153
|
|
|
153
154
|
class FillEvent(Event):
|
bbstrader/btengine/execution.py
CHANGED
|
@@ -7,6 +7,7 @@ from bbstrader.btengine.data import DataHandler
|
|
|
7
7
|
from bbstrader.btengine.event import Events, FillEvent, OrderEvent
|
|
8
8
|
from bbstrader.config import BBSTRADER_DIR
|
|
9
9
|
from bbstrader.metatrader.account import Account
|
|
10
|
+
from bbstrader.metatrader.utils import SymbolType
|
|
10
11
|
|
|
11
12
|
__all__ = ["ExecutionHandler", "SimExecutionHandler", "MT5ExecutionHandler"]
|
|
12
13
|
|
|
@@ -71,6 +72,8 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
71
72
|
self.events = events
|
|
72
73
|
self.bardata = data
|
|
73
74
|
self.logger = kwargs.get("logger") or logger
|
|
75
|
+
self.commissions = kwargs.get("commission")
|
|
76
|
+
self.exchange = kwargs.get("exchange", "ARCA")
|
|
74
77
|
|
|
75
78
|
def execute_order(self, event: OrderEvent):
|
|
76
79
|
"""
|
|
@@ -85,11 +88,11 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
85
88
|
fill_event = FillEvent(
|
|
86
89
|
timeindex=dtime,
|
|
87
90
|
symbol=event.symbol,
|
|
88
|
-
exchange=
|
|
91
|
+
exchange=self.exchange,
|
|
89
92
|
quantity=event.quantity,
|
|
90
93
|
direction=event.direction,
|
|
91
94
|
fill_cost=None,
|
|
92
|
-
commission=
|
|
95
|
+
commission=self.commissions,
|
|
93
96
|
order=event.signal,
|
|
94
97
|
)
|
|
95
98
|
self.events.put(fill_event)
|
|
@@ -135,6 +138,8 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
135
138
|
self.events = events
|
|
136
139
|
self.bardata = data
|
|
137
140
|
self.logger = kwargs.get("logger") or logger
|
|
141
|
+
self.commissions = kwargs.get("commission")
|
|
142
|
+
self.exchange = kwargs.get("exchange", "MT5")
|
|
138
143
|
self.__account = Account(**kwargs)
|
|
139
144
|
|
|
140
145
|
def _calculate_lot(self, symbol, quantity, price):
|
|
@@ -145,9 +150,13 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
145
150
|
lot = (quantity * price) / (contract_size * price)
|
|
146
151
|
if contract_size == 1:
|
|
147
152
|
lot = quantity
|
|
148
|
-
if
|
|
153
|
+
if (
|
|
154
|
+
symbol_type
|
|
155
|
+
in (SymbolType.COMMODITIES, SymbolType.FUTURES, SymbolType.CRYPTO)
|
|
156
|
+
and contract_size > 1
|
|
157
|
+
):
|
|
149
158
|
lot = quantity / contract_size
|
|
150
|
-
if symbol_type ==
|
|
159
|
+
if symbol_type == SymbolType.FOREX:
|
|
151
160
|
lot = quantity * price / contract_size
|
|
152
161
|
return self._check_lot(symbol, lot)
|
|
153
162
|
|
|
@@ -161,17 +170,17 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
161
170
|
|
|
162
171
|
def _estimate_total_fees(self, symbol, lot, qty, price):
|
|
163
172
|
symbol_type = self.__account.get_symbol_type(symbol)
|
|
164
|
-
if symbol_type in
|
|
173
|
+
if symbol_type in (SymbolType.STOCKS, SymbolType.ETFs):
|
|
165
174
|
return self._estimate_stock_commission(symbol, qty, price)
|
|
166
|
-
elif symbol_type ==
|
|
175
|
+
elif symbol_type == SymbolType.FOREX:
|
|
167
176
|
return self._estimate_forex_commission(lot)
|
|
168
|
-
elif symbol_type ==
|
|
177
|
+
elif symbol_type == SymbolType.COMMODITIES:
|
|
169
178
|
return self._estimate_commodity_commission(lot)
|
|
170
|
-
elif symbol_type ==
|
|
179
|
+
elif symbol_type == SymbolType.INDICES:
|
|
171
180
|
return self._estimate_index_commission(lot)
|
|
172
|
-
elif symbol_type ==
|
|
181
|
+
elif symbol_type == SymbolType.FUTURES:
|
|
173
182
|
return self._estimate_futures_commission()
|
|
174
|
-
elif symbol_type ==
|
|
183
|
+
elif symbol_type == SymbolType.CRYPTO:
|
|
175
184
|
return self._estimate_crypto_commission()
|
|
176
185
|
else:
|
|
177
186
|
return 0.0
|
|
@@ -187,7 +196,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
187
196
|
eu_asia_cm = 0.0015 # percent
|
|
188
197
|
if (
|
|
189
198
|
symbol in self.__account.get_stocks_from_country("USA")
|
|
190
|
-
or self.__account.get_symbol_type(symbol) ==
|
|
199
|
+
or self.__account.get_symbol_type(symbol) == SymbolType.ETFs
|
|
191
200
|
and self.__account.get_currency_rates(symbol)["mc"] == "USD"
|
|
192
201
|
):
|
|
193
202
|
return max(min_com, qty * us_com)
|
|
@@ -195,7 +204,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
195
204
|
symbol in self.__account.get_stocks_from_country("GBR")
|
|
196
205
|
or symbol in self.__account.get_stocks_from_country("FRA")
|
|
197
206
|
or symbol in self.__account.get_stocks_from_country("DEU")
|
|
198
|
-
or self.__account.get_symbol_type(symbol) ==
|
|
207
|
+
or self.__account.get_symbol_type(symbol) == SymbolType.ETFs
|
|
199
208
|
and self.__account.get_currency_rates(symbol)["mc"] in ["GBP", "EUR"]
|
|
200
209
|
):
|
|
201
210
|
return max(min_com, qty * price * ger_fr_uk_cm)
|
|
@@ -241,14 +250,15 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
241
250
|
lot = self._calculate_lot(symbol, quantity, price)
|
|
242
251
|
fees = self._estimate_total_fees(symbol, lot, quantity, price)
|
|
243
252
|
dtime = self.bardata.get_latest_bar_datetime(symbol)
|
|
253
|
+
commission = self.commissions or fees
|
|
244
254
|
fill_event = FillEvent(
|
|
245
255
|
timeindex=dtime,
|
|
246
256
|
symbol=symbol,
|
|
247
|
-
exchange=
|
|
257
|
+
exchange=self.exchange,
|
|
248
258
|
quantity=quantity,
|
|
249
259
|
direction=direction,
|
|
250
260
|
fill_cost=None,
|
|
251
|
-
commission=
|
|
261
|
+
commission=commission,
|
|
252
262
|
order=event.signal,
|
|
253
263
|
)
|
|
254
264
|
self.events.put(fill_event)
|
bbstrader/btengine/strategy.py
CHANGED
|
@@ -17,10 +17,12 @@ from bbstrader.metatrader.account import (
|
|
|
17
17
|
AdmiralMarktsGroup,
|
|
18
18
|
PepperstoneGroupLimited,
|
|
19
19
|
)
|
|
20
|
+
from bbstrader.metatrader.utils import SymbolType
|
|
20
21
|
from bbstrader.metatrader.rates import Rates
|
|
21
|
-
from bbstrader.metatrader.trade import TradeSignal
|
|
22
|
+
from bbstrader.metatrader.trade import TradeSignal, TradingMode
|
|
22
23
|
from bbstrader.models.optimization import optimized_weights
|
|
23
24
|
|
|
25
|
+
|
|
24
26
|
__all__ = ["Strategy", "MT5Strategy"]
|
|
25
27
|
|
|
26
28
|
logger.add(
|
|
@@ -76,7 +78,7 @@ class MT5Strategy(Strategy):
|
|
|
76
78
|
events: Queue = None,
|
|
77
79
|
symbol_list: List[str] = None,
|
|
78
80
|
bars: DataHandler = None,
|
|
79
|
-
mode:
|
|
81
|
+
mode: TradingMode = None,
|
|
80
82
|
**kwargs,
|
|
81
83
|
):
|
|
82
84
|
"""
|
|
@@ -86,7 +88,7 @@ class MT5Strategy(Strategy):
|
|
|
86
88
|
events : The event queue.
|
|
87
89
|
symbol_list : The list of symbols for the strategy.
|
|
88
90
|
bars : The data handler object.
|
|
89
|
-
mode : The mode of operation for the strategy
|
|
91
|
+
mode (TradingMode): The mode of operation for the strategy.
|
|
90
92
|
**kwargs : Additional keyword arguments for other classes (e.g, Portfolio, ExecutionHandler).
|
|
91
93
|
- max_trades : The maximum number of trades allowed per symbol.
|
|
92
94
|
- time_frame : The time frame for the strategy.
|
|
@@ -101,33 +103,49 @@ class MT5Strategy(Strategy):
|
|
|
101
103
|
self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
|
|
102
104
|
self.tf = kwargs.get("time_frame", "D1")
|
|
103
105
|
self.logger = kwargs.get("logger") or logger
|
|
104
|
-
if self.mode ==
|
|
106
|
+
if self.mode == TradingMode.BACKTEST:
|
|
105
107
|
self._initialize_portfolio()
|
|
106
108
|
self.kwargs = kwargs
|
|
107
109
|
self.periodes = 0
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def account(self):
|
|
113
|
+
return Account(**self.kwargs)
|
|
108
114
|
|
|
109
115
|
@property
|
|
110
116
|
def cash(self) -> float:
|
|
117
|
+
if self.mode == TradingMode.LIVE:
|
|
118
|
+
return self.account.balance
|
|
111
119
|
return self._porfolio_value
|
|
112
120
|
|
|
113
121
|
@cash.setter
|
|
114
122
|
def cash(self, value):
|
|
123
|
+
if self.mode == TradingMode.LIVE:
|
|
124
|
+
raise ValueError("Cannot set the account cash in live mode")
|
|
115
125
|
self._porfolio_value = value
|
|
116
126
|
|
|
117
127
|
@property
|
|
118
|
-
def orders(self)
|
|
128
|
+
def orders(self):
|
|
129
|
+
if self.mode == TradingMode.LIVE:
|
|
130
|
+
return self.account.get_orders()
|
|
119
131
|
return self._orders
|
|
120
132
|
|
|
121
133
|
@property
|
|
122
134
|
def trades(self) -> Dict[str, Dict[str, int]]:
|
|
135
|
+
if self.mode == TradingMode.LIVE:
|
|
136
|
+
raise ValueError("Cannot call this methode in live mode")
|
|
123
137
|
return self._trades
|
|
124
138
|
|
|
125
139
|
@property
|
|
126
|
-
def positions(self)
|
|
140
|
+
def positions(self):
|
|
141
|
+
if self.mode == TradingMode.LIVE:
|
|
142
|
+
return self.account.get_positions()
|
|
127
143
|
return self._positions
|
|
128
144
|
|
|
129
145
|
@property
|
|
130
146
|
def holdings(self) -> Dict[str, float]:
|
|
147
|
+
if self.mode == TradingMode.LIVE:
|
|
148
|
+
raise ValueError("Cannot call this methode in live mode")
|
|
131
149
|
return self._holdings
|
|
132
150
|
|
|
133
151
|
def _check_risk_budget(self, **kwargs):
|
|
@@ -606,7 +624,7 @@ class MT5Strategy(Strategy):
|
|
|
606
624
|
value_type: str = "returns",
|
|
607
625
|
array: bool = True,
|
|
608
626
|
bars: DataHandler = None,
|
|
609
|
-
mode:
|
|
627
|
+
mode: TradingMode = TradingMode.BACKTEST,
|
|
610
628
|
tf: str = "D1",
|
|
611
629
|
error: Literal["ignore", "raise"] = None,
|
|
612
630
|
) -> Dict[str, np.ndarray | pd.Series] | None:
|
|
@@ -634,7 +652,7 @@ class MT5Strategy(Strategy):
|
|
|
634
652
|
if mode not in ["backtest", "live"]:
|
|
635
653
|
raise ValueError("Mode must be either backtest or live.")
|
|
636
654
|
asset_values = {}
|
|
637
|
-
if mode ==
|
|
655
|
+
if mode == TradingMode.BACKTEST:
|
|
638
656
|
if bars is None:
|
|
639
657
|
raise ValueError("DataHandler is required for backtest mode.")
|
|
640
658
|
for asset in symbol_list:
|
|
@@ -644,11 +662,11 @@ class MT5Strategy(Strategy):
|
|
|
644
662
|
else:
|
|
645
663
|
values = bars.get_latest_bars(asset, N=window)
|
|
646
664
|
asset_values[asset] = getattr(values, value_type)
|
|
647
|
-
elif mode ==
|
|
665
|
+
elif mode == TradingMode.LIVE:
|
|
648
666
|
for asset in symbol_list:
|
|
649
667
|
rates = Rates(asset, timeframe=tf, count=window + 1, **self.kwargs)
|
|
650
668
|
if array:
|
|
651
|
-
values = getattr(rates, value_type).
|
|
669
|
+
values = getattr(rates, value_type).to_numpy()
|
|
652
670
|
asset_values[asset] = values[~np.isnan(values)]
|
|
653
671
|
else:
|
|
654
672
|
values = getattr(rates, value_type)
|
|
@@ -704,7 +722,7 @@ class MT5Strategy(Strategy):
|
|
|
704
722
|
Returns:
|
|
705
723
|
bool : True if there are open positions, False otherwise
|
|
706
724
|
"""
|
|
707
|
-
account = account or
|
|
725
|
+
account = account or self.account
|
|
708
726
|
positions = account.get_positions(symbol=symbol)
|
|
709
727
|
if positions is not None:
|
|
710
728
|
open_positions = [
|
|
@@ -730,7 +748,7 @@ class MT5Strategy(Strategy):
|
|
|
730
748
|
Returns:
|
|
731
749
|
prices : numpy array of buy or sell prices for open positions if any or an empty array.
|
|
732
750
|
"""
|
|
733
|
-
account = account or
|
|
751
|
+
account = account or self.account
|
|
734
752
|
positions = account.get_positions(symbol=symbol)
|
|
735
753
|
if positions is not None:
|
|
736
754
|
prices = np.array(
|
|
@@ -778,21 +796,21 @@ class MT5Strategy(Strategy):
|
|
|
778
796
|
return dt_to
|
|
779
797
|
|
|
780
798
|
@staticmethod
|
|
781
|
-
def get_mt5_equivalent(symbols,
|
|
799
|
+
def get_mt5_equivalent(symbols, symbol_type: str | SymbolType = SymbolType.STOCKS, **kwargs) -> List[str]:
|
|
782
800
|
"""
|
|
783
801
|
Get the MetaTrader 5 equivalent symbols for the symbols in the list.
|
|
784
802
|
This method is used to get the symbols that are available on the MetaTrader 5 platform.
|
|
785
803
|
|
|
786
804
|
Args:
|
|
787
805
|
symbols : The list of symbols to get the MetaTrader 5 equivalent symbols for.
|
|
788
|
-
|
|
806
|
+
symbol_type : The type of symbols to get (See `bbstrader.metatrader.utils.SymbolType`).
|
|
789
807
|
**kwargs : Additional keyword arguments for the `bbstrader.metatrader.Account` object.
|
|
790
808
|
|
|
791
809
|
Returns:
|
|
792
810
|
mt5_equivalent : The MetaTrader 5 equivalent symbols for the symbols in the list.
|
|
793
811
|
"""
|
|
794
812
|
account = Account(**kwargs)
|
|
795
|
-
mt5_symbols = account.get_symbols(symbol_type=
|
|
813
|
+
mt5_symbols = account.get_symbols(symbol_type=symbol_type)
|
|
796
814
|
mt5_equivalent = []
|
|
797
815
|
if account.broker == AdmiralMarktsGroup():
|
|
798
816
|
for s in mt5_symbols:
|
bbstrader/core/data.py
CHANGED
|
@@ -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
|
|
5
|
+
from typing import List, Literal
|
|
6
6
|
from urllib.request import urlopen
|
|
7
7
|
|
|
8
8
|
import certifi
|
|
@@ -18,7 +18,7 @@ __all__ = ["FmpData", "FmpNews", "FinancialNews"]
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _get_search_query(query: str) -> str:
|
|
21
|
-
if " " in query:
|
|
21
|
+
if " " in query or query == "":
|
|
22
22
|
return query
|
|
23
23
|
try:
|
|
24
24
|
name = yf.Ticker(query).info["shortName"]
|
|
@@ -198,7 +198,7 @@ class FmpNews(object):
|
|
|
198
198
|
class FinancialNews(object):
|
|
199
199
|
"""
|
|
200
200
|
The FinancialNews class provides methods to fetch financial news, articles, and discussions
|
|
201
|
-
from various sources such as Yahoo Finance, Google Finance, Reddit, and Twitter.
|
|
201
|
+
from various sources such as Yahoo Finance, Google Finance, Reddit, Coindesk and Twitter.
|
|
202
202
|
It also supports retrieving news using Financial Modeling Prep (FMP).
|
|
203
203
|
|
|
204
204
|
"""
|
|
@@ -422,6 +422,102 @@ class FinancialNews(object):
|
|
|
422
422
|
def get_fmp_news(self, api=None) -> FmpNews:
|
|
423
423
|
return FmpNews(api=api)
|
|
424
424
|
|
|
425
|
+
def get_coindesk_news(
|
|
426
|
+
self,
|
|
427
|
+
query="",
|
|
428
|
+
lang: Literal["EN", "ES", "TR", "FR", "JP", "PT"] = "EN",
|
|
429
|
+
limit=50,
|
|
430
|
+
list_of_str=False,
|
|
431
|
+
) -> List[str] | List[dict]:
|
|
432
|
+
"""
|
|
433
|
+
Fetches and filters recent news articles from CoinDesk's News API.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
query : str, optional
|
|
437
|
+
A search term to filter articles by title, body, or keywords.
|
|
438
|
+
If empty, all articles are returned without filtering (default is "").
|
|
439
|
+
|
|
440
|
+
lang : Literal["EN", "ES", "TR", "FR", "JP", "PT"], optional
|
|
441
|
+
Language in which to fetch news articles. Supported languages:
|
|
442
|
+
English (EN), Spanish (ES), Turkish (TR), French (FR), Japanese (JP), and Portuguese (PT).
|
|
443
|
+
Default is "EN".
|
|
444
|
+
|
|
445
|
+
limit : int, optional
|
|
446
|
+
Maximum number of articles to retrieve. Default is 50.
|
|
447
|
+
|
|
448
|
+
list_of_str : bool, optional
|
|
449
|
+
If True, returns a list of strings (concatenated article content).
|
|
450
|
+
If False, returns a list of filtered article dictionaries.
|
|
451
|
+
Default is False.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
List[str] | List[dict]
|
|
455
|
+
- If `query` is empty: returns a list of filtered article dictionaries.
|
|
456
|
+
- If `query` is provided:
|
|
457
|
+
- Returns a list of strings if `list_of_str=True`.
|
|
458
|
+
- Returns a list of filtered article dictionaries otherwise.
|
|
459
|
+
|
|
460
|
+
Each article dictionary contains the following fields:
|
|
461
|
+
- 'published_on': datetime of publication
|
|
462
|
+
- 'title': article headline
|
|
463
|
+
- 'subtitle': secondary headline
|
|
464
|
+
- 'url': direct link to the article
|
|
465
|
+
- 'body': article content
|
|
466
|
+
- 'keywords': associated tags
|
|
467
|
+
- 'sentiment': sentiment label
|
|
468
|
+
- 'status': publication status
|
|
469
|
+
|
|
470
|
+
Notes:
|
|
471
|
+
- Articles marked as sponsored are automatically excluded.
|
|
472
|
+
"""
|
|
473
|
+
maximum = 100
|
|
474
|
+
if limit > maximum:
|
|
475
|
+
raise ValueError(f"Number of total news articles allowed is {maximum}")
|
|
476
|
+
|
|
477
|
+
response = requests.get(
|
|
478
|
+
"https://data-api.coindesk.com/news/v1/article/list",
|
|
479
|
+
params={"lang": lang, "limit": limit},
|
|
480
|
+
headers={"Content-type": "application/json; charset=UTF-8"},
|
|
481
|
+
)
|
|
482
|
+
json_response = response.json()
|
|
483
|
+
articles = json_response["Data"]
|
|
484
|
+
if len(articles) == 0:
|
|
485
|
+
return []
|
|
486
|
+
to_keep = [
|
|
487
|
+
"PUBLISHED_ON",
|
|
488
|
+
"TITLE",
|
|
489
|
+
"SUBTITLE",
|
|
490
|
+
"URL",
|
|
491
|
+
"BODY",
|
|
492
|
+
"KEYWORDS",
|
|
493
|
+
"SENTIMENT",
|
|
494
|
+
"STATUS",
|
|
495
|
+
]
|
|
496
|
+
filtered_articles = []
|
|
497
|
+
for article in articles:
|
|
498
|
+
filtered_articles.append(
|
|
499
|
+
{
|
|
500
|
+
k.lower(): article[k]
|
|
501
|
+
if k in article and k != "PUBLISHED_ON"
|
|
502
|
+
else datetime.fromtimestamp(article[k])
|
|
503
|
+
for k in to_keep
|
|
504
|
+
if article[k] is not None and "sponsored" not in str(article[k])
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
if query == "" or len(filtered_articles) == 0:
|
|
508
|
+
return filtered_articles
|
|
509
|
+
to_return = []
|
|
510
|
+
query = _get_search_query(query)
|
|
511
|
+
for article in filtered_articles:
|
|
512
|
+
if not all(k in article for k in ("title", "body", "keywords")):
|
|
513
|
+
continue
|
|
514
|
+
text = article["title"] + " " + article["body"] + " " + article["keywords"]
|
|
515
|
+
if list_of_str and _find_news(query, text=text):
|
|
516
|
+
to_return.append(text)
|
|
517
|
+
if not list_of_str and _find_news(query, text=text):
|
|
518
|
+
to_return.append(article)
|
|
519
|
+
return to_return
|
|
520
|
+
|
|
425
521
|
|
|
426
522
|
class FmpData(Toolkit):
|
|
427
523
|
"""
|