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.

Files changed (35) hide show
  1. bbstrader/__ini__.py +20 -20
  2. bbstrader/__main__.py +50 -50
  3. bbstrader/btengine/__init__.py +54 -54
  4. bbstrader/btengine/scripts.py +157 -157
  5. bbstrader/compat.py +19 -19
  6. bbstrader/config.py +137 -137
  7. bbstrader/core/data.py +22 -22
  8. bbstrader/core/utils.py +146 -146
  9. bbstrader/metatrader/__init__.py +6 -6
  10. bbstrader/metatrader/account.py +1516 -1516
  11. bbstrader/metatrader/copier.py +750 -745
  12. bbstrader/metatrader/rates.py +584 -584
  13. bbstrader/metatrader/risk.py +749 -748
  14. bbstrader/metatrader/scripts.py +81 -81
  15. bbstrader/metatrader/trade.py +1836 -1836
  16. bbstrader/metatrader/utils.py +645 -645
  17. bbstrader/models/__init__.py +10 -10
  18. bbstrader/models/factors.py +312 -312
  19. bbstrader/models/ml.py +1272 -1272
  20. bbstrader/models/optimization.py +182 -182
  21. bbstrader/models/portfolio.py +223 -223
  22. bbstrader/models/risk.py +398 -398
  23. bbstrader/trading/__init__.py +11 -11
  24. bbstrader/trading/execution.py +846 -846
  25. bbstrader/trading/script.py +155 -155
  26. bbstrader/trading/scripts.py +69 -69
  27. bbstrader/trading/strategies.py +860 -860
  28. bbstrader/tseries.py +1842 -1842
  29. {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/LICENSE +21 -21
  30. {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/METADATA +188 -187
  31. bbstrader-0.2.95.dist-info/RECORD +44 -0
  32. bbstrader-0.2.93.dist-info/RECORD +0 -44
  33. {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/WHEEL +0 -0
  34. {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/entry_points.txt +0 -0
  35. {bbstrader-0.2.93.dist-info → bbstrader-0.2.95.dist-info}/top_level.txt +0 -0
@@ -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)