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.

@@ -1,19 +1,22 @@
1
1
  import os
2
2
  import time
3
- from datetime import datetime
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timedelta
5
+ from enum import Enum
4
6
  from logging import Logger
5
7
  from pathlib import Path
6
8
  from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
7
9
 
8
10
  import pandas as pd
11
+ import quantstats as qs
9
12
  from loguru import logger as log
10
13
  from tabulate import tabulate
11
14
 
12
- from bbstrader.btengine.performance import create_sharpe_ratio
13
15
  from bbstrader.config import BBSTRADER_DIR, config_logger
14
16
  from bbstrader.metatrader.account import INIT_MSG, check_mt5_connection
15
17
  from bbstrader.metatrader.risk import RiskManagement
16
18
  from bbstrader.metatrader.utils import (
19
+ TradeDeal,
17
20
  TradePosition,
18
21
  raise_mt5_error,
19
22
  trade_retcode_message,
@@ -47,6 +50,107 @@ global LOGGER
47
50
  LOGGER = log
48
51
 
49
52
 
53
+ class TradeAction(Enum):
54
+ """
55
+ An enumeration class for trade actions.
56
+ """
57
+
58
+ BUY = "LONG"
59
+ SELL = "SHORT"
60
+ LONG = "LONG"
61
+ SHORT = "SHORT"
62
+ BMKT = "BMKT"
63
+ SMKT = "SMKT"
64
+ BLMT = "BLMT"
65
+ SLMT = "SLMT"
66
+ BSTP = "BSTP"
67
+ SSTP = "SSTP"
68
+ BSTPLMT = "BSTPLMT"
69
+ SSTPLMT = "SSTPLMT"
70
+ EXIT = "EXIT"
71
+ EXIT_LONG = "EXIT_LONG"
72
+ EXIT_SHORT = "EXIT_SHORT"
73
+ EXIT_STOP = "EXIT_STOP"
74
+ EXIT_LIMIT = "EXIT_LIMIT"
75
+ EXIT_LONG_STOP = "EXIT_LONG_STOP"
76
+ EXIT_LONG_LIMIT = "EXIT_LONG_LIMIT"
77
+ EXIT_SHORT_STOP = "EXIT_SHORT_STOP"
78
+ EXIT_SHORT_LIMIT = "EXIT_SHORT_LIMIT"
79
+ EXIT_LONG_STOP_LIMIT = "EXIT_LONG_STOP_LIMIT"
80
+ EXIT_SHORT_STOP_LIMIT = "EXIT_SHORT_STOP_LIMIT"
81
+ EXIT_PROFITABLES = "EXIT_PROFITABLES"
82
+ EXIT_LOSINGS = "EXIT_LOSINGS"
83
+ EXIT_ALL_POSITIONS = "EXIT_ALL_POSITIONS"
84
+ EXIT_ALL_ORDERS = "EXIT_ALL_ORDERS"
85
+
86
+ def __str__(self):
87
+ return self.value
88
+
89
+
90
+ @dataclass()
91
+ class TradeSignal:
92
+ """
93
+ Represents a trading signal generated by a trading system or strategy.
94
+
95
+ Attributes:
96
+ id (int):
97
+ A unique identifier for the trade signal or the strategy.
98
+
99
+ symbol (str):
100
+ The trading symbol (e.g., stock ticker, forex pair, crypto asset)
101
+ related to the signal.
102
+
103
+ action (TradeAction):
104
+ The trading action to perform.
105
+ Must be an instance of the `TradeAction` enum (e.g., BUY, SELL).
106
+
107
+ price (float, optional):
108
+ The price at which the trade should be executed.
109
+
110
+ stoplimit (float, optional):
111
+ A stop-limit price for the trade.
112
+ Must not be set without specifying a price.
113
+
114
+ comment (str, optional):
115
+ An optional comment or description related to the trade signal.
116
+ """
117
+
118
+ id: int
119
+ symbol: str
120
+ action: TradeAction
121
+ price: float = None
122
+ stoplimit: float = None
123
+ comment: str = None
124
+
125
+ def __post_init__(self):
126
+ if not isinstance(self.action, TradeAction):
127
+ raise TypeError(
128
+ f"action must be of type TradeAction, not {type(self.action)}"
129
+ )
130
+ if self.stoplimit is not None and self.price is None:
131
+ raise ValueError("stoplimit cannot be set without price")
132
+
133
+ def __repr__(self):
134
+ return (
135
+ f"TradeSignal(id={self.id}, symbol='{self.symbol}', action='{self.action.value}', "
136
+ f"price={self.price}, stoplimit={self.stoplimit}), comment='{self.comment}'"
137
+ )
138
+
139
+
140
+ Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
141
+ Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
142
+ Positions = Literal["all", "buy", "sell", "profitable", "losing"]
143
+ Orders = Literal[
144
+ "all",
145
+ "buy_stops",
146
+ "sell_stops",
147
+ "buy_limits",
148
+ "sell_limits",
149
+ "buy_stop_limits",
150
+ "sell_stop_limits",
151
+ ]
152
+
153
+
50
154
  class Trade(RiskManagement):
51
155
  """
52
156
  Extends the `RiskManagement` class to include specific trading operations,
@@ -149,15 +253,12 @@ class Trade(RiskManagement):
149
253
  See the ``bbstrader.metatrader.risk.RiskManagement`` class for more details on these parameters.
150
254
  See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
151
255
  """
152
- # Call the parent class constructor first
153
256
  super().__init__(
154
257
  symbol=symbol,
155
258
  start_time=start_time,
156
259
  finishing_time=finishing_time,
157
- **kwargs, # Pass kwargs to the parent constructor
260
+ **kwargs,
158
261
  )
159
-
160
- # Initialize Trade-specific attributes
161
262
  self.symbol = symbol
162
263
  self.expert_name = expert_name
163
264
  self.expert_id = expert_id
@@ -171,12 +272,6 @@ class Trade(RiskManagement):
171
272
  self.tf = kwargs.get("time_frame", "D1")
172
273
  self.kwargs = kwargs
173
274
 
174
- self.start_time_hour, self.start_time_minutes = self.start.split(":")
175
- self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
176
- ":"
177
- )
178
- self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
179
-
180
275
  self.buy_positions = []
