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,43 +1,42 @@
1
- import numpy as np
2
- import pandas as pd
3
- from pypfopt import risk_models
4
- from pypfopt import expected_returns
1
+ import warnings
2
+
3
+ from pypfopt import expected_returns, risk_models
5
4
  from pypfopt.efficient_frontier import EfficientFrontier
6
5
  from pypfopt.hierarchical_portfolio import HRPOpt
7
- import warnings
8
6
 
9
7
  __all__ = [
10
- 'markowitz_weights',
11
- 'hierarchical_risk_parity',
12
- 'equal_weighted',
8
+ 'markowitz_weights',
9
+ 'hierarchical_risk_parity',
10
+ 'equal_weighted',
13
11
  'optimized_weights'
14
12
  ]
15
13
 
14
+
16
15
  def markowitz_weights(prices=None, rfr=0.0, freq=252):
17
16
  """
18
17
  Calculates optimal portfolio weights using Markowitz's mean-variance optimization (Max Sharpe Ratio) with multiple solvers.
19
18
 
20
- Parameters:
19
+ Parameters
21
20
  ----------
22
21
  prices : pd.DataFrame, optional
23
22
  Price data for assets, where rows represent time periods and columns represent assets.
24
23
  freq : int, optional
25
24
  Frequency of the data, such as 252 for daily returns in a year (default is 252).
26
25
 
27
- Returns:
26
+ Returns
28
27
  -------
29
28
  dict
30
29
  Dictionary containing the optimal asset weights for maximizing the Sharpe ratio, normalized to sum to 1.
31
30
 
32
- Notes:
31
+ Notes
33
32
  -----
34
33
  This function attempts to maximize the Sharpe ratio by iterating through various solvers ('SCS', 'ECOS', 'OSQP')
35
34
  from the PyPortfolioOpt library. If a solver fails, it proceeds to the next one. If none succeed, an error message
36
35
  is printed for each solver that fails.
37
36
 
38
37
  This function is useful for portfolio with a small number of assets, as it may not scale well for large portfolios.
39
-
40
- Raises:
38
+
39
+ Raises
41
40
  ------
42
41
  Exception
43
42
  If all solvers fail, each will print an exception error message during runtime.
@@ -47,21 +46,22 @@ def markowitz_weights(prices=None, rfr=0.0, freq=252):
47
46
 
48
47
  # Try different solvers to maximize Sharpe ratio
49
48
  for solver in ['SCS', 'ECOS', 'OSQP']:
50
- ef = EfficientFrontier(expected_returns=returns,
51
- cov_matrix=cov,
49
+ ef = EfficientFrontier(expected_returns=returns,
50
+ cov_matrix=cov,
52
51
  weight_bounds=(0, 1),
53
52
  solver=solver)
54
53
  try:
55
- weights = ef.max_sharpe(risk_free_rate=rfr)
54
+ ef.max_sharpe(risk_free_rate=rfr)
56
55
  return ef.clean_weights()
57
56
  except Exception as e:
58
57
  print(f"Solver {solver} failed with error: {e}")
59
58
 
59
+
60
60
  def hierarchical_risk_parity(prices=None, returns=None, freq=252):
61
61
  """
62
62
  Computes asset weights using Hierarchical Risk Parity (HRP) for risk-averse portfolio allocation.
63
63
 
64
- Parameters:
64
+ Parameters
65
65
  ----------
66
66
  prices : pd.DataFrame, optional
67
67
  Price data for assets; if provided, daily returns will be calculated.
@@ -70,17 +70,17 @@ def hierarchical_risk_parity(prices=None, returns=None, freq=252):
70
70
  freq : int, optional
71
71
  Number of days to consider in calculating portfolio weights (default is 252).
72
72
 
73
- Returns:
73
+ Returns
74
74
  -------
75
75
  dict
76
76
  Optimized asset weights using the HRP method, with asset weights summing to 1.
77
77
 
78
- Raises:
78
+ Raises
79
79
  ------
80
80
  ValueError
81
81
  If neither `prices` nor `returns` are provided.
82
82
 
83
- Notes:
83
+ Notes
84
84
  -----
85
85
  Hierarchical Risk Parity is particularly useful for portfolios with a large number of assets,
86
86
  as it mitigates issues of multicollinearity and estimation errors in covariance matrices by
