bbstrader 0.3.1__tar.gz → 0.3.3__tar.gz
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-0.3.1/bbstrader.egg-info → bbstrader-0.3.3}/PKG-INFO +36 -41
- {bbstrader-0.3.1 → bbstrader-0.3.3}/README.md +1 -1
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/__init__.py +1 -1
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/__main__.py +7 -5
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/btengine/backtest.py +7 -8
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/btengine/data.py +3 -3
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/btengine/execution.py +2 -2
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/btengine/strategy.py +70 -17
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/config.py +2 -2
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/core/data.py +3 -1
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/core/scripts.py +62 -19
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/metatrader/account.py +108 -23
- bbstrader-0.3.3/bbstrader/metatrader/copier.py +1319 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/metatrader/rates.py +2 -2
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/metatrader/risk.py +1 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/metatrader/scripts.py +35 -9
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/metatrader/trade.py +60 -43
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/metatrader/utils.py +3 -5
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/models/__init__.py +0 -1
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/models/ml.py +55 -26
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/models/nlp.py +159 -89
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/models/optimization.py +1 -1
- bbstrader-0.3.3/bbstrader/models/risk.py +28 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/trading/execution.py +109 -50
- bbstrader-0.3.3/bbstrader/trading/strategies.py +292 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/tseries.py +39 -711
- {bbstrader-0.3.1 → bbstrader-0.3.3/bbstrader.egg-info}/PKG-INFO +36 -41
- bbstrader-0.3.3/bbstrader.egg-info/requires.txt +38 -0
- bbstrader-0.3.3/requirements.txt +35 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/setup.py +1 -3
- bbstrader-0.3.1/bbstrader/metatrader/copier.py +0 -846
- bbstrader-0.3.1/bbstrader/models/risk.py +0 -398
- bbstrader-0.3.1/bbstrader/trading/strategies.py +0 -875
- bbstrader-0.3.1/bbstrader.egg-info/requires.txt +0 -43
- bbstrader-0.3.1/requirements.txt +0 -40
- {bbstrader-0.3.1 → bbstrader-0.3.3}/LICENSE +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/MANIFEST.in +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/btengine/__init__.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/btengine/event.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/btengine/performance.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/btengine/portfolio.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/btengine/scripts.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/compat.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/core/__init__.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/core/utils.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/ibkr/__init__.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/ibkr/utils.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/metatrader/__init__.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/metatrader/analysis.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/models/factors.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/models/portfolio.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/trading/__init__.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/trading/scripts.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader/trading/utils.py +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader.egg-info/SOURCES.txt +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader.egg-info/dependency_links.txt +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader.egg-info/entry_points.txt +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/bbstrader.egg-info/top_level.txt +0 -0
- {bbstrader-0.3.1 → bbstrader-0.3.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Simplified Investment & Trading Toolkit
|
|
5
5
|
Home-page: https://github.com/bbalouki/bbstrader
|
|
6
6
|
Download-URL: https://pypi.org/project/bbstrader/
|
|
@@ -24,46 +24,41 @@ Classifier: Operating System :: MacOS
|
|
|
24
24
|
Classifier: License :: OSI Approved :: MIT License
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
|
+
Requires-Dist: alphalens-reloaded>=0.4.5
|
|
28
|
+
Requires-Dist: beautifulsoup4>=4.13.1
|
|
29
|
+
Requires-Dist: colorama>=0.4.6
|
|
30
|
+
Requires-Dist: CurrencyConverter>=0.18.2
|
|
31
|
+
Requires-Dist: dash>=2.18.2
|
|
32
|
+
Requires-Dist: eodhd>=1.0.32
|
|
33
|
+
Requires-Dist: exchange-calendars>=4.9
|
|
34
|
+
Requires-Dist: filterpy>=1.4.5
|
|
35
|
+
Requires-Dist: financetoolkit>=1.9.9
|
|
36
|
+
Requires-Dist: ipython>=8.32.0
|
|
37
|
+
Requires-Dist: lightgbm>=4.5.0
|
|
38
|
+
Requires-Dist: nltk>=3.9.1
|
|
39
|
+
Requires-Dist: notify-py>=0.3.43
|
|
27
40
|
Requires-Dist: numpy<2.0.0,>=1.26.0
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist:
|
|
34
|
-
Requires-Dist:
|
|
35
|
-
Requires-Dist:
|
|
36
|
-
Requires-Dist:
|
|
37
|
-
Requires-Dist:
|
|
38
|
-
Requires-Dist:
|
|
39
|
-
Requires-Dist:
|
|
40
|
-
Requires-Dist:
|
|
41
|
-
Requires-Dist:
|
|
42
|
-
Requires-Dist:
|
|
43
|
-
Requires-Dist:
|
|
44
|
-
Requires-Dist:
|
|
45
|
-
Requires-Dist:
|
|
46
|
-
Requires-Dist:
|
|
47
|
-
Requires-Dist:
|
|
48
|
-
Requires-Dist:
|
|
49
|
-
Requires-Dist: QuantStats
|
|
50
|
-
Requires-Dist: exchange-calendars
|
|
51
|
-
Requires-Dist: tqdm
|
|
52
|
-
Requires-Dist: notify-py
|
|
53
|
-
Requires-Dist: python-telegram-bot
|
|
54
|
-
Requires-Dist: eodhd
|
|
55
|
-
Requires-Dist: financetoolkit
|
|
56
|
-
Requires-Dist: PyYAML
|
|
57
|
-
Requires-Dist: tables
|
|
58
|
-
Requires-Dist: pyfiglet
|
|
59
|
-
Requires-Dist: colorama
|
|
60
|
-
Requires-Dist: praw
|
|
61
|
-
Requires-Dist: tweepy
|
|
62
|
-
Requires-Dist: beautifulsoup4
|
|
63
|
-
Requires-Dist: textblob
|
|
64
|
-
Requires-Dist: vaderSentiment
|
|
65
|
-
Requires-Dist: pytest
|
|
66
|
-
Requires-Dist: pytest-mock
|
|
41
|
+
Requires-Dist: pandas_ta>=0.3.14b0
|
|
42
|
+
Requires-Dist: praw>=7.8.1
|
|
43
|
+
Requires-Dist: pyfiglet>=1.0.2
|
|
44
|
+
Requires-Dist: pykalman>=0.10.1
|
|
45
|
+
Requires-Dist: pyportfolioopt>=1.5.6
|
|
46
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
47
|
+
Requires-Dist: python-telegram-bot>=21.10
|
|
48
|
+
Requires-Dist: PyYAML>=6.0.2
|
|
49
|
+
Requires-Dist: QuantStats>=0.0.64
|
|
50
|
+
Requires-Dist: scikit-learn>=1.6.1
|
|
51
|
+
Requires-Dist: seaborn>=0.13.2
|
|
52
|
+
Requires-Dist: spacy>=3.8.4
|
|
53
|
+
Requires-Dist: statsmodels>=0.14.4
|
|
54
|
+
Requires-Dist: sumy>=0.11.0
|
|
55
|
+
Requires-Dist: tables>=3.10.2
|
|
56
|
+
Requires-Dist: tabulate>=0.9.0
|
|
57
|
+
Requires-Dist: textblob>=0.19.0
|
|
58
|
+
Requires-Dist: tqdm>=4.67.1
|
|
59
|
+
Requires-Dist: tweepy>=4.15.0
|
|
60
|
+
Requires-Dist: vaderSentiment>=3.3.2
|
|
61
|
+
Requires-Dist: yfinance>=0.2.55
|
|
67
62
|
Provides-Extra: mt5
|
|
68
63
|
Requires-Dist: MetaTrader5; extra == "mt5"
|
|
69
64
|
Dynamic: author
|
|
@@ -385,7 +380,7 @@ You may need to create the `~/.bbstrader/` directory and its subdirectories (lik
|
|
|
385
380
|
|
|
386
381
|
For comprehensive information, including detailed API references, tutorials, and advanced guides for each module, please refer to our full documentation hosted on ReadTheDocs:
|
|
387
382
|
|
|
388
|
-
[**View the Full Documentation
|
|
383
|
+
[**View the Full Documentation**](https://bbstrader.readthedocs.io/en/latest/)
|
|
389
384
|
|
|
390
385
|
Additionally, the codebase is commented and includes docstrings, which can be a valuable resource for understanding the implementation details of specific functions and classes.
|
|
391
386
|
|
|
@@ -301,7 +301,7 @@ You may need to create the `~/.bbstrader/` directory and its subdirectories (lik
|
|
|
301
301
|
|
|
302
302
|
For comprehensive information, including detailed API references, tutorials, and advanced guides for each module, please refer to our full documentation hosted on ReadTheDocs:
|
|
303
303
|
|
|
304
|
-
[**View the Full Documentation
|
|
304
|
+
[**View the Full Documentation**](https://bbstrader.readthedocs.io/en/latest/)
|
|
305
305
|
|
|
306
306
|
Additionally, the codebase is commented and includes docstrings, which can be a valuable resource for understanding the implementation details of specific functions and classes.
|
|
307
307
|
|
|
@@ -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.3.
|
|
10
|
+
__version__ = "0.3.2"
|
|
11
11
|
|
|
12
12
|
from bbstrader import compat # noqa: F401
|
|
13
13
|
from bbstrader import core # noqa: F401
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import multiprocessing
|
|
2
3
|
import sys
|
|
3
4
|
from enum import Enum
|
|
4
5
|
|
|
@@ -11,7 +12,7 @@ from bbstrader.metatrader.scripts import copy_trades
|
|
|
11
12
|
from bbstrader.trading.scripts import execute_strategy
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
class
|
|
15
|
+
class _Module(Enum):
|
|
15
16
|
COPIER = "copier"
|
|
16
17
|
BACKTEST = "backtest"
|
|
17
18
|
EXECUTION = "execution"
|
|
@@ -51,13 +52,13 @@ def main():
|
|
|
51
52
|
sys.exit(0)
|
|
52
53
|
try:
|
|
53
54
|
match args.run:
|
|
54
|
-
case
|
|
55
|
+
case _Module.COPIER.value:
|
|
55
56
|
copy_trades(unknown)
|
|
56
|
-
case
|
|
57
|
+
case _Module.BACKTEST.value:
|
|
57
58
|
backtest(unknown)
|
|
58
|
-
case
|
|
59
|
+
case _Module.EXECUTION.value:
|
|
59
60
|
execute_strategy(unknown)
|
|
60
|
-
case
|
|
61
|
+
case _Module.NEWS_FEED.value:
|
|
61
62
|
send_news_feed(unknown)
|
|
62
63
|
case _:
|
|
63
64
|
print(Fore.RED + f"Unknown module: {args.run}")
|
|
@@ -69,4 +70,5 @@ def main():
|
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
if __name__ == "__main__":
|
|
73
|
+
multiprocessing.freeze_support()
|
|
72
74
|
main()
|
|
@@ -248,14 +248,13 @@ def run_backtest(
|
|
|
248
248
|
|
|
249
249
|
start_date (datetime): Start date of the backtest.
|
|
250
250
|
|
|
251
|
-
data_handler (DataHandler):
|
|
251
|
+
data_handler (DataHandler): A subclass of the `DataHandler` class, responsible for managing
|
|
252
252
|
and processing market data. Available options include `CSVDataHandler`,
|
|
253
|
-
`MT5DataHandler`, and `YFDataHandler`.
|
|
254
|
-
instance is initialized before passing it to the function.
|
|
253
|
+
`MT5DataHandler`, and `YFDataHandler`.
|
|
255
254
|
|
|
256
255
|
strategy (Strategy): The trading strategy to be employed during the backtest.
|
|
257
|
-
The strategy must be
|
|
258
|
-
- `bars` (DataHandler): The `DataHandler`
|
|
256
|
+
The strategy must be a subclass of `Strategy` and should include the following attributes:
|
|
257
|
+
- `bars` (DataHandler): The `DataHandler` class for the strategy.
|
|
259
258
|
- `events` (Queue): Queue instance for managing events.
|
|
260
259
|
- `symbol_list` (List[str]): List of symbols to trade.
|
|
261
260
|
- `mode` (str): 'live' or 'backtest'.
|
|
@@ -307,9 +306,9 @@ def run_backtest(
|
|
|
307
306
|
>>> run_backtest(
|
|
308
307
|
... symbol_list=symbol_list,
|
|
309
308
|
... start_date=start,
|
|
310
|
-
... data_handler=MT5DataHandler
|
|
311
|
-
... strategy=StockIndexSTBOTrading
|
|
312
|
-
... exc_handler=MT5ExecutionHandler
|
|
309
|
+
... data_handler=MT5DataHandler,
|
|
310
|
+
... strategy=StockIndexSTBOTrading,
|
|
311
|
+
... exc_handler=MT5ExecutionHandler,
|
|
313
312
|
... initial_capital=100000.0,
|
|
314
313
|
... heartbeat=0.0,
|
|
315
314
|
... **kwargs
|
|
@@ -361,7 +361,7 @@ class CSVDataHandler(BaseCSVDataHandler):
|
|
|
361
361
|
csv_dir (str): Absolute directory path to the CSV files.
|
|
362
362
|
|
|
363
363
|
NOTE:
|
|
364
|
-
All csv fille can be
|
|
364
|
+
All csv fille can be stored in 'Home/.bbstrader/data/csv_data'
|
|
365
365
|
|
|
366
366
|
"""
|
|
367
367
|
csv_dir = kwargs.get("csv_dir")
|
|
@@ -580,7 +580,7 @@ class EODHDataHandler(BaseCSVDataHandler):
|
|
|
580
580
|
to_unix_time=unix_end,
|
|
581
581
|
)
|
|
582
582
|
|
|
583
|
-
def
|
|
583
|
+
def _format_data(self, data: List[Dict] | pd.DataFrame) -> pd.DataFrame:
|
|
584
584
|
if isinstance(data, pd.DataFrame):
|
|
585
585
|
if data.empty or len(data) == 0:
|
|
586
586
|
raise ValueError("No data found.")
|
|
@@ -608,7 +608,7 @@ class EODHDataHandler(BaseCSVDataHandler):
|
|
|
608
608
|
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
609
609
|
try:
|
|
610
610
|
data = self._get_data(symbol, self.period)
|
|
611
|
-
data = self.
|
|
611
|
+
data = self._format_data(data)
|
|
612
612
|
data.to_csv(filepath)
|
|
613
613
|
except Exception as e:
|
|
614
614
|
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
@@ -98,7 +98,7 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
98
98
|
self.events.put(fill_event)
|
|
99
99
|
self.logger.info(
|
|
100
100
|
f"{event.direction} ORDER FILLED: SYMBOL={event.symbol}, "
|
|
101
|
-
f"QUANTITY={event.quantity}, PRICE @{event.price} EXCHANGE={fill_event.exchange}",
|
|
101
|
+
f"QUANTITY={event.quantity}, PRICE @{round(event.price, 5)} EXCHANGE={fill_event.exchange}",
|
|
102
102
|
custom_time=fill_event.timeindex,
|
|
103
103
|
)
|
|
104
104
|
|
|
@@ -264,7 +264,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
264
264
|
self.events.put(fill_event)
|
|
265
265
|
self.logger.info(
|
|
266
266
|
f"{direction} ORDER FILLED: SYMBOL={symbol}, QUANTITY={quantity}, "
|
|
267
|
-
f"PRICE @{price} EXCHANGE={fill_event.exchange}",
|
|
267
|
+
f"PRICE @{round(event.price, 5)} EXCHANGE={fill_event.exchange}",
|
|
268
268
|
custom_time=fill_event.timeindex,
|
|
269
269
|
)
|
|
270
270
|
|
|
@@ -12,17 +12,18 @@ from loguru import logger
|
|
|
12
12
|
from bbstrader.btengine.data import DataHandler
|
|
13
13
|
from bbstrader.btengine.event import Events, FillEvent, SignalEvent
|
|
14
14
|
from bbstrader.config import BBSTRADER_DIR
|
|
15
|
-
from bbstrader.metatrader
|
|
15
|
+
from bbstrader.metatrader import (
|
|
16
16
|
Account,
|
|
17
17
|
AdmiralMarktsGroup,
|
|
18
18
|
PepperstoneGroupLimited,
|
|
19
|
+
TradeOrder,
|
|
20
|
+
Rates,
|
|
21
|
+
TradeSignal,
|
|
22
|
+
TradingMode,
|
|
23
|
+
SymbolType
|
|
19
24
|
)
|
|
20
|
-
from bbstrader.metatrader.utils import SymbolType
|
|
21
|
-
from bbstrader.metatrader.rates import Rates
|
|
22
|
-
from bbstrader.metatrader.trade import TradeSignal, TradingMode
|
|
23
25
|
from bbstrader.models.optimization import optimized_weights
|
|
24
26
|
|
|
25
|
-
|
|
26
27
|
__all__ = ["Strategy", "MT5Strategy"]
|
|
27
28
|
|
|
28
29
|
logger.add(
|
|
@@ -71,8 +72,13 @@ class MT5Strategy(Strategy):
|
|
|
71
72
|
calculate signals for the MetaTrader 5 trading platform. The signals
|
|
72
73
|
are generated by the `MT5Strategy` object and sent to the the `Mt5ExecutionEngine`
|
|
73
74
|
for live trading and `MT5BacktestEngine` objects for backtesting.
|
|
74
|
-
"""
|
|
75
75
|
|
|
76
|
+
# NOTE
|
|
77
|
+
It is recommanded that every strategy specfic method to be a private method
|
|
78
|
+
in order to avoid naming collusion.
|
|
79
|
+
"""
|
|
80
|
+
tf: str
|
|
81
|
+
max_trades: Dict[str, int]
|
|
76
82
|
def __init__(
|
|
77
83
|
self,
|
|
78
84
|
events: Queue = None,
|
|
@@ -107,9 +113,11 @@ class MT5Strategy(Strategy):
|
|
|
107
113
|
self._initialize_portfolio()
|
|
108
114
|
self.kwargs = kwargs
|
|
109
115
|
self.periodes = 0
|
|
110
|
-
|
|
111
|
-
@property
|
|
116
|
+
|
|
117
|
+
@property
|
|
112
118
|
def account(self):
|
|
119
|
+
if self.mode != TradingMode.LIVE:
|
|
120
|
+
raise ValueError("account attribute is only allowed in Live mode")
|
|
113
121
|
return Account(**self.kwargs)
|
|
114
122
|
|
|
115
123
|
@property
|
|
@@ -127,7 +135,7 @@ class MT5Strategy(Strategy):
|
|
|
127
135
|
@property
|
|
128
136
|
def orders(self):
|
|
129
137
|
if self.mode == TradingMode.LIVE:
|
|
130
|
-
return self.account.get_orders()
|
|
138
|
+
return self.account.get_orders() or []
|
|
131
139
|
return self._orders
|
|
132
140
|
|
|
133
141
|
@property
|
|
@@ -139,7 +147,7 @@ class MT5Strategy(Strategy):
|
|
|
139
147
|
@property
|
|
140
148
|
def positions(self):
|
|
141
149
|
if self.mode == TradingMode.LIVE:
|
|
142
|
-
return self.account.get_positions()
|
|
150
|
+
return self.account.get_positions() or []
|
|
143
151
|
return self._positions
|
|
144
152
|
|
|
145
153
|
@property
|
|
@@ -345,7 +353,7 @@ class MT5Strategy(Strategy):
|
|
|
345
353
|
log = True
|
|
346
354
|
if log:
|
|
347
355
|
self.logger.info(
|
|
348
|
-
f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}",
|
|
356
|
+
f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{round(price, 5)}",
|
|
349
357
|
custom_time=dtime,
|
|
350
358
|
)
|
|
351
359
|
|
|
@@ -526,7 +534,7 @@ class MT5Strategy(Strategy):
|
|
|
526
534
|
def logmsg(order, type, symbol, dtime):
|
|
527
535
|
return self.logger.info(
|
|
528
536
|
f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
529
|
-
f"PRICE @ {order.price}",
|
|
537
|
+
f"PRICE @ {round(order.price, 5)}",
|
|
530
538
|
custom_time=dtime,
|
|
531
539
|
)
|
|
532
540
|
|
|
@@ -614,7 +622,7 @@ class MT5Strategy(Strategy):
|
|
|
614
622
|
)
|
|
615
623
|
|
|
616
624
|
@staticmethod
|
|
617
|
-
def calculate_pct_change(current_price, lh_price):
|
|
625
|
+
def calculate_pct_change(current_price, lh_price) -> float:
|
|
618
626
|
return ((current_price - lh_price) / lh_price) * 100
|
|
619
627
|
|
|
620
628
|
def get_asset_values(
|
|
@@ -649,8 +657,8 @@ class MT5Strategy(Strategy):
|
|
|
649
657
|
In Live mode, the `bbstrader.metatrader.rates.Rates` class is used to get the historical data
|
|
650
658
|
so the value_type must be 'returns', 'open', 'high', 'low', 'close', 'adjclose', 'volume'.
|
|
651
659
|
"""
|
|
652
|
-
if mode not in [
|
|
653
|
-
raise ValueError("Mode must be
|
|
660
|
+
if mode not in [TradingMode.BACKTEST, TradingMode.LIVE]:
|
|
661
|
+
raise ValueError("Mode must be an instance of TradingMode")
|
|
654
662
|
asset_values = {}
|
|
655
663
|
if mode == TradingMode.BACKTEST:
|
|
656
664
|
if bars is None:
|
|
@@ -696,7 +704,7 @@ class MT5Strategy(Strategy):
|
|
|
696
704
|
if period_count == 0 or period_count is None:
|
|
697
705
|
return True
|
|
698
706
|
return period_count % signal_inverval == 0
|
|
699
|
-
|
|
707
|
+
|
|
700
708
|
@staticmethod
|
|
701
709
|
def stop_time(time_zone: str, stop_time: str) -> bool:
|
|
702
710
|
now = datetime.now(pytz.timezone(time_zone)).time()
|
|
@@ -760,6 +768,49 @@ class MT5Strategy(Strategy):
|
|
|
760
768
|
)
|
|
761
769
|
return prices
|
|
762
770
|
return np.array([])
|
|
771
|
+
|
|
772
|
+
def get_active_orders(self, symbol: str, strategy_id: int, order_type: int = None) -> List[TradeOrder]:
|
|
773
|
+
"""
|
|
774
|
+
Get the active orders for a given symbol and strategy.
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
symbol : The symbol for the trade.
|
|
778
|
+
strategy_id : The unique identifier for the strategy.
|
|
779
|
+
order_type : The type of order to filter by (optional):
|
|
780
|
+
"BUY_LIMIT": 2
|
|
781
|
+
"SELL_LIMIT": 3
|
|
782
|
+
"BUY_STOP": 4
|
|
783
|
+
"SELL_STOP": 5
|
|
784
|
+
"BUY_STOP_LIMIT": 6
|
|
785
|
+
"SELL_STOP_LIMIT": 7
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
List[TradeOrder] : A list of active orders for the given symbol and strategy.
|
|
789
|
+
"""
|
|
790
|
+
orders = [o for o in self.orders if o.symbol == symbol and o.magic == strategy_id]
|
|
791
|
+
if order_type is not None and len(orders) > 0:
|
|
792
|
+
orders = [o for o in orders if o.type == order_type]
|
|
793
|
+
return orders
|
|
794
|
+
|
|
795
|
+
def exit_positions(self, position, prices, asset, th: float = 0.01):
|
|
796
|
+
if len(prices) == 0:
|
|
797
|
+
return False
|
|
798
|
+
tick_info = self.account.get_tick_info(asset)
|
|
799
|
+
bid, ask = tick_info.bid, tick_info.ask
|
|
800
|
+
price = None
|
|
801
|
+
if len(prices) == 1:
|
|
802
|
+
price = prices[0]
|
|
803
|
+
elif len(prices) in range(2, self.max_trades[asset] + 1):
|
|
804
|
+
price = np.mean(prices)
|
|
805
|
+
if price is not None:
|
|
806
|
+
if (
|
|
807
|
+
position == 0
|
|
808
|
+
and self.calculate_pct_change(ask, price) >= th
|
|
809
|
+
or position == 1
|
|
810
|
+
and abs(self.calculate_pct_change(bid, price)) >= th
|
|
811
|
+
):
|
|
812
|
+
return True
|
|
813
|
+
return False
|
|
763
814
|
|
|
764
815
|
@staticmethod
|
|
765
816
|
def get_current_dt(time_zone: str = "US/Eastern") -> datetime:
|
|
@@ -796,7 +847,9 @@ class MT5Strategy(Strategy):
|
|
|
796
847
|
return dt_to
|
|
797
848
|
|
|
798
849
|
@staticmethod
|
|
799
|
-
def get_mt5_equivalent(
|
|
850
|
+
def get_mt5_equivalent(
|
|
851
|
+
symbols, symbol_type: str | SymbolType = SymbolType.STOCKS, **kwargs
|
|
852
|
+
) -> List[str]:
|
|
800
853
|
"""
|
|
801
854
|
Get the MetaTrader 5 equivalent symbols for the symbols in the list.
|
|
802
855
|
This method is used to get the symbols that are available on the MetaTrader 5 platform.
|
|
@@ -3,8 +3,8 @@ from pathlib import Path
|
|
|
3
3
|
from typing import List
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
TERMINAL = "
|
|
7
|
-
BASE_FOLDER = "C
|
|
6
|
+
TERMINAL = "/terminal64.exe"
|
|
7
|
+
BASE_FOLDER = "C:/Program Files/"
|
|
8
8
|
|
|
9
9
|
AMG_PATH = BASE_FOLDER + "Admirals Group MT5 Terminal" + TERMINAL
|
|
10
10
|
PGL_PATH = BASE_FOLDER + "Pepperstone MetaTrader 5" + TERMINAL
|
|
@@ -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),
|
|
@@ -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)
|