181
276
  self.sell_positions = []
182
277
  self.opened_positions = []
@@ -390,18 +485,14 @@ class Trade(RiskManagement):
390
485
  session_data, headers=["Statistics", "Values"], tablefmt="outline"
391
486
  )
392
487
 
393
- # Print the formatted statistics
394
488
  if self.verbose:
395
489
  print("\n[======= Trading Session Statistics =======]")
396
490
  print(session_table)
397
491
 
398
- # Save to CSV if specified
399
- if save:
492
+ if save and stats["deals"] > 0:
400
493
  today_date = datetime.now().strftime("%Y%m%d%H%M%S")
401
- # Create a dictionary with the statistics
402
494
  statistics_dict = {item[0]: item[1] for item in session_data}
403
495
  stats_df = pd.DataFrame(statistics_dict, index=[0])
404
- # Create the directory if it doesn't exist
405
496
  dir = dir or ".sessions"
406
497
  os.makedirs(dir, exist_ok=True)
407
498
  if "." in self.symbol:
@@ -414,8 +505,6 @@ class Trade(RiskManagement):
414
505
  stats_df.to_csv(filepath, index=False)
415
506
  LOGGER.info(f"Session statistics saved to {filepath}")
416
507
 
417
- Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
418
-
419
508
  def open_buy_position(
420
509
  self,
421
510
  action: Buys = "BMKT",
@@ -437,7 +526,8 @@ class Trade(RiskManagement):
437
526
  action (str): `BMKT` for Market orders or `BLMT`,
438
527
  `BSTP`,`BSTPLMT` for pending orders
439
528
  price (float): The price at which to open an order
440
- stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
529
+ stoplimit (float): A price a pending Limit order is set at when the price reaches
530
+ the 'price' value (this condition is mandatory).
441
531
  The pending order is not passed to the trading system until that moment
442
532
  id (int): The strategy id or expert Id
443
533
  mm (bool): Weither to put stop loss and tp or not
@@ -504,8 +594,6 @@ class Trade(RiskManagement):
504
594
  }
