bbstrader 0.3.0__py3-none-any.whl → 0.3.2__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/__init__.py +1 -1
- bbstrader/__main__.py +19 -13
- bbstrader/btengine/backtest.py +7 -8
- bbstrader/btengine/execution.py +2 -2
- bbstrader/btengine/strategy.py +68 -17
- bbstrader/config.py +2 -2
- bbstrader/core/data.py +92 -29
- bbstrader/metatrader/account.py +81 -16
- bbstrader/metatrader/copier.py +594 -195
- bbstrader/metatrader/risk.py +1 -0
- bbstrader/metatrader/scripts.py +53 -13
- bbstrader/metatrader/trade.py +79 -67
- bbstrader/metatrader/utils.py +3 -0
- bbstrader/models/__init__.py +0 -1
- bbstrader/models/ml.py +55 -26
- bbstrader/models/nlp.py +182 -74
- bbstrader/models/optimization.py +1 -1
- bbstrader/models/risk.py +16 -386
- bbstrader/trading/execution.py +70 -41
- bbstrader/trading/strategies.py +9 -592
- bbstrader/tseries.py +39 -709
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.2.dist-info}/METADATA +36 -44
- bbstrader-0.3.2.dist-info/RECORD +47 -0
- bbstrader-0.3.0.dist-info/RECORD +0 -47
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.2.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.2.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.2.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/risk.py
CHANGED
bbstrader/metatrader/scripts.py
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import multiprocessing
|
|
2
3
|
import sys
|
|
3
4
|
|
|
4
|
-
from bbstrader.
|
|
5
|
+
from bbstrader.apps._copier import main as RunCopyAPP
|
|
6
|
+
from bbstrader.metatrader.copier import RunCopier, config_copier, copier_worker_process
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
def copier_args(parser: argparse.ArgumentParser):
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"-m",
|
|
12
|
+
"--mode",
|
|
13
|
+
type=str,
|
|
14
|
+
default="CLI",
|
|
15
|
+
choices=("CLI", "GUI"),
|
|
16
|
+
help="Run the copier in the terminal or using the GUI",
|
|
17
|
+
)
|
|
8
18
|
parser.add_argument(
|
|
9
19
|
"-s", "--source", type=str, nargs="?", default=None, help="Source section name"
|
|
10
20
|
)
|
|
@@ -43,6 +53,12 @@ def copier_args(parser: argparse.ArgumentParser):
|
|
|
43
53
|
default=None,
|
|
44
54
|
help="End time in HH:MM format",
|
|
45
55
|
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"-M",
|
|
58
|
+
"--multiprocess",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Run each destination account in a separate process.",
|
|
61
|
+
)
|
|
46
62
|
return parser
|
|
47
63
|
|
|
48
64
|
|
|
@@ -52,9 +68,11 @@ def copy_trades(unknown):
|
|
|
52
68
|
python -m bbstrader --run copier [options]
|
|
53
69
|
|
|
54
70
|
Options:
|
|
71
|
+
-m, --mode: CLI for terminal app and GUI for Desktop app
|
|
55
72
|
-s, --source: Source Account section name
|
|
56
73
|
-d, --destinations: Destination Account section names (multiple allowed)
|
|
57
74
|
-i, --interval: Update interval in seconds
|
|
75
|
+
-M, --multiprocess: When set to True, each destination account runs in a separate process.
|
|
58
76
|
-c, --config: .ini file or path (default: ~/.bbstrader/copier/copier.ini)
|
|
59
77
|
-t, --start: Start time in HH:MM format
|
|
60
78
|
-e, --end: End time in HH:MM format
|
|
@@ -67,15 +85,37 @@ def copy_trades(unknown):
|
|
|
67
85
|
copy_parser = copier_args(copy_parser)
|
|
68
86
|
copy_args = copy_parser.parse_args(unknown)
|
|
69
87
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
copy_args.
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
if copy_args.mode == "GUI":
|
|
89
|
+
RunCopyAPP()
|
|
90
|
+
|
|
91
|
+
elif copy_args.mode == "CLI":
|
|
92
|
+
source, destinations = config_copier(
|
|
93
|
+
source_section=copy_args.source,
|
|
94
|
+
dest_sections=copy_args.destinations,
|
|
95
|
+
inifile=copy_args.config,
|
|
96
|
+
)
|
|
97
|
+
if copy_args.multiprocess:
|
|
98
|
+
copier_processes = []
|
|
99
|
+
for dest_config in destinations:
|
|
100
|
+
process = multiprocessing.Process(
|
|
101
|
+
target=copier_worker_process,
|
|
102
|
+
args=(
|
|
103
|
+
source,
|
|
104
|
+
dest_config,
|
|
105
|
+
copy_args.interval,
|
|
106
|
+
copy_args.start,
|
|
107
|
+
copy_args.end,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
process.start()
|
|
111
|
+
copier_processes.append(process)
|
|
112
|
+
for process in copier_processes:
|
|
113
|
+
process.join()
|
|
114
|
+
else:
|
|
115
|
+
RunCopier(
|
|
116
|
+
source,
|
|
117
|
+
destinations,
|
|
118
|
+
copy_args.interval,
|
|
119
|
+
copy_args.start,
|
|
120
|
+
copy_args.end,
|
|
121
|
+
)
|
bbstrader/metatrader/trade.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from logging import Logger
|
|
7
7
|
from pathlib import Path
|
|
@@ -16,7 +16,6 @@ from bbstrader.config import BBSTRADER_DIR, config_logger
|
|
|
16
16
|
from bbstrader.metatrader.account import INIT_MSG, check_mt5_connection
|
|
17
17
|
from bbstrader.metatrader.risk import RiskManagement
|
|
18
18
|
from bbstrader.metatrader.utils import (
|
|
19
|
-
TradeDeal,
|
|
20
19
|
TradePosition,
|
|
21
20
|
raise_mt5_error,
|
|
22
21
|
trade_retcode_message,
|
|
@@ -27,10 +26,13 @@ try:
|
|
|
27
26
|
except ImportError:
|
|
28
27
|
import bbstrader.compat # noqa: F401
|
|
29
28
|
|
|
30
|
-
|
|
31
29
|
__all__ = [
|
|
32
30
|
"Trade",
|
|
33
31
|
"create_trade_instance",
|
|
32
|
+
"TradeAction",
|
|
33
|
+
"TradeSignal",
|
|
34
|
+
"TradingMode",
|
|
35
|
+
"generate_signal",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
log.add(
|
|
@@ -134,15 +136,49 @@ class TradeSignal:
|
|
|
134
136
|
def __repr__(self):
|
|
135
137
|
return (
|
|
136
138
|
f"TradeSignal(id={self.id}, symbol='{self.symbol}', action='{self.action.value}', "
|
|
137
|
-
f"price={self.price}, stoplimit={self.stoplimit}
|
|
139
|
+
f"price={self.price}, stoplimit={self.stoplimit}, comment='{self.comment or ''}')"
|
|
138
140
|
)
|
|
139
141
|
|
|
142
|
+
|
|
143
|
+
def generate_signal(
|
|
144
|
+
id: int,
|
|
145
|
+
symbol: str,
|
|
146
|
+
action: TradeAction,
|
|
147
|
+
price: float = None,
|
|
148
|
+
stoplimit: float = None,
|
|
149
|
+
comment: str = None,
|
|
150
|
+
) -> TradeSignal:
|
|
151
|
+
"""
|
|
152
|
+
Generates a trade signal for MetaTrader 5.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
id (int): Unique identifier for the trade signal.
|
|
156
|
+
symbol (str): The symbol for which the trade signal is generated.
|
|
157
|
+
action (TradeAction): The action to be taken (e.g., BUY, SELL).
|
|
158
|
+
price (float, optional): The price at which to execute the trade.
|
|
159
|
+
stoplimit (float, optional): The stop limit price for the trade.
|
|
160
|
+
comment (str, optional): Additional comments for the trade.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
TradeSignal: A TradeSignal object containing the details of the trade signal.
|
|
164
|
+
"""
|
|
165
|
+
return TradeSignal(
|
|
166
|
+
id=id,
|
|
167
|
+
symbol=symbol,
|
|
168
|
+
action=action,
|
|
169
|
+
price=price,
|
|
170
|
+
stoplimit=stoplimit,
|
|
171
|
+
comment=comment,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
140
175
|
class TradingMode(Enum):
|
|
141
176
|
BACKTEST = "BACKTEST"
|
|
142
177
|
LIVE = "LIVE"
|
|
143
178
|
|
|
144
179
|
def isbacktest(self) -> bool:
|
|
145
180
|
return self == TradingMode.BACKTEST
|
|
181
|
+
|
|
146
182
|
def islive(self) -> bool:
|
|
147
183
|
return self == TradingMode.LIVE
|
|
148
184
|
|
|
@@ -222,7 +258,7 @@ class Trade(RiskManagement):
|
|
|
222
258
|
symbol: str = "EURUSD",
|
|
223
259
|
expert_name: str = "bbstrader",
|
|
224
260
|
expert_id: int = EXPERT_ID,
|
|
225
|
-
version: str = "
|
|
261
|
+
version: str = "3.0",
|
|
226
262
|
target: float = 5.0,
|
|
227
263
|
start_time: str = "0:00",
|
|
228
264
|
finishing_time: str = "23:59",
|
|
@@ -590,13 +626,15 @@ class Trade(RiskManagement):
|
|
|
590
626
|
request["tp"] = tp or mm_price + take_profit * point
|
|
591
627
|
self.break_even(mm=mm, id=Id, trail=trail)
|
|
592
628
|
if self.check(comment):
|
|
629
|
+
if action == "BSTPLMT":
|
|
630
|
+
_price = stoplimit
|
|
593
631
|
return self.request_result(_price, request, action)
|
|
594
632
|
return False
|
|
595
633
|
|
|
596
634
|
def _order_type(self):
|
|
597
635
|
return {
|
|
598
636
|
"BMKT": (Mt5.ORDER_TYPE_BUY, "BUY"),
|
|
599
|
-
"SMKT": (Mt5.
|
|
637
|
+
"SMKT": (Mt5.ORDER_TYPE_SELL, "SELL"),
|
|
600
638
|
"BLMT": (Mt5.ORDER_TYPE_BUY_LIMIT, "BUY_LIMIT"),
|
|
601
639
|
"SLMT": (Mt5.ORDER_TYPE_SELL_LIMIT, "SELL_LIMIT"),
|
|
602
640
|
"BSTP": (Mt5.ORDER_TYPE_BUY_STOP, "BUY_STOP"),
|
|
@@ -681,6 +719,8 @@ class Trade(RiskManagement):
|
|
|
681
719
|
request["tp"] = tp or mm_price - take_profit * point
|
|
682
720
|
self.break_even(mm=mm, id=Id, trail=trail)
|
|
683
721
|
if self.check(comment):
|
|
722
|
+
if action == "SSTPLMT":
|
|
723
|
+
_price = stoplimit
|
|
684
724
|
return self.request_result(_price, request, action)
|
|
685
725
|
return False
|
|
686
726
|
|
|
@@ -740,10 +780,8 @@ class Trade(RiskManagement):
|
|
|
740
780
|
self.check_order(request)
|
|
741
781
|
result = self.send_order(request)
|
|
742
782
|
except Exception as e:
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
746
|
-
)
|
|
783
|
+
msg = trade_retcode_message(result.retcode)
|
|
784
|
+
LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
|
|
747
785
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
748
786
|
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
749
787
|
for fill in FILLING_TYPE:
|
|
@@ -773,10 +811,8 @@ class Trade(RiskManagement):
|
|
|
773
811
|
self.check_order(request)
|
|
774
812
|
result = self.send_order(request)
|
|
775
813
|
except Exception as e:
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
779
|
-
)
|
|
814
|
+
msg = trade_retcode_message(result.retcode)
|
|
815
|
+
LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
|
|
780
816
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
781
817
|
break
|
|
782
818
|
tries += 1
|
|
@@ -787,7 +823,7 @@ class Trade(RiskManagement):
|
|
|
787
823
|
if type != "BMKT" or type != "SMKT":
|
|
788
824
|
self.opened_orders.append(result.order)
|
|
789
825
|
long_msg = (
|
|
790
|
-
f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{price}, "
|
|
826
|
+
f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{round(price, 5)}, "
|
|
791
827
|
f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
|
|
792
828
|
f"Tp: {self.get_take_profit()}"
|
|
793
829
|
)
|
|
@@ -808,7 +844,7 @@ class Trade(RiskManagement):
|
|
|
808
844
|
profit = round(self.get_account_info().profit, 5)
|
|
809
845
|
order_info = (
|
|
810
846
|
f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open, 5)}, "
|
|
811
|
-
f"Sl: @{position.sl} Tp: @{position.tp}"
|
|
847
|
+
f"Sl: @{round(position.sl, 5)} Tp: @{round(position.tp, 5)}"
|
|
812
848
|
)
|
|
813
849
|
LOGGER.info(order_info)
|
|
814
850
|
pos_info = (
|
|
@@ -1134,9 +1170,9 @@ class Trade(RiskManagement):
|
|
|
1134
1170
|
be = self.get_break_even()
|
|
1135
1171
|
if trail_after_points is not None:
|
|
1136
1172
|
if isinstance(trail_after_points, int):
|
|
1137
|
-
assert (
|
|
1138
|
-
trail_after_points
|
|
1139
|
-
)
|
|
1173
|
+
assert trail_after_points > be, (
|
|
1174
|
+
"trail_after_points must be greater than break even or set to None"
|
|
1175
|
+
)
|
|
1140
1176
|
trail_after_points = self._get_trail_after_points(trail_after_points)
|
|
1141
1177
|
if positions is not None:
|
|
1142
1178
|
for position in positions:
|
|
@@ -1294,10 +1330,8 @@ class Trade(RiskManagement):
|
|
|
1294
1330
|
self.check_order(request)
|
|
1295
1331
|
result = self.send_order(request)
|
|
1296
1332
|
except Exception as e:
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
1300
|
-
)
|
|
1333
|
+
msg = trade_retcode_message(result.retcode)
|
|
1334
|
+
LOGGER.error(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
|
|
1301
1335
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1302
1336
|
msg = trade_retcode_message(result.retcode)
|
|
1303
1337
|
if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
@@ -1314,9 +1348,9 @@ class Trade(RiskManagement):
|
|
|
1314
1348
|
self.check_order(request)
|
|
1315
1349
|
result = self.send_order(request)
|
|
1316
1350
|
except Exception as e:
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1351
|
+
msg = trade_retcode_message(result.retcode)
|
|
1352
|
+
LOGGER.error(
|
|
1353
|
+
f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}"
|
|
1320
1354
|
)
|
|
1321
1355
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1322
1356
|
break
|
|
@@ -1324,7 +1358,7 @@ class Trade(RiskManagement):
|
|
|
1324
1358
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1325
1359
|
msg = trade_retcode_message(result.retcode)
|
|
1326
1360
|
LOGGER.info(f"Break-Even Order {msg}{addtionnal}")
|
|
1327
|
-
info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
|
|
1361
|
+
info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{round(price, 5)}"
|
|
1328
1362
|
LOGGER.info(info)
|
|
1329
1363
|
self.break_even_status.append(tiket)
|
|
1330
1364
|
|
|
@@ -1402,9 +1436,9 @@ class Trade(RiskManagement):
|
|
|
1402
1436
|
self.check_order(request)
|
|
1403
1437
|
result = self.send_order(request)
|
|
1404
1438
|
except Exception as e:
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1439
|
+
msg = trade_retcode_message(result.retcode)
|
|
1440
|
+
LOGGER.error(
|
|
1441
|
+
f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
|
|
1408
1442
|
)
|
|
1409
1443
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1410
1444
|
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
@@ -1417,7 +1451,8 @@ class Trade(RiskManagement):
|
|
|
1417
1451
|
self._retcodes.append(result.retcode)
|
|
1418
1452
|
msg = trade_retcode_message(result.retcode)
|
|
1419
1453
|
LOGGER.error(
|
|
1420
|
-
f"Closing Order Request, {type.capitalize()}: #{ticket},
|
|
1454
|
+
f"Closing Order Request, {type.capitalize()}: #{ticket}, "
|
|
1455
|
+
f"RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
1421
1456
|
)
|
|
1422
1457
|
else:
|
|
1423
1458
|
tries = 0
|
|
@@ -1427,9 +1462,9 @@ class Trade(RiskManagement):
|
|
|
1427
1462
|
self.check_order(request)
|
|
1428
1463
|
result = self.send_order(request)
|
|
1429
1464
|
except Exception as e:
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1465
|
+
msg = trade_retcode_message(result.retcode)
|
|
1466
|
+
LOGGER.error(
|
|
1467
|
+
f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
|
|
1433
1468
|
)
|
|
1434
1469
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1435
1470
|
break
|
|
@@ -1437,7 +1472,10 @@ class Trade(RiskManagement):
|
|
|
1437
1472
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1438
1473
|
msg = trade_retcode_message(result.retcode)
|
|
1439
1474
|
LOGGER.info(f"Closing Order {msg}{addtionnal}")
|
|
1440
|
-
info =
|
|
1475
|
+
info = (
|
|
1476
|
+
f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol},"
|
|
1477
|
+
f"Price: @{round(request.get('price', 0.0), 5)}"
|
|
1478
|
+
)
|
|
1441
1479
|
LOGGER.info(info)
|
|
1442
1480
|
return True
|
|
1443
1481
|
else:
|
|
@@ -1466,7 +1504,7 @@ class Trade(RiskManagement):
|
|
|
1466
1504
|
orders = self.get_orders(ticket=ticket) or []
|
|
1467
1505
|
if len(orders) == 0:
|
|
1468
1506
|
LOGGER.error(
|
|
1469
|
-
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={price}"
|
|
1507
|
+
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={round(price, 5)}"
|
|
1470
1508
|
)
|
|
1471
1509
|
return
|
|
1472
1510
|
order = orders[0]
|
|
@@ -1482,7 +1520,8 @@ class Trade(RiskManagement):
|
|
|
1482
1520
|
result = self.send_order(request)
|
|
1483
1521
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1484
1522
|
LOGGER.info(
|
|
1485
|
-
f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={price
|
|
1523
|
+
f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={round(price, 5)},"
|
|
1524
|
+
f"SL={round(sl, 5)}, TP={round(tp, 5)}, STOP_LIMIT={round(stoplimit, 5)}"
|
|
1486
1525
|
)
|
|
1487
1526
|
else:
|
|
1488
1527
|
msg = trade_retcode_message(result.retcode)
|
|
@@ -1538,8 +1577,6 @@ class Trade(RiskManagement):
|
|
|
1538
1577
|
symbol = symbol or self.symbol
|
|
1539
1578
|
Id = id if id is not None else self.expert_id
|
|
1540
1579
|
positions = self.get_positions(ticket=ticket)
|
|
1541
|
-
buy_price = self.get_tick_info(symbol).ask
|
|
1542
|
-
sell_price = self.get_tick_info(symbol).bid
|
|
1543
1580
|
deviation = self.get_deviation()
|
|
1544
1581
|
if positions is not None and len(positions) == 1:
|
|
1545
1582
|
position = positions[0]
|
|
@@ -1551,9 +1588,8 @@ class Trade(RiskManagement):
|
|
|
1551
1588
|
"volume": (position.volume * pct),
|
|
1552
1589
|
"type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
|
|
1553
1590
|
"position": ticket,
|
|
1554
|
-
"price":
|
|
1591
|
+
"price": position.price_current,
|
|
1555
1592
|
"deviation": deviation,
|
|
1556
|
-
"magic": Id,
|
|
1557
1593
|
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
1558
1594
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
1559
1595
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
@@ -1670,32 +1706,8 @@ class Trade(RiskManagement):
|
|
|
1670
1706
|
comment=comment,
|
|
1671
1707
|
)
|
|
1672
1708
|
|
|
1673
|
-
def get_today_deals(self, group=None)
|
|
1674
|
-
|
|
1675
|
-
Get all today deals for a specific symbol or group of symbols
|
|
1676
|
-
|
|
1677
|
-
Args:
|
|
1678
|
-
group (str): Symbol or group or symbol
|
|
1679
|
-
Returns:
|
|
1680
|
-
List[TradeDeal]: List of today deals
|
|
1681
|
-
"""
|
|
1682
|
-
date_from = datetime.now() - timedelta(days=2)
|
|
1683
|
-
history = (
|
|
1684
|
-
self.get_trades_history(date_from=date_from, group=group, to_df=False) or []
|
|
1685
|
-
)
|
|
1686
|
-
positions_ids = set(
|
|
1687
|
-
[deal.position_id for deal in history if deal.magic == self.expert_id]
|
|
1688
|
-
)
|
|
1689
|
-
today_deals = []
|
|
1690
|
-
for position in positions_ids:
|
|
1691
|
-
deal = self.get_trades_history(
|
|
1692
|
-
date_from=date_from, position=position, to_df=False
|
|
1693
|
-
)
|
|
1694
|
-
if deal is not None and len(deal) == 2:
|
|
1695
|
-
deal_time = datetime.fromtimestamp(deal[1].time)
|
|
1696
|
-
if deal_time.date() == datetime.now().date():
|
|
1697
|
-
today_deals.append(deal[1])
|
|
1698
|
-
return today_deals
|
|
1709
|
+
def get_today_deals(self, group=None):
|
|
1710
|
+
return super().get_today_deals(self.expert_id, group=group)
|
|
1699
1711
|
|
|
1700
1712
|
def is_max_trades_reached(self) -> bool:
|
|
1701
1713
|
"""
|
bbstrader/metatrader/utils.py
CHANGED
|
@@ -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",
|
|
@@ -637,6 +638,8 @@ def raise_mt5_error(message: Optional[str] = None):
|
|
|
637
638
|
Raises:
|
|
638
639
|
MT5TerminalError: A specific exception based on the error code.
|
|
639
640
|
"""
|
|
641
|
+
if message and isinstance(message, Exception):
|
|
642
|
+
message = str(message)
|
|
640
643
|
error = _ERROR_CODE_TO_EXCEPTION_.get(MT5.last_error()[0])
|
|
641
644
|
if error is not None:
|
|
642
645
|
raise Exception(f"{error(None)} {message or MT5.last_error()[1]}")
|
bbstrader/models/__init__.py
CHANGED
|
@@ -3,7 +3,6 @@ The `models` module provides a foundational framework for implementing various q
|
|
|
3
3
|
|
|
4
4
|
It is designed to be a versatile base module for different types of models used in financial analysis and trading.
|
|
5
5
|
"""
|
|
6
|
-
from bbstrader.models.risk import * # noqa: F403
|
|
7
6
|
from bbstrader.models.optimization import * # noqa: F403
|
|
8
7
|
from bbstrader.models.portfolio import * # noqa: F403
|
|
9
8
|
from bbstrader.models.factors import * # noqa: F403
|
bbstrader/models/ml.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import warnings
|
|
3
|
-
from datetime import datetime
|
|
4
3
|
from itertools import product
|
|
5
4
|
from time import time
|
|
6
5
|
|
|
@@ -19,6 +18,7 @@ from alphalens.utils import (
|
|
|
19
18
|
rate_of_return,
|
|
20
19
|
std_conversion,
|
|
21
20
|
)
|
|
21
|
+
from loguru import logger as log
|
|
22
22
|
from scipy.stats import spearmanr
|
|
23
23
|
from sklearn.preprocessing import LabelEncoder, StandardScaler
|
|
24
24
|
|
|
@@ -165,6 +165,7 @@ class LightGBModel(object):
|
|
|
165
165
|
datastore: pd.HDFStore = "lgbdata.h5",
|
|
166
166
|
trainstore: pd.HDFStore = "lgbtrain.h5",
|
|
167
167
|
outstore: pd.HDFStore = "lgbout.h5",
|
|
168
|
+
logger=None,
|
|
168
169
|
):
|
|
169
170
|
"""
|
|
170
171
|
Args:
|
|
@@ -173,10 +174,12 @@ class LightGBModel(object):
|
|
|
173
174
|
datastore (str): The path to the HDF5 file for storing the model data.
|
|
174
175
|
trainstore (str): The path to the HDF5 file for storing the training data.
|
|
175
176
|
outstore (str): The path to the HDF5 file for storing the output data.
|
|
177
|
+
logger (Logger): Optional logger instance for logging messages. If not provided, a default logger will be used.
|
|
176
178
|
"""
|
|
177
179
|
self.datastore = datastore
|
|
178
180
|
self.trainstore = trainstore
|
|
179
181
|
self.outstore = outstore
|
|
182
|
+
self.logger = logger or log
|
|
180
183
|
if data is not None:
|
|
181
184
|
data.reset_index().to_hdf(path_or_buf=self.datastore, key="model_data")
|
|
182
185
|
|
|
@@ -243,11 +246,15 @@ class LightGBModel(object):
|
|
|
243
246
|
multi_level_index=False,
|
|
244
247
|
auto_adjust=True,
|
|
245
248
|
)
|
|
249
|
+
if prices.empty:
|
|
250
|
+
continue
|
|
246
251
|
prices["symbol"] = ticker
|
|
247
252
|
data.append(prices)
|
|
248
253
|
except: # noqa: E722
|
|
249
254
|
continue
|
|
250
255
|
data = pd.concat(data)
|
|
256
|
+
if "Adj Close" in data.columns:
|
|
257
|
+
data = data.drop(columns=["Adj Close"])
|
|
251
258
|
data = (
|
|
252
259
|
data.rename(columns={s: s.lower().replace(" ", "_") for s in data.columns})
|
|
253
260
|
.set_index("symbol", append=True)
|
|
@@ -255,8 +262,6 @@ class LightGBModel(object):
|
|
|
255
262
|
.sort_index()
|
|
256
263
|
.dropna()
|
|
257
264
|
)
|
|
258
|
-
if "adj_close" in data.columns:
|
|
259
|
-
data = data.drop(columns=["adj_close"])
|
|
260
265
|
return data
|
|
261
266
|
|
|
262
267
|
def download_metadata(self, tickers):
|
|
@@ -849,13 +854,23 @@ class LightGBModel(object):
|
|
|
849
854
|
data = store.select("stock_data")
|
|
850
855
|
data = data.set_index(["symbol", "date"]).sort_index()
|
|
851
856
|
data = data[~data.index.duplicated()]
|
|
852
|
-
|
|
853
|
-
data
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
857
|
+
try:
|
|
858
|
+
data = (
|
|
859
|
+
data.loc[idx[tickers, start:end], "open"]
|
|
860
|
+
.unstack("symbol")
|
|
861
|
+
.sort_index()
|
|
862
|
+
.shift(-1)
|
|
863
|
+
.tz_convert("UTC")
|
|
864
|
+
)
|
|
865
|
+
except TypeError:
|
|
866
|
+
data = (
|
|
867
|
+
data.loc[idx[tickers, start:end], "open"]
|
|
868
|
+
.unstack("symbol")
|
|
869
|
+
.sort_index()
|
|
870
|
+
.shift(-1)
|
|
871
|
+
.tz_localize("UTC")
|
|
872
|
+
)
|
|
873
|
+
return data
|
|
859
874
|
|
|
860
875
|
def plot_ic(self, lgb_ic, lgb_daily_ic, scope_params, lgb_train_params):
|
|
861
876
|
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(20, 5))
|
|
@@ -1086,13 +1101,22 @@ class LightGBModel(object):
|
|
|
1086
1101
|
# in order to compute the mean period-wise
|
|
1087
1102
|
# return earned on an equal-weighted portfolio invested in the daily factor quintiles
|
|
1088
1103
|
# for various holding periods:
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1104
|
+
try:
|
|
1105
|
+
factor = (
|
|
1106
|
+
best_predictions.iloc[:, :5]
|
|
1107
|
+
.mean(1)
|
|
1108
|
+
.dropna()
|
|
1109
|
+
.tz_convert("UTC", level="date")
|
|
1110
|
+
.swaplevel()
|
|
1111
|
+
)
|
|
1112
|
+
except TypeError:
|
|
1113
|
+
factor = (
|
|
1114
|
+
best_predictions.iloc[:, :5]
|
|
1115
|
+
.mean(1)
|
|
1116
|
+
.dropna()
|
|
1117
|
+
.tz_localize("UTC", level="date")
|
|
1118
|
+
.swaplevel()
|
|
1119
|
+
)
|
|
1096
1120
|
# Create AlphaLens Inputs
|
|
1097
1121
|
if verbose:
|
|
1098
1122
|
factor_data = get_clean_factor_and_forward_returns(
|
|
@@ -1134,6 +1158,8 @@ class LightGBModel(object):
|
|
|
1134
1158
|
elif mode == "live":
|
|
1135
1159
|
data[labels] = data[labels].fillna(0)
|
|
1136
1160
|
data = data.sort_index().dropna()
|
|
1161
|
+
else:
|
|
1162
|
+
raise ValueError("Mode must be either 'test' or 'live'.")
|
|
1137
1163
|
|
|
1138
1164
|
lgb_data = lgb.Dataset(
|
|
1139
1165
|
data=data[features],
|
|
@@ -1245,7 +1271,9 @@ class LightGBModel(object):
|
|
|
1245
1271
|
Usefull in Live Trading to ensure that the last date in the predictions
|
|
1246
1272
|
is the previous day, so it predicts today's returns.
|
|
1247
1273
|
"""
|
|
1274
|
+
last_date: pd.Timestamp
|
|
1248
1275
|
last_date = predictions.index.get_level_values("date").max()
|
|
1276
|
+
now = pd.Timestamp.now(tz="UTC")
|
|
1249
1277
|
try:
|
|
1250
1278
|
if last_date.tzinfo is None:
|
|
1251
1279
|
last_date = last_date.tz_localize("UTC")
|
|
@@ -1253,18 +1281,19 @@ class LightGBModel(object):
|
|
|
1253
1281
|
last_date = last_date.tz_convert("UTC")
|
|
1254
1282
|
last_date = last_date.normalize()
|
|
1255
1283
|
except Exception as e:
|
|
1256
|
-
|
|
1284
|
+
self.logger.error(f"Error getting last date: {e}")
|
|
1257
1285
|
try:
|
|
1258
|
-
days = 3 if
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
- (pd.Timestamp.now(tz="UTC") - pd.Timedelta(days=days)).normalize()
|
|
1262
|
-
)
|
|
1263
|
-
assert (
|
|
1264
|
-
td.days == days or last_date == (pd.Timestamp.now(tz="UTC")).normalize()
|
|
1265
|
-
)
|
|
1286
|
+
days = 3 if now.weekday() == 0 else 1
|
|
1287
|
+
time_delta = last_date - (now - pd.Timedelta(days=days)).normalize()
|
|
1288
|
+
assert time_delta.days == days or last_date == now.normalize()
|
|
1266
1289
|
return True
|
|
1267
1290
|
except AssertionError:
|
|
1291
|
+
yesterday = (now - pd.Timedelta(days=1)).normalize()
|
|
1292
|
+
last_friday = (now - pd.Timedelta(days=now.weekday() + 3)).normalize()
|
|
1293
|
+
self.logger.debug(
|
|
1294
|
+
f"Last date in predictions ({last_date}) is not equal to \
|
|
1295
|
+
yesterday ({yesterday}) or last Friday ({last_friday})"
|
|
1296
|
+
)
|
|
1268
1297
|
return False
|
|
1269
1298
|
|
|
1270
1299
|
def clean_stores(self, *stores):
|