bbstrader 0.2.92__py3-none-any.whl → 0.2.93__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/__main__.py CHANGED
@@ -18,7 +18,7 @@ USAGE_TEXT = """
18
18
  backtest: Backtest a strategy, see bbstrader.btengine.backtest.run_backtest
19
19
  execution: Execute a strategy, see bbstrader.trading.execution.MT5ExecutionEngine
20
20
 
21
- run <module> --help for more information on the module
21
+ python -m bbstrader --run <module> --help for more information on the module
22
22
  """
23
23
 
24
24
  FONT = pyfiglet.figlet_format("BBSTRADER", font="big")
@@ -175,9 +175,9 @@ class BaseCSVDataHandler(DataHandler):
175
175
  new_names = self.columns or default_names
176
176
  new_names = [name.strip().lower().replace(" ", "_") for name in new_names]
177
177
  self.columns = new_names
178
- assert (
179
- "adj_close" in new_names or "close" in new_names
180
- ), "Column names must contain 'Adj Close' and 'Close' or adj_close and close"
178
+ assert "adj_close" in new_names or "close" in new_names, (
179
+ "Column names must contain 'Adj Close' and 'Close' or adj_close and close"
180
+ )
181
181
  comb_index = None
182
182
  for s in self.symbol_list:
183
183
  # Load the CSV file with no header information,
@@ -207,7 +207,9 @@ class BaseCSVDataHandler(DataHandler):
207
207
  self.columns.append("adj_close")
208
208
  self.symbol_data[s]["adj_close"] = self.symbol_data[s]["close"]
209
209
  self.symbol_data[s]["returns"] = (
210
- self.symbol_data[s]["adj_close" if "adj_close" in new_names else "close"]
210
+ self.symbol_data[s][
211
+ "adj_close" if "adj_close" in new_names else "close"
212
+ ]
211
213
  .pct_change()
212
214
  .dropna()
213
215
  )
@@ -362,7 +364,7 @@ class CSVDataHandler(BaseCSVDataHandler):
362
364
 
363
365
  """
364
366
  csv_dir = kwargs.get("csv_dir")
365
- csv_dir = csv_dir or BBSTRADER_DIR / "csv_data"
367
+ csv_dir = csv_dir or BBSTRADER_DIR / "data" / "csv_data"
366
368
  super().__init__(
367
369
  events,
368
370
  symbol_list,
@@ -422,7 +424,7 @@ class MT5DataHandler(BaseCSVDataHandler):
422
424
  )
423
425
 
424
426
  def _download_and_cache_data(self, cache_dir: str):
425
- data_dir = cache_dir or BBSTRADER_DIR / "mt5" / self.tf
427
+ data_dir = cache_dir or BBSTRADER_DIR / "data" / "mt5" / self.tf
426
428
  data_dir.mkdir(parents=True, exist_ok=True)
427
429
  for symbol in self.symbol_list:
428
430
  try:
@@ -486,7 +488,7 @@ class YFDataHandler(BaseCSVDataHandler):
486
488
 
487
489
  def _download_and_cache_data(self, cache_dir: str):
488
490
  """Downloads and caches historical data as CSV files."""
489
- cache_dir = cache_dir or BBSTRADER_DIR / "yfinance" / "daily"
491
+ cache_dir = cache_dir or BBSTRADER_DIR / "data" / "yfinance" / "daily"
490
492
  os.makedirs(cache_dir, exist_ok=True)
491
493
  for symbol in self.symbol_list:
492
494
  filepath = os.path.join(cache_dir, f"{symbol}.csv")
@@ -598,7 +600,7 @@ class EODHDataHandler(BaseCSVDataHandler):
598
600
 
599
601
  def _download_and_cache_data(self, cache_dir: str):
600
602
  """Downloads and caches historical data as CSV files."""
601
- cache_dir = cache_dir or BBSTRADER_DIR / "eodhd" / self.period
603
+ cache_dir = cache_dir or BBSTRADER_DIR / "data" / "eodhd" / self.period
602
604
  os.makedirs(cache_dir, exist_ok=True)
603
605
  for symbol in self.symbol_list:
604
606
  filepath = os.path.join(cache_dir, f"{symbol}.csv")
@@ -699,7 +701,7 @@ class FMPDataHandler(BaseCSVDataHandler):
699
701
 
700
702
  def _download_and_cache_data(self, cache_dir: str):
701
703
  """Downloads and caches historical data as CSV files."""
702
- cache_dir = cache_dir or BBSTRADER_DIR / "fmp" / self.period
704
+ cache_dir = cache_dir or BBSTRADER_DIR / "data" / "fmp" / self.period
703
705
  os.makedirs(cache_dir, exist_ok=True)
704
706
  for symbol in self.symbol_list:
705
707
  filepath = os.path.join(cache_dir, f"{symbol}.csv")
bbstrader/compat.py CHANGED
@@ -3,7 +3,7 @@ import sys
3
3
 
4
4
 
5
5
  def setup_mock_metatrader():
6
- """Mock MetaTrader5 on Linux to prevent import errors."""
6
+ """Mock MetaTrader5 on Linux and MacOS to prevent import errors."""
7
7
  if platform.system() != "Windows":
8
8
  from unittest.mock import MagicMock
9
9
 
bbstrader/core/utils.py CHANGED
@@ -20,7 +20,7 @@ def load_module(file_path):
20
20
  raise FileNotFoundError(
21
21
  f"Strategy file {file_path} not found. Please create it."
22
22
  )
23
- spec = importlib.util.spec_from_file_location("strategies", file_path)
23
+ spec = importlib.util.spec_from_file_location("bbstrader.cli", file_path)
24
24
  module = importlib.util.module_from_spec(spec)
25
25
  spec.loader.exec_module(module)
26
26
  return module
@@ -66,7 +66,7 @@ def dict_from_ini(file_path, sections: str | List[str] = None) -> Dict[str, Any]
66
66
  Returns:
67
67
  A dictionary containing the INI file contents with proper data types.
68
68
  """