@@ -97,11 +97,12 @@ def hierarchical_risk_parity(prices=None, returns=None, freq=252):
97
97
  hrp = HRPOpt(returns=returns.iloc[-freq:])
98
98
  return hrp.optimize()
99
99
 
100
+
100
101
  def equal_weighted(prices=None, returns=None, round_digits=5):
101
102
  """
102
103
  Generates an equal-weighted portfolio by assigning an equal proportion to each asset.
103
104
 
104
- Parameters:
105
+ Parameters
105
106
  ----------
106
107
  prices : pd.DataFrame, optional
107
108
  Price data for assets, where each column represents an asset.
@@ -110,21 +111,22 @@ def equal_weighted(prices=None, returns=None, round_digits=5):
110
111
  round_digits : int, optional
111
112
  Number of decimal places to round each weight to (default is 5).
112
113
 
113
- Returns:
114
+ Returns
114
115
  -------
115
116
  dict
116
117
  Dictionary with equal weights assigned to each asset, summing to 1.
117
118
 
118
- Raises:
119
+ Raises
119
120
  ------
120
121
  ValueError
121
122
  If neither `prices` nor `returns` are provided.
122
123
 
123
- Notes:
124
+ Notes
124
125
  -----
125
126
  Equal weighting is a simple allocation method that assumes equal importance across all assets,
126
127
  useful as a baseline model and when no strong views exist on asset return expectations or risk.
127
128
  """
129
+
128
130
  if returns is None and prices is None:
129
131
  raise ValueError("Either prices or returns must be provided")
130
132
  if returns is None:
@@ -135,11 +137,12 @@ def equal_weighted(prices=None, returns=None, round_digits=5):
135
137
  columns = returns.columns
136
138
  return {col: round(1/n, round_digits) for col in columns}
137
139
 
140
+
138
141
  def optimized_weights(prices=None, returns=None, rfr=0.0, freq=252, method='equal'):
139
142
  """
140
143
  Selects an optimization method to calculate portfolio weights based on user preference.
141
144
 
142
- Parameters:
145
+ Parameters
143
146
  ----------
144
147
  prices : pd.DataFrame, optional
145
148
  Price data for assets, required for certain methods.
@@ -150,17 +153,17 @@ def optimized_weights(prices=None, returns=None, rfr=0.0, freq=252, method='equa
150
153
  method : str, optional
151
154
  Optimization method to use ('markowitz', 'hrp', or 'equal') (default is 'markowitz').
152
155
 
153
- Returns:
156
+ Returns
154
157
  -------
155
158
  dict
156
159
  Dictionary containing optimized asset weights based on the chosen method.
157
160
 
158
- Raises:
161
+ Raises
159
162
  ------
160
163
  ValueError
161
164
  If an unknown optimization method is specified.
162
165
 
163
- Notes:
166
+ Notes
164
167
  -----
165
168
  This function integrates different optimization methods:
166
169
  - 'markowitz': mean-variance optimization with max Sharpe ratio
@@ -1,27 +1,38 @@
1
- import numpy as np
1
+ import matplotlib.pyplot as plt
2
2
  import pandas as pd
3
3
  import seaborn as sns
4
- import matplotlib.pyplot as plt
5
4
  from sklearn.decomposition import PCA
6
5
  from sklearn.preprocessing import scale
6
+
7
7
  from bbstrader.models.optimization import (
8
- markowitz_weights,
9
- hierarchical_risk_parity,
10
- equal_weighted
8
+ equal_weighted,
9
+ hierarchical_risk_parity,
10
+ markowitz_weights,
11
11
  )
12
12
 
13
13
  __all__ = [
14
14
  'EigenPortfolios'
15
15
  ]
16
16
 
17
+
17
18
  class EigenPortfolios(object):
18
19
  """
19
20
  The `EigenPortfolios` class applies Principal Component Analysis (PCA) to a covariance matrix of normalized asset returns
20
21
  to derive portfolios (eigenportfolios) that capture distinct risk factors in the asset returns. Each eigenportfolio
21
22
  represents a principal component of the return covariance matrix, ordered by the magnitude of its eigenvalue. These
22
23
  portfolios capture most of the variance in asset returns and are mutually uncorrelated.
