bbstrader 0.2.991__py3-none-any.whl → 0.3.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.

@@ -33,12 +33,6 @@ __all__ = [
33
33
  "create_trade_instance",
34
34
  ]
35
35
 
36
- FILLING_TYPE = [
37
- Mt5.ORDER_FILLING_IOC,
38
- Mt5.ORDER_FILLING_RETURN,
39
- Mt5.ORDER_FILLING_BOC,
40
- ]
41
-
42
36
  log.add(
43
37
  f"{BBSTRADER_DIR}/logs/trade.log",
44
38
  enqueue=True,
@@ -50,6 +44,13 @@ global LOGGER
50
44
  LOGGER = log
51
45
 
52
46
 
47
+ FILLING_TYPE = [
48
+ Mt5.ORDER_FILLING_IOC,
49
+ Mt5.ORDER_FILLING_RETURN,
50
+ Mt5.ORDER_FILLING_BOC,
51
+ ]
52
+
53
+
53
54
  class TradeAction(Enum):
54
55
  """
55
56
  An enumeration class for trade actions.
@@ -133,10 +134,21 @@ class TradeSignal:
133
134
  def __repr__(self):
134
135
  return (
135
136
  f"TradeSignal(id={self.id}, symbol='{self.symbol}', action='{self.action.value}', "
136
- f"price={self.price}, stoplimit={self.stoplimit}), comment='{self.comment}'"
137
+ f"price={self.price}, stoplimit={self.stoplimit}, comment='{self.comment or ''}')"
137
138
  )
138
139
 
139
140
 
141
+ class TradingMode(Enum):
142
+ BACKTEST = "BACKTEST"
143
+ LIVE = "LIVE"
144
+
145
+ def isbacktest(self) -> bool:
146
+ return self == TradingMode.BACKTEST
147
+
148
+ def islive(self) -> bool:
149
+ return self == TradingMode.LIVE
150
+
151
+
140
152
  Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
141
153
  Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
142
154
  Positions = Literal["all", "buy", "sell", "profitable", "losing"]
@@ -152,6 +164,7 @@ Orders = Literal[
152
164
 
153
165
  EXPERT_ID = 98181105
154
166
 
167
+
155
168
  class Trade(RiskManagement):
156
169
  """
157
170
  Extends the `RiskManagement` class to include specific trading operations,
@@ -729,10 +742,8 @@ class Trade(RiskManagement):
729
742
  self.check_order(request)
730
743
  result = self.send_order(request)
731
744
  except Exception as e:
732
- print(f"{self.current_datetime()} -", end=" ")
733
- trade_retcode_message(
734
- result.retcode, display=True, add_msg=f"{e}{addtionnal}"
735
- )
745
+ msg = trade_retcode_message(result.retcode)
746
+ LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
736
747
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
737
748
  if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
738
749
  for fill in FILLING_TYPE:
@@ -762,10 +773,8 @@ class Trade(RiskManagement):
762
773
  self.check_order(request)
763
774
  result = self.send_order(request)
764
775
  except Exception as e:
765
- print(f"{self.current_datetime()} -", end=" ")
766
- trade_retcode_message(
767
- result.retcode, display=True, add_msg=f"{e}{addtionnal}"
768
- )
776
+ msg = trade_retcode_message(result.retcode)
777
+ LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
769
778
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
770
779
  break
771
780
  tries += 1
@@ -776,7 +785,7 @@ class Trade(RiskManagement):
776
785
  if type != "BMKT" or type != "SMKT":
777
786
  self.opened_orders.append(result.order)
778
787
  long_msg = (
779
- f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{price}, "
788
+ f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{round(price, 5)}, "
780
789
  f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
781
790
  f"Tp: {self.get_take_profit()}"
782
791
  )
@@ -797,7 +806,7 @@ class Trade(RiskManagement):
797
806
  profit = round(self.get_account_info().profit, 5)
798
807
  order_info = (
799
808
  f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open, 5)}, "
800
- f"Sl: @{position.sl} Tp: @{position.tp}"
809
+ f"Sl: @{round(position.sl, 5)} Tp: @{round(position.tp, 5)}"
801
810
  )
802
811
  LOGGER.info(order_info)
