bbstrader 2.0.3__cp312-cp312-macosx_11_0_arm64.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.
Files changed (45) hide show
  1. bbstrader/__init__.py +27 -0
  2. bbstrader/__main__.py +92 -0
  3. bbstrader/api/__init__.py +96 -0
  4. bbstrader/api/handlers.py +245 -0
  5. bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
  6. bbstrader/api/metatrader_client.pyi +624 -0
  7. bbstrader/assets/bbs_.png +0 -0
  8. bbstrader/assets/bbstrader.ico +0 -0
  9. bbstrader/assets/bbstrader.png +0 -0
  10. bbstrader/assets/qs_metrics_1.png +0 -0
  11. bbstrader/btengine/__init__.py +54 -0
  12. bbstrader/btengine/backtest.py +358 -0
  13. bbstrader/btengine/data.py +737 -0
  14. bbstrader/btengine/event.py +229 -0
  15. bbstrader/btengine/execution.py +287 -0
  16. bbstrader/btengine/performance.py +408 -0
  17. bbstrader/btengine/portfolio.py +393 -0
  18. bbstrader/btengine/strategy.py +588 -0
  19. bbstrader/compat.py +28 -0
  20. bbstrader/config.py +100 -0
  21. bbstrader/core/__init__.py +27 -0
  22. bbstrader/core/data.py +628 -0
  23. bbstrader/core/strategy.py +466 -0
  24. bbstrader/metatrader/__init__.py +48 -0
  25. bbstrader/metatrader/_copier.py +720 -0
  26. bbstrader/metatrader/account.py +865 -0
  27. bbstrader/metatrader/broker.py +418 -0
  28. bbstrader/metatrader/copier.py +1487 -0
  29. bbstrader/metatrader/rates.py +495 -0
  30. bbstrader/metatrader/risk.py +667 -0
  31. bbstrader/metatrader/trade.py +1692 -0
  32. bbstrader/metatrader/utils.py +402 -0
  33. bbstrader/models/__init__.py +39 -0
  34. bbstrader/models/nlp.py +932 -0
  35. bbstrader/models/optimization.py +182 -0
  36. bbstrader/scripts.py +665 -0
  37. bbstrader/trading/__init__.py +33 -0
  38. bbstrader/trading/execution.py +1159 -0
  39. bbstrader/trading/strategy.py +362 -0
  40. bbstrader/trading/utils.py +69 -0
  41. bbstrader-2.0.3.dist-info/METADATA +396 -0
  42. bbstrader-2.0.3.dist-info/RECORD +45 -0
  43. bbstrader-2.0.3.dist-info/WHEEL +5 -0
  44. bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
  45. bbstrader-2.0.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,865 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ import pandas as pd