23
-
24
+
25
+ Notes
26
+ -----
27
+ The implementation is inspired by the book "Machine Learning for Algorithmic Trading" by Stefan Jansen.
28
+
29
+ References
30
+ ----------
31
+ Stefan Jansen (2020). Machine Learning for Algorithmic Trading - Second Edition.
32
+ chapter 13, Data-Driven Risk Factors and Asset Allocation with Unsupervised Learning.
33
+
24
34
  """
35
+
25
36
  def __init__(self):
26
37
  self.returns = None
27
38
  self.n_portfolios = None
@@ -31,13 +42,13 @@ class EigenPortfolios(object):
31
42
  def get_portfolios(self) -> pd.DataFrame:
32
43
  """
33
44
  Returns the computed eigenportfolios (weights of assets in each portfolio).
34
-
35
- Returns:
45
+
46
+ Returns
36
47
  -------
37
48
  pd.DataFrame
38
49
  DataFrame containing eigenportfolio weights for each asset.
39
-
40
- Raises:
50
+
51
+ Raises
41
52
  ------
42
53
  ValueError
43
54
  If `fit()` has not been called before retrieving portfolios.
@@ -46,23 +57,23 @@ class EigenPortfolios(object):
46
57
  raise ValueError("fit() must be called first")
47
58
  return self._portfolios
48
59
 
49
- def fit(self, returns: pd.DataFrame, n_portfolios: int=4) -> pd.DataFrame:
60
+ def fit(self, returns: pd.DataFrame, n_portfolios: int = 4) -> pd.DataFrame:
50
61
  """
51
62
  Computes the eigenportfolios based on PCA of the asset returns' covariance matrix.
52
-
53
- Parameters:
63
+
64
+ Parameters
54
65
  ----------
55
66
  returns : pd.DataFrame
56
67
  Historical returns of assets to be used for PCA.
57
68
  n_portfolios : int, optional
58
69
  Number of eigenportfolios to compute (default is 4).
59
-
60
- Returns:
70
+
71
+ Returns
61
72
  -------
62
73
  pd.DataFrame
63
74
  DataFrame containing normalized weights for each eigenportfolio.
64
75
 
65
- Notes:
76
+ Notes
66
77
  -----
67
78
  This method performs winsorization and normalization on returns to reduce the impact of outliers
68
79
  and achieve zero mean and unit variance. It uses the first `n_portfolios` principal components
@@ -70,11 +81,12 @@ class EigenPortfolios(object):
70
81
  """
71
82
  # Winsorize and normalize the returns
72
83
  normed_returns = scale(returns
73
- .clip(lower=returns.quantile(q=.025),
74
- upper=returns.quantile(q=.975),
75
- axis=1)
76
- .apply(lambda x: x.sub(x.mean()).div(x.std())))
77
- returns = returns.dropna(thresh=int(normed_returns.shape[0] * .95), axis=1)
84
+ .clip(lower=returns.quantile(q=.025),
85
+ upper=returns.quantile(q=.975),
86
+ axis=1)
87
+ .apply(lambda x: x.sub(x.mean()).div(x.std())))
88
+ returns = returns.dropna(thresh=int(
89
+ normed_returns.shape[0] * .95), axis=1)
78
90
  returns = returns.dropna(thresh=int(normed_returns.shape[1] * .95))
79
91
 
80
92
  cov = returns.cov()
@@ -82,9 +94,12 @@ class EigenPortfolios(object):
82
94
  pca = PCA()
83
95
  pca.fit(cov)
84
96
 
85
- top_portfolios = pd.DataFrame(pca.components_[:n_portfolios], columns=cov.columns)
86
- eigen_portfolios = top_portfolios.div(top_portfolios.sum(axis=1), axis=0)
87
- eigen_portfolios.index = [f"Portfolio {i}" for i in range(1, n_portfolios + 1)]
97
+ top_portfolios = pd.DataFrame(
98
+ pca.components_[:n_portfolios], columns=cov.columns)
99
+ eigen_portfolios = top_portfolios.div(
100
+ top_portfolios.sum(axis=1), axis=0)
101
+ eigen_portfolios.index = [
102
+ f"Portfolio {i}" for i in range(1, n_portfolios + 1)]
88
103
  self._portfolios = eigen_portfolios
89
104
  self.returns = returns
90
105
  self.n_portfolios = n_portfolios