803
812
  pos_info = (
@@ -834,7 +843,7 @@ class Trade(RiskManagement):
834
843
  action (str): (`'BMKT'`, `'SMKT'`) for Market orders
835
844
  or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
836
845
  price (float): The price at which to open an order
837
- stoplimit (float): A price a pending Limit order is set at
846
+ stoplimit (float): A price a pending Limit order is set at
838
847
  when the price reaches the 'price' value (this condition is mandatory).
839
848
  The pending order is not passed to the trading system until that moment
840
849
  id (int): The strategy id or expert Id
@@ -873,30 +882,30 @@ class Trade(RiskManagement):
873
882
  @property
874
883
  def orders(self):
875
884
  """Return all opened order's tickets"""
876
- if len(self.opened_orders) != 0:
877
- return self.opened_orders
878
- return None
885
+ current_orders = self.get_current_orders() or []
886
+ opened_orders = set(current_orders + self.opened_orders)
887
+ return list(opened_orders) if len(opened_orders) != 0 else None
879
888
 
880
889
  @property
881
890
  def positions(self):
882
891
  """Return all opened position's tickets"""
883
- if len(self.opened_positions) != 0:
884
- return self.opened_positions
885
- return None
892
+ current_positions = self.get_current_positions() or []
893
+ opened_positions = set(current_positions + self.opened_positions)
894
+ return list(opened_positions) if len(opened_positions) != 0 else None
886
895
 
887
896
  @property
888
897
  def buypos(self):
889
898
  """Return all buy opened position's tickets"""
890
- if len(self.buy_positions) != 0:
891
- return self.buy_positions
892
- return None
899
+ buy_positions = self.get_current_buys() or []
900
+ buy_positions = set(buy_positions + self.buy_positions)
901
+ return list(buy_positions) if len(buy_positions) != 0 else None
893
902
 
894
903
  @property
895
904
  def sellpos(self):
896
905
  """Return all sell opened position's tickets"""
897
- if len(self.sell_positions) != 0:
898
- return self.sell_positions
899
- return None
906
+ sell_positions = self.get_current_sells() or []
907
+ sell_positions = set(sell_positions + self.sell_positions)
908
+ return list(sell_positions) if len(sell_positions) != 0 else None
900
909
 
901
910
  @property
902
911
  def bepos(self):
@@ -1214,7 +1223,7 @@ class Trade(RiskManagement):
1214
1223
  Sets the break-even level for a given trading position.
1215
1224
 
1216
1225
  Args:
1217
- position (TradePosition): The trading position for which the break-even is to be set.
1226
+ position (TradePosition): The trading position for which the break-even is to be set.
1218
1227
  This is the value return by `mt5.positions_get()`.
1219
1228
  be (int): The break-even level in points.
1220
1229
  level (float): The break-even level in price, if set to None , it will be calated automaticaly.
@@ -1283,10 +1292,8 @@ class Trade(RiskManagement):
1283
1292
  self.check_order(request)
1284
1293
  result = self.send_order(request)
1285
1294
  except Exception as e:
1286
- print(f"{self.current_datetime()} -", end=" ")
1287
- trade_retcode_message(
1288
- result.retcode, display=True, add_msg=f"{e}{addtionnal}"
1289
- )
1295
+ msg = trade_retcode_message(result.retcode)
1296
+ LOGGER.error(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
1290
1297
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
1291
1298
  msg = trade_retcode_message(result.retcode)
1292
1299
  if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
@@ -1303,17 +1310,15 @@ class Trade(RiskManagement):
1303
1310
  self.check_order(request)
1304
1311
  result = self.send_order(request)
1305
1312
  except Exception as e:
1306
- print(f"{self.current_datetime()} -", end=" ")
1307
- trade_retcode_message(
1308
- result.retcode, display=True, add_msg=f"{e}{addtionnal}"
1309
- )
1313
+ msg = trade_retcode_message(result.retcode)
1314
+ LOGGER.error(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
1310
1315
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
1311
1316
  break
1312
1317
  tries += 1
1313
1318
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
1314
1319
  msg = trade_retcode_message(result.retcode)
1315
1320
  LOGGER.info(f"Break-Even Order {msg}{addtionnal}")
1316
- info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
1321
+ info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{round(price, 5)}"
1317
1322
  LOGGER.info(info)
1318
1323
  self.break_even_status.append(tiket)
1319
1324
 
@@ -1391,10 +1396,8 @@ class Trade(RiskManagement):
1391
1396
  self.check_order(request)
1392
1397
  result = self.send_order(request)
1393
1398
  except Exception as e:
1394
- print(f"{self.current_datetime()} -", end=" ")
1395
- trade_retcode_message(
1396
- result.retcode, display=True, add_msg=f"{e}{addtionnal}"
1397
- )
1399
+ msg = trade_retcode_message(result.retcode)
1400
+ LOGGER.error(f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}")
1398
1401
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
1399
1402
  if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
1400
1403
  for fill in FILLING_TYPE:
@@ -1406,7 +1409,8 @@ class Trade(RiskManagement):
1406
1409
  self._retcodes.append(result.retcode)
1407
1410
  msg = trade_retcode_message(result.retcode)
1408
1411
  LOGGER.error(
1409
- f"Closing Order Request, {type.capitalize()}: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}"
1412
+ f"Closing Order Request, {type.capitalize()}: #{ticket}, "
1413
+ f"RETCODE={result.retcode}: {msg}{addtionnal}"
1410
1414
  )
1411
1415
  else:
1412
1416
  tries = 0
@@ -1416,17 +1420,18 @@ class Trade(RiskManagement):
1416
1420
  self.check_order(request)
1417
1421
  result = self.send_order(request)
1418
1422
  except Exception as e:
1419
- print(f"{self.current_datetime()} -", end=" ")
1420
- trade_retcode_message(
1421
- result.retcode, display=True, add_msg=f"{e}{addtionnal}"
1422
- )
1423
+ msg = trade_retcode_message(result.retcode)
1424
+ LOGGER.error(f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}")
1423
1425
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
1424
1426
  break
1425
1427
  tries += 1
1426
1428
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
1427
1429
  msg = trade_retcode_message(result.retcode)
1428
1430
  LOGGER.info(f"Closing Order {msg}{addtionnal}")
1429
- info = f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol}, Price: @{request.get('price', 0.0)}"
1431
+ info = (
1432
+ f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol},"
1433
+ f"Price: @{round(request.get('price', 0.0), 5)}"
1434
+ )
1430
1435
  LOGGER.info(info)
1431
1436
  return True
1432
1437
  else:
@@ -1446,7 +1451,7 @@ class Trade(RiskManagement):
1446
1451
  Args:
1447
1452
  ticket (int): Order ticket to modify (e.g TradeOrder.ticket)
1448
1453
  price (float): The price at which to modify the order
1449
- stoplimit (float): A price a pending Limit order is set at
1454
+ stoplimit (float): A price a pending Limit order is set at
1450
1455
  when the price reaches the 'price' value (this condition is mandatory).
1451
1456
  The pending order is not passed to the trading system until that moment
1452
1457
  sl (float): The stop loss in points
@@ -1455,7 +1460,7 @@ class Trade(RiskManagement):
1455
1460
  orders = self.get_orders(ticket=ticket) or []
1456
1461
  if len(orders) == 0:
1457
1462
  LOGGER.error(
1458
- f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={price}"
1463
+ f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={round(price, 5)}"
1459
1464
  )
1460
1465
  return
1461
1466
  order = orders[0]
@@ -1471,7 +1476,8 @@ class Trade(RiskManagement):
1471
1476
  result = self.send_order(request)
1472
1477
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
1473
1478
  LOGGER.info(
1474
- f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={price}, SL={sl}, TP={tp}, STOP_LIMIT={stoplimit}"
1479
+ f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={round(price, 5)},"
1480
+ f"SL={round(sl, 5)}, TP={round(tp, 5)}, STOP_LIMIT={round(stoplimit, 5)}"
1475
1481
  )
1476
1482
  else:
1477
1483
  msg = trade_retcode_message(result.retcode)
@@ -1597,7 +1603,7 @@ class Trade(RiskManagement):
1597
1603
  ):
