bbstrader 0.2.99__py3-none-any.whl → 0.3.0__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/{__ini__.py → __init__.py} +0 -1
- bbstrader/__main__.py +31 -14
- bbstrader/btengine/data.py +4 -2
- bbstrader/btengine/event.py +15 -14
- bbstrader/btengine/execution.py +24 -14
- bbstrader/btengine/strategy.py +33 -15
- bbstrader/core/data.py +99 -3
- bbstrader/core/scripts.py +130 -0
- bbstrader/metatrader/account.py +124 -129
- bbstrader/metatrader/copier.py +5 -6
- bbstrader/metatrader/rates.py +14 -13
- bbstrader/metatrader/risk.py +13 -11
- bbstrader/metatrader/trade.py +34 -23
- bbstrader/metatrader/utils.py +79 -26
- bbstrader/models/factors.py +3 -1
- bbstrader/models/ml.py +2 -1
- bbstrader/models/nlp.py +21 -4
- bbstrader/trading/execution.py +36 -19
- bbstrader/trading/strategies.py +15 -14
- bbstrader/tseries.py +8 -9
- bbstrader-0.3.0.dist-info/METADATA +469 -0
- bbstrader-0.3.0.dist-info/RECORD +47 -0
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/WHEEL +1 -1
- bbstrader-0.2.99.dist-info/METADATA +0 -193
- bbstrader-0.2.99.dist-info/RECORD +0 -46
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/rates.py
CHANGED
|
@@ -7,7 +7,7 @@ from pandas.tseries.holiday import USFederalHolidayCalendar
|
|
|
7
7
|
from pandas.tseries.offsets import CustomBusinessDay
|
|
8
8
|
|
|
9
9
|
from bbstrader.metatrader.account import AMG_EXCHANGES, Account, check_mt5_connection
|
|
10
|
-
from bbstrader.metatrader.utils import TIMEFRAMES, TimeFrame, raise_mt5_error
|
|
10
|
+
from bbstrader.metatrader.utils import TIMEFRAMES, TimeFrame, raise_mt5_error, SymbolType
|
|
11
11
|
|
|
12
12
|
try:
|
|
13
13
|
import MetaTrader5 as Mt5
|
|
@@ -46,13 +46,13 @@ COMD_CALENDARS = {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
CALENDARS = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
SymbolType.FOREX: "us_futures",
|
|
50
|
+
SymbolType.STOCKS: AMG_EXCHANGES,
|
|
51
|
+
SymbolType.ETFs: AMG_EXCHANGES,
|
|
52
|
+
SymbolType.INDICES: IDX_CALENDARS,
|
|
53
|
+
SymbolType.COMMODITIES: COMD_CALENDARS,
|
|
54
|
+
SymbolType.CRYPTO: "24/7",
|
|
55
|
+
SymbolType.FUTURES: None,
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
SESSION_TIMEFRAMES = [
|
|
@@ -206,6 +206,7 @@ class Rates(object):
|
|
|
206
206
|
) -> Union[pd.DataFrame, None]:
|
|
207
207
|
"""Fetches data from MT5 and returns a DataFrame or None."""
|
|
208
208
|
try:
|
|
209
|
+
rates = None
|
|
209
210
|
if isinstance(start, int) and isinstance(count, int):
|
|
210
211
|
rates = Mt5.copy_rates_from_pos(
|
|
211
212
|
self.symbol, self.time_frame, start, count
|
|
@@ -248,7 +249,7 @@ class Rates(object):
|
|
|
248
249
|
currencies = self.__account.get_currency_rates(self.symbol)
|
|
249
250
|
s_info = self.__account.get_symbol_info(self.symbol)
|
|
250
251
|
if symbol_type in CALENDARS:
|
|
251
|
-
if symbol_type ==
|
|
252
|
+
if symbol_type == SymbolType.STOCKS or symbol_type == SymbolType.ETFs:
|
|
252
253
|
for exchange in CALENDARS[symbol_type]:
|
|
253
254
|
if exchange in get_calendar_names():
|
|
254
255
|
symbols = self.__account.get_stocks_from_exchange(
|
|
@@ -257,20 +258,20 @@ class Rates(object):
|
|
|
257
258
|
if self.symbol in symbols:
|
|
258
259
|
calendar = get_calendar(exchange, side="right")
|
|
259
260
|
break
|
|
260
|
-
elif symbol_type ==
|
|
261
|
+
elif symbol_type == SymbolType.INDICES:
|
|
261
262
|
calendar = get_calendar(
|
|
262
263
|
CALENDARS[symbol_type][currencies["mc"]], side="right"
|
|
263
264
|
)
|
|
264
|
-
elif symbol_type ==
|
|
265
|
+
elif symbol_type == SymbolType.COMMODITIES:
|
|
265
266
|
for commodity in CALENDARS[symbol_type]:
|
|
266
267
|
if commodity in s_info.path:
|
|
267
268
|
calendar = get_calendar(
|
|
268
269
|
CALENDARS[symbol_type][commodity], side="right"
|
|
269
270
|
)
|
|
270
|
-
elif symbol_type ==
|
|
271
|
+
elif symbol_type == SymbolType.FUTURES:
|
|
271
272
|
if "Index" in s_info.path:
|
|
272
273
|
calendar = get_calendar(
|
|
273
|
-
CALENDARS[
|
|
274
|
+
CALENDARS[SymbolType.INDICES][currencies["mc"]], side="right"
|
|
274
275
|
)
|
|
275
276
|
else:
|
|
276
277
|
for commodity, cal in COMD_CALENDARS.items():
|
bbstrader/metatrader/risk.py
CHANGED
|
@@ -6,7 +6,7 @@ from scipy.stats import norm
|
|
|
6
6
|
|
|
7
7
|
from bbstrader.metatrader.account import Account
|
|
8
8
|
from bbstrader.metatrader.rates import Rates
|
|
9
|
-
from bbstrader.metatrader.utils import TIMEFRAMES, TimeFrame
|
|
9
|
+
from bbstrader.metatrader.utils import TIMEFRAMES, TimeFrame, SymbolType
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
12
|
import MetaTrader5 as Mt5
|
|
@@ -275,10 +275,12 @@ class RiskManagement(Account):
|
|
|
275
275
|
swap = df["swap"].sum()
|
|
276
276
|
total_profit = commisions + fees + swap + profit
|
|
277
277
|
initial_balance = balance - total_profit
|
|
278
|
-
if
|
|
278
|
+
if equity != 0:
|
|
279
279
|
risk_alowed = (((equity - initial_balance) / equity) * 100) * -1
|
|
280
280
|
return round(risk_alowed, 2)
|
|
281
|
-
|
|
281
|
+
else: # Handle equity is zero
|
|
282
|
+
return 0.0
|
|
283
|
+
return 0.0 # This is for the case where df is None
|
|
282
284
|
|
|
283
285
|
def get_lot(self) -> float:
|
|
284
286
|
""" "Get the approprite lot size for a trade"""
|
|
@@ -498,10 +500,10 @@ class RiskManagement(Account):
|
|
|
498
500
|
av_price = (s_info.bid + s_info.ask) / 2
|
|
499
501
|
trade_risk = self.get_trade_risk()
|
|
500
502
|
symbol_type = self.get_symbol_type(self.symbol)
|
|
501
|
-
FX = symbol_type ==
|
|
502
|
-
COMD = symbol_type ==
|
|
503
|
-
FUT = symbol_type ==
|
|
504
|
-
CRYPTO = symbol_type ==
|
|
503
|
+
FX = symbol_type == SymbolType.FOREX
|
|
504
|
+
COMD = symbol_type == SymbolType.COMMODITIES
|
|
505
|
+
FUT = symbol_type == SymbolType.FUTURES
|
|
506
|
+
CRYPTO = symbol_type == SymbolType.CRYPTO
|
|
505
507
|
if COMD:
|
|
506
508
|
supported = _COMMD_SUPPORTED_
|
|
507
509
|
if "." in self.symbol:
|
|
@@ -652,14 +654,14 @@ class RiskManagement(Account):
|
|
|
652
654
|
lot = self._check_lot(_lot)
|
|
653
655
|
|
|
654
656
|
volume = round(lot * size * av_price)
|
|
655
|
-
if self.get_symbol_type(self.symbol) ==
|
|
657
|
+
if self.get_symbol_type(self.symbol) == SymbolType.FOREX:
|
|
656
658
|
volume = round((trade_loss * size) / loss)
|
|
657
659
|
__lot = round((volume / size), 2)
|
|
658
660
|
lot = self._check_lot(__lot)
|
|
659
661
|
|
|
660
662
|
if (
|
|
661
|
-
self.get_symbol_type(self.symbol) ==
|
|
662
|
-
or self.get_symbol_type(self.symbol) ==
|
|
663
|
+
self.get_symbol_type(self.symbol) == SymbolType.COMMODITIES
|
|
664
|
+
or self.get_symbol_type(self.symbol) == SymbolType.CRYPTO
|
|
663
665
|
and size > 1
|
|
664
666
|
):
|
|
665
667
|
lot = currency_risk / (sl * loss * size)
|
|
@@ -705,7 +707,7 @@ class RiskManagement(Account):
|
|
|
705
707
|
if account:
|
|
706
708
|
return AL
|
|
707
709
|
|
|
708
|
-
if self.get_symbol_type(self.symbol) ==
|
|
710
|
+
if self.get_symbol_type(self.symbol) == SymbolType.FOREX:
|
|
709
711
|
return AL
|
|
710
712
|
else:
|
|
711
713
|
s_info = self.symbol_info
|
bbstrader/metatrader/trade.py
CHANGED
|
@@ -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.
|
|
@@ -136,6 +137,15 @@ class TradeSignal:
|
|
|
136
137
|
f"price={self.price}, stoplimit={self.stoplimit}), comment='{self.comment}'"
|
|
137
138
|
)
|
|
138
139
|
|
|
140
|
+
class TradingMode(Enum):
|
|
141
|
+
BACKTEST = "BACKTEST"
|
|
142
|
+
LIVE = "LIVE"
|
|
143
|
+
|
|
144
|
+
def isbacktest(self) -> bool:
|
|
145
|
+
return self == TradingMode.BACKTEST
|
|
146
|
+
def islive(self) -> bool:
|
|
147
|
+
return self == TradingMode.LIVE
|
|
148
|
+
|
|
139
149
|
|
|
140
150
|
Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
141
151
|
Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
|
|
@@ -152,6 +162,7 @@ Orders = Literal[
|
|
|
152
162
|
|
|
153
163
|
EXPERT_ID = 98181105
|
|
154
164
|
|
|
165
|
+
|
|
155
166
|
class Trade(RiskManagement):
|
|
156
167
|
"""
|
|
157
168
|
Extends the `RiskManagement` class to include specific trading operations,
|
|
@@ -531,6 +542,7 @@ class Trade(RiskManagement):
|
|
|
531
542
|
mm (bool): Weither to put stop loss and tp or not
|
|
532
543
|
trail (bool): Weither to trail the stop loss or not
|
|
533
544
|
comment (str): The comment for the opening position
|
|
545
|
+
volume (float): The volume (lot) to trade
|
|
534
546
|
sl (float): The stop loss price
|
|
535
547
|
tp (float): The take profit price
|
|
536
548
|
"""
|
|
@@ -621,7 +633,6 @@ class Trade(RiskManagement):
|
|
|
621
633
|
mm (bool): Weither to put stop loss and tp or not
|
|
622
634
|
trail (bool): Weither to trail the stop loss or not
|
|
623
635
|
comment (str): The comment for the closing position
|
|
624
|
-
symbol (str): The symbol to trade
|
|
625
636
|
volume (float): The volume (lot) to trade
|
|
626
637
|
sl (float): The stop loss price
|
|
627
638
|
tp (float): The take profit price
|
|
@@ -834,7 +845,7 @@ class Trade(RiskManagement):
|
|
|
834
845
|
action (str): (`'BMKT'`, `'SMKT'`) for Market orders
|
|
835
846
|
or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
|
|
836
847
|
price (float): The price at which to open an order
|
|
837
|
-
stoplimit (float): A price a pending Limit order is set at
|
|
848
|
+
stoplimit (float): A price a pending Limit order is set at
|
|
838
849
|
when the price reaches the 'price' value (this condition is mandatory).
|
|
839
850
|
The pending order is not passed to the trading system until that moment
|
|
840
851
|
id (int): The strategy id or expert Id
|
|
@@ -873,30 +884,30 @@ class Trade(RiskManagement):
|
|
|
873
884
|
@property
|
|
874
885
|
def orders(self):
|
|
875
886
|
"""Return all opened order's tickets"""
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
return None
|
|
887
|
+
current_orders = self.get_current_orders() or []
|
|
888
|
+
opened_orders = set(current_orders + self.opened_orders)
|
|
889
|
+
return list(opened_orders) if len(opened_orders) != 0 else None
|
|
879
890
|
|
|
880
891
|
@property
|
|
881
892
|
def positions(self):
|
|
882
893
|
"""Return all opened position's tickets"""
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
return None
|
|
894
|
+
current_positions = self.get_current_positions() or []
|
|
895
|
+
opened_positions = set(current_positions + self.opened_positions)
|
|
896
|
+
return list(opened_positions) if len(opened_positions) != 0 else None
|
|
886
897
|
|
|
887
898
|
@property
|
|
888
899
|
def buypos(self):
|
|
889
900
|
"""Return all buy opened position's tickets"""
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
return None
|
|
901
|
+
buy_positions = self.get_current_buys() or []
|
|
902
|
+
buy_positions = set(buy_positions + self.buy_positions)
|
|
903
|
+
return list(buy_positions) if len(buy_positions) != 0 else None
|
|
893
904
|
|
|
894
905
|
@property
|
|
895
906
|
def sellpos(self):
|
|
896
907
|
"""Return all sell opened position's tickets"""
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
return None
|
|
908
|
+
sell_positions = self.get_current_sells() or []
|
|
909
|
+
sell_positions = set(sell_positions + self.sell_positions)
|
|
910
|
+
return list(sell_positions) if len(sell_positions) != 0 else None
|
|
900
911
|
|
|
901
912
|
@property
|
|
902
913
|
def bepos(self):
|
|
@@ -1214,7 +1225,7 @@ class Trade(RiskManagement):
|
|
|
1214
1225
|
Sets the break-even level for a given trading position.
|
|
1215
1226
|
|
|
1216
1227
|
Args:
|
|
1217
|
-
position (TradePosition): The trading position for which the break-even is to be set.
|
|
1228
|
+
position (TradePosition): The trading position for which the break-even is to be set.
|
|
1218
1229
|
This is the value return by `mt5.positions_get()`.
|
|
1219
1230
|
be (int): The break-even level in points.
|
|
1220
1231
|
level (float): The break-even level in price, if set to None , it will be calated automaticaly.
|
|
@@ -1446,7 +1457,7 @@ class Trade(RiskManagement):
|
|
|
1446
1457
|
Args:
|
|
1447
1458
|
ticket (int): Order ticket to modify (e.g TradeOrder.ticket)
|
|
1448
1459
|
price (float): The price at which to modify the order
|
|
1449
|
-
stoplimit (float): A price a pending Limit order is set at
|
|
1460
|
+
stoplimit (float): A price a pending Limit order is set at
|
|
1450
1461
|
when the price reaches the 'price' value (this condition is mandatory).
|
|
1451
1462
|
The pending order is not passed to the trading system until that moment
|
|
1452
1463
|
sl (float): The stop loss in points
|
|
@@ -1597,7 +1608,7 @@ class Trade(RiskManagement):
|
|
|
1597
1608
|
):
|
|
1598
1609
|
"""
|
|
1599
1610
|
Args:
|
|
1600
|
-
order_type (str): Type of orders to close
|
|
1611
|
+
order_type (str): Type of orders to close
|
|
1601
1612
|
('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
|
|
1602
1613
|
id (int): The unique ID of the Expert or Strategy
|
|
1603
1614
|
comment (str): Comment for the closing position
|
bbstrader/metatrader/utils.py
CHANGED
|
@@ -70,27 +70,31 @@ class TimeFrame(Enum):
|
|
|
70
70
|
Rrepresent a time frame object
|
|
71
71
|
"""
|
|
72
72
|
|
|
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"
|
|
73
|
+
M1 = TIMEFRAMES["1m"]
|
|
74
|
+
M2 = TIMEFRAMES["2m"]
|
|
75
|
+
M3 = TIMEFRAMES["3m"]
|
|
76
|
+
M4 = TIMEFRAMES["4m"]
|
|
77
|
+
M5 = TIMEFRAMES["5m"]
|
|
78
|
+
M6 = TIMEFRAMES["6m"]
|
|
79
|
+
M10 = TIMEFRAMES["10m"]
|
|
80
|
+
M12 = TIMEFRAMES["12m"]
|
|
81
|
+
M15 = TIMEFRAMES["15m"]
|
|
82
|
+
M20 = TIMEFRAMES["20m"]
|
|
83
|
+
M30 = TIMEFRAMES["30m"]
|
|
84
|
+
H1 = TIMEFRAMES["1h"]
|
|
85
|
+
H2 = TIMEFRAMES["2h"]
|
|
86
|
+
H3 = TIMEFRAMES["3h"]
|
|
87
|
+
H4 = TIMEFRAMES["4h"]
|
|
88
|
+
H6 = TIMEFRAMES["6h"]
|
|
89
|
+
H8 = TIMEFRAMES["8h"]
|
|
90
|
+
H12 = TIMEFRAMES["12h"]
|
|
91
|
+
D1 = TIMEFRAMES["D1"]
|
|
92
|
+
W1 = TIMEFRAMES["W1"]
|
|
93
|
+
MN1 = TIMEFRAMES["MN1"]
|
|
94
|
+
|
|
95
|
+
def __str__(self):
|
|
96
|
+
"""Return the string representation of the time frame."""
|
|
97
|
+
return self.name
|
|
94
98
|
|
|
95
99
|
|
|
96
100
|
class TerminalInfo(NamedTuple):
|
|
@@ -263,6 +267,23 @@ class SymbolInfo(NamedTuple):
|
|
|
263
267
|
path: str
|
|
264
268
|
|
|
265
269
|
|
|
270
|
+
class SymbolType(Enum):
|
|
271
|
+
"""
|
|
272
|
+
Represents the type of a symbol.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
FOREX = "FOREX" # Forex currency pairs
|
|
276
|
+
FUTURES = "FUTURES" # Futures contracts
|
|
277
|
+
STOCKS = "STOCKS" # Stocks and shares
|
|
278
|
+
BONDS = "BONDS" # Bonds
|
|
279
|
+
CRYPTO = "CRYPTO" # Cryptocurrencies
|
|
280
|
+
ETFs = "ETFs" # Exchange-Traded Funds
|
|
281
|
+
INDICES = "INDICES" # Market indices
|
|
282
|
+
COMMODITIES = "COMMODITIES" # Commodities
|
|
283
|
+
OPTIONS = "OPTIONS" # Options contracts
|
|
284
|
+
unknown = "UNKNOWN" # Unknown or unsupported type
|
|
285
|
+
|
|
286
|
+
|
|
266
287
|
class TickInfo(NamedTuple):
|
|
267
288
|
"""
|
|
268
289
|
Represents the last tick for the specified financial instrument.
|
|
@@ -465,10 +486,12 @@ class MT5TerminalError(Exception):
|
|
|
465
486
|
self.message = message
|
|
466
487
|
|
|
467
488
|
def __str__(self) -> str:
|
|
468
|
-
if self.message is None:
|
|
469
|
-
|
|
470
|
-
else:
|
|
471
|
-
|
|
489
|
+
# if self.message is None:
|
|
490
|
+
# return f"{self.__class__.__name__}"
|
|
491
|
+
# else:
|
|
492
|
+
# return f"{self.__class__.__name__}, {self.message}"
|
|
493
|
+
msg_str = str(self.message) if self.message is not None else ""
|
|
494
|
+
return f"{self.code} - {self.__class__.__name__}: {msg_str}"
|
|
472
495
|
|
|
473
496
|
|
|
474
497
|
class GenericFail(MT5TerminalError):
|
|
@@ -561,6 +584,21 @@ class InternalFailTimeout(InternalFailError):
|
|
|
561
584
|
super().__init__(MT5.RES_E_INTERNAL_FAIL_TIMEOUT, message)
|
|
562
585
|
|
|
563
586
|
|
|
587
|
+
RES_E_FAIL = 1 # Generic error
|
|
588
|
+
RES_E_INVALID_PARAMS = 2 # Invalid parameters
|
|
589
|
+
RES_E_NOT_FOUND = 3 # Not found
|
|
590
|
+
RES_E_INVALID_VERSION = 4 # Invalid version
|
|
591
|
+
RES_E_AUTH_FAILED = 5 # Authorization failed
|
|
592
|
+
RES_E_UNSUPPORTED = 6 # Unsupported method
|
|
593
|
+
RES_E_AUTO_TRADING_DISABLED = 7 # Autotrading disabled
|
|
594
|
+
|
|
595
|
+
# Actual internal error codes from MetaTrader5
|
|
596
|
+
RES_E_INTERNAL_FAIL_CONNECT = -10000
|
|
597
|
+
RES_E_INTERNAL_FAIL_INIT = -10001
|
|
598
|
+
RES_E_INTERNAL_FAIL_SEND = -10006
|
|
599
|
+
RES_E_INTERNAL_FAIL_RECEIVE = -10007
|
|
600
|
+
RES_E_INTERNAL_FAIL_TIMEOUT = -10008
|
|
601
|
+
|
|
564
602
|
# Dictionary to map error codes to exception classes
|
|
565
603
|
_ERROR_CODE_TO_EXCEPTION_ = {
|
|
566
604
|
MT5.RES_E_FAIL: GenericFail,
|
|
@@ -575,6 +613,18 @@ _ERROR_CODE_TO_EXCEPTION_ = {
|
|
|
575
613
|
MT5.RES_E_INTERNAL_FAIL_INIT: InternalFailInit,
|
|
576
614
|
MT5.RES_E_INTERNAL_FAIL_CONNECT: InternalFailConnect,
|
|
577
615
|
MT5.RES_E_INTERNAL_FAIL_TIMEOUT: InternalFailTimeout,
|
|
616
|
+
RES_E_FAIL: GenericFail,
|
|
617
|
+
RES_E_INVALID_PARAMS: InvalidParams,
|
|
618
|
+
RES_E_NOT_FOUND: HistoryNotFound,
|
|
619
|
+
RES_E_INVALID_VERSION: InvalidVersion,
|
|
620
|
+
RES_E_AUTH_FAILED: AuthFailed,
|
|
621
|
+
RES_E_UNSUPPORTED: UnsupportedMethod,
|
|
622
|
+
RES_E_AUTO_TRADING_DISABLED: AutoTradingDisabled,
|
|
623
|
+
RES_E_INTERNAL_FAIL_SEND: InternalFailSend,
|
|
624
|
+
RES_E_INTERNAL_FAIL_RECEIVE: InternalFailReceive,
|
|
625
|
+
RES_E_INTERNAL_FAIL_INIT: InternalFailInit,
|
|
626
|
+
RES_E_INTERNAL_FAIL_CONNECT: InternalFailConnect,
|
|
627
|
+
RES_E_INTERNAL_FAIL_TIMEOUT: InternalFailTimeout,
|
|
578
628
|
}
|
|
579
629
|
|
|
580
630
|
|
|
@@ -588,7 +638,10 @@ def raise_mt5_error(message: Optional[str] = None):
|
|
|
588
638
|
MT5TerminalError: A specific exception based on the error code.
|
|
589
639
|
"""
|
|
590
640
|
error = _ERROR_CODE_TO_EXCEPTION_.get(MT5.last_error()[0])
|
|
591
|
-
|
|
641
|
+
if error is not None:
|
|
642
|
+
raise Exception(f"{error(None)} {message or MT5.last_error()[1]}")
|
|
643
|
+
else:
|
|
644
|
+
raise Exception(f"{message or MT5.last_error()[1]}")
|
|
592
645
|
|
|
593
646
|
|
|
594
647
|
_ORDER_FILLING_TYPE_ = "https://www.mql5.com/en/docs/constants/tradingconstants/orderproperties#enum_order_type_filling"
|
bbstrader/models/factors.py
CHANGED
|
@@ -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
|
-
|
|
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):
|
bbstrader/models/nlp.py
CHANGED
|
@@ -331,8 +331,18 @@ FINANCIAL_LEXICON = {
|
|
|
331
331
|
|
|
332
332
|
class TopicModeler(object):
|
|
333
333
|
def __init__(self):
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
nltk.download("punkt", quiet=True)
|
|
335
|
+
nltk.download("stopwords", quiet=True)
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
self.nlp = spacy.load("en_core_web_sm")
|
|
339
|
+
self.nlp.disable_pipes("ner")
|
|
340
|
+
except OSError:
|
|
341
|
+
raise RuntimeError(
|
|
342
|
+
"The SpaCy model 'en_core_web_sm' is not installed.\n"
|
|
343
|
+
"Please install it by running:\n"
|
|
344
|
+
" python -m spacy download en_core_web_sm"
|
|
345
|
+
)
|
|
336
346
|
|
|
337
347
|
def preprocess_texts(self, texts: list[str]):
|
|
338
348
|
def clean_doc(Doc):
|
|
@@ -506,6 +516,7 @@ class SentimentAnalyzer(object):
|
|
|
506
516
|
reddit_posts = news.get_reddit_posts(
|
|
507
517
|
ticker, n_posts=top_news, **{k: kwargs.get(k) for k in rd_params}
|
|
508
518
|
)
|
|
519
|
+
coindesk_news = news.get_coindesk_news(query=ticker, list_of_str=True)
|
|
509
520
|
fmp_source_news = []
|
|
510
521
|
fmp_news = news.get_fmp_news(kwargs.get("fmp_api"))
|
|
511
522
|
for source in ["articles"]: # , "releases", asset_type]:
|
|
@@ -518,7 +529,7 @@ class SentimentAnalyzer(object):
|
|
|
518
529
|
source_news = []
|
|
519
530
|
if any([len(s) > 0 for s in [yahoo_news, google_news]]):
|
|
520
531
|
sources += 1
|
|
521
|
-
for source in [reddit_posts, fmp_source_news]:
|
|
532
|
+
for source in [reddit_posts, fmp_source_news, coindesk_news]:
|
|
522
533
|
if len(source) > 0:
|
|
523
534
|
sources += 1
|
|
524
535
|
# Compute sentiment
|
|
@@ -531,11 +542,17 @@ class SentimentAnalyzer(object):
|
|
|
531
542
|
fmp_sentiment = self.analyze_sentiment(
|
|
532
543
|
fmp_source_news, lexicon=lexicon, textblob=True
|
|
533
544
|
)
|
|
545
|
+
coindesk_sentiment = self.analyze_sentiment(
|
|
546
|
+
coindesk_news, lexicon=lexicon, textblob=True
|
|
547
|
+
)
|
|
534
548
|
|
|
535
549
|
# Weighted average sentiment score
|
|
536
550
|
if sources != 0:
|
|
537
551
|
overall_sentiment = (
|
|
538
|
-
news_sentiment
|
|
552
|
+
news_sentiment
|
|
553
|
+
+ reddit_sentiment
|
|
554
|
+
+ fmp_sentiment
|
|
555
|
+
+ coindesk_sentiment
|
|
539
556
|
) / sources
|
|
540
557
|
else:
|
|
541
558
|
overall_sentiment = 0.0
|
bbstrader/trading/execution.py
CHANGED
|
@@ -9,7 +9,7 @@ from loguru import logger as log
|
|
|
9
9
|
from bbstrader.btengine.strategy import MT5Strategy, Strategy
|
|
10
10
|
from bbstrader.config import BBSTRADER_DIR
|
|
11
11
|
from bbstrader.metatrader.account import Account, check_mt5_connection
|
|
12
|
-
from bbstrader.metatrader.trade import Trade, TradeAction
|
|
12
|
+
from bbstrader.metatrader.trade import Trade, TradeAction, TradingMode
|
|
13
13
|
from bbstrader.trading.utils import send_message
|
|
14
14
|
|
|
15
15
|
try:
|
|
@@ -174,6 +174,7 @@ class Mt5ExecutionEngine:
|
|
|
174
174
|
strategy_cls: Strategy,
|
|
175
175
|
/,
|
|
176
176
|
mm: bool = True,
|
|
177
|
+
auto_trade: bool = True,
|
|
177
178
|
optimizer: str = "equal",
|
|
178
179
|
trail: bool = True,
|
|
179
180
|
stop_trail: Optional[int] = None,
|
|
@@ -187,7 +188,7 @@ class Mt5ExecutionEngine:
|
|
|
187
188
|
closing_pnl: Optional[float] = None,
|
|
188
189
|
trading_days: Optional[List[str]] = None,
|
|
189
190
|
comment: Optional[str] = None,
|
|
190
|
-
**kwargs
|
|
191
|
+
**kwargs,
|
|
191
192
|
):
|
|
192
193
|
"""
|
|
193
194
|
Args:
|
|
@@ -197,6 +198,9 @@ class Mt5ExecutionEngine:
|
|
|
197
198
|
mm : Enable Money Management. Defaults to True.
|
|
198
199
|
optimizer : Risk management optimizer. Defaults to 'equal'.
|
|
199
200
|
See `bbstrader.models.optimization` module for more information.
|
|
201
|
+
auto_trade : If set to true, when signal are generated by the strategy class,
|
|
202
|
+
the Execution engine will automaticaly open position in other whise it will prompt
|
|
203
|
+
the user for confimation.
|
|
200
204
|
show_positions_orders : Print open positions and orders. Defaults to False.
|
|
201
205
|
iter_time : Interval to check for signals and `mm`. Defaults to 5.
|
|
202
206
|
use_trade_time : Open trades after the time is completed. Defaults to True.
|
|
@@ -239,6 +243,7 @@ class Mt5ExecutionEngine:
|
|
|
239
243
|
self.trades_instances = trades_instances
|
|
240
244
|
self.strategy_cls = strategy_cls
|
|
241
245
|
self.mm = mm
|
|
246
|
+
self.auto_trade = auto_trade
|
|
242
247
|
self.optimizer = optimizer
|
|
243
248
|
self.trail = trail
|
|
244
249
|
self.stop_trail = stop_trail
|
|
@@ -266,7 +271,7 @@ class Mt5ExecutionEngine:
|
|
|
266
271
|
def __repr__(self):
|
|
267
272
|
trades = self.trades_instances.keys()
|
|
268
273
|
strategy = self.strategy_cls.__name__
|
|
269
|
-
return f"
|
|
274
|
+
return f"{self.__class__.__name__}(Symbols={list(trades)}, Strategy={strategy})"
|
|
270
275
|
|
|
271
276
|
def _initialize_engine(self, **kwargs):
|
|
272
277
|
global logger
|
|
@@ -300,14 +305,14 @@ class Mt5ExecutionEngine:
|
|
|
300
305
|
)
|
|
301
306
|
return
|
|
302
307
|
|
|
303
|
-
def _print_exc(self, msg, e: Exception):
|
|
308
|
+
def _print_exc(self, msg: str, e: Exception):
|
|
304
309
|
if isinstance(e, KeyboardInterrupt):
|
|
305
310
|
logger.info("Stopping the Execution Engine ...")
|
|
306
311
|
quit()
|
|
307
312
|
if self.debug_mode:
|
|
308
313
|
raise ValueError(msg).with_traceback(e.__traceback__)
|
|
309
314
|
else:
|
|
310
|
-
logger.error(msg)
|
|
315
|
+
logger.error(f"{msg, repr(e)}")
|
|
311
316
|
|
|
312
317
|
def _max_trades(self, mtrades):
|
|
313
318
|
max_trades = {
|
|
@@ -331,7 +336,7 @@ class Mt5ExecutionEngine:
|
|
|
331
336
|
try:
|
|
332
337
|
check_mt5_connection(**kwargs)
|
|
333
338
|
strategy: MT5Strategy = self.strategy_cls(
|
|
334
|
-
symbol_list=self.symbols, mode=
|
|
339
|
+
symbol_list=self.symbols, mode=TradingMode.LIVE, **kwargs
|
|
335
340
|
)
|
|
336
341
|
except Exception as e:
|
|
337
342
|
self._print_exc(
|
|
@@ -357,24 +362,24 @@ class Mt5ExecutionEngine:
|
|
|
357
362
|
}
|
|
358
363
|
|
|
359
364
|
info = (
|
|
360
|
-
"SIGNAL
|
|
365
|
+
"SIGNAL={signal}, SYMBOL={symbol}, STRATEGY={strategy}, "
|
|
361
366
|
"TIMEFRAME={timeframe}, ACCOUNT={account}"
|
|
362
367
|
).format(**common_data)
|
|
363
368
|
|
|
364
369
|
sigmsg = (
|
|
365
|
-
"SIGNAL
|
|
366
|
-
"SYMBOL
|
|
367
|
-
"TYPE
|
|
368
|
-
"DESCRIPTION
|
|
369
|
-
"PRICE
|
|
370
|
-
"STOPLIMIT
|
|
371
|
-
"STRATEGY
|
|
372
|
-
"TIMEFRAME
|
|
373
|
-
"BROKER
|
|
374
|
-
"TIMESTAMP
|
|
370
|
+
"SIGNAL={signal},\n"
|
|
371
|
+
"SYMBOL={symbol},\n"
|
|
372
|
+
"TYPE={symbol_type},\n"
|
|
373
|
+
"DESCRIPTION={description},\n"
|
|
374
|
+
"PRICE={price},\n"
|
|
375
|
+
"STOPLIMIT={stoplimit},\n"
|
|
376
|
+
"STRATEGY={strategy},\n"
|
|
377
|
+
"TIMEFRAME={timeframe},\n"
|
|
378
|
+
"BROKER={broker},\n"
|
|
379
|
+
"TIMESTAMP={timestamp}"
|
|
375
380
|
).format(
|
|
376
381
|
**common_data,
|
|
377
|
-
symbol_type=account.get_symbol_type(symbol),
|
|
382
|
+
symbol_type=account.get_symbol_type(symbol).value,
|
|
378
383
|
description=symbol_info.description,
|
|
379
384
|
price=price,
|
|
380
385
|
stoplimit=stoplimit,
|
|
@@ -382,7 +387,7 @@ class Mt5ExecutionEngine:
|
|
|
382
387
|
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
383
388
|
)
|
|
384
389
|
|
|
385
|
-
msg_template = "SYMBOL
|
|
390
|
+
msg_template = "SYMBOL={symbol}, STRATEGY={strategy}, ACCOUNT={account}"
|
|
386
391
|
msg = f"Sending {signal} Order ... " + msg_template.format(**common_data)
|
|
387
392
|
tfmsg = "Time Frame Not completed !!! " + msg_template.format(**common_data)
|
|
388
393
|
riskmsg = "Risk not allowed !!! " + msg_template.format(**common_data)
|
|
@@ -621,6 +626,12 @@ class Mt5ExecutionEngine:
|
|
|
621
626
|
):
|
|
622
627
|
if self.notify:
|
|
623
628
|
self._send_notification(sigmsg, symbol)
|
|
629
|
+
if not self.auto_trade:
|
|
630
|
+
auto_trade = input(
|
|
631
|
+
f"{sigmsg} \n\n Please enter Y/Yes to accept this order or N/No to reject it :"
|
|
632
|
+
)
|
|
633
|
+
if not auto_trade.upper().startswith("Y"):
|
|
634
|
+
return
|
|
624
635
|
if not self._check_retcode(trade, "BMKT"):
|
|
625
636
|
logger.info(msg)
|
|
626
637
|
trade.open_buy_position(
|
|
@@ -638,6 +649,12 @@ class Mt5ExecutionEngine:
|
|
|
638
649
|
):
|
|
639
650
|
if self.notify:
|
|
640
651
|
self._send_notification(sigmsg, symbol)
|
|
652
|
+
if not self.auto_trade:
|
|
653
|
+
auto_trade = input(
|
|
654
|
+
f"{sigmsg} \n\n Please enter Y/Yes to accept this order or N/No to reject it :"
|
|
655
|
+
)
|
|
656
|
+
if not auto_trade.upper().startswith("Y"):
|
|
657
|
+
return
|
|
641
658
|
if not self._check_retcode(trade, "SMKT"):
|
|
642
659
|
logger.info(msg)
|
|
643
660
|
trade.open_sell_position(
|