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.

@@ -44,6 +44,7 @@ _COMMD_SUPPORTED_ = [
44
44
  "Copper",
45
45
  "XCUUSD",
46
46
  "NatGas",
47
+ "NATGAS",
47
48
  "Gasoline",
48
49
  ]
49
50
 
@@ -1,10 +1,20 @@
1
1
  import argparse
2
+ import multiprocessing
2
3
  import sys
3
4
 
4
- from bbstrader.metatrader.copier import RunCopier, config_copier
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
- source, destinations = config_copier(
71
- source_section=copy_args.source,
72
- dest_sections=copy_args.destinations,
73
- inifile=copy_args.config,
74
- )
75
- RunCopier(
76
- source,
77
- destinations,
78
- copy_args.interval,
79
- copy_args.start,
80
- copy_args.end,
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
+ )
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import time
3
3
  from dataclasses import dataclass
4
- from datetime import datetime, timedelta
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}), comment='{self.comment}'"
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 = "2.0",
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.ORDER_TYPE_BUY, "SELL"),
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
- print(f"{self.current_datetime()} -", end=" ")
744
- trade_retcode_message(
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
- print(f"{self.current_datetime()} -", end=" ")
777
- trade_retcode_message(
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 > be
1139
- ), "trail_after_points must be greater than break even or set to None"
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
- print(f"{self.current_datetime()} -", end=" ")
1298
- trade_retcode_message(
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
- print(f"{self.current_datetime()} -", end=" ")
1318
- trade_retcode_message(
1319
- result.retcode, display=True, add_msg=f"{e}{addtionnal}"
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
- print(f"{self.current_datetime()} -", end=" ")
1406
- trade_retcode_message(
1407
- result.retcode, display=True, add_msg=f"{e}{addtionnal}"
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}, RETCODE={result.retcode}: {msg}{addtionnal}"
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
- print(f"{self.current_datetime()} -", end=" ")
1431
- trade_retcode_message(
1432
- result.retcode, display=True, add_msg=f"{e}{addtionnal}"
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 = f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol}, Price: @{request.get('price', 0.0)}"
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}, SL={sl}, TP={tp}, STOP_LIMIT={stoplimit}"
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": sell_price if buy else buy_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) -> List[TradeDeal]:
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
  """
@@ -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]}")
@@ -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
- return (
853
- data.loc[idx[tickers, start:end], "open"]
854
- .unstack("symbol")
855
- .sort_index()
856
- .shift(-1)
857
- .tz_convert("UTC")
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
- factor = (
1090
- best_predictions.iloc[:, :5]
1091
- .mean(1)
1092
- .dropna()
1093
- .tz_convert("UTC", level="date")
1094
- .swaplevel()
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
- print(f"Error getting last date: {e}")
1284
+ self.logger.error(f"Error getting last date: {e}")
1257
1285
  try:
1258
- days = 3 if datetime.now().strftime("%A") == "Monday" else 1
1259
- td = (
1260
- last_date
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):