1598
1604
  """
1599
1605
  Args:
1600
- order_type (str): Type of orders to close
1606
+ order_type (str): Type of orders to close
1601
1607
  ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
1602
1608
  id (int): The unique ID of the Expert or Strategy
1603
1609
  comment (str): Comment for the closing position
@@ -14,6 +14,7 @@ __all__ = [
14
14
  "TerminalInfo",
15
15
  "AccountInfo",
16
16
  "SymbolInfo",
17
+ "SymbolType",
17
18
  "TickInfo",
18
19
  "TradeRequest",
19
20
  "OrderCheckResult",
@@ -70,27 +71,31 @@ class TimeFrame(Enum):
70
71
  Rrepresent a time frame object
71
72
  """
72
73
 
73
- M1 = "1m"
74
- M2 = "2m"
75
- M3 = "3m"
76
- M4 = "4m"
77
- M5 = "5m"
78
- M6 = "6m"
79
- M10 = "10m"
80
- M12 = "12m"
81
- M15 = "15m"
82
- M20 = "20m"
83
- M30 = "30m"
84
- H1 = "1h"
85
- H2 = "2h"
86
- H3 = "3h"
87
- H4 = "4h"
88
- H6 = "6h"
89
- H8 = "8h"
90
- H12 = "12h"
91
- D1 = "D1"
92
- W1 = "W1"
93
- MN1 = "MN1"
74
+ M1 = TIMEFRAMES["1m"]
75
+ M2 = TIMEFRAMES["2m"]
76
+ M3 = TIMEFRAMES["3m"]
77
+ M4 = TIMEFRAMES["4m"]
78
+ M5 = TIMEFRAMES["5m"]
79
+ M6 = TIMEFRAMES["6m"]
80
+ M10 = TIMEFRAMES["10m"]
81
+ M12 = TIMEFRAMES["12m"]
82
+ M15 = TIMEFRAMES["15m"]
83
+ M20 = TIMEFRAMES["20m"]
84
+ M30 = TIMEFRAMES["30m"]
85
+ H1 = TIMEFRAMES["1h"]
86
+ H2 = TIMEFRAMES["2h"]
87
+ H3 = TIMEFRAMES["3h"]
88
+ H4 = TIMEFRAMES["4h"]
89
+ H6 = TIMEFRAMES["6h"]
90
+ H8 = TIMEFRAMES["8h"]
91
+ H12 = TIMEFRAMES["12h"]
92
+ D1 = TIMEFRAMES["D1"]
93
+ W1 = TIMEFRAMES["W1"]
94
+ MN1 = TIMEFRAMES["MN1"]
95
+
96
+ def __str__(self):
97
+ """Return the string representation of the time frame."""
98
+ return self.name
94
99
 
