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.

@@ -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._mt5_initialized(**kwargs)
127
+ self.__initializ_mt5(**kwargs)
128
128
  self.__account = Account(**kwargs)
129
129
  self.__data = self.get_rates_from_pos()
130
130
 
131
- def _mt5_initialized(self, **kwargs):
131
+ def __initializ_mt5(self, **kwargs):
132
132
  check_mt5_connection(**kwargs)
133
133
 
134
134
  def _get_start_pos(self, index, time_frame):
@@ -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,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
- RunCopier(
90
- source,
91
- destinations,
92
- copy_args.interval,
93
- copy_args.start,
94
- copy_args.end,
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
+ )
@@ -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(
@@ -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 = "2.0",
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).ask
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.ORDER_TYPE_BUY, "SELL"),
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).bid
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 > be
1137
- ), "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
+ )
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(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
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(f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}")
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(f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}")
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": sell_price if buy else buy_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) -> List[TradeDeal]:
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
  """
@@ -486,11 +486,7 @@ class MT5TerminalError(Exception):
486
486
  self.code = code
487
487
  self.message = message
488
488
 
489
- def __str__(self) -> str:
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]}")
@@ -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):