bbstrader 0.3.1__py3-none-any.whl → 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bbstrader might be problematic. Click here for more details.

bbstrader/__init__.py CHANGED
@@ -7,7 +7,7 @@ __author__ = "Bertin Balouki SIMYELI"
7
7
  __copyright__ = "2023-2025 Bertin Balouki SIMYELI"
8
8
  __email__ = "bertin@bbstrader.com"
9
9
  __license__ = "MIT"
10
- __version__ = "0.3.1"
10
+ __version__ = "0.3.2"
11
11
 
12
12
  from bbstrader import compat # noqa: F401
13
13
  from bbstrader import core # noqa: F401
bbstrader/__main__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import argparse
2
+ import multiprocessing
2
3
  import sys
3
4
  from enum import Enum
4
5
 
@@ -69,4 +70,5 @@ def main():
69
70
 
70
71
 
71
72
  if __name__ == "__main__":
73
+ multiprocessing.freeze_support()
72
74
  main()
@@ -248,14 +248,13 @@ def run_backtest(
248
248
 
249
249
  start_date (datetime): Start date of the backtest.
250
250
 
251
- data_handler (DataHandler): An instance of the `DataHandler` class, responsible for managing
251
+ data_handler (DataHandler): A subclass of the `DataHandler` class, responsible for managing
252
252
  and processing market data. Available options include `CSVDataHandler`,
253
- `MT5DataHandler`, and `YFDataHandler`. Ensure that the `DataHandler`
254
- instance is initialized before passing it to the function.
253
+ `MT5DataHandler`, and `YFDataHandler`.
255
254
 
256
255
  strategy (Strategy): The trading strategy to be employed during the backtest.
257
- The strategy must be an instance of `Strategy` and should include the following attributes:
258
- - `bars` (DataHandler): The `DataHandler` instance for the strategy.
256
+ The strategy must be a subclass of `Strategy` and should include the following attributes:
257
+ - `bars` (DataHandler): The `DataHandler` class for the strategy.
259
258
  - `events` (Queue): Queue instance for managing events.
260
259
  - `symbol_list` (List[str]): List of symbols to trade.
261
260
  - `mode` (str): 'live' or 'backtest'.
@@ -307,9 +306,9 @@ def run_backtest(
307
306
  >>> run_backtest(
308
307
  ... symbol_list=symbol_list,
309
308
  ... start_date=start,
310
- ... data_handler=MT5DataHandler(),
311
- ... strategy=StockIndexSTBOTrading(),
312
- ... exc_handler=MT5ExecutionHandler(),
309
+ ... data_handler=MT5DataHandler,
310
+ ... strategy=StockIndexSTBOTrading,
311
+ ... exc_handler=MT5ExecutionHandler,
313
312
  ... initial_capital=100000.0,
314
313
  ... heartbeat=0.0,
315
314
  ... **kwargs
@@ -98,7 +98,7 @@ class SimExecutionHandler(ExecutionHandler):
98
98
  self.events.put(fill_event)
99
99
  self.logger.info(
100
100
  f"{event.direction} ORDER FILLED: SYMBOL={event.symbol}, "
101
- f"QUANTITY={event.quantity}, PRICE @{event.price} EXCHANGE={fill_event.exchange}",
101
+ f"QUANTITY={event.quantity}, PRICE @{round(event.price, 5)} EXCHANGE={fill_event.exchange}",
102
102
  custom_time=fill_event.timeindex,
103
103
  )
104
104
 
@@ -264,7 +264,7 @@ class MT5ExecutionHandler(ExecutionHandler):
264
264
  self.events.put(fill_event)
265
265
  self.logger.info(
266
266
  f"{direction} ORDER FILLED: SYMBOL={symbol}, QUANTITY={quantity}, "
267
- f"PRICE @{price} EXCHANGE={fill_event.exchange}",
267
+ f"PRICE @{round(event.price, 5)} EXCHANGE={fill_event.exchange}",
268
268
  custom_time=fill_event.timeindex,
269
269
  )
270
270
 
@@ -12,17 +12,18 @@ from loguru import logger
12
12
  from bbstrader.btengine.data import DataHandler
13
13
  from bbstrader.btengine.event import Events, FillEvent, SignalEvent
14
14
  from bbstrader.config import BBSTRADER_DIR
15
- from bbstrader.metatrader.account import (
15
+ from bbstrader.metatrader import (
16
16
  Account,
17
17
  AdmiralMarktsGroup,
18
18
  PepperstoneGroupLimited,
19
+ TradeOrder,
20
+ Rates,
21
+ TradeSignal,
22
+ TradingMode,
23
+ SymbolType
19
24
  )
20
- from bbstrader.metatrader.utils import SymbolType
21
- from bbstrader.metatrader.rates import Rates
22
- from bbstrader.metatrader.trade import TradeSignal, TradingMode
23
25
  from bbstrader.models.optimization import optimized_weights
24
26
 
25
-
26
27
  __all__ = ["Strategy", "MT5Strategy"]
27
28
 
28
29
  logger.add(
@@ -71,8 +72,13 @@ class MT5Strategy(Strategy):
71
72
  calculate signals for the MetaTrader 5 trading platform. The signals
72
73
  are generated by the `MT5Strategy` object and sent to the the `Mt5ExecutionEngine`
73
74
  for live trading and `MT5BacktestEngine` objects for backtesting.
74
- """
75
75
 
76
+ # NOTE
77
+ It is recommanded that every strategy specfic method to be a private method
78
+ in order to avoid naming collusion.
79
+ """
80
+ tf: str
81
+ max_trades: Dict[str, int]
76
82
  def __init__(
77
83
  self,
78
84
  events: Queue = None,
@@ -107,9 +113,11 @@ class MT5Strategy(Strategy):
107
113
  self._initialize_portfolio()
108
114
  self.kwargs = kwargs
109
115
  self.periodes = 0
110
-
111
- @property
116
+
117
+ @property
112
118
  def account(self):
119
+ if self.mode != TradingMode.LIVE:
120
+ raise ValueError("account attribute is only allowed in Live mode")
113
121
  return Account(**self.kwargs)
114
122
 
115
123
  @property
@@ -127,7 +135,7 @@ class MT5Strategy(Strategy):
127
135
  @property
128
136
  def orders(self):
129
137
  if self.mode == TradingMode.LIVE:
130
- return self.account.get_orders()
138
+ return self.account.get_orders() or []
131
139
  return self._orders
132
140
 
133
141
  @property
@@ -139,7 +147,7 @@ class MT5Strategy(Strategy):
139
147
  @property
140
148
  def positions(self):
141
149
  if self.mode == TradingMode.LIVE:
142
- return self.account.get_positions()
150
+ return self.account.get_positions() or []
143
151
  return self._positions
144
152
 
145
153
  @property
@@ -345,7 +353,7 @@ class MT5Strategy(Strategy):
345
353
  log = True
346
354
  if log:
347
355
  self.logger.info(
348
- f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}",
356
+ f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{round(price, 5)}",
349
357
  custom_time=dtime,
350
358
  )
351
359
 
@@ -526,7 +534,7 @@ class MT5Strategy(Strategy):
526
534
  def logmsg(order, type, symbol, dtime):
527
535
  return self.logger.info(
528
536
  f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
529
- f"PRICE @ {order.price}",
537
+ f"PRICE @ {round(order.price, 5)}",
530
538
  custom_time=dtime,
531
539
  )
532
540
 
@@ -614,7 +622,7 @@ class MT5Strategy(Strategy):
614
622
  )
615
623
 
616
624
  @staticmethod
617
- def calculate_pct_change(current_price, lh_price):
625
+ def calculate_pct_change(current_price, lh_price) -> float:
618
626
  return ((current_price - lh_price) / lh_price) * 100
619
627
 
620
628
  def get_asset_values(
@@ -649,8 +657,8 @@ class MT5Strategy(Strategy):
649
657
  In Live mode, the `bbstrader.metatrader.rates.Rates` class is used to get the historical data
650
658
  so the value_type must be 'returns', 'open', 'high', 'low', 'close', 'adjclose', 'volume'.
651
659
  """
652
- if mode not in ["backtest", "live"]:
653
- raise ValueError("Mode must be either backtest or live.")
660
+ if mode not in [TradingMode.BACKTEST, TradingMode.LIVE]:
661
+ raise ValueError("Mode must be an instance of TradingMode")
654
662
  asset_values = {}
655
663
  if mode == TradingMode.BACKTEST:
656
664
  if bars is None:
@@ -696,7 +704,7 @@ class MT5Strategy(Strategy):
696
704
  if period_count == 0 or period_count is None:
697
705
  return True
698
706
  return period_count % signal_inverval == 0
699
-
707
+
700
708
  @staticmethod
701
709
  def stop_time(time_zone: str, stop_time: str) -> bool:
702
710
  now = datetime.now(pytz.timezone(time_zone)).time()
@@ -760,6 +768,47 @@ class MT5Strategy(Strategy):
760
768
  )
761
769
  return prices
762
770
  return np.array([])
771
+
772
+ def get_active_orders(self, symbol: str, strategy_id: int, order_type: int = None) -> List[TradeOrder]:
773
+ """
774
+ Get the active orders for a given symbol and strategy.
775
+
776
+ Args:
777
+ symbol : The symbol for the trade.
778
+ strategy_id : The unique identifier for the strategy.
779
+ order_type : The type of order to filter by (optional):
780
+ "BUY_LIMIT": 2
781
+ "SELL_LIMIT": 3
782
+ "BUY_STOP": 4
783
+ "SELL_STOP": 5
784
+ "BUY_STOP_LIMIT": 6
785
+ "SELL_STOP_LIMIT": 7
786
+
787
+ Returns:
788
+ List[TradeOrder] : A list of active orders for the given symbol and strategy.
789
+ """
790
+ orders = [o for o in self.orders if o.symbol == symbol and o.magic == strategy_id]
791
+ if order_type is not None and len(orders) > 0:
792
+ orders = [o for o in orders if o.type == order_type]
793
+ return orders
794
+
795
+ def exit_positions(self, position, prices, asset, th: float = 0.01):
796
+ if len(prices) == 0:
797
+ return False
798
+ tick_info = self.account.get_tick_info(asset)
799
+ bid, ask = tick_info.bid, tick_info.ask
800
+ if len(prices) == 1:
801
+ price = prices[0]
802
+ elif len(prices) in range(2, self.max_trades[asset] + 1):
803
+ price = np.mean(prices)
804
+ if (
805
+ position == 0
806
+ and self.calculate_pct_change(ask, price) >= th
807
+ or position == 1
808
+ and abs(self.calculate_pct_change(bid, price)) >= th
809
+ ):
810
+ return True
811
+ return False
763
812
 
764
813
  @staticmethod
765
814
  def get_current_dt(time_zone: str = "US/Eastern") -> datetime:
@@ -796,7 +845,9 @@ class MT5Strategy(Strategy):
796
845
  return dt_to
797
846
 
798
847
  @staticmethod
799
- def get_mt5_equivalent(symbols, symbol_type: str | SymbolType = SymbolType.STOCKS, **kwargs) -> List[str]:
848
+ def get_mt5_equivalent(
849
+ symbols, symbol_type: str | SymbolType = SymbolType.STOCKS, **kwargs
850
+ ) -> List[str]:
800
851
  """
801
852
  Get the MetaTrader 5 equivalent symbols for the symbols in the list.
802
853
  This method is used to get the symbols that are available on the MetaTrader 5 platform.
bbstrader/config.py CHANGED
@@ -3,8 +3,8 @@ from pathlib import Path
3
3
  from typing import List
4
4
 
5
5
 
6
- TERMINAL = "\\terminal64.exe"
7
- BASE_FOLDER = "C:\\Program Files\\"
6
+ TERMINAL = "/terminal64.exe"
7
+ BASE_FOLDER = "C:/Program Files/"
8
8
 
9
9
  AMG_PATH = BASE_FOLDER + "Admirals Group MT5 Terminal" + TERMINAL
10
10
  PGL_PATH = BASE_FOLDER + "Pepperstone MetaTrader 5" + TERMINAL
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import re
3
3
  import urllib.request
4
- from datetime import datetime
4
+ from datetime import datetime, timedelta
5
5
  from typing import Any, Dict, List, Literal, Optional, Tuple, Union
6
6
 
7
7
  import pandas as pd
@@ -54,9 +54,7 @@ BROKERS_TIMEZONES = {
54
54
  "PGL": "Europe/Helsinki",
55
55
  }
56
56
 
57
- _ADMIRAL_MARKETS_URL_ = (
58
- "https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets"
59
- )
57
+ _ADMIRAL_MARKETS_URL_ = "https://one.justmarkets.link/a/tufvj0xugm/registration/trader"
60
58
  _JUST_MARKETS_URL_ = "https://one.justmarkets.link/a/tufvj0xugm/registration/trader"
61
59
  _FTMO_URL_ = "https://trader.ftmo.com/?affiliates=JGmeuQqepAZLMcdOEQRp"
62
60
  _ADMIRAL_MARKETS_PRODUCTS_ = [
@@ -138,13 +136,13 @@ AMG_EXCHANGES = {
138
136
  }
139
137
 
140
138
 
141
- def check_mt5_connection(**kwargs):
139
+ def check_mt5_connection(**kwargs) -> bool:
142
140
  """
143
141
  Initialize the connection to the MetaTrader 5 terminal.
144
142
 
145
143
  Args:
146
144
  path (str, optional): The path to the MetaTrader 5 terminal executable file.
147
- Defaults to None (e.g., "C:\\Program Files\\MetaTrader 5\\terminal64.exe").
145
+ Defaults to None (e.g., "C:/Program Files/MetaTrader 5/terminal64.exe").
148
146
  login (int, optional): The login ID of the trading account. Defaults to None.
149
147
  password (str, optional): The password of the trading account. Defaults to None.
150
148
  server (str, optional): The name of the trade server to which the client terminal is connected.
@@ -165,6 +163,7 @@ def check_mt5_connection(**kwargs):
165
163
  timeout = kwargs.get("timeout", 60_000)
166
164
  portable = kwargs.get("portable", False)
167
165
 
166
+ init = False
168
167
  if path is None and (login or password or server):
169
168
  raise ValueError(
170
169
  "You must provide a path to the terminal executable file"
@@ -189,6 +188,7 @@ def check_mt5_connection(**kwargs):
189
188
  raise_mt5_error(INIT_MSG)
190
189
  except Exception:
191
190
  raise_mt5_error(INIT_MSG)
191
+ return init
192
192
 
193
193
 
194
194
  def shutdown_mt5():
@@ -391,6 +391,7 @@ class Account(object):
391
391
  password: Optional[str] = None,
392
392
  server: Optional[str] = None,
393
393
  timeout: Optional[int] = 60_000,
394
+ path: Optional[str] = None,
394
395
  ) -> Union[AccountInfo, None]:
395
396
  """
396
397
  Get info on the current trading account or a specific account .
@@ -408,6 +409,8 @@ class Account(object):
408
409
  If not specified, the value of 60 000 (60 seconds) is applied.
409
410
  If the connection is not established within the specified time,
410
411
  the call is forcibly terminated and the exception is generated.
412
+ path (str, optional): The path to the MetaTrader 5 terminal executable file.
413
+ Defaults to None (e.g., "C:/Program Files/MetaTrader 5/terminal64.exe").
411
414
 
412
415
  Returns:
413
416
  - AccountInfo in the form of a Namedtuple structure.
@@ -419,6 +422,15 @@ class Account(object):
419
422
  # connect to the trade account specifying a password and a server
420
423
  if account is not None and password is not None and server is not None:
421
424
  try:
425
+ # If a path is provided, initialize the MT5 terminal with it
426
+ if path is not None:
427
+ check_mt5_connection(
428
+ path=path,
429
+ login=account,
430
+ password=password,
431
+ server=server,
432
+ timeout=timeout,
433
+ )
422
434
  authorized = mt5.login(
423
435
  account, password=password, server=server, timeout=timeout
424
436
  )
@@ -1079,6 +1091,39 @@ class Account(object):
1079
1091
  except Exception as e:
1080
1092
  raise_mt5_error(e)
1081
1093
 
1094
+ def calculate_profit(
1095
+ self,
1096
+ action: Literal["buy", "sell"],
1097
+ symbol: str,
1098
+ lot: float,
1099
+ price_open: float,
1100
+ price_close: float,
1101
+ ) -> float:
1102
+ """
1103
+ Calculate profit in the account currency for a specified trading operation.
1104
+
1105
+ Args:
1106
+ action (str): The trading action, either 'buy' or 'sell'.
1107
+ symbol (str): The symbol of the financial instrument.
1108
+ lot (float): The lot size of the order.
1109
+ price_open (float): The price at which to position was opened.
1110
+ price_close (float): The price at which to position was closed.
1111
+
1112
+ Returns:
1113
+ float: The profit value
1114
+
1115
+ Raises:
1116
+ MT5TerminalError: A specific exception based on the error code.
1117
+
1118
+ """
1119
+ actions = {"buy": mt5.ORDER_TYPE_BUY, "sell": mt5.ORDER_TYPE_SELL}
1120
+ try:
1121
+ return mt5.order_calc_profit(
1122
+ actions[action], symbol, lot, price_open, price_close
1123
+ )
1124
+ except Exception as e:
1125
+ raise_mt5_error(e)
1126
+
1082
1127
  def check_order(self, request: Dict[str, Any]) -> OrderCheckResult:
1083
1128
  """
1084
1129
  Check funds sufficiency for performing a required trading operation.
@@ -1527,3 +1572,29 @@ class Account(object):
1527
1572
  else:
1528
1573
  history_orders = [TradeOrder(**td._asdict()) for td in history_orders]
1529
1574
  return tuple(history_orders)
1575
+
1576
+ def get_today_deals(self, id, group=None) -> List[TradeDeal]:
1577
+ """
1578
+ Get all today deals for a specific symbol or group of symbols
1579
+
1580
+ Args:
1581
+ id (int): strategy or expert id
1582
+ group (str): Symbol or group or symbol
1583
+ Returns:
1584
+ List[TradeDeal]: List of today deals
1585
+ """
1586
+ date_from = datetime.now() - timedelta(days=2)
1587
+ history = (
1588
+ self.get_trades_history(date_from=date_from, group=group, to_df=False) or []
1589
+ )
1590
+ positions_ids = set([deal.position_id for deal in history if deal.magic == id])
1591
+ today_deals = []
1592
+ for position in positions_ids:
1593
+ deal = self.get_trades_history(
1594
+ date_from=date_from, position=position, to_df=False
1595
+ )
1596
+ if deal is not None and len(deal) == 2:
1597
+ deal_time = datetime.fromtimestamp(deal[1].time)
1598
+ if deal_time.date() == datetime.now().date():
1599
+ today_deals.append(deal[1])
1600
+ return today_deals