95
100
 
96
101
  class TerminalInfo(NamedTuple):
@@ -263,6 +268,23 @@ class SymbolInfo(NamedTuple):
263
268
  path: str
264
269
 
265
270
 
271
+ class SymbolType(Enum):
272
+ """
273
+ Represents the type of a symbol.
274
+ """
275
+
276
+ FOREX = "FOREX" # Forex currency pairs
277
+ FUTURES = "FUTURES" # Futures contracts
278
+ STOCKS = "STOCKS" # Stocks and shares
279
+ BONDS = "BONDS" # Bonds
280
+ CRYPTO = "CRYPTO" # Cryptocurrencies
281
+ ETFs = "ETFs" # Exchange-Traded Funds
282
+ INDICES = "INDICES" # Market indices
283
+ COMMODITIES = "COMMODITIES" # Commodities
284
+ OPTIONS = "OPTIONS" # Options contracts
285
+ unknown = "UNKNOWN" # Unknown or unsupported type
286
+
287
+
266
288
  class TickInfo(NamedTuple):
267
289
  """
268
290
  Represents the last tick for the specified financial instrument.
@@ -465,10 +487,12 @@ class MT5TerminalError(Exception):
465
487
  self.message = message
466
488
 
467
489
  def __str__(self) -> str:
468
- if self.message is None:
469
- return f"{self.__class__.__name__}"
470
- else:
471
- return f"{self.__class__.__name__}, {self.message}"
490
+ # if self.message is None:
491
+ # return f"{self.__class__.__name__}"
492
+ # else:
493
+ # return f"{self.__class__.__name__}, {self.message}"
494
+ msg_str = str(self.message) if self.message is not None else ""
495
+ return f"{self.code} - {self.__class__.__name__}: {msg_str}"
472
496
 
473
497
 
474
498
  class GenericFail(MT5TerminalError):
@@ -561,6 +585,21 @@ class InternalFailTimeout(InternalFailError):
561
585
  super().__init__(MT5.RES_E_INTERNAL_FAIL_TIMEOUT, message)
562
586
 
563
587
 
588
+ RES_E_FAIL = 1 # Generic error
589
+ RES_E_INVALID_PARAMS = 2 # Invalid parameters
590
+ RES_E_NOT_FOUND = 3 # Not found
591
+ RES_E_INVALID_VERSION = 4 # Invalid version
592
+ RES_E_AUTH_FAILED = 5 # Authorization failed
593
+ RES_E_UNSUPPORTED = 6 # Unsupported method
594
+ RES_E_AUTO_TRADING_DISABLED = 7 # Autotrading disabled
595
+
596
+ # Actual internal error codes from MetaTrader5
597
+ RES_E_INTERNAL_FAIL_CONNECT = -10000
598
+ RES_E_INTERNAL_FAIL_INIT = -10001
599
+ RES_E_INTERNAL_FAIL_SEND = -10006
600
+ RES_E_INTERNAL_FAIL_RECEIVE = -10007
601
+ RES_E_INTERNAL_FAIL_TIMEOUT = -10008
602
+
564
603
  # Dictionary to map error codes to exception classes
565
604
  _ERROR_CODE_TO_EXCEPTION_ = {
566
605
  MT5.RES_E_FAIL: GenericFail,
@@ -575,6 +614,18 @@ _ERROR_CODE_TO_EXCEPTION_ = {
575
614
  MT5.RES_E_INTERNAL_FAIL_INIT: InternalFailInit,
576
615
  MT5.RES_E_INTERNAL_FAIL_CONNECT: InternalFailConnect,
577
616
  MT5.RES_E_INTERNAL_FAIL_TIMEOUT: InternalFailTimeout,
617
+ RES_E_FAIL: GenericFail,
618
+ RES_E_INVALID_PARAMS: InvalidParams,
619
+ RES_E_NOT_FOUND: HistoryNotFound,
620
+ RES_E_INVALID_VERSION: InvalidVersion,
621
+ RES_E_AUTH_FAILED: AuthFailed,
622
+ RES_E_UNSUPPORTED: UnsupportedMethod,
623
+ RES_E_AUTO_TRADING_DISABLED: AutoTradingDisabled,
624
+ RES_E_INTERNAL_FAIL_SEND: InternalFailSend,
625
+ RES_E_INTERNAL_FAIL_RECEIVE: InternalFailReceive,
626
+ RES_E_INTERNAL_FAIL_INIT: InternalFailInit,
627
+ RES_E_INTERNAL_FAIL_CONNECT: InternalFailConnect,
628
+ RES_E_INTERNAL_FAIL_TIMEOUT: InternalFailTimeout,
578
629
  }
579
630
 
580
631
 
@@ -588,7 +639,10 @@ def raise_mt5_error(message: Optional[str] = None):
588
639
  MT5TerminalError: A specific exception based on the error code.
589
640
  """
