bbstrader 0.3.1__py3-none-any.whl → 0.3.3__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 +7 -5
- bbstrader/btengine/backtest.py +7 -8
- bbstrader/btengine/data.py +3 -3
- bbstrader/btengine/execution.py +2 -2
- bbstrader/btengine/strategy.py +70 -17
- bbstrader/config.py +2 -2
- bbstrader/core/data.py +3 -1
- bbstrader/core/scripts.py +62 -19
- bbstrader/metatrader/account.py +108 -23
- bbstrader/metatrader/copier.py +753 -280
- bbstrader/metatrader/rates.py +2 -2
- bbstrader/metatrader/risk.py +1 -0
- bbstrader/metatrader/scripts.py +35 -9
- bbstrader/metatrader/trade.py +60 -43
- bbstrader/metatrader/utils.py +3 -5
- bbstrader/models/__init__.py +0 -1
- bbstrader/models/ml.py +55 -26
- bbstrader/models/nlp.py +159 -89
- bbstrader/models/optimization.py +1 -1
- bbstrader/models/risk.py +16 -386
- bbstrader/trading/execution.py +109 -50
- bbstrader/trading/strategies.py +9 -592
- bbstrader/tseries.py +39 -711
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/METADATA +36 -41
- bbstrader-0.3.3.dist-info/RECORD +47 -0
- bbstrader-0.3.1.dist-info/RECORD +0 -47
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/rates.py
CHANGED
|
@@ -124,11 +124,11 @@ class Rates(object):
|
|
|
124
124
|
self.sd = session_duration
|
|
125
125
|
self.start_pos = self._get_start_pos(start_pos, timeframe)
|
|
126
126
|
self.count = count
|
|
127
|
-
self.
|
|
127
|
+
self.__initializ_mt5(**kwargs)
|
|
128
128
|
self.__account = Account(**kwargs)
|
|
129
129
|
self.__data = self.get_rates_from_pos()
|
|
130
130
|
|
|
131
|
-
def
|
|
131
|
+
def __initializ_mt5(self, **kwargs):
|
|
132
132
|
check_mt5_connection(**kwargs)
|
|
133
133
|
|
|
134
134
|
def _get_start_pos(self, index, time_frame):
|
bbstrader/metatrader/risk.py
CHANGED
bbstrader/metatrader/scripts.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import multiprocessing
|
|
2
3
|
import sys
|
|
3
4
|
|
|
4
|
-
from bbstrader.metatrader.copier import RunCopier, config_copier
|
|
5
5
|
from bbstrader.apps._copier import main as RunCopyAPP
|
|
6
|
+
from bbstrader.metatrader.copier import RunCopier, config_copier, copier_worker_process
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def copier_args(parser: argparse.ArgumentParser):
|
|
@@ -52,6 +53,12 @@ def copier_args(parser: argparse.ArgumentParser):
|
|
|
52
53
|
default=None,
|
|
53
54
|
help="End time in HH:MM format",
|
|
54
55
|
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"-M",
|
|
58
|
+
"--multiprocess",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Run each destination account in a separate process.",
|
|
61
|
+
)
|
|
55
62
|
return parser
|
|
56
63
|
|
|
57
64
|
|
|
@@ -65,6 +72,7 @@ def copy_trades(unknown):
|
|
|
65
72
|
-s, --source: Source Account section name
|
|
66
73
|
-d, --destinations: Destination Account section names (multiple allowed)
|
|
67
74
|
-i, --interval: Update interval in seconds
|
|
75
|
+
-M, --multiprocess: When set to True, each destination account runs in a separate process.
|
|
68
76
|
-c, --config: .ini file or path (default: ~/.bbstrader/copier/copier.ini)
|
|
69
77
|
-t, --start: Start time in HH:MM format
|
|
70
78
|
-e, --end: End time in HH:MM format
|
|
@@ -79,17 +87,35 @@ def copy_trades(unknown):
|
|
|
79
87
|
|
|
80
88
|
if copy_args.mode == "GUI":
|
|
81
89
|
RunCopyAPP()
|
|
82
|
-
|
|
90
|
+
|
|
83
91
|
elif copy_args.mode == "CLI":
|
|
84
92
|
source, destinations = config_copier(
|
|
85
93
|
source_section=copy_args.source,
|
|
86
94
|
dest_sections=copy_args.destinations,
|
|
87
95
|
inifile=copy_args.config,
|
|
88
96
|
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
destinations
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
@@ -138,6 +140,38 @@ class TradeSignal:
|
|
|
138
140
|
)
|
|
139
141
|
|
|
140
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
|
+
|
|
141
175
|
class TradingMode(Enum):
|
|
142
176
|
BACKTEST = "BACKTEST"
|
|
143
177
|
LIVE = "LIVE"
|
|
@@ -224,7 +258,7 @@ class Trade(RiskManagement):
|
|
|
224
258
|
symbol: str = "EURUSD",
|
|
225
259
|
expert_name: str = "bbstrader",
|
|
226
260
|
expert_id: int = EXPERT_ID,
|
|
227
|
-
version: str = "
|
|
261
|
+
version: str = "3.0",
|
|
228
262
|
target: float = 5.0,
|
|
229
263
|
start_time: str = "0:00",
|
|
230
264
|
finishing_time: str = "23:59",
|
|
@@ -556,7 +590,7 @@ class Trade(RiskManagement):
|
|
|
556
590
|
else:
|
|
557
591
|
raise ValueError("You need to set a price for pending orders")
|
|
558
592
|
else:
|
|
559
|
-
_price = self.get_tick_info(self.symbol).
|
|
593
|
+
_price = self.get_tick_info(self.symbol).bid
|
|
560
594
|
|
|
561
595
|
lot = volume or self.get_lot()
|
|
562
596
|
stop_loss = self.get_stop_loss()
|
|
@@ -592,13 +626,15 @@ class Trade(RiskManagement):
|
|
|
592
626
|
request["tp"] = tp or mm_price + take_profit * point
|
|
593
627
|
self.break_even(mm=mm, id=Id, trail=trail)
|
|
594
628
|
if self.check(comment):
|
|
629
|
+
if action == "BSTPLMT":
|
|
630
|
+
_price = stoplimit
|
|
595
631
|
return self.request_result(_price, request, action)
|
|
596
632
|
return False
|
|
597
633
|
|
|
598
634
|
def _order_type(self):
|
|
599
635
|
return {
|
|
600
636
|
"BMKT": (Mt5.ORDER_TYPE_BUY, "BUY"),
|
|
601
|
-
"SMKT": (Mt5.
|
|
637
|
+
"SMKT": (Mt5.ORDER_TYPE_SELL, "SELL"),
|
|
602
638
|
"BLMT": (Mt5.ORDER_TYPE_BUY_LIMIT, "BUY_LIMIT"),
|
|
603
639
|
"SLMT": (Mt5.ORDER_TYPE_SELL_LIMIT, "SELL_LIMIT"),
|
|
604
640
|
"BSTP": (Mt5.ORDER_TYPE_BUY_STOP, "BUY_STOP"),
|
|
@@ -647,7 +683,7 @@ class Trade(RiskManagement):
|
|
|
647
683
|
else:
|
|
648
684
|
raise ValueError("You need to set a price for pending orders")
|
|
649
685
|
else:
|
|
650
|
-
_price = self.get_tick_info(self.symbol).
|
|
686
|
+
_price = self.get_tick_info(self.symbol).ask
|
|
651
687
|
|
|
652
688
|
lot = volume or self.get_lot()
|
|
653
689
|
stop_loss = self.get_stop_loss()
|
|
@@ -683,6 +719,8 @@ class Trade(RiskManagement):
|
|
|
683
719
|
request["tp"] = tp or mm_price - take_profit * point
|
|
684
720
|
self.break_even(mm=mm, id=Id, trail=trail)
|
|
685
721
|
if self.check(comment):
|
|
722
|
+
if action == "SSTPLMT":
|
|
723
|
+
_price = stoplimit
|
|
686
724
|
return self.request_result(_price, request, action)
|
|
687
725
|
return False
|
|
688
726
|
|
|
@@ -1132,9 +1170,9 @@ class Trade(RiskManagement):
|
|
|
1132
1170
|
be = self.get_break_even()
|
|
1133
1171
|
if trail_after_points is not None:
|
|
1134
1172
|
if isinstance(trail_after_points, int):
|
|
1135
|
-
assert (
|
|
1136
|
-
trail_after_points
|
|
1137
|
-
)
|
|
1173
|
+
assert trail_after_points > be, (
|
|
1174
|
+
"trail_after_points must be greater than break even or set to None"
|
|
1175
|
+
)
|
|
1138
1176
|
trail_after_points = self._get_trail_after_points(trail_after_points)
|
|
1139
1177
|
if positions is not None:
|
|
1140
1178
|
for position in positions:
|
|
@@ -1311,7 +1349,9 @@ class Trade(RiskManagement):
|
|
|
1311
1349
|
result = self.send_order(request)
|
|
1312
1350
|
except Exception as e:
|
|
1313
1351
|
msg = trade_retcode_message(result.retcode)
|
|
1314
|
-
LOGGER.error(
|
|
1352
|
+
LOGGER.error(
|
|
1353
|
+
f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}"
|
|
1354
|
+
)
|
|
1315
1355
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1316
1356
|
break
|
|
1317
1357
|
tries += 1
|
|
@@ -1397,7 +1437,9 @@ class Trade(RiskManagement):
|
|
|
1397
1437
|
result = self.send_order(request)
|
|
1398
1438
|
except Exception as e:
|
|
1399
1439
|
msg = trade_retcode_message(result.retcode)
|
|
1400
|
-
LOGGER.error(
|
|
1440
|
+
LOGGER.error(
|
|
1441
|
+
f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
|
|
1442
|
+
)
|
|
1401
1443
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1402
1444
|
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
1403
1445
|
for fill in FILLING_TYPE:
|
|
@@ -1421,7 +1463,9 @@ class Trade(RiskManagement):
|
|
|
1421
1463
|
result = self.send_order(request)
|
|
1422
1464
|
except Exception as e:
|
|
1423
1465
|
msg = trade_retcode_message(result.retcode)
|
|
1424
|
-
LOGGER.error(
|
|
1466
|
+
LOGGER.error(
|
|
1467
|
+
f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}"
|
|
1468
|
+
)
|
|
1425
1469
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1426
1470
|
break
|
|
1427
1471
|
tries += 1
|
|
@@ -1533,8 +1577,6 @@ class Trade(RiskManagement):
|
|
|
1533
1577
|
symbol = symbol or self.symbol
|
|
1534
1578
|
Id = id if id is not None else self.expert_id
|
|
1535
1579
|
positions = self.get_positions(ticket=ticket)
|
|
1536
|
-
buy_price = self.get_tick_info(symbol).ask
|
|
1537
|
-
sell_price = self.get_tick_info(symbol).bid
|
|
1538
1580
|
deviation = self.get_deviation()
|
|
1539
1581
|
if positions is not None and len(positions) == 1:
|
|
1540
1582
|
position = positions[0]
|
|
@@ -1546,9 +1588,8 @@ class Trade(RiskManagement):
|
|
|
1546
1588
|
"volume": (position.volume * pct),
|
|
1547
1589
|
"type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
|
|
1548
1590
|
"position": ticket,
|
|
1549
|
-
"price":
|
|
1591
|
+
"price": position.price_current,
|
|
1550
1592
|
"deviation": deviation,
|
|
1551
|
-
"magic": Id,
|
|
1552
1593
|
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
1553
1594
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
1554
1595
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
@@ -1665,32 +1706,8 @@ class Trade(RiskManagement):
|
|
|
1665
1706
|
comment=comment,
|
|
1666
1707
|
)
|
|
1667
1708
|
|
|
1668
|
-
def get_today_deals(self, group=None)
|
|
1669
|
-
|
|
1670
|
-
Get all today deals for a specific symbol or group of symbols
|
|
1671
|
-
|
|
1672
|
-
Args:
|
|
1673
|
-
group (str): Symbol or group or symbol
|
|
1674
|
-
Returns:
|
|
1675
|
-
List[TradeDeal]: List of today deals
|
|
1676
|
-
"""
|
|
1677
|
-
date_from = datetime.now() - timedelta(days=2)
|
|
1678
|
-
history = (
|
|
1679
|
-
self.get_trades_history(date_from=date_from, group=group, to_df=False) or []
|
|
1680
|
-
)
|
|
1681
|
-
positions_ids = set(
|
|
1682
|
-
[deal.position_id for deal in history if deal.magic == self.expert_id]
|
|
1683
|
-
)
|
|
1684
|
-
today_deals = []
|
|
1685
|
-
for position in positions_ids:
|
|
1686
|
-
deal = self.get_trades_history(
|
|
1687
|
-
date_from=date_from, position=position, to_df=False
|
|
1688
|
-
)
|
|
1689
|
-
if deal is not None and len(deal) == 2:
|
|
1690
|
-
deal_time = datetime.fromtimestamp(deal[1].time)
|
|
1691
|
-
if deal_time.date() == datetime.now().date():
|
|
1692
|
-
today_deals.append(deal[1])
|
|
1693
|
-
return today_deals
|
|
1709
|
+
def get_today_deals(self, group=None):
|
|
1710
|
+
return super().get_today_deals(self.expert_id, group=group)
|
|
1694
1711
|
|
|
1695
1712
|
def is_max_trades_reached(self) -> bool:
|
|
1696
1713
|
"""
|
bbstrader/metatrader/utils.py
CHANGED
|
@@ -486,11 +486,7 @@ class MT5TerminalError(Exception):
|
|
|
486
486
|
self.code = code
|
|
487
487
|
self.message = message
|
|
488
488
|
|
|
489
|
-
def
|
|
490
|
-
# if self.message is None:
|
|
491
|
-
# return f"{self.__class__.__name__}"
|
|
492
|
-
# else:
|
|
493
|
-
# return f"{self.__class__.__name__}, {self.message}"
|
|
489
|
+
def __repr__(self) -> str:
|
|
494
490
|
msg_str = str(self.message) if self.message is not None else ""
|
|
495
491
|
return f"{self.code} - {self.__class__.__name__}: {msg_str}"
|
|
496
492
|
|
|
@@ -638,6 +634,8 @@ def raise_mt5_error(message: Optional[str] = None):
|
|
|
638
634
|
Raises:
|
|
639
635
|
MT5TerminalError: A specific exception based on the error code.
|
|
640
636
|
"""
|
|
637
|
+
if message and isinstance(message, Exception):
|
|
638
|
+
message = str(message)
|
|
641
639
|
error = _ERROR_CODE_TO_EXCEPTION_.get(MT5.last_error()[0])
|
|
642
640
|
if error is not None:
|
|
643
641
|
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):
|