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

Files changed (38) hide show
  1. bbstrader/__ini__.py +9 -9
  2. bbstrader/btengine/__init__.py +7 -7
  3. bbstrader/btengine/backtest.py +30 -26
  4. bbstrader/btengine/data.py +100 -79
  5. bbstrader/btengine/event.py +2 -1
  6. bbstrader/btengine/execution.py +18 -16
  7. bbstrader/btengine/performance.py +11 -7
  8. bbstrader/btengine/portfolio.py +35 -36
  9. bbstrader/btengine/strategy.py +119 -94
  10. bbstrader/config.py +14 -8
  11. bbstrader/core/__init__.py +0 -0
  12. bbstrader/core/data.py +22 -0
  13. bbstrader/core/utils.py +57 -0
  14. bbstrader/ibkr/__init__.py +0 -0
  15. bbstrader/ibkr/utils.py +0 -0
  16. bbstrader/metatrader/__init__.py +5 -5
  17. bbstrader/metatrader/account.py +117 -121
  18. bbstrader/metatrader/rates.py +83 -80
  19. bbstrader/metatrader/risk.py +23 -37
  20. bbstrader/metatrader/trade.py +169 -140
  21. bbstrader/metatrader/utils.py +3 -3
  22. bbstrader/models/__init__.py +5 -5
  23. bbstrader/models/factors.py +280 -0
  24. bbstrader/models/ml.py +1092 -0
  25. bbstrader/models/optimization.py +31 -28
  26. bbstrader/models/{portfolios.py → portfolio.py} +64 -46
  27. bbstrader/models/risk.py +15 -9
  28. bbstrader/trading/__init__.py +2 -2
  29. bbstrader/trading/execution.py +252 -164
  30. bbstrader/trading/scripts.py +8 -4
  31. bbstrader/trading/strategies.py +79 -66
  32. bbstrader/tseries.py +482 -107
  33. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
  34. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/METADATA +6 -1
  35. bbstrader-0.2.1.dist-info/RECORD +37 -0
  36. bbstrader-0.1.94.dist-info/RECORD +0 -32
  37. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
  38. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,8 @@
1
1
  from datetime import datetime
2
- import MetaTrader5 as MT5
3
- import logging
4
- from typing import List, NamedTuple, Optional
5
2
  from enum import Enum
3
+ from typing import NamedTuple, Optional
6
4
 
5
+ import MetaTrader5 as MT5
7
6
 
8
7
  __all__ = [
9
8
  "TIMEFRAMES",
@@ -421,6 +420,7 @@ class TradeDeal(NamedTuple):
421
420
 
422
421
  class InvalidBroker(Exception):
423
422
  """Exception raised for invalid broker errors."""
423
+
424
424
  def __init__(self, message="Invalid broker."):
425
425
  super().__init__(message)
426
426
 
@@ -3,8 +3,8 @@ The `models` module provides a foundational framework for implementing various q
3
3
 
4
4
  It is designed to be a versatile base module for different types of models used in financial analysis and trading.
5
5
  """
6
- from bbstrader.models.risk import *
7
- from bbstrader.models.optimization import *
8
- from bbstrader.models.portfolios import *
9
- from bbstrader.models.factors import *
10
- from bbstrader.models.ml import *
6
+ from bbstrader.models.risk import * # noqa: F403
7
+ from bbstrader.models.optimization import * # noqa: F403
8
+ from bbstrader.models.portfolio import * # noqa: F403
9
+ from bbstrader.models.factors import * # noqa: F403
10
+ from bbstrader.models.ml import * # noqa: F403
@@ -0,0 +1,280 @@
1
+ from datetime import datetime
2
+ from typing import Dict, List
3
+
4
+ import pandas as pd
5
+ import yfinance as yf
6
+
7
+ from bbstrader.btengine.data import EODHDataHandler, FMPDataHandler
8
+ from bbstrader.metatrader.rates import download_historical_data
9
+ from bbstrader.tseries import (
10
+ find_cointegrated_pairs,
11
+ select_assets,
12
+ select_candidate_pairs,
13
+ )
14
+
15
+ __all__ = [
16
+ "search_coint_candidate_pairs",
17
+ ]
18
+
19
+
20
+ def search_coint_candidate_pairs(
21
+ securities: pd.DataFrame | List[str] = None,
22
+ candidates: pd.DataFrame | List[str] = None,
23
+ start: str = None,
24
+ end: str = None,
25
+ period_search: bool = False,
26
+ select: bool = True,
27
+ source: str = None,
28
+ universe: int = 100,
29
+ window: int = 2,
30
+ rolling_window: int = None,
31
+ npairs: int = 10,
32
+ tf: str = 'D1',
33
+ path: str = None,
34
+ **kwargs
35
+ ) -> List[Dict[str, str]] | pd.DataFrame:
36
+ """
37
+ Searches for candidate pairs of securities based on cointegration analysis.
38
+
39
+ This function either processes preloaded securities and candidates data
40
+ (as pandas DataFrames) or downloads historical data from a specified
41
+ source (e.g., Yahoo Finance, MetaTrader 5, Financial Modeling Prep, or EODHD).
42
+ It then selects the top `npairs` based on cointegration.
43
+
44
+ Args:
45
+ securities (pd.DataFrame | List[str], optional):
46
+ A DataFrame or list of tickers representing the securities for analysis.
47
+ If using a DataFrame, it should include a MultiIndex with levels
48
+ ['ticker', 'date'].
49
+ candidates (pd.DataFrame | List[str], optional):
50
+ A DataFrame or list of tickers representing the candidate securities
51
+ for pair selection.
52
+ start (str, optional):
53
+ The start date for data retrieval in 'YYYY-MM-DD' format. Ignored
54
+ if both `securities` and `candidates` are DataFrames.
55
+ end (str, optional):
56
+ The end date for data retrieval in 'YYYY-MM-DD' format. Ignored
57
+ if both `securities` and `candidates` are DataFrames.
58
+ period_search (bool, optional):
59
+ If True, the function will perform a periodic search for cointegrated from 3 years
60
+ to the end date by taking 2 yerars rolling window. So you need to have at least 3 years of data
61
+ or set the `window` parameter to 3. Defaults to False.
62
+ select (bool, optional):
63
+ If True, the function will select the top cointegrated pairs based on the
64
+ cointegration test results in form of List[dict].
65
+ If False, the function will return all cointegrated pairs in form of DataFrame.
66
+ This can be useful for further analysis or visualization.
67
+ source (str, optional):
68
+ The data source for historical data retrieval. Must be one of
69
+ ['yf', 'mt5', 'fmp', 'eodhd']. Required if `securities` and
70
+ `candidates` are lists of tickers.
71
+ universe (int, optional):
72
+ The maximum number of assets to retain for analysis. Defaults to 100.
73
+ window (int, optional):
74
+ The number of years of historical data to retrieve if `start` and `end`
75
+ are not specified. Defaults to 2 years.
76
+ rolling_window (int, optional):
77
+ The size of the rolling window (in days) used for asset selection.
78
+ Defaults to None.
79
+ npairs (int, optional):
80
+ The number of top cointegrated pairs to select. Defaults to 10.
81
+ tf (str, optional):
82
+ The timeframe for MetaTrader 5 data retrieval. Defaults to 'D1'.
83
+ path (str, optional):
84
+ The path to MetaTrader 5 historical data files. Required if `source='mt5'`.
85
+ **kwargs:
86
+ Additional parameters for data retrieval (e.g., API keys, date ranges
87
+ for specific sources), see ``bbstrader.btengine.data.FMPDataHandler`` or
88
+ ``bbstrader.btengine.data.EODHDataHandler`` for more details.
89
+
90
+ Returns:
91
+ List[dict]: A list containing the selected top cointegrated pairs if `select=True`.
92
+ pd.DataFrame: A DataFrame containing all cointegrated pairs if `select=False`.
93
+
94
+ Raises:
95
+ ValueError: If the inputs are invalid or if the `source` is not one of
96
+ the supported sources.
97
+
98
+ Examples:
99
+ Using preloaded DataFrames:
100
+ >>> securities = pd.read_csv('securities.csv', index_col=['ticker', 'date'])
101
+ >>> candidates = pd.read_csv('candidates.csv', index_col=['ticker', 'date'])
102
+ >>> pairs = search_candidate_pairs(securities=securities, candidates=candidates)
103
+
104
+ Using a data source (Yahoo Finance):
105
+ >>> securities = ['SPY', 'IWM', 'XLF', 'HYG', 'XLE', 'LQD', 'GDX', 'FXI', 'EWZ', ...]
106
+ >>> candidates = ['AAPL', 'AMZN', 'NVDA', 'MSFT', 'GOOGL', 'AMD', 'BAC', 'NFLX', ...]
107
+
108
+ >>> pairs = search_candidate_pairs(
109
+ ... securities=securities,
110
+ ... candidates=candidates,
111
+ ... start='2022-12-12',
112
+ ... end='2024-12-10',
113
+ ... source='yf',
114
+ ... npairs=10
115
+ ... )
116
+ >>> [
117
+ ... {'x': 'LQD', 'y': 'TMO'},
118
+ ... {'x': 'IEF', 'y': 'COP'},
119
+ ... {'x': 'WMT', 'y': 'IWM'},
120
+ ... {'x': 'MDT', 'y': 'OIH'},
121
+ ... {'x': 'EWZ', 'y': 'CMCSA'},
122
+ ... {'x': 'VLO', 'y': 'XOP'},
123
+ ... {'x': 'SHY', 'y': 'F'},
124
+ ... {'x': 'ABT', 'y': 'LQD'},
125
+ ... {'x': 'PFE', 'y': 'USO'},
126
+ ... {'x': 'LQD', 'y': 'MDT'}
127
+ ... ]
128
+
129
+ Using MetaTrader 5:
130
+ >>> securities = ['EURUSD', 'GBPUSD']
131
+ >>> candidates = ['USDJPY', 'AUDUSD']
132
+ >>> pairs = search_candidate_pairs(
133
+ ... securities=securities,
134
+ ... candidates=candidates,
135
+ ... source='mt5',
136
+ ... tf='H1',
137
+ ... path='/path/to/terminal64.exe',
138
+ ... )
139
+
140
+ Notes:
141
+ - If `securities` and `candidates` are DataFrames, the function assumes
142
+ the data is already preprocessed and indexed by ['ticker', 'date'].
143
+ - When using `source='fmp'` or `source='eodhd'`, API keys and other
144
+ required parameters should be passed via `kwargs`.
145
+
146
+ """
147
+
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, start=start, end=end, progress=False, multi_level_index=False)
156
+ data = data.drop(columns=['Adj Close'], axis=1)
157
+ elif source == 'mt5':
158
+ start, end = pd.Timestamp(start), pd.Timestamp(end)
159
+ data = download_historical_data(
160
+ symbol=ticker, timeframe=tf, date_from=start, date_to=end, **{'path': path})
161
+ data = data.drop(columns=['adj_close'], axis=1)
162
+ elif source in ['fmp', 'eodhd']:
163
+ handler_class = FMPDataHandler if source == 'fmp' else EODHDataHandler
164
+ handler = handler_class(
165
+ events=None, symbol_list=[ticker], **kwargs)
166
+ data = handler.data[ticker]
167
+ else:
168
+ raise ValueError(f"Invalid source: {source}")
169
+
170
+ data = data.reset_index()
171
+ data = data.rename(columns=str.lower)
172
+ data['ticker'] = ticker
173
+ data_list.append(data)
174
+
175
+ except Exception as e:
176
+ print(f"No Data found for {ticker}: {e}")
177
+ continue
178
+
179
+ return pd.concat(data_list)
180
+
181
+ def _handle_date_range(start, end, window):
182
+ """Handle start and end date generation."""
183
+ if start is None or end is None:
184
+ end = pd.Timestamp(datetime.now()).strftime('%Y-%m-%d')
185
+ start = (
186
+ pd.Timestamp(datetime.now()) -
187
+ pd.DateOffset(years=window) + pd.DateOffset(days=1)
188
+ ).strftime('%Y-%m-%d')
189
+ return start, end
190
+
191
+ def _period_search(start, end, securities, candidates, npairs=npairs):
192
+ if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
193
+ raise ValueError(
194
+ "The date range must be at least two (2) years for period search."
195
+ )
196
+ top_pairs = []
197
+ p_start = pd.Timestamp(end) - pd.DateOffset(years=1)
198
+ periods = pd.date_range(
199
+ start=p_start, end=pd.Timestamp(end), freq='BQE')
200
+ npairs = max(round(npairs/2), 1)
201
+ for period in periods:
202
+ s_start = period - pd.DateOffset(years=2) + pd.DateOffset(days=1)
203
+ print(f"Searching for pairs in period: {s_start} - {period}")
204
+ pairs = find_cointegrated_pairs(
205
+ securities, candidates, n=npairs, start=str(s_start), stop=str(period), coint=True)
206
+ pairs['period'] = period
207
+ top_pairs.append(pairs)
208
+ top_pairs = pd.concat(top_pairs)
209
+ if len(top_pairs.columns) <= 1:
210
+ raise ValueError(
211
+ "No pairs found in the specified period."
212
+ "Please adjust the date range or increase the number of pairs."
213
+ )
214
+ return top_pairs.head(npairs*2)
215
+
216
+ def _process_asset_data(securities, candidates, universe, rolling_window):
217
+ """Process and select assets from the data."""
218
+ securities = select_assets(
219
+ securities, n=universe, rolling_window=rolling_window)
220
+ candidates = select_assets(
221
+ candidates, n=universe, rolling_window=rolling_window)
222
+ return securities, candidates
223
+
224
+ if (securities is not None and candidates is not None
225
+ and isinstance(securities, pd.DataFrame)
226
+ and isinstance(candidates, pd.DataFrame)
227
+ ):
228
+ if isinstance(securities.index, pd.MultiIndex) and isinstance(candidates.index, pd.MultiIndex):
229
+ securities, candidates = _process_asset_data(
230
+ securities, candidates, universe, rolling_window)
231
+ if period_search:
232
+ start = securities.index.get_level_values('date').min()
233
+ end = securities.index.get_level_values('date').max()
234
+ top_pairs = _period_search(start, end, securities, candidates)
235
+ else:
236
+ top_pairs = find_cointegrated_pairs(
237
+ securities, candidates, n=npairs, coint=True)
238
+ if select:
239
+ return select_candidate_pairs(top_pairs, period=True if period_search else False)
240
+ else:
241
+ return top_pairs
242
+
243
+ elif source is not None:
244
+ if source not in ['yf', 'mt5', 'fmp', 'eodhd']:
245
+ raise ValueError(
246
+ "source must be either 'yf', 'mt5', 'fmp', or 'eodhd'")
247
+ if not isinstance(securities, list) or not isinstance(candidates, list):
248
+ raise ValueError(
249
+ "securities and candidates must be a list of tickers")
250
+
251
+ start, end = _handle_date_range(start, end, window)
252
+ if source in ['fmp', 'eodhd']:
253
+ kwargs[f'{source}_start'] = kwargs.get(f'{source}_start') or start
254
+ kwargs[f'{source}_end'] = kwargs.get(f'{source}_end') or end
255
+
256
+ securities_data = _download_and_process_data(
257
+ source, securities, start, end, tf, path, **kwargs)
258
+ candidates_data = _download_and_process_data(
259
+ source, candidates, start, end, tf, path, **kwargs)
260
+ securities_data = securities_data.set_index(['ticker', 'date'])
261
+ candidates_data = candidates_data.set_index(['ticker', 'date'])
262
+ securities_data, candidates_data = _process_asset_data(
263
+ securities_data, candidates_data, universe, rolling_window)
264
+ if period_search:
265
+ top_pairs = _period_search(
266
+ start, end, securities_data, candidates_data).head(npairs)
267
+ else:
268
+ top_pairs = find_cointegrated_pairs(
269
+ securities_data, candidates_data, n=npairs, coint=True)
270
+ if select:
271
+ return select_candidate_pairs(top_pairs, period=True if period_search else False)
272
+ else:
273
+ return top_pairs
274
+
275
+ else:
276
+ msg = (
277
+ "Invalid input. Either provide securities"
278
+ "and candidates as DataFrames or specify a data source."
279
+ )
280
+ raise ValueError(msg)