@@ -93,8 +108,8 @@ class EigenPortfolios(object):
93
108
  def plot_weights(self):
94
109
  """
95
110
  Plots the weights of each asset in each eigenportfolio as bar charts.
96
-
97
- Notes:
111
+
112
+ Notes
98
113
  -----
99
114
  Each subplot represents one eigenportfolio, showing the contribution of each asset.
100
115
  """
@@ -109,7 +124,7 @@ class EigenPortfolios(object):
109
124
  for ax in axes.flatten():
110
125
  ax.set_ylabel('Portfolio Weight')
111
126
  ax.set_xlabel('')
112
-
127
+
113
128
  sns.despine()
114
129
  plt.tight_layout()
115
130
  plt.show()
@@ -117,25 +132,27 @@ class EigenPortfolios(object):
117
132
  def plot_performance(self):
118
133
  """
119
134
  Plots the cumulative returns of each eigenportfolio over time.
120
-
121
- Notes:
135
+
136
+ Notes
122
137
  -----
123
138
  This method calculates the historical cumulative performance of each eigenportfolio
124
139
  by weighting asset returns according to eigenportfolio weights.
125
140
  """
126
141
  eigen_portfolios = self.get_portfolios()
127
142
  returns = self.returns.copy()
128
-
143
+
129
144
  n_cols = 2
130
145
  n_rows = (self.n_portfolios + 1 + n_cols - 1) // n_cols
131
146
  figsize = (n_cols * 10, n_rows * 5)
132
147
  fig, axes = plt.subplots(nrows=n_rows, ncols=n_cols,
133
148
  figsize=figsize, sharex=True)
134
149
  axes = axes.flatten()
135
- returns.mean(1).add(1).cumprod().sub(1).plot(title='The Market', ax=axes[0])
136
-
150
+ returns.mean(1).add(1).cumprod().sub(
151
+ 1).plot(title='The Market', ax=axes[0])
152
+
137
153
  for i in range(self.n_portfolios):
138
- rc = returns.mul(eigen_portfolios.iloc[i]).sum(1).add(1).cumprod().sub(1)
154
+ rc = returns.mul(eigen_portfolios.iloc[i]).sum(
155
+ 1).add(1).cumprod().sub(1)
139
156
  rc.plot(title=f'Portfolio {i+1}', ax=axes[i + 1], lw=1, rot=0)
140
157
 
141
158
  for j in range(self.n_portfolios + 1, len(axes)):
@@ -143,16 +160,16 @@ class EigenPortfolios(object):
143
160
 
144
161
  for i in range(self.n_portfolios + 1):
145
162
  axes[i].set_xlabel('')
146
-
163
+
147
164
  sns.despine()
148
165
  fig.tight_layout()
149
166
  plt.show()
150
-
167
+
151
168
  def optimize(self, portfolio: int = 1, optimizer: str = 'hrp', prices=None, freq=252, plot=True):
152
169
  """
153
170
  Optimizes the chosen eigenportfolio based on a specified optimization method.
154
-
155
- Parameters:
171
+
172
+ Parameters
156
173
  ----------
157
174
  portfolio : int, optional
158
175
  Index of the eigenportfolio to optimize (default is 1).
@@ -164,29 +181,30 @@ class EigenPortfolios(object):
164
181
  Frequency of returns (e.g., 252 for daily returns).
165
182
  plot : bool, optional
166
183
  Whether to plot the performance of the optimized portfolio (default is True).
167
-
168
- Returns:
184
+
185
+ Returns
169
186
  -------
170
187
  dict
171
188
  Dictionary of optimized asset weights.
172
-
173
- Raises:
189
+
190
+ Raises
174
191
  ------
175
192
  ValueError
176
193
  If an unknown optimizer is specified, or if prices are not provided when using Markowitz optimization.
177
-
178
- Notes:
194
+
195
+ Notes
179
196
  -----
180
197
  The optimization method varies based on risk-return assumptions, with options for traditional Markowitz optimization,
181
198
  Hierarchical Risk Parity, or equal weighting.
182
199
  """
183
200
  portfolio = self.get_portfolios().iloc[portfolio - 1]
184
201
  returns = self.returns.loc[:, portfolio.index]
