bbstrader 0.2.97__py3-none-any.whl → 0.2.99__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.

@@ -137,6 +137,21 @@ class TradeSignal:
137
137
  )
138
138
 
139
139
 
140
+ Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
141
+ Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
142
+ Positions = Literal["all", "buy", "sell", "profitable", "losing"]
143
+ Orders = Literal[
144
+ "all",
145
+ "buy_stops",
146
+ "sell_stops",
147
+ "buy_limits",
148
+ "sell_limits",
149
+ "buy_stop_limits",
150
+ "sell_stop_limits",
151
+ ]
152
+
153
+ EXPERT_ID = 98181105
154
+
140
155
  class Trade(RiskManagement):
141
156
  """
142
157
  Extends the `RiskManagement` class to include specific trading operations,
@@ -195,7 +210,7 @@ class Trade(RiskManagement):
195
210
  self,
196
211
  symbol: str = "EURUSD",
197
212
  expert_name: str = "bbstrader",
198
- expert_id: int = 9818,
213
+ expert_id: int = EXPERT_ID,
199
214
  version: str = "2.0",
200
215
  target: float = 5.0,
201
216
  start_time: str = "0:00",
@@ -239,15 +254,12 @@ class Trade(RiskManagement):
239
254
  See the ``bbstrader.metatrader.risk.RiskManagement`` class for more details on these parameters.
240
255
  See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
241
256
  """
242
- # Call the parent class constructor first
243
257
  super().__init__(
244
258
  symbol=symbol,
245
259
  start_time=start_time,
246
260
  finishing_time=finishing_time,
247
- **kwargs, # Pass kwargs to the parent constructor
261
+ **kwargs,
248
262
  )
249
-
250
- # Initialize Trade-specific attributes
251
263
  self.symbol = symbol
252
264
  self.expert_name = expert_name
253
265
  self.expert_id = expert_id
@@ -261,12 +273,6 @@ class Trade(RiskManagement):
261
273
  self.tf = kwargs.get("time_frame", "D1")
262
274
  self.kwargs = kwargs
263
275
 
264
- self.start_time_hour, self.start_time_minutes = self.start.split(":")
265
- self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
266
- ":"
267
- )
268
- self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
269
-
270
276
  self.buy_positions = []
271
277
  self.sell_positions = []
272
278
  self.opened_positions = []
@@ -371,21 +377,18 @@ class Trade(RiskManagement):
371
377
 
372
378
  def summary(self):
373
379
  """Show a brief description about the trading program"""
380
+ start = datetime.strptime(self.start, "%H:%M").time()
381
+ finish = datetime.strptime(self.finishing, "%H:%M").time()
382
+ end = datetime.strptime(self.end, "%H:%M").time()
374
383
  summary_data = [
375
384
  ["Expert Advisor Name", f"@{self.expert_name}"],
376
385
  ["Expert Advisor Version", f"@{self.version}"],
377
386
  ["Expert | Strategy ID", self.expert_id],
378
387
  ["Trading Symbol", self.symbol],
379
388
  ["Trading Time Frame", self.tf],
380
- ["Start Trading Time", f"{self.start_time_hour}:{self.start_time_minutes}"],
381
- [
382
- "Finishing Trading Time",
383
- f"{self.finishing_time_hour}:{self.finishing_time_minutes}",
384
- ],
385
- [
386
- "Closing Position After",
387
- f"{self.ending_time_hour}:{self.ending_time_minutes}",
388
- ],
389
+ ["Start Trading Time", f"{start}"],
390
+ ["Finishing Trading Time", f"{finish}"],
391
+ ["Closing Position After", f"{end}"],
389
392
  ]
390
393
  # Custom table format
391
394
  summary_table = tabulate(
@@ -480,18 +483,14 @@ class Trade(RiskManagement):
480
483
  session_data, headers=["Statistics", "Values"], tablefmt="outline"
481
484
  )
482
485
 
483
- # Print the formatted statistics
484
486
  if self.verbose:
485
487
  print("\n[======= Trading Session Statistics =======]")