5
+
6
+ from bbstrader.api import (
7
+ AccountInfo,
8
+ SymbolInfo,
9
+ TerminalInfo,
10
+ TickInfo,
11
+ TradeDeal,
12
+ TradeOrder,
13
+ TradePosition,
14
+ )
15
+ from bbstrader.api import Mt5client as client
16
+ from bbstrader.metatrader.broker import Broker, check_mt5_connection
17
+ from bbstrader.metatrader.utils import TIMEFRAMES, RateInfo, SymbolType, raise_mt5_error
18
+
19
+ __all__ = ["Account"]
20
+
21
+
22
+ class Account(object):
23
+ """
24
+ The `Account` class is utilized to retrieve information about
25
+ the current trading account or a specific account.
26
+ It enables interaction with the MT5 terminal to manage account details,
27
+ including account informations, terminal status, financial instrument details,
28
+ active orders, open positions, and trading history.
29
+
30
+ Example:
31
+ >>> # Instantiating the Account class
32
+ >>> account = Account()
33
+
34
+ >>> # Getting account information
35
+ >>> account_info = account.get_account_info()
36
+
37
+ >>> # Getting terminal information
38
+ >>> terminal_info = account.get_terminal_info()
39
+
40
+ >>> # Getting active orders
41
+ >>> orders = account.get_orders()
42
+
43
+ >>> # Fetching open positions
44
+ >>> positions = account.get_positions()
45
+
46
+ >>> # Accessing trade history
47
+ >>> from_date = datetime(2020, 1, 1)
48
+ >>> to_date = datetime.now()
49
+ >>> trade_history = account.get_trade_history(from_date, to_date)
50
+ """
51
+
52
+ def __init__(self, broker: Optional[Broker] = None, **kwargs):
53
+ """
54
+ Initialize the Account class.
55
+
56
+ See `bbstrader.metatrader.broker.check_mt5_connection()`
57
+ for more details on how to connect to MT5 terminal.
58
+
59
+ """
60
+ check_mt5_connection(**kwargs)
61
+ self._info = client.account_info()
62
+ terminal_info = self.get_terminal_info()
63
+ self._broker = (
64
+ broker
65
+ if broker is not None
66
+ else Broker(terminal_info.company if terminal_info else "Unknown")
67
+ )
68
+
69
+ @property
70
+ def info(self) -> AccountInfo:
71
+ return self._info
72
+
73
+ @property
74
+ def broker(self) -> Broker:
75
+ return self._broker
76
+
77
+ @property
78
+ def timezone(self) -> Optional[str]:
79
+ return self.broker.get_terminal_timezone()
80
+
81
+ @property
82
+ def name(self) -> str:
83
+ return self._info.name
84
+
85
+ @property
86
+ def number(self) -> int:
87
+ return self._info.login
88
+
89
+ @property
90
+ def server(self) -> str:
91
+ """The name of the trade server to which the client terminal is connected.
92
+ (e.g., 'AdmiralsGroup-Demo')
93
+ """
94
+ return self._info.server
95
+
96
+ @property
97
+ def balance(self) -> float:
98
+ return self._info.balance
99
+
100
+ @property
101
+ def leverage(self) -> int:
102
+ return self._info.leverage
103
+
104
+ @property
105
+ def equity(self) -> float:
106
+ return self._info.equity
107
+
108
+ @property
109
+ def currency(self) -> str:
110
+ return self._info.currency
111
+
112
+ def shutdown(self):
113
+ """Close the connection to the MetaTrader 5 terminal."""
114
+ client.shutdown()
115
+
116
+ def get_account_info(
117
+ self,
118
+ account: Optional[int] = None,
119
+ password: Optional[str] = None,
120
+ server: Optional[str] = None,
121
+ timeout: Optional[int] = 60_000,
122
+ path: Optional[str] = None,
123
+ ) -> Union[AccountInfo, None]:
124
+ """
125
+ Get info on the current trading account or a specific account .
126
+
127
+ Args:
128
+ account (int, optinal) : MT5 Trading account number.
129
+ password (str, optinal): MT5 Trading account password.
130
+
131
+ server (str, optinal): MT5 Trading account server
132
+ [Brokers or terminal server ["demo", "real"]]
133
+ If no server is set, the last used server is applied automaticall
134
+
135
+ timeout (int, optinal):
136
+ Connection timeout in milliseconds. Optional named parameter.
137
+ If not specified, the value of 60 000 (60 seconds) is applied.
138
+ If the connection is not established within the specified time,
139
+ the call is forcibly terminated and the exception is generated.
140
+ path (str, optional): The path to the MetaTrader 5 terminal executable file.
141
+ Defaults to None (e.g., "C:/Program Files/MetaTrader 5/terminal64.exe").
142
+
143
+ Returns:
144
+ - AccountInfo
145
+ - None in case of an error
146
+
147
+ Raises:
148
+ MT5TerminalError: A specific exception based on the error code.
149
+ """
150
+ # connect to the trade account specifying a password and a server
151
+ if account is not None and password is not None and server is not None:
152
+ try:
153
+ if path is not None:
154
+ self.broker.initialize_connection(
155
+ path=path,
156
+ login=account,
157
+ password=password,
158
+ server=server,
159
+ timeout=timeout,
160
+ )
161
+ authorized = client.login(
162
+ account, password=password, server=server, timeout=timeout
163
+ )
164
+ if not authorized:
165
+ raise_mt5_error(f"Failed to connect to account #{account}")
166
+ info = client.account_info()
167
+ return info
168
+ except Exception as e:
169
+ raise_mt5_error(e)
170
+ else:
171
+ try:
172
+ return client.account_info()
173
+ except Exception as e:
174
+ raise_mt5_error(e)
175
+
176
+ def get_terminal_info(self) -> TerminalInfo | None:
177
+ """
178
+ Get the connected MetaTrader 5 client terminal status and settings.
179
+
180
+ Returns:
181
+ - TerminalInfo
182
+ - None in case of an error
183
+
184
+ Raises:
185
+ MT5TerminalError: A specific exception based on the error code.
186
+ """
187
+ try:
188
+ terminal_info = client.terminal_info()
189
+ if terminal_info is None:
190
+ return None
191
+ except Exception as e:
192
+ raise_mt5_error(e)
193
+ return terminal_info
194
+
195
+ def get_symbol_info(self, symbol: str) -> SymbolInfo | None:
196
+ """Get symbol properties
197
+
198
+ Args:
199
+ symbol (str): Symbol name
200
+
201
+ Returns:
202
+ - SymbolInfo.
203
+ - None in case of an error.
204
+
205
+ Raises:
206
+ MT5TerminalError: A specific exception based on the error code.
207
+
208
+ """
209
+ try:
210
+ symbol_info = client.symbol_info(symbol)
211
+ if symbol_info is None:
212
+ return None
213
+ else:
214
+ return symbol_info
215
+ except Exception as e:
216
+ msg = self._symbol_info_msg(symbol)
217
+ raise_mt5_error(message=f"{str(e)} {msg}")
218
+
219
+ def _symbol_info_msg(self, symbol):
220
+ return (
221
+ f"No history found for {symbol} in Market Watch.\n"
222
+ f"* Ensure {symbol} is selected and displayed in the Market Watch window.\n"
223
+ f"* See https://www.metatrader5.com/en/terminal/help/trading/market_watch\n"
224
+ f"* Ensure the symbol name is correct.\n"
225
+ )
226
+
227
+ def get_tick_info(self, symbol: str) -> TickInfo | None:
228
+ """Get symbol tick properties
229
+
230
+ Args:
231
+ symbol (str): Symbol name
232
+
233
+ Returns:
234
+ - TickInfo.
235
+ - None in case of an error.
236
+
237
+ Raises:
238
+ MT5TerminalError: A specific exception based on the error code.
239
+
240
+ """
241
+ try:
242
+ tick_info = client.symbol_info_tick(symbol)
243
+ if tick_info is None:
244
+ return None
245
+ else:
246
+ return tick_info
247
+ except Exception as e:
248
+ msg = self._symbol_info_msg(symbol)
249
+ raise_mt5_error(message=f"{str(e)} {msg}")
250
+
251
+ def get_currency_rates(self, symbol: str) -> Dict[str, str]:
252
+ """
253
+ Args:
254
+ symbol (str): The symbol for which to get currencies
255
+
256
+ Returns:
257
+ - `base currency` (bc)
258
+ - `margin currency` (mc)
259
+ - `profit currency` (pc)
260
+ - `account currency` (ac)
261
+
262
+ Exemple:
263
+ >>> account = Account()
264
+ >>> account.get_currency_rates('EURUSD')
265
+ {'bc': 'EUR', 'mc': 'EUR', 'pc': 'USD', 'ac': 'USD'}
266
+ """
267
+ info = self.get_symbol_info(symbol)
268
+ bc = info.currency_base
269
+ pc = info.currency_profit
270
+ mc = info.currency_margin
271
+ ac = self._info.currency
272
+ return {"bc": bc, "mc": mc, "pc": pc, "ac": ac}
273
+
274
+ def get_symbols(
275
+ self,
276
+ symbol_type: SymbolType | str = "ALL",
277
+ check_etf=False,
278
+ save=False,
279
+ file_name="symbols",
280
+ include_desc=False,
281
+ display_total=False,
282
+ ) -> List[str]:
283
+ """
284
+ Get all specified financial instruments from the MetaTrader 5 terminal.
285
+
286
+ Args:
287
+ symbol_type (SymbolType | str): The type of financial instruments to retrieve.
288
+ - `ALL`: For all available symbols
289
+ - See `bbstrader.metatrader.utils.SymbolType` for more details.
290
+
291
+ check_etf (bool): If True and symbol_type is 'etf', check if the
292
+ ETF description contains 'ETF'.
293
+
294
+ save (bool): If True, save the symbols to a file.
295
+
296
+ file_name (str): The name of the file to save the symbols to
297
+ (without the extension).
298
+
299
+ include_desc (bool): If True, include the symbol's description
300
+ in the output and saved file.
301
+
302
+ Returns:
303
+ list: A list of symbols.
304
+
305
+ Raises:
306
+ Exception: If there is an error connecting to MT5 or retrieving symbols.
307
+ """
308
+ return self.broker.get_symbols(
309
+ symbol_type=symbol_type,
310
+ check_etf=check_etf,
311
+ save=save,
312
+ file_name=file_name,
313
+ include_desc=include_desc,
314
+ display_total=display_total,
315
+ )
316
+
317
+ def get_symbol_type(self, symbol: str) -> SymbolType:
318
+ """
319
+ Determines the type of a given financial instrument symbol.
320
+
321
+ Args:
322
+ symbol (str): The symbol of the financial instrument (e.g., `GOOGL`, `EURUSD`).
323
+
324
+ Returns:
325
+ SymbolType: The type of the financial instrument, one of the following:
326
+ - `SymbolType.ETFs`
327
+ - `SymbolType.BONDS`
328
+ - `SymbolType.FOREX`
329
+ - `SymbolType.FUTURES`
330
+ - `SymbolType.STOCKS`
331
+ - `SymbolType.INDICES`
332
+ - `SymbolType.COMMODITIES`
333
+ - `SymbolType.CRYPTO`
334
+ - `SymbolType.unknown` if the type cannot be determined.
335
+
336
+ """
337
+ return self.broker.get_symbol_type(symbol)
338
+
339
+ def _get_symbols_by_category(
340
+ self, symbol_type: SymbolType | str, category: str, category_map: Dict[str, str]
341
+ ) -> List[str]:
342
+ return self.broker.get_symbols_by_category(symbol_type, category, category_map)
343
+
344
+ def get_stocks_from_country(
345
+ self, country_code: str = "USA", etf=False
346
+ ) -> List[str]:
347
+ """
348
+ Retrieves a list of stock symbols from a specific country.
349
+
350
+ Supported countries are:
351
+ * **Australia:** AUS
352
+ * **Belgium:** BEL
353
+ * **Denmark:** DNK
354
+ * **Finland:** FIN
355
+ * **France:** FRA
356
+ * **Germany:** DEU
357
+ * **Netherlands:** NLD
358
+ * **Norway:** NOR
359
+ * **Portugal:** PRT
360
+ * **Spain:** ESP
361
+ * **Sweden:** SWE
362
+ * **United Kingdom:** GBR
363
+ * **United States:** USA
364
+ * **Switzerland:** CHE
365
+ * **Hong Kong:** HKG
366
+ * **Ireland:** IRL
367
+ * **Austria:** AUT
368
+
369
+ Args:
370
+ country (str, optional): The country code of stocks to retrieve.
371
+ Defaults to 'USA'.
372
+
373
+ Returns:
374
+ list: A list of stock symbol names from the specified country.
375
+
376
+ Raises:
377
+ ValueError: If an unsupported country is provided.
378
+
379
+ Notes:
380
+ This mthods works primarly with brokers who specify the stock symbols type and exchanges,
381
+ For other brokers use `get_symbols()` or this method will use it by default.
382
+ """
383
+ stocks = self._get_symbols_by_category(
384
+ SymbolType.STOCKS, country_code, self.broker.countries_stocks
385
+ )
386
+ etfs = (
387
+ self._get_symbols_by_category(
388
+ SymbolType.ETFs, country_code, self.broker.countries_stocks
389
+ )
390
+ if etf
391
+ else []
392
+ )
393
+ if not stocks and not etfs:
394
+ stocks = self.get_symbols(symbol_type=SymbolType.STOCKS)
395
+ etfs = self.get_symbols(symbol_type=SymbolType.ETFs) if etf else []
396
+ return stocks + etfs
397
+
398
+ def get_stocks_from_exchange(
399
+ self, exchange_code: str = "XNYS", etf=True
400
+ ) -> List[str]:
401
+ """
402
+ Get stock symbols from a specific exchange using the ISO Code for the exchange.
403
+
404
+ Supported exchanges are from Admirals Group AS products:
405
+ * **XASX:** **Australian Securities Exchange**
406
+ * **XBRU:** **Euronext Brussels Exchange**
407
+ * **XCSE:** **Copenhagen Stock Exchange**
408
+ * **XHEL:** **NASDAQ OMX Helsinki**
409
+ * **XPAR:** **Euronext Paris**
410
+ * **XETR:** **Xetra Frankfurt**
411
+ * **XOSL:** **Oslo Stock Exchange**
412
+ * **XLIS:** **Euronext Lisbon**
413
+ * **XMAD:** **Bolsa de Madrid**
414
+ * **XSTO:** **NASDAQ OMX Stockholm**
415
+ * **XLON:** **London Stock Exchange**
416
+ * **NYSE:** **New York Stock Exchange**
417
+ * **ARCA:** **NYSE ARCA**
418
+ * **AMEX:** **NYSE AMEX**
419
+ * **XNYS:** **New York Stock Exchange (AMEX, ARCA, NYSE)**
420
+ * **NASDAQ:** **NASDAQ**
421
+ * **BATS:** **BATS Exchange**
422
+ * **XSWX:** **SWX Swiss Exchange**
423
+ * **XAMS:** **Euronext Amsterdam**
424
+
425
+ Args:
426
+ exchange_code (str, optional): The ISO code of the exchange.
427
+ etf (bool, optional): If True, include ETFs from the exchange. Defaults to True.
428
+
429
+ Returns:
430
+ list: A list of stock symbol names from the specified exchange.
431
+
432
+ Raises:
433
+ ValueError: If an unsupported exchange is provided.
434
+
435
+ Notes:
436
+ This mthods works primarly with brokers who specify the stock symbols type and exchanges,
437
+ For other brokers use `get_symbols()` or this method will use it by default.
438
+ """
439
+ stocks = self._get_symbols_by_category(
440
+ SymbolType.STOCKS, exchange_code, self.broker.exchanges
441
+ )
442
+ etfs = (
443
+ self._get_symbols_by_category(
444
+ SymbolType.ETFs, exchange_code, self.broker.exchanges
445
+ )
446
+ if etf
447
+ else []
448
+ )
449
+ if not stocks and not etfs:
450
+ stocks = self.get_symbols(symbol_type=SymbolType.STOCKS)
451
+ etfs = self.get_symbols(symbol_type=SymbolType.ETFs) if etf else []
452
+ return stocks + etfs
453
+
454
+ def get_rate_info(self, symbol: str, timeframe: str = "1m") -> RateInfo | None:
455
+ """Get the most recent bar for a specified symbol and timeframe.
456
+
457
+ Args:
458
+ symbol (str): The symbol for which to get the rate information.
459
+ timeframe (str): The timeframe for the rate information. Default is '1m'.
460
+ See ``bbstrader.metatrader.utils.TIMEFRAMES`` for supported timeframes.
461
+ Returns:
462
+ RateInfo: The most recent bar as a RateInfo named tuple.
463
+ None: If no rates are found or an error occurs.
464
+ Raises:
465
+ MT5TerminalError: A specific exception based on the error code.
466
+ """
467
+ rates = client.copy_rates_from_pos(symbol, TIMEFRAMES[timeframe], 0, 1)
468
+ if rates is None or len(rates) == 0:
469
+ return None
470
+ rate = rates[0]
471
+ return RateInfo(*rate)
472
+
473
+ def get_positions(
474
+ self,
475
+ symbol: Optional[str] = None,
476
+ group: Optional[str] = None,
477
+ ticket: Optional[int] = None,
478
+ ) -> Union[List[TradePosition] | None]:
479
+ """
480
+ Get open positions with the ability to filter by symbol or ticket.
481
+ There are four call options:
482
+
483
+ - Call without parameters. Returns open positions for all symbols.
484
+ - Call specifying a symbol. Returns open positions for the specified symbol.
485
+ - Call specifying a group of symbols. Returns open positions for the specified group of symbols.
486
+ - Call specifying a position ticket. Returns the position corresponding to the specified ticket.
487
+
488
+ Args:
489
+ symbol (Optional[str]): Symbol name. Optional named parameter.
490
+ If a symbol is specified, the `ticket` parameter is ignored.
491
+
492
+ group (Optional[str]): The filter for arranging a group of necessary symbols.
493
+ Optional named parameter. If the group is specified,
494
+ the function returns only positions meeting specified criteria
495
+ for a symbol name.
496
+
497
+ ticket (Optional[int]): Position ticket. Optional named parameter.
498
+ A unique number assigned to each newly opened position.
499
+ It usually matches the ticket of the order used to open the position,
500
+ except when the ticket is changed as a result of service operations on the server,
501
+ for example, when charging swaps with position re-opening.
502
+
503
+
504
+ Returns:
505
+ [List[TradePosition] | None]:
506
+ - List of `TradePosition`.
507
+
508
+ Notes:
509
+ The method allows receiving all open positions within a specified period.
510
+
511
+ The `group` parameter may contain several comma-separated conditions.
512
+
513
+ A condition can be set as a mask using '*'.
514
+
515
+ The logical negation symbol '!' can be used for exclusion.
516
+
517
+ All conditions are applied sequentially, which means conditions for inclusion
518
+ in a group should be specified first, followed by an exclusion condition.
519
+
520
+ For example, `group="*, !EUR"` means that deals for all symbols should be selected first,
521
+ and those containing "EUR" in symbol names should be excluded afterward.
522
+ """
523
+
524
+ if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
525
+ raise ValueError(
526
+ "Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
527
+ )
528
+
529
+ if symbol is not None:
530
+ positions = client.positions_get(symbol)
531
+ elif group is not None:
532
+ positions = client.positions_get_by_group(group)
533
+ elif ticket is not None:
534
+ positions = client.position_get_by_ticket(ticket)
535
+ else:
536
+ positions = client.positions_get()
537
+
538
+ if positions is None:
539
+ return None
540
+ if isinstance(positions, TradePosition):
541
+ return [positions]
542
+ if len(positions) == 0:
543
+ return None
544
+
545
+ return positions
546
+
547
+ def get_orders(
548
+ self,
549
+ symbol: Optional[str] = None,
550
+ group: Optional[str] = None,
551
+ ticket: Optional[int] = None,
552
+ ) -> Union[List[TradeOrder] | None]:
553
+ """
554
+ Get active orders with the ability to filter by symbol or ticket.
555
+ There are four call options:
556
+
557
+ - Call without parameters. Returns open positions for all symbols.
558
+ - Call specifying a symbol, open positions should be received for.
559
+ - Call specifying a group of symbols, open positions should be received for.
560
+ - Call specifying a position ticket.
561
+
562
+ Args:
563
+ symbol (Optional[str]): Symbol name. Optional named parameter.
564
+ If a symbol is specified, the ticket parameter is ignored.
565
+
566
+ group (Optional[str]): The filter for arranging a group of necessary symbols.
567
+ Optional named parameter. If the group is specified,
568
+ the function returns only positions meeting a specified criteria
569
+ for a symbol name.
570
+
571
+ ticket (Optional[int]): Order ticket. Optional named parameter.
572
+ Unique number assigned to each order.
573
+
574
+ to_df (bool): If True, a DataFrame is returned.
575
+
576
+ Returns:
577
+ [List[TradeOrder] | None]:
578
+ - List of `TradeOrder` .
579
+
580
+ Notes:
581
+ The method allows receiving all history orders within a specified period.
582
+ The `group` parameter may contain several comma-separated conditions.
583
+ A condition can be set as a mask using '*'.
584
+
585
+ The logical negation symbol '!' can be used for exclusion.
586
+ All conditions are applied sequentially, which means conditions for inclusion
587
+ in a group should be specified first, followed by an exclusion condition.
588
+
589
+ For example, `group="*, !EUR"` means that deals for all symbols should be selected first
590
+ and the ones containing "EUR" in symbol names should be excluded afterward.
591
+ """
592
+
593
+ if (symbol is not None) + (group is not None) + (ticket is not None) > 1:
594
+ raise ValueError(
595
+ "Only one of 'symbol', 'group', or 'ticket' can be specified as filter or None of them."
596
+ )
597
+
598
+ orders = None
599
+ if symbol is not None:
600
+ orders = client.orders_get(symbol)
601
+ elif group is not None:
602
+ orders = client.orders_get_by_group(group)
603
+ elif ticket is not None:
604
+ orders = client.order_get_by_ticket(ticket)
605
+ else:
606
+ orders = client.orders_get()
607
+
608
+ if orders is None or len(orders) == 0:
609
+ return None
610
+ return orders
611
+
612
+ def _fetch_history(
613
+ self,
614
+ fetch_type: str, # "deals" or "orders"
615
+ date_from: datetime,
616
+ date_to: Optional[datetime],
617
+ group: Optional[str],
618
+ ticket: Optional[int],
619
+ position: Optional[int],
620
+ to_df: bool,
621
+ drop_cols: List[str],
622
+ time_cols: List[str],
623
+ ) -> Any:
624
+ date_to = date_to or datetime.now()
625
+
626
+ filters = [group, ticket, position]
627
+ if sum(f is not None for f in filters) > 1:
628
+ raise ValueError(
629
+ "Only one of 'position', 'group', or 'ticket' can be specified."
630
+ )
631
+
632
+ if fetch_type == "deals":
633
+ client_func = client.history_deals_get
634
+ pos_func = client.history_deals_get_by_pos
635
+ else:
636
+ client_func = client.history_orders_get
637
+ pos_func = client.history_orders_get_by_pos
638
+
639
+ data = None
640
+ if ticket:
641
+ data = client_func(ticket)
642
+ elif position:
643
+ data = pos_func(position)
644
+ elif group:
645
+ data = client_func(date_from, date_to, group)
646
+ else:
647
+ data = client_func(date_from, date_to)
648
+
649
+ if not data:
650
+ return None
651
+
652
+ if to_df:
653
+ from bbstrader.api import trade_object_to_df
654
+
655
+ df = trade_object_to_df(data)
656
+ for col in time_cols:
657
+ if col in df.columns:
658
+ df[col] = pd.to_datetime(df[col], unit="s")
659
+
660
+ df.drop(columns=[c for c in drop_cols if c in df.columns], inplace=True)
661
+ if fetch_type == "deals" and "time" in df.columns:
662
+ df.set_index("time", inplace=True)
663
+ return df
664
+
665
+ return data
666
+
667
+ def get_trades_history(
668
+ self,
669
+ date_from: datetime = datetime(2000, 1, 1),
670
+ date_to: Optional[datetime] = None,
671
+ group: Optional[str] = None,
672
+ ticket: Optional[int] = None, # TradeDeal.ticket
673
+ position: Optional[int] = None, # TradePosition.ticket
674
+ to_df: bool = True,
675
+ ) -> Union[pd.DataFrame, List[TradeDeal] | None]:
676
+ """
677
+ Get deals from trading history within the specified interval
678
+ with the ability to filter by `ticket` or `position`.
679
+
680
+ This method is useful if you need panda dataframe.
681
+
682
+ You can call this method in the following ways:
683
+
684
+ - Call with a `time interval`. Returns all deals falling within the specified interval.
685
+
686
+ - Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.
687
+
688
+ - Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.
689
+
690
+ Args:
691
+ date_from (datetime): Date the bars are requested from.
692
+ Set by the `datetime` object or as a number of seconds elapsed since 1970-01-01.
693
+ Bars with the open time >= `date_from` are returned. Required unnamed parameter.
694
+
695
+ date_to (Optional[datetime]): Same as `date_from`.
696
+
697
+ group (Optional[str]): The filter for arranging a group of necessary symbols.
698
+ Optional named parameter. If the group is specified,
699
+ the function returns only positions meeting specified criteria
700
+ for a symbol name.
701
+
702
+ ticket (Optional[int]): Ticket of an order (stored in `DEAL_ORDER`) for which all deals should be received.
703
+ Optional parameter. If not specified, the filter is not applied.
704
+
705
+ position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) for which all deals should be received.
706
+ Optional parameter. If not specified, the filter is not applied.
707
+
708
+ to_df (bool): If True, a DataFrame is returned.
709
+
710
+ Returns:
711
+ Union[pd.DataFrame, Tuple[TradeDeal], None]:
712
+ - `TradeDeal` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
713
+
714
+ Notes:
715
+ The method allows receiving all history orders within a specified period.
716
+
717
+ The `group` parameter may contain several comma-separated conditions.
718
+
719
+ A condition can be set as a mask using '*'.
720
+
721
+ The logical negation symbol '!' can be used for exclusion.
722
+
723
+ All conditions are applied sequentially, which means conditions for inclusion
724
+ in a group should be specified first, followed by an exclusion condition.
725
+
726
+ For example, `group="*, !EUR"` means that deals for all symbols should be selected first
727
+ and those containing "EUR" in symbol names should be excluded afterward.
728
+
729
+ Example:
730
+ >>> # Get the number of deals in history
731
+ >>> from datetime import datetime
732
+ >>> from_date = datetime(2020, 1, 1)
733
+ >>> to_date = datetime.now()
734
+ >>> account = Account()
735
+ >>> history = account.get_trades_history(from_date, to_date)
736
+ """
737
+
738
+ return self._fetch_history(
739
+ fetch_type="deals",
740
+ drop_cols=["time_msc", "external_id"],
741
+ time_cols=["time"],
742
+ **dict(
743
+ date_from=date_from,
744
+ date_to=date_to,
745
+ group=group,
746
+ ticket=ticket,
747
+ position=position,
748
+ to_df=to_df,
749
+ ),
750
+ )
751
+
752
+ def get_orders_history(
753
+ self,
754
+ date_from: datetime = datetime(2000, 1, 1),
755
+ date_to: Optional[datetime] = None,
756
+ group: Optional[str] = None,
757
+ ticket: Optional[int] = None, # order ticket
758
+ position: Optional[int] = None, # position ticket
759
+ to_df: bool = True,
760
+ ) -> Union[pd.DataFrame, List[TradeOrder] | None]:
761
+ """
762
+ Get orders 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]): Order ticket to filter results. Optional parameter.
786
+ If not specified, the filter is not applied.
787
+
788
+ position (Optional[int]): Ticket of a position (stored in `DEAL_POSITION_ID`) to filter results.
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 True, a CSV file will be created to save the history.
794
+
795
+ Returns:
796
+ Union[pd.DataFrame, List[TradeOrder], None]
797
+ - List of `TradeOrder` .
798
+
799
+ Notes:
800
+ The method allows receiving all history orders within a specified period.
801
+
802
+ The `group` parameter may contain several comma-separated conditions.
803
+
804
+ A condition can be set as a mask using '*'.
805
+
806
+ The logical negation symbol '!' can be used for exclusion.
807
+
808
+ All conditions are applied sequentially, which means conditions for inclusion
809
+ in a group should be specified first, followed by an exclusion condition.
810
+
811
+ For example, `group="*, !EUR"` means that deals for all symbols should be selected first
812
+ and those containing "EUR" in symbol names should be excluded afterward.
813
+
814
+ Example:
815
+ >>> # Get the number of deals in history
816
+ >>> from datetime import datetime
817
+ >>> from_date = datetime(2020, 1, 1)
818
+ >>> to_date = datetime.now()
819
+ >>> account = Account()
820
+ >>> history = account.get_orders_history(from_date, to_date)
821
+ """
822
+ return self._fetch_history(
823
+ fetch_type="orders",
824
+ drop_cols=[
825
+ "time_expiration",
826
+ "type_time",
827
+ "state",
828
+ "position_by_id",
829
+ "reason",
830
+ "volume_current",
831
+ "price_stoplimit",
832
+ "sl",
833
+ "tp",
834
+ ],
835
+ time_cols=["time_setup", "time_done"],
836
+ **dict(
837
+ date_from=date_from,
838
+ date_to=date_to,
839
+ group=group,
840
+ ticket=ticket,
841
+ position=position,
842
+ to_df=to_df,
843
+ ),
844
+ )
845
+
846
+ def get_today_deals(self, id, group=None) -> List[TradeDeal]:
847
+ """
848
+ Get all today deals for a specific symbol or group of symbols
849
+
850
+ Args:
851
+ id (int): strategy or expert id
852
+ group (str): Symbol or group or symbol
853
+ Returns:
854
+ List[TradeDeal]: List of today deals
855
+ """
856
+ history = self.get_trades_history(group=group, to_df=False) or []
857
+ positions_ids = set([deal.position_id for deal in history if deal.magic == id])
858
+ today_deals = []
859
+ for position in positions_ids:
860
+ deal = self.get_trades_history(position=position, to_df=False) or []
861
+ if deal is not None and len(deal) == 2:
862
+ deal_time = datetime.fromtimestamp(deal[1].time)
863
+ if deal_time.date() == datetime.now().date():
864
+ today_deals.append(deal[1])
865
+ return today_deals