bbstrader 0.3.4__tar.gz → 0.3.5__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.4/bbstrader.egg-info → bbstrader-0.3.5}/PKG-INFO +24 -27
- {bbstrader-0.3.4 → bbstrader-0.3.5}/README.md +1 -1
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/__init__.py +1 -1
- bbstrader-0.3.5/bbstrader/compat.py +27 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/config.py +0 -16
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/core/scripts.py +4 -3
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/metatrader/copier.py +61 -18
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/metatrader/trade.py +28 -24
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/models/factors.py +17 -13
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/models/ml.py +96 -49
- {bbstrader-0.3.4 → bbstrader-0.3.5/bbstrader.egg-info}/PKG-INFO +24 -27
- bbstrader-0.3.5/bbstrader.egg-info/requires.txt +38 -0
- bbstrader-0.3.5/requirements.txt +35 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/setup.py +6 -7
- bbstrader-0.3.4/bbstrader/compat.py +0 -19
- bbstrader-0.3.4/bbstrader.egg-info/requires.txt +0 -38
- bbstrader-0.3.4/requirements.txt +0 -35
- {bbstrader-0.3.4 → bbstrader-0.3.5}/LICENSE +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/MANIFEST.in +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/__main__.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/apps/__init__.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/apps/_copier.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/btengine/__init__.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/btengine/backtest.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/btengine/data.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/btengine/event.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/btengine/execution.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/btengine/performance.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/btengine/portfolio.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/btengine/scripts.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/btengine/strategy.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/core/__init__.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/core/data.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/core/utils.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/ibkr/__init__.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/ibkr/utils.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/metatrader/__init__.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/metatrader/account.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/metatrader/analysis.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/metatrader/rates.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/metatrader/risk.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/metatrader/scripts.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/metatrader/utils.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/models/__init__.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/models/nlp.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/models/optimization.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/models/portfolio.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/models/risk.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/trading/__init__.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/trading/execution.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/trading/scripts.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/trading/strategies.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/trading/utils.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader/tseries.py +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader.egg-info/SOURCES.txt +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader.egg-info/dependency_links.txt +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader.egg-info/entry_points.txt +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/bbstrader.egg-info/top_level.txt +0 -0
- {bbstrader-0.3.4 → bbstrader-0.3.5}/setup.cfg +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
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/
|
|
7
7
|
Author: Bertin Balouki SIMYELI
|
|
8
8
|
Author-email: <bertin@bbstrader.com>
|
|
9
9
|
Maintainer: Bertin Balouki SIMYELI
|
|
10
|
-
License:
|
|
10
|
+
License: MIT
|
|
11
11
|
Project-URL: Documentation, https://bbstrader.readthedocs.io/en/latest/
|
|
12
12
|
Project-URL: Source Code, https://github.com/bbalouki/bbstrader
|
|
13
13
|
Keywords: Finance,Toolkit,Financial,Analysis,Fundamental,Quantitative,Database,Equities,Currencies,Economics,ETFs,Funds,Indices,Moneymarkets,Commodities,Futures,CFDs,Derivatives,Trading,Investing,Portfolio,Optimization,Performance
|
|
@@ -15,50 +15,47 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
17
17
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
19
|
Classifier: Operating System :: Microsoft :: Windows
|
|
22
20
|
Classifier: Operating System :: POSIX :: Linux
|
|
23
21
|
Classifier: Operating System :: MacOS
|
|
24
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
25
22
|
Description-Content-Type: text/markdown
|
|
26
23
|
License-File: LICENSE
|
|
27
|
-
Requires-Dist: alphalens-reloaded>=0.4.
|
|
28
|
-
Requires-Dist: beautifulsoup4>=4.13.
|
|
24
|
+
Requires-Dist: alphalens-reloaded>=0.4.6
|
|
25
|
+
Requires-Dist: beautifulsoup4>=4.13.5
|
|
29
26
|
Requires-Dist: colorama>=0.4.6
|
|
30
|
-
Requires-Dist: CurrencyConverter>=0.18.
|
|
31
|
-
Requires-Dist: dash>=2.
|
|
27
|
+
Requires-Dist: CurrencyConverter>=0.18.9
|
|
28
|
+
Requires-Dist: dash>=3.2.0
|
|
32
29
|
Requires-Dist: eodhd>=1.0.32
|
|
33
|
-
Requires-Dist:
|
|
30
|
+
Requires-Dist: exchange_calendars>=4.11.1
|
|
34
31
|
Requires-Dist: filterpy>=1.4.5
|
|
35
|
-
Requires-Dist: financetoolkit>=
|
|
36
|
-
Requires-Dist: ipython>=
|
|
37
|
-
Requires-Dist: lightgbm>=4.
|
|
32
|
+
Requires-Dist: financetoolkit>=2.0.4
|
|
33
|
+
Requires-Dist: ipython>=9.5.0
|
|
34
|
+
Requires-Dist: lightgbm>=4.6.0
|
|
38
35
|
Requires-Dist: nltk>=3.9.1
|
|
39
|
-
Requires-Dist:
|
|
40
|
-
Requires-Dist: numpy>=
|
|
41
|
-
Requires-Dist:
|
|
36
|
+
Requires-Dist: notify_py>=0.3.43
|
|
37
|
+
Requires-Dist: numpy>=2.2.6
|
|
38
|
+
Requires-Dist: pandas-ta>=0.4.67b0
|
|
42
39
|
Requires-Dist: praw>=7.8.1
|
|
43
|
-
Requires-Dist: pyfiglet>=1.0.
|
|
44
|
-
Requires-Dist: pykalman>=0.10.
|
|
40
|
+
Requires-Dist: pyfiglet>=1.0.4
|
|
41
|
+
Requires-Dist: pykalman>=0.10.2
|
|
45
42
|
Requires-Dist: pyportfolioopt>=1.5.6
|
|
46
|
-
Requires-Dist: python-dotenv>=1.
|
|
47
|
-
Requires-Dist: python-telegram-bot>=
|
|
43
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
44
|
+
Requires-Dist: python-telegram-bot>=22.3
|
|
48
45
|
Requires-Dist: PyYAML>=6.0.2
|
|
49
|
-
Requires-Dist: QuantStats>=0.0.
|
|
50
|
-
Requires-Dist: scikit-learn>=1.
|
|
46
|
+
Requires-Dist: QuantStats>=0.0.77
|
|
47
|
+
Requires-Dist: scikit-learn>=1.7.2
|
|
51
48
|
Requires-Dist: seaborn>=0.13.2
|
|
52
|
-
Requires-Dist: spacy>=3.8.
|
|
53
|
-
Requires-Dist: statsmodels>=0.14.
|
|
49
|
+
Requires-Dist: spacy>=3.8.7
|
|
50
|
+
Requires-Dist: statsmodels>=0.14.5
|
|
54
51
|
Requires-Dist: sumy>=0.11.0
|
|
55
52
|
Requires-Dist: tables>=3.10.2
|
|
56
53
|
Requires-Dist: tabulate>=0.9.0
|
|
57
54
|
Requires-Dist: textblob>=0.19.0
|
|
58
55
|
Requires-Dist: tqdm>=4.67.1
|
|
59
|
-
Requires-Dist: tweepy>=4.
|
|
56
|
+
Requires-Dist: tweepy>=4.16.0
|
|
60
57
|
Requires-Dist: vaderSentiment>=3.3.2
|
|
61
|
-
Requires-Dist: yfinance>=0.2.
|
|
58
|
+
Requires-Dist: yfinance>=0.2.65
|
|
62
59
|
Provides-Extra: mt5
|
|
63
60
|
Requires-Dist: MetaTrader5; extra == "mt5"
|
|
64
61
|
Dynamic: author
|
|
@@ -84,7 +81,7 @@ Dynamic: summary
|
|
|
84
81
|
[](https://pypi.org/project/bbstrader/)
|
|
85
82
|
[](https://pepy.tech/projects/bbstrader)
|
|
86
83
|
[](https://www.codefactor.io/repository/github/bbalouki/bbstrader)
|
|
87
|
-
[](https://www.linkedin.com/in/bertin-balouki-
|
|
84
|
+
[](https://www.linkedin.com/in/bertin-balouki-s-15b17a1a6)
|
|
88
85
|
[](https://paypal.me/bertinbalouki?country.x=SN&locale.x=en_US)
|
|
89
86
|
|
|
90
87
|
[Dcoumentation](https://bbstrader.readthedocs.io/en/latest/index.html)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://pypi.org/project/bbstrader/)
|
|
6
6
|
[](https://pepy.tech/projects/bbstrader)
|
|
7
7
|
[](https://www.codefactor.io/repository/github/bbalouki/bbstrader)
|
|
8
|
-
[](https://www.linkedin.com/in/bertin-balouki-
|
|
8
|
+
[](https://www.linkedin.com/in/bertin-balouki-s-15b17a1a6)
|
|
9
9
|
[](https://paypal.me/bertinbalouki?country.x=SN&locale.x=en_US)
|
|
10
10
|
|
|
11
11
|
[Dcoumentation](https://bbstrader.readthedocs.io/en/latest/index.html)
|
|
@@ -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.5"
|
|
11
11
|
|
|
12
12
|
from bbstrader import compat # noqa: F401
|
|
13
13
|
from bbstrader import core # noqa: F401
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def setup_mock_modules():
|
|
6
|
+
"""Mock some modules not available on some OS to prevent import errors."""
|
|
7
|
+
from unittest.mock import MagicMock
|
|
8
|
+
|
|
9
|
+
class Mock(MagicMock):
|
|
10
|
+
@classmethod
|
|
11
|
+
def __getattr__(cls, name):
|
|
12
|
+
return MagicMock()
|
|
13
|
+
|
|
14
|
+
MOCK_MODULES = []
|
|
15
|
+
|
|
16
|
+
# Mock Metatrader5 on Linux and MacOS
|
|
17
|
+
if platform.system() != "Windows":
|
|
18
|
+
MOCK_MODULES.append("MetaTrader5")
|
|
19
|
+
|
|
20
|
+
# Mock posix On windows
|
|
21
|
+
if platform.system() == "Windows":
|
|
22
|
+
MOCK_MODULES.append("posix")
|
|
23
|
+
|
|
24
|
+
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
setup_mock_modules()
|
|
@@ -3,22 +3,6 @@ from pathlib import Path
|
|
|
3
3
|
from typing import List
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
TERMINAL = "/terminal64.exe"
|
|
7
|
-
BASE_FOLDER = "C:/Program Files/"
|
|
8
|
-
|
|
9
|
-
AMG_PATH = BASE_FOLDER + "Admirals Group MT5 Terminal" + TERMINAL
|
|
10
|
-
PGL_PATH = BASE_FOLDER + "Pepperstone MetaTrader 5" + TERMINAL
|
|
11
|
-
FTMO_PATH = BASE_FOLDER + "FTMO MetaTrader 5" + TERMINAL
|
|
12
|
-
JGM_PATH = BASE_FOLDER + "JustMarkets MetaTrader 5" + TERMINAL
|
|
13
|
-
|
|
14
|
-
BROKERS_PATHS = {
|
|
15
|
-
"AMG": AMG_PATH,
|
|
16
|
-
"FTMO": FTMO_PATH,
|
|
17
|
-
"PGL": PGL_PATH,
|
|
18
|
-
"JGM": JGM_PATH,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
6
|
def get_config_dir(name: str = ".bbstrader") -> Path:
|
|
23
7
|
"""
|
|
24
8
|
Get the path to the configuration directory.
|
|
@@ -141,18 +141,19 @@ def send_news_feed(unknown):
|
|
|
141
141
|
|
|
142
142
|
nltk.download("punkt", quiet=True)
|
|
143
143
|
news = FinancialNews()
|
|
144
|
+
fmp_news = news.get_fmp_news(api=args.fmp) if args.fmp else None
|
|
144
145
|
logger.info(f"Starting the News Feed on {args.interval} minutes")
|
|
145
146
|
while True:
|
|
146
147
|
try:
|
|
147
148
|
fmp_articles = []
|
|
148
|
-
|
|
149
|
-
if args.fmp:
|
|
149
|
+
if fmp_news is not None:
|
|
150
150
|
start = datetime.now() - timedelta(minutes=args.interval)
|
|
151
151
|
start = start.strftime("%Y-%m-%d %H:%M:%S")
|
|
152
152
|
end = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
153
|
-
fmp_articles =
|
|
153
|
+
fmp_articles = fmp_news.get_latest_articles(
|
|
154
154
|
save=True, start=start, end=end
|
|
155
155
|
)
|
|
156
|
+
coindesk_articles = news.get_coindesk_news(query=args.query)
|
|
156
157
|
if len(coindesk_articles) != 0:
|
|
157
158
|
asyncio.run(
|
|
158
159
|
send_articles(
|
|
@@ -51,6 +51,31 @@ ORDER_TYPE = {
|
|
|
51
51
|
7: (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, "SELL STOP LIMIT"),
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
STOP_RETCODES = [
|
|
55
|
+
Mt5.TRADE_RETCODE_TRADE_DISABLED,
|
|
56
|
+
Mt5.TRADE_RETCODE_NO_MONEY,
|
|
57
|
+
Mt5.TRADE_RETCODE_SERVER_DISABLES_AT,
|
|
58
|
+
Mt5.TRADE_RETCODE_CLIENT_DISABLES_AT,
|
|
59
|
+
Mt5.TRADE_RETCODE_ONLY_REAL,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
RETURN_RETCODE = [
|
|
63
|
+
Mt5.TRADE_RETCODE_MARKET_CLOSED,
|
|
64
|
+
Mt5.TRADE_RETCODE_CONNECTION,
|
|
65
|
+
Mt5.TRADE_RETCODE_LIMIT_ORDERS,
|
|
66
|
+
Mt5.TRADE_RETCODE_LIMIT_VOLUME,
|
|
67
|
+
Mt5.TRADE_RETCODE_LIMIT_POSITIONS,
|
|
68
|
+
Mt5.TRADE_RETCODE_LONG_ONLY,
|
|
69
|
+
Mt5.TRADE_RETCODE_SHORT_ONLY,
|
|
70
|
+
Mt5.TRADE_RETCODE_CLOSE_ONLY,
|
|
71
|
+
Mt5.TRADE_RETCODE_FIFO_CLOSE,
|
|
72
|
+
Mt5.TRADE_RETCODE_INVALID_VOLUME,
|
|
73
|
+
Mt5.TRADE_RETCODE_INVALID_PRICE,
|
|
74
|
+
Mt5.TRADE_RETCODE_INVALID_STOPS,
|
|
75
|
+
Mt5.TRADE_RETCODE_NO_CHANGES
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
54
79
|
|
|
55
80
|
class OrderAction(Enum):
|
|
56
81
|
COPY_NEW = "COPY_NEW"
|
|
@@ -65,25 +90,23 @@ CopyMode = Literal["fix", "multiply", "percentage", "dynamic", "replicate"]
|
|
|
65
90
|
|
|
66
91
|
def fix_lot(fixed):
|
|
67
92
|
if fixed == 0 or fixed is None:
|
|
68
|
-
raise ValueError("Fixed lot must be a number")
|
|
93
|
+
raise ValueError("Fixed lot must be a number > 0")
|
|
69
94
|
return fixed
|
|
70
95
|
|
|
71
96
|
|
|
72
97
|
def multiply_lot(lot, multiplier):
|
|
73
98
|
if multiplier == 0 or multiplier is None:
|
|
74
|
-
raise ValueError("Multiplier lot must be a number")
|
|
99
|
+
raise ValueError("Multiplier lot must be a number > 0")
|
|
75
100
|
return lot * multiplier
|
|
76
101
|
|
|
77
102
|
|
|
78
103
|
def percentage_lot(lot, percentage):
|
|
79
104
|
if percentage == 0 or percentage is None:
|
|
80
|
-
raise ValueError("Percentage lot must be a number")
|
|
105
|
+
raise ValueError("Percentage lot must be a number > 0")
|
|
81
106
|
return round(lot * percentage / 100, 2)
|
|
82
107
|
|
|
83
108
|
|
|
84
109
|
def dynamic_lot(source_lot, source_eqty: float, dest_eqty: float):
|
|
85
|
-
if source_eqty == 0 or dest_eqty == 0:
|
|
86
|
-
raise ValueError("Source or destination account equity is zero")
|
|
87
110
|
try:
|
|
88
111
|
ratio = dest_eqty / source_eqty
|
|
89
112
|
return round(source_lot * ratio, 2)
|
|
@@ -118,6 +141,7 @@ def fixed_lot(lot, symbol, destination) -> float:
|
|
|
118
141
|
else:
|
|
119
142
|
return _check_lot(round(lot), s_info)
|
|
120
143
|
|
|
144
|
+
|
|
121
145
|
def calculate_copy_lot(
|
|
122
146
|
source_lot,
|
|
123
147
|
symbol: str,
|
|
@@ -173,7 +197,7 @@ def get_symbols_from_string(symbols_string: str):
|
|
|
173
197
|
|
|
174
198
|
def get_copy_symbols(destination: dict, source: dict) -> List[str] | Dict[str, str]:
|
|
175
199
|
symbols = destination.get("symbols", "all")
|
|
176
|
-
if symbols == "all" or symbols == "*" or
|
|
200
|
+
if symbols == "all" or symbols == "*" or isinstance(symbols, list):
|
|
177
201
|
src_account = Account(**source)
|
|
178
202
|
src_symbols = src_account.get_symbols()
|
|
179
203
|
dest_account = Account(**destination)
|
|
@@ -360,7 +384,10 @@ class TradeCopier(object):
|
|
|
360
384
|
for destination in self.destinations:
|
|
361
385
|
destination["copy"] = destination.get("copy", True)
|
|
362
386
|
|
|
363
|
-
def log_message(
|
|
387
|
+
def log_message(
|
|
388
|
+
self, message, type: Literal["info", "error", "debug", "warning"] = "info"
|
|
389
|
+
):
|
|
390
|
+
logger.trace
|
|
364
391
|
if self.log_queue:
|
|
365
392
|
try:
|
|
366
393
|
now = datetime.now()
|
|
@@ -370,7 +397,7 @@ class TradeCopier(object):
|
|
|
370
397
|
)
|
|
371
398
|
space = len("exception") # longest log name
|
|
372
399
|
self.log_queue.put(
|
|
373
|
-
f"{formatted} |{type.upper()} {' '*(space - len(type))} | - {message}"
|
|
400
|
+
f"{formatted} |{type.upper()} {' ' * (space - len(type))} | - {message}"
|
|
374
401
|
)
|
|
375
402
|
except Exception:
|
|
376
403
|
pass
|
|
@@ -384,8 +411,8 @@ class TradeCopier(object):
|
|
|
384
411
|
error_msg = repr(e)
|
|
385
412
|
if error_msg not in self.errors:
|
|
386
413
|
self.errors.add(error_msg)
|
|
387
|
-
add_msg = f"SYMBOL={symbol}" if symbol else ""
|
|
388
|
-
message = f"Error encountered: {error_msg}
|
|
414
|
+
add_msg = f", SYMBOL={symbol}" if symbol else ""
|
|
415
|
+
message = f"Error encountered: {error_msg}{add_msg}"
|
|
389
416
|
self.log_message(message, type="error")
|
|
390
417
|
|
|
391
418
|
def _validate_source(self):
|
|
@@ -487,6 +514,14 @@ class TradeCopier(object):
|
|
|
487
514
|
if new_result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
488
515
|
break
|
|
489
516
|
return new_result
|
|
517
|
+
|
|
518
|
+
def handle_retcode(self, retcode) -> int:
|
|
519
|
+
if retcode in STOP_RETCODES:
|
|
520
|
+
msg = trade_retcode_message(retcode)
|
|
521
|
+
self.log_error(f"Critical Error on @{self.source['login']}: {msg} ")
|
|
522
|
+
self.stop()
|
|
523
|
+
if retcode in RETURN_RETCODE:
|
|
524
|
+
return 1
|
|
490
525
|
|
|
491
526
|
def copy_new_trade(self, trade: TradeOrder | TradePosition, destination: dict):
|
|
492
527
|
if not self.iscopy_time():
|
|
@@ -508,7 +543,6 @@ class TradeCopier(object):
|
|
|
508
543
|
trade_action = (
|
|
509
544
|
Mt5.TRADE_ACTION_DEAL if trade.type in [0, 1] else Mt5.TRADE_ACTION_PENDING
|
|
510
545
|
)
|
|
511
|
-
action = ORDER_TYPE[trade.type][1]
|
|
512
546
|
tick = Mt5.symbol_info_tick(symbol)
|
|
513
547
|
price = tick.bid if trade.type == 0 else tick.ask
|
|
514
548
|
try:
|
|
@@ -535,14 +569,18 @@ class TradeCopier(object):
|
|
|
535
569
|
result = Mt5.order_send(request)
|
|
536
570
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
537
571
|
result = self._update_filling_type(request, result)
|
|
572
|
+
action = ORDER_TYPE[trade.type][1]
|
|
573
|
+
copy_action = "Position" if trade.type in [0, 1] else "Order"
|
|
538
574
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
539
575
|
self.log_message(
|
|
540
|
-
f"Copy {action}
|
|
576
|
+
f"Copy {action} {copy_action} #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
|
|
541
577
|
f"to @{destination.get('login')}::{symbol}",
|
|
542
578
|
)
|
|
543
579
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
580
|
+
if self.handle_retcode(result.retcode) == 1:
|
|
581
|
+
return
|
|
544
582
|
self.log_message(
|
|
545
|
-
f"Error copying {action}
|
|
583
|
+
f"Error copying {action} {copy_action} #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
|
|
546
584
|
f"to @{destination.get('login')}::{symbol}, {trade_retcode_message(result.retcode)}",
|
|
547
585
|
type="error",
|
|
548
586
|
)
|
|
@@ -572,9 +610,9 @@ class TradeCopier(object):
|
|
|
572
610
|
f"Modify {ORDER_TYPE[source_order.type][1]} Order #{ticket} on @{destination.get('login')}::{symbol}, "
|
|
573
611
|
f"SOURCE=@{self.source.get('login')}::{source_order.symbol}"
|
|
574
612
|
)
|
|
575
|
-
if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
576
|
-
return
|
|
577
613
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
614
|
+
if self.handle_retcode(result.retcode) == 1:
|
|
615
|
+
return
|
|
578
616
|
self.log_message(
|
|
579
617
|
f"Error modifying {ORDER_TYPE[source_order.type][1]} Order #{ticket} on @{destination.get('login')}::{symbol},"
|
|
580
618
|
f"SOURCE=@{self.source.get('login')}::{source_order.symbol}, {trade_retcode_message(result.retcode)}",
|
|
@@ -597,6 +635,8 @@ class TradeCopier(object):
|
|
|
597
635
|
f"SOURCE=@{self.source.get('login')}::{src_symbol}"
|
|
598
636
|
)
|
|
599
637
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
638
|
+
if self.handle_retcode(result.retcode) == 1:
|
|
639
|
+
return
|
|
600
640
|
self.log_message(
|
|
601
641
|
f"Error closing {ORDER_TYPE[order.type][1]} Order #{order.ticket} on @{destination.get('login')}::{order.symbol}, "
|
|
602
642
|
f"SOURCE=@{self.source.get('login')}::{src_symbol}, {trade_retcode_message(result.retcode)}",
|
|
@@ -626,9 +666,9 @@ class TradeCopier(object):
|
|
|
626
666
|
f"Modify {ORDER_TYPE[source_pos.type][1]} Position #{ticket} on @{destination.get('login')}::{symbol}, "
|
|
627
667
|
f"SOURCE=@{self.source.get('login')}::{source_pos.symbol}"
|
|
628
668
|
)
|
|
629
|
-
if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
630
|
-
return
|
|
631
669
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
670
|
+
if self.handle_retcode(result.retcode) == 1:
|
|
671
|
+
return
|
|
632
672
|
self.log_message(
|
|
633
673
|
f"Error modifying {ORDER_TYPE[source_pos.type][1]} Position #{ticket} on @{destination.get('login')}::{symbol}, "
|
|
634
674
|
f"SOURCE=@{self.source.get('login')}::{source_pos.symbol}, {trade_retcode_message(result.retcode)}",
|
|
@@ -663,6 +703,8 @@ class TradeCopier(object):
|
|
|
663
703
|
f"SOURCE=@{self.source.get('login')}::{src_symbol}"
|
|
664
704
|
)
|
|
665
705
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
706
|
+
if self.handle_retcode(result.retcode) == 1:
|
|
707
|
+
return
|
|
666
708
|
self.log_message(
|
|
667
709
|
f"Error closing {ORDER_TYPE[position.type][1]} Position #{position.ticket} "
|
|
668
710
|
f"on @{destination.get('login')}::{position.symbol}, "
|
|
@@ -1023,7 +1065,8 @@ class TradeCopier(object):
|
|
|
1023
1065
|
self.destinations
|
|
1024
1066
|
):
|
|
1025
1067
|
self.log_message(
|
|
1026
|
-
"Two or more destination accounts have the same Terminal path, which is not allowed."
|
|
1068
|
+
"Two or more destination accounts have the same Terminal path, which is not allowed.",
|
|
1069
|
+
type="error",
|
|
1027
1070
|
)
|
|
1028
1071
|
return
|
|
1029
1072
|
|
|
@@ -776,18 +776,19 @@ class Trade(RiskManagement):
|
|
|
776
776
|
# Check the execution result
|
|
777
777
|
pos = self._order_type()[type][1]
|
|
778
778
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
779
|
+
result = None
|
|
779
780
|
try:
|
|
780
781
|
self.check_order(request)
|
|
781
782
|
result = self.send_order(request)
|
|
782
783
|
except Exception as e:
|
|
783
|
-
msg = trade_retcode_message(result.retcode)
|
|
784
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
784
785
|
LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
|
|
785
|
-
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
786
|
+
if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
786
787
|
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
787
788
|
for fill in FILLING_TYPE:
|
|
788
789
|
request["type_filling"] = fill
|
|
789
790
|
result = self.send_order(request)
|
|
790
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
791
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
791
792
|
break
|
|
792
793
|
elif result.retcode == Mt5.TRADE_RETCODE_INVALID_VOLUME: # 10014
|
|
793
794
|
new_volume = int(request["volume"])
|
|
@@ -811,14 +812,14 @@ class Trade(RiskManagement):
|
|
|
811
812
|
self.check_order(request)
|
|
812
813
|
result = self.send_order(request)
|
|
813
814
|
except Exception as e:
|
|
814
|
-
msg = trade_retcode_message(result.retcode)
|
|
815
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
815
816
|
LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
|
|
816
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
817
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
817
818
|
break
|
|
818
819
|
tries += 1
|
|
819
820
|
# Print the result
|
|
820
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
821
|
-
msg = trade_retcode_message(result.retcode)
|
|
821
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
822
|
+
msg = trade_retcode_message(result.retcode)
|
|
822
823
|
LOGGER.info(f"Trade Order {msg}{addtionnal}")
|
|
823
824
|
if type != "BMKT" or type != "SMKT":
|
|
824
825
|
self.opened_orders.append(result.order)
|
|
@@ -854,7 +855,7 @@ class Trade(RiskManagement):
|
|
|
854
855
|
LOGGER.info(pos_info)
|
|
855
856
|
return True
|
|
856
857
|
else:
|
|
857
|
-
msg = trade_retcode_message(result.retcode)
|
|
858
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
858
859
|
LOGGER.error(
|
|
859
860
|
f"Unable to Open Position, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
860
861
|
)
|
|
@@ -1325,15 +1326,16 @@ class Trade(RiskManagement):
|
|
|
1325
1326
|
request (dict): The request to set the stop loss to break even.
|
|
1326
1327
|
"""
|
|
1327
1328
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
1329
|
+
result = None
|
|
1328
1330
|
time.sleep(0.1)
|
|
1329
1331
|
try:
|
|
1330
1332
|
self.check_order(request)
|
|
1331
1333
|
result = self.send_order(request)
|
|
1332
1334
|
except Exception as e:
|
|
1333
|
-
msg = trade_retcode_message(result.retcode)
|
|
1335
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
1334
1336
|
LOGGER.error(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
|
|
1335
|
-
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1336
|
-
msg = trade_retcode_message(result.retcode)
|
|
1337
|
+
if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1338
|
+
msg = trade_retcode_message(result.retcode)
|
|
1337
1339
|
if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
1338
1340
|
LOGGER.error(
|
|
1339
1341
|
f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
@@ -1348,15 +1350,15 @@ class Trade(RiskManagement):
|
|
|
1348
1350
|
self.check_order(request)
|
|
1349
1351
|
result = self.send_order(request)
|
|
1350
1352
|
except Exception as e:
|
|
1351
|
-
msg = trade_retcode_message(result.retcode)
|
|
1353
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
1352
1354
|
LOGGER.error(
|
|
1353
1355
|
f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}"
|
|
1354
1356
|
)
|
|
1355
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1357
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1356
1358
|
break
|
|
1357
1359
|
tries += 1
|
|
1358
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1359
|
-
msg = trade_retcode_message(result.retcode)
|
|
1360
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1361
|
+
msg = trade_retcode_message(result.retcode)
|
|
1360
1362
|
LOGGER.info(f"Break-Even Order {msg}{addtionnal}")
|
|
1361
1363
|
info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{round(price, 5)}"
|
|
1362
1364
|
LOGGER.info(info)
|
|
@@ -1432,15 +1434,17 @@ class Trade(RiskManagement):
|
|
|
1432
1434
|
"""
|
|
1433
1435
|
ticket = request[type]
|
|
1434
1436
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
1437
|
+
result = None
|
|
1435
1438
|
try:
|
|
1436
1439
|
self.check_order(request)
|
|
1437
1440
|
result = self.send_order(request)
|
|
1438
1441
|
except Exception as e:
|
|
1439
|
-
msg = trade_retcode_message(result.retcode)
|
|
1442
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
1440
1443
|
LOGGER.error(
|
|
1441
|
-
f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
|
|
1444
|
+
f"Closing {type.capitalize()} Request, RETCODE={msg}{addtionnal}, Error: {e}"
|
|
1442
1445
|
)
|
|
1443
|
-
|
|
1446
|
+
|
|
1447
|
+
if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1444
1448
|
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
1445
1449
|
for fill in FILLING_TYPE:
|
|
1446
1450
|
request["type_filling"] = fill
|
|
@@ -1462,14 +1466,14 @@ class Trade(RiskManagement):
|
|
|
1462
1466
|
self.check_order(request)
|
|
1463
1467
|
result = self.send_order(request)
|
|
1464
1468
|
except Exception as e:
|
|
1465
|
-
msg = trade_retcode_message(result.retcode)
|
|
1469
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
1466
1470
|
LOGGER.error(
|
|
1467
1471
|
f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
|
|
1468
1472
|
)
|
|
1469
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1473
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1470
1474
|
break
|
|
1471
1475
|
tries += 1
|
|
1472
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1476
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1473
1477
|
msg = trade_retcode_message(result.retcode)
|
|
1474
1478
|
LOGGER.info(f"Closing Order {msg}{addtionnal}")
|
|
1475
1479
|
info = (
|
|
@@ -1504,7 +1508,7 @@ class Trade(RiskManagement):
|
|
|
1504
1508
|
orders = self.get_orders(ticket=ticket) or []
|
|
1505
1509
|
if len(orders) == 0:
|
|
1506
1510
|
LOGGER.error(
|
|
1507
|
-
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={round(price, 5)}"
|
|
1511
|
+
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={round(price, 5) if price else 'N/A'}"
|
|
1508
1512
|
)
|
|
1509
1513
|
return
|
|
1510
1514
|
order = orders[0]
|
|
@@ -1520,8 +1524,8 @@ class Trade(RiskManagement):
|
|
|
1520
1524
|
result = self.send_order(request)
|
|
1521
1525
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1522
1526
|
LOGGER.info(
|
|
1523
|
-
f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={round(price, 5)},"
|
|
1524
|
-
f"SL={round(sl, 5)}, TP={round(tp, 5)}, STOP_LIMIT={round(stoplimit, 5)}"
|
|
1527
|
+
f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={round(request['price'], 5)},"
|
|
1528
|
+
f"SL={round(request['sl'], 5)}, TP={round(request['tp'], 5)}, STOP_LIMIT={round(request['stoplimit'], 5)}"
|
|
1525
1529
|
)
|
|
1526
1530
|
else:
|
|
1527
1531
|
msg = trade_retcode_message(result.retcode)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import Dict, List
|
|
2
|
+
from typing import Dict, List, Literal
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
import yfinance as yf
|
|
6
|
+
from loguru import logger
|
|
6
7
|
|
|
7
8
|
from bbstrader.btengine.data import EODHDataHandler, FMPDataHandler
|
|
8
9
|
from bbstrader.metatrader.rates import download_historical_data
|
|
@@ -16,6 +17,7 @@ __all__ = [
|
|
|
16
17
|
"search_coint_candidate_pairs",
|
|
17
18
|
]
|
|
18
19
|
|
|
20
|
+
|
|
19
21
|
def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
|
|
20
22
|
"""Download and process data for a list of tickers from the specified source."""
|
|
21
23
|
data_list = []
|
|
@@ -43,9 +45,7 @@ def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
|
|
|
43
45
|
)
|
|
44
46
|
data = data.drop(columns=["adj_close"], axis=1)
|
|
45
47
|
elif source in ["fmp", "eodhd"]:
|
|
46
|
-
handler_class =
|
|
47
|
-
FMPDataHandler if source == "fmp" else EODHDataHandler
|
|
48
|
-
)
|
|
48
|
+
handler_class = FMPDataHandler if source == "fmp" else EODHDataHandler
|
|
49
49
|
handler = handler_class(events=None, symbol_list=[ticker], **kwargs)
|
|
50
50
|
data = handler.data[ticker]
|
|
51
51
|
else:
|
|
@@ -62,6 +62,7 @@ def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
|
|
|
62
62
|
|
|
63
63
|
return pd.concat(data_list)
|
|
64
64
|
|
|
65
|
+
|
|
65
66
|
def _handle_date_range(start, end, window):
|
|
66
67
|
"""Handle start and end date generation."""
|
|
67
68
|
if start is None or end is None:
|
|
@@ -73,6 +74,7 @@ def _handle_date_range(start, end, window):
|
|
|
73
74
|
).strftime("%Y-%m-%d")
|
|
74
75
|
return start, end
|
|
75
76
|
|
|
77
|
+
|
|
76
78
|
def _period_search(start, end, securities, candidates, window, npairs):
|
|
77
79
|
if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
|
|
78
80
|
raise ValueError(
|
|
@@ -103,14 +105,11 @@ def _period_search(start, end, securities, candidates, window, npairs):
|
|
|
103
105
|
)
|
|
104
106
|
return top_pairs.head(npairs * 2)
|
|
105
107
|
|
|
108
|
+
|
|
106
109
|
def _process_asset_data(securities, candidates, universe, rolling_window):
|
|
107
110
|
"""Process and select assets from the data."""
|
|
108
|
-
securities = select_assets(
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
candidates = select_assets(
|
|
112
|
-
candidates, n=universe, rolling_window=rolling_window
|
|
113
|
-
)
|
|
111
|
+
securities = select_assets(securities, n=universe, rolling_window=rolling_window)
|
|
112
|
+
candidates = select_assets(candidates, n=universe, rolling_window=rolling_window)
|
|
114
113
|
return securities, candidates
|
|
115
114
|
|
|
116
115
|
|
|
@@ -121,7 +120,7 @@ def search_coint_candidate_pairs(
|
|
|
121
120
|
end: str = None,
|
|
122
121
|
period_search: bool = False,
|
|
123
122
|
select: bool = True,
|
|
124
|
-
source:
|
|
123
|
+
source: Literal["yf", "mt5", "fmp", "eodhd"] = None,
|
|
125
124
|
universe: int = 100,
|
|
126
125
|
window: int = 2,
|
|
127
126
|
rolling_window: int = None,
|
|
@@ -257,7 +256,9 @@ def search_coint_candidate_pairs(
|
|
|
257
256
|
if period_search:
|
|
258
257
|
start = securities.index.get_level_values("date").min()
|
|
259
258
|
end = securities.index.get_level_values("date").max()
|
|
260
|
-
top_pairs = _period_search(
|
|
259
|
+
top_pairs = _period_search(
|
|
260
|
+
start, end, securities, candidates, window, npairs
|
|
261
|
+
)
|
|
261
262
|
else:
|
|
262
263
|
top_pairs = find_cointegrated_pairs(
|
|
263
264
|
securities, candidates, n=npairs, coint=True
|
|
@@ -286,6 +287,10 @@ def search_coint_candidate_pairs(
|
|
|
286
287
|
candidates_data = _download_and_process_data(
|
|
287
288
|
source, candidates, start, end, tf, path, **kwargs
|
|
288
289
|
)
|
|
290
|
+
if securities_data.empty or candidates_data.empty:
|
|
291
|
+
logger.error("No data found for candidates and securities")
|
|
292
|
+
return [] if select else pd.DataFrame()
|
|
293
|
+
|
|
289
294
|
securities_data = securities_data.set_index(["ticker", "date"])
|
|
290
295
|
candidates_data = candidates_data.set_index(["ticker", "date"])
|
|
291
296
|
securities_data, candidates_data = _process_asset_data(
|
|
@@ -305,7 +310,6 @@ def search_coint_candidate_pairs(
|
|
|
305
310
|
)
|
|
306
311
|
else:
|
|
307
312
|
return top_pairs
|
|
308
|
-
|
|
309
313
|
else:
|
|
310
314
|
msg = (
|
|
311
315
|
"Invalid input. Either provide securities"
|