bbstrader 0.2.96__py3-none-any.whl → 0.2.98__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/__main__.py CHANGED
@@ -16,7 +16,7 @@ USAGE_TEXT = """
16
16
  Modules:
17
17
  copier: Copy trades from one MetaTrader account to another or multiple accounts
18
18
  backtest: Backtest a strategy, see bbstrader.btengine.backtest.run_backtest
19
- execution: Execute a strategy, see bbstrader.trading.execution.MT5ExecutionEngine
19
+ execution: Execute a strategy, see bbstrader.trading.execution.Mt5ExecutionEngine
20
20
 
21
21
  python -m bbstrader --run <module> --help for more information on the module
22
22
  """
@@ -26,6 +26,7 @@ FONT = pyfiglet.figlet_format("BBSTRADER", font="big")
26
26
 
27
27
  def main():
28
28
  print(Fore.BLUE + FONT)
29
+ print(Fore.WHITE + "")
29
30
  parser = argparse.ArgumentParser(
30
31
  prog="BBSTRADER",
31
32
  usage=USAGE_TEXT,
@@ -66,21 +66,21 @@ class DataHandler(metaclass=ABCMeta):
66
66
  """
67
67
  Returns the last bar updated.
68
68
  """
69
- raise NotImplementedError("Should implement get_latest_bar()")
69
+ pass
70
70
 
71
71
  @abstractmethod
72
72
  def get_latest_bars(self, symbol, N=1, df=True) -> pd.DataFrame | List[pd.Series]:
73
73
  """
74
74
  Returns the last N bars updated.
75
75
  """
76
- raise NotImplementedError("Should implement get_latest_bars()")
76
+ pass
77
77
 
78
78
  @abstractmethod
79
79
  def get_latest_bar_datetime(self, symbol) -> datetime | pd.Timestamp:
80
80
  """
81
81
  Returns a Python datetime object for the last bar.
82
82
  """
83
- raise NotImplementedError("Should implement get_latest_bar_datetime()")
83
+ pass
84
84
 
85
85
  @abstractmethod
86
86
  def get_latest_bar_value(self, symbol, val_type) -> float:
@@ -88,7 +88,7 @@ class DataHandler(metaclass=ABCMeta):
88
88
  Returns one of the Open, High, Low, Close, Adj Close, Volume or Returns
89
89
  from the last bar.
90
90
  """
91
- raise NotImplementedError("Should implement get_latest_bar_value()")
91
+ pass
92
92
 
93
93
  @abstractmethod
94
94
  def get_latest_bars_values(self, symbol, val_type, N=1) -> np.ndarray:
@@ -96,7 +96,7 @@ class DataHandler(metaclass=ABCMeta):
96
96
  Returns the last N bar values from the
97
97
  latest_symbol list, or N-k if less available.
98
98
  """
99
- raise NotImplementedError("Should implement get_latest_bars_values()")
99
+ pass
100
100
 
101
101
  @abstractmethod
102
102
  def update_bars(self):
@@ -105,7 +105,7 @@ class DataHandler(metaclass=ABCMeta):
105
105
  in a tuple OHLCVI format: (datetime, Open, High, Low,
106
106
  Close, Adj Close, Volume, Retruns).
107
107
  """
108
- raise NotImplementedError("Should implement update_bars()")
108
+ pass
109
109
 
110
110
 
111
111
  class BaseCSVDataHandler(DataHandler):
@@ -46,7 +46,7 @@ class ExecutionHandler(metaclass=ABCMeta):
46
46
  Args:
47
47
  event (OrderEvent): Contains an Event object with order information.
48
48
  """
49
- raise NotImplementedError("Should implement execute_order()")
49
+ pass
50
50
 
51
51
 
52
52
  class SimExecutionHandler(ExecutionHandler):
@@ -12,13 +12,13 @@ from loguru import logger
12
12
  from bbstrader.btengine.data import DataHandler
13
13
  from bbstrader.btengine.event import FillEvent, SignalEvent
14
14
  from bbstrader.config import BBSTRADER_DIR
15
- from bbstrader.core.utils import TradeSignal
16
15
  from bbstrader.metatrader.account import (
17
16
  Account,
18
17
  AdmiralMarktsGroup,
19
18
  PepperstoneGroupLimited,
20
19
  )
21
20
  from bbstrader.metatrader.rates import Rates
21
+ from bbstrader.metatrader.trade import TradeSignal
22
22
  from bbstrader.models.optimization import optimized_weights
23
23
 
24
24
  __all__ = ["Strategy", "MT5Strategy"]
@@ -55,7 +55,7 @@ class Strategy(metaclass=ABCMeta):
55
55
 
56
56
  @abstractmethod
57
57
  def calculate_signals(self, *args, **kwargs) -> List[TradeSignal]:
58
- raise NotImplementedError("Should implement calculate_signals()")
58
+ pass
59
59
 
60
60
  def check_pending_orders(self, *args, **kwargs): ...
61
61
  def get_update_from_portfolio(self, *args, **kwargs): ...
@@ -67,7 +67,7 @@ class MT5Strategy(Strategy):
67
67
  """
68
68
  A `MT5Strategy()` object is a subclass of `Strategy` that is used to
69
69
  calculate signals for the MetaTrader 5 trading platform. The signals
70
- are generated by the `MT5Strategy` object and sent to the the `MT5ExecutionEngine`
70
+ are generated by the `MT5Strategy` object and sent to the the `Mt5ExecutionEngine`
71
71
  for live trading and `MT5BacktestEngine` objects for backtesting.
72
72
  """
73
73
 
@@ -146,9 +146,9 @@ class MT5Strategy(Strategy):
146
146
  def _initialize_portfolio(self):
147
147
  positions = ["LONG", "SHORT"]
148
148
  orders = ["BLMT", "BSTP", "BSTPLMT", "SLMT", "SSTP", "SSTPLMT"]
149
+ self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
149
150
  self._positions: Dict[str, Dict[str, int | float]] = {}
150
151
  self._trades: Dict[str, Dict[str, int]] = {}
151
- self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
152
152
  for symbol in self.symbols:
153
153
  self._positions[symbol] = {}
154
154
  self._orders[symbol] = {}
@@ -206,6 +206,7 @@ class MT5Strategy(Strategy):
206
206
  - ``price``: The price at which to execute the action, used for pending orders.
207
207
  - ``stoplimit``: The stop-limit price for STOP-LIMIT orders, used for pending stop limit orders.
208
208
  - ``id``: The unique identifier for the strategy or order.
209
+ - ``comment``: An optional comment or description related to the trade signal.
209
210
  """
210
211
  pass
211
212
 
@@ -503,108 +504,96 @@ class MT5Strategy(Strategy):
503
504
  """
504
505
  Check for pending orders and handle them accordingly.
505
506
  """
506
- for symbol in self.symbols:
507
- dtime = self.data.get_latest_bar_datetime(symbol)
508
507
 
509
- def logmsg(order, type):
510
- return self.logger.info(
511
- f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
512
- f"PRICE @ {order.price}",
513
- custom_time=dtime,
514
- )
508
+ def logmsg(order, type, symbol, dtime):
509
+ return self.logger.info(
510
+ f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
511
+ f"PRICE @ {order.price}",
512
+ custom_time=dtime,
513
+ )
515
514
 
516
- for order in self._orders[symbol]["BLMT"].copy():
517
- if self.data.get_latest_bar_value(symbol, "close") <= order.price:
518
- self.buy_mkt(
519
- order.strategy_id, symbol, order.price, order.quantity, dtime
520
- )
515
+ def process_orders(order_type, condition, execute_fn, log_label, symbol, dtime):
516
+ for order in self._orders[symbol][order_type].copy():
517
+ if condition(order):
518
+ execute_fn(order)
521
519
  try:
522
- self._orders[symbol]["BLMT"].remove(order)
523
- assert order not in self._orders[symbol]["BLMT"]
524
- logmsg(order, "BUY LIMIT")
520
+ self._orders[symbol][order_type].remove(order)
521
+ assert order not in self._orders[symbol][order_type]
525
522
  except AssertionError:
526
- self._orders[symbol]["BLMT"] = [
527
- o for o in self._orders[symbol]["BLMT"] if o != order
523
+ self._orders[symbol][order_type] = [
524
+ o for o in self._orders[symbol][order_type] if o != order
528
525
  ]
529
- logmsg(order, "BUY LIMIT")
530
- for order in self._orders[symbol]["SLMT"].copy():
531
- if self.data.get_latest_bar_value(symbol, "close") >= order.price:
532
- self.sell_mkt(
533
- order.strategy_id, symbol, order.price, order.quantity, dtime
534
- )
535
- try:
536
- self._orders[symbol]["SLMT"].remove(order)
537
- assert order not in self._orders[symbol]["SLMT"]
538
- logmsg(order, "SELL LIMIT")
539
- except AssertionError:
540
- self._orders[symbol]["SLMT"] = [
541
- o for o in self._orders[symbol]["SLMT"] if o != order
542
- ]
543
- logmsg(order, "SELL LIMIT")
544
- for order in self._orders[symbol]["BSTP"].copy():
545
- if self.data.get_latest_bar_value(symbol, "close") >= order.price:
546
- self.buy_mkt(
547
- order.strategy_id, symbol, order.price, order.quantity, dtime
548
- )
549
- try:
550
- self._orders[symbol]["BSTP"].remove(order)
551
- assert order not in self._orders[symbol]["BSTP"]
552
- logmsg(order, "BUY STOP")
553
- except AssertionError:
554
- self._orders[symbol]["BSTP"] = [
555
- o for o in self._orders[symbol]["BSTP"] if o != order
556
- ]
557
- logmsg(order, "BUY STOP")
558
- for order in self._orders[symbol]["SSTP"].copy():
559
- if self.data.get_latest_bar_value(symbol, "close") <= order.price:
560
- self.sell_mkt(
561
- order.strategy_id, symbol, order.price, order.quantity, dtime
562
- )
563
- try:
564
- self._orders[symbol]["SSTP"].remove(order)
565
- assert order not in self._orders[symbol]["SSTP"]
566
- logmsg(order, "SELL STOP")
567
- except AssertionError:
568
- self._orders[symbol]["SSTP"] = [
569
- o for o in self._orders[symbol]["SSTP"] if o != order
570
- ]
571
- logmsg(order, "SELL STOP")
572
- for order in self._orders[symbol]["BSTPLMT"].copy():
573
- if self.data.get_latest_bar_value(symbol, "close") >= order.price:
574
- self.buy_limit(
575
- order.strategy_id,
576
- symbol,
577
- order.stoplimit,
578
- order.quantity,
579
- dtime,
580
- )
581
- try:
582
- self._orders[symbol]["BSTPLMT"].remove(order)
583
- assert order not in self._orders[symbol]["BSTPLMT"]
584
- logmsg(order, "BUY STOP LIMIT")
585
- except AssertionError:
586
- self._orders[symbol]["BSTPLMT"] = [
587
- o for o in self._orders[symbol]["BSTPLMT"] if o != order
588
- ]
589
- logmsg(order, "BUY STOP LIMIT")
590
- for order in self._orders[symbol]["SSTPLMT"].copy():
591
- if self.data.get_latest_bar_value(symbol, "close") <= order.price:
592
- self.sell_limit(
593
- order.strategy_id,
594
- symbol,
595
- order.stoplimit,
596
- order.quantity,
597
- dtime,
598
- )
599
- try:
600
- self._orders[symbol]["SSTPLMT"].remove(order)
601
- assert order not in self._orders[symbol]["SSTPLMT"]
602
- logmsg(order, "SELL STOP LIMIT")
603
- except AssertionError:
604
- self._orders[symbol]["SSTPLMT"] = [
605
- o for o in self._orders[symbol]["SSTPLMT"] if o != order
606
- ]
607
- logmsg(order, "SELL STOP LIMIT")
526
+ logmsg(order, log_label, symbol, dtime)
527
+
528
+ for symbol in self.symbols:
529
+ dtime = self.data.get_latest_bar_datetime(symbol)
530
+ latest_close = self.data.get_latest_bar_value(symbol, "close")
531
+
532
+ process_orders(
533
+ "BLMT",
534
+ lambda o: latest_close <= o.price,
535
+ lambda o: self.buy_mkt(
536
+ o.strategy_id, symbol, o.price, o.quantity, dtime
537
+ ),
538
+ "BUY LIMIT",
539
+ symbol,
540
+ dtime,
541
+ )
542
+
543
+ process_orders(
544
+ "SLMT",
545
+ lambda o: latest_close >= o.price,
546
+ lambda o: self.sell_mkt(
547
+ o.strategy_id, symbol, o.price, o.quantity, dtime
548
+ ),
549
+ "SELL LIMIT",
550
+ symbol,
551
+ dtime,
552
+ )
553
+
554
+ process_orders(
555
+ "BSTP",
556
+ lambda o: latest_close >= o.price,
557
+ lambda o: self.buy_mkt(
558
+ o.strategy_id, symbol, o.price, o.quantity, dtime
559
+ ),
560
+ "BUY STOP",
561
+ symbol,
562
+ dtime,
563
+ )
564
+
565
+ process_orders(
566
+ "SSTP",
567
+ lambda o: latest_close <= o.price,
568
+ lambda o: self.sell_mkt(
569
+ o.strategy_id, symbol, o.price, o.quantity, dtime
570
+ ),
571
+ "SELL STOP",
572
+ symbol,
573
+ dtime,
574
+ )
575
+
576
+ process_orders(
577
+ "BSTPLMT",
578
+ lambda o: latest_close >= o.price,
579
+ lambda o: self.buy_limit(
580
+ o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
581
+ ),
582
+ "BUY STOP LIMIT",
583
+ symbol,
584
+ dtime,
585
+ )
586
+
587
+ process_orders(
588
+ "SSTPLMT",
589
+ lambda o: latest_close <= o.price,
590
+ lambda o: self.sell_limit(
591
+ o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
592
+ ),
593
+ "SELL STOP LIMIT",
594
+ symbol,
595
+ dtime,
596
+ )
608
597
 
609
598
  @staticmethod
610
599
  def calculate_pct_change(current_price, lh_price):
bbstrader/core/utils.py CHANGED
@@ -2,8 +2,6 @@ import configparser
2
2
  import importlib
3
3
  import importlib.util
4
4
  import os
5
- from dataclasses import dataclass
6
- from enum import Enum
7
5
  from typing import Any, Dict, List
8
6
 
9
7
  __all__ = ["load_module", "load_class"]
@@ -90,59 +88,3 @@ def dict_from_ini(file_path, sections: str | List[str] = None) -> Dict[str, Any]
90
88
  except KeyError:
91
89
  raise KeyError(f"{section} not found in the {file_path} file")
92
90
  return ini_dict
93
-
94
-
95
- class TradeAction(Enum):
96
- """
97
- An enumeration class for trade actions.
98
- """
99
-
100
- BUY = "LONG"
101
- LONG = "LONG"
102
- SELL = "SHORT"
103
- EXIT = "EXIT"
104
- BMKT = "BMKT"
105
- SMKT = "SMKT"
106
- BLMT = "BLMT"
107
- SLMT = "SLMT"
108
- BSTP = "BSTP"
109
- SSTP = "SSTP"
110
- SHORT = "SHORT"
111
- BSTPLMT = "BSTPLMT"
112
- SSTPLMT = "SSTPLMT"
113
- EXIT_LONG = "EXIT_LONG"
114
- EXIT_SHORT = "EXIT_SHORT"
115
- EXIT_STOP = "EXIT_STOP"
116
- EXIT_LIMIT = "EXIT_LIMIT"
117
- EXIT_LONG_STOP = "EXIT_LONG_STOP"
118
- EXIT_LONG_LIMIT = "EXIT_LONG_LIMIT"
119
- EXIT_SHORT_STOP = "EXIT_SHORT_STOP"
120
- EXIT_SHORT_LIMIT = "EXIT_SHORT_LIMIT"
121
- EXIT_LONG_STOP_LIMIT = "EXIT_LONG_STOP_LIMIT"
122
- EXIT_SHORT_STOP_LIMIT = "EXIT_SHORT_STOP_LIMIT"
123
- EXIT_PROFITABLES = "EXIT_PROFITABLES"
124
- EXIT_LOSINGS = "EXIT_LOSINGS"
125
- EXIT_ALL_POSITIONS = "EXIT_ALL_POSITIONS"
126
- EXIT_ALL_ORDERS = "EXIT_ALL_ORDERS"
127
-
128
- def __str__(self):
129
- return self.value
130
-
131
-
132
- @dataclass()
133
- class TradeSignal:
134
- """
135
- A dataclass for storing trading signal.
136
- """
137
-
138
- id: int
139
- symbol: str
140
- action: TradeAction
141
- price: float = None
142
- stoplimit: float = None
143
-
144
- def __repr__(self):
145
- return (
146
- f"TradeSignal(id={self.id}, symbol='{self.symbol}', "
147
- f"action='{self.action.value}', price={self.price}, stoplimit={self.stoplimit})"
148
- )
@@ -9,6 +9,7 @@ from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
9
9
 
10
10
  from bbstrader.metatrader.utils import (
11
11
  AccountInfo,
12
+ BookInfo,
12
13
  InvalidBroker,
13
14
  OrderCheckResult,
14
15
  OrderSentResult,
@@ -188,10 +189,12 @@ def check_mt5_connection(**kwargs):
188
189
  except Exception:
189
190
  raise_mt5_error(INIT_MSG)
190
191
 
192
+
191
193
  def shutdown_mt5():
192
194
  """Close the connection to the MetaTrader 5 terminal."""
193
195
  mt5.shutdown()
194
-
196
+
197
+
195
198
  class Broker(object):
196
199
  def __init__(self, name: str = None, **kwargs):
197
200
  if name is None:
@@ -323,7 +326,7 @@ class Account(object):
323
326
  f"For {supported['FTMO'].name}, See [{ftmo_url}]\n"
324
327
  )
325
328
  raise InvalidBroker(message=msg)
326
-
329
+
327
330
  def shutdown(self):
328
331
  """Close the connection to the MetaTrader 5 terminal."""
329
332
  shutdown_mt5()
@@ -1037,6 +1040,29 @@ class Account(object):
1037
1040
  """
1038
1041
  self._show_info(self.get_tick_info, "tick", symbol=symbol)
1039
1042
 
1043
+ def get_market_book(self, symbol: str) -> Union[Tuple[BookInfo], None]:
1044
+ """
1045
+ Get the Market Depth content for a specific symbol.
1046
+ Args:
1047
+ symbol (str): Financial instrument name. Required unnamed parameter.
1048
+ The symbol name should be specified in the same format as in the Market Watch window.
1049
+
1050
+ Returns:
1051
+ The Market Depth content as a tuple from BookInfo entries featuring order type, price and volume in lots.
1052
+ Return None in case of an error.
1053
+
1054
+ Raises:
1055
+ MT5TerminalError: A specific exception based on the error code.
1056
+ """
1057
+ try:
1058
+ book = mt5.market_book_get(symbol)
1059
+ if book is None:
1060
+ return None
1061
+ else:
1062
+ return Tuple([BookInfo(**entry._asdict()) for entry in book])
1063
+ except Exception as e:
1064
+ raise_mt5_error(e)
1065
+
1040
1066
  def calculate_margin(
1041
1067
  self, action: Literal["buy", "sell"], symbol: str, lot: float, price: float
1042
1068
  ) -> float:
@@ -1299,8 +1325,7 @@ class Account(object):
1299
1325
  df.drop(["time_msc", "external_id"], axis=1, inplace=True)
1300
1326
  df.set_index("time", inplace=True)
1301
1327
  if save:
1302
- file = "trade_history.csv"
1303
- df.to_csv(file)
1328
+ df.to_csv("trades_history.csv")
1304
1329
  if to_df:
1305
1330
  return df
1306
1331
  else:
@@ -1507,8 +1532,7 @@ class Account(object):
1507
1532
  df["time_done"] = pd.to_datetime(df["time_done"], unit="s")
1508
1533
 
1509
1534
  if save:
1510
- file = "trade_history.csv"
1511
- df.to_csv(file)
1535
+ df.to_csv("orders_history.csv")
1512
1536
  if to_df:
1513
1537
  return df
1514
1538
  else:
@@ -302,16 +302,11 @@ class TradeCopier(object):
302
302
  if self.start_time is None or self.end_time is None:
303
303
  return True
304
304
  else:
305
- start_hour, start_minutes = self.start_time.split(":")
306
- end_hour, end_minutes = self.end_time.split(":")
307
- if int(start_hour) < datetime.now().hour < int(end_hour):
305
+ now = datetime.now()
306
+ start_time = datetime.strptime(self.start_time, "%H:%M").time()
307
+ end_time = datetime.strptime(self.end_time, "%H:%M").time()
308
+ if start_time <= now.time() <= end_time:
308
309
  return True
309
- elif datetime.now().hour == int(start_hour):
310
- if datetime.now().minute >= int(start_minutes):
311
- return True
312
- elif datetime.now().hour == int(end_hour):
313
- if datetime.now().minute < int(end_minutes):
314
- return True
315
310
  return False
316
311
 
317
312
  def copy_new_trade(
@@ -607,10 +602,11 @@ class TradeCopier(object):
607
602
  continue
608
603
  self.copy_orders(destination)
609
604
  self.copy_positions(destination)
610
- except Exception as e:
611
- self.log_error(e)
605
+ time.sleep(0.1)
612
606
  except KeyboardInterrupt:
613
607
  break
608
+ except Exception as e:
609
+ self.log_error(e)
614
610
  time.sleep(self.sleeptime)
615
611
 
616
612