486
488
  print(session_table)
487
489
 
488
- # Save to CSV if specified
489
- if save:
490
+ if save and stats["deals"] > 0:
490
491
  today_date = datetime.now().strftime("%Y%m%d%H%M%S")
491
- # Create a dictionary with the statistics
492
492
  statistics_dict = {item[0]: item[1] for item in session_data}
493
493
  stats_df = pd.DataFrame(statistics_dict, index=[0])
494
- # Create the directory if it doesn't exist
495
494
  dir = dir or ".sessions"
496
495
  os.makedirs(dir, exist_ok=True)
497
496
  if "." in self.symbol:
@@ -504,8 +503,6 @@ class Trade(RiskManagement):
504
503
  stats_df.to_csv(filepath, index=False)
505
504
  LOGGER.info(f"Session statistics saved to {filepath}")
506
505
 
507
- Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
508
-
509
506
  def open_buy_position(
510
507
  self,
511
508
  action: Buys = "BMKT",
@@ -521,7 +518,7 @@ class Trade(RiskManagement):
521
518
  tp: Optional[float] = None,
522
519
  ):
523
520
  """
524
- Open a Buy positin
521
+ Open a Buy position
525
522
 
526
523
  Args:
527
524
  action (str): `BMKT` for Market orders or `BLMT`,
@@ -534,6 +531,8 @@ class Trade(RiskManagement):
534
531
  mm (bool): Weither to put stop loss and tp or not
535
532
  trail (bool): Weither to trail the stop loss or not
536
533
  comment (str): The comment for the opening position
534
+ sl (float): The stop loss price
535
+ tp (float): The take profit price
537
536
  """
538
537
  Id = id if id is not None else self.expert_id
539
538
  point = self.get_symbol_info(self.symbol).point
@@ -583,7 +582,7 @@ class Trade(RiskManagement):
583
582
  return False
584
583
 
585
584
  def _order_type(self):
