bbstrader 0.2.95__py3-none-any.whl → 0.2.97__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,93 @@ 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
+
50
140
  class Trade(RiskManagement):
51
141
  """
52
142
  Extends the `RiskManagement` class to include specific trading operations,
@@ -437,7 +527,8 @@ class Trade(RiskManagement):
437
527
  action (str): `BMKT` for Market orders or `BLMT`,
438
528
  `BSTP`,`BSTPLMT` for pending orders
439
529
  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).
530
+ stoplimit (float): A price a pending Limit order is set at when the price reaches
531
+ the 'price' value (this condition is mandatory).
441
532
  The pending order is not passed to the trading system until that moment
442
533
  id (int): The strategy id or expert Id
443
534
  mm (bool): Weither to put stop loss and tp or not
@@ -527,7 +618,8 @@ class Trade(RiskManagement):
527
618
  action (str): `SMKT` for Market orders
528
619
  or ``SLMT``, ``SSTP``,``SSTPLMT`` for pending orders
529
620
  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).
621
+ stoplimit (float): A price a pending Limit order is set at when the price reaches
622
+ the 'price' value (this condition is mandatory).
531
623
  The pending order is not passed to the trading system until that moment
532
624
  id (int): The strategy id or expert Id
533
625
  mm (bool): Weither to put stop loss and tp or not
@@ -594,6 +686,8 @@ class Trade(RiskManagement):
594
686
  Args:
595
687
  comment (str): The comment for the closing position
596
688
  """
689
+ if self.copy_mode:
690
+ return True
597
691
  if self.days_end():
598
692
  return False
599
693
  elif not self.trading_time():
@@ -603,6 +697,9 @@ class Trade(RiskManagement):
603
697
  LOGGER.error(f"Account Risk not allowed, SYMBOL={self.symbol}")
604
698
  self._check(comment)
605
699
  return False
700
+ elif self.is_max_trades_reached():
701
+ LOGGER.error(f"Maximum trades reached for Today, SYMBOL={self.symbol}")
702
+ return False
606
703
  elif self.profit_target():
607
704
  self._check(f"Profit target Reached !!! SYMBOL={self.symbol}")
608
705
  return True
@@ -1582,6 +1679,50 @@ class Trade(RiskManagement):
1582
1679
  comment=comment,
1583
1680
  )
1584
1681
 
1682
+ def get_today_deals(self, group=None) -> List[TradeDeal]:
1683
+ """
1684
+ Get all today deals for a specific symbol or group of symbols
1685
+
1686
+ Args:
1687
+ group (str): Symbol or group or symbol
1688
+ Returns:
1689
+ List[TradeDeal]: List of today deals
1690
+ """
1691
+ date_from = datetime.now() - timedelta(days=2)
1692
+ history = self.get_trades_history(date_from=date_from, group=group, to_df=False)
1693
+ positions_ids = set(
1694
+ [
1695
+ deal.position_id
1696
+ for deal in history
1697
+ if history is not None and deal.magic == self.expert_id
1698
+ ]
1699
+ )
1700
+ today_deals = []
1701
+ for position in positions_ids:
1702
+ deal = self.get_trades_history(
1703
+ date_from=date_from, position=position, to_df=False
1704
+ )
1705
+ if deal is not None and len(deal) == 2:
1706
+ deal_time = datetime.fromtimestamp(deal[1].time)
1707
+ if deal_time.date() == datetime.now().date():
1708
+ today_deals.append(deal[1])
1709
+ return today_deals
1710
+
1711
+ def is_max_trades_reached(self) -> bool:
1712
+ """
1713
+ Check if the maximum number of trades for the day has been reached.
1714
+
1715
+ :return: bool
1716
+ """
1717
+ negative_deals = 0
1718
+ today_deals = self.get_today_deals(group=self.symbol)
1719
+ for deal in today_deals:
1720
+ if deal.profit < 0:
1721
+ negative_deals += 1
1722
+ if negative_deals >= self.max_trades:
1723
+ return True
1724
+ return False
1725
+
1585
1726
  def get_stats(self) -> Tuple[Dict[str, Any]]:
1586
1727
  """
1587
1728
  get some stats about the trading day and trading history
@@ -1594,11 +1735,14 @@ class Trade(RiskManagement):
1594
1735
  loss_trades = 0
1595
1736
  win_trades = 0
1596
1737
  balance = self.get_account_info().balance
1597
- deals = len(self.opened_positions)
1738
+ today_deals = self.get_today_deals(group=self.symbol)
1739
+ deals = len(today_deals)
1598
1740
  if deals != 0:
1599
- for position in self.opened_positions:
1741
+ for position in today_deals:
1600
1742
  time.sleep(0.1)
1601
- history = self.get_trades_history(position=position, to_df=False)
1743
+ history = self.get_trades_history(
1744
+ position=position.position_id, to_df=False
1745
+ )
1602
1746
  if history is not None and len(history) == 2:
1603
1747
  result = history[1].profit
1604
1748
  comm = history[0].commission
@@ -1641,13 +1785,12 @@ class Trade(RiskManagement):
1641
1785
  _fees = df2["fee"].sum()
1642
1786
  _swap = df2["swap"].sum()
1643
1787
  total_profit = commisions + _fees + _swap + profit
1644
- account_info = self.get_account_info()
1645
- balance = account_info.balance
1788
+ balance = self.get_account_info().balance
1646
1789
  initial_balance = balance - total_profit
1647
1790
  profittable = "Yes" if balance > initial_balance else "No"
1648
1791
  stats2 = {"total_profit": total_profit, "profitability": profittable}
1649
1792
  else:
1650
- stats2 = {"total_profit": 0, "profitability": 0}
1793
+ stats2 = {"total_profit": 0, "profitability": "No"}
1651
1794
  return (stats1, stats2)
1652
1795
 
1653
1796
  def sharpe(self):
@@ -1658,6 +1801,9 @@ class Trade(RiskManagement):
1658
1801
  those compared to a benchmark.
1659
1802
  """
