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.
- bbstrader/__main__.py +3 -2
- bbstrader/btengine/data.py +6 -6
- bbstrader/btengine/execution.py +1 -1
- bbstrader/btengine/strategy.py +87 -99
- bbstrader/core/__init__.py +2 -0
- bbstrader/core/data.py +424 -7
- bbstrader/core/utils.py +2 -58
- bbstrader/metatrader/copier.py +1 -0
- bbstrader/metatrader/trade.py +157 -11
- bbstrader/models/__init__.py +1 -0
- bbstrader/models/ml.py +7 -7
- bbstrader/models/nlp.py +784 -0
- bbstrader/models/risk.py +2 -2
- bbstrader/trading/execution.py +18 -7
- bbstrader/trading/scripts.py +144 -58
- bbstrader/trading/strategies.py +6 -0
- bbstrader/trading/utils.py +69 -0
- {bbstrader-0.2.95.dist-info → bbstrader-0.2.97.dist-info}/METADATA +11 -2
- {bbstrader-0.2.95.dist-info → bbstrader-0.2.97.dist-info}/RECORD +23 -22
- {bbstrader-0.2.95.dist-info → bbstrader-0.2.97.dist-info}/WHEEL +1 -1
- bbstrader/trading/script.py +0 -155
- {bbstrader-0.2.95.dist-info → bbstrader-0.2.97.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.95.dist-info → bbstrader-0.2.97.dist-info/licenses}/LICENSE +0 -0
- {bbstrader-0.2.95.dist-info → bbstrader-0.2.97.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/trade.py
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
|
-
from
|
|
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
|
|
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
|
|
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
|
-
|
|
1738
|
+
today_deals = self.get_today_deals(group=self.symbol)
|
|
1739
|
+
deals = len(today_deals)
|
|
1598
1740
|
if deals != 0:
|
|
1599
|
-
for position in
|
|
1741
|
+
for position in today_deals:
|
|
1600
1742
|
time.sleep(0.1)
|
|
1601
|
-
history = self.get_trades_history(
|
|
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
|
-
|
|
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":
|
|
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 =
|
|
1814
|
+
sharpe = qs.stats.sharpe(returns, periods=periods)
|
|
1669
1815
|
|
|
1670
1816
|
return round(sharpe, 3)
|
|
1671
1817
|
|
bbstrader/models/__init__.py
CHANGED
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
|
|
753
|
-
msg += f
|
|
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 (
|
|
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
|
"""
|