69
- config = configparser.ConfigParser()
69
+ config = configparser.ConfigParser(interpolation=None)
70
70
  config.read(file_path)
71
71
 
72
72
  ini_dict = {}
@@ -326,7 +326,7 @@ class Account(object):
326
326
 
327
327
  def shutdown(self):
328
328
  """Close the connection to the MetaTrader 5 terminal."""
329
- mt5.shutdown()
329
+ shutdown_mt5()
330
330
 
331
331
  @property
332
332
  def broker(self) -> Broker:
@@ -960,7 +960,7 @@ class Account(object):
960
960
  symbol (str): Symbol name
961
961
 
962
962
  Returns:
963
- - AccountInfo in the form of a NamedTuple().
963
+ - SymbolInfo in the form of a NamedTuple().
964
964
  - None in case of an error.
965
965
 
966
966
  Raises:
@@ -1006,7 +1006,7 @@ class Account(object):
1006
1006
  symbol (str): Symbol name
1007
1007
 
1008
1008
  Returns:
1009
- - AccountInfo in the form of a NamedTuple().
1009
+ - TickInfo in the form of a NamedTuple().
1010
1010
  - None in case of an error.
1011
1011
 
1012
1012
  Raises:
@@ -294,11 +294,11 @@ class TradeCopier(object):
294
294
  return False
295
295
 
296
296
  def iscopy_time(self):
297
- start_hour, start_minutes = self.start_time.split(":")
298
- end_hour, end_minutes = self.end_time.split(":")
299
297
  if self.start_time is None or self.end_time is None:
300
298
  return True
301
299
  else:
300
+ start_hour, start_minutes = self.start_time.split(":")
301
+ end_hour, end_minutes = self.end_time.split(":")
302
302
  if int(start_hour) < datetime.now().hour < int(end_hour):
303
303
  return True
304
304
  elif datetime.now().hour == int(start_hour):
@@ -346,13 +346,13 @@ class TradeCopier(object):
346
346
  comment=destination.get("comment", trade.comment + "#Copied"),
347
347
  ):
348
348
  logger.info(
349
- f"Copy {action} Order #{trade.ticket} from @{self.source.get('login')} "
350
- f"to @{destination.get('login')} on @{symbol}"
349
+ f"Copy {action} Order #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
350
+ f"to @{destination.get('login')}::{symbol}"
351
351
  )
352
352
  else:
353
353
  logger.error(
354
- f"Error copying {action} Order #{trade.ticket} from @{self.source.get('login')} "
355
- f"to @{destination.get('login')} on @{symbol}"
354
+ f"Error copying {action} Order #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
355
+ f"to @{destination.get('login')}::{symbol}"
356
356
  )
357
357
  except Exception as e:
358
358
  self.log_error(e, symbol=symbol)
@@ -384,27 +384,27 @@ class TradeCopier(object):
384
384
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
385
385
  msg = trade_retcode_message(result.retcode)
386
386
  logger.error(
387
- f"Error modifying Order #{ticket} on @{destination.get('login')}: {msg}, "
388
- f"SOURCE=@{self.source.get('login')} SYMBOL=@{symbol}"
387
+ f"Error modifying Order #{ticket} on @{destination.get('login')}::{symbol}, {msg}, "
388
+ f"SOURCE=@{self.source.get('login')}::{source_order.symbol}"
389
389
  )
