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.
- bbstrader/__ini__.py +2 -2
- bbstrader/btengine/data.py +241 -40
- bbstrader/btengine/strategy.py +12 -8
- bbstrader/config.py +4 -0
- bbstrader/core/__init__.py +0 -0
- bbstrader/core/data.py +23 -0
- bbstrader/core/utils.py +0 -0
- bbstrader/ibkr/__init__.py +0 -0
- bbstrader/metatrader/account.py +66 -12
- bbstrader/metatrader/rates.py +24 -20
- bbstrader/metatrader/risk.py +6 -3
- bbstrader/metatrader/trade.py +31 -13
- bbstrader/models/__init__.py +1 -1
- bbstrader/models/factors.py +275 -0
- bbstrader/models/ml.py +1026 -0
- bbstrader/models/optimization.py +17 -16
- bbstrader/models/{portfolios.py → portfolio.py} +20 -11
- bbstrader/models/risk.py +10 -2
- bbstrader/trading/execution.py +67 -35
- bbstrader/trading/strategies.py +5 -5
- bbstrader/tseries.py +412 -63
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/METADATA +9 -3
- bbstrader-0.2.0.dist-info/RECORD +36 -0
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/WHEEL +1 -1
- bbstrader-0.1.93.dist-info/RECORD +0 -32
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/rates.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
bbstrader/metatrader/risk.py
CHANGED
|
@@ -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()
|
bbstrader/metatrader/trade.py
CHANGED
|
@@ -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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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.
|
|
874
|
-
it
|
|
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
|
|
883
|
-
|
|
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
|
bbstrader/models/__init__.py
CHANGED
|
@@ -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.
|
|
8
|
+
from bbstrader.models.portfolio import *
|
|
9
9
|
from bbstrader.models.factors import *
|
|
10
10
|
from bbstrader.models.ml import *
|
bbstrader/models/factors.py
CHANGED
|
@@ -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)
|