590
641
  error = _ERROR_CODE_TO_EXCEPTION_.get(MT5.last_error()[0])
591
- raise Exception(f"{error(None)} {message or MT5.last_error()[1]}")
642
+ if error is not None:
643
+ raise Exception(f"{error(None)} {message or MT5.last_error()[1]}")
644
+ else:
645
+ raise Exception(f"{message or MT5.last_error()[1]}")
592
646
 
593
647
 
594
648
  _ORDER_FILLING_TYPE_ = "https://www.mql5.com/en/docs/constants/tradingconstants/orderproperties#enum_order_type_filling"
@@ -28,8 +28,10 @@ def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
28
28
  end=end,
29
29
  progress=False,
30
30
  multi_level_index=False,
31
+ auto_adjust=True,
31
32
  )
32
- data = data.drop(columns=["Adj Close"], axis=1)
33
+ if "Adj Close" in data.columns:
34
+ data = data.drop(columns=["Adj Close"], axis=1)
33
35
  elif source == "mt5":
34
36
  start, end = pd.Timestamp(start), pd.Timestamp(end)
35
37
  data = download_historical_data(
bbstrader/models/ml.py CHANGED
@@ -250,12 +250,13 @@ class LightGBModel(object):
250
250
  data = pd.concat(data)
251
251
  data = (
252
252
  data.rename(columns={s: s.lower().replace(" ", "_") for s in data.columns})
253
- .drop(columns=["adj_close"])
254
253
  .set_index("symbol", append=True)
255
254
  .swaplevel()
256
255
  .sort_index()
257
256
  .dropna()
258
257
  )
258
+ if "adj_close" in data.columns:
259
+ data = data.drop(columns=["adj_close"])
259
260
  return data
260
261
 
261
262
  def download_metadata(self, tickers):