bbstrader 0.2.4__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.

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