390
390
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
391
391
  logger.info(
392
- f"Modify Order #{ticket} on @{destination.get('login')}, "
393
- f"SOURCE=@{self.source.get('login')} SYMBOL=@{symbol}"
392
+ f"Modify Order #{ticket} on @{destination.get('login')}::{symbol}, "
393
+ f"SOURCE=@{self.source.get('login')}::{source_order.symbol}"
394
394
  )
395
395
 
396
- def remove_order(self, order: TradeOrder, destination: dict):
396
+ def remove_order(self, src_symbol, order: TradeOrder, destination: dict):
397
397
  check_mt5_connection(**destination)
398
398
  trade = Trade(symbol=order.symbol, **destination)
399
399
  if trade.close_order(order.ticket, id=order.magic):
400
400
  logger.info(
401
- f"Close Order #{order.ticket} on @{destination.get('login')}, "
402
- f"SOURCE=@{self.source.get('login')} SYMBOL=@{order.symbol}"
401
+ f"Close Order #{order.ticket} on @{destination.get('login')}::{order.symbol}, "
402
+ f"SOURCE=@{self.source.get('login')}::{src_symbol}"
403
403
  )
404
404
  else:
405
405
  logger.error(
406
- f"Error closing Order #{order.ticket} on @{destination.get('login')}, "
407
- f"SOURCE=@{self.source.get('login')} SYMBOL=@{order.symbol}"
406
+ f"Error closing Order #{order.ticket} on @{destination.get('login')}::{order.symbol}, "
407
+ f"SOURCE=@{self.source.get('login')}::{src_symbol}"
408
408
  )
409
409
 
410
410
  def copy_new_position(self, position: TradePosition, destination: dict):
@@ -427,27 +427,27 @@ class TradeCopier(object):
427
427
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
428
428
  msg = trade_retcode_message(result.retcode)
429
429
  logger.error(
430
- f"Error modifying Position #{ticket} on @{destination.get('login')}: {msg}, "
431
- f"SOURCE=@{self.source.get('login')} SYMBOL=@{symbol}"
430
+ f"Error modifying Position #{ticket} on @{destination.get('login')}::{symbol}, {msg}, "
431
+ f"SOURCE=@{self.source.get('login')}::{source_pos.symbol}"
432
432
  )
433
433
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
434
434
  logger.info(
435
- f"Modify Position #{ticket} on @{destination.get('login')}, "
436
- f"SOURCE=@{self.source.get('login')} SYMBOL=@{symbol}"
435
+ f"Modify Position #{ticket} on @{destination.get('login')}::{symbol}, "
436
+ f"SOURCE=@{self.source.get('login')}::{source_pos.symbol}"
437
437
  )
438
438
 
439
- def remove_position(self, position: TradePosition, destination: dict):
439
+ def remove_position(self, src_symbol, position: TradePosition, destination: dict):
440
440
  check_mt5_connection(**destination)
441
441
  trade = Trade(symbol=position.symbol, **destination)
442
442
  if trade.close_position(position.ticket, id=position.magic):
443
443
  logger.info(
444
- f"Close Position #{position.ticket} on @{destination.get('login')}, "
445
- f"SOURCE=@{self.source.get('login')} SYMBOL=@{position.symbol}"
444
+ f"Close Position #{position.ticket} on @{destination.get('login')}::{position.symbol}, "
445
+ f"SOURCE=@{self.source.get('login')}::{src_symbol}"
446
446
  )
447
447
  else:
448
448
  logger.error(
449
- f"Error closing Position #{position.ticket} on @{destination.get('login')}, "
450
- f"SOURCE=@{self.source.get('login')} SYMBOL=@{position.symbol}"
449
+ f"Error closing Position #{position.ticket} on @{destination.get('login')}::{position.symbol}, "
450
+ f"SOURCE=@{self.source.get('login')}::{src_symbol}"
451
451
  )
452
452
 
453
453
  def filter_positions_and_orders(self, pos_or_orders, symbols=None):
@@ -514,7 +514,10 @@ class TradeCopier(object):
514
514
  source_ids = [order.ticket for order in source_orders]
515
515
  for destination_order in destination_orders:
516
516
  if destination_order.magic not in source_ids:
517
- self.remove_order(destination_order, destination)
517
+ for order in source_orders:
518
+ if order.ticket == destination_order.magic:
519
+ src_symbol = order.ticket
520
+ self.remove_order(src_symbol, destination_order, destination)
518
521
 
519
522
  # Check if order are triggered on source account
520
523
  # and not on destination account or vice versa
@@ -523,7 +526,9 @@ class TradeCopier(object):
523
526
  for source_position in source_positions:
524
527
  for destination_order in destination_orders:
525
528
  if source_position.ticket == destination_order.magic:
526
- self.remove_order(destination_order, destination)
529
+ self.remove_order(
530
+ source_position.symbol, destination_order, destination
531
+ )
527
532
  if what in ["all", "positions"]:
528
533
  if not self.slippage(source_position, destination):
529
534
  self.copy_new_position(source_position, destination)
@@ -533,7 +538,9 @@ class TradeCopier(object):
533
538
  for destination_position in destination_positions:
534
539
  for source_order in source_orders:
535
540
  if destination_position.magic == source_order.ticket:
536
- self.remove_position(destination_position, destination)
541
+ self.remove_position(
542
+ source_order.symbol, destination_position, destination
543
+ )
537
544
  if not self.slippage(source_order, destination):
538
545
  self.copy_new_order(source_order, destination)
539
546
  Mt5.shutdown()
@@ -569,7 +576,10 @@ class TradeCopier(object):
569
576
  source_ids = [pos.ticket for pos in source_positions]
570
577
  for destination_position in destination_positions:
571
578
  if destination_position.magic not in source_ids:
572
- self.remove_position(destination_position, destination)
579
+ for position in source_positions:
580
+ if position.ticket == destination_position.magic:
581
+ src_symbol = position.ticket
582
+ self.remove_position(src_symbol, destination_position, destination)
573
583
  Mt5.shutdown()
574
584
 
575
585
  def log_error(self, e, symbol=None):
