bbstrader 0.3.4__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.

bbstrader/__init__.py CHANGED
@@ -7,7 +7,7 @@ __author__ = "Bertin Balouki SIMYELI"
7
7
  __copyright__ = "2023-2025 Bertin Balouki SIMYELI"
8
8
  __email__ = "bertin@bbstrader.com"
9
9
  __license__ = "MIT"
10
- __version__ = "0.3.2"
10
+ __version__ = "0.3.5"
11
11
 
12
12
  from bbstrader import compat # noqa: F401
13
13
  from bbstrader import core # noqa: F401
bbstrader/compat.py CHANGED
@@ -2,18 +2,26 @@ import platform
2
2
  import sys
3
3
 
4
4
 
5
- def setup_mock_metatrader():
6
- """Mock MetaTrader5 on Linux and MacOS to prevent import errors."""
5
+ def setup_mock_modules():
6
+ """Mock some modules not available on some OS to prevent import errors."""
7
+ from unittest.mock import MagicMock
8
+
9
+ class Mock(MagicMock):
10
+ @classmethod
11
+ def __getattr__(cls, name):
12
+ return MagicMock()
13
+
14
+ MOCK_MODULES = []
15
+
16
+ # Mock Metatrader5 on Linux and MacOS
7
17
  if platform.system() != "Windows":
8
- from unittest.mock import MagicMock
18
+ MOCK_MODULES.append("MetaTrader5")
9
19
 
10
- class Mock(MagicMock):
11
- @classmethod
12
- def __getattr__(cls, name):
13
- return MagicMock()
20
+ # Mock posix On windows
21
+ if platform.system() == "Windows":
22
+ MOCK_MODULES.append("posix")
14
23
 
15
- MOCK_MODULES = ["MetaTrader5"]
16
- sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
24
+ sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
17
25
 
18
26
 
19
- setup_mock_metatrader()
27
+ setup_mock_modules()
bbstrader/config.py CHANGED
@@ -3,22 +3,6 @@ from pathlib import Path
3
3
  from typing import List
4
4
 
5
5
 
6
- TERMINAL = "/terminal64.exe"
7
- BASE_FOLDER = "C:/Program Files/"
8
-
9
- AMG_PATH = BASE_FOLDER + "Admirals Group MT5 Terminal" + TERMINAL
10
- PGL_PATH = BASE_FOLDER + "Pepperstone MetaTrader 5" + TERMINAL
11
- FTMO_PATH = BASE_FOLDER + "FTMO MetaTrader 5" + TERMINAL
12
- JGM_PATH = BASE_FOLDER + "JustMarkets MetaTrader 5" + TERMINAL
13
-
14
- BROKERS_PATHS = {
15
- "AMG": AMG_PATH,
16
- "FTMO": FTMO_PATH,
17
- "PGL": PGL_PATH,
18
- "JGM": JGM_PATH,
19
- }
20
-
21
-
22
6
  def get_config_dir(name: str = ".bbstrader") -> Path:
