bbstrader 0.2.991__py3-none-any.whl → 0.3.1__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 +35 -14
- bbstrader/btengine/data.py +4 -2
- bbstrader/btengine/execution.py +24 -14
- bbstrader/btengine/strategy.py +33 -15
- bbstrader/core/data.py +93 -30
- bbstrader/core/scripts.py +130 -0
- bbstrader/metatrader/account.py +115 -126
- bbstrader/metatrader/copier.py +114 -39
- bbstrader/metatrader/rates.py +14 -13
- bbstrader/metatrader/risk.py +13 -11
- bbstrader/metatrader/scripts.py +26 -12
- bbstrader/metatrader/trade.py +60 -54
- bbstrader/metatrader/utils.py +80 -26
- bbstrader/models/factors.py +3 -1
- bbstrader/models/ml.py +2 -1
- bbstrader/models/nlp.py +123 -70
- bbstrader/trading/execution.py +74 -36
- bbstrader/trading/strategies.py +15 -14
- bbstrader/tseries.py +8 -9
- bbstrader-0.3.1.dist-info/METADATA +466 -0
- bbstrader-0.3.1.dist-info/RECORD +47 -0
- {bbstrader-0.2.991.dist-info → bbstrader-0.3.1.dist-info}/WHEEL +1 -1
- bbstrader/__ini__.py +0 -20
- bbstrader-0.2.991.dist-info/METADATA +0 -191
- bbstrader-0.2.991.dist-info/RECORD +0 -47
- {bbstrader-0.2.991.dist-info → bbstrader-0.3.1.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.991.dist-info → bbstrader-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.2.991.dist-info → bbstrader-0.3.1.dist-info}/top_level.txt +0 -0
bbstrader/__init__.py
CHANGED
|
@@ -7,7 +7,7 @@ __author__ = "Bertin Balouki SIMYELI"
|
|
|
7
7
|
__copyright__ = "2023-2025 Bertin Balouki SIMYELI"
|
|
8
8
|
__email__ = "bertin@bbstrader.com"
|
|
9
9
|
__license__ = "MIT"
|
|
10
|
-
__version__ = "0.
|
|
10
|
+
__version__ = "0.3.1"
|
|
11
11
|
|
|
12
12
|
from bbstrader import compat # noqa: F401
|
|
13
13
|
from bbstrader import core # noqa: F401
|
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,23 @@ 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
|
+
try:
|
|
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)
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
except Exception:
|
|
68
|
+
sys.exit(1)
|
|
48
69
|
|
|
49
70
|
|
|
50
71
|
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/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
|
@@ -149,7 +149,7 @@ class FmpNews(object):
|
|
|
149
149
|
try:
|
|
150
150
|
articles = pd.read_csv("latest_fmp_articles.csv")
|
|
151
151
|
articles = articles.to_dict(orient="records")
|
|
152
|
-
if self._last_date(articles[0]["date"]) < end_date:
|
|
152
|
+
if self._last_date(articles[0]["date"]).hour < end_date.hour:
|
|
153
153
|
articles = self.get_articles(**kwargs)
|
|
154
154
|
else:
|
|
155
155
|
return articles
|
|
@@ -161,7 +161,9 @@ class FmpNews(object):
|
|
|
161
161
|
df.to_csv("latest_fmp_articles.csv", index=False)
|
|
162
162
|
return articles
|
|
163
163
|
|
|
164
|
-
def get_news(
|
|
164
|
+
def get_news(
|
|
165
|
+
self, query, source="articles", articles=None, symbol: str = None, **kwargs
|
|
166
|
+
):
|
|
165
167
|
"""
|
|
166
168
|
Retrieves relevant financial news based on the specified source.
|
|
167
169
|
|
|
@@ -183,6 +185,10 @@ class FmpNews(object):
|
|
|
183
185
|
Returns an empty list if no relevant news is found.
|
|
184
186
|
"""
|
|
185
187
|
query = _get_search_query(query)
|
|
188
|
+
if symbol is not None:
|
|
189
|
+
symbol = symbol.replace("-", "").split("=")[
|
|
190
|
+
0
|
|
191
|
+
] # if symbol is a yahoo finance ticker
|
|
186
192
|
source_methods = {
|
|
187
193
|
"articles": lambda: self.get_latest_articles(articles=articles, save=True),
|
|
188
194
|
"releases": lambda: self.get_releases(symbol=symbol, **kwargs),
|
|
@@ -191,6 +197,8 @@ class FmpNews(object):
|
|
|
191
197
|
"forex": lambda: self.get_forex_news(symbol=symbol, **kwargs),
|
|
192
198
|
}
|
|
193
199
|
news_source = source_methods.get(source, lambda: [])()
|
|
200
|
+
if source == "articles":
|
|
201
|
+
symbol = None # Articles do not require a symbol filter
|
|
194
202
|
news = self.parse_news(news_source, symbol=symbol)
|
|
195
203
|
return _filter_news(news, query)
|
|
196
204
|
|
|
@@ -198,12 +206,12 @@ class FmpNews(object):
|
|
|
198
206
|
class FinancialNews(object):
|
|
199
207
|
"""
|
|
200
208
|
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.
|
|
209
|
+
from various sources such as Yahoo Finance, Google Finance, Reddit, Coindesk and Twitter.
|
|
202
210
|
It also supports retrieving news using Financial Modeling Prep (FMP).
|
|
203
211
|
|
|
204
212
|
"""
|
|
205
213
|
|
|
206
|
-
def _fetch_news(self, url, query,
|
|
214
|
+
def _fetch_news(self, url, query, n_news, headline_tag) -> List[str]:
|
|
207
215
|
headers = {"User-Agent": "Mozilla/5.0"}
|
|
208
216
|
try:
|
|
209
217
|
response = requests.get(url, headers=headers)
|
|
@@ -224,32 +232,60 @@ class FinancialNews(object):
|
|
|
224
232
|
]
|
|
225
233
|
return headlines[:n_news]
|
|
226
234
|
|
|
227
|
-
def get_yahoo_finance_news(self, query, asset_type="stock", n_news=10):
|
|
235
|
+
def get_yahoo_finance_news(self, query: str, asset_type="stock", n_news=10):
|
|
228
236
|
"""
|
|
229
237
|
Fetches recent Yahoo Finance news headlines for a given financial asset.
|
|
230
238
|
|
|
231
239
|
Args:
|
|
232
240
|
query (str): The asset symbol or name (e.g., "AAPL").
|
|
233
|
-
asset_type (str, optional): The type of asset (e.g., "stock", "etf"). Defaults to "stock"
|
|
241
|
+
asset_type (str, optional): The type of asset (e.g., "stock", "etf"). Defaults to "stock",
|
|
242
|
+
supported types include:
|
|
243
|
+
- "stock": Stock symbols (e.g., AAPL, MSFT)
|
|
244
|
+
- "etf": Exchange-traded funds (e.g., SPY, QQQ)
|
|
245
|
+
- "future": Futures contracts (e.g., CL=F for crude oil)
|
|
246
|
+
- "forex": Forex pairs (e.g., EURUSD=X, USDJPY=X)
|
|
247
|
+
- "crypto": Cryptocurrency pairs (e.g., BTC-USD, ETH-USD)
|
|
248
|
+
- "index": Stock market indices (e.g., ^GSPC for S&P 500)
|
|
234
249
|
n_news (int, optional): The number of news headlines to return. Defaults to 10.
|
|
235
250
|
|
|
251
|
+
Note:
|
|
252
|
+
For commotities and bonds, use the "Future" asset type.
|
|
253
|
+
|
|
236
254
|
Returns:
|
|
237
255
|
list[str]: A list of Yahoo Finance news headlines relevant to the query.
|
|
238
256
|
"""
|
|
257
|
+
if asset_type == "forex" or asset_type == "future":
|
|
258
|
+
assert (
|
|
259
|
+
"=" in query
|
|
260
|
+
), "Forex query must contain '=' for currency pairs (e.g., EURUSD=X, CL=F)"
|
|
261
|
+
if asset_type == "crypto":
|
|
262
|
+
assert (
|
|
263
|
+
"-" in query
|
|
264
|
+
), "Crypto query must contain '-' for crypto pairs (e.g., BTC-USD, ETH-USD)"
|
|
265
|
+
if asset_type == "index":
|
|
266
|
+
assert query.startswith(
|
|
267
|
+
"^"
|
|
268
|
+
), "Index query must start with '^' (e.g., ^GSPC for S&P 500)"
|
|
239
269
|
url = (
|
|
240
270
|
f"https://finance.yahoo.com/quote/{query}/news"
|
|
241
|
-
if asset_type in ["stock", "etf"]
|
|
271
|
+
if asset_type in ["stock", "etf", "index", "future", "forex"]
|
|
242
272
|
else "https://finance.yahoo.com/news"
|
|
243
273
|
)
|
|
244
|
-
return self._fetch_news(url, query,
|
|
274
|
+
return self._fetch_news(url, query, n_news, "h3")
|
|
245
275
|
|
|
246
|
-
def get_google_finance_news(self, query, asset_type="stock", n_news=10):
|
|
276
|
+
def get_google_finance_news(self, query: str, asset_type="stock", n_news=10):
|
|
247
277
|
"""
|
|
248
278
|
Fetches recent Google Finance news headlines for a given financial asset.
|
|
249
279
|
|
|
250
280
|
Args:
|
|
251
281
|
query (str): The asset symbol or name (e.g., "AAPL").
|
|
252
282
|
asset_type (str, optional): The type of asset (e.g., "stock", "crypto"). Defaults to "stock".
|
|
283
|
+
Supported types include:
|
|
284
|
+
- "stock": Stock symbols (e.g., AAPL, MSFT)
|
|
285
|
+
- "etf": Exchange-traded funds (e.g., SPY, QQQ)
|
|
286
|
+
- "future": Futures contracts (e.g., CL=F or crude oil)
|
|
287
|
+
- "forex": Forex pairs (e.g., EURUSD, USDJPY)
|
|
288
|
+
- "crypto": Cryptocurrency pairs (e.g., BTCUSD, ETHUSD)
|
|
253
289
|
n_news (int, optional): The number of news headlines to return. Defaults to 10.
|
|
254
290
|
|
|
255
291
|
Returns:
|
|
@@ -258,20 +294,18 @@ class FinancialNews(object):
|
|
|
258
294
|
search_terms = {
|
|
259
295
|
"stock": f"{query} stock OR {query} shares OR {query} market",
|
|
260
296
|
"etf": f"{query} ETF OR {query} fund OR {query} exchange-traded fund",
|
|
261
|
-
"
|
|
262
|
-
"commodity": f"{query} price OR {query} futures OR {query} market",
|
|
297
|
+
"future": f"{query} futures OR {query} price OR {query} market",
|
|
263
298
|
"forex": f"{query} forex OR {query} exchange rate OR {query} market",
|
|
264
299
|
"crypto": f"{query} cryptocurrency OR {query} price OR {query} market",
|
|
265
|
-
"bond": f"{query} bond OR {query} yield OR {query} interest rate",
|
|
266
300
|
"index": f"{query} index OR {query} stock market OR {query} performance",
|
|
267
301
|
}
|
|
268
302
|
search_query = search_terms.get(asset_type, query)
|
|
269
303
|
url = f"https://news.google.com/search?q={search_query.replace(' ', '+')}"
|
|
270
|
-
return self._fetch_news(url, query,
|
|
304
|
+
return self._fetch_news(url, query, n_news, "a")
|
|
271
305
|
|
|
272
306
|
def get_reddit_posts(
|
|
273
307
|
self,
|
|
274
|
-
symbol,
|
|
308
|
+
symbol: str,
|
|
275
309
|
client_id=None,
|
|
276
310
|
client_secret=None,
|
|
277
311
|
user_agent=None,
|
|
@@ -294,8 +328,8 @@ class FinancialNews(object):
|
|
|
294
328
|
- "stock": Searches in stock-related subreddits (e.g., wallstreetbets, stocks).
|
|
295
329
|
- "forex": Searches in forex-related subreddits.
|
|
296
330
|
- "commodities": Searches in commodity-related subreddits (e.g., gold, oil).
|
|
297
|
-
- "
|
|
298
|
-
- "
|
|
331
|
+
- "etf": Searches in ETF-related subreddits.
|
|
332
|
+
- "future": Searches in futures and options trading subreddits.
|
|
299
333
|
- "crypto": Searches in cryptocurrency-related subreddits.
|
|
300
334
|
- If an unrecognized asset class is provided, defaults to stock-related subreddits.
|
|
301
335
|
n_posts (int, optional): The number of posts to return per subreddit. Defaults to 10.
|
|
@@ -317,15 +351,36 @@ class FinancialNews(object):
|
|
|
317
351
|
"""
|
|
318
352
|
|
|
319
353
|
reddit = praw.Reddit(
|
|
320
|
-
client_id=client_id,
|
|
354
|
+
client_id=client_id,
|
|
355
|
+
client_secret=client_secret,
|
|
356
|
+
user_agent=user_agent,
|
|
357
|
+
check_for_updates=False,
|
|
358
|
+
comment_kind="t1",
|
|
359
|
+
message_kind="t4",
|
|
360
|
+
redditor_kind="t2",
|
|
361
|
+
submission_kind="t3",
|
|
362
|
+
subreddit_kind="t5",
|
|
363
|
+
trophy_kind="t6",
|
|
364
|
+
oauth_url="https://oauth.reddit.com",
|
|
365
|
+
reddit_url="https://www.reddit.com",
|
|
366
|
+
short_url="https://redd.it",
|
|
367
|
+
timeout=16,
|
|
368
|
+
ratelimit_seconds=5,
|
|
321
369
|
)
|
|
322
|
-
|
|
370
|
+
assert reddit.read_only
|
|
323
371
|
subreddit_mapping = {
|
|
324
372
|
"stock": ["wallstreetbets", "stocks", "investing", "StockMarket"],
|
|
325
373
|
"forex": ["Forex", "ForexTrading", "DayTrading"],
|
|
326
|
-
"commodities": ["Commodities", "Gold", "Silverbugs", "oil"],
|
|
327
374
|
"etfs": ["ETFs", "investing"],
|
|
328
|
-
"futures": [
|
|
375
|
+
"futures": [
|
|
376
|
+
"FuturesTrading",
|
|
377
|
+
"OptionsTrading",
|
|
378
|
+
"DayTrading",
|
|
379
|
+
"Commodities",
|
|
380
|
+
"Gold",
|
|
381
|
+
"Silverbugs",
|
|
382
|
+
"oil",
|
|
383
|
+
],
|
|
329
384
|
"crypto": ["CryptoCurrency", "Bitcoin", "ethereum", "altcoin"],
|
|
330
385
|
}
|
|
331
386
|
try:
|
|
@@ -473,16 +528,23 @@ class FinancialNews(object):
|
|
|
473
528
|
maximum = 100
|
|
474
529
|
if limit > maximum:
|
|
475
530
|
raise ValueError(f"Number of total news articles allowed is {maximum}")
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
531
|
+
try:
|
|
532
|
+
response = requests.get(
|
|
533
|
+
"https://data-api.coindesk.com/news/v1/article/list",
|
|
534
|
+
params={"lang": lang, "limit": limit},
|
|
535
|
+
headers={"Content-type": "application/json; charset=UTF-8"},
|
|
536
|
+
)
|
|
537
|
+
response.raise_for_status()
|
|
538
|
+
json_response = response.json()
|
|
539
|
+
except requests.exceptions.RequestException:
|
|
485
540
|
return []
|
|
541
|
+
if (
|
|
542
|
+
response.status_code != 200
|
|
543
|
+
or "Data" not in json_response
|
|
544
|
+
or len(json_response["Data"]) == 0
|
|
545
|
+
):
|
|
546
|
+
return []
|
|
547
|
+
articles = json_response["Data"]
|
|
486
548
|
to_keep = [
|
|
487
549
|
"PUBLISHED_ON",
|
|
488
550
|
"TITLE",
|
|
@@ -495,10 +557,11 @@ class FinancialNews(object):
|
|
|
495
557
|
]
|
|
496
558
|
filtered_articles = []
|
|
497
559
|
for article in articles:
|
|
560
|
+
keys = article.keys()
|
|
498
561
|
filtered_articles.append(
|
|
499
562
|
{
|
|
500
563
|
k.lower(): article[k]
|
|
501
|
-
if k in
|
|
564
|
+
if k in keys and k != "PUBLISHED_ON"
|
|
502
565
|
else datetime.fromtimestamp(article[k])
|
|
503
566
|
for k in to_keep
|
|
504
567
|
if article[k] is not None and "sponsored" not in str(article[k])
|