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.

@@ -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
- if result.retcode != Mt5.TRADE_RETCODE_DONE:
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)
@@ -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.
@@ -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
- securities, n=universe, rolling_window=rolling_window
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: str = None,
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(start, end, securities, candidates, window, npairs)
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
- __author__ = "Stefan Jansen"
32
- """Generates tuples of train_idx, test_idx pairs
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
- data = []
239
- for ticker in tickers:
240
- try:
241
- prices = yf.download(
242
- ticker,
243
- start=start,
244
- end=end,
245
- progress=False,
246
- multi_level_index=False,
247
- auto_adjust=True,
248
- )
249
- if prices.empty:
250
- continue
251
- prices["symbol"] = ticker
252
- data.append(prices)
253
- except: # noqa: E722
254
- continue
255
- data = pd.concat(data)
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
- def clean_text_column(series: pd.Series) -> pd.Series:
269
- return (
270
- series.str.lower()
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
- metadata = [
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
- columns = {
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
- data = []
320
- for symbol in tickers:
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
- symbol_info = yf.Ticker(symbol).info
323
- except: # noqa: E722
324
- continue
325
- infos = {}
326
- for info in metadata:
327
- infos[info] = symbol_info.get(info)
328
- data.append(infos)
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=columns)
331
- metadata.dyield = metadata.dyield.fillna(0)
332
- metadata.sector = clean_text_column(metadata.sector)
333
- metadata.industry = clean_text_column(metadata.industry)
334
- metadata = metadata.set_index("symbol")
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
- 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()
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
 
@@ -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"] = "week",
196
- period_end_action: Literal["break", "sleep"] = "break",
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: