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

bbstrader/__ini__.py CHANGED
@@ -5,9 +5,9 @@ Simplified Investment & Trading Toolkit
5
5
  """
6
6
  __author__ = "Bertin Balouki SIMYELI"
7
7
  __copyright__ = "2023-2024 Bertin Balouki SIMYELI"
8
- __email__ = "bbalouki@outlook.com"
8
+ __email__ = "bertin@bbstrader.com"
9
9
  __license__ = "MIT"
10
- __version__ = '0.1.91'
10
+ __version__ = '0.2.0'
11
11
 
12
12
 
13
13
  from bbstrader import btengine
@@ -153,12 +153,19 @@ class BaseCSVDataHandler(DataHandler):
153
153
  @property
154
154
  def symbols(self)-> List[str]:
155
155
  return self.symbol_list
156
+
156
157
  @property
157
158
  def data(self)-> Dict[str, pd.DataFrame]:
158
159
  return self.symbol_data
160
+
161
+ @property
162
+ def datadir(self)-> str:
163
+ return self.csv_dir
164
+
159
165
  @property
160
166
  def labels(self)-> List[str]:
161
167
  return self.columns
168
+
162
169
  @property
163
170
  def index(self)-> str | List[str]:
164
171
  return self._index
@@ -203,6 +210,7 @@ class BaseCSVDataHandler(DataHandler):
203
210
  'adj_close' if 'adj_close' in new_names else 'close'
204
211
  ].pct_change().dropna()
205
212
  self._index = self.symbol_data[s].index.name
213
+ self.symbol_data[s].to_csv(os.path.join(self.csv_dir, f'{s}.csv'))
206
214
  if self.events is not None:
207
215
  self.symbol_data[s] = self.symbol_data[s].iterrows()
208
216
 
@@ -474,7 +482,8 @@ class YFDataHandler(BaseCSVDataHandler):
474
482
  filepath = os.path.join(cache_dir, f"{symbol}.csv")
475
483
  try:
476
484
  data = yf.download(
477
- symbol, start=self.start_date, end=self.end_date, multi_level_index=False)
485
+ symbol, start=self.start_date, end=self.end_date,
486
+ multi_level_index=False, progress=False)
478
487
  if data.empty:
479
488
  raise ValueError(f"No data found for {symbol}")
480
489
  data.to_csv(filepath)
@@ -634,12 +643,13 @@ class FMPDataHandler(BaseCSVDataHandler):
634
643
  api_key=self.__api_key,
635
644
  start_date=self.start_date,
636
645
  end_date=self.end_date,
637
- benchmark_ticker=None
646
+ benchmark_ticker=None,
647
+ progress_bar=False
638
648
  )
639
649
  if period in ['daily', 'weekly', 'monthly', 'quarterly', 'yearly']:
640
- return toolkit.get_historical_data(period=period)
650
+ return toolkit.get_historical_data(period=period, progress_bar=False)
641
651
  elif period in ['1min', '5min', '15min', '30min', '1hour']:
642
- return toolkit.get_intraday_data(period=period)
652
+ return toolkit.get_intraday_data(period=period, progress_bar=False)
643
653
 
644
654
  def _format_data(self, data: pd.DataFrame, period: str) -> pd.DataFrame:
645
655
  if data.empty or len(data) == 0:
@@ -86,6 +86,7 @@ class MT5Strategy(Strategy):
86
86
  self.tf = kwargs.get("time_frame", 'D1')
87
87
  self.logger = kwargs.get("logger")
88
88
  self._initialize_portfolio()
89
+ self.kwargs = kwargs
89
90
 
90
91
  @property
91
92
  def cash(self) -> float:
@@ -201,19 +202,22 @@ class MT5Strategy(Strategy):
201
202
  - ``action``: The action to take for the symbol (LONG, SHORT, EXIT, etc.)
202
203
  - ``price``: The price at which to execute the action.
203
204
  - ``stoplimit``: The stop-limit price for STOP-LIMIT orders.
205
+ - ``id``: The unique identifier for the strategy or order.
204
206
 
205
- The dictionary can be use for pending orders (limit, stop, stop-limit) where the price is required.
207
+ The dictionary can be use for pending orders (limit, stop, stop-limit) where the price is required
208
+ or for executing orders where the each order has a unique identifier.
206
209
  """
207
210
  pass
208
211
 
209
- def apply_risk_management(self, optimer, freq=252) -> Dict[str, float] | None:
212
+ def apply_risk_management(self, optimer, symbols=None, freq=252) -> Dict[str, float] | None:
210
213
  """
211
214
  Apply risk management rules to the strategy.
212
215
  """
213
216
  if optimer is None:
214
217
  return None
218
+ symbols = symbols or self.symbols
215
219
  prices = self.get_asset_values(
216
- symbol_list=self.symbols, bars=self.data, mode=self.mode,
220
+ symbol_list=symbols, bars=self.data, mode=self.mode,
217
221
  window=freq, value_type='close', array=False, tf=self.tf
218
222
  )
219
223
  prices = pd.DataFrame(prices)
@@ -222,7 +226,7 @@ class MT5Strategy(Strategy):
222
226
  weights = optimized_weights(prices=prices, freq=freq, method=optimer)
223
227
  return {symbol: weight for symbol, weight in weights.items()}
224
228
  except Exception:
225
- return {symbol: 0.0 for symbol in self.symbols}
229
+ return {symbol: 0.0 for symbol in symbols}
226
230
 
227
231
  def get_quantity(self, symbol, weight, price=None, volume=None, maxqty=None) -> int:
228
232
  """
@@ -530,7 +534,7 @@ class MT5Strategy(Strategy):
530
534
  asset_values[asset] = getattr(values, value_type)
531
535
  elif mode == 'live':
532
536
  for asset in symbol_list:
533
- rates = Rates(symbol=asset, time_frame=tf, count=window + 1)
537
+ rates = Rates(asset, timeframe=tf, count=window + 1, **self.kwargs)
534
538
  if array:
535
539
  values = getattr(rates, value_type).values
536
540
  asset_values[asset] = values[~np.isnan(values)]
@@ -575,11 +579,11 @@ class MT5Strategy(Strategy):
575
579
  Returns:
576
580
  bool : True if there are open positions, False otherwise
577
581
  """
578
- account = account or Account()
582
+ account = account or Account(**self.kwargs)
579
583
  positions = account.get_positions(symbol=symbol)
580
584
  if positions is not None:
581
585
  open_positions = [
582
- pos for pos in positions if pos.type == position
586
+ pos.ticket for pos in positions if pos.type == position
583
587
  and pos.magic == strategy_id
584
588
  ]
585
589
  if one_true:
@@ -600,7 +604,7 @@ class MT5Strategy(Strategy):
600
604
  Returns:
601
605
  prices : numpy array of buy or sell prices for open positions if any or an empty array.
602
606
  """
603
- account = account or Account()
607
+ account = account or Account(**self.kwargs)
604
608
  positions = account.get_positions(symbol=symbol)
605
609
  if positions is not None:
606
610
  prices = np.array([
bbstrader/config.py CHANGED
@@ -3,6 +3,10 @@ from typing import List
3
3
  from pathlib import Path
4
4
  from datetime import datetime
5
5
 
6
+
7
+ ADMIRAL_PATH = "C:\\Program Files\\Admirals Group MT5 Terminal\\terminal64.exe"
8
+ FTMO_PATH = "C:\\Program Files\\FTMO MetaTrader 5\\terminal64.exe"
9
+
6
10
  def get_config_dir(name: str=".bbstrader") -> Path:
7
11
  """
8
12
  Get the path to the configuration directory.
File without changes
bbstrader/core/data.py ADDED
@@ -0,0 +1,23 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from financetoolkit import Toolkit
4
+
5
+
6
+ __all__ = [
7
+ 'FMP',
8
+ ]
9
+
10
+ class FMP(Toolkit):
11
+ """
12
+ FMPData class for fetching data from Financial Modeling Prep API
13
+ using the Toolkit class from financetoolkit package.
14
+
15
+ See `financetoolkit` for more details.
16
+
17
+ """
18
+ def __init__(self, api_key: str ='', symbols: str | list = 'AAPL'):
19
+ super().__init__(tickers=symbols, api_key=api_key)
20
+
21
+
22
+ class DataBendo:
23
+ ...
File without changes
File without changes
@@ -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
@@ -121,7 +123,6 @@ class Rates(object):
121
123
  For `session_duration` check your broker symbols details
122
124
  """
123
125
  self.symbol = symbol
124
- tf = kwargs.get('time_frame')
125
126
  self.time_frame = self._validate_time_frame(timeframe)
126
127
  self.sd = session_duration
127
128
  self.start_pos = self._get_start_pos(start_pos, timeframe)
@@ -223,7 +224,6 @@ class Rates(object):
223
224
  df.columns = ['Date', 'Open', 'High', 'Low', 'Close', 'Volume']
224
225
  df['Adj Close'] = df['Close']
225
226
  df = df[['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume']]
226
- #df = df.columns.rename(str.lower).str.replace(' ', '_')
227
227
  df['Date'] = pd.to_datetime(df['Date'], unit='s', utc=utc)
228
228
  df.set_index('Date', inplace=True)
229
229
  if lower_colnames:
@@ -874,8 +874,9 @@ class Trade(RiskManagement):
874
874
  """
875
875
  This function checks if it's time to set the break-even level for a trading position.
876
876
  If it is, it sets the break-even level. If the break-even level has already been set,
877
- it checks if the price has moved in a favorable direction. If it has, and the trail parameter is set to True,
878
- 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.
879
880
 
880
881
  Args:
881
882
  id (int): The strategy ID or expert ID.
@@ -883,8 +884,10 @@ class Trade(RiskManagement):
883
884
  trail (bool): Whether to trail the stop loss or not.
884
885
  stop_trail (int): Number of points to trail the stop loss by.
885
886
  It represent the distance from the current price to the stop loss.
886
- trail_after_points (int): Number of points in profit from where the strategy will start to trail the stop loss.
887
- 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.
888
891
  """
889
892
  time.sleep(0.1)
890
893
  if not mm:
@@ -1515,6 +1518,7 @@ def create_trade_instance(
1515
1518
  based on the importance of the symbol in the portfolio or strategy.
1516
1519
  """
1517
1520
  logger = params.get('logger', None)
1521
+ ids = params.get('expert_id', None)
1518
1522
  trade_instances = {}
1519
1523
  if not symbols:
1520
1524
  raise ValueError("The 'symbols' list cannot be empty.")
@@ -1534,10 +1538,19 @@ def create_trade_instance(
1534
1538
  for symbol in symbols:
1535
1539
  if symbol not in pchange_sl:
1536
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}'.")
1537
1545
 
1538
1546
  for symbol in symbols:
1539
1547
  try:
1540
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
+ )
1541
1554
  params['pchange_sl'] = (
1542
1555
  pchange_sl[symbol] if pchange_sl is not None
1543
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)