185
- returns = returns.loc[:, ~returns.columns.duplicated()]
202
+ returns = returns.loc[:, ~returns.columns.duplicated()]
186
203
  returns = returns.loc[~returns.index.duplicated(keep='first')]
187
204
  if optimizer == 'markowitz':
188
205
  if prices is None:
189
- raise ValueError("prices must be provided for markowitz optimization")
206
+ raise ValueError(
207
+ "prices must be provided for markowitz optimization")
190
208
  prices = prices.loc[:, returns.columns]
191
209
  weights = markowitz_weights(prices=prices, freq=freq)
192
210
  elif optimizer == 'hrp':
@@ -202,4 +220,4 @@ class EigenPortfolios(object):
202
220
  rc.plot(title=f'Optimized {portfolio.name}', lw=1, rot=0)
203
221
  sns.despine()
204
222
  plt.show()
205
- return weights
223
+ return weights
bbstrader/models/risk.py CHANGED
@@ -1,14 +1,18 @@
1
1
  import pickle
2
+ from abc import ABCMeta, abstractmethod
3
+ from datetime import datetime
4
+ from typing import Dict, Optional
5
+
2
6
  import numpy as np
3
7
  import pandas as pd
4
8
  import seaborn as sns
5
- from datetime import datetime
6
9
  from hmmlearn.hmm import GaussianHMM
7
- from abc import ABCMeta, abstractmethod
8
- from matplotlib import cm, pyplot as plt
9
- from matplotlib.dates import YearLocator, MonthLocator
10
- from typing import Optional, Dict
10
+ from matplotlib import cm
11
+ from matplotlib import pyplot as plt
12
+ from matplotlib.dates import MonthLocator, YearLocator
13
+
11
14
  from bbstrader.metatrader.rates import Rates
15
+
12
16
  sns.set_theme()
13
17
 
14
18
  __all__ = [
@@ -311,7 +315,7 @@ class HMMRiskManager(RiskModel):
311
315
  """
312
316
  df = data_frame.copy()
313
317
  if 'Returns' or 'returns' not in df.columns:
314
- if 'Close' in df.columns:
318
+ if 'Close' in df.columns:
315
319
  df['Returns'] = df['Close'].pct_change()
316
320
  elif 'Adj Close' in df.columns:
317
321
  df['Returns'] = df['Adj Close'].pct_change()
@@ -358,12 +362,13 @@ class HMMRiskManager(RiskModel):
358
362
  ax.grid(True)
359
363
  plt.show()
360
364
 
365
+
361
366
  def build_hmm_models(symbol_list=None, **kwargs
362
367
  ) -> Dict[str, HMMRiskManager]:
363
368
  mt5_data = kwargs.get("use_mt5_data", False)
364
369
  data = kwargs.get("hmm_data")
365
370
  tf = kwargs.get("time_frame", 'D1')
366
- hmm_end = kwargs.get("hmm_end", 0)
371
+ hmm_end = kwargs.get("hmm_end", 0)
367
372
  sd = kwargs.get("session_duration", 23.0)
368
373
  hmm_tickers = kwargs.get("hmm_tickers")
369
374
  if hmm_tickers is not None:
@@ -385,10 +390,11 @@ def build_hmm_models(symbol_list=None, **kwargs
385
390
  hmm_models[symbol] = hmm
386
391
  if mt5_data:
387
392
  for symbol in symbols:
388
- rates = Rates(symbol, tf, start_pos=hmm_end, session_duration=sd)
393
+ rates = Rates(symbol, timeframe=tf, start_pos=hmm_end,
394
+ session_duration=sd, **kwargs)
389
395
  data = rates.get_rates_from_pos()
390
396
  assert data is not None, f"No data for {symbol}"
391
397
  hmm = HMMRiskManager(
392
398
  data=data, verbose=True, iterations=1000, **kwargs)
393
399
  hmm_models[symbol] = hmm
394
- return hmm_models
400
+ return hmm_models
@@ -7,5 +7,5 @@ The module is designed to be flexible and extensible, allowing users to define t
7
7
  strategies and customize the trading process.
8
8
 
9
9
  """
10
- from bbstrader.trading.execution import *
11
- from bbstrader.trading.strategies import *
10
+ from bbstrader.trading.execution import * # noqa: F403
11
+ from bbstrader.trading.strategies import * # noqa: F403