@@ -607,7 +617,7 @@ def RunCopier(
607
617
 
608
618
 
609
619
  def RunMultipleCopier(
610
- accounts: List[Dict],
620
+ accounts: List[dict],
611
621
  sleeptime: float = 0.1,
612
622
  start_delay: float = 1.0,
613
623
  start_time: str = None,
@@ -664,7 +674,7 @@ def config_copier(
664
674
  Example:
665
675
  ```python
666
676
  from pathlib import Path
667
- config_file = Path().home() / ".bbstrader" / "copier.ini"
677
+ config_file = ~/.bbstrader/copier/copier.ini
668
678
  source, destinations = config_copier(config_file, "SOURCE", ["DEST1", "DEST2"])
669
679
  ```
670
680
  """
@@ -675,7 +685,7 @@ def config_copier(
675
685
  return dict(item.split(":") for item in string.split(","))
676
686
 
677
687
  if not inifile:
678
- inifile = Path().home() / ".bbstrader" / "copier.ini"
688
+ inifile = Path().home() / ".bbstrader" / "copier" / "copier.ini"
679
689
  if not inifile.exists() or not inifile.is_file():
680
690
  raise FileNotFoundError(f"{inifile} not found")
681
691
 
@@ -6,10 +6,10 @@ from bbstrader.metatrader.copier import RunCopier, config_copier
6
6
 
7
7
  def copier_args(parser: argparse.ArgumentParser):
8
8
  parser.add_argument(
9
- "-S", "--source", type=str, nargs="?", default=None, help="Source section name"
9
+ "-s", "--source", type=str, nargs="?", default=None, help="Source section name"
10
10
  )
11
11
  parser.add_argument(
12
- "-D",
12
+ "-d",
13
13
  "--destinations",
14
14
  type=str,
15
15
  nargs="*",
@@ -17,10 +17,10 @@ def copier_args(parser: argparse.ArgumentParser):
17
17
  help="Destination section names",
18
18
  )
19
19
  parser.add_argument(
20
- "-I", "--interval", type=float, default=0.1, help="Update interval in seconds"
20
+ "-i", "--interval", type=float, default=0.1, help="Update interval in seconds"
21
21
  )
22
22
  parser.add_argument(
23
- "-C",
23
+ "-c",
24
24
  "--config",
25
25
  nargs="?",
26
26
  default=None,
@@ -28,7 +28,7 @@ def copier_args(parser: argparse.ArgumentParser):
28
28
  help="Config file name or path",
29
29
  )
30
30
  parser.add_argument(
31
- "-T",
31
+ "-t",
32
32
  "--start",
33
33
  type=str,
34
34
  nargs="?",
@@ -36,7 +36,7 @@ def copier_args(parser: argparse.ArgumentParser):
36
36
  help="Start time in HH:MM format",
37
37
  )
38
38
  parser.add_argument(
39
- "-E",
39
+ "-e",
40
40
  "--end",
41
41
  type=str,
42
42
  nargs="?",
@@ -422,6 +422,7 @@ class Trade(RiskManagement):
422
422
  price: Optional[float] = None,
423
423
  stoplimit: Optional[float] = None,
424
424
  mm: bool = True,
425
+ trail: bool = True,
425
426
  id: Optional[int] = None,
426
427
  comment: Optional[str] = None,
427
428
  symbol: Optional[str] = None,
@@ -440,6 +441,7 @@ class Trade(RiskManagement):
440
441
  The pending order is not passed to the trading system until that moment
441
442
  id (int): The strategy id or expert Id
442
443
  mm (bool): Weither to put stop loss and tp or not
444
+ trail (bool): Weither to trail the stop loss or not
443
445
  comment (str): The comment for the opening position
444
446
  """
445
447
  Id = id if id is not None else self.expert_id
@@ -484,7 +486,7 @@ class Trade(RiskManagement):
484
486
  if mm:
485
487
  request["sl"] = sl or mm_price - stop_loss * point
486
488
  request["tp"] = tp or mm_price + take_profit * point
487
- self.break_even(mm=mm, id=Id)
489
+ self.break_even(mm=mm, id=Id, trail=trail)
488
490
  if self.check(comment):
489
491
  return self.request_result(_price, request, action)
490
492
  return False
@@ -510,6 +512,7 @@ class Trade(RiskManagement):
510
512
  price: Optional[float] = None,
511
513
  stoplimit: Optional[float] = None,
512
514
  mm: bool = True,
515
+ trail: bool = True,
513
516
  id: Optional[int] = None,
514
517
  comment: Optional[str] = None,
515
518
  symbol: Optional[str] = None,
@@ -528,6 +531,7 @@ class Trade(RiskManagement):
528
531
  The pending order is not passed to the trading system until that moment
529
532
  id (int): The strategy id or expert Id
530
533
  mm (bool): Weither to put stop loss and tp or not
534
+ trail (bool): Weither to trail the stop loss or not
531
535
  comment (str): The comment for the closing position
532
536
  symbol (str): The symbol to trade
533
537
  volume (float): The volume (lot) to trade
@@ -576,7 +580,7 @@ class Trade(RiskManagement):
576
580
  if mm:
577
581
  request["sl"] = sl or mm_price + stop_loss * point
578
582
  request["tp"] = tp or mm_price - take_profit * point
579
- self.break_even(mm=mm, id=Id)
583
+ self.break_even(mm=mm, id=Id, trail=trail)
580
584
  if self.check(comment):
581
585
  return self.request_result(_price, request, action)
582
586
  return False
@@ -723,6 +727,7 @@ class Trade(RiskManagement):
723
727
  stoplimit: Optional[float] = None,
724
728
  id: Optional[int] = None,
725
729
  mm: bool = True,
730
+ trail: bool = True,
726
731
  comment: Optional[str] = None,
727
732
  symbol: Optional[str] = None,
728
733
  volume: Optional[float] = None,
@@ -740,6 +745,7 @@ class Trade(RiskManagement):
740
745
  The pending order is not passed to the trading system until that moment
741
746
  id (int): The strategy id or expert Id
742
747
  mm (bool): Weither to put stop loss and tp or not
748
+ trail (bool): Weither to trail the stop loss or not
743
749
  comment (str): The comment for the closing position
744
750
  symbol (str): The symbol to trade
745
751
  volume (float): The volume (lot) to trade
@@ -755,6 +761,7 @@ class Trade(RiskManagement):
755
761
  stoplimit=stoplimit,
756
762
  id=id,
757
763
  mm=mm,
764
+ trail=trail,
758
765
  comment=comment,
759
766
  symbol=symbol,
760
767
  volume=volume,
@@ -768,9 +775,12 @@ class Trade(RiskManagement):
768
775
  stoplimit=stoplimit,
769
776
  id=id,
770
777
  mm=mm,
778
+ trail=trail,
771
779
  comment=comment,
772
780
  symbol=symbol,
773
781
  volume=volume,
782
+ sl=sl,
783
+ tp=tp,
774
784
  )
775
785
  else:
776
786
  raise ValueError(
@@ -1242,7 +1252,7 @@ class Trade(RiskManagement):
1242
1252
  try:
1243
1253
  min_be = round((fees / risk)) + 2
1244
1254
  except ZeroDivisionError:
1245
- min_be = self.symbol_info(self.symbol).spread
1255
+ min_be = self.symbol_info.spread
1246
1256
  be = self.get_break_even()
1247
1257
  if th is not None:
1248
1258
  win_be = th
bbstrader/models/ml.py CHANGED
@@ -634,14 +634,13 @@ class LightGBModel(object):
634
634
 
635
635
  # set up cross-validation
636
636
  n_splits = int(2 * YEAR / test_length)
637
- if verbose:
638
- print(
639
- f"Lookahead: {lookahead:2.0f} | "
640
- f"Train: {train_length:3.0f} | "
641
- f"Test: {test_length:2.0f} | "
642
- f"Params: {len(cv_params_):3.0f} | "
643
- f"Train configs: {len(test_params)}"
644
- )
637
+ print(
638
+ f"Lookahead: {lookahead:2.0f} | "
639
+ f"Train: {train_length:3.0f} | "
640
+ f"Test: {test_length:2.0f} | "
641
+ f"Params: {len(cv_params_):3.0f} | "
642
+ f"Train configs: {len(test_params)}"
643
+ )
645
644
 
646
645
  # time-series cross-validation
647
646
  cv = MultipleTimeSeriesCV(
@@ -1235,7 +1234,10 @@ class LightGBModel(object):
1235
1234
  .to_frame("prediction")
1236
1235
  )
1237
1236
  tickers = predictions.index.get_level_values("symbol").unique().tolist()
1238
- return (predictions.unstack("symbol").prediction.tz_convert("UTC")), tickers
1237
+ try:
1238
+ return (predictions.unstack("symbol").prediction.tz_convert("UTC")), tickers
1239
+ except TypeError:
1240
+ return (predictions.unstack("symbol").prediction.tz_localize("UTC")), tickers
1239
1241
 
1240
1242
  def assert_last_date(self, predictions: pd.DataFrame):
1241
1243
  """
@@ -1243,9 +1245,14 @@ class LightGBModel(object):
1243
1245
  is the previous day, so it predicts today's returns.
1244
1246
  """
1245
1247
  last_date = predictions.index.get_level_values("date").max()
1246
- if last_date.tzinfo is None:
1247
- last_date = last_date.tz_localize("UTC")
1248
- last_date = last_date.normalize()
1248
+ try:
1249
+ if last_date.tzinfo is None:
1250
+ last_date = last_date.tz_localize("UTC")
1251
+ else:
1252
+ last_date = last_date.tz_convert("UTC")
1253
+ last_date = last_date.normalize()
1254
+ except Exception as e:
1255
+ print(f"Error getting last date: {e}")
1249
1256
  try:
1250
1257
  days = 3 if datetime.now().strftime("%A") == "Monday" else 1
1251
1258
  td = (
@@ -221,7 +221,7 @@ def _mt5_execution(
221
221
  desc = account.get_symbol_info(symbol).description
222
222
  sigmsg = (
223
223
  f"SIGNAL = {signal}, \nSYMBOL={symbol}, \nTYPE={symbol_type}, \nDESCRIPTION={desc}, "
224
- f"\nPRICE={price}, \nSTOPLIMIT={stoplimit}, \nSTRATEGY={STRATEGY}, \nTIMEFRAME={time_frame}"
224
+ f"\nPRICE={price}, \nSTOPLIMIT={stoplimit}, \nSTRATEGY={STRATEGY}, \nTIMEFRAME={time_frame},"
225
225
  f"\nBROKER={account.broker.name}, \nTIMESTAMP={datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
226
226
  )
227
227
  msg = f"Sending {signal} Order ... SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
@@ -258,6 +258,7 @@ def _mt5_execution(
258
258
  stoplimit=stoplimit,
259
259
  id=id,
260
260
  mm=mm,
261
+ trail=trail,
261
262
  comment=comment,
262
263
  )
263
264
  else:
@@ -274,6 +275,7 @@ def _mt5_execution(
274
275
  stoplimit=stoplimit,
275
276
  id=id,
276
277
  mm=mm,
278
+ trail=trail,
277
279
  comment=comment,
278
280
  )
279
281
 
@@ -293,6 +295,7 @@ def _mt5_execution(
293
295
  stoplimit=stoplimit,
294
296
  id=id,
295
297
  mm=mm,
298
+ trail=trail,
296
299
  comment=comment,
297
300
  )
298
301
  else:
@@ -309,6 +312,7 @@ def _mt5_execution(
309
312
  stoplimit=stoplimit,
310
313
  id=id,
311
314
  mm=mm,
315
+ trail=trail,
312
316
  comment=comment,
313
317
  )
314
318
 
@@ -143,7 +143,7 @@ def execute_strategy(unknown):
143
143
  parser.add_argument("-a", "--account", type=str, nargs="*", default=[])
144
144
  parser.add_argument("-p", "--path", type=str, default=EXECUTION_PATH)
145
145
  parser.add_argument("-c", "--config", type=str, default=CONFIG_PATH)
146
- parser.add_argument("-l", "--parallel", type=bool, default=False)
146
+ parser.add_argument("-l", "--parallel", action="store_true")
147
147
  parser.add_argument(
148
148
  "-t", "--terminal", type=str, default="MT5", choices=["MT5", "TWS"]
149
149
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: bbstrader
3
- Version: 0.2.92
3
+ Version: 0.2.93
4
4
  Summary: Simplified Investment & Trading Toolkit
5
5
  Home-page: https://github.com/bbalouki/bbstrader
6
6
  Download-URL: https://pypi.org/project/bbstrader/
@@ -1,11 +1,11 @@
1
1
  bbstrader/__ini__.py,sha256=ahTl0_LzPsSMLr0Ae_hV78IuMFvMFDwPOBQh4kQltes,641
2
- bbstrader/__main__.py,sha256=gAULPhYSKMy5IP-KC1V-gJC__AsbUZKWJDuOIvZb6Vk,1512
3
- bbstrader/compat.py,sha256=dqw4j3JqEq2aiYAxgiV4NqaJNeRX-8hTQM5gZ8zhLYM,496
2
+ bbstrader/__main__.py,sha256=JvWLoIN03J1VKwmrJN0Z8m7uFfAL3F-6c_Yij_CUAgM,1534
3
+ bbstrader/compat.py,sha256=YYubimfqHj8yPFhfqCDvz9eddMK9JJUYvxQDle6-tNU,506
4
4
  bbstrader/config.py,sha256=YMH9O_b0eJdfTwlw6GNbjRo0ieQMTH893crgwbbW5fQ,4110
5
5
  bbstrader/tseries.py,sha256=lbsbEl7jNyKHqkHdzX_DqAlZ7z46dGOJWB6kG2Y60qg,70181
6
6
  bbstrader/btengine/__init__.py,sha256=FL0kC0NcsnlTH-yuTv4lu6AexY1wZKN1AQ9rv9MZagQ,3009
7
7
  bbstrader/btengine/backtest.py,sha256=ZzGhoN-_g0cF-OCyk173imze2OXEhykHTUiJ9MowDO8,14582
8
- bbstrader/btengine/data.py,sha256=fquEuMIUokWjykPFptpGNGxR6CTtFF3--AR_Kb7NAfM,27211
8
+ bbstrader/btengine/data.py,sha256=UpHW27o7cb9ibQJ6JYovfG_zyxA9SCwR0-Zv7zUQxSM,27296
9
9
  bbstrader/btengine/event.py,sha256=38mhZH9d53C4x7bZER2B0O6M18txzS4u7zveKyxeP5Y,8603
10
10
  bbstrader/btengine/execution.py,sha256=Ij5dLc9mGgtWp2dKAH5oURplA3RS_ZtwTwSrp9IYfpk,10644
11
11
  bbstrader/btengine/performance.py,sha256=1ecWrTzHBQbk4ORvbTEKxwCzlL1brcXOEUwgbnjAwx4,12470
@@ -14,31 +14,31 @@ bbstrader/btengine/scripts.py,sha256=Dff0cMvaZBynczVagY91XvHxQR7v53lMXyHMF69n3DY
14
14
  bbstrader/btengine/strategy.py,sha256=c-wvotJdhHu5FWAlPkv33LfjoW-7ID2G0Di_hc7CYMM,33217
15
15
  bbstrader/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  bbstrader/core/data.py,sha256=3_odj9jxokyKU15j1aHTlgLQDjW75tKqGpCUfkpYq2Q,452
17
- bbstrader/core/utils.py,sha256=LRHpDT7j5-l56NKiwvMuhLf3adL6kUxJPkjYn__FdKc,4204
17
+ bbstrader/core/utils.py,sha256=yC4_BqymZ4_t5J6y3bRY5J8hCT_0ooSPD8rqwj_aYdM,4225
18
18
  bbstrader/ibkr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  bbstrader/ibkr/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  bbstrader/metatrader/__init__.py,sha256=_vDoSBDTQ9JVhN6HsSYmTBx8womTvbJ222aqTRG5rmE,339
21
- bbstrader/metatrader/account.py,sha256=U620mTbG30NbWCb1Abr2qONHnclJHUzgGkUOF5wMW0c,58013
22
- bbstrader/metatrader/copier.py,sha256=ZCeLPJjvznxntfUrAkHJnOlH2YqNCz9AAe6Yd8fa8qE,31749
21
+ bbstrader/metatrader/account.py,sha256=As2LtWUbVnobM5I1WPNAYv5_w_7mMm5xF31jaOtI8IU,58009
22
+ bbstrader/metatrader/copier.py,sha256=eoztbIlzBAJ-zjNpcq4NN7QnFkGJ3VSRqVmr6oGhUtA,32412
23
23
  bbstrader/metatrader/rates.py,sha256=DzjObLlas38TnprPsw4xPxTupCRwy_jZzLezCU_4p0I,21169
24
24
  bbstrader/metatrader/risk.py,sha256=jLrLiQWb_v8Dl480jXaUbA-yV4ck8ES4PArTRAWILE0,27680
25
- bbstrader/metatrader/scripts.py,sha256=3EgiBrP9qU7-JBDWr2w0i4XB98LZjmGFZMdwJ0yOrnk,2196
26
- bbstrader/metatrader/trade.py,sha256=6DIXAd0ZxhjTqocLZxsJV5GxboxUHh8vEzmcl9ZiVpg,78213
25
+ bbstrader/metatrader/scripts.py,sha256=zgY94YGL7l_tzVnbkZWlj5YRRogM1jHY9i3Vxxmh6qQ,2196
26
+ bbstrader/metatrader/trade.py,sha256=f1fETc83bmLru0hXtu5VpV73lB3dGMVJrHf8cdsirdI,78616
27
27
  bbstrader/metatrader/utils.py,sha256=1d7A8XhUmiLGpG4ImcBhCgs7xHrmYe_YJhynWIdLr30,17748
28
28
  bbstrader/models/__init__.py,sha256=SnGBMQ-zcUIpms3oNeqg7EVDFpg-7OPjNAD8kvi_Q84,508
29
29
  bbstrader/models/factors.py,sha256=dWuXh83hLkwxUp3XwjgUl-r3_cjVcV_s0aFRlSLIfo8,13332
30
- bbstrader/models/ml.py,sha256=pwa8Ss-JvWMoHkCQGFv3HTnZnvpEQDbWBG-oMBPFTFc,48522
30
+ bbstrader/models/ml.py,sha256=dSoKL409RPEKUc0lQkzMh66--x7JsiB69BZKF1RzGoU,48791
31
31
  bbstrader/models/optimization.py,sha256=gp0n9a9vwbUldaNiZUYry_4RP2NW0VFZ2k5NoOkz30c,6581
32
32
  bbstrader/models/portfolio.py,sha256=-Zq9cmzyOZUlGq9RWfAxClpX0KJZqYZYpc5EGNTcPGI,8302
33
33
  bbstrader/models/risk.py,sha256=IFQoHXxpBwJiifANRgwyAUOp7EgTWBAhfJFCO1sGR3g,15405
34
34
  bbstrader/trading/__init__.py,sha256=2VoxbzfP1XBLVuxJtjRhjEBCtnv9HqwQzfMV4B5mM7M,468
35
- bbstrader/trading/execution.py,sha256=QDMOBBbzoHEXmpHAUBygLszO3MpZaSInexdi3bBkSmQ,35334
36
- bbstrader/trading/script.py,sha256=rzwx0gsbD9glEdeOHElK7GUJU9mC5X4F1AFpDyYxSds,5880
35
+ bbstrader/trading/execution.py,sha256=kRkvrX2HjGvIWgtGR3ar8FSkJmqPDFAWExYOaQMUrZQ,35495
36
+ bbstrader/trading/script.py,sha256=No6XW_Cz4vc28CtfcMhCDokb_ctXK-D4sGiOjxwMCXg,5875
37
37
  bbstrader/trading/scripts.py,sha256=rYK-LF2rmiHwpImfJ3P2HTMUcVU372Fxqj5UP6ypTJ0,1926
38
38
  bbstrader/trading/strategies.py,sha256=rMvLIhX_8MQg7_Lbo127UqdTRxBUof2m3jgRQTm55p0,37019
39
- bbstrader-0.2.92.dist-info/LICENSE,sha256=P3PBO9RuYPzl6-PkjysTNnwmwMB64ph36Bz9DBj8MS4,1115
40
- bbstrader-0.2.92.dist-info/METADATA,sha256=aOU1XGj6T2QVJdiR__RrymFwHApOmzIOc1J6XBZWqTw,11546
41
- bbstrader-0.2.92.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
42
- bbstrader-0.2.92.dist-info/entry_points.txt,sha256=0yDCbhbgHswOzJnY5wRSM_FjjyMHGvY7lJpSSVh0xtI,54
43
- bbstrader-0.2.92.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
44
- bbstrader-0.2.92.dist-info/RECORD,,
39
+ bbstrader-0.2.93.dist-info/LICENSE,sha256=P3PBO9RuYPzl6-PkjysTNnwmwMB64ph36Bz9DBj8MS4,1115
40
+ bbstrader-0.2.93.dist-info/METADATA,sha256=IUHCIzp-HO_VPJPubFU_IWVhaoeHMM41_ytwOcSBM4M,11546
41
+ bbstrader-0.2.93.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
42
+ bbstrader-0.2.93.dist-info/entry_points.txt,sha256=0yDCbhbgHswOzJnY5wRSM_FjjyMHGvY7lJpSSVh0xtI,54
43
+ bbstrader-0.2.93.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
44
+ bbstrader-0.2.93.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5