bbstrader 0.3.3__py3-none-any.whl → 0.3.5__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 +10 -2
- bbstrader/apps/__init__.py +0 -0
- bbstrader/apps/_copier.py +664 -0
- bbstrader/btengine/strategy.py +163 -90
- bbstrader/compat.py +18 -10
- bbstrader/config.py +0 -16
- bbstrader/core/scripts.py +4 -3
- bbstrader/core/utils.py +5 -3
- bbstrader/metatrader/account.py +169 -29
- bbstrader/metatrader/analysis.py +7 -5
- bbstrader/metatrader/copier.py +61 -14
- bbstrader/metatrader/scripts.py +15 -2
- bbstrader/metatrader/trade.py +28 -24
- bbstrader/metatrader/utils.py +64 -0
- bbstrader/models/factors.py +17 -13
- bbstrader/models/ml.py +104 -54
- bbstrader/trading/execution.py +9 -8
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/METADATA +25 -28
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/RECORD +24 -22
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/trade.py
CHANGED
|
@@ -776,18 +776,19 @@ class Trade(RiskManagement):
|
|
|
776
776
|
# Check the execution result
|
|
777
777
|
pos = self._order_type()[type][1]
|
|
778
778
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
779
|
+
result = None
|
|
779
780
|
try:
|
|
780
781
|
self.check_order(request)
|
|
781
782
|
result = self.send_order(request)
|
|
782
783
|
except Exception as e:
|
|
783
|
-
msg = trade_retcode_message(result.retcode)
|
|
784
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
784
785
|
LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
|
|
785
|
-
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
786
|
+
if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
786
787
|
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
787
788
|
for fill in FILLING_TYPE:
|
|
788
789
|
request["type_filling"] = fill
|
|
789
790
|
result = self.send_order(request)
|
|
790
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
791
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
791
792
|
break
|
|
792
793
|
elif result.retcode == Mt5.TRADE_RETCODE_INVALID_VOLUME: # 10014
|
|
793
794
|
new_volume = int(request["volume"])
|
|
@@ -811,14 +812,14 @@ class Trade(RiskManagement):
|
|
|
811
812
|
self.check_order(request)
|
|
812
813
|
result = self.send_order(request)
|
|
813
814
|
except Exception as e:
|
|
814
|
-
msg = trade_retcode_message(result.retcode)
|
|
815
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
815
816
|
LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
|
|
816
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
817
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
817
818
|
break
|
|
818
819
|
tries += 1
|
|
819
820
|
# Print the result
|
|
820
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
821
|
-
msg = trade_retcode_message(result.retcode)
|
|
821
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
822
|
+
msg = trade_retcode_message(result.retcode)
|
|
822
823
|
LOGGER.info(f"Trade Order {msg}{addtionnal}")
|
|
823
824
|
if type != "BMKT" or type != "SMKT":
|
|
824
825
|
self.opened_orders.append(result.order)
|
|
@@ -854,7 +855,7 @@ class Trade(RiskManagement):
|
|
|
854
855
|
LOGGER.info(pos_info)
|
|
855
856
|
return True
|
|
856
857
|
else:
|
|
857
|
-
msg = trade_retcode_message(result.retcode)
|
|
858
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
858
859
|
LOGGER.error(
|
|
859
860
|
f"Unable to Open Position, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
860
861
|
)
|
|
@@ -1325,15 +1326,16 @@ class Trade(RiskManagement):
|
|
|
1325
1326
|
request (dict): The request to set the stop loss to break even.
|
|
1326
1327
|
"""
|
|
1327
1328
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
1329
|
+
result = None
|
|
1328
1330
|
time.sleep(0.1)
|
|
1329
1331
|
try:
|
|
1330
1332
|
self.check_order(request)
|
|
1331
1333
|
result = self.send_order(request)
|
|
1332
1334
|
except Exception as e:
|
|
1333
|
-
msg = trade_retcode_message(result.retcode)
|
|
1335
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
1334
1336
|
LOGGER.error(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
|
|
1335
|
-
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1336
|
-
msg = trade_retcode_message(result.retcode)
|
|
1337
|
+
if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1338
|
+
msg = trade_retcode_message(result.retcode)
|
|
1337
1339
|
if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
1338
1340
|
LOGGER.error(
|
|
1339
1341
|
f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
@@ -1348,15 +1350,15 @@ class Trade(RiskManagement):
|
|
|
1348
1350
|
self.check_order(request)
|
|
1349
1351
|
result = self.send_order(request)
|
|
1350
1352
|
except Exception as e:
|
|
1351
|
-
msg = trade_retcode_message(result.retcode)
|
|
1353
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
1352
1354
|
LOGGER.error(
|
|
1353
1355
|
f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}"
|
|
1354
1356
|
)
|
|
1355
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1357
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1356
1358
|
break
|
|
1357
1359
|
tries += 1
|
|
1358
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1359
|
-
msg = trade_retcode_message(result.retcode)
|
|
1360
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1361
|
+
msg = trade_retcode_message(result.retcode)
|
|
1360
1362
|
LOGGER.info(f"Break-Even Order {msg}{addtionnal}")
|
|
1361
1363
|
info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{round(price, 5)}"
|
|
1362
1364
|
LOGGER.info(info)
|
|
@@ -1432,15 +1434,17 @@ class Trade(RiskManagement):
|
|
|
1432
1434
|
"""
|
|
1433
1435
|
ticket = request[type]
|
|
1434
1436
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
1437
|
+
result = None
|
|
1435
1438
|
try:
|
|
1436
1439
|
self.check_order(request)
|
|
1437
1440
|
result = self.send_order(request)
|
|
1438
1441
|
except Exception as e:
|
|
1439
|
-
msg = trade_retcode_message(result.retcode)
|
|
1442
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
1440
1443
|
LOGGER.error(
|
|
1441
|
-
f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
|
|
1444
|
+
f"Closing {type.capitalize()} Request, RETCODE={msg}{addtionnal}, Error: {e}"
|
|
1442
1445
|
)
|
|
1443
|
-
|
|
1446
|
+
|
|
1447
|
+
if result and result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1444
1448
|
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
1445
1449
|
for fill in FILLING_TYPE:
|
|
1446
1450
|
request["type_filling"] = fill
|
|
@@ -1462,14 +1466,14 @@ class Trade(RiskManagement):
|
|
|
1462
1466
|
self.check_order(request)
|
|
1463
1467
|
result = self.send_order(request)
|
|
1464
1468
|
except Exception as e:
|
|
1465
|
-
msg = trade_retcode_message(result.retcode)
|
|
1469
|
+
msg = trade_retcode_message(result.retcode) if result else "N/A"
|
|
1466
1470
|
LOGGER.error(
|
|
1467
1471
|
f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
|
|
1468
1472
|
)
|
|
1469
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1473
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1470
1474
|
break
|
|
1471
1475
|
tries += 1
|
|
1472
|
-
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1476
|
+
if result and result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1473
1477
|
msg = trade_retcode_message(result.retcode)
|
|
1474
1478
|
LOGGER.info(f"Closing Order {msg}{addtionnal}")
|
|
1475
1479
|
info = (
|
|
@@ -1504,7 +1508,7 @@ class Trade(RiskManagement):
|
|
|
1504
1508
|
orders = self.get_orders(ticket=ticket) or []
|
|
1505
1509
|
if len(orders) == 0:
|
|
1506
1510
|
LOGGER.error(
|
|
1507
|
-
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={round(price, 5)}"
|
|
1511
|
+
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={round(price, 5) if price else 'N/A'}"
|
|
1508
1512
|
)
|
|
1509
1513
|
return
|
|
1510
1514
|
order = orders[0]
|
|
@@ -1520,8 +1524,8 @@ class Trade(RiskManagement):
|
|
|
1520
1524
|
result = self.send_order(request)
|
|
1521
1525
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1522
1526
|
LOGGER.info(
|
|
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)}"
|
|
1527
|
+
f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={round(request['price'], 5)},"
|
|
1528
|
+
f"SL={round(request['sl'], 5)}, TP={round(request['tp'], 5)}, STOP_LIMIT={round(request['stoplimit'], 5)}"
|
|
1525
1529
|
)
|
|
1526
1530
|
else:
|
|
1527
1531
|
msg = trade_retcode_message(result.retcode)
|
bbstrader/metatrader/utils.py
CHANGED
|
@@ -2,6 +2,8 @@ from datetime import datetime
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import NamedTuple, Optional
|
|
4
4
|
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
5
7
|
try:
|
|
6
8
|
import MetaTrader5 as MT5
|
|
7
9
|
except ImportError:
|
|
@@ -28,6 +30,10 @@ __all__ = [
|
|
|
28
30
|
"HistoryNotFound",
|
|
29
31
|
"InvalidVersion",
|
|
30
32
|
"AuthFailed",
|
|
33
|
+
"RateInfo",
|
|
34
|
+
"RateDtype",
|
|
35
|
+
"TickDtype",
|
|
36
|
+
"TickFlag",
|
|
31
37
|
"UnsupportedMethod",
|
|
32
38
|
"AutoTradingDisabled",
|
|
33
39
|
"InternalFailSend",
|
|
@@ -285,6 +291,26 @@ class SymbolType(Enum):
|
|
|
285
291
|
unknown = "UNKNOWN" # Unknown or unsupported type
|
|
286
292
|
|
|
287
293
|
|
|
294
|
+
TickDtype = np.dtype(
|
|
295
|
+
[
|
|
296
|
+
("time", "<i8"),
|
|
297
|
+
("bid", "<f8"),
|
|
298
|
+
("ask", "<f8"),
|
|
299
|
+
("last", "<f8"),
|
|
300
|
+
("volume", "<u8"),
|
|
301
|
+
("time_msc", "<i8"),
|
|
302
|
+
("flags", "<u4"),
|
|
303
|
+
("volume_real", "<f8"),
|
|
304
|
+
]
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
TickFlag = {
|
|
308
|
+
"all": MT5.COPY_TICKS_ALL,
|
|
309
|
+
"info": MT5.COPY_TICKS_INFO,
|
|
310
|
+
"trade": MT5.COPY_TICKS_TRADE,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
288
314
|
class TickInfo(NamedTuple):
|
|
289
315
|
"""
|
|
290
316
|
Represents the last tick for the specified financial instrument.
|
|
@@ -308,6 +334,44 @@ class TickInfo(NamedTuple):
|
|
|
308
334
|
volume_real: float
|
|
309
335
|
|
|
310
336
|
|
|
337
|
+
RateDtype = np.dtype(
|
|
338
|
+
[
|
|
339
|
+
("time", "<i8"),
|
|
340
|
+
("open", "<f8"),
|
|
341
|
+
("high", "<f8"),
|
|
342
|
+
("low", "<f8"),
|
|
343
|
+
("close", "<f8"),
|
|
344
|
+
("tick_volume", "<u8"),
|
|
345
|
+
("spread", "<i4"),
|
|
346
|
+
("real_volume", "<u8"),
|
|
347
|
+
]
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class RateInfo(NamedTuple):
|
|
352
|
+
"""
|
|
353
|
+
Reprents a candle (bar) for a specified period.
|
|
354
|
+
* time: Time in seconds since 1970.01.01 00:00
|
|
355
|
+
* open: Open price
|
|
356
|
+
* high: High price
|
|
357
|
+
* low: Low price
|
|
358
|
+
* close: Close price
|
|
359
|
+
* tick_volume: Tick volume
|
|
360
|
+
* spread: Spread value
|
|
361
|
+
* real_volume: Real volume
|
|
362
|
+
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
time: int
|
|
366
|
+
open: float
|
|
367
|
+
high: float
|
|
368
|
+
low: float
|
|
369
|
+
close: float
|
|
370
|
+
tick_volume: float
|
|
371
|
+
spread: int
|
|
372
|
+
real_volume: float
|
|
373
|
+
|
|
374
|
+
|
|
311
375
|
class BookInfo(NamedTuple):
|
|
312
376
|
"""
|
|
313
377
|
Represents the structure of a book.
|
bbstrader/models/factors.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import Dict, List
|
|
2
|
+
from typing import Dict, List, Literal
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
import yfinance as yf
|
|
6
|
+
from loguru import logger
|
|
6
7
|
|
|
7
8
|
from bbstrader.btengine.data import EODHDataHandler, FMPDataHandler
|
|
8
9
|
from bbstrader.metatrader.rates import download_historical_data
|
|
@@ -16,6 +17,7 @@ __all__ = [
|
|
|
16
17
|
"search_coint_candidate_pairs",
|
|
17
18
|
]
|
|
18
19
|
|
|
20
|
+
|
|
19
21
|
def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
|
|
20
22
|
"""Download and process data for a list of tickers from the specified source."""
|
|
21
23
|
data_list = []
|
|
@@ -43,9 +45,7 @@ def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
|
|
|
43
45
|
)
|
|
44
46
|
data = data.drop(columns=["adj_close"], axis=1)
|
|
45
47
|
elif source in ["fmp", "eodhd"]:
|
|
46
|
-
handler_class =
|
|
47
|
-
FMPDataHandler if source == "fmp" else EODHDataHandler
|
|
48
|
-
)
|
|
48
|
+
handler_class = FMPDataHandler if source == "fmp" else EODHDataHandler
|
|
49
49
|
handler = handler_class(events=None, symbol_list=[ticker], **kwargs)
|
|
50
50
|
data = handler.data[ticker]
|
|
51
51
|
else:
|
|
@@ -62,6 +62,7 @@ def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
|
|
|
62
62
|
|
|
63
63
|
return pd.concat(data_list)
|
|
64
64
|
|
|
65
|
+
|
|
65
66
|
def _handle_date_range(start, end, window):
|
|
66
67
|
"""Handle start and end date generation."""
|
|
67
68
|
if start is None or end is None:
|
|
@@ -73,6 +74,7 @@ def _handle_date_range(start, end, window):
|
|
|
73
74
|
).strftime("%Y-%m-%d")
|
|
74
75
|
return start, end
|
|
75
76
|
|
|
77
|
+
|
|
76
78
|
def _period_search(start, end, securities, candidates, window, npairs):
|
|
77
79
|
if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
|
|
78
80
|
raise ValueError(
|
|
@@ -103,14 +105,11 @@ def _period_search(start, end, securities, candidates, window, npairs):
|
|
|
103
105
|
)
|
|
104
106
|
return top_pairs.head(npairs * 2)
|
|
105
107
|
|
|
108
|
+
|
|
106
109
|
def _process_asset_data(securities, candidates, universe, rolling_window):
|
|
107
110
|
"""Process and select assets from the data."""
|
|
108
|
-
securities = select_assets(
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
candidates = select_assets(
|
|
112
|
-
candidates, n=universe, rolling_window=rolling_window
|
|
113
|
-
)
|
|
111
|
+
securities = select_assets(securities, n=universe, rolling_window=rolling_window)
|
|
112
|
+
candidates = select_assets(candidates, n=universe, rolling_window=rolling_window)
|
|
114
113
|
return securities, candidates
|
|
115
114
|
|
|
116
115
|
|
|
@@ -121,7 +120,7 @@ def search_coint_candidate_pairs(
|
|
|
121
120
|
end: str = None,
|
|
122
121
|
period_search: bool = False,
|
|
123
122
|
select: bool = True,
|
|
124
|
-
source:
|
|
123
|
+
source: Literal["yf", "mt5", "fmp", "eodhd"] = None,
|
|
125
124
|
universe: int = 100,
|
|
126
125
|
window: int = 2,
|
|
127
126
|
rolling_window: int = None,
|
|
@@ -257,7 +256,9 @@ def search_coint_candidate_pairs(
|
|
|
257
256
|
if period_search:
|
|
258
257
|
start = securities.index.get_level_values("date").min()
|
|
259
258
|
end = securities.index.get_level_values("date").max()
|
|
260
|
-
top_pairs = _period_search(
|
|
259
|
+
top_pairs = _period_search(
|
|
260
|
+
start, end, securities, candidates, window, npairs
|
|
261
|
+
)
|
|
261
262
|
else:
|
|
262
263
|
top_pairs = find_cointegrated_pairs(
|
|
263
264
|
securities, candidates, n=npairs, coint=True
|
|
@@ -286,6 +287,10 @@ def search_coint_candidate_pairs(
|
|
|
286
287
|
candidates_data = _download_and_process_data(
|
|
287
288
|
source, candidates, start, end, tf, path, **kwargs
|
|
288
289
|
)
|
|
290
|
+
if securities_data.empty or candidates_data.empty:
|
|
291
|
+
logger.error("No data found for candidates and securities")
|
|
292
|
+
return [] if select else pd.DataFrame()
|
|
293
|
+
|
|
289
294
|
securities_data = securities_data.set_index(["ticker", "date"])
|
|
290
295
|
candidates_data = candidates_data.set_index(["ticker", "date"])
|
|
291
296
|
securities_data, candidates_data = _process_asset_data(
|
|
@@ -305,7 +310,6 @@ def search_coint_candidate_pairs(
|
|
|
305
310
|
)
|
|
306
311
|
else:
|
|
307
312
|
return top_pairs
|
|
308
|
-
|
|
309
313
|
else:
|
|
310
314
|
msg = (
|
|
311
315
|
"Invalid input. Either provide securities"
|
bbstrader/models/ml.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import warnings
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
3
4
|
from itertools import product
|
|
4
5
|
from time import time
|
|
5
6
|
|
|
@@ -7,7 +8,6 @@ import lightgbm as lgb
|
|
|
7
8
|
import matplotlib.pyplot as plt
|
|
8
9
|
import numpy as np
|
|
9
10
|
import pandas as pd
|
|
10
|
-
import pandas_ta as ta
|
|
11
11
|
import seaborn as sns
|
|
12
12
|
import yfinance as yf
|
|
13
13
|
from alphalens import performance as perf
|
|
@@ -22,15 +22,30 @@ from loguru import logger as log
|
|
|
22
22
|
from scipy.stats import spearmanr
|
|
23
23
|
from sklearn.preprocessing import LabelEncoder, StandardScaler
|
|
24
24
|
|
|
25
|
+
try:
|
|
26
|
+
# This is to fix posix import error in pandas_ta
|
|
27
|
+
# On windows systems
|
|
28
|
+
import posix # noqa: F401
|
|
29
|
+
except (ImportError, Exception):
|
|
30
|
+
import bbstrader.compat # noqa: F401
|
|
31
|
+
|
|
32
|
+
import pandas_ta as ta
|
|
33
|
+
|
|
25
34
|
warnings.filterwarnings("ignore")
|
|
26
35
|
|
|
27
36
|
__all__ = ["OneStepTimeSeriesSplit", "MultipleTimeSeriesCV", "LightGBModel"]
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
class OneStepTimeSeriesSplit:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Assumes the index contains a level labeled 'date'
|
|
40
|
+
"""
|
|
41
|
+
Generates tuples of train_idx, test_idx pairs
|
|
42
|
+
Assumes the index contains a level labeled 'date'
|
|
43
|
+
|
|
44
|
+
References
|
|
45
|
+
----------
|
|
46
|
+
Stefan Jansen (2020). Machine Learning for Algorithmic Trading - Second Edition.
|
|
47
|
+
Chapter 12, Boosting Your Trading Strategy.
|
|
48
|
+
"""
|
|
34
49
|
|
|
35
50
|
def __init__(self, n_splits=3, test_period_length=1, shuffle=False):
|
|
36
51
|
self.n_splits = n_splits
|
|
@@ -62,11 +77,15 @@ class OneStepTimeSeriesSplit:
|
|
|
62
77
|
|
|
63
78
|
|
|
64
79
|
class MultipleTimeSeriesCV:
|
|
65
|
-
__author__ = "Stefan Jansen"
|
|
66
80
|
"""
|
|
67
81
|
Generates tuples of train_idx, test_idx pairs
|
|
68
82
|
Assumes the MultiIndex contains levels 'symbol' and 'date'
|
|
69
83
|
purges overlapping outcomes
|
|
84
|
+
|
|
85
|
+
References
|
|
86
|
+
----------
|
|
87
|
+
Stefan Jansen (2020). Machine Learning for Algorithmic Trading - Second Edition.
|
|
88
|
+
Chapter 12, Boosting Your Trading Strategy.
|
|
70
89
|
"""
|
|
71
90
|
|
|
72
91
|
def __init__(
|
|
@@ -187,7 +206,7 @@ class LightGBModel(object):
|
|
|
187
206
|
# Compute Bollinger Bands using pandas_ta
|
|
188
207
|
bb = ta.bbands(close, length=20)
|
|
189
208
|
return pd.DataFrame(
|
|
190
|
-
{"bb_high": bb["BBU_20_2.0"], "bb_low": bb["BBL_20_2.0"]}, index=close.index
|
|
209
|
+
{"bb_high": bb["BBU_20_2.0_2.0"], "bb_low": bb["BBL_20_2.0_2.0"]}, index=close.index
|
|
191
210
|
)
|
|
192
211
|
|
|
193
212
|
def _compute_atr(self, stock_data):
|
|
@@ -235,26 +254,29 @@ class LightGBModel(object):
|
|
|
235
254
|
return prices
|
|
236
255
|
|
|
237
256
|
def download_boosting_data(self, tickers, start, end=None):
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
try:
|
|
258
|
+
data = yf.download(
|
|
259
|
+
tickers,
|
|
260
|
+
start=start,
|
|
261
|
+
end=end,
|
|
262
|
+
progress=False,
|
|
263
|
+
auto_adjust=True,
|
|
264
|
+
threads=True,
|
|
265
|
+
)
|
|
266
|
+
if data.empty:
|
|
267
|
+
return pd.DataFrame()
|
|
268
|
+
|
|
269
|
+
data = (
|
|
270
|
+
data.stack(level=1).rename_axis(["Date", "symbol"]).reset_index(level=1)
|
|
271
|
+
)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
self.logger.error(f"An error occurred during data download: {e}")
|
|
274
|
+
return pd.DataFrame()
|
|
275
|
+
|
|
276
|
+
# The rest of the data processing is the same as the original function
|
|
256
277
|
if "Adj Close" in data.columns:
|
|
257
278
|
data = data.drop(columns=["Adj Close"])
|
|
279
|
+
|
|
258
280
|
data = (
|
|
259
281
|
data.rename(columns={s: s.lower().replace(" ", "_") for s in data.columns})
|
|
260
282
|
.set_index("symbol", append=True)
|
|
@@ -265,17 +287,11 @@ class LightGBModel(object):
|
|
|
265
287
|
return data
|
|
266
288
|
|
|
267
289
|
def download_metadata(self, tickers):
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# use regex=False for literal string replacements
|
|
272
|
-
.str.replace("-", "", regex=False)
|
|
273
|
-
.str.replace("&", "and", regex=False)
|
|
274
|
-
.str.replace(" ", "_", regex=False)
|
|
275
|
-
.str.replace("__", "_", regex=False)
|
|
276
|
-
)
|
|
290
|
+
"""
|
|
291
|
+
Downloads metadata for multiple tickers concurrently using a thread pool.
|
|
292
|
+
"""
|
|
277
293
|
|
|
278
|
-
|
|
294
|
+
METADATA_KEYS = [
|
|
279
295
|
"industry",
|
|
280
296
|
"sector",
|
|
281
297
|
"exchange",
|
|
@@ -296,7 +312,7 @@ class LightGBModel(object):
|
|
|
296
312
|
"marketCap",
|
|
297
313
|
]
|
|
298
314
|
|
|
299
|
-
|
|
315
|
+
COLUMN_MAP = {
|
|
300
316
|
"industry": "industry",
|
|
301
317
|
"sector": "sector",
|
|
302
318
|
"exchange": "exchange",
|
|
@@ -316,22 +332,53 @@ class LightGBModel(object):
|
|
|
316
332
|
"askSize": "asksize",
|
|
317
333
|
"marketCap": "marketcap",
|
|
318
334
|
}
|
|
319
|
-
|
|
320
|
-
|
|
335
|
+
|
|
336
|
+
def _clean_text_column(series: pd.Series) -> pd.Series:
|
|
337
|
+
"""Helper function to clean text columns."""
|
|
338
|
+
return (
|
|
339
|
+
series.str.lower()
|
|
340
|
+
.str.replace("-", "", regex=False)
|
|
341
|
+
.str.replace("&", "and", regex=False)
|
|
342
|
+
.str.replace(" ", "_", regex=False)
|
|
343
|
+
.str.replace("__", "_", regex=False)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def _fetch_single_ticker_info(symbol):
|
|
347
|
+
"""Worker function to fetch and process info for one ticker."""
|
|
321
348
|
try:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
349
|
+
ticker_info = yf.Ticker(symbol).info
|
|
350
|
+
return {key: ticker_info.get(key) for key in METADATA_KEYS}
|
|
351
|
+
except Exception:
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
data = []
|
|
355
|
+
with ThreadPoolExecutor(max_workers=20) as executor:
|
|
356
|
+
future_to_ticker = {
|
|
357
|
+
executor.submit(_fetch_single_ticker_info, ticker): ticker
|
|
358
|
+
for ticker in tickers
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for future in as_completed(future_to_ticker):
|
|
362
|
+
result = future.result()
|
|
363
|
+
if result:
|
|
364
|
+
data.append(result)
|
|
365
|
+
|
|
366
|
+
if not data:
|
|
367
|
+
return pd.DataFrame()
|
|
368
|
+
|
|
329
369
|
metadata = pd.DataFrame(data)
|
|
330
|
-
metadata = metadata.rename(columns=
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
370
|
+
metadata = metadata.rename(columns=COLUMN_MAP)
|
|
371
|
+
|
|
372
|
+
if "dyield" in metadata.columns:
|
|
373
|
+
metadata.dyield = metadata.dyield.fillna(0)
|
|
374
|
+
if "sector" in metadata.columns:
|
|
375
|
+
metadata.sector = _clean_text_column(metadata.sector)
|
|
376
|
+
if "industry" in metadata.columns:
|
|
377
|
+
metadata.industry = _clean_text_column(metadata.industry)
|
|
378
|
+
|
|
379
|
+
if "symbol" in metadata.columns:
|
|
380
|
+
metadata = metadata.set_index("symbol")
|
|
381
|
+
|
|
335
382
|
return metadata
|
|
336
383
|
|
|
337
384
|
def _select_nlargest_liquidity_stocks(
|
|
@@ -1283,16 +1330,19 @@ class LightGBModel(object):
|
|
|
1283
1330
|
except Exception as e:
|
|
1284
1331
|
self.logger.error(f"Error getting last date: {e}")
|
|
1285
1332
|
try:
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1333
|
+
if now.weekday() == 0: # Monday
|
|
1334
|
+
expected_date = (now - pd.Timedelta(days=3)).normalize() # last Friday
|
|
1335
|
+
else:
|
|
1336
|
+
expected_date = (now - pd.Timedelta(days=1)).normalize() # yesterday
|
|
1337
|
+
|
|
1338
|
+
assert last_date == expected_date or last_date == now.normalize()
|
|
1289
1339
|
return True
|
|
1290
1340
|
except AssertionError:
|
|
1291
1341
|
yesterday = (now - pd.Timedelta(days=1)).normalize()
|
|
1292
1342
|
last_friday = (now - pd.Timedelta(days=now.weekday() + 3)).normalize()
|
|
1293
1343
|
self.logger.debug(
|
|
1294
|
-
f"Last date in predictions ({last_date}) is not equal to
|
|
1295
|
-
yesterday ({yesterday}) or last Friday ({last_friday})"
|
|
1344
|
+
f"Last date in predictions ({last_date}) is not equal to "
|
|
1345
|
+
f"yesterday ({yesterday}) or last Friday ({last_friday})"
|
|
1296
1346
|
)
|
|
1297
1347
|
return False
|
|
1298
1348
|
|
bbstrader/trading/execution.py
CHANGED
|
@@ -192,8 +192,8 @@ class Mt5ExecutionEngine:
|
|
|
192
192
|
show_positions_orders: bool = False,
|
|
193
193
|
iter_time: int | float = 5,
|
|
194
194
|
use_trade_time: bool = True,
|
|
195
|
-
period: Literal["24/7", "day", "week", "month"] = "
|
|
196
|
-
period_end_action: Literal["break", "sleep"] = "
|
|
195
|
+
period: Literal["24/7", "day", "week", "month"] = "month",
|
|
196
|
+
period_end_action: Literal["break", "sleep"] = "sleep",
|
|
197
197
|
closing_pnl: Optional[float] = None,
|
|
198
198
|
trading_days: Optional[List[str]] = None,
|
|
199
199
|
comment: Optional[str] = None,
|
|
@@ -631,12 +631,12 @@ class Mt5ExecutionEngine:
|
|
|
631
631
|
def _update_risk(self, weights):
|
|
632
632
|
try:
|
|
633
633
|
check_mt5_connection(**self.kwargs)
|
|
634
|
-
if weights is not None:
|
|
634
|
+
if weights is not None and not all(v == 0 for v in weights.values()):
|
|
635
|
+
assert self.daily_risk is not None
|
|
635
636
|
for symbol in self.symbols:
|
|
636
637
|
if symbol not in weights:
|
|
637
638
|
continue
|
|
638
639
|
trade = self.trades_instances[symbol]
|
|
639
|
-
assert self.daily_risk is not None
|
|
640
640
|
dailydd = round(weights[symbol] * self.daily_risk, 5)
|
|
641
641
|
trade.dailydd = dailydd
|
|
642
642
|
except Exception as e:
|
|
@@ -930,7 +930,6 @@ class Mt5ExecutionEngine:
|
|
|
930
930
|
return
|
|
931
931
|
|
|
932
932
|
if not signals:
|
|
933
|
-
logger.info("No signals to process.")
|
|
934
933
|
return
|
|
935
934
|
|
|
936
935
|
# We want to create a temporary function that
|
|
@@ -994,14 +993,12 @@ class Mt5ExecutionEngine:
|
|
|
994
993
|
self._sleep()
|
|
995
994
|
self._handle_period_end_actions(today)
|
|
996
995
|
except KeyboardInterrupt:
|
|
997
|
-
logger.info(
|
|
998
|
-
f"Stopping Execution Engine for {self.STRATEGY} STRATEGY on {self.ACCOUNT} Account"
|
|
999
|
-
)
|
|
1000
996
|
self.stop()
|
|
1001
997
|
sys.exit(0)
|
|
1002
998
|
except Exception as e:
|
|
1003
999
|
msg = f"Running Execution Engine, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
1004
1000
|
self._print_exc(msg, e)
|
|
1001
|
+
self._sleep()
|
|
1005
1002
|
|
|
1006
1003
|
def stop(self):
|
|
1007
1004
|
"""Stops the execution engine."""
|
|
@@ -1038,6 +1035,10 @@ def RunMt5Engine(account_id: str, **kwargs):
|
|
|
1038
1035
|
symbol_list, trades_instances, strategy_cls, **kwargs
|
|
1039
1036
|
)
|
|
1040
1037
|
engine.run()
|
|
1038
|
+
except KeyboardInterrupt:
|
|
1039
|
+
log.info(f"Execution engine for {account_id} interrupted by user")
|
|
1040
|
+
engine.stop()
|
|
1041
|
+
sys.exit(0)
|
|
1041
1042
|
except Exception as e:
|
|
1042
1043
|
log.exception(f"Error running execution engine for {account_id}: {e}")
|
|
1043
1044
|
finally:
|