bbstrader 0.2.93__py3-none-any.whl → 0.2.95__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 +20 -20
- bbstrader/__main__.py +50 -50
- bbstrader/btengine/__init__.py +54 -54
- bbstrader/btengine/scripts.py +157 -157
- bbstrader/compat.py +19 -19
- bbstrader/config.py +137 -137
- bbstrader/core/data.py +22 -22
- bbstrader/core/utils.py +146 -146
- bbstrader/metatrader/__init__.py +6 -6
- bbstrader/metatrader/account.py +1516 -1516
- bbstrader/metatrader/copier.py +750 -745
- bbstrader/metatrader/rates.py +584 -584
- bbstrader/metatrader/risk.py +749 -748
- bbstrader/metatrader/scripts.py +81 -81
- bbstrader/metatrader/trade.py +1836 -1836
- bbstrader/metatrader/utils.py +645 -645
- bbstrader/models/__init__.py +10 -10
- bbstrader/models/factors.py +312 -312
- bbstrader/models/ml.py +1272 -1272
- bbstrader/models/optimization.py +182 -182
- bbstrader/models/portfolio.py +223 -223
- bbstrader/models/risk.py +398 -398
- bbstrader/trading/__init__.py +11 -11
- bbstrader/trading/execution.py +846 -846
- bbstrader/trading/script.py +155 -155
- bbstrader/trading/scripts.py +69 -69
- bbstrader/trading/strategies.py +860 -860
- bbstrader/tseries.py +1842 -1842
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/LICENSE +21 -21
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/METADATA +188 -187
- bbstrader-0.2.95.dist-info/RECORD +44 -0
- bbstrader-0.2.93.dist-info/RECORD +0 -44
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/account.py
CHANGED
|
@@ -1,1516 +1,1516 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import urllib.request
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
|
6
|
-
|
|
7
|
-
import pandas as pd
|
|
8
|
-
from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
|
|
9
|
-
|
|
10
|
-
from bbstrader.metatrader.utils import (
|
|
11
|
-
AccountInfo,
|
|
12
|
-
InvalidBroker,
|
|
13
|
-
OrderCheckResult,
|
|
14
|
-
OrderSentResult,
|
|
15
|
-
SymbolInfo,
|
|
16
|
-
TerminalInfo,
|
|
17
|
-
TickInfo,
|
|
18
|
-
TradeDeal,
|
|
19
|
-
TradeOrder,
|
|
20
|
-
TradePosition,
|
|
21
|
-
TradeRequest,
|
|
22
|
-
raise_mt5_error,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
try:
|
|
26
|
-
import MetaTrader5 as mt5
|
|
27
|
-
except ImportError:
|
|
28
|
-
import bbstrader.compat # noqa: F401
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
__all__ = [
|
|
32
|
-
"Account",
|
|
33
|
-
"Broker",
|
|
34
|
-
"AdmiralMarktsGroup",
|
|
35
|
-
"JustGlobalMarkets",
|
|
36
|
-
"PepperstoneGroupLimited",
|
|
37
|
-
"check_mt5_connection",
|
|
38
|
-
"FTMO",
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
__BROKERS__ = {
|
|
42
|
-
"AMG": "Admirals Group AS",
|
|
43
|
-
"JGM": "Just Global Markets Ltd.",
|
|
44
|
-
"FTMO": "FTMO S.R.O.",
|
|
45
|
-
"PGL": "Pepperstone Group Limited",
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
BROKERS_TIMEZONES = {
|
|
49
|
-
"AMG": "Europe/Helsinki",
|
|
50
|
-
"JGM": "Europe/Helsinki",
|
|
51
|
-
"FTMO": "Europe/Helsinki",
|
|
52
|
-
"PGL": "Europe/Helsinki",
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
_ADMIRAL_MARKETS_URL_ = (
|
|
56
|
-
"https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets"
|
|
57
|
-
)
|
|
58
|
-
_JUST_MARKETS_URL_ = "https://one.justmarkets.link/a/tufvj0xugm/registration/trader"
|
|
59
|
-
_FTMO_URL_ = "https://trader.ftmo.com/?affiliates=JGmeuQqepAZLMcdOEQRp"
|
|
60
|
-
_ADMIRAL_MARKETS_PRODUCTS_ = [
|
|
61
|
-
"Stocks",
|
|
62
|
-
"ETFs",
|
|
63
|
-
"Indices",
|
|
64
|
-
"Commodities",
|
|
65
|
-
"Futures",
|
|
66
|
-
"Forex",
|
|
67
|
-
]
|
|
68
|
-
_JUST_MARKETS_PRODUCTS_ = ["Stocks", "Crypto", "indices", "Commodities", "Forex"]
|
|
69
|
-
|
|
70
|
-
SUPPORTED_BROKERS = [__BROKERS__[b] for b in {"AMG", "JGM", "FTMO"}]
|
|
71
|
-
INIT_MSG = (
|
|
72
|
-
f"\n* Ensure you have a good and stable internet connexion\n"
|
|
73
|
-
f"* Ensure you have an activete MT5 terminal install on your machine\n"
|
|
74
|
-
f"* Ensure you have an active MT5 Account with {' or '.join(SUPPORTED_BROKERS)}\n"
|
|
75
|
-
f"* If you want to trade {', '.join(_ADMIRAL_MARKETS_PRODUCTS_)}, See [{_ADMIRAL_MARKETS_URL_}]\n"
|
|
76
|
-
f"* If you want to trade {', '.join(_JUST_MARKETS_PRODUCTS_)}, See [{_JUST_MARKETS_URL_}]\n"
|
|
77
|
-
f"* If you are looking for a prop firm, See [{_FTMO_URL_}]\n"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
amg_url = _ADMIRAL_MARKETS_URL_
|
|
81
|
-
jgm_url = _JUST_MARKETS_URL_
|
|
82
|
-
ftmo_url = _FTMO_URL_
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
_SYMBOLS_TYPE_ = {
|
|
86
|
-
"ETF": r"\b(ETFs?)\b",
|
|
87
|
-
"BOND": r"\b(Treasuries?)\b",
|
|
88
|
-
"FX": r"\b(Forex|Exotics?)\b",
|
|
89
|
-
"FUT": r"\b(Futures?|Forwards)\b",
|
|
90
|
-
"STK": r"\b(Stocks?|Equities?|Shares?)\b",
|
|
91
|
-
"IDX": r"\b(?:Indices?|Cash|Index)\b(?!.*\\(?:UKOIL|USOIL))",
|
|
92
|
-
"COMD": r"\b(Commodity|Commodities?|Metals?|Agricultures?|Energies?|OIL|Oil|USOIL|UKOIL)\b",
|
|
93
|
-
"CRYPTO": r"\b(Cryptos?|Cryptocurrencies|Cryptocurrency)\b",
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
_COUNTRY_MAP_ = {
|
|
97
|
-
"USA": r"\b(US|USA)\b",
|
|
98
|
-
"AUS": r"\b(Australia)\b",
|
|
99
|
-
"BEL": r"\b(Belgium)\b",
|
|
100
|
-
"DNK": r"\b(Denmark)\b",
|
|
101
|
-
"FIN": r"\b(Finland)\b",
|
|
102
|
-
"FRA": r"\b(France)\b",
|
|
103
|
-
"DEU": r"\b(Germany)\b",
|
|
104
|
-
"NLD": r"\b(Netherlands)\b",
|
|
105
|
-
"NOR": r"\b(Norway)\b",
|
|
106
|
-
"PRT": r"\b(Portugal)\b",
|
|
107
|
-
"ESP": r"\b(Spain)\b",
|
|
108
|
-
"SWE": r"\b(Sweden)\b",
|
|
109
|
-
"GBR": r"\b(UK)\b",
|
|
110
|
-
"CHE": r"\b(Switzerland)\b",
|
|
111
|
-
"HKG": r"\b(Hong Kong)\b",
|
|
112
|
-
"IRL": r"\b(Ireland)\b",
|
|
113
|
-
"AUT": r"\b(Austria)\b",
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
AMG_EXCHANGES = {
|
|
117
|
-
"XASX": r"Australia.*\(ASX\)",
|
|
118
|
-
"XBRU": r"Belgium.*\(Euronext\)",
|
|
119
|
-
"XCSE": r"Denmark.*\(CSE\)",
|
|
120
|
-
"XHEL": r"Finland.*\(NASDAQ\)",
|
|
121
|
-
"XPAR": r"France.*\(Euronext\)",
|
|
122
|
-
"XETR": r"Germany.*\(Xetra\)",
|
|
123
|
-
"XAMS": r"Netherlands.*\(Euronext\)",
|
|
124
|
-
"XOSL": r"Norway.*\(NASDAQ\)",
|
|
125
|
-
"XLIS": r"Portugal.*\(Euronext\)",
|
|
126
|
-
"XMAD": r"Spain.*\(BME\)",
|
|
127
|
-
"XSTO": r"Sweden.*\(NASDAQ\)",
|
|
128
|
-
"XLON": r"UK.*\(LSE\)",
|
|
129
|
-
"XNYS": r"US.*\((NYSE|ARCA|AMEX)\)",
|
|
130
|
-
"NYSE": r"US.*\(NYSE\)",
|
|
131
|
-
"ARCA": r"US.*\(ARCA\)",
|
|
132
|
-
"AMEX": r"US.*\(AMEX\)",
|
|
133
|
-
"NASDAQ": r"US.*\(NASDAQ\)",
|
|
134
|
-
"BATS": r"US.*\(BATS\)",
|
|
135
|
-
"XSWX": r"Switzerland.*\(SWX\)",
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def check_mt5_connection(**kwargs):
|
|
140
|
-
"""
|
|
141
|
-
Initialize the connection to the MetaTrader 5 terminal.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
path (str, optional): The path to the MetaTrader 5 terminal executable file.
|
|
145
|
-
Defaults to None (e.g., "C:\\Program Files\\MetaTrader 5\\terminal64.exe").
|
|
146
|
-
login (int, optional): The login ID of the trading account. Defaults to None.
|
|
147
|
-
password (str, optional): The password of the trading account. Defaults to None.
|
|
148
|
-
server (str, optional): The name of the trade server to which the client terminal is connected.
|
|
149
|
-
Defaults to None.
|
|
150
|
-
timeout (int, optional): Connection timeout in milliseconds. Defaults to 60_000.
|
|
151
|
-
portable (bool, optional): If True, the portable mode of the terminal is used.
|
|
152
|
-
Defaults to False (See https://www.metatrader5.com/en/terminal/help/start_advanced/start#portable).
|
|
153
|
-
|
|
154
|
-
Notes:
|
|
155
|
-
If you want to lunch multiple terminal instances:
|
|
156
|
-
- Follow these instructions to lunch each terminal in portable mode first:
|
|
157
|
-
https://www.metatrader5.com/en/terminal/help/start_advanced/start#configuration_file
|
|
158
|
-
"""
|
|
159
|
-
path = kwargs.get("path", None)
|
|
160
|
-
login = kwargs.get("login", None)
|
|
161
|
-
password = kwargs.get("password", None)
|
|
162
|
-
server = kwargs.get("server", None)
|
|
163
|
-
timeout = kwargs.get("timeout", 60_000)
|
|
164
|
-
portable = kwargs.get("portable", False)
|
|
165
|
-
|
|
166
|
-
if path is None and (login or password or server):
|
|
167
|
-
raise ValueError(
|
|
168
|
-
"You must provide a path to the terminal executable file"
|
|
169
|
-
"when providing login, password or server"
|
|
170
|
-
)
|
|
171
|
-
try:
|
|
172
|
-
if path is not None:
|
|
173
|
-
if login is not None and password is not None and server is not None:
|
|
174
|
-
init = mt5.initialize(
|
|
175
|
-
path=path,
|
|
176
|
-
login=login,
|
|
177
|
-
password=password,
|
|
178
|
-
server=server,
|
|
179
|
-
timeout=timeout,
|
|
180
|
-
portable=portable,
|
|
181
|
-
)
|
|
182
|
-
else:
|
|
183
|
-
init = mt5.initialize(path=path)
|
|
184
|
-
else:
|
|
185
|
-
init = mt5.initialize()
|
|
186
|
-
if not init:
|
|
187
|
-
raise_mt5_error(INIT_MSG)
|
|
188
|
-
except Exception:
|
|
189
|
-
raise_mt5_error(INIT_MSG)
|
|
190
|
-
|
|
191
|
-
def shutdown_mt5():
|
|
192
|
-
"""Close the connection to the MetaTrader 5 terminal."""
|
|
193
|
-
mt5.shutdown()
|
|
194
|
-
|
|
195
|
-
class Broker(object):
|
|
196
|
-
def __init__(self, name: str = None, **kwargs):
|
|
197
|
-
if name is None:
|
|
198
|
-
check_mt5_connection(**kwargs)
|
|
199
|
-
self._name = mt5.account_info().company
|
|
200
|
-
else:
|
|
201
|
-
self._name = name
|
|
202
|
-
|
|
203
|
-
@property
|
|
204
|
-
def name(self):
|
|
205
|
-
return self._name
|
|
206
|
-
|
|
207
|
-
def __str__(self):
|
|
208
|
-
return self.name
|
|
209
|
-
|
|
210
|
-
def __eq__(self, orther) -> bool:
|
|
211
|
-
return self.name == orther.name
|
|
212
|
-
|
|
213
|
-
def __ne__(self, orther) -> bool:
|
|
214
|
-
return self.name != orther.name
|
|
215
|
-
|
|
216
|
-
def __repr__(self):
|
|
217
|
-
return f"{self.__class__.__name__}({self.name})"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
class AdmiralMarktsGroup(Broker):
|
|
221
|
-
def __init__(self, **kwargs):
|
|
222
|
-
super().__init__("Admirals Group AS", **kwargs)
|
|
223
|
-
|
|
224
|
-
@property
|
|
225
|
-
def timezone(self) -> str:
|
|
226
|
-
return BROKERS_TIMEZONES["AMG"]
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
class JustGlobalMarkets(Broker):
|
|
230
|
-
def __init__(self, **kwargs):
|
|
231
|
-
super().__init__("Just Global Markets Ltd.", **kwargs)
|
|
232
|
-
|
|
233
|
-
@property
|
|
234
|
-
def timezone(self) -> str:
|
|
235
|
-
return BROKERS_TIMEZONES["JGM"]
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
class FTMO(Broker):
|
|
239
|
-
def __init__(self, **kwargs):
|
|
240
|
-
super().__init__("FTMO S.R.O.", **kwargs)
|
|
241
|
-
|
|
242
|
-
@property
|
|
243
|
-
def timezone(self) -> str:
|
|
244
|
-
return BROKERS_TIMEZONES["FTMO"]
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
class PepperstoneGroupLimited(Broker):
|
|
248
|
-
def __init__(self, **kwargs):
|
|
249
|
-
super().__init__("Pepperstone Group Limited", **kwargs)
|
|
250
|
-
|
|
251
|
-
@property
|
|
252
|
-
def timezone(self) -> str:
|
|
253
|
-
return BROKERS_TIMEZONES["PGL"]
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
class AMP(Broker): ...
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
BROKERS: Dict[str, Broker] = {
|
|
260
|
-
"FTMO": FTMO(),
|
|
261
|
-
"AMG": AdmiralMarktsGroup(),
|
|
262
|
-
"JGM": JustGlobalMarkets(),
|
|
263
|
-
"PGL": PepperstoneGroupLimited(),
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
class Account(object):
|
|
268
|
-
"""
|
|
269
|
-
The `Account` class is utilized to retrieve information about
|
|
270
|
-
the current trading account or a specific account.
|
|
271
|
-
It enables interaction with the MT5 terminal to manage account details,
|
|
272
|
-
including account informations, terminal status, financial instrument details,
|
|
273
|
-
active orders, open positions, and trading history.
|
|
274
|
-
|
|
275
|
-
Example:
|
|
276
|
-
>>> # Instantiating the Account class
|
|
277
|
-
>>> account = Account()
|
|
278
|
-
|
|
279
|
-
>>> # Getting account information
|
|
280
|
-
>>> account_info = account.get_account_info()
|
|
281
|
-
|
|
282
|
-
>>> # Printing account information
|
|
283
|
-
>>> account.print_account_info()
|
|
284
|
-
|
|
285
|
-
>>> # Getting terminal information
|
|
286
|
-
>>> terminal_info = account.get_terminal_info()
|
|
287
|
-
|
|
288
|
-
>>> # Retrieving and printing symbol information
|
|
289
|
-
>>> symbol_info = account.show_symbol_info('EURUSD')
|
|
290
|
-
|
|
291
|
-
>>> # Getting active orders
|
|
292
|
-
>>> orders = account.get_orders()
|
|
293
|
-
|
|
294
|
-
>>> # Fetching open positions
|
|
295
|
-
>>> positions = account.get_positions()
|
|
296
|
-
|
|
297
|
-
>>> # Accessing trade history
|
|
298
|
-
>>> from_date = datetime(2020, 1, 1)
|
|
299
|
-
>>> to_date = datetime.now()
|
|
300
|
-
>>> trade_history = account.get_trade_history(from_date, to_date)
|
|
301
|
-
"""
|
|
302
|
-
|
|
303
|
-
def __init__(self, **kwargs):
|
|
304
|
-
"""
|
|
305
|
-
Initialize the Account class.
|
|
306
|
-
|
|
307
|
-
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
308
|
-
|
|
309
|
-
"""
|
|
310
|
-
check_mt5_connection(**kwargs)
|
|
311
|
-
self._check_brokers(**kwargs)
|
|
312
|
-
|
|
313
|
-
def _check_brokers(self, **kwargs):
|
|
314
|
-
if kwargs.get("copy", False):
|
|
315
|
-
return
|
|
316
|
-
supported = BROKERS.copy()
|
|
317
|
-
if self.broker not in supported.values():
|
|
318
|
-
msg = (
|
|
319
|
-
f"{self.broker.name} is not currently supported broker for the Account() class\n"
|
|
320
|
-
f"Currently Supported brokers are: {', '.join(SUPPORTED_BROKERS)}\n"
|
|
321
|
-
f"For {supported['AMG'].name}, See [{amg_url}]\n"
|
|
322
|
-
f"For {supported['JGM'].name}, See [{jgm_url}]\n"
|
|
323
|
-
f"For {supported['FTMO'].name}, See [{ftmo_url}]\n"
|
|
324
|
-
)
|
|
325
|
-
raise InvalidBroker(message=msg)
|
|
326
|
-
|
|
327
|
-
def shutdown(self):
|
|
328
|
-
"""Close the connection to the MetaTrader 5 terminal."""
|
|
329
|
-
shutdown_mt5()
|
|
330
|
-
|
|
331
|
-
@property
|
|
332
|
-
def broker(self) -> Broker:
|
|
333
|
-
return Broker(self.get_terminal_info().company)
|
|
334
|
-
|
|
335
|
-
@property
|
|
336
|
-
def timezone(self) -> str:
|
|
337
|
-
for broker in BROKERS.values():
|
|
338
|
-
if broker == self.broker:
|
|
339
|
-
return broker.timezone
|
|
340
|
-
|
|
341
|
-
@property
|
|
342
|
-
def name(self) -> str:
|
|
343
|
-
return self.get_account_info().name
|
|
344
|
-
|
|
345
|
-
@property
|
|
346
|
-
def number(self) -> int:
|
|
347
|
-
return self.get_account_info().login
|
|
348
|
-
|
|
349
|
-
@property
|
|
350
|
-
def server(self) -> str:
|
|
351
|
-
"""The name of the trade server to which the client terminal is connected.
|
|
352
|
-
(e.g., 'AdmiralsGroup-Demo')
|
|
353
|
-
"""
|
|
354
|
-
return self.get_account_info().server
|
|
355
|
-
|
|
356
|
-
@property
|
|
357
|
-
def balance(self) -> float:
|
|
358
|
-
return self.get_account_info().balance
|
|
359
|
-
|
|
360
|
-
@property
|
|
361
|
-
def leverage(self) -> int:
|
|
362
|
-
return self.get_account_info().leverage
|
|
363
|
-
|
|
364
|
-
@property
|
|
365
|
-
def equity(self) -> float:
|
|
366
|
-
return self.get_account_info().equity
|
|
367
|
-
|
|
368
|
-
@property
|
|
369
|
-
def currency(self) -> str:
|
|
370
|
-
return self.get_account_info().currency
|
|
371
|
-
|
|
372
|
-
@property
|
|
373
|
-
def language(self) -> str:
|
|
374
|
-
"""The language of the terminal interface."""
|
|
375
|
-
return self.get_terminal_info().language
|
|
376
|
-
|
|
377
|
-
@property
|
|
378
|
-
def maxbars(self) -> int:
|
|
379
|
-
"""The maximal bars count on the chart."""
|
|
380
|
-
return self.get_terminal_info().maxbars
|
|
381
|
-
|
|
382
|
-
def get_account_info(
|
|
383
|
-
self,
|
|
384
|
-
account: Optional[int] = None,
|
|
385
|
-
password: Optional[str] = None,
|
|
386
|
-
server: Optional[str] = None,
|
|
387
|
-
timeout: Optional[int] = 60_000,
|
|
388
|
-
) -> Union[AccountInfo, None]:
|
|
389
|
-
"""
|
|
390
|
-
Get info on the current trading account or a specific account .
|
|
391
|
-
|
|
392
|
-
Args:
|
|
393
|
-
account (int, optinal) : MT5 Trading account number.
|
|
394
|
-
password (str, optinal): MT5 Trading account password.
|
|
395
|
-
|
|
396
|
-
server (str, optinal): MT5 Trading account server
|
|
397
|
-
[Brokers or terminal server ["demo", "real"]]
|
|
398
|
-
If no server is set, the last used server is applied automaticall
|
|
399
|
-
|
|
400
|
-
timeout (int, optinal):
|
|
401
|
-
Connection timeout in milliseconds. Optional named parameter.
|
|
402
|
-
If not specified, the value of 60 000 (60 seconds) is applied.
|
|
403
|
-
If the connection is not established within the specified time,
|
|
404
|
-
the call is forcibly terminated and the exception is generated.
|
|
405
|
-
|
|
406
|
-
Returns:
|
|
407
|
-
- AccountInfo in the form of a Namedtuple structure.
|
|
408
|
-
- None in case of an error
|
|
409
|
-
|
|
410
|
-
Raises:
|
|
411
|
-
MT5TerminalError: A specific exception based on the error code.
|
|
412
|
-
"""
|
|
413
|
-
# connect to the trade account specifying a password and a server
|
|
414
|
-
if account is not None and password is not None and server is not None:
|
|
415
|
-
try:
|
|
416
|
-
authorized = mt5.login(
|
|
417
|
-
account, password=password, server=server, timeout=timeout
|
|
418
|
-
)
|
|
419
|
-
if not authorized:
|
|
420
|
-
raise_mt5_error(message=f"Failed to connect to account #{account}")
|
|
421
|
-
else:
|
|
422
|
-
info = mt5.account_info()
|
|
423
|
-
if info is None:
|
|
424
|
-
return None
|
|
425
|
-
else:
|
|
426
|
-
return AccountInfo(**info._asdict())
|
|
427
|
-
except Exception as e:
|
|
428
|
-
raise_mt5_error(e)
|
|
429
|
-
else:
|
|
430
|
-
try:
|
|
431
|
-
info = mt5.account_info()
|
|
432
|
-
if info is None:
|
|
433
|
-
return None
|
|
434
|
-
else:
|
|
435
|
-
return AccountInfo(**info._asdict())
|
|
436
|
-
except Exception as e:
|
|
437
|
-
raise_mt5_error(e)
|
|
438
|
-
|
|
439
|
-
def _show_info(self, info_getter, info_name, symbol=None):
|
|
440
|
-
"""
|
|
441
|
-
Generic function to retrieve and print information.
|
|
442
|
-
|
|
443
|
-
Args:
|
|
444
|
-
info_getter (callable): Function to retrieve the information.
|
|
445
|
-
info_name (str): Name of the information being retrieved.
|
|
446
|
-
symbol (str, optional): Symbol name, required for some info types.
|
|
447
|
-
Defaults to None.
|
|
448
|
-
|
|
449
|
-
Raises:
|
|
450
|
-
MT5TerminalError: A specific exception based on the error code.
|
|
451
|
-
"""
|
|
452
|
-
|
|
453
|
-
# Call the provided info retrieval function
|
|
454
|
-
if symbol is not None:
|
|
455
|
-
info = info_getter(symbol)
|
|
456
|
-
else:
|
|
457
|
-
info = info_getter()
|
|
458
|
-
|
|
459
|
-
if info is not None:
|
|
460
|
-
info_dict = info._asdict()
|
|
461
|
-
df = pd.DataFrame(list(info_dict.items()), columns=["PROPERTY", "VALUE"])
|
|
462
|
-
|
|
463
|
-
# Construct the print message based on whether a symbol is provided
|
|
464
|
-
if symbol:
|
|
465
|
-
if hasattr(info, "description"):
|
|
466
|
-
print(
|
|
467
|
-
f"\n{info_name.upper()} INFO FOR {symbol} ({info.description})"
|
|
468
|
-
)
|
|
469
|
-
else:
|
|
470
|
-
print(f"\n{info_name.upper()} INFO FOR {symbol}")
|
|
471
|
-
else:
|
|
472
|
-
print(f"\n{info_name.upper()} INFORMATIONS:")
|
|
473
|
-
|
|
474
|
-
pd.set_option("display.max_rows", None)
|
|
475
|
-
pd.set_option("display.max_columns", None)
|
|
476
|
-
print(df.to_string())
|
|
477
|
-
else:
|
|
478
|
-
if symbol:
|
|
479
|
-
msg = self._symbol_info_msg(symbol)
|
|
480
|
-
raise_mt5_error(message=msg)
|
|
481
|
-
else:
|
|
482
|
-
raise_mt5_error()
|
|
483
|
-
|
|
484
|
-
def show_account_info(self):
|
|
485
|
-
"""Helper function to print account info"""
|
|
486
|
-
self._show_info(self.get_account_info, "account")
|
|
487
|
-
|
|
488
|
-
def get_terminal_info(self, show=False) -> Union[TerminalInfo, None]:
|
|
489
|
-
"""
|
|
490
|
-
Get the connected MetaTrader 5 client terminal status and settings.
|
|
491
|
-
|
|
492
|
-
Args:
|
|
493
|
-
show (bool): If True the Account information will be printed
|
|
494
|
-
|
|
495
|
-
Returns:
|
|
496
|
-
- TerminalInfo in the form of NamedTuple Structure.
|
|
497
|
-
- None in case of an error
|
|
498
|
-
|
|
499
|
-
Raises:
|
|
500
|
-
MT5TerminalError: A specific exception based on the error code.
|
|
501
|
-
"""
|
|
502
|
-
try:
|
|
503
|
-
terminal_info = mt5.terminal_info()
|
|
504
|
-
if terminal_info is None:
|
|
505
|
-
return None
|
|
506
|
-
except Exception as e:
|
|
507
|
-
raise_mt5_error(e)
|
|
508
|
-
|
|
509
|
-
terminal_info_dict = terminal_info._asdict()
|
|
510
|
-
# convert the dictionary into DataFrame and print
|
|
511
|
-
df = pd.DataFrame(
|
|
512
|
-
list(terminal_info_dict.items()), columns=["PROPERTY", "VALUE"]
|
|
513
|
-
)
|
|
514
|
-
if show:
|
|
515
|
-
pd.set_option("display.max_rows", None)
|
|
516
|
-
pd.set_option("display.max_columns", None)
|
|
517
|
-
print(df.to_string())
|
|
518
|
-
return TerminalInfo(**terminal_info_dict)
|
|
519
|
-
|
|
520
|
-
def convert_currencies(self, qty: float, from_c: str, to_c: str) -> float:
|
|
521
|
-
"""Convert amount from a currency to another one.
|
|
522
|
-
|
|
523
|
-
Args:
|
|
524
|
-
qty (float): The amount of `currency` to convert.
|
|
525
|
-
from_c (str): The currency to convert from.
|
|
526
|
-
to_c (str): The currency to convert to.
|
|
527
|
-
|
|
528
|
-
Returns:
|
|
529
|
-
- The value of `qty` in converted in `to_c`.
|
|
530
|
-
|
|
531
|
-
Notes:
|
|
532
|
-
If `from_c` or `to_co` are not supported, the `qty` will be return;
|
|
533
|
-
check "https://www.ecb.europa.eu/stats/eurofxref/eurofxref.zip"
|
|
534
|
-
for supported currencies or you can take a look at the `CurrencyConverter` project
|
|
535
|
-
on Github https://github.com/alexprengere/currencyconverter .
|
|
536
|
-
"""
|
|
537
|
-
filename = f"ecb_{datetime.now():%Y%m%d}.zip"
|
|
538
|
-
if not os.path.isfile(filename):
|
|
539
|
-
urllib.request.urlretrieve(SINGLE_DAY_ECB_URL, filename)
|
|
540
|
-
c = CurrencyConverter(filename)
|
|
541
|
-
os.remove(filename)
|
|
542
|
-
supported = c.currencies
|
|
543
|
-
if from_c not in supported or to_c not in supported:
|
|
544
|
-
rate = qty
|
|
545
|
-
else:
|
|
546
|
-
rate = c.convert(amount=qty, currency=from_c, new_currency=to_c)
|
|
547
|
-
return rate
|
|
548
|
-
|
|
549
|
-
def get_currency_rates(self, symbol: str) -> Dict[str, str]:
|
|
550
|
-
"""
|
|
551
|
-
Args:
|
|
552
|
-
symbol (str): The symbol for which to get currencies
|
|
553
|
-
|
|
554
|
-
Returns:
|
|
555
|
-
- `base currency` (bc)
|
|
556
|
-
- `margin currency` (mc)
|
|
557
|
-
- `profit currency` (pc)
|
|
558
|
-
- `account currency` (ac)
|
|
559
|
-
|
|
560
|
-
Exemple:
|
|
561
|
-
>>> account = Account()
|
|
562
|
-
>>> account.get_currency_rates('EURUSD')
|
|
563
|
-
{'bc': 'EUR', 'mc': 'EUR', 'pc': 'USD', 'ac': 'USD'}
|
|
564
|
-
"""
|
|
565
|
-
info = self.get_symbol_info(symbol)
|
|
566
|
-
bc = info.currency_base
|
|
567
|
-
pc = info.currency_profit
|
|
568
|
-
mc = info.currency_margin
|
|
569
|
-
ac = self.get_account_info().currency
|
|
570
|
-
return {"bc": bc, "mc": mc, "pc": pc, "ac": ac}
|
|
571
|
-
|
|
572
|
-
def get_symbols(
|
|
573
|
-
self,
|
|
574
|
-
symbol_type="ALL",
|
|
575
|
-
check_etf=False,
|
|
576
|
-
save=False,
|
|
577
|
-
file_name="symbols",
|
|
578
|
-
include_desc=False,
|
|
579
|
-
display_total=False,
|
|
580
|
-
) -> List[str]:
|
|
581
|
-
"""
|
|
582
|
-
Get all specified financial instruments from the MetaTrader 5 terminal.
|
|
583
|
-
|
|
584
|
-
Args:
|
|
585
|
-
symbol_type (str) The category of instrument to get
|
|
586
|
-
- `ALL`: For all available symbols
|
|
587
|
-
- `STK`: Stocks (e.g., 'GOOGL')
|
|
588
|
-
- `ETF`: ETFs (e.g., 'QQQ')
|
|
589
|
-
- `IDX`: Indices (e.g., 'SP500')
|
|
590
|
-
- `FX`: Forex pairs (e.g., 'EURUSD')
|
|
591
|
-
- `COMD`: Commodities (e.g., 'CRUDOIL', 'GOLD')
|
|
592
|
-
- `FUT`: Futures (e.g., 'USTNote_U4'),
|
|
593
|
-
- `CRYPTO`: Cryptocurrencies (e.g., 'BTC', 'ETH')
|
|
594
|
-
- `BOND`: Bonds (e.g., 'USTN10YR')
|
|
595
|
-
|
|
596
|
-
check_etf (bool): If True and symbol_type is 'etf', check if the
|
|
597
|
-
ETF description contains 'ETF'.
|
|
598
|
-
|
|
599
|
-
save (bool): If True, save the symbols to a file.
|
|
600
|
-
|
|
601
|
-
file_name (str): The name of the file to save the symbols to
|
|
602
|
-
(without the extension).
|
|
603
|
-
|
|
604
|
-
include_desc (bool): If True, include the symbol's description
|
|
605
|
-
in the output and saved file.
|
|
606
|
-
|
|
607
|
-
Returns:
|
|
608
|
-
list: A list of symbols.
|
|
609
|
-
|
|
610
|
-
Raises:
|
|
611
|
-
Exception: If there is an error connecting to MT5 or retrieving symbols.
|
|
612
|
-
"""
|
|
613
|
-
symbols = mt5.symbols_get()
|
|
614
|
-
if not symbols:
|
|
615
|
-
raise_mt5_error()
|
|
616
|
-
|
|
617
|
-
symbol_list = []
|
|
618
|
-
patterns = _SYMBOLS_TYPE_
|
|
619
|
-
|
|
620
|
-
if symbol_type != "ALL":
|
|
621
|
-
if symbol_type not in patterns:
|
|
622
|
-
raise ValueError(f"Unsupported symbol type: {symbol_type}")
|
|
623
|
-
|
|
624
|
-
if save:
|
|
625
|
-
max_lengh = max([len(s.name) for s in symbols])
|
|
626
|
-
file_path = f"{file_name}.txt"
|
|
627
|
-
with open(file_path, mode="w", encoding="utf-8") as file:
|
|
628
|
-
for s in symbols:
|
|
629
|
-
info = self.get_symbol_info(s.name)
|
|
630
|
-
if symbol_type == "ALL":
|
|
631
|
-
self._write_symbol(file, info, include_desc, max_lengh)
|
|
632
|
-
symbol_list.append(s.name)
|
|
633
|
-
else:
|
|
634
|
-
pattern = re.compile(patterns[symbol_type])
|
|
635
|
-
match = re.search(pattern, info.path)
|
|
636
|
-
if match:
|
|
637
|
-
if (
|
|
638
|
-
symbol_type == "ETF"
|
|
639
|
-
and check_etf
|
|
640
|
-
and "ETF" not in info.description
|
|
641
|
-
):
|
|
642
|
-
raise ValueError(
|
|
643
|
-
f"{info.name} doesn't have 'ETF' in its description. "
|
|
644
|
-
"If this is intended, set check_etf=False."
|
|
645
|
-
)
|
|
646
|
-
self._write_symbol(file, info, include_desc, max_lengh)
|
|
647
|
-
symbol_list.append(s.name)
|
|
648
|
-
|
|
649
|
-
else: # If not saving to a file, just process the symbols
|
|
650
|
-
for s in symbols:
|
|
651
|
-
info = self.get_symbol_info(s.name)
|
|
652
|
-
if symbol_type == "ALL":
|
|
653
|
-
symbol_list.append(s.name)
|
|
654
|
-
else:
|
|
655
|
-
pattern = re.compile(patterns[symbol_type]) # , re.IGNORECASE
|
|
656
|
-
match = re.search(pattern, info.path)
|
|
657
|
-
if match:
|
|
658
|
-
if (
|
|
659
|
-
symbol_type == "ETF"
|
|
660
|
-
and check_etf
|
|
661
|
-
and "ETF" not in info.description
|
|
662
|
-
):
|
|
663
|
-
raise ValueError(
|
|
664
|
-
f"{info.name} doesn't have 'ETF' in its description. "
|
|
665
|
-
"If this is intended, set check_etf=False."
|
|
666
|
-
)
|
|
667
|
-
symbol_list.append(s.name)
|
|
668
|
-
|
|
669
|
-
# Print a summary of the retrieved symbols
|
|
670
|
-
if display_total:
|
|
671
|
-
names = {
|
|
672
|
-
"ALL": "Symbols",
|
|
673
|
-
"STK": "Stocks",
|
|
674
|
-
"ETF": "ETFs",
|
|
675
|
-
"IDX": "Indices",
|
|
676
|
-
"FX": "Forex Paires",
|
|
677
|
-
"COMD": "Commodities",
|
|
678
|
-
"FUT": "Futures",
|
|
679
|
-
"CRYPTO": "Cryptos Assets",
|
|
680
|
-
"BOND": "Bonds",
|
|
681
|
-
}
|
|
682
|
-
print(f"Total {names[symbol_type]}: {len(symbol_list)}")
|
|
683
|
-
|
|
684
|
-
return symbol_list
|
|
685
|
-
|
|
686
|
-
def _write_symbol(self, file, info, include_desc, max_lengh):
|
|
687
|
-
"""Helper function to write symbol information to a file."""
|
|
688
|
-
if include_desc:
|
|
689
|
-
space = " " * int(max_lengh - len(info.name))
|
|
690
|
-
file.write(info.name + space + "|" + info.description + "\n")
|
|
691
|
-
else:
|
|
692
|
-
file.write(info.name + "\n")
|
|
693
|
-
|
|
694
|
-
def get_symbol_type(
|
|
695
|
-
self, symbol: str
|
|
696
|
-
) -> Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "BOND", "unknown"]:
|
|
697
|
-
"""
|
|
698
|
-
Determines the type of a given financial instrument symbol.
|
|
699
|
-
|
|
700
|
-
Args:
|
|
701
|
-
symbol (str): The symbol of the financial instrument (e.g., `GOOGL`, `EURUSD`).
|
|
702
|
-
|
|
703
|
-
Returns:
|
|
704
|
-
Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "BOND", "unknown"]:
|
|
705
|
-
The type of the financial instrument, one of the following:
|
|
706
|
-
|
|
707
|
-
- `STK`: For Stocks (e.g., `GOOGL`)
|
|
708
|
-
- `ETF`: For ETFs (e.g., `QQQ`)
|
|
709
|
-
- `IDX`: For Indices (e.g., `SP500`)
|
|
710
|
-
- `FX` : For Forex pairs (e.g., `EURUSD`)
|
|
711
|
-
- `COMD`: For Commodities (e.g., `CRUDOIL`, `GOLD`)
|
|
712
|
-
- `FUT` : For Futures (e.g., `USTNote_U4`)
|
|
713
|
-
- `CRYPTO`: For Cryptocurrencies (e.g., `BTC`, `ETH`)
|
|
714
|
-
- `BOND`: For Bonds (e.g., `USTN10YR`)
|
|
715
|
-
|
|
716
|
-
Returns `unknown` if the type cannot be determined.
|
|
717
|
-
"""
|
|
718
|
-
|
|
719
|
-
patterns = _SYMBOLS_TYPE_
|
|
720
|
-
info = self.get_symbol_info(symbol)
|
|
721
|
-
indices = self.get_symbols(symbol_type="IDX")
|
|
722
|
-
commodity = self.get_symbols(symbol_type="COMD")
|
|
723
|
-
if info is not None:
|
|
724
|
-
for symbol_type, pattern in patterns.items():
|
|
725
|
-
if (
|
|
726
|
-
symbol_type in ["IDX", "COMD"]
|
|
727
|
-
and self.broker == PepperstoneGroupLimited()
|
|
728
|
-
and info.name.endswith("-F")
|
|
729
|
-
and info.name in indices + commodity
|
|
730
|
-
):
|
|
731
|
-
symbol_type = "FUT"
|
|
732
|
-
pattern = r"\b(Forwards?)\b"
|
|
733
|
-
search = re.compile(pattern)
|
|
734
|
-
if re.search(search, info.path):
|
|
735
|
-
return symbol_type
|
|
736
|
-
return "unknown"
|
|
737
|
-
|
|
738
|
-
def _get_symbols_by_category(self, symbol_type, category, category_map):
|
|
739
|
-
if category not in category_map:
|
|
740
|
-
raise ValueError(
|
|
741
|
-
f"Unsupported category: {category}. Choose from: {', '.join(category_map)}"
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
symbols = self.get_symbols(symbol_type=symbol_type)
|
|
745
|
-
pattern = re.compile(category_map[category], re.IGNORECASE)
|
|
746
|
-
|
|
747
|
-
symbol_list = []
|
|
748
|
-
for s in symbols:
|
|
749
|
-
info = self.get_symbol_info(s)
|
|
750
|
-
match = re.search(pattern, info.path)
|
|
751
|
-
if match:
|
|
752
|
-
symbol_list.append(s)
|
|
753
|
-
return symbol_list
|
|
754
|
-
|
|
755
|
-
def get_fx_symbols(
|
|
756
|
-
self,
|
|
757
|
-
category: Literal["majors", "minors", "exotics", "crosses", "ndfs"] = "majors",
|
|
758
|
-
) -> List[str]:
|
|
759
|
-
"""
|
|
760
|
-
Retrieves a list of forex symbols belonging to a specific category.
|
|
761
|
-
|
|
762
|
-
Args:
|
|
763
|
-
category (str, optional): The category of forex symbols to retrieve.
|
|
764
|
-
Possible values are 'majors', 'minors', 'exotics', 'crosses', 'ndfs'.
|
|
765
|
-
Defaults to 'majors'.
|
|
766
|
-
|
|
767
|
-
Returns:
|
|
768
|
-
list: A list of forex symbol names matching the specified category.
|
|
769
|
-
|
|
770
|
-
Raises:
|
|
771
|
-
ValueError: If an unsupported category is provided.
|
|
772
|
-
|
|
773
|
-
Notes:
|
|
774
|
-
This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
|
|
775
|
-
For other brokers use `get_symbols()` or this method will use it by default.
|
|
776
|
-
"""
|
|
777
|
-
if self.broker not in [AdmiralMarktsGroup(), PepperstoneGroupLimited()]:
|
|
778
|
-
return self.get_symbols(symbol_type="FX")
|
|
779
|
-
else:
|
|
780
|
-
fx_categories = {
|
|
781
|
-
"majors": r"\b(Majors?)\b",
|
|
782
|
-
"minors": r"\b(Minors?)\b",
|
|
783
|
-
"exotics": r"\b(Exotics?)\b",
|
|
784
|
-
"crosses": r"\b(Crosses?)\b",
|
|
785
|
-
"ndfs": r"\b(NDFs?)\b",
|
|
786
|
-
}
|
|
787
|
-
return self._get_symbols_by_category("FX", category, fx_categories)
|
|
788
|
-
|
|
789
|
-
def get_stocks_from_country(
|
|
790
|
-
self, country_code: str = "USA", etf=False
|
|
791
|
-
) -> List[str]:
|
|
792
|
-
"""
|
|
793
|
-
Retrieves a list of stock symbols from a specific country.
|
|
794
|
-
|
|
795
|
-
Supported countries are:
|
|
796
|
-
* **Australia:** AUS
|
|
797
|
-
* **Belgium:** BEL
|
|
798
|
-
* **Denmark:** DNK
|
|
799
|
-
* **Finland:** FIN
|
|
800
|
-
* **France:** FRA
|
|
801
|
-
* **Germany:** DEU
|
|
802
|
-
* **Netherlands:** NLD
|
|
803
|
-
* **Norway:** NOR
|
|
804
|
-
* **Portugal:** PRT
|
|
805
|
-
* **Spain:** ESP
|
|
806
|
-
* **Sweden:** SWE
|
|
807
|
-
* **United Kingdom:** GBR
|
|
808
|
-
* **United States:** USA
|
|
809
|
-
* **Switzerland:** CHE
|
|
810
|
-
* **Hong Kong:** HKG
|
|
811
|
-
* **Ireland:** IRL
|
|
812
|
-
* **Austria:** AUT
|
|
813
|
-
|
|
814
|
-
Args:
|
|
815
|
-
country (str, optional): The country code of stocks to retrieve.
|
|
816
|
-
Defaults to 'USA'.
|
|
817
|
-
|
|
818
|
-
Returns:
|
|
819
|
-
list: A list of stock symbol names from the specified country.
|
|
820
|
-
|
|
821
|
-
Raises:
|
|
822
|
-
ValueError: If an unsupported country is provided.
|
|
823
|
-
|
|
824
|
-
Notes:
|
|
825
|
-
This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
|
|
826
|
-
For other brokers use `get_symbols()` or this method will use it by default.
|
|
827
|
-
"""
|
|
828
|
-
|
|
829
|
-
if self.broker not in [AdmiralMarktsGroup(), PepperstoneGroupLimited()]:
|
|
830
|
-
stocks = self.get_symbols(symbol_type="STK")
|
|
831
|
-
return stocks
|
|
832
|
-
else:
|
|
833
|
-
country_map = _COUNTRY_MAP_
|
|
834
|
-
stocks = self._get_symbols_by_category("STK", country_code, country_map)
|
|
835
|
-
if etf:
|
|
836
|
-
etfs = self._get_symbols_by_category("ETF", country_code, country_map)
|
|
837
|
-
return stocks + etfs
|
|
838
|
-
return stocks
|
|
839
|
-
|
|
840
|
-
def get_stocks_from_exchange(
|
|
841
|
-
self, exchange_code: str = "XNYS", etf=True
|
|
842
|
-
) -> List[str]:
|
|
843
|
-
"""
|
|
844
|
-
Get stock symbols from a specific exchange using the ISO Code for the exchange.
|
|
845
|
-
|
|
846
|
-
Supported exchanges are from Admirals Group AS products:
|
|
847
|
-
* **XASX:** **Australian Securities Exchange**
|
|
848
|
-
* **XBRU:** **Euronext Brussels Exchange**
|
|
849
|
-
* **XCSE:** **Copenhagen Stock Exchange**
|
|
850
|
-
* **XHEL:** **NASDAQ OMX Helsinki**
|
|
851
|
-
* **XPAR:** **Euronext Paris**
|
|
852
|
-
* **XETR:** **Xetra Frankfurt**
|
|
853
|
-
* **XOSL:** **Oslo Stock Exchange**
|
|
854
|
-
* **XLIS:** **Euronext Lisbon**
|
|
855
|
-
* **XMAD:** **Bolsa de Madrid**
|
|
856
|
-
* **XSTO:** **NASDAQ OMX Stockholm**
|
|
857
|
-
* **XLON:** **London Stock Exchange**
|
|
858
|
-
* **NYSE:** **New York Stock Exchange**
|
|
859
|
-
* **ARCA:** **NYSE ARCA**
|
|
860
|
-
* **AMEX:** **NYSE AMEX**
|
|
861
|
-
* **XNYS:** **New York Stock Exchange (AMEX, ARCA, NYSE)**
|
|
862
|
-
* **NASDAQ:** **NASDAQ**
|
|
863
|
-
* **BATS:** **BATS Exchange**
|
|
864
|
-
* **XSWX:** **SWX Swiss Exchange**
|
|
865
|
-
* **XAMS:** **Euronext Amsterdam**
|
|
866
|
-
|
|
867
|
-
Args:
|
|
868
|
-
exchange_code (str, optional): The ISO code of the exchange.
|
|
869
|
-
etf (bool, optional): If True, include ETFs from the exchange. Defaults to True.
|
|
870
|
-
|
|
871
|
-
Returns:
|
|
872
|
-
list: A list of stock symbol names from the specified exchange.
|
|
873
|
-
|
|
874
|
-
Raises:
|
|
875
|
-
ValueError: If an unsupported exchange is provided.
|
|
876
|
-
|
|
877
|
-
Notes:
|
|
878
|
-
This mthods works primarly with Admirals Group AS products,
|
|
879
|
-
For other brokers use `get_symbols()` or this method will use it by default.
|
|
880
|
-
"""
|
|
881
|
-
if self.broker != AdmiralMarktsGroup():
|
|
882
|
-
stocks = self.get_symbols(symbol_type="STK")
|
|
883
|
-
return stocks
|
|
884
|
-
else:
|
|
885
|
-
exchange_map = AMG_EXCHANGES
|
|
886
|
-
stocks = self._get_symbols_by_category("STK", exchange_code, exchange_map)
|
|
887
|
-
if etf:
|
|
888
|
-
etfs = self._get_symbols_by_category("ETF", exchange_code, exchange_map)
|
|
889
|
-
return stocks + etfs
|
|
890
|
-
return stocks
|
|
891
|
-
|
|
892
|
-
def get_future_symbols(self, category: str = "ALL") -> List[str]:
|
|
893
|
-
"""
|
|
894
|
-
Retrieves a list of future symbols belonging to a specific category.
|
|
895
|
-
|
|
896
|
-
Args:
|
|
897
|
-
category : The category of future symbols to retrieve.
|
|
898
|
-
Possible values are 'ALL', 'agricultures', 'energies', 'metals'.
|
|
899
|
-
Defaults to 'ALL'.
|
|
900
|
-
|
|
901
|
-
Returns:
|
|
902
|
-
list: A list of future symbol names matching the specified category.
|
|
903
|
-
|
|
904
|
-
Raises:
|
|
905
|
-
ValueError: If an unsupported category is provided.
|
|
906
|
-
|
|
907
|
-
Notes:
|
|
908
|
-
This mthods works primarly with Admirals Group AS products,
|
|
909
|
-
For other brokers use `get_symbols()` or this method will use it by default.
|
|
910
|
-
"""
|
|
911
|
-
category = category.lower()
|
|
912
|
-
if self.broker != AdmiralMarktsGroup():
|
|
913
|
-
return self.get_symbols(symbol_type="FUT")
|
|
914
|
-
elif category in ["all", "index"]:
|
|
915
|
-
categories = {
|
|
916
|
-
"all": r"\b(Futures?)\b",
|
|
917
|
-
"index": r"\b(Index)\b",
|
|
918
|
-
}
|
|
919
|
-
return self._get_symbols_by_category("FUT", category, categories)
|
|
920
|
-
else:
|
|
921
|
-
metals = []
|
|
922
|
-
energies = []
|
|
923
|
-
agricultures = []
|
|
924
|
-
bonds = []
|
|
925
|
-
commodities = self.get_symbols(symbol_type="COMD")
|
|
926
|
-
futures = self.get_symbols(symbol_type="FUT")
|
|
927
|
-
for symbol in futures:
|
|
928
|
-
info = self.get_symbol_info(symbol)
|
|
929
|
-
if info.name.startswith("_"):
|
|
930
|
-
if "XAU" in info.name:
|
|
931
|
-
metals.append(info.name)
|
|
932
|
-
if "oil" in info.name.lower():
|
|
933
|
-
energies.append(info.name)
|
|
934
|
-
name = info.name.split("_")[1]
|
|
935
|
-
if name in commodities:
|
|
936
|
-
_info = self.get_symbol_info(name)
|
|
937
|
-
if "Metals" in _info.path:
|
|
938
|
-
metals.append(info.name)
|
|
939
|
-
elif "Energies" in _info.path:
|
|
940
|
-
energies.append(info.name)
|
|
941
|
-
elif "Agricultures" in _info.path:
|
|
942
|
-
agricultures.append(info.name)
|
|
943
|
-
|
|
944
|
-
elif info.name.startswith("#"):
|
|
945
|
-
if "Index" not in info.path:
|
|
946
|
-
bonds.append(info.name)
|
|
947
|
-
if category == "metals":
|
|
948
|
-
return metals
|
|
949
|
-
elif category == "energies":
|
|
950
|
-
return energies
|
|
951
|
-
elif category == "agricultures":
|
|
952
|
-
return agricultures
|
|
953
|
-
elif category == "bonds":
|
|
954
|
-
return bonds
|
|
955
|
-
|
|
956
|
-
def get_symbol_info(self, symbol: str) -> Union[SymbolInfo, None]:
|
|
957
|
-
"""Get symbol properties
|
|
958
|
-
|
|
959
|
-
Args:
|
|
960
|
-
symbol (str): Symbol name
|
|
961
|
-
|
|
962
|
-
Returns:
|
|
963
|
-
- SymbolInfo in the form of a NamedTuple().
|
|
964
|
-
- None in case of an error.
|
|
965
|
-
|
|
966
|
-
Raises:
|
|
967
|
-
MT5TerminalError: A specific exception based on the error code.
|
|
968
|
-
|
|
969
|
-
Notes:
|
|
970
|
-
The `time` property is converted to a `datetime` object using Broker server time.
|
|
971
|
-
"""
|
|
972
|
-
try:
|
|
973
|
-
symbol_info = mt5.symbol_info(symbol)
|
|
974
|
-
if symbol_info is None:
|
|
975
|
-
return None
|
|
976
|
-
else:
|
|
977
|
-
symbol_info_dict = symbol_info._asdict()
|
|
978
|
-
time = datetime.fromtimestamp(symbol_info.time)
|
|
979
|
-
symbol_info_dict["time"] = time
|
|
980
|
-
return SymbolInfo(**symbol_info_dict)
|
|
981
|
-
except Exception as e:
|
|
982
|
-
msg = self._symbol_info_msg(symbol)
|
|
983
|
-
raise_mt5_error(message=f"{e + msg}")
|
|
984
|
-
|
|
985
|
-
def show_symbol_info(self, symbol: str):
|
|
986
|
-
"""
|
|
987
|
-
Print symbol properties
|
|
988
|
-
|
|
989
|
-
Args:
|
|
990
|
-
symbol (str): Symbol name
|
|
991
|
-
"""
|
|
992
|
-
self._show_info(self.get_symbol_info, "symbol", symbol=symbol)
|
|
993
|
-
|
|
994
|
-
def _symbol_info_msg(self, symbol):
|
|
995
|
-
return (
|
|
996
|
-
f"No history found for {symbol} in Market Watch.\n"
|
|
997
|
-
f"* Ensure {symbol} is selected and displayed in the Market Watch window.\n"
|
|
998
|
-
f"* See https://www.metatrader5.com/en/terminal/help/trading/market_watch\n"
|
|
999
|
-
f"* Ensure the symbol name is correct.\n"
|
|
1000
|
-
)
|
|
1001
|
-
|
|
1002
|
-
def get_tick_info(self, symbol: str) -> Union[TickInfo, None]:
|
|
1003
|
-
"""Get symbol tick properties
|
|
1004
|
-
|
|
1005
|
-
Args:
|
|
1006
|
-
symbol (str): Symbol name
|
|
1007
|
-
|
|
1008
|
-
Returns:
|
|
1009
|
-
- TickInfo in the form of a NamedTuple().
|
|
1010
|
-
- None in case of an error.
|
|
1011
|
-
|
|
1012
|
-
Raises:
|
|
1013
|
-
MT5TerminalError: A specific exception based on the error code.
|
|
1014
|
-
|
|
1015
|
-
Notes:
|
|
1016
|
-
The `time` property is converted to a `datetime` object using Broker server time.
|
|
1017
|
-
"""
|
|
1018
|
-
try:
|
|
1019
|
-
tick_info = mt5.symbol_info_tick(symbol)
|
|
1020
|
-
if tick_info is None:
|
|
1021
|
-
return None
|
|
1022
|
-
else:
|
|
1023
|
-
info_dict = tick_info._asdict()
|
|
1024
|
-
time = datetime.fromtimestamp(tick_info.time)
|
|
1025
|
-
info_dict["time"] = time
|
|
1026
|
-
return TickInfo(**info_dict)
|
|
1027
|
-
except Exception as e:
|
|
1028
|
-
msg = self._symbol_info_msg(symbol)
|
|
1029
|
-
raise_mt5_error(message=f"{e + msg}")
|
|
1030
|
-
|
|
1031
|
-
def show_tick_info(self, symbol: str):
|
|
1032
|
-
"""
|
|
1033
|
-
Print Tick properties
|
|
1034
|
-
|
|
1035
|
-
Args:
|
|
1036
|
-
symbol (str): Symbol name
|
|
1037
|
-
"""
|
|
1038
|
-
self._show_info(self.get_tick_info, "tick", symbol=symbol)
|
|
1039
|
-
|
|
1040
|
-
def calculate_margin(
|
|
1041
|
-
self, action: Literal["buy", "sell"], symbol: str, lot: float, price: float
|
|
1042
|
-
) -> float:
|
|
1043
|
-
"""
|
|
1044
|
-
Calculate margin required for an order.
|
|
1045
|
-
|
|
1046
|
-
Args:
|
|
1047
|
-
action (str): The trading action, either 'buy' or 'sell'.
|
|
1048
|
-
symbol (str): The symbol of the financial instrument.
|
|
1049
|
-
lot (float): The lot size of the order.
|
|
1050
|
-
price (float): The price of the order.
|
|
1051
|
-
|
|
1052
|
-
Returns:
|
|
1053
|
-
float: The margin required for the order.
|
|
1054
|
-
|
|
1055
|
-
Raises:
|
|
1056
|
-
MT5TerminalError: A specific exception based on the error code.
|
|
1057
|
-
"""
|
|
1058
|
-
actions = {"buy": mt5.ORDER_TYPE_BUY, "sell": mt5.ORDER_TYPE_SELL}
|
|
1059
|
-
try:
|
|
1060
|
-
margin = mt5.order_calc_margin(actions[action], symbol, lot, price)
|
|
1061
|
-
if margin is None:
|
|
1062
|
-
return None
|
|
1063
|
-
return margin
|
|
1064
|
-
except Exception as e:
|
|
1065
|
-
raise_mt5_error(e)
|
|
1066
|
-
|
|
1067
|
-
def check_order(self, request: Dict[str, Any]) -> OrderCheckResult:
|
|
1068
|
-
"""
|
|
1069
|
-
Check funds sufficiency for performing a required trading operation.
|
|
1070
|
-
|
|
1071
|
-
Args:
|
|
1072
|
-
request (Dict[str, Any]): `TradeRequest` type structure describing the required trading action.
|
|
1073
|
-
|
|
1074
|
-
Returns:
|
|
1075
|
-
OrderCheckResult:
|
|
1076
|
-
The check result as the `OrderCheckResult` structure.
|
|
1077
|
-
|
|
1078
|
-
The `request` field in the returned structure contains the trading request passed to `check_order()`.
|
|
1079
|
-
|
|
1080
|
-
Raises:
|
|
1081
|
-
MT5TerminalError: Raised if there is an error in the trading terminal based on the error code.
|
|
1082
|
-
|
|
1083
|
-
Notes:
|
|
1084
|
-
Successful submission of a request does not guarantee that the requested trading
|
|
1085
|
-
operation will be executed successfully.
|
|
1086
|
-
"""
|
|
1087
|
-
|
|
1088
|
-
try:
|
|
1089
|
-
result = mt5.order_check(request)
|
|
1090
|
-
result_dict = result._asdict()
|
|
1091
|
-
trade_request = TradeRequest(**result.request._asdict())
|
|
1092
|
-
result_dict["request"] = trade_request
|
|
1093
|
-
return OrderCheckResult(**result_dict)
|
|
1094
|
-
except Exception as e:
|
|
1095
|
-
raise_mt5_error(e)
|
|
1096
|
-
|
|
1097
|
-
def send_order(self, request: Dict[str, Any]) -> OrderSentResult:
|
|
1098
|
-
"""
|
|
1099
|
-
Send a request to perform a trading operation from the terminal to the trade server.
|
|
1100
|
-
|
|
1101
|
-
Args:
|
|
1102
|
-
request (Dict[str, Any]): `TradeRequest` type structure describing the required trading action.
|
|
1103
|
-
|
|
1104
|
-
Returns:
|
|
1105
|
-
OrderSentResult:
|
|
1106
|
-
The execution result as the `OrderSentResult` structure.
|
|
1107
|
-
|
|
1108
|
-
The `request` field in the returned structure contains the trading request passed to `send_order()`.
|
|
1109
|
-
|
|
1110
|
-
Raises:
|
|
1111
|
-
MT5TerminalError: Raised if there is an error in the trading terminal based on the error code.
|
|
1112
|
-
"""
|
|
1113
|
-
try:
|
|
1114
|
-
result = mt5.order_send(request)
|
|
1115
|
-
result_dict = result._asdict()
|
|
1116
|
-
trade_request = TradeRequest(**result.request._asdict())
|
|
1117
|
-
result_dict["request"] = trade_request
|
|
1118
|
-
return OrderSentResult(**result_dict)
|
|
1119
|
-
except Exception as e:
|
|
1120
|
-
raise_mt5_error(e)
|
|
1121
|
-
|
|
1122
|
-
def get_positions(
|
|
1123
|
-
self,
|
|
1124
|
-
symbol: Optional[str] = None,
|
|
1125
|
-
group: Optional[str] = None,
|
|
1126
|
-
ticket: Optional[int] = None,
|
|
1127
|
-
to_df: bool = False,
|
|
1128
|
-
) -> Union[pd.DataFrame, Tuple[TradePosition], None]:
|
|
1129
|
-
"""
|
|
1130
|
-
Get open positions with the ability to filter by symbol or ticket.
|
|
1131
|
-
There are four call options:
|
|
1132
|
-
|
|
1133
|
-
- Call without parameters. Returns open positions for all symbols.
|
|
1134
|
-
- Call specifying a symbol. Returns open positions for the specified symbol.
|
|
1135
|
-
- Call specifying a group of symbols. Returns open positions for the specified group of symbols.
|
|
1136
|
-
- Call specifying a position ticket. Returns the position corresponding to the specified ticket.
|
|
1137
|
-
|
|
1138
|
-
Args:
|
|
1139
|
-
symbol (Optional[str]): Symbol name. Optional named parameter.
|
|
1140
|
-
If a symbol is specified, the `ticket` parameter is ignored.
|
|
1141
|
-
|
|
1142
|
-
group (Optional[str]): The filter for arranging a group of necessary symbols.
|
|
1143
|
-
Optional named parameter. If the group is specified,
|
|
1144
|
-
the function returns only positions meeting specified criteria
|
|
1145
|
-
for a symbol name.
|
|
1146
|
-
|
|
1147
|
-
ticket (Optional[int]): Position ticket. Optional named parameter.
|
|
1148
|
-
A unique number assigned to each newly opened position.
|
|
1149
|
-
It usually matches the ticket of the order used to open the position,
|
|
1150
|
-
except when the ticket is changed as a result of service operations on the server,
|
|
1151
|
-
for example, when charging swaps with position re-opening.
|
|
1152
|
-
|
|
1153
|
-
to_df (bool): If True, a DataFrame is returned.
|
|
1154
|
-
|
|
1155
|
-
Returns:
|
|
1156
|
-
Union[pd.DataFrame, Tuple[TradePosition], None]:
|
|
1157
|
-
- `TradePosition` in the form of a named tuple structure (namedtuple) or pd.DataFrame.
|
|
1158
|
-
- `None` in case of an error.
|
|
1159
|
-
|
|
1160
|
-
Notes:
|
|
1161
|
-
The method allows receiving all open positions within a specified period.
|
|
1162
|
-
|
|
1163
|
-
The `group` parameter may contain several comma-separated conditions.
|
|
1164
|
-
|
|
1165
|
-
A condition can be set as a mask using '*'.
|
|
1166
|
-
|
|
1167
|
-
The logical negation symbol '!' can be used for exclusion.
|
|
1168
|
-
|
|
1169
|
-
All conditions are applied sequentially, which means conditions for inclusion
|
|
1170
|
-
in a group should be specified first, followed by an exclusion condition.
|
|
1171
|
-
|
|
1172
|
-
For example, `group="*, !EUR"` means that deals for all symbols should be selected first,
|
|
1173
|
-
and those containing "EUR" in symbol names should be excluded afterward.
|
|
1174
|
-
"""
|
|
1175
|
-
|
|
1176
|
-
if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
|
|
1177
|
-
raise ValueError(
|
|
1178
|
-
"Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
|
|
1179
|
-
)
|
|
1180
|
-
|
|
1181
|
-
if symbol is not None:
|
|
1182
|
-
positions = mt5.positions_get(symbol=symbol)
|
|
1183
|
-
elif group is not None:
|
|
1184
|
-
positions = mt5.positions_get(group=group)
|
|
1185
|
-
elif ticket is not None:
|
|
1186
|
-
positions = mt5.positions_get(ticket=ticket)
|
|
1187
|
-
else:
|
|
1188
|
-
positions = mt5.positions_get()
|
|
1189
|
-
|
|
1190
|
-
if positions is None or len(positions) == 0:
|
|
1191
|
-
return None
|
|
1192
|
-
if to_df:
|
|
1193
|
-
df = pd.DataFrame(list(positions), columns=positions[0]._asdict())
|
|
1194
|
-
df["time"] = pd.to_datetime(df["time"], unit="s")
|
|
1195
|
-
df.drop(
|
|
1196
|
-
["time_update", "time_msc", "time_update_msc", "external_id"],
|
|
1197
|
-
axis=1,
|
|
1198
|
-
inplace=True,
|
|
1199
|
-
)
|
|
1200
|
-
return df
|
|
1201
|
-
else:
|
|
1202
|
-
trade_positions = [TradePosition(**p._asdict()) for p in positions]
|
|
1203
|
-
return tuple(trade_positions)
|
|
1204
|
-
|
|
1205
|
-
def get_trades_history(
|
|
1206
|
-
self,
|
|
1207
|
-
date_from: datetime = datetime(2000, 1, 1),
|
|
1208
|
-
date_to: Optional[datetime] = None,
|
|
1209
|
-
group: Optional[str] = None,
|
|
1210
|
-
ticket: Optional[int] = None, # TradeDeal.ticket
|
|
1211
|
-
position: Optional[int] = None, # TradePosition.ticket
|
|
1212
|
-
to_df: bool = True,
|
|
1213
|
-
save: bool = False,
|
|
1214
|
-
) -> Union[pd.DataFrame, Tuple[TradeDeal], None]:
|
|
1215
|
-
"""
|
|
1216
|
-
Get deals from trading history within the specified interval
|
|
1217
|
-
with the ability to filter by `ticket` or `position`.
|
|
1218
|
-
|
|
1219
|
-
You can call this method in the following ways:
|
|
1220
|
-
|
|
1221
|
-
- Call with a `time interval`. Returns all deals falling within the specified interval.
|
|
1222
|
-
|
|
1223
|
-
- Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.
|
|
1224
|
-
|
|
1225
|
-
- Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.
|
|
1226
|
-
|
|
1227
|
-
Args:
|
|
1228
|
-
date_from (datetime): Date the bars are requested from.
|
|
1229
|
-
Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
|
|
1230
|
-
Bars with the open time >= `date_from` are returned. Required unnamed parameter.
|
|
1231
|
-
|
|
1232
|
-
date_to (Optional[datetime]): Same as `date_from`.
|
|
1233
|
-
|
|
1234
|
-
group (Optional[str]): The filter for arranging a group of necessary symbols.
|
|
1235
|
-
Optional named parameter. If the group is specified,
|
|
1236
|
-
the function returns only positions meeting specified criteria
|
|
1237
|
-
for a symbol name.
|
|
1238
|
-
|
|
1239
|
-
ticket (Optional[int]): Ticket of an order (stored in `DEAL_ORDER`) for which all deals should be received.
|
|
1240
|
-
Optional parameter. If not specified, the filter is not applied.
|
|
1241
|
-
|
|
1242
|
-
position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) for which all deals should be received.
|
|
1243
|
-
Optional parameter. If not specified, the filter is not applied.
|
|
1244
|
-
|
|
1245
|
-
to_df (bool): If True, a DataFrame is returned.
|
|
1246
|
-
|
|
1247
|
-
save (bool): If set to True, a CSV file will be created to save the history.
|
|
1248
|
-
|
|
1249
|
-
Returns:
|
|
1250
|
-
Union[pd.DataFrame, Tuple[TradeDeal], None]:
|
|
1251
|
-
- `TradeDeal` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
|
|
1252
|
-
- `None` in case of an error.
|
|
1253
|
-
|
|
1254
|
-
Notes:
|
|
1255
|
-
The method allows receiving all history orders within a specified period.
|
|
1256
|
-
|
|
1257
|
-
The `group` parameter may contain several comma-separated conditions.
|
|
1258
|
-
|
|
1259
|
-
A condition can be set as a mask using '*'.
|
|
1260
|
-
|
|
1261
|
-
The logical negation symbol '!' can be used for exclusion.
|
|
1262
|
-
|
|
1263
|
-
All conditions are applied sequentially, which means conditions for inclusion
|
|
1264
|
-
in a group should be specified first, followed by an exclusion condition.
|
|
1265
|
-
|
|
1266
|
-
For example, `group="*, !EUR"` means that deals for all symbols should be selected first
|
|
1267
|
-
and those containing "EUR" in symbol names should be excluded afterward.
|
|
1268
|
-
|
|
1269
|
-
Example:
|
|
1270
|
-
>>> # Get the number of deals in history
|
|
1271
|
-
>>> from datetime import datetime
|
|
1272
|
-
>>> from_date = datetime(2020, 1, 1)
|
|
1273
|
-
>>> to_date = datetime.now()
|
|
1274
|
-
>>> account = Account()
|
|
1275
|
-
>>> history = account.get_trades_history(from_date, to_date)
|
|
1276
|
-
"""
|
|
1277
|
-
|
|
1278
|
-
if date_to is None:
|
|
1279
|
-
date_to = datetime.now()
|
|
1280
|
-
|
|
1281
|
-
if (ticket is not None) + (group is not None) + (position is not None) > 1:
|
|
1282
|
-
raise ValueError(
|
|
1283
|
-
"Only one of 'position', 'group' or 'ticket' can be specified as filter or None of them ."
|
|
1284
|
-
)
|
|
1285
|
-
if group is not None:
|
|
1286
|
-
position_deals = mt5.history_deals_get(date_from, date_to, group=group)
|
|
1287
|
-
elif ticket is not None:
|
|
1288
|
-
position_deals = mt5.history_deals_get(ticket=ticket)
|
|
1289
|
-
elif position is not None:
|
|
1290
|
-
position_deals = mt5.history_deals_get(position=position)
|
|
1291
|
-
else:
|
|
1292
|
-
position_deals = mt5.history_deals_get(date_from, date_to)
|
|
1293
|
-
|
|
1294
|
-
if position_deals is None or len(position_deals) == 0:
|
|
1295
|
-
return None
|
|
1296
|
-
|
|
1297
|
-
df = pd.DataFrame(list(position_deals), columns=position_deals[0]._asdict())
|
|
1298
|
-
df["time"] = pd.to_datetime(df["time"], unit="s")
|
|
1299
|
-
df.drop(["time_msc", "external_id"], axis=1, inplace=True)
|
|
1300
|
-
df.set_index("time", inplace=True)
|
|
1301
|
-
if save:
|
|
1302
|
-
file = "trade_history.csv"
|
|
1303
|
-
df.to_csv(file)
|
|
1304
|
-
if to_df:
|
|
1305
|
-
return df
|
|
1306
|
-
else:
|
|
1307
|
-
position_deals = [TradeDeal(**td._asdict()) for td in position_deals]
|
|
1308
|
-
return tuple(position_deals)
|
|
1309
|
-
|
|
1310
|
-
def get_orders(
|
|
1311
|
-
self,
|
|
1312
|
-
symbol: Optional[str] = None,
|
|
1313
|
-
group: Optional[str] = None,
|
|
1314
|
-
ticket: Optional[int] = None,
|
|
1315
|
-
to_df: bool = False,
|
|
1316
|
-
) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
|
|
1317
|
-
"""
|
|
1318
|
-
Get active orders with the ability to filter by symbol or ticket.
|
|
1319
|
-
There are four call options:
|
|
1320
|
-
|
|
1321
|
-
- Call without parameters. Returns open positions for all symbols.
|
|
1322
|
-
- Call specifying a symbol, open positions should be received for.
|
|
1323
|
-
- Call specifying a group of symbols, open positions should be received for.
|
|
1324
|
-
- Call specifying a position ticket.
|
|
1325
|
-
|
|
1326
|
-
Args:
|
|
1327
|
-
symbol (Optional[str]): Symbol name. Optional named parameter.
|
|
1328
|
-
If a symbol is specified, the ticket parameter is ignored.
|
|
1329
|
-
|
|
1330
|
-
group (Optional[str]): The filter for arranging a group of necessary symbols.
|
|
1331
|
-
Optional named parameter. If the group is specified,
|
|
1332
|
-
the function returns only positions meeting a specified criteria
|
|
1333
|
-
for a symbol name.
|
|
1334
|
-
|
|
1335
|
-
ticket (Optional[int]): Order ticket. Optional named parameter.
|
|
1336
|
-
Unique number assigned to each order.
|
|
1337
|
-
|
|
1338
|
-
to_df (bool): If True, a DataFrame is returned.
|
|
1339
|
-
|
|
1340
|
-
Returns:
|
|
1341
|
-
Union[pd.DataFrame, Tuple[TradeOrder], None]:
|
|
1342
|
-
- `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
|
|
1343
|
-
- `None` in case of an error.
|
|
1344
|
-
|
|
1345
|
-
Notes:
|
|
1346
|
-
The method allows receiving all history orders within a specified period.
|
|
1347
|
-
The `group` parameter may contain several comma-separated conditions.
|
|
1348
|
-
A condition can be set as a mask using '*'.
|
|
1349
|
-
|
|
1350
|
-
The logical negation symbol '!' can be used for exclusion.
|
|
1351
|
-
All conditions are applied sequentially, which means conditions for inclusion
|
|
1352
|
-
in a group should be specified first, followed by an exclusion condition.
|
|
1353
|
-
|
|
1354
|
-
For example, `group="*, !EUR"` means that deals for all symbols should be selected first
|
|
1355
|
-
and the ones containing "EUR" in symbol names should be excluded afterward.
|
|
1356
|
-
"""
|
|
1357
|
-
|
|
1358
|
-
if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
|
|
1359
|
-
raise ValueError(
|
|
1360
|
-
"Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
|
|
1361
|
-
)
|
|
1362
|
-
|
|
1363
|
-
if symbol is not None:
|
|
1364
|
-
orders = mt5.orders_get(symbol=symbol)
|
|
1365
|
-
elif group is not None:
|
|
1366
|
-
orders = mt5.orders_get(group=group)
|
|
1367
|
-
elif ticket is not None:
|
|
1368
|
-
orders = mt5.orders_get(ticket=ticket)
|
|
1369
|
-
else:
|
|
1370
|
-
orders = mt5.orders_get()
|
|
1371
|
-
|
|
1372
|
-
if orders is None or len(orders) == 0:
|
|
1373
|
-
return None
|
|
1374
|
-
|
|
1375
|
-
if to_df:
|
|
1376
|
-
df = pd.DataFrame(list(orders), columns=orders[0]._asdict())
|
|
1377
|
-
df.drop(
|
|
1378
|
-
[
|
|
1379
|
-
"time_expiration",
|
|
1380
|
-
"type_time",
|
|
1381
|
-
"state",
|
|
1382
|
-
"position_by_id",
|
|
1383
|
-
"reason",
|
|
1384
|
-
"volume_current",
|
|
1385
|
-
"price_stoplimit",
|
|
1386
|
-
"sl",
|
|
1387
|
-
"tp",
|
|
1388
|
-
],
|
|
1389
|
-
axis=1,
|
|
1390
|
-
inplace=True,
|
|
1391
|
-
)
|
|
1392
|
-
df["time_setup"] = pd.to_datetime(df["time_setup"], unit="s")
|
|
1393
|
-
df["time_done"] = pd.to_datetime(df["time_done"], unit="s")
|
|
1394
|
-
return df
|
|
1395
|
-
else:
|
|
1396
|
-
trade_orders = [TradeOrder(**o._asdict()) for o in orders]
|
|
1397
|
-
return tuple(trade_orders)
|
|
1398
|
-
|
|
1399
|
-
def get_orders_history(
|
|
1400
|
-
self,
|
|
1401
|
-
date_from: datetime = datetime(2000, 1, 1),
|
|
1402
|
-
date_to: Optional[datetime] = None,
|
|
1403
|
-
group: Optional[str] = None,
|
|
1404
|
-
ticket: Optional[int] = None, # order ticket
|
|
1405
|
-
position: Optional[int] = None, # position ticket
|
|
1406
|
-
to_df: bool = True,
|
|
1407
|
-
save: bool = False,
|
|
1408
|
-
) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
|
|
1409
|
-
"""
|
|
1410
|
-
Get orders from trading history within the specified interval
|
|
1411
|
-
with the ability to filter by `ticket` or `position`.
|
|
1412
|
-
|
|
1413
|
-
You can call this method in the following ways:
|
|
1414
|
-
|
|
1415
|
-
- Call with a `time interval`. Returns all deals falling within the specified interval.
|
|
1416
|
-
|
|
1417
|
-
- Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.
|
|
1418
|
-
|
|
1419
|
-
- Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.
|
|
1420
|
-
|
|
1421
|
-
Args:
|
|
1422
|
-
date_from (datetime): Date the bars are requested from.
|
|
1423
|
-
Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
|
|
1424
|
-
Bars with the open time >= `date_from` are returned. Required unnamed parameter.
|
|
1425
|
-
|
|
1426
|
-
date_to (Optional[datetime]): Same as `date_from`.
|
|
1427
|
-
|
|
1428
|
-
group (Optional[str]): The filter for arranging a group of necessary symbols.
|
|
1429
|
-
Optional named parameter. If the group is specified,
|
|
1430
|
-
the function returns only positions meeting specified criteria
|
|
1431
|
-
for a symbol name.
|
|
1432
|
-
|
|
1433
|
-
ticket (Optional[int]): Order ticket to filter results. Optional parameter.
|
|
1434
|
-
If not specified, the filter is not applied.
|
|
1435
|
-
|
|
1436
|
-
position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) to filter results.
|
|
1437
|
-
Optional parameter. If not specified, the filter is not applied.
|
|
1438
|
-
|
|
1439
|
-
to_df (bool): If True, a DataFrame is returned.
|
|
1440
|
-
|
|
1441
|
-
save (bool): If True, a CSV file will be created to save the history.
|
|
1442
|
-
|
|
1443
|
-
Returns:
|
|
1444
|
-
Union[pd.DataFrame, Tuple[TradeOrder], None]
|
|
1445
|
-
- `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
|
|
1446
|
-
- `None` in case of an error.
|
|
1447
|
-
|
|
1448
|
-
Notes:
|
|
1449
|
-
The method allows receiving all history orders within a specified period.
|
|
1450
|
-
|
|
1451
|
-
The `group` parameter may contain several comma-separated conditions.
|
|
1452
|
-
|
|
1453
|
-
A condition can be set as a mask using '*'.
|
|
1454
|
-
|
|
1455
|
-
The logical negation symbol '!' can be used for exclusion.
|
|
1456
|
-
|
|
1457
|
-
All conditions are applied sequentially, which means conditions for inclusion
|
|
1458
|
-
in a group should be specified first, followed by an exclusion condition.
|
|
1459
|
-
|
|
1460
|
-
For example, `group="*, !EUR"` means that deals for all symbols should be selected first
|
|
1461
|
-
and those containing "EUR" in symbol names should be excluded afterward.
|
|
1462
|
-
|
|
1463
|
-
Example:
|
|
1464
|
-
>>> # Get the number of deals in history
|
|
1465
|
-
>>> from datetime import datetime
|
|
1466
|
-
>>> from_date = datetime(2020, 1, 1)
|
|
1467
|
-
>>> to_date = datetime.now()
|
|
1468
|
-
>>> account = Account()
|
|
1469
|
-
>>> history = account.get_orders_history(from_date, to_date)
|
|
1470
|
-
"""
|
|
1471
|
-
if date_to is None:
|
|
1472
|
-
date_to = datetime.now()
|
|
1473
|
-
|
|
1474
|
-
if (group is not None) + (ticket is not None) + (position is not None) > 1:
|
|
1475
|
-
raise ValueError(
|
|
1476
|
-
"Only one of 'position', 'group' or 'ticket' can be specified or None of them as filter."
|
|
1477
|
-
)
|
|
1478
|
-
if group is not None:
|
|
1479
|
-
history_orders = mt5.history_orders_get(date_from, date_to, group=group)
|
|
1480
|
-
elif ticket is not None:
|
|
1481
|
-
history_orders = mt5.history_orders_get(ticket=ticket)
|
|
1482
|
-
elif position is not None:
|
|
1483
|
-
history_orders = mt5.history_orders_get(position=position)
|
|
1484
|
-
else:
|
|
1485
|
-
history_orders = mt5.history_orders_get(date_from, date_to)
|
|
1486
|
-
|
|
1487
|
-
if history_orders is None or len(history_orders) == 0:
|
|
1488
|
-
return None
|
|
1489
|
-
|
|
1490
|
-
df = pd.DataFrame(list(history_orders), columns=history_orders[0]._asdict())
|
|
1491
|
-
df.drop(
|
|
1492
|
-
[
|
|
1493
|
-
"time_expiration",
|
|
1494
|
-
"type_time",
|
|
1495
|
-
"state",
|
|
1496
|
-
"position_by_id",
|
|
1497
|
-
"reason",
|
|
1498
|
-
"volume_current",
|
|
1499
|
-
"price_stoplimit",
|
|
1500
|
-
"sl",
|
|
1501
|
-
"tp",
|
|
1502
|
-
],
|
|
1503
|
-
axis=1,
|
|
1504
|
-
inplace=True,
|
|
1505
|
-
)
|
|
1506
|
-
df["time_setup"] = pd.to_datetime(df["time_setup"], unit="s")
|
|
1507
|
-
df["time_done"] = pd.to_datetime(df["time_done"], unit="s")
|
|
1508
|
-
|
|
1509
|
-
if save:
|
|
1510
|
-
file = "trade_history.csv"
|
|
1511
|
-
df.to_csv(file)
|
|
1512
|
-
if to_df:
|
|
1513
|
-
return df
|
|
1514
|
-
else:
|
|
1515
|
-
history_orders = [TradeOrder(**td._asdict()) for td in history_orders]
|
|
1516
|
-
return tuple(history_orders)
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import urllib.request
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
|
|
9
|
+
|
|
10
|
+
from bbstrader.metatrader.utils import (
|
|
11
|
+
AccountInfo,
|
|
12
|
+
InvalidBroker,
|
|
13
|
+
OrderCheckResult,
|
|
14
|
+
OrderSentResult,
|
|
15
|
+
SymbolInfo,
|
|
16
|
+
TerminalInfo,
|
|
17
|
+
TickInfo,
|
|
18
|
+
TradeDeal,
|
|
19
|
+
TradeOrder,
|
|
20
|
+
TradePosition,
|
|
21
|
+
TradeRequest,
|
|
22
|
+
raise_mt5_error,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import MetaTrader5 as mt5
|
|
27
|
+
except ImportError:
|
|
28
|
+
import bbstrader.compat # noqa: F401
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Account",
|
|
33
|
+
"Broker",
|
|
34
|
+
"AdmiralMarktsGroup",
|
|
35
|
+
"JustGlobalMarkets",
|
|
36
|
+
"PepperstoneGroupLimited",
|
|
37
|
+
"check_mt5_connection",
|
|
38
|
+
"FTMO",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
__BROKERS__ = {
|
|
42
|
+
"AMG": "Admirals Group AS",
|
|
43
|
+
"JGM": "Just Global Markets Ltd.",
|
|
44
|
+
"FTMO": "FTMO S.R.O.",
|
|
45
|
+
"PGL": "Pepperstone Group Limited",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
BROKERS_TIMEZONES = {
|
|
49
|
+
"AMG": "Europe/Helsinki",
|
|
50
|
+
"JGM": "Europe/Helsinki",
|
|
51
|
+
"FTMO": "Europe/Helsinki",
|
|
52
|
+
"PGL": "Europe/Helsinki",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_ADMIRAL_MARKETS_URL_ = (
|
|
56
|
+
"https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets"
|
|
57
|
+
)
|
|
58
|
+
_JUST_MARKETS_URL_ = "https://one.justmarkets.link/a/tufvj0xugm/registration/trader"
|
|
59
|
+
_FTMO_URL_ = "https://trader.ftmo.com/?affiliates=JGmeuQqepAZLMcdOEQRp"
|
|
60
|
+
_ADMIRAL_MARKETS_PRODUCTS_ = [
|
|
61
|
+
"Stocks",
|
|
62
|
+
"ETFs",
|
|
63
|
+
"Indices",
|
|
64
|
+
"Commodities",
|
|
65
|
+
"Futures",
|
|
66
|
+
"Forex",
|
|
67
|
+
]
|
|
68
|
+
_JUST_MARKETS_PRODUCTS_ = ["Stocks", "Crypto", "indices", "Commodities", "Forex"]
|
|
69
|
+
|
|
70
|
+
SUPPORTED_BROKERS = [__BROKERS__[b] for b in {"AMG", "JGM", "FTMO"}]
|
|
71
|
+
INIT_MSG = (
|
|
72
|
+
f"\n* Ensure you have a good and stable internet connexion\n"
|
|
73
|
+
f"* Ensure you have an activete MT5 terminal install on your machine\n"
|
|
74
|
+
f"* Ensure you have an active MT5 Account with {' or '.join(SUPPORTED_BROKERS)}\n"
|
|
75
|
+
f"* If you want to trade {', '.join(_ADMIRAL_MARKETS_PRODUCTS_)}, See [{_ADMIRAL_MARKETS_URL_}]\n"
|
|
76
|
+
f"* If you want to trade {', '.join(_JUST_MARKETS_PRODUCTS_)}, See [{_JUST_MARKETS_URL_}]\n"
|
|
77
|
+
f"* If you are looking for a prop firm, See [{_FTMO_URL_}]\n"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
amg_url = _ADMIRAL_MARKETS_URL_
|
|
81
|
+
jgm_url = _JUST_MARKETS_URL_
|
|
82
|
+
ftmo_url = _FTMO_URL_
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_SYMBOLS_TYPE_ = {
|
|
86
|
+
"ETF": r"\b(ETFs?)\b",
|
|
87
|
+
"BOND": r"\b(Treasuries?)\b",
|
|
88
|
+
"FX": r"\b(Forex|Exotics?)\b",
|
|
89
|
+
"FUT": r"\b(Futures?|Forwards)\b",
|
|
90
|
+
"STK": r"\b(Stocks?|Equities?|Shares?)\b",
|
|
91
|
+
"IDX": r"\b(?:Indices?|Cash|Index)\b(?!.*\\(?:UKOIL|USOIL))",
|
|
92
|
+
"COMD": r"\b(Commodity|Commodities?|Metals?|Agricultures?|Energies?|OIL|Oil|USOIL|UKOIL)\b",
|
|
93
|
+
"CRYPTO": r"\b(Cryptos?|Cryptocurrencies|Cryptocurrency)\b",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_COUNTRY_MAP_ = {
|
|
97
|
+
"USA": r"\b(US|USA)\b",
|
|
98
|
+
"AUS": r"\b(Australia)\b",
|
|
99
|
+
"BEL": r"\b(Belgium)\b",
|
|
100
|
+
"DNK": r"\b(Denmark)\b",
|
|
101
|
+
"FIN": r"\b(Finland)\b",
|
|
102
|
+
"FRA": r"\b(France)\b",
|
|
103
|
+
"DEU": r"\b(Germany)\b",
|
|
104
|
+
"NLD": r"\b(Netherlands)\b",
|
|
105
|
+
"NOR": r"\b(Norway)\b",
|
|
106
|
+
"PRT": r"\b(Portugal)\b",
|
|
107
|
+
"ESP": r"\b(Spain)\b",
|
|
108
|
+
"SWE": r"\b(Sweden)\b",
|
|
109
|
+
"GBR": r"\b(UK)\b",
|
|
110
|
+
"CHE": r"\b(Switzerland)\b",
|
|
111
|
+
"HKG": r"\b(Hong Kong)\b",
|
|
112
|
+
"IRL": r"\b(Ireland)\b",
|
|
113
|
+
"AUT": r"\b(Austria)\b",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
AMG_EXCHANGES = {
|
|
117
|
+
"XASX": r"Australia.*\(ASX\)",
|
|
118
|
+
"XBRU": r"Belgium.*\(Euronext\)",
|
|
119
|
+
"XCSE": r"Denmark.*\(CSE\)",
|
|
120
|
+
"XHEL": r"Finland.*\(NASDAQ\)",
|
|
121
|
+
"XPAR": r"France.*\(Euronext\)",
|
|
122
|
+
"XETR": r"Germany.*\(Xetra\)",
|
|
123
|
+
"XAMS": r"Netherlands.*\(Euronext\)",
|
|
124
|
+
"XOSL": r"Norway.*\(NASDAQ\)",
|
|
125
|
+
"XLIS": r"Portugal.*\(Euronext\)",
|
|
126
|
+
"XMAD": r"Spain.*\(BME\)",
|
|
127
|
+
"XSTO": r"Sweden.*\(NASDAQ\)",
|
|
128
|
+
"XLON": r"UK.*\(LSE\)",
|
|
129
|
+
"XNYS": r"US.*\((NYSE|ARCA|AMEX)\)",
|
|
130
|
+
"NYSE": r"US.*\(NYSE\)",
|
|
131
|
+
"ARCA": r"US.*\(ARCA\)",
|
|
132
|
+
"AMEX": r"US.*\(AMEX\)",
|
|
133
|
+
"NASDAQ": r"US.*\(NASDAQ\)",
|
|
134
|
+
"BATS": r"US.*\(BATS\)",
|
|
135
|
+
"XSWX": r"Switzerland.*\(SWX\)",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def check_mt5_connection(**kwargs):
|
|
140
|
+
"""
|
|
141
|
+
Initialize the connection to the MetaTrader 5 terminal.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
path (str, optional): The path to the MetaTrader 5 terminal executable file.
|
|
145
|
+
Defaults to None (e.g., "C:\\Program Files\\MetaTrader 5\\terminal64.exe").
|
|
146
|
+
login (int, optional): The login ID of the trading account. Defaults to None.
|
|
147
|
+
password (str, optional): The password of the trading account. Defaults to None.
|
|
148
|
+
server (str, optional): The name of the trade server to which the client terminal is connected.
|
|
149
|
+
Defaults to None.
|
|
150
|
+
timeout (int, optional): Connection timeout in milliseconds. Defaults to 60_000.
|
|
151
|
+
portable (bool, optional): If True, the portable mode of the terminal is used.
|
|
152
|
+
Defaults to False (See https://www.metatrader5.com/en/terminal/help/start_advanced/start#portable).
|
|
153
|
+
|
|
154
|
+
Notes:
|
|
155
|
+
If you want to lunch multiple terminal instances:
|
|
156
|
+
- Follow these instructions to lunch each terminal in portable mode first:
|
|
157
|
+
https://www.metatrader5.com/en/terminal/help/start_advanced/start#configuration_file
|
|
158
|
+
"""
|
|
159
|
+
path = kwargs.get("path", None)
|
|
160
|
+
login = kwargs.get("login", None)
|
|
161
|
+
password = kwargs.get("password", None)
|
|
162
|
+
server = kwargs.get("server", None)
|
|
163
|
+
timeout = kwargs.get("timeout", 60_000)
|
|
164
|
+
portable = kwargs.get("portable", False)
|
|
165
|
+
|
|
166
|
+
if path is None and (login or password or server):
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"You must provide a path to the terminal executable file"
|
|
169
|
+
"when providing login, password or server"
|
|
170
|
+
)
|
|
171
|
+
try:
|
|
172
|
+
if path is not None:
|
|
173
|
+
if login is not None and password is not None and server is not None:
|
|
174
|
+
init = mt5.initialize(
|
|
175
|
+
path=path,
|
|
176
|
+
login=login,
|
|
177
|
+
password=password,
|
|
178
|
+
server=server,
|
|
179
|
+
timeout=timeout,
|
|
180
|
+
portable=portable,
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
init = mt5.initialize(path=path)
|
|
184
|
+
else:
|
|
185
|
+
init = mt5.initialize()
|
|
186
|
+
if not init:
|
|
187
|
+
raise_mt5_error(INIT_MSG)
|
|
188
|
+
except Exception:
|
|
189
|
+
raise_mt5_error(INIT_MSG)
|
|
190
|
+
|
|
191
|
+
def shutdown_mt5():
|
|
192
|
+
"""Close the connection to the MetaTrader 5 terminal."""
|
|
193
|
+
mt5.shutdown()
|
|
194
|
+
|
|
195
|
+
class Broker(object):
|
|
196
|
+
def __init__(self, name: str = None, **kwargs):
|
|
197
|
+
if name is None:
|
|
198
|
+
check_mt5_connection(**kwargs)
|
|
199
|
+
self._name = mt5.account_info().company
|
|
200
|
+
else:
|
|
201
|
+
self._name = name
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def name(self):
|
|
205
|
+
return self._name
|
|
206
|
+
|
|
207
|
+
def __str__(self):
|
|
208
|
+
return self.name
|
|
209
|
+
|
|
210
|
+
def __eq__(self, orther) -> bool:
|
|
211
|
+
return self.name == orther.name
|
|
212
|
+
|
|
213
|
+
def __ne__(self, orther) -> bool:
|
|
214
|
+
return self.name != orther.name
|
|
215
|
+
|
|
216
|
+
def __repr__(self):
|
|
217
|
+
return f"{self.__class__.__name__}({self.name})"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class AdmiralMarktsGroup(Broker):
|
|
221
|
+
def __init__(self, **kwargs):
|
|
222
|
+
super().__init__("Admirals Group AS", **kwargs)
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def timezone(self) -> str:
|
|
226
|
+
return BROKERS_TIMEZONES["AMG"]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class JustGlobalMarkets(Broker):
|
|
230
|
+
def __init__(self, **kwargs):
|
|
231
|
+
super().__init__("Just Global Markets Ltd.", **kwargs)
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def timezone(self) -> str:
|
|
235
|
+
return BROKERS_TIMEZONES["JGM"]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class FTMO(Broker):
|
|
239
|
+
def __init__(self, **kwargs):
|
|
240
|
+
super().__init__("FTMO S.R.O.", **kwargs)
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def timezone(self) -> str:
|
|
244
|
+
return BROKERS_TIMEZONES["FTMO"]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class PepperstoneGroupLimited(Broker):
|
|
248
|
+
def __init__(self, **kwargs):
|
|
249
|
+
super().__init__("Pepperstone Group Limited", **kwargs)
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def timezone(self) -> str:
|
|
253
|
+
return BROKERS_TIMEZONES["PGL"]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class AMP(Broker): ...
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
BROKERS: Dict[str, Broker] = {
|
|
260
|
+
"FTMO": FTMO(),
|
|
261
|
+
"AMG": AdmiralMarktsGroup(),
|
|
262
|
+
"JGM": JustGlobalMarkets(),
|
|
263
|
+
"PGL": PepperstoneGroupLimited(),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class Account(object):
|
|
268
|
+
"""
|
|
269
|
+
The `Account` class is utilized to retrieve information about
|
|
270
|
+
the current trading account or a specific account.
|
|
271
|
+
It enables interaction with the MT5 terminal to manage account details,
|
|
272
|
+
including account informations, terminal status, financial instrument details,
|
|
273
|
+
active orders, open positions, and trading history.
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
>>> # Instantiating the Account class
|
|
277
|
+
>>> account = Account()
|
|
278
|
+
|
|
279
|
+
>>> # Getting account information
|
|
280
|
+
>>> account_info = account.get_account_info()
|
|
281
|
+
|
|
282
|
+
>>> # Printing account information
|
|
283
|
+
>>> account.print_account_info()
|
|
284
|
+
|
|
285
|
+
>>> # Getting terminal information
|
|
286
|
+
>>> terminal_info = account.get_terminal_info()
|
|
287
|
+
|
|
288
|
+
>>> # Retrieving and printing symbol information
|
|
289
|
+
>>> symbol_info = account.show_symbol_info('EURUSD')
|
|
290
|
+
|
|
291
|
+
>>> # Getting active orders
|
|
292
|
+
>>> orders = account.get_orders()
|
|
293
|
+
|
|
294
|
+
>>> # Fetching open positions
|
|
295
|
+
>>> positions = account.get_positions()
|
|
296
|
+
|
|
297
|
+
>>> # Accessing trade history
|
|
298
|
+
>>> from_date = datetime(2020, 1, 1)
|
|
299
|
+
>>> to_date = datetime.now()
|
|
300
|
+
>>> trade_history = account.get_trade_history(from_date, to_date)
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
def __init__(self, **kwargs):
|
|
304
|
+
"""
|
|
305
|
+
Initialize the Account class.
|
|
306
|
+
|
|
307
|
+
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
308
|
+
|
|
309
|
+
"""
|
|
310
|
+
check_mt5_connection(**kwargs)
|
|
311
|
+
self._check_brokers(**kwargs)
|
|
312
|
+
|
|
313
|
+
def _check_brokers(self, **kwargs):
|
|
314
|
+
if kwargs.get("copy", False):
|
|
315
|
+
return
|
|
316
|
+
supported = BROKERS.copy()
|
|
317
|
+
if self.broker not in supported.values():
|
|
318
|
+
msg = (
|
|
319
|
+
f"{self.broker.name} is not currently supported broker for the Account() class\n"
|
|
320
|
+
f"Currently Supported brokers are: {', '.join(SUPPORTED_BROKERS)}\n"
|
|
321
|
+
f"For {supported['AMG'].name}, See [{amg_url}]\n"
|
|
322
|
+
f"For {supported['JGM'].name}, See [{jgm_url}]\n"
|
|
323
|
+
f"For {supported['FTMO'].name}, See [{ftmo_url}]\n"
|
|
324
|
+
)
|
|
325
|
+
raise InvalidBroker(message=msg)
|
|
326
|
+
|
|
327
|
+
def shutdown(self):
|
|
328
|
+
"""Close the connection to the MetaTrader 5 terminal."""
|
|
329
|
+
shutdown_mt5()
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def broker(self) -> Broker:
|
|
333
|
+
return Broker(self.get_terminal_info().company)
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def timezone(self) -> str:
|
|
337
|
+
for broker in BROKERS.values():
|
|
338
|
+
if broker == self.broker:
|
|
339
|
+
return broker.timezone
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def name(self) -> str:
|
|
343
|
+
return self.get_account_info().name
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def number(self) -> int:
|
|
347
|
+
return self.get_account_info().login
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def server(self) -> str:
|
|
351
|
+
"""The name of the trade server to which the client terminal is connected.
|
|
352
|
+
(e.g., 'AdmiralsGroup-Demo')
|
|
353
|
+
"""
|
|
354
|
+
return self.get_account_info().server
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def balance(self) -> float:
|
|
358
|
+
return self.get_account_info().balance
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def leverage(self) -> int:
|
|
362
|
+
return self.get_account_info().leverage
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def equity(self) -> float:
|
|
366
|
+
return self.get_account_info().equity
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def currency(self) -> str:
|
|
370
|
+
return self.get_account_info().currency
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def language(self) -> str:
|
|
374
|
+
"""The language of the terminal interface."""
|
|
375
|
+
return self.get_terminal_info().language
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def maxbars(self) -> int:
|
|
379
|
+
"""The maximal bars count on the chart."""
|
|
380
|
+
return self.get_terminal_info().maxbars
|
|
381
|
+
|
|
382
|
+
def get_account_info(
|
|
383
|
+
self,
|
|
384
|
+
account: Optional[int] = None,
|
|
385
|
+
password: Optional[str] = None,
|
|
386
|
+
server: Optional[str] = None,
|
|
387
|
+
timeout: Optional[int] = 60_000,
|
|
388
|
+
) -> Union[AccountInfo, None]:
|
|
389
|
+
"""
|
|
390
|
+
Get info on the current trading account or a specific account .
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
account (int, optinal) : MT5 Trading account number.
|
|
394
|
+
password (str, optinal): MT5 Trading account password.
|
|
395
|
+
|
|
396
|
+
server (str, optinal): MT5 Trading account server
|
|
397
|
+
[Brokers or terminal server ["demo", "real"]]
|
|
398
|
+
If no server is set, the last used server is applied automaticall
|
|
399
|
+
|
|
400
|
+
timeout (int, optinal):
|
|
401
|
+
Connection timeout in milliseconds. Optional named parameter.
|
|
402
|
+
If not specified, the value of 60 000 (60 seconds) is applied.
|
|
403
|
+
If the connection is not established within the specified time,
|
|
404
|
+
the call is forcibly terminated and the exception is generated.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
- AccountInfo in the form of a Namedtuple structure.
|
|
408
|
+
- None in case of an error
|
|
409
|
+
|
|
410
|
+
Raises:
|
|
411
|
+
MT5TerminalError: A specific exception based on the error code.
|
|
412
|
+
"""
|
|
413
|
+
# connect to the trade account specifying a password and a server
|
|
414
|
+
if account is not None and password is not None and server is not None:
|
|
415
|
+
try:
|
|
416
|
+
authorized = mt5.login(
|
|
417
|
+
account, password=password, server=server, timeout=timeout
|
|
418
|
+
)
|
|
419
|
+
if not authorized:
|
|
420
|
+
raise_mt5_error(message=f"Failed to connect to account #{account}")
|
|
421
|
+
else:
|
|
422
|
+
info = mt5.account_info()
|
|
423
|
+
if info is None:
|
|
424
|
+
return None
|
|
425
|
+
else:
|
|
426
|
+
return AccountInfo(**info._asdict())
|
|
427
|
+
except Exception as e:
|
|
428
|
+
raise_mt5_error(e)
|
|
429
|
+
else:
|
|
430
|
+
try:
|
|
431
|
+
info = mt5.account_info()
|
|
432
|
+
if info is None:
|
|
433
|
+
return None
|
|
434
|
+
else:
|
|
435
|
+
return AccountInfo(**info._asdict())
|
|
436
|
+
except Exception as e:
|
|
437
|
+
raise_mt5_error(e)
|
|
438
|
+
|
|
439
|
+
def _show_info(self, info_getter, info_name, symbol=None):
|
|
440
|
+
"""
|
|
441
|
+
Generic function to retrieve and print information.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
info_getter (callable): Function to retrieve the information.
|
|
445
|
+
info_name (str): Name of the information being retrieved.
|
|
446
|
+
symbol (str, optional): Symbol name, required for some info types.
|
|
447
|
+
Defaults to None.
|
|
448
|
+
|
|
449
|
+
Raises:
|
|
450
|
+
MT5TerminalError: A specific exception based on the error code.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
# Call the provided info retrieval function
|
|
454
|
+
if symbol is not None:
|
|
455
|
+
info = info_getter(symbol)
|
|
456
|
+
else:
|
|
457
|
+
info = info_getter()
|
|
458
|
+
|
|
459
|
+
if info is not None:
|
|
460
|
+
info_dict = info._asdict()
|
|
461
|
+
df = pd.DataFrame(list(info_dict.items()), columns=["PROPERTY", "VALUE"])
|
|
462
|
+
|
|
463
|
+
# Construct the print message based on whether a symbol is provided
|
|
464
|
+
if symbol:
|
|
465
|
+
if hasattr(info, "description"):
|
|
466
|
+
print(
|
|
467
|
+
f"\n{info_name.upper()} INFO FOR {symbol} ({info.description})"
|
|
468
|
+
)
|
|
469
|
+
else:
|
|
470
|
+
print(f"\n{info_name.upper()} INFO FOR {symbol}")
|
|
471
|
+
else:
|
|
472
|
+
print(f"\n{info_name.upper()} INFORMATIONS:")
|
|
473
|
+
|
|
474
|
+
pd.set_option("display.max_rows", None)
|
|
475
|
+
pd.set_option("display.max_columns", None)
|
|
476
|
+
print(df.to_string())
|
|
477
|
+
else:
|
|
478
|
+
if symbol:
|
|
479
|
+
msg = self._symbol_info_msg(symbol)
|
|
480
|
+
raise_mt5_error(message=msg)
|
|
481
|
+
else:
|
|
482
|
+
raise_mt5_error()
|
|
483
|
+
|
|
484
|
+
def show_account_info(self):
|
|
485
|
+
"""Helper function to print account info"""
|
|
486
|
+
self._show_info(self.get_account_info, "account")
|
|
487
|
+
|
|
488
|
+
def get_terminal_info(self, show=False) -> Union[TerminalInfo, None]:
|
|
489
|
+
"""
|
|
490
|
+
Get the connected MetaTrader 5 client terminal status and settings.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
show (bool): If True the Account information will be printed
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
- TerminalInfo in the form of NamedTuple Structure.
|
|
497
|
+
- None in case of an error
|
|
498
|
+
|
|
499
|
+
Raises:
|
|
500
|
+
MT5TerminalError: A specific exception based on the error code.
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
terminal_info = mt5.terminal_info()
|
|
504
|
+
if terminal_info is None:
|
|
505
|
+
return None
|
|
506
|
+
except Exception as e:
|
|
507
|
+
raise_mt5_error(e)
|
|
508
|
+
|
|
509
|
+
terminal_info_dict = terminal_info._asdict()
|
|
510
|
+
# convert the dictionary into DataFrame and print
|
|
511
|
+
df = pd.DataFrame(
|
|
512
|
+
list(terminal_info_dict.items()), columns=["PROPERTY", "VALUE"]
|
|
513
|
+
)
|
|
514
|
+
if show:
|
|
515
|
+
pd.set_option("display.max_rows", None)
|
|
516
|
+
pd.set_option("display.max_columns", None)
|
|
517
|
+
print(df.to_string())
|
|
518
|
+
return TerminalInfo(**terminal_info_dict)
|
|
519
|
+
|
|
520
|
+
def convert_currencies(self, qty: float, from_c: str, to_c: str) -> float:
|
|
521
|
+
"""Convert amount from a currency to another one.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
qty (float): The amount of `currency` to convert.
|
|
525
|
+
from_c (str): The currency to convert from.
|
|
526
|
+
to_c (str): The currency to convert to.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
- The value of `qty` in converted in `to_c`.
|
|
530
|
+
|
|
531
|
+
Notes:
|
|
532
|
+
If `from_c` or `to_co` are not supported, the `qty` will be return;
|
|
533
|
+
check "https://www.ecb.europa.eu/stats/eurofxref/eurofxref.zip"
|
|
534
|
+
for supported currencies or you can take a look at the `CurrencyConverter` project
|
|
535
|
+
on Github https://github.com/alexprengere/currencyconverter .
|
|
536
|
+
"""
|
|
537
|
+
filename = f"ecb_{datetime.now():%Y%m%d}.zip"
|
|
538
|
+
if not os.path.isfile(filename):
|
|
539
|
+
urllib.request.urlretrieve(SINGLE_DAY_ECB_URL, filename)
|
|
540
|
+
c = CurrencyConverter(filename)
|
|
541
|
+
os.remove(filename)
|
|
542
|
+
supported = c.currencies
|
|
543
|
+
if from_c not in supported or to_c not in supported:
|
|
544
|
+
rate = qty
|
|
545
|
+
else:
|
|
546
|
+
rate = c.convert(amount=qty, currency=from_c, new_currency=to_c)
|
|
547
|
+
return rate
|
|
548
|
+
|
|
549
|
+
def get_currency_rates(self, symbol: str) -> Dict[str, str]:
|
|
550
|
+
"""
|
|
551
|
+
Args:
|
|
552
|
+
symbol (str): The symbol for which to get currencies
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
- `base currency` (bc)
|
|
556
|
+
- `margin currency` (mc)
|
|
557
|
+
- `profit currency` (pc)
|
|
558
|
+
- `account currency` (ac)
|
|
559
|
+
|
|
560
|
+
Exemple:
|
|
561
|
+
>>> account = Account()
|
|
562
|
+
>>> account.get_currency_rates('EURUSD')
|
|
563
|
+
{'bc': 'EUR', 'mc': 'EUR', 'pc': 'USD', 'ac': 'USD'}
|
|
564
|
+
"""
|
|
565
|
+
info = self.get_symbol_info(symbol)
|
|
566
|
+
bc = info.currency_base
|
|
567
|
+
pc = info.currency_profit
|
|
568
|
+
mc = info.currency_margin
|
|
569
|
+
ac = self.get_account_info().currency
|
|
570
|
+
return {"bc": bc, "mc": mc, "pc": pc, "ac": ac}
|
|
571
|
+
|
|
572
|
+
def get_symbols(
|
|
573
|
+
self,
|
|
574
|
+
symbol_type="ALL",
|
|
575
|
+
check_etf=False,
|
|
576
|
+
save=False,
|
|
577
|
+
file_name="symbols",
|
|
578
|
+
include_desc=False,
|
|
579
|
+
display_total=False,
|
|
580
|
+
) -> List[str]:
|
|
581
|
+
"""
|
|
582
|
+
Get all specified financial instruments from the MetaTrader 5 terminal.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
symbol_type (str) The category of instrument to get
|
|
586
|
+
- `ALL`: For all available symbols
|
|
587
|
+
- `STK`: Stocks (e.g., 'GOOGL')
|
|
588
|
+
- `ETF`: ETFs (e.g., 'QQQ')
|
|
589
|
+
- `IDX`: Indices (e.g., 'SP500')
|
|
590
|
+
- `FX`: Forex pairs (e.g., 'EURUSD')
|
|
591
|
+
- `COMD`: Commodities (e.g., 'CRUDOIL', 'GOLD')
|
|
592
|
+
- `FUT`: Futures (e.g., 'USTNote_U4'),
|
|
593
|
+
- `CRYPTO`: Cryptocurrencies (e.g., 'BTC', 'ETH')
|
|
594
|
+
- `BOND`: Bonds (e.g., 'USTN10YR')
|
|
595
|
+
|
|
596
|
+
check_etf (bool): If True and symbol_type is 'etf', check if the
|
|
597
|
+
ETF description contains 'ETF'.
|
|
598
|
+
|
|
599
|
+
save (bool): If True, save the symbols to a file.
|
|
600
|
+
|
|
601
|
+
file_name (str): The name of the file to save the symbols to
|
|
602
|
+
(without the extension).
|
|
603
|
+
|
|
604
|
+
include_desc (bool): If True, include the symbol's description
|
|
605
|
+
in the output and saved file.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
list: A list of symbols.
|
|
609
|
+
|
|
610
|
+
Raises:
|
|
611
|
+
Exception: If there is an error connecting to MT5 or retrieving symbols.
|
|
612
|
+
"""
|
|
613
|
+
symbols = mt5.symbols_get()
|
|
614
|
+
if not symbols:
|
|
615
|
+
raise_mt5_error()
|
|
616
|
+
|
|
617
|
+
symbol_list = []
|
|
618
|
+
patterns = _SYMBOLS_TYPE_
|
|
619
|
+
|
|
620
|
+
if symbol_type != "ALL":
|
|
621
|
+
if symbol_type not in patterns:
|
|
622
|
+
raise ValueError(f"Unsupported symbol type: {symbol_type}")
|
|
623
|
+
|
|
624
|
+
if save:
|
|
625
|
+
max_lengh = max([len(s.name) for s in symbols])
|
|
626
|
+
file_path = f"{file_name}.txt"
|
|
627
|
+
with open(file_path, mode="w", encoding="utf-8") as file:
|
|
628
|
+
for s in symbols:
|
|
629
|
+
info = self.get_symbol_info(s.name)
|
|
630
|
+
if symbol_type == "ALL":
|
|
631
|
+
self._write_symbol(file, info, include_desc, max_lengh)
|
|
632
|
+
symbol_list.append(s.name)
|
|
633
|
+
else:
|
|
634
|
+
pattern = re.compile(patterns[symbol_type])
|
|
635
|
+
match = re.search(pattern, info.path)
|
|
636
|
+
if match:
|
|
637
|
+
if (
|
|
638
|
+
symbol_type == "ETF"
|
|
639
|
+
and check_etf
|
|
640
|
+
and "ETF" not in info.description
|
|
641
|
+
):
|
|
642
|
+
raise ValueError(
|
|
643
|
+
f"{info.name} doesn't have 'ETF' in its description. "
|
|
644
|
+
"If this is intended, set check_etf=False."
|
|
645
|
+
)
|
|
646
|
+
self._write_symbol(file, info, include_desc, max_lengh)
|
|
647
|
+
symbol_list.append(s.name)
|
|
648
|
+
|
|
649
|
+
else: # If not saving to a file, just process the symbols
|
|
650
|
+
for s in symbols:
|
|
651
|
+
info = self.get_symbol_info(s.name)
|
|
652
|
+
if symbol_type == "ALL":
|
|
653
|
+
symbol_list.append(s.name)
|
|
654
|
+
else:
|
|
655
|
+
pattern = re.compile(patterns[symbol_type]) # , re.IGNORECASE
|
|
656
|
+
match = re.search(pattern, info.path)
|
|
657
|
+
if match:
|
|
658
|
+
if (
|
|
659
|
+
symbol_type == "ETF"
|
|
660
|
+
and check_etf
|
|
661
|
+
and "ETF" not in info.description
|
|
662
|
+
):
|
|
663
|
+
raise ValueError(
|
|
664
|
+
f"{info.name} doesn't have 'ETF' in its description. "
|
|
665
|
+
"If this is intended, set check_etf=False."
|
|
666
|
+
)
|
|
667
|
+
symbol_list.append(s.name)
|
|
668
|
+
|
|
669
|
+
# Print a summary of the retrieved symbols
|
|
670
|
+
if display_total:
|
|
671
|
+
names = {
|
|
672
|
+
"ALL": "Symbols",
|
|
673
|
+
"STK": "Stocks",
|
|
674
|
+
"ETF": "ETFs",
|
|
675
|
+
"IDX": "Indices",
|
|
676
|
+
"FX": "Forex Paires",
|
|
677
|
+
"COMD": "Commodities",
|
|
678
|
+
"FUT": "Futures",
|
|
679
|
+
"CRYPTO": "Cryptos Assets",
|
|
680
|
+
"BOND": "Bonds",
|
|
681
|
+
}
|
|
682
|
+
print(f"Total {names[symbol_type]}: {len(symbol_list)}")
|
|
683
|
+
|
|
684
|
+
return symbol_list
|
|
685
|
+
|
|
686
|
+
def _write_symbol(self, file, info, include_desc, max_lengh):
|
|
687
|
+
"""Helper function to write symbol information to a file."""
|
|
688
|
+
if include_desc:
|
|
689
|
+
space = " " * int(max_lengh - len(info.name))
|
|
690
|
+
file.write(info.name + space + "|" + info.description + "\n")
|
|
691
|
+
else:
|
|
692
|
+
file.write(info.name + "\n")
|
|
693
|
+
|
|
694
|
+
def get_symbol_type(
|
|
695
|
+
self, symbol: str
|
|
696
|
+
) -> Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "BOND", "unknown"]:
|
|
697
|
+
"""
|
|
698
|
+
Determines the type of a given financial instrument symbol.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
symbol (str): The symbol of the financial instrument (e.g., `GOOGL`, `EURUSD`).
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "BOND", "unknown"]:
|
|
705
|
+
The type of the financial instrument, one of the following:
|
|
706
|
+
|
|
707
|
+
- `STK`: For Stocks (e.g., `GOOGL`)
|
|
708
|
+
- `ETF`: For ETFs (e.g., `QQQ`)
|
|
709
|
+
- `IDX`: For Indices (e.g., `SP500`)
|
|
710
|
+
- `FX` : For Forex pairs (e.g., `EURUSD`)
|
|
711
|
+
- `COMD`: For Commodities (e.g., `CRUDOIL`, `GOLD`)
|
|
712
|
+
- `FUT` : For Futures (e.g., `USTNote_U4`)
|
|
713
|
+
- `CRYPTO`: For Cryptocurrencies (e.g., `BTC`, `ETH`)
|
|
714
|
+
- `BOND`: For Bonds (e.g., `USTN10YR`)
|
|
715
|
+
|
|
716
|
+
Returns `unknown` if the type cannot be determined.
|
|
717
|
+
"""
|
|
718
|
+
|
|
719
|
+
patterns = _SYMBOLS_TYPE_
|
|
720
|
+
info = self.get_symbol_info(symbol)
|
|
721
|
+
indices = self.get_symbols(symbol_type="IDX")
|
|
722
|
+
commodity = self.get_symbols(symbol_type="COMD")
|
|
723
|
+
if info is not None:
|
|
724
|
+
for symbol_type, pattern in patterns.items():
|
|
725
|
+
if (
|
|
726
|
+
symbol_type in ["IDX", "COMD"]
|
|
727
|
+
and self.broker == PepperstoneGroupLimited()
|
|
728
|
+
and info.name.endswith("-F")
|
|
729
|
+
and info.name in indices + commodity
|
|
730
|
+
):
|
|
731
|
+
symbol_type = "FUT"
|
|
732
|
+
pattern = r"\b(Forwards?)\b"
|
|
733
|
+
search = re.compile(pattern)
|
|
734
|
+
if re.search(search, info.path):
|
|
735
|
+
return symbol_type
|
|
736
|
+
return "unknown"
|
|
737
|
+
|
|
738
|
+
def _get_symbols_by_category(self, symbol_type, category, category_map):
|
|
739
|
+
if category not in category_map:
|
|
740
|
+
raise ValueError(
|
|
741
|
+
f"Unsupported category: {category}. Choose from: {', '.join(category_map)}"
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
symbols = self.get_symbols(symbol_type=symbol_type)
|
|
745
|
+
pattern = re.compile(category_map[category], re.IGNORECASE)
|
|
746
|
+
|
|
747
|
+
symbol_list = []
|
|
748
|
+
for s in symbols:
|
|
749
|
+
info = self.get_symbol_info(s)
|
|
750
|
+
match = re.search(pattern, info.path)
|
|
751
|
+
if match:
|
|
752
|
+
symbol_list.append(s)
|
|
753
|
+
return symbol_list
|
|
754
|
+
|
|
755
|
+
def get_fx_symbols(
|
|
756
|
+
self,
|
|
757
|
+
category: Literal["majors", "minors", "exotics", "crosses", "ndfs"] = "majors",
|
|
758
|
+
) -> List[str]:
|
|
759
|
+
"""
|
|
760
|
+
Retrieves a list of forex symbols belonging to a specific category.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
category (str, optional): The category of forex symbols to retrieve.
|
|
764
|
+
Possible values are 'majors', 'minors', 'exotics', 'crosses', 'ndfs'.
|
|
765
|
+
Defaults to 'majors'.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
list: A list of forex symbol names matching the specified category.
|
|
769
|
+
|
|
770
|
+
Raises:
|
|
771
|
+
ValueError: If an unsupported category is provided.
|
|
772
|
+
|
|
773
|
+
Notes:
|
|
774
|
+
This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
|
|
775
|
+
For other brokers use `get_symbols()` or this method will use it by default.
|
|
776
|
+
"""
|
|
777
|
+
if self.broker not in [AdmiralMarktsGroup(), PepperstoneGroupLimited()]:
|
|
778
|
+
return self.get_symbols(symbol_type="FX")
|
|
779
|
+
else:
|
|
780
|
+
fx_categories = {
|
|
781
|
+
"majors": r"\b(Majors?)\b",
|
|
782
|
+
"minors": r"\b(Minors?)\b",
|
|
783
|
+
"exotics": r"\b(Exotics?)\b",
|
|
784
|
+
"crosses": r"\b(Crosses?)\b",
|
|
785
|
+
"ndfs": r"\b(NDFs?)\b",
|
|
786
|
+
}
|
|
787
|
+
return self._get_symbols_by_category("FX", category, fx_categories)
|
|
788
|
+
|
|
789
|
+
def get_stocks_from_country(
|
|
790
|
+
self, country_code: str = "USA", etf=False
|
|
791
|
+
) -> List[str]:
|
|
792
|
+
"""
|
|
793
|
+
Retrieves a list of stock symbols from a specific country.
|
|
794
|
+
|
|
795
|
+
Supported countries are:
|
|
796
|
+
* **Australia:** AUS
|
|
797
|
+
* **Belgium:** BEL
|
|
798
|
+
* **Denmark:** DNK
|
|
799
|
+
* **Finland:** FIN
|
|
800
|
+
* **France:** FRA
|
|
801
|
+
* **Germany:** DEU
|
|
802
|
+
* **Netherlands:** NLD
|
|
803
|
+
* **Norway:** NOR
|
|
804
|
+
* **Portugal:** PRT
|
|
805
|
+
* **Spain:** ESP
|
|
806
|
+
* **Sweden:** SWE
|
|
807
|
+
* **United Kingdom:** GBR
|
|
808
|
+
* **United States:** USA
|
|
809
|
+
* **Switzerland:** CHE
|
|
810
|
+
* **Hong Kong:** HKG
|
|
811
|
+
* **Ireland:** IRL
|
|
812
|
+
* **Austria:** AUT
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
country (str, optional): The country code of stocks to retrieve.
|
|
816
|
+
Defaults to 'USA'.
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
list: A list of stock symbol names from the specified country.
|
|
820
|
+
|
|
821
|
+
Raises:
|
|
822
|
+
ValueError: If an unsupported country is provided.
|
|
823
|
+
|
|
824
|
+
Notes:
|
|
825
|
+
This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
|
|
826
|
+
For other brokers use `get_symbols()` or this method will use it by default.
|
|
827
|
+
"""
|
|
828
|
+
|
|
829
|
+
if self.broker not in [AdmiralMarktsGroup(), PepperstoneGroupLimited()]:
|
|
830
|
+
stocks = self.get_symbols(symbol_type="STK")
|
|
831
|
+
return stocks
|
|
832
|
+
else:
|
|
833
|
+
country_map = _COUNTRY_MAP_
|
|
834
|
+
stocks = self._get_symbols_by_category("STK", country_code, country_map)
|
|
835
|
+
if etf:
|
|
836
|
+
etfs = self._get_symbols_by_category("ETF", country_code, country_map)
|
|
837
|
+
return stocks + etfs
|
|
838
|
+
return stocks
|
|
839
|
+
|
|
840
|
+
def get_stocks_from_exchange(
|
|
841
|
+
self, exchange_code: str = "XNYS", etf=True
|
|
842
|
+
) -> List[str]:
|
|
843
|
+
"""
|
|
844
|
+
Get stock symbols from a specific exchange using the ISO Code for the exchange.
|
|
845
|
+
|
|
846
|
+
Supported exchanges are from Admirals Group AS products:
|
|
847
|
+
* **XASX:** **Australian Securities Exchange**
|
|
848
|
+
* **XBRU:** **Euronext Brussels Exchange**
|
|
849
|
+
* **XCSE:** **Copenhagen Stock Exchange**
|
|
850
|
+
* **XHEL:** **NASDAQ OMX Helsinki**
|
|
851
|
+
* **XPAR:** **Euronext Paris**
|
|
852
|
+
* **XETR:** **Xetra Frankfurt**
|
|
853
|
+
* **XOSL:** **Oslo Stock Exchange**
|
|
854
|
+
* **XLIS:** **Euronext Lisbon**
|
|
855
|
+
* **XMAD:** **Bolsa de Madrid**
|
|
856
|
+
* **XSTO:** **NASDAQ OMX Stockholm**
|
|
857
|
+
* **XLON:** **London Stock Exchange**
|
|
858
|
+
* **NYSE:** **New York Stock Exchange**
|
|
859
|
+
* **ARCA:** **NYSE ARCA**
|
|
860
|
+
* **AMEX:** **NYSE AMEX**
|
|
861
|
+
* **XNYS:** **New York Stock Exchange (AMEX, ARCA, NYSE)**
|
|
862
|
+
* **NASDAQ:** **NASDAQ**
|
|
863
|
+
* **BATS:** **BATS Exchange**
|
|
864
|
+
* **XSWX:** **SWX Swiss Exchange**
|
|
865
|
+
* **XAMS:** **Euronext Amsterdam**
|
|
866
|
+
|
|
867
|
+
Args:
|
|
868
|
+
exchange_code (str, optional): The ISO code of the exchange.
|
|
869
|
+
etf (bool, optional): If True, include ETFs from the exchange. Defaults to True.
|
|
870
|
+
|
|
871
|
+
Returns:
|
|
872
|
+
list: A list of stock symbol names from the specified exchange.
|
|
873
|
+
|
|
874
|
+
Raises:
|
|
875
|
+
ValueError: If an unsupported exchange is provided.
|
|
876
|
+
|
|
877
|
+
Notes:
|
|
878
|
+
This mthods works primarly with Admirals Group AS products,
|
|
879
|
+
For other brokers use `get_symbols()` or this method will use it by default.
|
|
880
|
+
"""
|
|
881
|
+
if self.broker != AdmiralMarktsGroup():
|
|
882
|
+
stocks = self.get_symbols(symbol_type="STK")
|
|
883
|
+
return stocks
|
|
884
|
+
else:
|
|
885
|
+
exchange_map = AMG_EXCHANGES
|
|
886
|
+
stocks = self._get_symbols_by_category("STK", exchange_code, exchange_map)
|
|
887
|
+
if etf:
|
|
888
|
+
etfs = self._get_symbols_by_category("ETF", exchange_code, exchange_map)
|
|
889
|
+
return stocks + etfs
|
|
890
|
+
return stocks
|
|
891
|
+
|
|
892
|
+
def get_future_symbols(self, category: str = "ALL") -> List[str]:
|
|
893
|
+
"""
|
|
894
|
+
Retrieves a list of future symbols belonging to a specific category.
|
|
895
|
+
|
|
896
|
+
Args:
|
|
897
|
+
category : The category of future symbols to retrieve.
|
|
898
|
+
Possible values are 'ALL', 'agricultures', 'energies', 'metals'.
|
|
899
|
+
Defaults to 'ALL'.
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
list: A list of future symbol names matching the specified category.
|
|
903
|
+
|
|
904
|
+
Raises:
|
|
905
|
+
ValueError: If an unsupported category is provided.
|
|
906
|
+
|
|
907
|
+
Notes:
|
|
908
|
+
This mthods works primarly with Admirals Group AS products,
|
|
909
|
+
For other brokers use `get_symbols()` or this method will use it by default.
|
|
910
|
+
"""
|
|
911
|
+
category = category.lower()
|
|
912
|
+
if self.broker != AdmiralMarktsGroup():
|
|
913
|
+
return self.get_symbols(symbol_type="FUT")
|
|
914
|
+
elif category in ["all", "index"]:
|
|
915
|
+
categories = {
|
|
916
|
+
"all": r"\b(Futures?)\b",
|
|
917
|
+
"index": r"\b(Index)\b",
|
|
918
|
+
}
|
|
919
|
+
return self._get_symbols_by_category("FUT", category, categories)
|
|
920
|
+
else:
|
|
921
|
+
metals = []
|
|
922
|
+
energies = []
|
|
923
|
+
agricultures = []
|
|
924
|
+
bonds = []
|
|
925
|
+
commodities = self.get_symbols(symbol_type="COMD")
|
|
926
|
+
futures = self.get_symbols(symbol_type="FUT")
|
|
927
|
+
for symbol in futures:
|
|
928
|
+
info = self.get_symbol_info(symbol)
|
|
929
|
+
if info.name.startswith("_"):
|
|
930
|
+
if "XAU" in info.name:
|
|
931
|
+
metals.append(info.name)
|
|
932
|
+
if "oil" in info.name.lower():
|
|
933
|
+
energies.append(info.name)
|
|
934
|
+
name = info.name.split("_")[1]
|
|
935
|
+
if name in commodities:
|
|
936
|
+
_info = self.get_symbol_info(name)
|
|
937
|
+
if "Metals" in _info.path:
|
|
938
|
+
metals.append(info.name)
|
|
939
|
+
elif "Energies" in _info.path:
|
|
940
|
+
energies.append(info.name)
|
|
941
|
+
elif "Agricultures" in _info.path:
|
|
942
|
+
agricultures.append(info.name)
|
|
943
|
+
|
|
944
|
+
elif info.name.startswith("#"):
|
|
945
|
+
if "Index" not in info.path:
|
|
946
|
+
bonds.append(info.name)
|
|
947
|
+
if category == "metals":
|
|
948
|
+
return metals
|
|
949
|
+
elif category == "energies":
|
|
950
|
+
return energies
|
|
951
|
+
elif category == "agricultures":
|
|
952
|
+
return agricultures
|
|
953
|
+
elif category == "bonds":
|
|
954
|
+
return bonds
|
|
955
|
+
|
|
956
|
+
def get_symbol_info(self, symbol: str) -> Union[SymbolInfo, None]:
|
|
957
|
+
"""Get symbol properties
|
|
958
|
+
|
|
959
|
+
Args:
|
|
960
|
+
symbol (str): Symbol name
|
|
961
|
+
|
|
962
|
+
Returns:
|
|
963
|
+
- SymbolInfo in the form of a NamedTuple().
|
|
964
|
+
- None in case of an error.
|
|
965
|
+
|
|
966
|
+
Raises:
|
|
967
|
+
MT5TerminalError: A specific exception based on the error code.
|
|
968
|
+
|
|
969
|
+
Notes:
|
|
970
|
+
The `time` property is converted to a `datetime` object using Broker server time.
|
|
971
|
+
"""
|
|
972
|
+
try:
|
|
973
|
+
symbol_info = mt5.symbol_info(symbol)
|
|
974
|
+
if symbol_info is None:
|
|
975
|
+
return None
|
|
976
|
+
else:
|
|
977
|
+
symbol_info_dict = symbol_info._asdict()
|
|
978
|
+
time = datetime.fromtimestamp(symbol_info.time)
|
|
979
|
+
symbol_info_dict["time"] = time
|
|
980
|
+
return SymbolInfo(**symbol_info_dict)
|
|
981
|
+
except Exception as e:
|
|
982
|
+
msg = self._symbol_info_msg(symbol)
|
|
983
|
+
raise_mt5_error(message=f"{e + msg}")
|
|
984
|
+
|
|
985
|
+
def show_symbol_info(self, symbol: str):
|
|
986
|
+
"""
|
|
987
|
+
Print symbol properties
|
|
988
|
+
|
|
989
|
+
Args:
|
|
990
|
+
symbol (str): Symbol name
|
|
991
|
+
"""
|
|
992
|
+
self._show_info(self.get_symbol_info, "symbol", symbol=symbol)
|
|
993
|
+
|
|
994
|
+
def _symbol_info_msg(self, symbol):
|
|
995
|
+
return (
|
|
996
|
+
f"No history found for {symbol} in Market Watch.\n"
|
|
997
|
+
f"* Ensure {symbol} is selected and displayed in the Market Watch window.\n"
|
|
998
|
+
f"* See https://www.metatrader5.com/en/terminal/help/trading/market_watch\n"
|
|
999
|
+
f"* Ensure the symbol name is correct.\n"
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
def get_tick_info(self, symbol: str) -> Union[TickInfo, None]:
|
|
1003
|
+
"""Get symbol tick properties
|
|
1004
|
+
|
|
1005
|
+
Args:
|
|
1006
|
+
symbol (str): Symbol name
|
|
1007
|
+
|
|
1008
|
+
Returns:
|
|
1009
|
+
- TickInfo in the form of a NamedTuple().
|
|
1010
|
+
- None in case of an error.
|
|
1011
|
+
|
|
1012
|
+
Raises:
|
|
1013
|
+
MT5TerminalError: A specific exception based on the error code.
|
|
1014
|
+
|
|
1015
|
+
Notes:
|
|
1016
|
+
The `time` property is converted to a `datetime` object using Broker server time.
|
|
1017
|
+
"""
|
|
1018
|
+
try:
|
|
1019
|
+
tick_info = mt5.symbol_info_tick(symbol)
|
|
1020
|
+
if tick_info is None:
|
|
1021
|
+
return None
|
|
1022
|
+
else:
|
|
1023
|
+
info_dict = tick_info._asdict()
|
|
1024
|
+
time = datetime.fromtimestamp(tick_info.time)
|
|
1025
|
+
info_dict["time"] = time
|
|
1026
|
+
return TickInfo(**info_dict)
|
|
1027
|
+
except Exception as e:
|
|
1028
|
+
msg = self._symbol_info_msg(symbol)
|
|
1029
|
+
raise_mt5_error(message=f"{e + msg}")
|
|
1030
|
+
|
|
1031
|
+
def show_tick_info(self, symbol: str):
|
|
1032
|
+
"""
|
|
1033
|
+
Print Tick properties
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
symbol (str): Symbol name
|
|
1037
|
+
"""
|
|
1038
|
+
self._show_info(self.get_tick_info, "tick", symbol=symbol)
|
|
1039
|
+
|
|
1040
|
+
def calculate_margin(
|
|
1041
|
+
self, action: Literal["buy", "sell"], symbol: str, lot: float, price: float
|
|
1042
|
+
) -> float:
|
|
1043
|
+
"""
|
|
1044
|
+
Calculate margin required for an order.
|
|
1045
|
+
|
|
1046
|
+
Args:
|
|
1047
|
+
action (str): The trading action, either 'buy' or 'sell'.
|
|
1048
|
+
symbol (str): The symbol of the financial instrument.
|
|
1049
|
+
lot (float): The lot size of the order.
|
|
1050
|
+
price (float): The price of the order.
|
|
1051
|
+
|
|
1052
|
+
Returns:
|
|
1053
|
+
float: The margin required for the order.
|
|
1054
|
+
|
|
1055
|
+
Raises:
|
|
1056
|
+
MT5TerminalError: A specific exception based on the error code.
|
|
1057
|
+
"""
|
|
1058
|
+
actions = {"buy": mt5.ORDER_TYPE_BUY, "sell": mt5.ORDER_TYPE_SELL}
|
|
1059
|
+
try:
|
|
1060
|
+
margin = mt5.order_calc_margin(actions[action], symbol, lot, price)
|
|
1061
|
+
if margin is None:
|
|
1062
|
+
return None
|
|
1063
|
+
return margin
|
|
1064
|
+
except Exception as e:
|
|
1065
|
+
raise_mt5_error(e)
|
|
1066
|
+
|
|
1067
|
+
def check_order(self, request: Dict[str, Any]) -> OrderCheckResult:
|
|
1068
|
+
"""
|
|
1069
|
+
Check funds sufficiency for performing a required trading operation.
|
|
1070
|
+
|
|
1071
|
+
Args:
|
|
1072
|
+
request (Dict[str, Any]): `TradeRequest` type structure describing the required trading action.
|
|
1073
|
+
|
|
1074
|
+
Returns:
|
|
1075
|
+
OrderCheckResult:
|
|
1076
|
+
The check result as the `OrderCheckResult` structure.
|
|
1077
|
+
|
|
1078
|
+
The `request` field in the returned structure contains the trading request passed to `check_order()`.
|
|
1079
|
+
|
|
1080
|
+
Raises:
|
|
1081
|
+
MT5TerminalError: Raised if there is an error in the trading terminal based on the error code.
|
|
1082
|
+
|
|
1083
|
+
Notes:
|
|
1084
|
+
Successful submission of a request does not guarantee that the requested trading
|
|
1085
|
+
operation will be executed successfully.
|
|
1086
|
+
"""
|
|
1087
|
+
|
|
1088
|
+
try:
|
|
1089
|
+
result = mt5.order_check(request)
|
|
1090
|
+
result_dict = result._asdict()
|
|
1091
|
+
trade_request = TradeRequest(**result.request._asdict())
|
|
1092
|
+
result_dict["request"] = trade_request
|
|
1093
|
+
return OrderCheckResult(**result_dict)
|
|
1094
|
+
except Exception as e:
|
|
1095
|
+
raise_mt5_error(e)
|
|
1096
|
+
|
|
1097
|
+
def send_order(self, request: Dict[str, Any]) -> OrderSentResult:
|
|
1098
|
+
"""
|
|
1099
|
+
Send a request to perform a trading operation from the terminal to the trade server.
|
|
1100
|
+
|
|
1101
|
+
Args:
|
|
1102
|
+
request (Dict[str, Any]): `TradeRequest` type structure describing the required trading action.
|
|
1103
|
+
|
|
1104
|
+
Returns:
|
|
1105
|
+
OrderSentResult:
|
|
1106
|
+
The execution result as the `OrderSentResult` structure.
|
|
1107
|
+
|
|
1108
|
+
The `request` field in the returned structure contains the trading request passed to `send_order()`.
|
|
1109
|
+
|
|
1110
|
+
Raises:
|
|
1111
|
+
MT5TerminalError: Raised if there is an error in the trading terminal based on the error code.
|
|
1112
|
+
"""
|
|
1113
|
+
try:
|
|
1114
|
+
result = mt5.order_send(request)
|
|
1115
|
+
result_dict = result._asdict()
|
|
1116
|
+
trade_request = TradeRequest(**result.request._asdict())
|
|
1117
|
+
result_dict["request"] = trade_request
|
|
1118
|
+
return OrderSentResult(**result_dict)
|
|
1119
|
+
except Exception as e:
|
|
1120
|
+
raise_mt5_error(e)
|
|
1121
|
+
|
|
1122
|
+
def get_positions(
|
|
1123
|
+
self,
|
|
1124
|
+
symbol: Optional[str] = None,
|
|
1125
|
+
group: Optional[str] = None,
|
|
1126
|
+
ticket: Optional[int] = None,
|
|
1127
|
+
to_df: bool = False,
|
|
1128
|
+
) -> Union[pd.DataFrame, Tuple[TradePosition], None]:
|
|
1129
|
+
"""
|
|
1130
|
+
Get open positions with the ability to filter by symbol or ticket.
|
|
1131
|
+
There are four call options:
|
|
1132
|
+
|
|
1133
|
+
- Call without parameters. Returns open positions for all symbols.
|
|
1134
|
+
- Call specifying a symbol. Returns open positions for the specified symbol.
|
|
1135
|
+
- Call specifying a group of symbols. Returns open positions for the specified group of symbols.
|
|
1136
|
+
- Call specifying a position ticket. Returns the position corresponding to the specified ticket.
|
|
1137
|
+
|
|
1138
|
+
Args:
|
|
1139
|
+
symbol (Optional[str]): Symbol name. Optional named parameter.
|
|
1140
|
+
If a symbol is specified, the `ticket` parameter is ignored.
|
|
1141
|
+
|
|
1142
|
+
group (Optional[str]): The filter for arranging a group of necessary symbols.
|
|
1143
|
+
Optional named parameter. If the group is specified,
|
|
1144
|
+
the function returns only positions meeting specified criteria
|
|
1145
|
+
for a symbol name.
|
|
1146
|
+
|
|
1147
|
+
ticket (Optional[int]): Position ticket. Optional named parameter.
|
|
1148
|
+
A unique number assigned to each newly opened position.
|
|
1149
|
+
It usually matches the ticket of the order used to open the position,
|
|
1150
|
+
except when the ticket is changed as a result of service operations on the server,
|
|
1151
|
+
for example, when charging swaps with position re-opening.
|
|
1152
|
+
|
|
1153
|
+
to_df (bool): If True, a DataFrame is returned.
|
|
1154
|
+
|
|
1155
|
+
Returns:
|
|
1156
|
+
Union[pd.DataFrame, Tuple[TradePosition], None]:
|
|
1157
|
+
- `TradePosition` in the form of a named tuple structure (namedtuple) or pd.DataFrame.
|
|
1158
|
+
- `None` in case of an error.
|
|
1159
|
+
|
|
1160
|
+
Notes:
|
|
1161
|
+
The method allows receiving all open positions within a specified period.
|
|
1162
|
+
|
|
1163
|
+
The `group` parameter may contain several comma-separated conditions.
|
|
1164
|
+
|
|
1165
|
+
A condition can be set as a mask using '*'.
|
|
1166
|
+
|
|
1167
|
+
The logical negation symbol '!' can be used for exclusion.
|
|
1168
|
+
|
|
1169
|
+
All conditions are applied sequentially, which means conditions for inclusion
|
|
1170
|
+
in a group should be specified first, followed by an exclusion condition.
|
|
1171
|
+
|
|
1172
|
+
For example, `group="*, !EUR"` means that deals for all symbols should be selected first,
|
|
1173
|
+
and those containing "EUR" in symbol names should be excluded afterward.
|
|
1174
|
+
"""
|
|
1175
|
+
|
|
1176
|
+
if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
|
|
1177
|
+
raise ValueError(
|
|
1178
|
+
"Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
if symbol is not None:
|
|
1182
|
+
positions = mt5.positions_get(symbol=symbol)
|
|
1183
|
+
elif group is not None:
|
|
1184
|
+
positions = mt5.positions_get(group=group)
|
|
1185
|
+
elif ticket is not None:
|
|
1186
|
+
positions = mt5.positions_get(ticket=ticket)
|
|
1187
|
+
else:
|
|
1188
|
+
positions = mt5.positions_get()
|
|
1189
|
+
|
|
1190
|
+
if positions is None or len(positions) == 0:
|
|
1191
|
+
return None
|
|
1192
|
+
if to_df:
|
|
1193
|
+
df = pd.DataFrame(list(positions), columns=positions[0]._asdict())
|
|
1194
|
+
df["time"] = pd.to_datetime(df["time"], unit="s")
|
|
1195
|
+
df.drop(
|
|
1196
|
+
["time_update", "time_msc", "time_update_msc", "external_id"],
|
|
1197
|
+
axis=1,
|
|
1198
|
+
inplace=True,
|
|
1199
|
+
)
|
|
1200
|
+
return df
|
|
1201
|
+
else:
|
|
1202
|
+
trade_positions = [TradePosition(**p._asdict()) for p in positions]
|
|
1203
|
+
return tuple(trade_positions)
|
|
1204
|
+
|
|
1205
|
+
def get_trades_history(
|
|
1206
|
+
self,
|
|
1207
|
+
date_from: datetime = datetime(2000, 1, 1),
|
|
1208
|
+
date_to: Optional[datetime] = None,
|
|
1209
|
+
group: Optional[str] = None,
|
|
1210
|
+
ticket: Optional[int] = None, # TradeDeal.ticket
|
|
1211
|
+
position: Optional[int] = None, # TradePosition.ticket
|
|
1212
|
+
to_df: bool = True,
|
|
1213
|
+
save: bool = False,
|
|
1214
|
+
) -> Union[pd.DataFrame, Tuple[TradeDeal], None]:
|
|
1215
|
+
"""
|
|
1216
|
+
Get deals from trading history within the specified interval
|
|
1217
|
+
with the ability to filter by `ticket` or `position`.
|
|
1218
|
+
|
|
1219
|
+
You can call this method in the following ways:
|
|
1220
|
+
|
|
1221
|
+
- Call with a `time interval`. Returns all deals falling within the specified interval.
|
|
1222
|
+
|
|
1223
|
+
- Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.
|
|
1224
|
+
|
|
1225
|
+
- Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.
|
|
1226
|
+
|
|
1227
|
+
Args:
|
|
1228
|
+
date_from (datetime): Date the bars are requested from.
|
|
1229
|
+
Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
|
|
1230
|
+
Bars with the open time >= `date_from` are returned. Required unnamed parameter.
|
|
1231
|
+
|
|
1232
|
+
date_to (Optional[datetime]): Same as `date_from`.
|
|
1233
|
+
|
|
1234
|
+
group (Optional[str]): The filter for arranging a group of necessary symbols.
|
|
1235
|
+
Optional named parameter. If the group is specified,
|
|
1236
|
+
the function returns only positions meeting specified criteria
|
|
1237
|
+
for a symbol name.
|
|
1238
|
+
|
|
1239
|
+
ticket (Optional[int]): Ticket of an order (stored in `DEAL_ORDER`) for which all deals should be received.
|
|
1240
|
+
Optional parameter. If not specified, the filter is not applied.
|
|
1241
|
+
|
|
1242
|
+
position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) for which all deals should be received.
|
|
1243
|
+
Optional parameter. If not specified, the filter is not applied.
|
|
1244
|
+
|
|
1245
|
+
to_df (bool): If True, a DataFrame is returned.
|
|
1246
|
+
|
|
1247
|
+
save (bool): If set to True, a CSV file will be created to save the history.
|
|
1248
|
+
|
|
1249
|
+
Returns:
|
|
1250
|
+
Union[pd.DataFrame, Tuple[TradeDeal], None]:
|
|
1251
|
+
- `TradeDeal` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
|
|
1252
|
+
- `None` in case of an error.
|
|
1253
|
+
|
|
1254
|
+
Notes:
|
|
1255
|
+
The method allows receiving all history orders within a specified period.
|
|
1256
|
+
|
|
1257
|
+
The `group` parameter may contain several comma-separated conditions.
|
|
1258
|
+
|
|
1259
|
+
A condition can be set as a mask using '*'.
|
|
1260
|
+
|
|
1261
|
+
The logical negation symbol '!' can be used for exclusion.
|
|
1262
|
+
|
|
1263
|
+
All conditions are applied sequentially, which means conditions for inclusion
|
|
1264
|
+
in a group should be specified first, followed by an exclusion condition.
|
|
1265
|
+
|
|
1266
|
+
For example, `group="*, !EUR"` means that deals for all symbols should be selected first
|
|
1267
|
+
and those containing "EUR" in symbol names should be excluded afterward.
|
|
1268
|
+
|
|
1269
|
+
Example:
|
|
1270
|
+
>>> # Get the number of deals in history
|
|
1271
|
+
>>> from datetime import datetime
|
|
1272
|
+
>>> from_date = datetime(2020, 1, 1)
|
|
1273
|
+
>>> to_date = datetime.now()
|
|
1274
|
+
>>> account = Account()
|
|
1275
|
+
>>> history = account.get_trades_history(from_date, to_date)
|
|
1276
|
+
"""
|
|
1277
|
+
|
|
1278
|
+
if date_to is None:
|
|
1279
|
+
date_to = datetime.now()
|
|
1280
|
+
|
|
1281
|
+
if (ticket is not None) + (group is not None) + (position is not None) > 1:
|
|
1282
|
+
raise ValueError(
|
|
1283
|
+
"Only one of 'position', 'group' or 'ticket' can be specified as filter or None of them ."
|
|
1284
|
+
)
|
|
1285
|
+
if group is not None:
|
|
1286
|
+
position_deals = mt5.history_deals_get(date_from, date_to, group=group)
|
|
1287
|
+
elif ticket is not None:
|
|
1288
|
+
position_deals = mt5.history_deals_get(ticket=ticket)
|
|
1289
|
+
elif position is not None:
|
|
1290
|
+
position_deals = mt5.history_deals_get(position=position)
|
|
1291
|
+
else:
|
|
1292
|
+
position_deals = mt5.history_deals_get(date_from, date_to)
|
|
1293
|
+
|
|
1294
|
+
if position_deals is None or len(position_deals) == 0:
|
|
1295
|
+
return None
|
|
1296
|
+
|
|
1297
|
+
df = pd.DataFrame(list(position_deals), columns=position_deals[0]._asdict())
|
|
1298
|
+
df["time"] = pd.to_datetime(df["time"], unit="s")
|
|
1299
|
+
df.drop(["time_msc", "external_id"], axis=1, inplace=True)
|
|
1300
|
+
df.set_index("time", inplace=True)
|
|
1301
|
+
if save:
|
|
1302
|
+
file = "trade_history.csv"
|
|
1303
|
+
df.to_csv(file)
|
|
1304
|
+
if to_df:
|
|
1305
|
+
return df
|
|
1306
|
+
else:
|
|
1307
|
+
position_deals = [TradeDeal(**td._asdict()) for td in position_deals]
|
|
1308
|
+
return tuple(position_deals)
|
|
1309
|
+
|
|
1310
|
+
def get_orders(
|
|
1311
|
+
self,
|
|
1312
|
+
symbol: Optional[str] = None,
|
|
1313
|
+
group: Optional[str] = None,
|
|
1314
|
+
ticket: Optional[int] = None,
|
|
1315
|
+
to_df: bool = False,
|
|
1316
|
+
) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
|
|
1317
|
+
"""
|
|
1318
|
+
Get active orders with the ability to filter by symbol or ticket.
|
|
1319
|
+
There are four call options:
|
|
1320
|
+
|
|
1321
|
+
- Call without parameters. Returns open positions for all symbols.
|
|
1322
|
+
- Call specifying a symbol, open positions should be received for.
|
|
1323
|
+
- Call specifying a group of symbols, open positions should be received for.
|
|
1324
|
+
- Call specifying a position ticket.
|
|
1325
|
+
|
|
1326
|
+
Args:
|
|
1327
|
+
symbol (Optional[str]): Symbol name. Optional named parameter.
|
|
1328
|
+
If a symbol is specified, the ticket parameter is ignored.
|
|
1329
|
+
|
|
1330
|
+
group (Optional[str]): The filter for arranging a group of necessary symbols.
|
|
1331
|
+
Optional named parameter. If the group is specified,
|
|
1332
|
+
the function returns only positions meeting a specified criteria
|
|
1333
|
+
for a symbol name.
|
|
1334
|
+
|
|
1335
|
+
ticket (Optional[int]): Order ticket. Optional named parameter.
|
|
1336
|
+
Unique number assigned to each order.
|
|
1337
|
+
|
|
1338
|
+
to_df (bool): If True, a DataFrame is returned.
|
|
1339
|
+
|
|
1340
|
+
Returns:
|
|
1341
|
+
Union[pd.DataFrame, Tuple[TradeOrder], None]:
|
|
1342
|
+
- `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
|
|
1343
|
+
- `None` in case of an error.
|
|
1344
|
+
|
|
1345
|
+
Notes:
|
|
1346
|
+
The method allows receiving all history orders within a specified period.
|
|
1347
|
+
The `group` parameter may contain several comma-separated conditions.
|
|
1348
|
+
A condition can be set as a mask using '*'.
|
|
1349
|
+
|
|
1350
|
+
The logical negation symbol '!' can be used for exclusion.
|
|
1351
|
+
All conditions are applied sequentially, which means conditions for inclusion
|
|
1352
|
+
in a group should be specified first, followed by an exclusion condition.
|
|
1353
|
+
|
|
1354
|
+
For example, `group="*, !EUR"` means that deals for all symbols should be selected first
|
|
1355
|
+
and the ones containing "EUR" in symbol names should be excluded afterward.
|
|
1356
|
+
"""
|
|
1357
|
+
|
|
1358
|
+
if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
|
|
1359
|
+
raise ValueError(
|
|
1360
|
+
"Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
if symbol is not None:
|
|
1364
|
+
orders = mt5.orders_get(symbol=symbol)
|
|
1365
|
+
elif group is not None:
|
|
1366
|
+
orders = mt5.orders_get(group=group)
|
|
1367
|
+
elif ticket is not None:
|
|
1368
|
+
orders = mt5.orders_get(ticket=ticket)
|
|
1369
|
+
else:
|
|
1370
|
+
orders = mt5.orders_get()
|
|
1371
|
+
|
|
1372
|
+
if orders is None or len(orders) == 0:
|
|
1373
|
+
return None
|
|
1374
|
+
|
|
1375
|
+
if to_df:
|
|
1376
|
+
df = pd.DataFrame(list(orders), columns=orders[0]._asdict())
|
|
1377
|
+
df.drop(
|
|
1378
|
+
[
|
|
1379
|
+
"time_expiration",
|
|
1380
|
+
"type_time",
|
|
1381
|
+
"state",
|
|
1382
|
+
"position_by_id",
|
|
1383
|
+
"reason",
|
|
1384
|
+
"volume_current",
|
|
1385
|
+
"price_stoplimit",
|
|
1386
|
+
"sl",
|
|
1387
|
+
"tp",
|
|
1388
|
+
],
|
|
1389
|
+
axis=1,
|
|
1390
|
+
inplace=True,
|
|
1391
|
+
)
|
|
1392
|
+
df["time_setup"] = pd.to_datetime(df["time_setup"], unit="s")
|
|
1393
|
+
df["time_done"] = pd.to_datetime(df["time_done"], unit="s")
|
|
1394
|
+
return df
|
|
1395
|
+
else:
|
|
1396
|
+
trade_orders = [TradeOrder(**o._asdict()) for o in orders]
|
|
1397
|
+
return tuple(trade_orders)
|
|
1398
|
+
|
|
1399
|
+
def get_orders_history(
|
|
1400
|
+
self,
|
|
1401
|
+
date_from: datetime = datetime(2000, 1, 1),
|
|
1402
|
+
date_to: Optional[datetime] = None,
|
|
1403
|
+
group: Optional[str] = None,
|
|
1404
|
+
ticket: Optional[int] = None, # order ticket
|
|
1405
|
+
position: Optional[int] = None, # position ticket
|
|
1406
|
+
to_df: bool = True,
|
|
1407
|
+
save: bool = False,
|
|
1408
|
+
) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
|
|
1409
|
+
"""
|
|
1410
|
+
Get orders from trading history within the specified interval
|
|
1411
|
+
with the ability to filter by `ticket` or `position`.
|
|
1412
|
+
|
|
1413
|
+
You can call this method in the following ways:
|
|
1414
|
+
|
|
1415
|
+
- Call with a `time interval`. Returns all deals falling within the specified interval.
|
|
1416
|
+
|
|
1417
|
+
- Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.
|
|
1418
|
+
|
|
1419
|
+
- Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.
|
|
1420
|
+
|
|
1421
|
+
Args:
|
|
1422
|
+
date_from (datetime): Date the bars are requested from.
|
|
1423
|
+
Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
|
|
1424
|
+
Bars with the open time >= `date_from` are returned. Required unnamed parameter.
|
|
1425
|
+
|
|
1426
|
+
date_to (Optional[datetime]): Same as `date_from`.
|
|
1427
|
+
|
|
1428
|
+
group (Optional[str]): The filter for arranging a group of necessary symbols.
|
|
1429
|
+
Optional named parameter. If the group is specified,
|
|
1430
|
+
the function returns only positions meeting specified criteria
|
|
1431
|
+
for a symbol name.
|
|
1432
|
+
|
|
1433
|
+
ticket (Optional[int]): Order ticket to filter results. Optional parameter.
|
|
1434
|
+
If not specified, the filter is not applied.
|
|
1435
|
+
|
|
1436
|
+
position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) to filter results.
|
|
1437
|
+
Optional parameter. If not specified, the filter is not applied.
|
|
1438
|
+
|
|
1439
|
+
to_df (bool): If True, a DataFrame is returned.
|
|
1440
|
+
|
|
1441
|
+
save (bool): If True, a CSV file will be created to save the history.
|
|
1442
|
+
|
|
1443
|
+
Returns:
|
|
1444
|
+
Union[pd.DataFrame, Tuple[TradeOrder], None]
|
|
1445
|
+
- `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
|
|
1446
|
+
- `None` in case of an error.
|
|
1447
|
+
|
|
1448
|
+
Notes:
|
|
1449
|
+
The method allows receiving all history orders within a specified period.
|
|
1450
|
+
|
|
1451
|
+
The `group` parameter may contain several comma-separated conditions.
|
|
1452
|
+
|
|
1453
|
+
A condition can be set as a mask using '*'.
|
|
1454
|
+
|
|
1455
|
+
The logical negation symbol '!' can be used for exclusion.
|
|
1456
|
+
|
|
1457
|
+
All conditions are applied sequentially, which means conditions for inclusion
|
|
1458
|
+
in a group should be specified first, followed by an exclusion condition.
|
|
1459
|
+
|
|
1460
|
+
For example, `group="*, !EUR"` means that deals for all symbols should be selected first
|
|
1461
|
+
and those containing "EUR" in symbol names should be excluded afterward.
|
|
1462
|
+
|
|
1463
|
+
Example:
|
|
1464
|
+
>>> # Get the number of deals in history
|
|
1465
|
+
>>> from datetime import datetime
|
|
1466
|
+
>>> from_date = datetime(2020, 1, 1)
|
|
1467
|
+
>>> to_date = datetime.now()
|
|
1468
|
+
>>> account = Account()
|
|
1469
|
+
>>> history = account.get_orders_history(from_date, to_date)
|
|
1470
|
+
"""
|
|
1471
|
+
if date_to is None:
|
|
1472
|
+
date_to = datetime.now()
|
|
1473
|
+
|
|
1474
|
+
if (group is not None) + (ticket is not None) + (position is not None) > 1:
|
|
1475
|
+
raise ValueError(
|
|
1476
|
+
"Only one of 'position', 'group' or 'ticket' can be specified or None of them as filter."
|
|
1477
|
+
)
|
|
1478
|
+
if group is not None:
|
|
1479
|
+
history_orders = mt5.history_orders_get(date_from, date_to, group=group)
|
|
1480
|
+
elif ticket is not None:
|
|
1481
|
+
history_orders = mt5.history_orders_get(ticket=ticket)
|
|
1482
|
+
elif position is not None:
|
|
1483
|
+
history_orders = mt5.history_orders_get(position=position)
|
|
1484
|
+
else:
|
|
1485
|
+
history_orders = mt5.history_orders_get(date_from, date_to)
|
|
1486
|
+
|
|
1487
|
+
if history_orders is None or len(history_orders) == 0:
|
|
1488
|
+
return None
|
|
1489
|
+
|
|
1490
|
+
df = pd.DataFrame(list(history_orders), columns=history_orders[0]._asdict())
|
|
1491
|
+
df.drop(
|
|
1492
|
+
[
|
|
1493
|
+
"time_expiration",
|
|
1494
|
+
"type_time",
|
|
1495
|
+
"state",
|
|
1496
|
+
"position_by_id",
|
|
1497
|
+
"reason",
|
|
1498
|
+
"volume_current",
|
|
1499
|
+
"price_stoplimit",
|
|
1500
|
+
"sl",
|
|
1501
|
+
"tp",
|
|
1502
|
+
],
|
|
1503
|
+
axis=1,
|
|
1504
|
+
inplace=True,
|
|
1505
|
+
)
|
|
1506
|
+
df["time_setup"] = pd.to_datetime(df["time_setup"], unit="s")
|
|
1507
|
+
df["time_done"] = pd.to_datetime(df["time_done"], unit="s")
|
|
1508
|
+
|
|
1509
|
+
if save:
|
|
1510
|
+
file = "trade_history.csv"
|
|
1511
|
+
df.to_csv(file)
|
|
1512
|
+
if to_df:
|
|
1513
|
+
return df
|
|
1514
|
+
else:
|
|
1515
|
+
history_orders = [TradeOrder(**td._asdict()) for td in history_orders]
|
|
1516
|
+
return tuple(history_orders)
|