23
7
  """
24
8
  Get the path to the configuration directory.
bbstrader/core/scripts.py CHANGED
@@ -141,18 +141,19 @@ def send_news_feed(unknown):
141
141
 
142
142
  nltk.download("punkt", quiet=True)
143
143
  news = FinancialNews()
144
+ fmp_news = news.get_fmp_news(api=args.fmp) if args.fmp else None
144
145
  logger.info(f"Starting the News Feed on {args.interval} minutes")
145
146
  while True:
146
147
  try:
147
148
  fmp_articles = []
148
- coindesk_articles = news.get_coindesk_news(query=args.query)
149
- if args.fmp:
149
+ if fmp_news is not None:
150
150
  start = datetime.now() - timedelta(minutes=args.interval)
151
151
  start = start.strftime("%Y-%m-%d %H:%M:%S")
152
152
  end = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
153
- fmp_articles = news.get_fmp_news(api=args.fmp).get_latest_articles(
153
+ fmp_articles = fmp_news.get_latest_articles(
154
154
  save=True, start=start, end=end
155
155
  )
156
+ coindesk_articles = news.get_coindesk_news(query=args.query)
156
157
  if len(coindesk_articles) != 0:
157
158
  asyncio.run(
158
159
  send_articles(
@@ -51,6 +51,31 @@ ORDER_TYPE = {
51
51
  7: (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, "SELL STOP LIMIT"),
52
52
  }
53
53
 
54
+ STOP_RETCODES = [
55
+ Mt5.TRADE_RETCODE_TRADE_DISABLED,
56
+ Mt5.TRADE_RETCODE_NO_MONEY,
57
+ Mt5.TRADE_RETCODE_SERVER_DISABLES_AT,
58
+ Mt5.TRADE_RETCODE_CLIENT_DISABLES_AT,
59
+ Mt5.TRADE_RETCODE_ONLY_REAL,
60
+ ]
61
+
62
+ RETURN_RETCODE = [
63
+ Mt5.TRADE_RETCODE_MARKET_CLOSED,
64
+ Mt5.TRADE_RETCODE_CONNECTION,
65
+ Mt5.TRADE_RETCODE_LIMIT_ORDERS,
66
+ Mt5.TRADE_RETCODE_LIMIT_VOLUME,
67
+ Mt5.TRADE_RETCODE_LIMIT_POSITIONS,
68
+ Mt5.TRADE_RETCODE_LONG_ONLY,
69
+ Mt5.TRADE_RETCODE_SHORT_ONLY,
70
+ Mt5.TRADE_RETCODE_CLOSE_ONLY,
71
+ Mt5.TRADE_RETCODE_FIFO_CLOSE,
72
+ Mt5.TRADE_RETCODE_INVALID_VOLUME,
73
+ Mt5.TRADE_RETCODE_INVALID_PRICE,
74
+ Mt5.TRADE_RETCODE_INVALID_STOPS,
75
+ Mt5.TRADE_RETCODE_NO_CHANGES
76
+ ]
77
+
78
+
54
79
 
55
80
  class OrderAction(Enum):
56
81
  COPY_NEW = "COPY_NEW"
@@ -65,25 +90,23 @@ CopyMode = Literal["fix", "multiply", "percentage", "dynamic", "replicate"]
65
90
 
66
91
  def fix_lot(fixed):
67
92
  if fixed == 0 or fixed is None:
68
- raise ValueError("Fixed lot must be a number")
93
+ raise ValueError("Fixed lot must be a number > 0")
69
94
  return fixed
70
95
 
71
96
 
72
97
  def multiply_lot(lot, multiplier):
73
98
  if multiplier == 0 or multiplier is None:
74
- raise ValueError("Multiplier lot must be a number")
99
+ raise ValueError("Multiplier lot must be a number > 0")
75
100
  return lot * multiplier
76
101
 
77
102
 
78
103
  def percentage_lot(lot, percentage):
79
104
  if percentage == 0 or percentage is None:
80
- raise ValueError("Percentage lot must be a number")
105
+ raise ValueError("Percentage lot must be a number > 0")
81
106
  return round(lot * percentage / 100, 2)
82
107
 
83
108
 
84
109
  def dynamic_lot(source_lot, source_eqty: float, dest_eqty: float):
85
- if source_eqty == 0 or dest_eqty == 0:
86
- raise ValueError("Source or destination account equity is zero")
87
110
  try:
88
111
  ratio = dest_eqty / source_eqty
89
112
  return round(source_lot * ratio, 2)
@@ -118,6 +141,7 @@ def fixed_lot(lot, symbol, destination) -> float:
118
141
  else:
119
142
  return _check_lot(round(lot), s_info)
120
143
 
144
+
121
145
  def calculate_copy_lot(
122
146
  source_lot,
123
147
  symbol: str,
@@ -173,7 +197,7 @@ def get_symbols_from_string(symbols_string: str):
173
197
 
174
198
  def get_copy_symbols(destination: dict, source: dict) -> List[str] | Dict[str, str]:
175
199
  symbols = destination.get("symbols", "all")
176
- if symbols == "all" or symbols == "*" or isinstance(symbols, list) :
200
+ if symbols == "all" or symbols == "*" or isinstance(symbols, list):
177
201
  src_account = Account(**source)
178
202
  src_symbols = src_account.get_symbols()
179
203
  dest_account = Account(**destination)
@@ -360,7 +384,10 @@ class TradeCopier(object):
360
384
  for destination in self.destinations:
361
385
  destination["copy"] = destination.get("copy", True)
362
386
 
363
- def log_message(self, message, type="info"):
387
+ def log_message(
388
+ self, message, type: Literal["info", "error", "debug", "warning"] = "info"
389
+ ):
390
+ logger.trace
364
391
  if self.log_queue:
365
392
  try:
366
393
  now = datetime.now()
@@ -370,7 +397,7 @@ class TradeCopier(object):
370
397
  )
371
398
  space = len("exception") # longest log name
372
399
  self.log_queue.put(
373
- f"{formatted} |{type.upper()} {' '*(space - len(type))} | - {message}"
400
+ f"{formatted} |{type.upper()} {' ' * (space - len(type))} | - {message}"
374
401
  )
375
402
  except Exception:
376
403
  pass
@@ -384,8 +411,8 @@ class TradeCopier(object):
384
411
  error_msg = repr(e)
385
412
  if error_msg not in self.errors:
386
413
  self.errors.add(error_msg)
387
- add_msg = f"SYMBOL={symbol}" if symbol else ""
388
- message = f"Error encountered: {error_msg}, {add_msg}"
414
+ add_msg = f", SYMBOL={symbol}" if symbol else ""
415
+ message = f"Error encountered: {error_msg}{add_msg}"
389
416
  self.log_message(message, type="error")
390
417
 
391
418
  def _validate_source(self):
@@ -487,6 +514,14 @@ class TradeCopier(object):
487
514
  if new_result.retcode == Mt5.TRADE_RETCODE_DONE:
488
515
  break
489
516
  return new_result
517
+
518
+ def handle_retcode(self, retcode) -> int:
519
+ if retcode in STOP_RETCODES:
520
+ msg = trade_retcode_message(retcode)
521
+ self.log_error(f"Critical Error on @{self.source['login']}: {msg} ")
522
+ self.stop()
523
+ if retcode in RETURN_RETCODE:
524
+ return 1
490
525
 
491
526
  def copy_new_trade(self, trade: TradeOrder | TradePosition, destination: dict):
492
527
  if not self.iscopy_time():
@@ -508,7 +543,6 @@ class TradeCopier(object):
508
543
  trade_action = (
509
544
  Mt5.TRADE_ACTION_DEAL if trade.type in [0, 1] else Mt5.TRADE_ACTION_PENDING
510
545
  )
511
- action = ORDER_TYPE[trade.type][1]
512
546
  tick = Mt5.symbol_info_tick(symbol)
513
547
  price = tick.bid if trade.type == 0 else tick.ask
514
548
  try:
@@ -535,14 +569,18 @@ class TradeCopier(object):
535
569
  result = Mt5.order_send(request)
536
570
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
537
571
  result = self._update_filling_type(request, result)
572
+ action = ORDER_TYPE[trade.type][1]
573
+ copy_action = "Position" if trade.type in [0, 1] else "Order"
538
574
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
539
575
  self.log_message(
540
- f"Copy {action} Order #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
576
+ f"Copy {action} {copy_action} #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
541
577
  f"to @{destination.get('login')}::{symbol}",
542
578
  )
543
579
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
580
+ if self.handle_retcode(result.retcode) == 1:
581
+ return
544
582
  self.log_message(
545
- f"Error copying {action} Order #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
583
+ f"Error copying {action} {copy_action} #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
546
584
  f"to @{destination.get('login')}::{symbol}, {trade_retcode_message(result.retcode)}",
547
585
  type="error",
548
586
  )
@@ -572,9 +610,9 @@ class TradeCopier(object):
572
610
  f"Modify {ORDER_TYPE[source_order.type][1]} Order #{ticket} on @{destination.get('login')}::{symbol}, "
573
611
  f"SOURCE=@{self.source.get('login')}::{source_order.symbol}"
574
612
  )
575
- if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
576
- return
577
613
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
614
+ if self.handle_retcode(result.retcode) == 1:
615
+ return
578
616
  self.log_message(
579
617
  f"Error modifying {ORDER_TYPE[source_order.type][1]} Order #{ticket} on @{destination.get('login')}::{symbol},"
580
618
  f"SOURCE=@{self.source.get('login')}::{source_order.symbol}, {trade_retcode_message(result.retcode)}",
@@ -597,6 +635,8 @@ class TradeCopier(object):
597
635
  f"SOURCE=@{self.source.get('login')}::{src_symbol}"
598
636
  )
599
637
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
638
+ if self.handle_retcode(result.retcode) == 1:
639
+ return
600
640
  self.log_message(
601
641
  f"Error closing {ORDER_TYPE[order.type][1]} Order #{order.ticket} on @{destination.get('login')}::{order.symbol}, "
602
642
  f"SOURCE=@{self.source.get('login')}::{src_symbol}, {trade_retcode_message(result.retcode)}",
@@ -626,9 +666,9 @@ class TradeCopier(object):
626
666
  f"Modify {ORDER_TYPE[source_pos.type][1]} Position #{ticket} on @{destination.get('login')}::{symbol}, "
627
667
  f"SOURCE=@{self.source.get('login')}::{source_pos.symbol}"
628
668
  )
629
- if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
630
- return
631
669
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
670
+ if self.handle_retcode(result.retcode) == 1:
671
+ return
632
672
  self.log_message(
633
673
  f"Error modifying {ORDER_TYPE[source_pos.type][1]} Position #{ticket} on @{destination.get('login')}::{symbol}, "
634
674
  f"SOURCE=@{self.source.get('login')}::{source_pos.symbol}, {trade_retcode_message(result.retcode)}",
@@ -663,6 +703,8 @@ class TradeCopier(object):
663
703
  f"SOURCE=@{self.source.get('login')}::{src_symbol}"
664
704
  )
665
705
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
706
+ if self.handle_retcode(result.retcode) == 1:
707
+ return
666
708
  self.log_message(
667
709
  f"Error closing {ORDER_TYPE[position.type][1]} Position #{position.ticket} "
668
710
  f"on @{destination.get('login')}::{position.symbol}, "
@@ -1023,7 +1065,8 @@ class TradeCopier(object):
1023
1065
  self.destinations
1024
1066
  ):
1025
1067
  self.log_message(
1026
- "Two or more destination accounts have the same Terminal path, which is not allowed."
1068
+ "Two or more destination accounts have the same Terminal path, which is not allowed.",
1069
+ type="error",
1027
1070
  )
1028
1071
  return
1029
1072
 
@@ -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)
@@ -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(
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bbstrader
3
- Version: 0.3.4
3
+ Version: 0.3.5
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/
7
7
  Author: Bertin Balouki SIMYELI
8
8
  Author-email: <bertin@bbstrader.com>
9
9
  Maintainer: Bertin Balouki SIMYELI
10
- License: The MIT License (MIT)
10
+ License: MIT
11
11
  Project-URL: Documentation, https://bbstrader.readthedocs.io/en/latest/
12
12
  Project-URL: Source Code, https://github.com/bbalouki/bbstrader
13
13
  Keywords: Finance,Toolkit,Financial,Analysis,Fundamental,Quantitative,Database,Equities,Currencies,Economics,ETFs,Funds,Indices,Moneymarkets,Commodities,Futures,CFDs,Derivatives,Trading,Investing,Portfolio,Optimization,Performance
@@ -15,50 +15,47 @@ Classifier: Development Status :: 5 - Production/Stable
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: Intended Audience :: Financial and Insurance Industry
17
17
  Classifier: Topic :: Office/Business :: Financial :: Investment
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
18
  Classifier: Programming Language :: Python :: 3.12
21
19
  Classifier: Operating System :: Microsoft :: Windows
22
20
  Classifier: Operating System :: POSIX :: Linux
23
21
  Classifier: Operating System :: MacOS
24
- Classifier: License :: OSI Approved :: MIT License
25
22
  Description-Content-Type: text/markdown
26
23
  License-File: LICENSE
27
- Requires-Dist: alphalens-reloaded>=0.4.5
28
- Requires-Dist: beautifulsoup4>=4.13.1
24
+ Requires-Dist: alphalens-reloaded>=0.4.6
25
+ Requires-Dist: beautifulsoup4>=4.13.5
29
26
  Requires-Dist: colorama>=0.4.6
30
- Requires-Dist: CurrencyConverter>=0.18.2
31
- Requires-Dist: dash>=2.18.2
27
+ Requires-Dist: CurrencyConverter>=0.18.9
28
+ Requires-Dist: dash>=3.2.0
32
29
  Requires-Dist: eodhd>=1.0.32
33
- Requires-Dist: exchange-calendars>=4.9
30
+ Requires-Dist: exchange_calendars>=4.11.1
34
31
  Requires-Dist: filterpy>=1.4.5
35
- Requires-Dist: financetoolkit>=1.9.9
36
- Requires-Dist: ipython>=8.32.0
37
- Requires-Dist: lightgbm>=4.5.0
32
+ Requires-Dist: financetoolkit>=2.0.4
33
+ Requires-Dist: ipython>=9.5.0
34
+ Requires-Dist: lightgbm>=4.6.0
38
35
  Requires-Dist: nltk>=3.9.1
39
- Requires-Dist: notify-py>=0.3.43
40
- Requires-Dist: numpy>=1.26.0
41
- Requires-Dist: pandas_ta<=0.4.67b0
36
+ Requires-Dist: notify_py>=0.3.43
37
+ Requires-Dist: numpy>=2.2.6
38
+ Requires-Dist: pandas-ta>=0.4.67b0
42
39
  Requires-Dist: praw>=7.8.1
43
- Requires-Dist: pyfiglet>=1.0.2
44
- Requires-Dist: pykalman>=0.10.1
40
+ Requires-Dist: pyfiglet>=1.0.4
41
+ Requires-Dist: pykalman>=0.10.2
45
42
  Requires-Dist: pyportfolioopt>=1.5.6
46
- Requires-Dist: python-dotenv>=1.0.1
47
- Requires-Dist: python-telegram-bot>=21.10
43
+ Requires-Dist: python-dotenv>=1.1.1
44
+ Requires-Dist: python-telegram-bot>=22.3
48
45
  Requires-Dist: PyYAML>=6.0.2
49
- Requires-Dist: QuantStats>=0.0.64
50
- Requires-Dist: scikit-learn>=1.6.1
46
+ Requires-Dist: QuantStats>=0.0.77
47
+ Requires-Dist: scikit-learn>=1.7.2
51
48
  Requires-Dist: seaborn>=0.13.2
52
- Requires-Dist: spacy>=3.8.4
53
- Requires-Dist: statsmodels>=0.14.4
49
+ Requires-Dist: spacy>=3.8.7
50
+ Requires-Dist: statsmodels>=0.14.5
54
51
  Requires-Dist: sumy>=0.11.0
55
52
  Requires-Dist: tables>=3.10.2
56
53
  Requires-Dist: tabulate>=0.9.0
57
54
  Requires-Dist: textblob>=0.19.0
58
55
  Requires-Dist: tqdm>=4.67.1
59
- Requires-Dist: tweepy>=4.15.0
56
+ Requires-Dist: tweepy>=4.16.0
60
57
  Requires-Dist: vaderSentiment>=3.3.2
61
- Requires-Dist: yfinance>=0.2.55
58
+ Requires-Dist: yfinance>=0.2.65
62
59
  Provides-Extra: mt5
63
60
  Requires-Dist: MetaTrader5; extra == "mt5"
64
61
  Dynamic: author
@@ -84,7 +81,7 @@ Dynamic: summary
84
81
  [![Supported Python Versions](https://img.shields.io/pypi/pyversions/bbstrader)](https://pypi.org/project/bbstrader/)
85
82
  [![PyPI Downloads](https://static.pepy.tech/badge/bbstrader)](https://pepy.tech/projects/bbstrader)
86
83
  [![CodeFactor](https://www.codefactor.io/repository/github/bbalouki/bbstrader/badge)](https://www.codefactor.io/repository/github/bbalouki/bbstrader)
87
- [![LinkedIn](https://img.shields.io/badge/LinkedIn-grey?logo=Linkedin&logoColor=white)](https://www.linkedin.com/in/bertin-balouki-simyeli-15b17a1a6/)
84
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-grey?logo=Linkedin&logoColor=white)](https://www.linkedin.com/in/bertin-balouki-s-15b17a1a6)
88
85
  [![PayPal Me](https://img.shields.io/badge/PayPal%20Me-blue?logo=paypal)](https://paypal.me/bertinbalouki?country.x=SN&locale.x=en_US)
89
86
 
90
87
  [Dcoumentation](https://bbstrader.readthedocs.io/en/latest/index.html)
@@ -1,7 +1,7 @@
1
- bbstrader/__init__.py,sha256=4KVGBEYU3ao9zPVM3rMWqNuvCleCeA6C2MVe_AFc4rw,581
1
+ bbstrader/__init__.py,sha256=-5f30lprSg-ufbPbmj1J3P63elB--NPXo9_G_QuhWF0,581
2
2
  bbstrader/__main__.py,sha256=RjUIJWaD2_Od8ZMSLL8dzc2ZuCkkzlvNaE7-LIu3RGU,2488
3
- bbstrader/compat.py,sha256=djbHMvTvy0HYm1zyZ6Ttp_LMwP2PqTSVw1r7pqbz7So,487
4
- bbstrader/config.py,sha256=riZxwb4hN0I-dSsWcjnROc5dWQpSJ9iKOMIp4PMGfko,3970
3
+ bbstrader/compat.py,sha256=bHTPTalffQNVv7MNRPaimJ4d9rQEwQFLSovHlUbxNgA,647
4
+ bbstrader/config.py,sha256=Q8xi6laFo-R-NNGotbHOgy8GLLuwvH6MaoiLhfTbYok,3548
5
5
  bbstrader/tseries.py,sha256=SM_LTQHJ3ZXVkVJyZ51CefUDzJDl2TkJqBKMp_uM8s4,43833
6
6
  bbstrader/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  bbstrader/apps/_copier.py,sha256=z6GhLSmFjHc_oh7pYRTflH42Xmpe3Z7B2fUyedzDQPo,25387
@@ -16,22 +16,22 @@ bbstrader/btengine/scripts.py,sha256=8o66dq4Ex4DsH4s8xvJqUOFjLzZJSnbBvvNBzohtzoE
16
16
  bbstrader/btengine/strategy.py,sha256=hDghr5sXNitWlWXXvgl8Vj-QHWaIuVlx54LUXAbQrHQ,36725
17
17
  bbstrader/core/__init__.py,sha256=GIFzFSStPfE0XM2j7mDeZZQeMTh_AwPsDOQXwMVJLgw,97
18
18
  bbstrader/core/data.py,sha256=5-ByClb-E3-iqDz8CBJ4om9wBIA7DmUWezu4A-tv5ys,25095
19
- bbstrader/core/scripts.py,sha256=bMZ5I_hCTPCsAn9v9Sz9fQQ7JFkf_Zwtv7uUJwaqVBU,5466
19
+ bbstrader/core/scripts.py,sha256=7lNddfX7WaZfiE5dENEfzv3XrAPrwoE9FYSaTie3cwM,5524
20
20
  bbstrader/core/utils.py,sha256=tHXQimmmlYZHktNnYNKn_wVq6v-85pI7DXF6xlJV7ps,2780
21
21
  bbstrader/ibkr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  bbstrader/ibkr/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  bbstrader/metatrader/__init__.py,sha256=A5Ye9tpc2sp9Xk5qjKw-EfYsoRcZtAt8nqvC3tCtZs8,333
24
24
  bbstrader/metatrader/account.py,sha256=Ur0vKz_puUP9jmdJUpvdQq6W5ue0RfnhkL0JLPBQkOE,65200
25
25
  bbstrader/metatrader/analysis.py,sha256=ywETmG3qxZ7ms_DCjR1GcQoUrQ0es5-n-CPDYKBAm8Q,3614
26
- bbstrader/metatrader/copier.py,sha256=knGvLsiWUzM6eYe7CrKF6rTeReFdogdCQU5Q7p7UHmY,54186
26
+ bbstrader/metatrader/copier.py,sha256=RxXm_NKpaad4sMT1kuJNjeJcbP368asXLaAVqmoRfmE,55553
27
27
  bbstrader/metatrader/rates.py,sha256=w9mr6FB6E1zLcHCDtDGt-oMnw6sakIU6Qe3455KDsSg,20782
28
28
  bbstrader/metatrader/risk.py,sha256=NhW8qtSg350Z6H9oLcDqOU_erqd_7Y7F5FwpfPN5Qso,27262
29
29
  bbstrader/metatrader/scripts.py,sha256=8meq6_zz6jPSibNgtYtaO8Ba-uJZOoLkpqYUIjidk-U,4010
30
- bbstrader/metatrader/trade.py,sha256=ezkALiUgtIu54R4m4blQjptWoRXNVG5wwuoctP2b90Y,80624
30
+ bbstrader/metatrader/trade.py,sha256=X6DNpPrHL0xQUAblJsCxmpSNWlFw8otEM1pWpgYTydE,81041
31
31
  bbstrader/metatrader/utils.py,sha256=PnFZ8EuBSZgsYlvwZDOxj4vUTtt-hUYnnwFBmu7gxxw,20738
32
32
  bbstrader/models/__init__.py,sha256=B-bn2h_SCK6gRAs2li6dDVnvV8jDT5suZimldk5xxcw,497
33
- bbstrader/models/factors.py,sha256=Y1rjwhWU4aiSRd-jFOLnLZczFCY0bJUxauCo17HvOFY,12791
34
- bbstrader/models/ml.py,sha256=d_maEXwGOGuWawisjIbMO5FUsaSjlgmJu5XiMu28Wf8,48921
33
+ bbstrader/models/factors.py,sha256=J7yxtDr1sCTw1AI59kluF89e2b9HkpEXfFyIcfPHUCQ,13008
34
+ bbstrader/models/ml.py,sha256=NVN9zxRRDJn2S8KSgGBkiSHvdFjsDiaNsW2Y6rs51Io,50314
35
35
  bbstrader/models/nlp.py,sha256=hcvz9d_8j1cIC1h3oqa1DBjExRIEd6WSiZb95Vr3NPo,32638
36
36
  bbstrader/models/optimization.py,sha256=Fa4tdhynMmvKt5KHV9cH1TXmmJVJwU4QWpYkbeVq4aI,6395
37
37
  bbstrader/models/portfolio.py,sha256=r-47Zrn2r7iKCHm5YVtwkbBJXAZGM3QYy-rXCWY9-Bg,8079
@@ -41,9 +41,9 @@ bbstrader/trading/execution.py,sha256=CYt4fageoqcpMFvdRH-jX4hexAGUiG_wE94i1qg7BF
41
41
  bbstrader/trading/scripts.py,sha256=Tf5q33WqqygjpIv43_8nA82VZ3GM0qgb4Ggo3fHJ_wg,5744
42
42
  bbstrader/trading/strategies.py,sha256=RZ6P4SfIyRW72v0OnPnrc4Hv8X00FdxR-_sD23xe_Pg,11756
43
43
  bbstrader/trading/utils.py,sha256=57dKF9dcRu04oU2VRqydRrzW39dCW2wlDWhVt-sZdRw,1857
44
- bbstrader-0.3.4.dist-info/licenses/LICENSE,sha256=ZwC_RqqGmOPBUiMDKqLyJZ5HBeHq53LpL7TMRzrJY8c,1094
45
- bbstrader-0.3.4.dist-info/METADATA,sha256=_Zc1Yp1Exs0wUpD0KV1G41uDnR3v7mhHWawRDbkutSM,27089
46
- bbstrader-0.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- bbstrader-0.3.4.dist-info/entry_points.txt,sha256=0yDCbhbgHswOzJnY5wRSM_FjjyMHGvY7lJpSSVh0xtI,54
48
- bbstrader-0.3.4.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
49
- bbstrader-0.3.4.dist-info/RECORD,,
44
+ bbstrader-0.3.5.dist-info/licenses/LICENSE,sha256=ZwC_RqqGmOPBUiMDKqLyJZ5HBeHq53LpL7TMRzrJY8c,1094
45
+ bbstrader-0.3.5.dist-info/METADATA,sha256=-FeMRpGJTL9HvVnWmNbhLiHlu0kYfb7wqqANfLzVrew,26910
46
+ bbstrader-0.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
+ bbstrader-0.3.5.dist-info/entry_points.txt,sha256=0yDCbhbgHswOzJnY5wRSM_FjjyMHGvY7lJpSSVh0xtI,54
48
+ bbstrader-0.3.5.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
49
+ bbstrader-0.3.5.dist-info/RECORD,,