505
595
  return type
506
596
 
507
- Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
508
-
509
597
  def open_sell_position(
510
598
  self,
511
599
  action: Sells = "SMKT",
@@ -527,7 +615,8 @@ class Trade(RiskManagement):
527
615
  action (str): `SMKT` for Market orders
528
616
  or ``SLMT``, ``SSTP``,``SSTPLMT`` for pending orders
529
617
  price (float): The price at which to open an order
530
- stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
618
+ stoplimit (float): A price a pending Limit order is set at when the price reaches
619
+ the 'price' value (this condition is mandatory).
531
620
  The pending order is not passed to the trading system until that moment
532
621
  id (int): The strategy id or expert Id
533
622
  mm (bool): Weither to put stop loss and tp or not
@@ -594,6 +683,8 @@ class Trade(RiskManagement):
594
683
  Args:
595
684
  comment (str): The comment for the closing position
596
685
  """
686
+ if self.copy_mode:
687
+ return True
597
688
  if self.days_end():
598
689
  return False
599
690
  elif not self.trading_time():
@@ -603,6 +694,9 @@ class Trade(RiskManagement):
603
694
  LOGGER.error(f"Account Risk not allowed, SYMBOL={self.symbol}")
604
695
  self._check(comment)
605
696
  return False
697
+ elif self.is_max_trades_reached():
698
+ LOGGER.error(f"Maximum trades reached for Today, SYMBOL={self.symbol}")
699
+ return False
606
700
  elif self.profit_target():
607
701
  self._check(f"Profit target Reached !!! SYMBOL={self.symbol}")
608
702
  return True
@@ -1273,13 +1367,16 @@ class Trade(RiskManagement):
1273
1367
  profit = 0.0
1274
1368
  balance = self.get_account_info().balance
1275
1369
  target = round((balance * self.target) / 100, 2)
1276
- if len(self.opened_positions) != 0:
1277
- for position in self.opened_positions:
1370
+ opened_positions = self.get_today_deals(group=self.symbol)
1371
+ if len(opened_positions) != 0:
1372
+ for position in opened_positions:
1278
1373
  time.sleep(0.1)
1279
1374
  # This return two TradeDeal Object,
1280
1375
  # The first one is the opening order
1281
1376
  # The second is the closing order
1282
- history = self.get_trades_history(position=position, to_df=False)
1377
+ history = self.get_trades_history(
1378
+ position=position.position_id, to_df=False
1379
+ )
1283
1380
  if history is not None and len(history) == 2:
1284
1381
  profit += history[1].profit
1285
1382
  commission += history[0].commission
@@ -1501,16 +1598,6 @@ class Trade(RiskManagement):
1501
1598
  f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}."
1502
1599
  )
1503
1600
 
1504
- Orders = Literal[
1505
- "all",
1506
- "buy_stops",
1507
- "sell_stops",
1508
- "buy_limits",
1509
- "sell_limits",
1510
- "buy_stop_limits",
1511
- "sell_stop_limits",
1512
- ]
1513
-
1514
1601
  def close_orders(
1515
1602
  self,
1516
1603
  order_type: Orders,
@@ -1545,8 +1632,6 @@ class Trade(RiskManagement):
1545
1632
  orders, "orders", self.close_order, order_type, id=id, comment=comment
1546
1633
  )
1547
1634
 
1548
- Positions = Literal["all", "buy", "sell", "profitable", "losing"]
1549
-
1550
1635
  def close_positions(
1551
1636
  self,
1552
1637
  position_type: Positions,
@@ -1582,6 +1667,49 @@ class Trade(RiskManagement):
1582
1667
  comment=comment,
1583
1668
  )
1584
1669
 
1670
+ def get_today_deals(self, group=None) -> List[TradeDeal]:
1671
+ """
1672
+ Get all today deals for a specific symbol or group of symbols
1673
+
1674
+ Args:
1675
+ group (str): Symbol or group or symbol
1676
+ Returns:
1677
+ List[TradeDeal]: List of today deals
1678
+ """
1679
+ date_from = datetime.now() - timedelta(days=2)
1680
+ history = (
1681
+ self.get_trades_history(date_from=date_from, group=group, to_df=False) or []
1682
+ )
1683
+ positions_ids = set(
1684
+ [deal.position_id for deal in history if deal.magic == self.expert_id]
1685
+ )
1686
+ today_deals = []
1687
+ for position in positions_ids:
1688
+ deal = self.get_trades_history(
1689
+ date_from=date_from, position=position, to_df=False
1690
+ )
1691
+ if deal is not None and len(deal) == 2:
1692
+ deal_time = datetime.fromtimestamp(deal[1].time)
1693
+ if deal_time.date() == datetime.now().date():
1694
+ today_deals.append(deal[1])
1695
+ return today_deals
1696
+
1697
+ def is_max_trades_reached(self) -> bool:
1698
+ """
1699
+ Check if the maximum number of trades for the day has been reached.
1700
+
1701
+ :return: bool
1702
+ """
1703
+ negative_deals = 0
1704
+ max_trades = self.max_trade()
1705
+ today_deals = self.get_today_deals(group=self.symbol)
1706
+ for deal in today_deals:
1707
+ if deal.profit < 0:
1708
+ negative_deals += 1
1709
+ if negative_deals >= max_trades:
1710
+ return True
1711
+ return False
1712
+
1585
1713
  def get_stats(self) -> Tuple[Dict[str, Any]]:
1586
1714
  """
1587
1715
  get some stats about the trading day and trading history
@@ -1594,11 +1722,14 @@ class Trade(RiskManagement):
1594
1722
  loss_trades = 0
1595
1723
  win_trades = 0
1596
1724
  balance = self.get_account_info().balance
1597
- deals = len(self.opened_positions)
1725
+ today_deals = self.get_today_deals(group=self.symbol)
1726
+ deals = len(today_deals)
1598
1727
  if deals != 0:
1599
- for position in self.opened_positions:
1728
+ for position in today_deals:
1600
1729
  time.sleep(0.1)
1601
- history = self.get_trades_history(position=position, to_df=False)
1730
+ history = self.get_trades_history(
1731
+ position=position.position_id, to_df=False
1732
+ )
1602
1733
  if history is not None and len(history) == 2:
1603
1734
  result = history[1].profit
1604
1735
  comm = history[0].commission
@@ -1641,13 +1772,12 @@ class Trade(RiskManagement):
1641
1772
  _fees = df2["fee"].sum()
1642
1773
  _swap = df2["swap"].sum()
1643
1774
  total_profit = commisions + _fees + _swap + profit
1644
- account_info = self.get_account_info()
1645
- balance = account_info.balance
1775
+ balance = self.get_account_info().balance
1646
1776
  initial_balance = balance - total_profit
1647
1777
  profittable = "Yes" if balance > initial_balance else "No"
1648
1778
  stats2 = {"total_profit": total_profit, "profitability": profittable}
1649
1779
  else:
1650
- stats2 = {"total_profit": 0, "profitability": 0}
1780
+ stats2 = {"total_profit": 0, "profitability": "No"}
1651
1781
  return (stats1, stats2)
1652
1782
 
1653
1783
  def sharpe(self):
@@ -1657,7 +1787,9 @@ class Trade(RiskManagement):
1657
1787
  The function assumes that the returns are the excess of
1658
1788
  those compared to a benchmark.
1659
1789
  """
1660
- # Get total history
1790
+ import warnings
1791
+
1792
+ warnings.filterwarnings("ignore")
1661
1793
  df2 = self.get_trades_history()
1662
1794
  if df2 is None:
1663
1795
  return 0.0
@@ -1665,39 +1797,25 @@ class Trade(RiskManagement):
1665
1797
  profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
1666
1798
  returns = profit.pct_change(fill_method=None)
1667
1799
  periods = self.max_trade() * 252
1668
- sharpe = create_sharpe_ratio(returns, periods=periods)
1800
+ sharpe = qs.stats.sharpe(returns, periods=periods)
1669
1801
 
1670
1802
  return round(sharpe, 3)
1671
1803
 
1672
1804
  def days_end(self) -> bool:
1673
1805
  """Check if it is the end of the trading day."""
1674
- current_hour = datetime.now().hour
1675
- current_minute = datetime.now().minute
1676
-
1677
- ending_hour = int(self.ending_time_hour)
1678
- ending_minute = int(self.ending_time_minutes)
1679
-
1680
- if current_hour > ending_hour or (
1681
- current_hour == ending_hour and current_minute >= ending_minute
1682
- ):
1806
+ now = datetime.now()
1807
+ end = datetime.strptime(self.end, "%H:%M").time()
1808
+ if now.time() >= end:
1683
1809
  return True
1684
- else:
1685
- return False
1810
+ return False
1686
1811
 
1687
1812
  def trading_time(self):
1688
1813
  """Check if it is time to trade."""
1689
- if (
1690
- int(self.start_time_hour)
1691
- < datetime.now().hour
1692
- < int(self.finishing_time_hour)
1693
- ):
1814
+ now = datetime.now()
1815
+ start = datetime.strptime(self.start, "%H:%M").time()
1816
+ end = datetime.strptime(self.finishing, "%H:%M").time()
1817
+ if start <= now.time() <= end:
1694
1818
  return True
1695
- elif datetime.now().hour == int(self.start_time_hour):
1696
- if datetime.now().minute >= int(self.start_time_minutes):
1697
- return True
1698
- elif datetime.now().hour == int(self.finishing_time_hour):
1699
- if datetime.now().minute < int(self.finishing_time_minutes):
1700
- return True
1701
1819
  return False
1702
1820
 
1703
1821
  def sleep_time(self, weekend=False):
@@ -286,6 +286,22 @@ class TickInfo(NamedTuple):
286
286
  volume_real: float
287
287
 
288
288
 
289
+ class BookInfo(NamedTuple):
290
+ """
291
+ Represents the structure of a book.
292
+ * type: Type of the order (buy/sell)
293
+ * price: Price of the order
294
+ * volume: Volume of the order in lots
295
+ * volume_dbl: Volume with greater accuracy
296
+
297
+ """
298
+
299
+ type: int
300
+ price: float
301
+ volume: float
302
+ volume_dbl: float
303
+
304
+
289
305
  class TradeRequest(NamedTuple):
290
306
  """
291
307
  Represents a Trade Request Structure
bbstrader/models/risk.py CHANGED
@@ -68,7 +68,7 @@ class RiskModel(metaclass=ABCMeta):
68
68
  such as historical returns or volatility, used to
69
69
  assess market conditions.
70
70
  """
71
- raise NotImplementedError("Should implement which_trade_allowed()")
71
+ pass
72
72
 
73
73
  @abstractmethod
74
74
  def which_quantity_allowed(self):
@@ -76,7 +76,7 @@ class RiskModel(metaclass=ABCMeta):
76
76
  Defines the strategy for asset allocation within
77
77
  the portfolio to optimize risk-reward ratio.
78
78
  """
79
- raise NotImplementedError("Should implement which_quantity_allowed()")
79
+ pass
80
80
 
81
81
 
82
82
  class HMMRiskManager(RiskModel):