bbstrader 0.0.1__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,1038 @@
1
+ import re
2
+ import os
3
+ import pandas as pd
4
+ import urllib.request
5
+ from datetime import datetime
6
+ import MetaTrader5 as mt5
7
+ from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
8
+ from bbstrader.metatrader.utils import (
9
+ raise_mt5_error, AccountInfo, TerminalInfo,
10
+ SymbolInfo, TickInfo, TradeRequest, OrderCheckResult,
11
+ OrderSentResult, TradePosition, TradeOrder, TradeDeal,
12
+ )
13
+ from typing import Tuple, Union, List, Dict, Any, Optional, Literal
14
+
15
+
16
+ __BROKERS__ = {
17
+ 'AMG': "Admirals Group AS",
18
+ 'JGM': "Just Global Markets Ltd.",
19
+ 'FTMO': "FTMO S.R.O."
20
+ }
21
+ _ADMIRAL_MARKETS_URL_ = "https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets"
22
+ _ADMIRAL_MARKETS_PRODUCTS_ = ["Stocks", "ETFs",
23
+ "Indices", "Commodities", "Futures", "Forex"]
24
+ _JUST_MARKETS_URL_ = "https://one.justmarkets.link/a/tufvj0xugm/registration/trader"
25
+ _JUST_MARKETS_PRODUCTS_ = ["Stocks", "Crypto", "indices", "Commodities", "Forex"]
26
+ _FTMO_URL_ = "https://trader.ftmo.com/?affiliates=JGmeuQqepAZLMcdOEQRp"
27
+
28
+ INIT_MSG = (
29
+ f"\n* Ensure you have a good and stable internet connexion\n"
30
+ f"* Ensure you have an activete MT5 terminal install on your machine\n"
31
+ f"* Ensure you have an active MT5 Account with {'or '.join(__BROKERS__.values())}\n"
32
+ f"* If you want to trade {', '.join(_ADMIRAL_MARKETS_PRODUCTS_)}, See {_ADMIRAL_MARKETS_URL_}\n"
33
+ f"* If you want to trade {', '.join(_JUST_MARKETS_PRODUCTS_)}, See {_JUST_MARKETS_URL_}\n"
34
+ f"* If you are looking for a prop firm, See {_FTMO_URL_}\n"
35
+ )
36
+
37
+ amg_url = _ADMIRAL_MARKETS_URL_
38
+ jgm_url = _JUST_MARKETS_URL_
39
+ ftmo_url = _FTMO_URL_
40
+
41
+ _SYMBOLS_TYPE_ = {
42
+ "STK": r'\b(Stocks?|Equities?|Shares?)\b',
43
+ "ETF": r'\b(ETFs?)\b',
44
+ "IDX": r'\b(?:Indices?|Cash)\b(?!.*\\(?:UKOIL|USOIL))',
45
+ "FX": r'\b(Forex|Exotics?)\b',
46
+ "COMD": r'\b(Metals?|Agricultures?|Energies?|OIL|USOIL|UKOIL)\b',
47
+ "FUT": r'\b(Futures?)\b',
48
+ "CRYPTO": r'\b(Cryptos?)\b'
49
+ }
50
+
51
+ class Account(object):
52
+ """
53
+ The `Account` class is utilized to retrieve information about
54
+ the current trading account or a specific account.
55
+ It enables interaction with the MT5 terminal to manage account details,
56
+ including account informations, terminal status, financial instrument details,
57
+ active orders, open positions, and trading history.
58
+
59
+ Example:
60
+ >>> # Instantiating the Account class
61
+ >>> account = Account()
62
+
63
+ >>> # Getting account information
64
+ >>> account_info = account.get_account_info()
65
+
66
+ >>> # Printing account information
67
+ >>> account.print_account_info()
68
+
69
+ >>> # Getting terminal information
70
+ >>> terminal_info = account.get_terminal_info()
71
+
72
+ >>> # Retrieving and printing symbol information
73
+ >>> symbol_info = account.show_symbol_info('EURUSD')
74
+
75
+ >>> # Getting active orders
76
+ >>> orders = account.get_orders()
77
+
78
+ >>> # Fetching open positions
79
+ >>> positions = account.get_positions()
80
+
81
+ >>> # Accessing trade history
82
+ >>> from_date = datetime(2020, 1, 1)
83
+ >>> to_date = datetime.now()
84
+ >>> trade_history = account.get_trade_history(from_date, to_date)
85
+ """
86
+
87
+ def __init__(self):
88
+ if not mt5.initialize():
89
+ raise_mt5_error(message=INIT_MSG)
90
+ self._check_brokers()
91
+
92
+ def _check_brokers(self):
93
+ supported = __BROKERS__.copy()
94
+ broker = self.get_terminal_info().company
95
+ if broker not in supported.values():
96
+ raise ValueError(
97
+ f"{broker} is not currently supported broker for the Account() class\n"
98
+ f"Currently Supported brokers are: {', '.join(supported.values())}\n"
99
+ f"For {supported['AMG']}, See {amg_url}\n"
100
+ f"For {supported['JGM']}, See {jgm_url}\n"
101
+ f"For {supported['FTMO']}, See {ftmo_url}\n"
102
+ )
103
+
104
+ def get_account_info(
105
+ self,
106
+ account: Optional[int] = None,
107
+ password: Optional[str] = None,
108
+ server: Optional[str] = None,
109
+ timeout: Optional[int] = 60_000
110
+ ) -> Union[AccountInfo, None]:
111
+ """
112
+ Get info on the current trading account or a specific account .
113
+
114
+ Args:
115
+ account (int, optinal) : MT5 Trading account number.
116
+ password (str, optinal): MT5 Trading account password.
117
+
118
+ server (str, optinal): MT5 Trading account server
119
+ [Brokers or terminal server ["demo", "real"]]
120
+ If no server is set, the last used server is applied automaticall
121
+
122
+ timeout (int, optinal):
123
+ Connection timeout in milliseconds. Optional named parameter.
124
+ If not specified, the value of 60 000 (60 seconds) is applied.
125
+ If the connection is not established within the specified time,
126
+ the call is forcibly terminated and the exception is generated.
127
+
128
+ Returns:
129
+ - AccountInfo in the form of a Namedtuple structure.
130
+ - None in case of an error
131
+
132
+ Raises:
133
+ MT5TerminalError: A specific exception based on the error code.
134
+ """
135
+ # connect to the trade account specifying a password and a server
136
+ if (
137
+ account is not None and
138
+ password is not None and
139
+ server is not None
140
+ ):
141
+ try:
142
+ authorized = mt5.login(
143
+ account, password=password, server=server, timeout=timeout)
144
+ if not authorized:
145
+ raise_mt5_error(
146
+ message=f"Failed to connect to account #{account}")
147
+ else:
148
+ info = mt5.account_info()
149
+ if info is None:
150
+ return None
151
+ else:
152
+ return AccountInfo(**info._asdict())
153
+ except Exception as e:
154
+ raise_mt5_error(e)
155
+ else:
156
+ try:
157
+ info = mt5.account_info()
158
+ if info is None:
159
+ return None
160
+ else:
161
+ return AccountInfo(**info._asdict())
162
+ except Exception as e:
163
+ raise_mt5_error(e)
164
+
165
+ def _show_info(self, info_getter, info_name, symbol=None):
166
+ """
167
+ Generic function to retrieve and print information.
168
+
169
+ Args:
170
+ info_getter (callable): Function to retrieve the information.
171
+ info_name (str): Name of the information being retrieved.
172
+ symbol (str, optional): Symbol name, required for some info types.
173
+ Defaults to None.
174
+
175
+ Raises:
176
+ MT5TerminalError: A specific exception based on the error code.
177
+ """
178
+
179
+ # Call the provided info retrieval function
180
+ if symbol is not None:
181
+ info = info_getter(symbol)
182
+ else:
183
+ info = info_getter()
184
+
185
+ if info is not None:
186
+ info_dict = info._asdict()
187
+ df = pd.DataFrame(list(info_dict.items()),
188
+ columns=['PROPERTY', 'VALUE'])
189
+
190
+ # Construct the print message based on whether a symbol is provided
191
+ if symbol:
192
+ print(
193
+ f"\n{info_name.upper()} INFO FOR {symbol} ({info.description})")
194
+ else:
195
+ print(f"\n{info_name.upper()} INFORMATIONS:")
196
+
197
+ pd.set_option('display.max_rows', None)
198
+ pd.set_option('display.max_columns', None)
199
+ print(df.to_string())
200
+ else:
201
+ if symbol:
202
+ msg = self._symbol_info_msg(symbol)
203
+ raise_mt5_error(message=msg)
204
+ else:
205
+ raise_mt5_error()
206
+
207
+ def show_account_info(self):
208
+ """ Helper function to print account info"""
209
+ self._show_info(self.get_account_info, "account")
210
+
211
+ def get_terminal_info(self, show=False) -> Union[TerminalInfo, None]:
212
+ """
213
+ Get the connected MetaTrader 5 client terminal status and settings.
214
+
215
+ Args:
216
+ show (bool): If True the Account information will be printed
217
+
218
+ Returns:
219
+ - TerminalInfo in the form of NamedTuple Structure.
220
+ - None in case of an error
221
+
222
+ Raises:
223
+ MT5TerminalError: A specific exception based on the error code.
224
+ """
225
+ try:
226
+ terminal_info = mt5.terminal_info()
227
+ if terminal_info is None:
228
+ return None
229
+ except Exception as e:
230
+ raise_mt5_error(e)
231
+
232
+ terminal_info_dict = terminal_info._asdict()
233
+ # convert the dictionary into DataFrame and print
234
+ df = pd.DataFrame(list(terminal_info_dict.items()),
235
+ columns=['PROPERTY', 'VALUE'])
236
+ if show:
237
+ pd.set_option('display.max_rows', None)
238
+ pd.set_option('display.max_columns', None)
239
+ print(df.to_string())
240
+ return TerminalInfo(**terminal_info_dict)
241
+
242
+ def convert_currencies(self, qty: float, from_c: str, to_c: str) -> float:
243
+ """Convert amount from a currency to another one.
244
+
245
+ Args:
246
+ qty (float): The amount of `currency` to convert.
247
+ from_c (str): The currency to convert from.
248
+ to_c (str): The currency to convert to.
249
+
250
+ Returns:
251
+ - The value of `qty` in converted in `to_c`.
252
+
253
+ Notes:
254
+ If `from_c` or `to_co` are not supported, the `qty` will be return;
255
+ check "https://www.ecb.europa.eu/stats/eurofxref/eurofxref.zip"
256
+ for supported currencies or you can take a look at the `CurrencyConverter` project
257
+ on Github https://github.com/alexprengere/currencyconverter .
258
+ """
259
+ filename = f"ecb_{datetime.now():%Y%m%d}.zip"
260
+ if not os.path.isfile(filename):
261
+ urllib.request.urlretrieve(SINGLE_DAY_ECB_URL, filename)
262
+ c = CurrencyConverter(filename)
263
+ os.remove(filename)
264
+ supported = c.currencies
265
+ if (from_c not in supported or
266
+ to_c not in supported
267
+ ):
268
+ rate = qty
269
+ else:
270
+ rate = c.convert(amount=qty, currency=from_c, new_currency=to_c)
271
+ return rate
272
+
273
+ def get_currency_rates(self, symbol: str) -> Dict[str, str]:
274
+ """
275
+ Args:
276
+ symbol (str): The symbol for which to get currencies
277
+
278
+ Returns:
279
+ - `base currency` (bc)
280
+ - `margin currency` (mc)
281
+ - `profit currency` (pc)
282
+ - `account currency` (ac)
283
+
284
+ Exemple:
285
+ >>> account = Account()
286
+ >>> account.get_rates('EURUSD')
287
+ {'bc': 'EUR', 'mc': 'EUR', 'pc': 'USD', 'ac': 'USD'}
288
+ """
289
+ info = self.get_symbol_info(symbol)
290
+ bc = info.currency_base
291
+ pc = info.currency_profit
292
+ mc = info.currency_margin
293
+ ac = self.get_account_info().currency
294
+ return {'bc': bc, 'mc': mc, 'pc': pc, 'ac': ac}
295
+
296
+ def get_symbols(self,
297
+ symbol_type="ALL",
298
+ check_etf=False,
299
+ save=False,
300
+ file_name="symbols",
301
+ include_desc=False,
302
+ display_total=False
303
+ ) -> List[str]:
304
+ """
305
+ Get all specified financial instruments from the MetaTrader 5 terminal.
306
+
307
+ Args:
308
+ symbol_type (str) The category of instrument to get
309
+ - `ALL`: For all available symbols
310
+ - `STK`: Stocks (e.g., 'GOOGL')
311
+ - `ETF`: ETFs (e.g., 'QQQ')
312
+ - `IDX`: Indices (e.g., 'SP500')
313
+ - `FX`: Forex pairs (e.g., 'EURUSD')
314
+ - `COMD`: Commodities (e.g., 'CRUDOIL', 'GOLD')
315
+ - `FUT`: Futures (e.g., 'USTNote_U4'),
316
+ - `CRYPTO`: Cryptocurrencies (e.g., 'BTC', 'ETH')
317
+
318
+ check_etf (bool): If True and symbol_type is 'etf', check if the
319
+ ETF description contains 'ETF'.
320
+
321
+ save (bool): If True, save the symbols to a file.
322
+
323
+ file_name (str): The name of the file to save the symbols to
324
+ (without the extension).
325
+
326
+ include_desc (bool): If True, include the symbol's description
327
+ in the output and saved file.
328
+
329
+ Returns:
330
+ list: A list of symbols.
331
+
332
+ Raises:
333
+ Exception: If there is an error connecting to MT5 or retrieving symbols.
334
+ """
335
+ symbols = mt5.symbols_get()
336
+ if not symbols:
337
+ raise_mt5_error()
338
+
339
+ symbol_list = []
340
+ patterns = _SYMBOLS_TYPE_
341
+
342
+ if symbol_type != 'ALL':
343
+ if symbol_type not in patterns:
344
+ raise ValueError(f"Unsupported symbol type: {symbol_type}")
345
+
346
+ if save:
347
+ max_lengh = max([len(s.name) for s in symbols])
348
+ file_path = f"{file_name}.txt"
349
+ with open(file_path, mode='w', encoding='utf-8') as file:
350
+ for s in symbols:
351
+ info = self.get_symbol_info(s.name)
352
+ if symbol_type == 'ALL':
353
+ self._write_symbol(file, info, include_desc, max_lengh)
354
+ symbol_list.append(s.name)
355
+ else:
356
+ pattern = re.compile(patterns[symbol_type])
357
+ match = re.search(pattern, info.path)
358
+ if match:
359
+ if symbol_type == "ETF" and check_etf and "ETF" not in info.description:
360
+ raise ValueError(
361
+ f"{info.name} doesn't have 'ETF' in its description. "
362
+ "If this is intended, set check_etf=False."
363
+ )
364
+ self._write_symbol(
365
+ file, info, include_desc, max_lengh)
366
+ symbol_list.append(s.name)
367
+
368
+ else: # If not saving to a file, just process the symbols
369
+ for s in symbols:
370
+ info = self.get_symbol_info(s.name)
371
+ if symbol_type == 'ALL':
372
+ symbol_list.append(s.name)
373
+ else:
374
+ pattern = re.compile(
375
+ patterns[symbol_type]) # , re.IGNORECASE
376
+ match = re.search(pattern, info.path)
377
+ if match:
378
+ if symbol_type == "ETF" and check_etf and "ETF" not in info.description:
379
+ raise ValueError(
380
+ f"{info.name} doesn't have 'ETF' in its description. "
381
+ "If this is intended, set check_etf=False."
382
+ )
383
+ symbol_list.append(s.name)
384
+
385
+ # Print a summary of the retrieved symbols
386
+ if display_total:
387
+ names = {
388
+ "ALL": 'Symbols',
389
+ "STK": 'Stocks',
390
+ "ETF": 'ETFs',
391
+ "IDX": 'Indices',
392
+ "FX": 'Forex Paires',
393
+ "COMD": 'Commodities',
394
+ "FUT": 'Futures',
395
+ "CRYPTO": 'Cryptos Assets'
396
+ }
397
+ print(f"Total {names[symbol_type]}: {len(symbol_list)}")
398
+
399
+ return symbol_list
400
+
401
+ def _write_symbol(self, file, info, include_desc, max_lengh):
402
+ """Helper function to write symbol information to a file."""
403
+ if include_desc:
404
+ space = " "*int(max_lengh-len(info.name))
405
+ file.write(info.name + space + '|' +
406
+ info.description + '\n')
407
+ else:
408
+ file.write(info.name + '\n')
409
+
410
+ def get_symbol_type(
411
+ self,
412
+ symbol: str
413
+ ) -> Literal[
414
+ "STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "unknown"]:
415
+ """
416
+ Determines the type of a given financial instrument symbol.
417
+
418
+ Args:
419
+ symbol (str): The symbol of the financial instrument (e.g., `GOOGL`, `EURUSD`).
420
+
421
+ Returns:
422
+ Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "unknown"]:
423
+ The type of the financial instrument, one of the following:
424
+
425
+ - `STK`: For Stocks (e.g., `GOOGL`)
426
+ - `ETF`: For ETFs (e.g., `QQQ`)
427
+ - `IDX`: For Indices (e.g., `SP500`)
428
+ - `FX` : For Forex pairs (e.g., `EURUSD`)
429
+ - `COMD`: For Commodities (e.g., `CRUDOIL`, `GOLD`)
430
+ - `FUT` : For Futures (e.g., `USTNote_U4`)
431
+ - `CRYPTO`: For Cryptocurrencies (e.g., `BTC`, `ETH`)
432
+
433
+ Returns `unknown` if the type cannot be determined.
434
+ """
435
+
436
+ patterns = _SYMBOLS_TYPE_
437
+ info = self.get_symbol_info(symbol)
438
+ if info is not None:
439
+ for symbol_type, pattern in patterns.items():
440
+ match = re.search(pattern, info.path) # , re.IGNORECASE
441
+ if match:
442
+ return symbol_type
443
+ return "unknown"
444
+
445
+ def _get_symbols_by_category(self, symbol_type, category, category_map):
446
+ if category not in category_map:
447
+ raise ValueError(
448
+ f"Unsupported category: {category}. Choose from: {', '.join(category_map)}"
449
+ )
450
+
451
+ symbols = self.get_symbols(symbol_type=symbol_type)
452
+ pattern = re.compile(category_map[category], re.IGNORECASE)
453
+
454
+ symbol_list = []
455
+ for s in symbols:
456
+ info = self.get_symbol_info(s)
457
+ match = re.search(pattern, info.path)
458
+ if match:
459
+ symbol_list.append(s)
460
+ return symbol_list
461
+
462
+ def get_fx_symbols(
463
+ self,
464
+ category: Literal["majors", "minors", "exotics"] = 'majors'
465
+ ) -> List[str]:
466
+ """
467
+ Retrieves a list of forex symbols belonging to a specific category.
468
+
469
+ Args:
470
+ category (str, optional): The category of forex symbols to retrieve.
471
+ Possible values are 'majors', 'minors', 'exotics'.
472
+ Defaults to 'majors'.
473
+
474
+ Returns:
475
+ list: A list of forex symbol names matching the specified category.
476
+
477
+ Raises:
478
+ ValueError: If an unsupported category is provided.
479
+
480
+ Notes:
481
+ This mthods works primarly with Admirals Group AS products,
482
+ For other brokers use `get_symbols()`
483
+ """
484
+ fx_categories = {
485
+ "majors": r"\b(Majors?)\b",
486
+ "minors": r"\b(Minors?)\b",
487
+ "exotics": r"\b(Exotics?)\b",
488
+ }
489
+ return self._get_symbols_by_category('forex', category, fx_categories)
490
+
491
+ def get_stocks_from(self, country_code: str = 'USA') -> List[str]:
492
+ """
493
+ Retrieves a list of stock symbols from a specific country.
494
+
495
+ Supported countries are:
496
+ * **Australia:** AUS
497
+ * **Belgium:** BEL
498
+ * **Denmark:** DNK
499
+ * **Finland:** FIN
500
+ * **France:** FRA
501
+ * **Germany:** DEU
502
+ * **Netherlands:** NLD
503
+ * **Norway:** NOR
504
+ * **Portugal:** PRT
505
+ * **Spain:** ESP
506
+ * **Sweden:** SWE
507
+ * **United Kingdom:** GBR
508
+ * **United States:** USA
509
+ * **Switzerland:** CHE
510
+
511
+ Args:
512
+ country (str, optional): The country code of stocks to retrieve.
513
+ Defaults to 'USA'.
514
+
515
+ Returns:
516
+ list: A list of stock symbol names from the specified country.
517
+
518
+ Raises:
519
+ ValueError: If an unsupported country is provided.
520
+
521
+ Notes:
522
+ This mthods works primarly with Admirals Group AS products,
523
+ For other brokers use `get_symbols()`
524
+ """
525
+ country_map = {
526
+ "USA": r"\b(US)\b",
527
+ "AUS": r"\b(Australia)\b",
528
+ "BEL": r"\b(Belgium)\b",
529
+ "DNK": r"\b(Denmark)\b",
530
+ "FIN": r"\b(Finland)\b",
531
+ "FRA": r"\b(France)\b",
532
+ "DEU": r"\b(Germany)\b",
533
+ "NLD": r"\b(Netherlands)\b",
534
+ "NOR": r"\b(Norway)\b",
535
+ "PRT": r"\b(Portugal)\b",
536
+ "ESP": r"\b(Spain)\b",
537
+ "SWE": r"\b(Sweden)\b",
538
+ "GBR": r"\b(UK)\b",
539
+ "CHE": r"\b(Switzerland)\b",
540
+ }
541
+ return self._get_symbols_by_category('stocks', country_code, country_map)
542
+
543
+ def get_symbol_info(self, symbol: str) -> Union[SymbolInfo, None]:
544
+ """Get symbol properties
545
+
546
+ Args:
547
+ symbol (str): Symbol name
548
+
549
+ Returns:
550
+ - AccountInfo in the form of a NamedTuple().
551
+ - None in case of an error.
552
+
553
+ Raises:
554
+ MT5TerminalError: A specific exception based on the error code.
555
+ """
556
+ try:
557
+ symbol_info = mt5.symbol_info(symbol)
558
+ if symbol_info is None:
559
+ return None
560
+ else:
561
+ return SymbolInfo(**symbol_info._asdict())
562
+ except Exception as e:
563
+ msg = self._symbol_info_msg(symbol)
564
+ raise_mt5_error(message=f"{e+msg}")
565
+
566
+ def show_symbol_info(self, symbol: str):
567
+ """
568
+ Print symbol properties
569
+
570
+ Args:
571
+ symbol (str): Symbol name
572
+ """
573
+ self._show_info(self.get_symbol_info, "symbol", symbol=symbol)
574
+
575
+ def _symbol_info_msg(self, symbol):
576
+ return (
577
+ f"No history found for {symbol} in Market Watch.\n"
578
+ f"* Ensure {symbol} is selected and displayed in the Market Watch window.\n"
579
+ f"* See https://www.metatrader5.com/en/terminal/help/trading/market_watch\n"
580
+ f"* Ensure the symbol name is correct.\n"
581
+ )
582
+
583
+ def get_tick_info(self, symbol: str) -> Union[TickInfo, None]:
584
+ """Get symbol tick properties
585
+
586
+ Args:
587
+ symbol (str): Symbol name
588
+
589
+ Returns:
590
+ - AccountInfo in the form of a NamedTuple().
591
+ - None in case of an error.
592
+
593
+ Raises:
594
+ MT5TerminalError: A specific exception based on the error code.
595
+ """
596
+ try:
597
+ tick_info = mt5.symbol_info_tick(symbol)
598
+ if tick_info is None:
599
+ return None
600
+ else:
601
+ return TickInfo(**tick_info._asdict())
602
+ except Exception as e:
603
+ msg = self._symbol_info_msg(symbol)
604
+ raise_mt5_error(message=f"{e+msg}")
605
+
606
+ def show_tick_info(self, symbol: str):
607
+ """
608
+ Print Tick properties
609
+
610
+ Args:
611
+ symbol (str): Symbol name
612
+ """
613
+ self._show_info(self.get_tick_info, "tick", symbol=symbol)
614
+
615
+ def check_order(self,
616
+ request: Dict[str, Any]) -> OrderCheckResult:
617
+ """
618
+ Check funds sufficiency for performing a required trading operation.
619
+
620
+ Args:
621
+ request (Dict[str, Any]): `TradeRequest` type structure describing the required trading action.
622
+
623
+ Returns:
624
+ OrderCheckResult:
625
+ The check result as the `OrderCheckResult` structure.
626
+
627
+ The `request` field in the returned structure contains the trading request passed to `check_order()`.
628
+
629
+ Raises:
630
+ MT5TerminalError: Raised if there is an error in the trading terminal based on the error code.
631
+
632
+ Notes:
633
+ Successful submission of a request does not guarantee that the requested trading
634
+ operation will be executed successfully.
635
+ """
636
+
637
+ try:
638
+ result = mt5.order_check(request)
639
+ result_dict = result._asdict()
640
+ trade_request = TradeRequest(**result.request._asdict())
641
+ result_dict['request'] = trade_request
642
+ return OrderCheckResult(**result_dict)
643
+ except Exception as e:
644
+ raise_mt5_error(e)
645
+
646
+ def send_order(self,
647
+ request: Dict[str, Any]) -> OrderSentResult:
648
+ """
649
+ Send a request to perform a trading operation from the terminal to the trade server.
650
+
651
+ Args:
652
+ request (Dict[str, Any]): `TradeRequest` type structure describing the required trading action.
653
+
654
+ Returns:
655
+ OrderSentResult:
656
+ The execution result as the `OrderSentResult` structure.
657
+
658
+ The `request` field in the returned structure contains the trading request passed to `send_order()`.
659
+
660
+ Raises:
661
+ MT5TerminalError: Raised if there is an error in the trading terminal based on the error code.
662
+ """
663
+ try:
664
+ result = mt5.order_send(request)
665
+ result_dict = result._asdict()
666
+ trade_request = TradeRequest(**result.request._asdict())
667
+ result_dict['request'] = trade_request
668
+ return OrderSentResult(**result_dict)
669
+ except Exception as e:
670
+ raise_mt5_error(e)
671
+
672
+ def get_positions(self,
673
+ symbol: Optional[str] = None,
674
+ group: Optional[str] = None,
675
+ ticket: Optional[int] = None,
676
+ to_df: bool = False
677
+ ) -> Union[pd.DataFrame, Tuple[TradePosition], None]:
678
+ """
679
+ Get open positions with the ability to filter by symbol or ticket.
680
+ There are four call options:
681
+
682
+ - Call without parameters. Returns open positions for all symbols.
683
+ - Call specifying a symbol. Returns open positions for the specified symbol.
684
+ - Call specifying a group of symbols. Returns open positions for the specified group of symbols.
685
+ - Call specifying a position ticket. Returns the position corresponding to the specified ticket.
686
+
687
+ Args:
688
+ symbol (Optional[str]): Symbol name. Optional named parameter.
689
+ If a symbol is specified, the `ticket` parameter is ignored.
690
+
691
+ group (Optional[str]): The filter for arranging a group of necessary symbols.
692
+ Optional named parameter. If the group is specified,
693
+ the function returns only positions meeting specified criteria
694
+ for a symbol name.
695
+
696
+ ticket (Optional[int]): Position ticket. Optional named parameter.
697
+ A unique number assigned to each newly opened position.
698
+ It usually matches the ticket of the order used to open the position,
699
+ except when the ticket is changed as a result of service operations on the server,
700
+ for example, when charging swaps with position re-opening.
701
+
702
+ to_df (bool): If True, a DataFrame is returned.
703
+
704
+ Returns:
705
+ Union[pd.DataFrame, Tuple[TradePosition], None]:
706
+ - `TradePosition` in the form of a named tuple structure (namedtuple) or pd.DataFrame.
707
+ - `None` in case of an error.
708
+
709
+ Notes:
710
+ The method allows receiving all open positions within a specified period.
711
+
712
+ The `group` parameter may contain several comma-separated conditions.
713
+
714
+ A condition can be set as a mask using '*'.
715
+
716
+ The logical negation symbol '!' can be used for exclusion.
717
+
718
+ All conditions are applied sequentially, which means conditions for inclusion
719
+ in a group should be specified first, followed by an exclusion condition.
720
+
721
+ For example, `group="*, !EUR"` means that deals for all symbols should be selected first,
722
+ and those containing "EUR" in symbol names should be excluded afterward.
723
+ """
724
+
725
+ if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
726
+ raise ValueError(
727
+ "Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them.")
728
+
729
+ if symbol is not None:
730
+ positions = mt5.positions_get(symbol=symbol)
731
+ elif group is not None:
732
+ positions = mt5.positions_get(group=group)
733
+ elif ticket is not None:
734
+ positions = mt5.positions_get(ticket=ticket)
735
+ else:
736
+ positions = mt5.positions_get()
737
+
738
+ if len(positions) == 0:
739
+ return None
740
+ if to_df:
741
+ df = pd.DataFrame(list(positions), columns=positions[0]._asdict())
742
+ df['time'] = pd.to_datetime(df['time'], unit='s')
743
+ df.drop(['time_update', 'time_msc', 'time_update_msc', 'external_id'],
744
+ axis=1, inplace=True)
745
+ return df
746
+ else:
747
+ trade_positions = [
748
+ TradePosition(**p._asdict()) for p in positions]
749
+ return tuple(trade_positions)
750
+
751
+ def get_trades_history(
752
+ self,
753
+ date_from: datetime = datetime(2000, 1, 1),
754
+ date_to: Optional[datetime] = None,
755
+ group: Optional[str] = None,
756
+ ticket: Optional[int] = None, # TradeDeal.ticket
757
+ position: Optional[int] = None, # TradePosition.ticket
758
+ to_df: bool = True,
759
+ save: bool = False
760
+ ) -> Union[pd.DataFrame, Tuple[TradeDeal], None]:
761
+ """
762
+ Get deals from trading history within the specified interval
763
+ with the ability to filter by `ticket` or `position`.
764
+
765
+ You can call this method in the following ways:
766
+
767
+ - Call with a `time interval`. Returns all deals falling within the specified interval.
768
+
769
+ - Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.
770
+
771
+ - Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.
772
+
773
+ Args:
774
+ date_from (datetime): Date the bars are requested from.
775
+ Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
776
+ Bars with the open time >= `date_from` are returned. Required unnamed parameter.
777
+
778
+ date_to (Optional[datetime]): Same as `date_from`.
779
+
780
+ group (Optional[str]): The filter for arranging a group of necessary symbols.
781
+ Optional named parameter. If the group is specified,
782
+ the function returns only positions meeting specified criteria
783
+ for a symbol name.
784
+
785
+ ticket (Optional[int]): Ticket of an order (stored in `DEAL_ORDER`) for which all deals should be received.
786
+ Optional parameter. If not specified, the filter is not applied.
787
+
788
+ position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) for which all deals should be received.
789
+ Optional parameter. If not specified, the filter is not applied.
790
+
791
+ to_df (bool): If True, a DataFrame is returned.
792
+
793
+ save (bool): If set to True, a CSV file will be created to save the history.
794
+
795
+ Returns:
796
+ Union[pd.DataFrame, Tuple[TradeDeal], None]:
797
+ - `TradeDeal` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
798
+ - `None` in case of an error.
799
+
800
+ Notes:
801
+ The method allows receiving all history orders within a specified period.
802
+
803
+ The `group` parameter may contain several comma-separated conditions.
804
+
805
+ A condition can be set as a mask using '*'.
806
+
807
+ The logical negation symbol '!' can be used for exclusion.
808
+
809
+ All conditions are applied sequentially, which means conditions for inclusion
810
+ in a group should be specified first, followed by an exclusion condition.
811
+
812
+ For example, `group="*, !EUR"` means that deals for all symbols should be selected first
813
+ and those containing "EUR" in symbol names should be excluded afterward.
814
+
815
+ Example:
816
+ >>> # Get the number of deals in history
817
+ >>> from datetime import datetime
818
+ >>> from_date = datetime(2020, 1, 1)
819
+ >>> to_date = datetime.now()
820
+ >>> account = Account()
821
+ >>> history = account.get_trades_history(from_date, to_date)
822
+ """
823
+
824
+ if date_to is None:
825
+ date_to = datetime.now()
826
+
827
+ if (ticket is not None) + (group is not None) + (position is not None) > 1:
828
+ raise ValueError(
829
+ "Only one of 'position', 'group' or 'ticket' can be specified as filter or None of them .")
830
+ if group is not None:
831
+ position_deals = mt5.history_deals_get(
832
+ date_from, date_to, group=group
833
+ )
834
+ elif ticket is not None:
835
+ position_deals = mt5.history_deals_get(ticket=ticket)
836
+ elif position is not None:
837
+ position_deals = mt5.history_deals_get(position=position)
838
+ else:
839
+ position_deals = mt5.history_deals_get(date_from, date_to)
840
+
841
+ if len(position_deals) == 0:
842
+ return None
843
+
844
+ df = pd.DataFrame(list(position_deals),
845
+ columns=position_deals[0]._asdict())
846
+ df['time'] = pd.to_datetime(df['time'], unit='s')
847
+ if save:
848
+ file = "trade_history.csv"
849
+ df.to_csv(file)
850
+ if to_df:
851
+ return df
852
+ else:
853
+ position_deals = [
854
+ TradeDeal(**td._asdict())for td in position_deals]
855
+ return tuple(position_deals)
856
+
857
+ def get_orders(self,
858
+ symbol: Optional[str] = None,
859
+ group: Optional[str] = None,
860
+ ticket: Optional[int] = None,
861
+ to_df: bool = False
862
+ ) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
863
+ """
864
+ Get active orders with the ability to filter by symbol or ticket.
865
+ There are four call options:
866
+
867
+ - Call without parameters. Returns open positions for all symbols.
868
+ - Call specifying a symbol, open positions should be received for.
869
+ - Call specifying a group of symbols, open positions should be received for.
870
+ - Call specifying a position ticket.
871
+
872
+ Args:
873
+ symbol (Optional[str]): Symbol name. Optional named parameter.
874
+ If a symbol is specified, the ticket parameter is ignored.
875
+
876
+ group (Optional[str]): The filter for arranging a group of necessary symbols.
877
+ Optional named parameter. If the group is specified,
878
+ the function returns only positions meeting a specified criteria
879
+ for a symbol name.
880
+
881
+ ticket (Optional[int]): Order ticket. Optional named parameter.
882
+ Unique number assigned to each order.
883
+
884
+ to_df (bool): If True, a DataFrame is returned.
885
+
886
+ Returns:
887
+ Union[pd.DataFrame, Tuple[TradeOrder], None]:
888
+ - `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
889
+ - `None` in case of an error.
890
+
891
+ Notes:
892
+ The method allows receiving all history orders within a specified period.
893
+ The `group` parameter may contain several comma-separated conditions.
894
+ A condition can be set as a mask using '*'.
895
+
896
+ The logical negation symbol '!' can be used for exclusion.
897
+ All conditions are applied sequentially, which means conditions for inclusion
898
+ in a group should be specified first, followed by an exclusion condition.
899
+
900
+ For example, `group="*, !EUR"` means that deals for all symbols should be selected first
901
+ and the ones containing "EUR" in symbol names should be excluded afterward.
902
+ """
903
+
904
+ if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
905
+ raise ValueError(
906
+ "Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them.")
907
+
908
+ if symbol is not None:
909
+ orders = mt5.orders_get(symbol=symbol)
910
+ elif group is not None:
911
+ orders = mt5.orders_get(group=group)
912
+ elif ticket is not None:
913
+ orders = mt5.orders_get(ticket=ticket)
914
+ else:
915
+ orders = mt5.orders_get()
916
+
917
+ if len(orders) == 0:
918
+ return None
919
+
920
+ if to_df:
921
+ df = pd.DataFrame(list(orders), columns=orders[0]._asdict())
922
+ df.drop(['time_expiration', 'type_time', 'state', 'position_by_id', 'reason',
923
+ 'volume_current', 'price_stoplimit', 'sl', 'tp'], axis=1, inplace=True)
924
+ df['time_setup'] = pd.to_datetime(df['time_setup'], unit='s')
925
+ df['time_done'] = pd.to_datetime(df['time_done'], unit='s')
926
+ return df
927
+ else:
928
+ trade_orders = [TradeOrder(**o._asdict()) for o in orders]
929
+ return tuple(trade_orders)
930
+
931
+ def get_orders_history(
932
+ self,
933
+ date_from: datetime = datetime(2000, 1, 1),
934
+ date_to: Optional[datetime] = None,
935
+ group: Optional[str] = None,
936
+ ticket: Optional[int] = None, # order ticket
937
+ position: Optional[int] = None, # position ticket
938
+ to_df: bool = True,
939
+ save: bool = False
940
+ ) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
941
+ """
942
+ Get orders from trading history within the specified interval
943
+ with the ability to filter by `ticket` or `position`.
944
+
945
+ You can call this method in the following ways:
946
+
947
+ - Call with a `time interval`. Returns all deals falling within the specified interval.
948
+
949
+ - Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.
950
+
951
+ - Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.
952
+
953
+ Args:
954
+ date_from (datetime): Date the bars are requested from.
955
+ Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
956
+ Bars with the open time >= `date_from` are returned. Required unnamed parameter.
957
+
958
+ date_to (Optional[datetime]): Same as `date_from`.
959
+
960
+ group (Optional[str]): The filter for arranging a group of necessary symbols.
961
+ Optional named parameter. If the group is specified,
962
+ the function returns only positions meeting specified criteria
963
+ for a symbol name.
964
+
965
+ ticket (Optional[int]): Order ticket to filter results. Optional parameter.
966
+ If not specified, the filter is not applied.
967
+
968
+ position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) to filter results.
969
+ Optional parameter. If not specified, the filter is not applied.
970
+
971
+ to_df (bool): If True, a DataFrame is returned.
972
+
973
+ save (bool): If True, a CSV file will be created to save the history.
974
+
975
+ Returns:
976
+ Union[pd.DataFrame, Tuple[TradeOrder], None]
977
+ - `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
978
+ - `None` in case of an error.
979
+
980
+ Notes:
981
+ The method allows receiving all history orders within a specified period.
982
+
983
+ The `group` parameter may contain several comma-separated conditions.
984
+
985
+ A condition can be set as a mask using '*'.
986
+
987
+ The logical negation symbol '!' can be used for exclusion.
988
+
989
+ All conditions are applied sequentially, which means conditions for inclusion
990
+ in a group should be specified first, followed by an exclusion condition.
991
+
992
+ For example, `group="*, !EUR"` means that deals for all symbols should be selected first
993
+ and those containing "EUR" in symbol names should be excluded afterward.
994
+
995
+ Example:
996
+ >>> # Get the number of deals in history
997
+ >>> from datetime import datetime
998
+ >>> from_date = datetime(2020, 1, 1)
999
+ >>> to_date = datetime.now()
1000
+ >>> account = Account()
1001
+ >>> history = account.get_orders_history(from_date, to_date)
1002
+ """
1003
+ if date_to is None:
1004
+ date_to = datetime.now()
1005
+
1006
+ if (group is not None) + (ticket is not None) + (position is not None) > 1:
1007
+ raise ValueError(
1008
+ "Only one of 'position', 'group' or 'ticket' can be specified or None of them as filter.")
1009
+ if group is not None:
1010
+ history_orders = mt5.history_orders_get(
1011
+ date_from, date_to, group=group
1012
+ )
1013
+ elif ticket is not None:
1014
+ history_orders = mt5.history_orders_get(ticket=ticket)
1015
+ elif position is not None:
1016
+ history_orders = mt5.history_orders_get(position=position)
1017
+ else:
1018
+ history_orders = mt5.history_orders_get(date_from, date_to)
1019
+
1020
+ if len(history_orders) == 0:
1021
+ return None
1022
+
1023
+ df = pd.DataFrame(list(history_orders),
1024
+ columns=history_orders[0]._asdict())
1025
+ df.drop(['time_expiration', 'type_time', 'state', 'position_by_id', 'reason',
1026
+ 'volume_current', 'price_stoplimit', 'sl', 'tp'], axis=1, inplace=True)
1027
+ df['time_setup'] = pd.to_datetime(df['time_setup'], unit='s')
1028
+ df['time_done'] = pd.to_datetime(df['time_done'], unit='s')
1029
+
1030
+ if save:
1031
+ file = "trade_history.csv"
1032
+ df.to_csv(file)
1033
+ if to_df:
1034
+ return df
1035
+ else:
1036
+ history_orders = [
1037
+ TradeOrder(**td._asdict())for td in history_orders]
1038
+ return tuple(history_orders)