1660
1803
  # Get total history
1804
+ import warnings
1805
+
1806
+ warnings.filterwarnings("ignore")
1661
1807
  df2 = self.get_trades_history()
1662
1808
  if df2 is None:
1663
1809
  return 0.0
@@ -1665,7 +1811,7 @@ class Trade(RiskManagement):
1665
1811
  profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
1666
1812
  returns = profit.pct_change(fill_method=None)
1667
1813
  periods = self.max_trade() * 252
1668
- sharpe = create_sharpe_ratio(returns, periods=periods)
1814
+ sharpe = qs.stats.sharpe(returns, periods=periods)
1669
1815
 
1670
1816
  return round(sharpe, 3)
1671
1817
 
@@ -8,3 +8,4 @@ from bbstrader.models.optimization import * # noqa: F403
8
8
  from bbstrader.models.portfolio import * # noqa: F403
9
9
  from bbstrader.models.factors import * # noqa: F403
10
10
  from bbstrader.models.ml import * # noqa: F403
11
+ from bbstrader.models.nlp import * # noqa: F403
bbstrader/models/ml.py CHANGED
@@ -8,8 +8,8 @@ import lightgbm as lgb
8
8
  import matplotlib.pyplot as plt
9
9
  import numpy as np
10
10
  import pandas as pd
11
+ import pandas_ta as ta
11
12
  import seaborn as sns
12
-
13
13
  import yfinance as yf
14
14
  from alphalens import performance as perf
15
15
  from alphalens import plotting
@@ -21,11 +21,9 @@ from alphalens.utils import (
21
21
  )
22
22
  from scipy.stats import spearmanr
23
23
  from sklearn.preprocessing import LabelEncoder, StandardScaler
24
- import pandas_ta as ta
25
24
 
26
25
  warnings.filterwarnings("ignore")
27
26
 
28
-
29
27
  __all__ = ["OneStepTimeSeriesSplit", "MultipleTimeSeriesCV", "LightGBModel"]
30
28
 
31
29
 
@@ -749,8 +747,8 @@ class LightGBModel(object):
749
747
  index=metric_cols,
750
748
  )
751
749
  if verbose:
752
- msg = f'\t{p:3.0f} | {self.format_time(T)} ({t:3.0f}) | {params["learning_rate"]:5.2f} | '
753
- msg += f'{params["num_leaves"]:3.0f} | {params["feature_fraction"]:3.0%} | {params["min_data_in_leaf"]:4.0f} | '
750
+ msg = f"\t{p:3.0f} | {self.format_time(T)} ({t:3.0f}) | {params['learning_rate']:5.2f} | "
751
+ msg += f"{params['num_leaves']:3.0f} | {params['feature_fraction']:3.0%} | {params['min_data_in_leaf']:4.0f} | "
754
752
  msg += f" {max(ic):6.2%} | {ic_by_day.mean().max(): 6.2%} | {daily_ic_mean_n: 4.0f} | {ic_by_day.median().max(): 6.2%} | {daily_ic_median_n: 4.0f}"
755
753
  print(msg)
756
754
 
@@ -871,7 +869,7 @@ class LightGBModel(object):
871
869
  med = data.ic.median()
872
870
  rolling.plot(
873
871
  ax=axes[i],
874
- title=f"Horizon: {t} Day(s) | IC: Mean={avg*100:.2f} Median={med*100:.2f}",
872
+ title=f"Horizon: {t} Day(s) | IC: Mean={avg * 100:.2f} Median={med * 100:.2f}",
875
873
  )
876
874
  axes[i].axhline(avg, c="darkred", lw=1)
877
875
  axes[i].axhline(0, ls="--", c="k", lw=1)
@@ -1237,7 +1235,9 @@ class LightGBModel(object):
1237
1235
  try:
1238
1236
  return (predictions.unstack("symbol").prediction.tz_convert("UTC")), tickers
1239
1237
  except TypeError:
1240
- return (predictions.unstack("symbol").prediction.tz_localize("UTC")), tickers
1238
+ return (
1239
+ predictions.unstack("symbol").prediction.tz_localize("UTC")
1240
+ ), tickers
1241
1241
 
1242
1242
  def assert_last_date(self, predictions: pd.DataFrame):
1243
1243
  """