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

@@ -20,7 +20,9 @@ from exchange_calendars import(
20
20
  __all__ = [
21
21
  'Rates',
22
22
  'download_historical_data',
23
- 'get_data_from_pos'
23
+ 'get_data_from_pos',
24
+ 'get_data_from_date'
25
+
24
26
  ]
25
27
 
26
28
  MAX_BARS = 10_000_000
@@ -82,6 +84,8 @@ class Rates(object):
82
84
  2. The `open, high, low, close, adjclose, returns,
83
85
  volume` properties returns data in Broker's timezone by default.
84
86
 
87
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
88
+
85
89
  Example:
86
90
  >>> rates = Rates("EURUSD", "1h")
87
91
  >>> df = rates.get_historical_data(
@@ -94,17 +98,18 @@ class Rates(object):
94
98
  def __init__(
95
99
  self,
96
100
  symbol: str,
97
- time_frame: TimeFrame = 'D1',
101
+ timeframe: TimeFrame = 'D1',
98
102
  start_pos: Union[int , str] = 0,
99
103
  count: Optional[int] = MAX_BARS,
100
- session_duration: Optional[float] = None
104
+ session_duration: Optional[float] = None,
105
+ **kwargs
101
106
  ):
102
107
  """
103
108
  Initializes a new Rates instance.
104
109
 
105
110
  Args:
106
111
  symbol (str): Financial instrument symbol (e.g., "EURUSD").
107
- time_frame (str): Timeframe string (e.g., "D1", "1h", "5m").
112
+ timeframe (str): Timeframe string (e.g., "D1", "1h", "5m").
108
113
  start_pos (int, | str): Starting index (int) or date (str) for data retrieval.
109
114
  count (int, optional): Number of bars to retrieve default is
110
115
  the maximum bars availble in the MT5 terminal.
@@ -118,16 +123,16 @@ class Rates(object):
118
123
  For `session_duration` check your broker symbols details
119
124
  """
120
125
  self.symbol = symbol
121
- self.time_frame = self._validate_time_frame(time_frame)
126
+ self.time_frame = self._validate_time_frame(timeframe)
122
127
  self.sd = session_duration
123
- self.start_pos = self._get_start_pos(start_pos, time_frame)
128
+ self.start_pos = self._get_start_pos(start_pos, timeframe)
124
129
  self.count = count
125
- self._mt5_initialized()
126
- self.__account = Account()
130
+ self._mt5_initialized(**kwargs)
131
+ self.__account = Account(**kwargs)
127
132
  self.__data = self.get_rates_from_pos()
128
133
 
129
- def _mt5_initialized(self):
130
- check_mt5_connection()
134
+ def _mt5_initialized(self, **kwargs):
135
+ check_mt5_connection(**kwargs)
131
136
 
132
137
  def _get_start_pos(self, index, time_frame):
133
138
  if isinstance(index, int):
@@ -219,7 +224,6 @@ class Rates(object):
219
224
  df.columns = ['Date', 'Open', 'High', 'Low', 'Close', 'Volume']
220
225
  df['Adj Close'] = df['Close']
221
226
  df = df[['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume']]
222
- #df = df.columns.rename(str.lower).str.replace(' ', '_')
223
227
  df['Date'] = pd.to_datetime(df['Date'], unit='s', utc=utc)
224
228
  df.set_index('Date', inplace=True)
225
229
  if lower_colnames:
@@ -471,13 +475,13 @@ class Rates(object):
471
475
  df.to_csv(f"{self.symbol}.csv")
472
476
  return df
473
477
 
474
- def download_historical_data(symbol, time_frame, date_from,
478
+ def download_historical_data(symbol, timeframe, date_from,
475
479
  date_to=pd.Timestamp.now(),lower_colnames=True,
476
- utc=False, filter=False, fill_na=False, save_csv=False):
480
+ utc=False, filter=False, fill_na=False, save_csv=False, **kwargs):
477
481
  """Download historical data from MetaTrader 5 terminal.
478
482
  See `Rates.get_historical_data` for more details.
479
483
  """
480
- rates = Rates(symbol, time_frame)
484
+ rates = Rates(symbol, timeframe, **kwargs)
481
485
  data = rates.get_historical_data(
482
486
  date_from=date_from,
483
487
  date_to=date_to,
@@ -488,23 +492,23 @@ def download_historical_data(symbol, time_frame, date_from,
488
492
  )
489
493
  return data
490
494
 
491
- def get_data_from_pos(symbol, time_frame, start_pos=0, fill_na=False,
495
+ def get_data_from_pos(symbol, timeframe, start_pos=0, fill_na=False,
492
496
  count=MAX_BARS, lower_colnames=False, utc=False, filter=False,
493
- session_duration=23.0):
497
+ session_duration=23.0, **kwargs):
494
498
  """Get historical data from a specific position.
495
499
  See `Rates.get_rates_from_pos` for more details.
496
500
  """
497
- rates = Rates(symbol, time_frame, start_pos, count, session_duration)
501
+ rates = Rates(symbol, timeframe, start_pos, count, session_duration, **kwargs)
498
502
  data = rates.get_rates_from_pos(filter=filter, fill_na=fill_na,
499
503
  lower_colnames=lower_colnames, utc=utc)
500
504
  return data
501
505
 
502
- def get_data_from_date(symbol, time_frame, date_from, count=MAX_BARS, fill_na=False,
503
- lower_colnames=False, utc=False, filter=False):
506
+ def get_data_from_date(symbol, timeframe, date_from, count=MAX_BARS, fill_na=False,
507
+ lower_colnames=False, utc=False, filter=False, **kwargs):
504
508
  """Get historical data from a specific date.
505
509
  See `Rates.get_rates_from` for more details.
506
510
  """
507
- rates = Rates(symbol, time_frame)
511
+ rates = Rates(symbol, timeframe, **kwargs)
508
512
  data = rates.get_rates_from(date_from, count, filter=filter, fill_na=fill_na,
509
513
  lower_colnames=lower_colnames, utc=utc)
510
514
  return data
@@ -118,8 +118,10 @@ class RiskManagement(Account):
118
118
  tp (int, optional): Take Profit in points, Must be a positive number.
119
119
  be (int, optional): Break Even in points, Must be a positive number.
120
120
  rr (float, optional): Risk reward ratio, Must be a positive number. Defaults to 1.5.
121
+
122
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
121
123
  """
122
- super().__init__()
124
+ super().__init__(**kwargs)
123
125
 
124
126
  # Validation
125
127
  if daily_risk is not None and daily_risk < 0:
@@ -137,6 +139,7 @@ class RiskManagement(Account):
137
139
  if var_time_frame not in TIMEFRAMES:
138
140
  raise ValueError("Unsupported time frame {}".format(var_time_frame))
139
141
 
142
+ self.kwargs = kwargs
140
143
  self.symbol = symbol
141
144
  self.start_time = start_time
142
145
  self.finishing_time = finishing_time
@@ -279,7 +282,7 @@ class RiskManagement(Account):
279
282
  tf_int = self._convert_time_frame(self._tf)
280
283
  interval = round((minutes / tf_int) * 252)
281
284
 
282
- rate = Rates(self.symbol, self._tf, 0, interval)
285
+ rate = Rates(self.symbol, self._tf, 0, interval, **self.kwargs)
283
286
  returns = rate.returns*100
284
287
  std = returns.std()
285
288
  point = self.get_symbol_info(self.symbol).point
@@ -333,7 +336,7 @@ class RiskManagement(Account):
333
336
  tf_int = self._convert_time_frame(tf)
334
337
  interval = round((minutes / tf_int) * 252)
335
338
 
336
- rate = Rates(self.symbol, tf, 0, interval)
339
+ rate = Rates(self.symbol, tf, 0, interval, **self.kwargs)
337
340
  returns = rate.returns*100
338
341
  p = self.get_account_info().margin_free
339
342
  mu = returns.mean()
@@ -138,6 +138,7 @@ class Trade(RiskManagement):
138
138
  - tp
139
139
  - be
140
140
  See the RiskManagement class for more details on these parameters.
141
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
141
142
  """
142
143
  # Call the parent class constructor first
143
144
  super().__init__(
@@ -160,6 +161,7 @@ class Trade(RiskManagement):
160
161
  self.console_log = console_log
161
162
  self.logger = self._get_logger(logger, console_log)
162
163
  self.tf = kwargs.get("time_frame", 'D1')
164
+ self.kwargs = kwargs
163
165
 
164
166
  self.start_time_hour, self.start_time_minutes = self.start.split(":")
165
167
  self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
@@ -174,8 +176,8 @@ class Trade(RiskManagement):
174
176
  self.break_even_points = {}
175
177
  self.trail_after_points = []
176
178
 
177
- self.initialize()
178
- self.select_symbol()
179
+ self.initialize(**kwargs)
180
+ self.select_symbol(**kwargs)
179
181
  self.prepare_symbol()
180
182
 
181
183
  if self.verbose:
@@ -194,7 +196,7 @@ class Trade(RiskManagement):
194
196
  return config_logger(f'{log_path}/{logger}', consol_log)
195
197
  return logger
196
198
 
197
- def initialize(self):
199
+ def initialize(self, **kwargs):
198
200
  """
199
201
  Initializes the MetaTrader 5 (MT5) terminal for trading operations.
200
202
  This method attempts to establish a connection with the MT5 terminal.
@@ -207,7 +209,7 @@ class Trade(RiskManagement):
207
209
  try:
208
210
  if self.verbose:
209
211
  print("\nInitializing the basics.")
210
- check_mt5_connection()
212
+ check_mt5_connection(**kwargs)
211
213
  if self.verbose:
212
214
  print(
213
215
  f"You are running the @{self.expert_name} Expert advisor,"
@@ -216,7 +218,7 @@ class Trade(RiskManagement):
216
218
  except Exception as e:
217
219
  self.logger.error(f"During initialization: {e}")
218
220
 
219
- def select_symbol(self):
221
+ def select_symbol(self, **kwargs):
220
222
  """
221
223
  Selects the trading symbol in the MetaTrader 5 (MT5) terminal.
222
224
  This method ensures that the specified trading
@@ -228,6 +230,7 @@ class Trade(RiskManagement):
228
230
  MT5TerminalError: If symbole selection fails.
229
231
  """
230
232
  try:
233
+ check_mt5_connection(**kwargs)
231
234
  if not Mt5.symbol_select(self.symbol, True):
232
235
  raise_mt5_error(message=INIT_MSG)
233
236
  except Exception as e:
@@ -848,10 +851,11 @@ class Trade(RiskManagement):
848
851
  positions = self.get_positions(symbol=self.symbol)
849
852
  if positions is not None:
850
853
  positions = [position for position in positions if position.magic == id]
851
- profit = 0.0
852
- balance = self.get_account_info().balance
853
- target = round((balance * self.target)/100, 2)
854
- if positions is not None or len(positions) != 0:
854
+
855
+ if positions is not None:
856
+ profit = 0.0
857
+ balance = self.get_account_info().balance
858
+ target = round((balance * self.target)/100, 2)
855
859
  for position in positions:
856
860
  profit += position.profit
857
861
  fees = self.get_stats()[0]["average_fee"] * len(positions)
@@ -870,8 +874,9 @@ class Trade(RiskManagement):
870
874
  """
871
875
  This function checks if it's time to set the break-even level for a trading position.
872
876
  If it is, it sets the break-even level. If the break-even level has already been set,
873
- it checks if the price has moved in a favorable direction. If it has, and the trail parameter is set to True,
874
- it updates the break-even level based on the trail_after_points and stop_trail parameters.
877
+ it checks if the price has moved in a favorable direction.
878
+ If it has, and the trail parameter is set to True, it updates
879
+ the break-even level based on the trail_after_points and stop_trail parameters.
875
880
 
876
881
  Args:
877
882
  id (int): The strategy ID or expert ID.
@@ -879,8 +884,10 @@ class Trade(RiskManagement):
879
884
  trail (bool): Whether to trail the stop loss or not.
880
885
  stop_trail (int): Number of points to trail the stop loss by.
881
886
  It represent the distance from the current price to the stop loss.
882
- trail_after_points (int): Number of points in profit from where the strategy will start to trail the stop loss.
883
- be_plus_points (int): Number of points to add to the break-even level. Represents the minimum profit to secure.
887
+ trail_after_points (int): Number of points in profit
888
+ from where the strategy will start to trail the stop loss.
889
+ be_plus_points (int): Number of points to add to the break-even level.
890
+ Represents the minimum profit to secure.
884
891
  """
885
892
  time.sleep(0.1)
886
893
  if not mm:
@@ -1489,6 +1496,7 @@ def create_trade_instance(
1489
1496
  daily_risk: Optional[Dict[str, float]] = None,
1490
1497
  max_risk: Optional[Dict[str, float]] = None,
1491
1498
  pchange_sl: Optional[Dict[str, float] | float] = None,
1499
+ **kwargs
1492
1500
  ) -> Dict[str, Trade]:
1493
1501
  """
1494
1502
  Creates Trade instances for each symbol provided.
@@ -1510,6 +1518,7 @@ def create_trade_instance(
1510
1518
  based on the importance of the symbol in the portfolio or strategy.
1511
1519
  """
1512
1520
  logger = params.get('logger', None)
1521
+ ids = params.get('expert_id', None)
1513
1522
  trade_instances = {}
1514
1523
  if not symbols:
1515
1524
  raise ValueError("The 'symbols' list cannot be empty.")
@@ -1529,10 +1538,19 @@ def create_trade_instance(
1529
1538
  for symbol in symbols:
1530
1539
  if symbol not in pchange_sl:
1531
1540
  raise ValueError(f"Missing percentage change for symbol '{symbol}'.")
1541
+ if isinstance(ids, dict):
1542
+ for symbol in symbols:
1543
+ if symbol not in ids:
1544
+ raise ValueError(f"Missing expert ID for symbol '{symbol}'.")
1532
1545
 
1533
1546
  for symbol in symbols:
1534
1547
  try:
1535
1548
  params['symbol'] = symbol
1549
+ params['expert_id'] = (
1550
+ ids[symbol] if ids is not None and isinstance(ids, dict) else
1551
+ ids if ids is not None and isinstance(ids, (int, float)) else
1552
+ params['expert_id'] if 'expert_id' in params else None
1553
+ )
1536
1554
  params['pchange_sl'] = (
1537
1555
  pchange_sl[symbol] if pchange_sl is not None
1538
1556
  and isinstance(pchange_sl, dict) else
@@ -5,6 +5,6 @@ It is designed to be a versatile base module for different types of models used
5
5
  """
6
6
  from bbstrader.models.risk import *
7
7
  from bbstrader.models.optimization import *
8
- from bbstrader.models.portfolios import *
8
+ from bbstrader.models.portfolio import *
9
9
  from bbstrader.models.factors import *
10
10
  from bbstrader.models.ml import *
@@ -0,0 +1,275 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import yfinance as yf
4
+ from datetime import datetime
5
+ from bbstrader.metatrader.rates import download_historical_data
6
+ from bbstrader.btengine.data import FMPDataHandler, EODHDataHandler
7
+ from typing import List, Dict, Literal, Union
8
+ from bbstrader.tseries import (
9
+ find_cointegrated_pairs,
10
+ select_candidate_pairs,
11
+ select_assets,
12
+ )
13
+
14
+ __all__ = [
15
+ "search_coint_candidate_pairs",
16
+ ]
17
+
18
+ def search_coint_candidate_pairs(
19
+ securities: pd.DataFrame | List[str] = None,
20
+ candidates: pd.DataFrame | List[str] = None,
21
+ start: str = None,
22
+ end: str = None,
23
+ period_search: bool = False,
24
+ select: bool = True,
25
+ source: str = None,
26
+ universe: int = 100,
27
+ window: int = 2,
28
+ rolling_window: int = None,
29
+ npairs: int = 10,
30
+ tf: str = 'D1',
31
+ path: str = None,
32
+ **kwargs
33
+ ) -> List[Dict[str, str]] | pd.DataFrame:
34
+ """
35
+ Searches for candidate pairs of securities based on cointegration analysis.
36
+
37
+ This function either processes preloaded securities and candidates data
38
+ (as pandas DataFrames) or downloads historical data from a specified
39
+ source (e.g., Yahoo Finance, MetaTrader 5, Financial Modeling Prep, or EODHD).
40
+ It then selects the top `npairs` based on cointegration.
41
+
42
+ Args:
43
+ securities (pd.DataFrame | List[str], optional):
44
+ A DataFrame or list of tickers representing the securities for analysis.
45
+ If using a DataFrame, it should include a MultiIndex with levels
46
+ ['ticker', 'date'].
47
+ candidates (pd.DataFrame | List[str], optional):
48
+ A DataFrame or list of tickers representing the candidate securities
49
+ for pair selection.
50
+ start (str, optional):
51
+ The start date for data retrieval in 'YYYY-MM-DD' format. Ignored
52
+ if both `securities` and `candidates` are DataFrames.
53
+ end (str, optional):
54
+ The end date for data retrieval in 'YYYY-MM-DD' format. Ignored
55
+ if both `securities` and `candidates` are DataFrames.
56
+ period_search (bool, optional):
57
+ If True, the function will perform a periodic search for cointegrated from 3 years
58
+ to the end date by taking 2 yerars rolling window. So you need to have at least 3 years of data
59
+ or set the `window` parameter to 3. Defaults to False.
60
+ select (bool, optional):
61
+ If True, the function will select the top cointegrated pairs based on the
62
+ cointegration test results in form of List[dict].
63
+ If False, the function will return all cointegrated pairs in form of DataFrame.
64
+ This can be useful for further analysis or visualization.
65
+ source (str, optional):
66
+ The data source for historical data retrieval. Must be one of
67
+ ['yf', 'mt5', 'fmp', 'eodhd']. Required if `securities` and
68
+ `candidates` are lists of tickers.
69
+ universe (int, optional):
70
+ The maximum number of assets to retain for analysis. Defaults to 100.
71
+ window (int, optional):
72
+ The number of years of historical data to retrieve if `start` and `end`
73
+ are not specified. Defaults to 2 years.
74
+ rolling_window (int, optional):
75
+ The size of the rolling window (in days) used for asset selection.
76
+ Defaults to None.
77
+ npairs (int, optional):
78
+ The number of top cointegrated pairs to select. Defaults to 10.
79
+ tf (str, optional):
80
+ The timeframe for MetaTrader 5 data retrieval. Defaults to 'D1'.
81
+ path (str, optional):
82
+ The path to MetaTrader 5 historical data files. Required if `source='mt5'`.
83
+ **kwargs:
84
+ Additional parameters for data retrieval (e.g., API keys, date ranges
85
+ for specific sources), see ``bbstrader.btengine.data.FMPDataHandler`` or
86
+ ``bbstrader.btengine.data.EODHDataHandler`` for more details.
87
+
88
+ Returns:
89
+ List[dict]: A list containing the selected top cointegrated pairs if `select=True`.
90
+ pd.DataFrame: A DataFrame containing all cointegrated pairs if `select=False`.
91
+
92
+ Raises:
93
+ ValueError: If the inputs are invalid or if the `source` is not one of
94
+ the supported sources.
95
+
96
+ Examples:
97
+ Using preloaded DataFrames:
98
+ >>> securities = pd.read_csv('securities.csv', index_col=['ticker', 'date'])
99
+ >>> candidates = pd.read_csv('candidates.csv', index_col=['ticker', 'date'])
100
+ >>> pairs = search_candidate_pairs(securities=securities, candidates=candidates)
101
+
102
+ Using a data source (Yahoo Finance):
103
+ >>> securities = ['SPY', 'IWM', 'XLF', 'HYG', 'XLE', 'LQD', 'GDX', 'FXI', 'EWZ', ...]
104
+ >>> candidates = ['AAPL', 'AMZN', 'NVDA', 'MSFT', 'GOOGL', 'AMD', 'BAC', 'NFLX', ...]
105
+
106
+ >>> pairs = search_candidate_pairs(
107
+ ... securities=securities,
108
+ ... candidates=candidates,
109
+ ... start='2022-12-12',
110
+ ... end='2024-12-10',
111
+ ... source='yf',
112
+ ... npairs=10
113
+ ... )
114
+ >>> [
115
+ ... {'x': 'LQD', 'y': 'TMO'},
116
+ ... {'x': 'IEF', 'y': 'COP'},
117
+ ... {'x': 'WMT', 'y': 'IWM'},
118
+ ... {'x': 'MDT', 'y': 'OIH'},
119
+ ... {'x': 'EWZ', 'y': 'CMCSA'},
120
+ ... {'x': 'VLO', 'y': 'XOP'},
121
+ ... {'x': 'SHY', 'y': 'F'},
122
+ ... {'x': 'ABT', 'y': 'LQD'},
123
+ ... {'x': 'PFE', 'y': 'USO'},
124
+ ... {'x': 'LQD', 'y': 'MDT'}
125
+ ... ]
126
+
127
+ Using MetaTrader 5:
128
+ >>> securities = ['EURUSD', 'GBPUSD']
129
+ >>> candidates = ['USDJPY', 'AUDUSD']
130
+ >>> pairs = search_candidate_pairs(
131
+ ... securities=securities,
132
+ ... candidates=candidates,
133
+ ... source='mt5',
134
+ ... tf='H1',
135
+ ... path='/path/to/terminal64.exe',
136
+ ... )
137
+
138
+ Notes:
139
+ - If `securities` and `candidates` are DataFrames, the function assumes
140
+ the data is already preprocessed and indexed by ['ticker', 'date'].
141
+ - When using `source='fmp'` or `source='eodhd'`, API keys and other
142
+ required parameters should be passed via `kwargs`.
143
+
144
+ """
145
+
146
+ def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
147
+ """Download and process data for a list of tickers from the specified source."""
148
+ data_list = []
149
+ for ticker in tickers:
150
+ try:
151
+ if source == 'yf':
152
+ data = yf.download(
153
+ ticker, start=start, end=end, progress=False, multi_level_index=False)
154
+ data = data.drop(columns=['Adj Close'], axis=1)
155
+ elif source == 'mt5':
156
+ start, end = pd.Timestamp(start), pd.Timestamp(end)
157
+ data = download_historical_data(
158
+ symbol=ticker, timeframe=tf, date_from=start, date_to=end, **{'path': path})
159
+ data = data.drop(columns=['adj_close'], axis=1)
160
+ elif source in ['fmp', 'eodhd']:
161
+ handler_class = FMPDataHandler if source == 'fmp' else EODHDataHandler
162
+ handler = handler_class(
163
+ events=None, symbol_list=[ticker], **kwargs)
164
+ data = handler.data[ticker]
165
+ else:
166
+ raise ValueError(f"Invalid source: {source}")
167
+
168
+ data = data.reset_index()
169
+ data = data.rename(columns=str.lower)
170
+ data['ticker'] = ticker
171
+ data_list.append(data)
172
+
173
+ except Exception as e:
174
+ print(f"No Data found for {ticker}: {e}")
175
+ continue
176
+
177
+ return pd.concat(data_list)
178
+
179
+ def _handle_date_range(start, end, window):
180
+ """Handle start and end date generation."""
181
+ if start is None or end is None:
182
+ end = pd.Timestamp(datetime.now()).strftime('%Y-%m-%d')
183
+ start = (
184
+ pd.Timestamp(datetime.now()) -
185
+ pd.DateOffset(years=window) + pd.DateOffset(days=1)
186
+ ).strftime('%Y-%m-%d')
187
+ return start, end
188
+
189
+ def _period_search(start, end, securities, candidates, npairs=npairs):
190
+ if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
191
+ raise ValueError(
192
+ "The date range must be at least two (2) years for period search."
193
+ )
194
+ top_pairs = []
195
+ p_start = pd.Timestamp(end) - pd.DateOffset(years=1)
196
+ periods = pd.date_range(start=p_start, end=pd.Timestamp(end), freq='BQE')
197
+ npairs = max(round(npairs/2), 1)
198
+ for period in periods:
199
+ s_start = period - pd.DateOffset(years=2) + pd.DateOffset(days=1)
200
+ print(f"Searching for pairs in period: {s_start} - {period}")
201
+ pairs = find_cointegrated_pairs(
202
+ securities, candidates, n=npairs, start=str(s_start), stop=str(period), coint=True)
203
+ pairs['period'] = period
204
+ top_pairs.append(pairs)
205
+ top_pairs = pd.concat(top_pairs)
206
+ if len(top_pairs.columns) <= 1:
207
+ raise ValueError(
208
+ "No pairs found in the specified period."
209
+ "Please adjust the date range or increase the number of pairs."
210
+ )
211
+ return top_pairs.head(npairs*2)
212
+
213
+ def _process_asset_data(securities, candidates, universe, rolling_window):
214
+ """Process and select assets from the data."""
215
+ securities = select_assets(
216
+ securities, n=universe, rolling_window=rolling_window)
217
+ candidates = select_assets(
218
+ candidates, n=universe, rolling_window=rolling_window)
219
+ return securities, candidates
220
+
221
+ if (securities is not None and candidates is not None
222
+ and isinstance(securities, pd.DataFrame)
223
+ and isinstance(candidates, pd.DataFrame)
224
+ ):
225
+ if isinstance(securities.index, pd.MultiIndex) and isinstance(candidates.index, pd.MultiIndex):
226
+ securities, candidates = _process_asset_data(
227
+ securities, candidates, universe, rolling_window)
228
+ if period_search:
229
+ start = securities.index.get_level_values('date').min()
230
+ end = securities.index.get_level_values('date').max()
231
+ top_pairs = _period_search(start, end, securities, candidates)
232
+ else:
233
+ top_pairs = find_cointegrated_pairs(
234
+ securities, candidates, n=npairs, coint=True)
235
+ if select:
236
+ return select_candidate_pairs(top_pairs, period=True if period_search else False)
237
+ else:
238
+ return top_pairs
239
+
240
+ elif source is not None:
241
+ if source not in ['yf', 'mt5', 'fmp', 'eodhd']:
242
+ raise ValueError(
243
+ "source must be either 'yf', 'mt5', 'fmp', or 'eodhd'")
244
+ if not isinstance(securities, list) or not isinstance(candidates, list):
245
+ raise ValueError(
246
+ "securities and candidates must be a list of tickers")
247
+
248
+ start, end = _handle_date_range(start, end, window)
249
+ if source in ['fmp', 'eodhd']:
250
+ kwargs[f'{source}_start'] = kwargs.get(f'{source}_start') or start
251
+ kwargs[f'{source}_end'] = kwargs.get(f'{source}_end') or end
252
+
253
+ securities_data = _download_and_process_data(
254
+ source, securities, start, end, tf, path, **kwargs)
255
+ candidates_data = _download_and_process_data(
256
+ source, candidates, start, end, tf, path, **kwargs)
257
+ securities_data = securities_data.set_index(['ticker', 'date'])
258
+ candidates_data = candidates_data.set_index(['ticker', 'date'])
259
+ securities_data, candidates_data = _process_asset_data(
260
+ securities_data, candidates_data, universe, rolling_window)
261
+ if period_search:
262
+ top_pairs = _period_search(start, end, securities_data, candidates_data).head(npairs)
263
+ else:
264
+ top_pairs = find_cointegrated_pairs(securities_data, candidates_data, n=npairs, coint=True)
265
+ if select:
266
+ return select_candidate_pairs(top_pairs, period=True if period_search else False)
267
+ else:
268
+ return top_pairs
269
+
270
+ else:
271
+ msg = (
272
+ "Invalid input. Either provide securities"
273
+ "and candidates as DataFrames or specify a data source."
274
+ )
275
+ raise ValueError(msg)