586
- type = {
585
+ return {
587
586
  "BMKT": (Mt5.ORDER_TYPE_BUY, "BUY"),
588
587
  "SMKT": (Mt5.ORDER_TYPE_BUY, "SELL"),
589
588
  "BLMT": (Mt5.ORDER_TYPE_BUY_LIMIT, "BUY_LIMIT"),
@@ -593,9 +592,6 @@ class Trade(RiskManagement):
593
592
  "BSTPLMT": (Mt5.ORDER_TYPE_BUY_STOP_LIMIT, "BUY_STOP_LIMIT"),
594
593
  "SSTPLMT": (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, "SELL_STOP_LIMIT"),
595
594
  }
596
- return type
597
-
598
- Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
599
595
 
600
596
  def open_sell_position(
601
597
  self,
@@ -612,7 +608,7 @@ class Trade(RiskManagement):
612
608
  tp: Optional[float] = None,
613
609
  ):
614
610
  """
615
- Open a sell positin
611
+ Open a sell position
616
612
 
617
613
  Args:
618
614
  action (str): `SMKT` for Market orders
@@ -627,8 +623,8 @@ class Trade(RiskManagement):
627
623
  comment (str): The comment for the closing position
628
624
  symbol (str): The symbol to trade
629
625
  volume (float): The volume (lot) to trade
630
- sl (float): The stop loss in points
631
- tp (float): The take profit in points
626
+ sl (float): The stop loss price
627
+ tp (float): The take profit price
632
628
  """
633
629
  Id = id if id is not None else self.expert_id
634
630
  point = self.get_symbol_info(self.symbol).point
@@ -694,11 +690,11 @@ class Trade(RiskManagement):
694
690
  LOGGER.info(f"Not Trading time, SYMBOL={self.symbol}")
695
691
  return False
696
692
  elif not self.is_risk_ok():
697
- LOGGER.error(f"Account Risk not allowed, SYMBOL={self.symbol}")
693
+ LOGGER.warning(f"Account Risk not allowed, SYMBOL={self.symbol}")
698
694
  self._check(comment)
699
695
  return False
700
696
  elif self.is_max_trades_reached():
701
- LOGGER.error(f"Maximum trades reached for Today, SYMBOL={self.symbol}")
697
+ LOGGER.warning(f"Maximum trades reached for Today, SYMBOL={self.symbol}")
702
698
  return False
703
699
  elif self.profit_target():
704
700
  self._check(f"Profit target Reached !!! SYMBOL={self.symbol}")
@@ -838,7 +834,8 @@ class Trade(RiskManagement):
838
834
  action (str): (`'BMKT'`, `'SMKT'`) for Market orders
839
835
  or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
840
836
  price (float): The price at which to open an order
841
- stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
837
+ stoplimit (float): A price a pending Limit order is set at
838
+ when the price reaches the 'price' value (this condition is mandatory).
842
839
  The pending order is not passed to the trading system until that moment
843
840
  id (int): The strategy id or expert Id
844
841
  mm (bool): Weither to put stop loss and tp or not
@@ -846,43 +843,32 @@ class Trade(RiskManagement):
846
843
  comment (str): The comment for the closing position
847
844
  symbol (str): The symbol to trade
848
845
  volume (float): The volume (lot) to trade
849
- sl (float): The stop loss in points
850
- tp (float): The take profit in points
846
+ sl (float): The stop loss price
847
+ tp (float): The take profit price
851
848
  """
852
849
  BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
853
850
  SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
854
851
  if action in BUYS:
855
- return self.open_buy_position(
856
- action=action,
857
- price=price,
858
- stoplimit=stoplimit,
859
- id=id,
860
- mm=mm,
861
- trail=trail,
862
- comment=comment,
863
- symbol=symbol,
864
- volume=volume,
865
- sl=sl,
866
- tp=tp,
867
- )
852
+ open_position = self.open_buy_position
868
853
  elif action in SELLS:
869
- return self.open_sell_position(
870
- action=action,
871
- price=price,
872
- stoplimit=stoplimit,
873
- id=id,
874
- mm=mm,
875
- trail=trail,
876
- comment=comment,
877
- symbol=symbol,
878
- volume=volume,
879
- sl=sl,
880
- tp=tp,
881
- )
854
+ open_position = self.open_sell_position
882
855
  else:
883
856
  raise ValueError(
884
857
  f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}"
885
858
  )
859
+ return open_position(
860
+ action=action,
861
+ price=price,
862
+ stoplimit=stoplimit,
863
+ id=id,
864
+ mm=mm,
865
+ trail=trail,
866
+ comment=comment,
867
+ symbol=symbol,
868
+ volume=volume,
869
+ sl=sl,
870
+ tp=tp,
871
+ )
886
872
 
887
873
  @property
888
874
  def orders(self):
@@ -1137,9 +1123,9 @@ class Trade(RiskManagement):
1137
1123
  be = self.get_break_even()
1138
1124
  if trail_after_points is not None:
1139
1125
  if isinstance(trail_after_points, int):
1140
- assert trail_after_points > be, (
1141
- "trail_after_points must be greater than break even or set to None"
1142
- )
1126
+ assert (
1127
+ trail_after_points > be
1128
+ ), "trail_after_points must be greater than break even or set to None"
1143
1129
  trail_after_points = self._get_trail_after_points(trail_after_points)
1144
1130
  if positions is not None:
1145
1131
  for position in positions:
@@ -1228,7 +1214,8 @@ class Trade(RiskManagement):
1228
1214
  Sets the break-even level for a given trading position.
1229
1215
 
1230
1216
  Args:
1231
- position (TradePosition): The trading position for which the break-even is to be set. This is the value return by `mt5.positions_get()`.
1217
+ position (TradePosition): The trading position for which the break-even is to be set.
1218
+ This is the value return by `mt5.positions_get()`.
1232
1219
  be (int): The break-even level in points.
1233
1220
  level (float): The break-even level in price, if set to None , it will be calated automaticaly.
1234
1221
  price (float): The break-even price, if set to None , it will be calated automaticaly.
@@ -1370,13 +1357,16 @@ class Trade(RiskManagement):
1370
1357
  profit = 0.0
1371
1358
  balance = self.get_account_info().balance
1372
1359
  target = round((balance * self.target) / 100, 2)
1373
- if len(self.opened_positions) != 0:
1374
- for position in self.opened_positions:
1360
+ opened_positions = self.get_today_deals(group=self.symbol)
1361
+ if len(opened_positions) != 0:
1362
+ for position in opened_positions:
1375
1363
  time.sleep(0.1)
1376
1364
  # This return two TradeDeal Object,
1377
1365
  # The first one is the opening order
1378
1366
  # The second is the closing order
1379
- history = self.get_trades_history(position=position, to_df=False)
1367
+ history = self.get_trades_history(
1368
+ position=position.position_id, to_df=False
1369
+ )
1380
1370
  if history is not None and len(history) == 2:
1381
1371
  profit += history[1].profit
1382
1372
  commission += history[0].commission
@@ -1456,7 +1446,8 @@ class Trade(RiskManagement):
1456
1446
  Args:
1457
1447
  ticket (int): Order ticket to modify (e.g TradeOrder.ticket)
1458
1448
  price (float): The price at which to modify the order
1459
- stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
1449
+ stoplimit (float): A price a pending Limit order is set at
1450
+ when the price reaches the 'price' value (this condition is mandatory).
1460
1451
  The pending order is not passed to the trading system until that moment
1461
1452
  sl (float): The stop loss in points
1462
1453
  tp (float): The take profit in points
@@ -1598,16 +1589,6 @@ class Trade(RiskManagement):
1598
1589
  f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}."
1599
1590
  )
1600
1591
 
1601
- Orders = Literal[
1602
- "all",
1603
- "buy_stops",
1604
- "sell_stops",
1605
- "buy_limits",
1606
- "sell_limits",
1607
- "buy_stop_limits",
1608
- "sell_stop_limits",
1609
- ]
1610
-
1611
1592
  def close_orders(
1612
1593
  self,
1613
1594
  order_type: Orders,
@@ -1616,7 +1597,8 @@ class Trade(RiskManagement):
1616
1597
  ):
1617
1598
  """
1618
1599
  Args:
1619
- order_type (str): Type of orders to close ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
1600
+ order_type (str): Type of orders to close
1601
+ ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
1620
1602
  id (int): The unique ID of the Expert or Strategy
1621
1603
  comment (str): Comment for the closing position
1622
1604
  """
@@ -1642,8 +1624,6 @@ class Trade(RiskManagement):
1642
1624
  orders, "orders", self.close_order, order_type, id=id, comment=comment
1643
1625
  )
1644
1626
 
1645
- Positions = Literal["all", "buy", "sell", "profitable", "losing"]
1646
-
1647
1627
  def close_positions(
1648
1628
  self,
1649
1629
  position_type: Positions,
@@ -1689,13 +1669,11 @@ class Trade(RiskManagement):
1689
1669
  List[TradeDeal]: List of today deals
1690
1670
  """
1691
1671
  date_from = datetime.now() - timedelta(days=2)
1692
- history = self.get_trades_history(date_from=date_from, group=group, to_df=False)
1672
+ history = (
1673
+ self.get_trades_history(date_from=date_from, group=group, to_df=False) or []
1674
+ )
1693
1675
  positions_ids = set(
1694
- [
1695
- deal.position_id
1696
- for deal in history
1697
- if history is not None and deal.magic == self.expert_id
1698
- ]
1676
+ [deal.position_id for deal in history if deal.magic == self.expert_id]
1699
1677
  )
1700
1678
  today_deals = []
1701
1679
  for position in positions_ids:
@@ -1715,11 +1693,12 @@ class Trade(RiskManagement):
1715
1693
  :return: bool
1716
1694
  """
1717
1695
  negative_deals = 0
1696
+ max_trades = self.max_trade()
1718
1697
  today_deals = self.get_today_deals(group=self.symbol)
1719
1698
  for deal in today_deals:
1720
1699
  if deal.profit < 0:
1721
1700
  negative_deals += 1
1722
- if negative_deals >= self.max_trades:
1701
+ if negative_deals >= max_trades:
1723
1702
  return True
1724
1703
  return False
1725
1704
 
@@ -1800,7 +1779,6 @@ class Trade(RiskManagement):
1800
1779
  The function assumes that the returns are the excess of
1801
1780
  those compared to a benchmark.
1802
1781
  """
1803
- # Get total history
1804
1782
  import warnings
1805
1783
 
1806
1784
  warnings.filterwarnings("ignore")
@@ -1817,33 +1795,19 @@ class Trade(RiskManagement):
1817
1795
 
1818
1796
  def days_end(self) -> bool:
1819
1797
  """Check if it is the end of the trading day."""
1820
- current_hour = datetime.now().hour
1821
- current_minute = datetime.now().minute
1822
-
1823
- ending_hour = int(self.ending_time_hour)
1824
- ending_minute = int(self.ending_time_minutes)
1825
-
1826
- if current_hour > ending_hour or (
1827
- current_hour == ending_hour and current_minute >= ending_minute
1828
- ):
1798
+ now = datetime.now()
1799
+ end = datetime.strptime(self.end, "%H:%M").time()
1800
+ if now.time() >= end:
1829
1801
  return True
1830
- else:
1831
- return False
1802
+ return False
1832
1803
 
1833
1804
  def trading_time(self):
1834
1805
  """Check if it is time to trade."""
1835
- if (
1836
- int(self.start_time_hour)
1837
- < datetime.now().hour
1838
- < int(self.finishing_time_hour)
1839
- ):
1806
+ now = datetime.now()
1807
+ start = datetime.strptime(self.start, "%H:%M").time()
1808
+ end = datetime.strptime(self.finishing, "%H:%M").time()
1809
+ if start <= now.time() <= end:
1840
1810
  return True
1841
- elif datetime.now().hour == int(self.start_time_hour):
1842
- if datetime.now().minute >= int(self.start_time_minutes):
1843
- return True
1844
- elif datetime.now().hour == int(self.finishing_time_hour):
1845
- if datetime.now().minute < int(self.finishing_time_minutes):
1846
- return True
1847
1811
  return False
1848
1812
 
1849
1813
  def sleep_time(self, weekend=False):
@@ -1943,7 +1907,7 @@ def create_trade_instance(
1943
1907
  if ids is not None and isinstance(ids, (int, float))
1944
1908
  else params["expert_id"]
1945
1909
  if "expert_id" in params
1946
- else None
1910
+ else EXPERT_ID
1947
1911
  )
1948
1912
  params["pchange_sl"] = (
1949
1913
  pchange_sl[symbol]
@@ -286,6 +286,22 @@ class TickInfo(NamedTuple):
286
286
  volume_real: float
287
287
 
288
288
 
289
+ class BookInfo(NamedTuple):
290
+ """
291
+ Represents the structure of a book.
292
+ * type: Type of the order (buy/sell)
293
+ * price: Price of the order
294
+ * volume: Volume of the order in lots
295
+ * volume_dbl: Volume with greater accuracy
296
+
297
+ """
298
+
299
+ type: int
300
+ price: float
301
+ volume: float
302
+ volume_dbl: float
303
+
304
+
289
305
  class TradeRequest(NamedTuple):
290
306
  """
291
307
  Represents a Trade Request Structure
@@ -16,6 +16,101 @@ __all__ = [
16
16
  "search_coint_candidate_pairs",
17
17
  ]
18
18
 
19
+ def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
20
+ """Download and process data for a list of tickers from the specified source."""
21
+ data_list = []
22
+ for ticker in tickers:
23
+ try:
24
+ if source == "yf":
25
+ data = yf.download(
26
+ ticker,
27
+ start=start,
28
+ end=end,
29
+ progress=False,
30
+ multi_level_index=False,
31
+ )
32
+ data = data.drop(columns=["Adj Close"], axis=1)
33
+ elif source == "mt5":
34
+ start, end = pd.Timestamp(start), pd.Timestamp(end)
35
+ data = download_historical_data(
36
+ symbol=ticker,
37
+ timeframe=tf,
38
+ date_from=start,
39
+ date_to=end,
40
+ **{"path": path},
41
+ )
42
+ data = data.drop(columns=["adj_close"], axis=1)
43
+ elif source in ["fmp", "eodhd"]:
44
+ handler_class = (
45
+ FMPDataHandler if source == "fmp" else EODHDataHandler
46
+ )
47
+ handler = handler_class(events=None, symbol_list=[ticker], **kwargs)
48
+ data = handler.data[ticker]
49
+ else:
50
+ raise ValueError(f"Invalid source: {source}")
51
+
52
+ data = data.reset_index()
53
+ data = data.rename(columns=str.lower)
54
+ data["ticker"] = ticker
55
+ data_list.append(data)
56
+
57
+ except Exception as e:
58
+ print(f"No Data found for {ticker}: {e}")
59
+ continue
60
+
61
+ return pd.concat(data_list)
62
+
63
+ def _handle_date_range(start, end, window):
64
+ """Handle start and end date generation."""
65
+ if start is None or end is None:
66
+ end = pd.Timestamp(datetime.now()).strftime("%Y-%m-%d")
67
+ start = (
68
+ pd.Timestamp(datetime.now())
69
+ - pd.DateOffset(years=window)
70
+ + pd.DateOffset(days=1)
71
+ ).strftime("%Y-%m-%d")
72
+ return start, end
73
+
74
+ def _period_search(start, end, securities, candidates, window, npairs):
75
+ if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
76
+ raise ValueError(
77
+ "The date range must be at least two (2) years for period search."
78
+ )
79
+ top_pairs = []
80
+ p_start = pd.Timestamp(end) - pd.DateOffset(years=1)
81
+ periods = pd.date_range(start=p_start, end=pd.Timestamp(end), freq="BQE")
82
+ npairs = max(round(npairs / 2), 1)
83
+ for period in periods:
84
+ s_start = period - pd.DateOffset(years=2) + pd.DateOffset(days=1)
85
+ print(f"Searching for pairs in period: {s_start} - {period}")
86
+ pairs = find_cointegrated_pairs(
87
+ securities,
88
+ candidates,
89
+ n=npairs,
90
+ start=str(s_start),
91
+ stop=str(period),
92
+ coint=True,
93
+ )
94
+ pairs["period"] = period
95
+ top_pairs.append(pairs)
96
+ top_pairs = pd.concat(top_pairs)
97
+ if len(top_pairs.columns) <= 1:
98
+ raise ValueError(
99
+ "No pairs found in the specified period."
100
+ "Please adjust the date range or increase the number of pairs."
101
+ )
102
+ return top_pairs.head(npairs * 2)
103
+
104
+ def _process_asset_data(securities, candidates, universe, rolling_window):
105
+ """Process and select assets from the data."""
106
+ securities = select_assets(
107
+ securities, n=universe, rolling_window=rolling_window
108
+ )
109
+ candidates = select_assets(
110
+ candidates, n=universe, rolling_window=rolling_window
111
+ )
112
+ return securities, candidates
113
+
19
114
 
20
115
  def search_coint_candidate_pairs(
21
116
  securities: pd.DataFrame | List[str] = None,
@@ -145,101 +240,6 @@ def search_coint_candidate_pairs(
145
240
 
146
241
  """
147
242
 
148
- def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
149
- """Download and process data for a list of tickers from the specified source."""
150
- data_list = []
151
- for ticker in tickers:
152
- try:
153
- if source == "yf":
154
- data = yf.download(
155
- ticker,
156
- start=start,
157
- end=end,
158
- progress=False,
159
- multi_level_index=False,
160
- )
161
- data = data.drop(columns=["Adj Close"], axis=1)
162
- elif source == "mt5":
163
- start, end = pd.Timestamp(start), pd.Timestamp(end)
164
- data = download_historical_data(
165
- symbol=ticker,
166
- timeframe=tf,
167
- date_from=start,
168
- date_to=end,
169
- **{"path": path},
170
- )
171
- data = data.drop(columns=["adj_close"], axis=1)
172
- elif source in ["fmp", "eodhd"]:
173
- handler_class = (
174
- FMPDataHandler if source == "fmp" else EODHDataHandler
175
- )
176
- handler = handler_class(events=None, symbol_list=[ticker], **kwargs)
177
- data = handler.data[ticker]
178
- else:
179
- raise ValueError(f"Invalid source: {source}")
180
-
181
- data = data.reset_index()
182
- data = data.rename(columns=str.lower)
183
- data["ticker"] = ticker
184
- data_list.append(data)
185
-
186
- except Exception as e:
187
- print(f"No Data found for {ticker}: {e}")
188
- continue
189
-
190
- return pd.concat(data_list)
191
-
192
- def _handle_date_range(start, end, window):
193
- """Handle start and end date generation."""
194
- if start is None or end is None:
195
- end = pd.Timestamp(datetime.now()).strftime("%Y-%m-%d")
196
- start = (
197
- pd.Timestamp(datetime.now())
198
- - pd.DateOffset(years=window)
199
- + pd.DateOffset(days=1)
200
- ).strftime("%Y-%m-%d")
201
- return start, end
202
-
203
- def _period_search(start, end, securities, candidates, npairs=npairs):
204
- if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
205
- raise ValueError(
206
- "The date range must be at least two (2) years for period search."
207
- )
208
- top_pairs = []
209
- p_start = pd.Timestamp(end) - pd.DateOffset(years=1)
210
- periods = pd.date_range(start=p_start, end=pd.Timestamp(end), freq="BQE")
211
- npairs = max(round(npairs / 2), 1)
212
- for period in periods:
213
- s_start = period - pd.DateOffset(years=2) + pd.DateOffset(days=1)
214
- print(f"Searching for pairs in period: {s_start} - {period}")
215
- pairs = find_cointegrated_pairs(
216
- securities,
217
- candidates,
218
- n=npairs,
219
- start=str(s_start),
220
- stop=str(period),
221
- coint=True,
222
- )
223
- pairs["period"] = period
224
- top_pairs.append(pairs)
225
- top_pairs = pd.concat(top_pairs)
226
- if len(top_pairs.columns) <= 1:
227
- raise ValueError(
228
- "No pairs found in the specified period."
229
- "Please adjust the date range or increase the number of pairs."
230
- )
231
- return top_pairs.head(npairs * 2)
232
-
233
- def _process_asset_data(securities, candidates, universe, rolling_window):
234
- """Process and select assets from the data."""
235
- securities = select_assets(
236
- securities, n=universe, rolling_window=rolling_window
237
- )
238
- candidates = select_assets(
239
- candidates, n=universe, rolling_window=rolling_window
240
- )
241
- return securities, candidates
242
-
243
243
  if (
244
244
  securities is not None
245
245
  and candidates is not None
@@ -255,7 +255,7 @@ def search_coint_candidate_pairs(
255
255
  if period_search:
256
256
  start = securities.index.get_level_values("date").min()
257
257
  end = securities.index.get_level_values("date").max()
258
- top_pairs = _period_search(start, end, securities, candidates)
258
+ top_pairs = _period_search(start, end, securities, candidates, window, npairs)
259
259
  else:
260
260
  top_pairs = find_cointegrated_pairs(
261
261
  securities, candidates, n=npairs, coint=True
@@ -291,7 +291,7 @@ def search_coint_candidate_pairs(
291
291
  )
292
292
  if period_search:
293
293
  top_pairs = _period_search(
294
- start, end, securities_data, candidates_data
294
+ start, end, securities_data, candidates_data, window, npairs
295
295
  ).head(npairs)
296
296
  else:
297
297
  top_pairs = find_cointegrated_pairs(