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

@@ -1,18 +1,17 @@
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.
@@ -36,7 +35,7 @@ def markowitz_weights(prices=None, rfr=0.0, freq=252):
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
-
38
+
40
39
  Raises
41
40
  ------
42
41
  Exception
@@ -47,16 +46,17 @@ 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.
@@ -97,6 +97,7 @@ 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.
@@ -136,6 +137,7 @@ def equal_weighted(prices=None, returns=None, round_digits=5):
136
137
  columns = returns.columns
137
138
  return {col: round(1/n, round_digits) for col in columns}
138
139
 
140
+
139
141
  def optimized_weights(prices=None, returns=None, rfr=0.0, freq=252, method='equal'):
140
142
  """
141
143
  Selects an optimization method to calculate portfolio weights based on user preference.
@@ -1,19 +1,20 @@
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
@@ -29,8 +30,9 @@ class EigenPortfolios(object):
29
30
  ----------
30
31
  Stefan Jansen (2020). Machine Learning for Algorithmic Trading - Second Edition.
31
32
  chapter 13, Data-Driven Risk Factors and Asset Allocation with Unsupervised Learning.
32
-
33
+
33
34
  """
35
+
34
36
  def __init__(self):
35
37
  self.returns = None
36
38
  self.n_portfolios = None
@@ -40,12 +42,12 @@ class EigenPortfolios(object):
40
42
  def get_portfolios(self) -> pd.DataFrame:
41
43
  """
42
44
  Returns the computed eigenportfolios (weights of assets in each portfolio).
43
-
45
+
44
46
  Returns
45
47
  -------
46
48
  pd.DataFrame
47
49
  DataFrame containing eigenportfolio weights for each asset.
48
-
50
+
49
51
  Raises
50
52
  ------
51
53
  ValueError
@@ -55,17 +57,17 @@ class EigenPortfolios(object):
55
57
  raise ValueError("fit() must be called first")
56
58
  return self._portfolios
57
59
 
58
- 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:
59
61
  """
60
62
  Computes the eigenportfolios based on PCA of the asset returns' covariance matrix.
61
-
63
+
62
64
  Parameters
63
65
  ----------
64
66
  returns : pd.DataFrame
65
67
  Historical returns of assets to be used for PCA.
66
68
  n_portfolios : int, optional
67
69
  Number of eigenportfolios to compute (default is 4).
68
-
70
+
69
71
  Returns
70
72
  -------
71
73
  pd.DataFrame
@@ -79,11 +81,12 @@ class EigenPortfolios(object):
79
81
  """
80
82
  # Winsorize and normalize the returns
81
83
  normed_returns = scale(returns
82
- .clip(lower=returns.quantile(q=.025),
83
- upper=returns.quantile(q=.975),
84
- axis=1)
85
- .apply(lambda x: x.sub(x.mean()).div(x.std())))
86
- 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)
87
90
  returns = returns.dropna(thresh=int(normed_returns.shape[1] * .95))
88
91
 
89
92
  cov = returns.cov()
@@ -91,9 +94,12 @@ class EigenPortfolios(object):
91
94
  pca = PCA()
92
95
  pca.fit(cov)
93
96
 
94
- top_portfolios = pd.DataFrame(pca.components_[:n_portfolios], columns=cov.columns)
95
- eigen_portfolios = top_portfolios.div(top_portfolios.sum(axis=1), axis=0)
96
- 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)]
97
103
  self._portfolios = eigen_portfolios
98
104
  self.returns = returns
99
105
  self.n_portfolios = n_portfolios
@@ -102,7 +108,7 @@ class EigenPortfolios(object):
102
108
  def plot_weights(self):
103
109
  """
104
110
  Plots the weights of each asset in each eigenportfolio as bar charts.
105
-
111
+
106
112
  Notes
107
113
  -----
108
114
  Each subplot represents one eigenportfolio, showing the contribution of each asset.
@@ -118,7 +124,7 @@ class EigenPortfolios(object):
118
124
  for ax in axes.flatten():
119
125
  ax.set_ylabel('Portfolio Weight')
120
126
  ax.set_xlabel('')
121
-
127
+
122
128
  sns.despine()
123
129
  plt.tight_layout()
124
130
  plt.show()
@@ -126,7 +132,7 @@ class EigenPortfolios(object):
126
132
  def plot_performance(self):
127
133
  """
128
134
  Plots the cumulative returns of each eigenportfolio over time.
129
-
135
+
130
136
  Notes
131
137
  -----
132
138
  This method calculates the historical cumulative performance of each eigenportfolio
@@ -134,17 +140,19 @@ class EigenPortfolios(object):
134
140
  """
135
141
  eigen_portfolios = self.get_portfolios()
136
142
  returns = self.returns.copy()
137
-
143
+
138
144
  n_cols = 2
139
145
  n_rows = (self.n_portfolios + 1 + n_cols - 1) // n_cols
140
146
  figsize = (n_cols * 10, n_rows * 5)
141
147
  fig, axes = plt.subplots(nrows=n_rows, ncols=n_cols,
142
148
  figsize=figsize, sharex=True)
143
149
  axes = axes.flatten()
144
- returns.mean(1).add(1).cumprod().sub(1).plot(title='The Market', ax=axes[0])
145
-
150
+ returns.mean(1).add(1).cumprod().sub(
151
+ 1).plot(title='The Market', ax=axes[0])
152
+
146
153
  for i in range(self.n_portfolios):
147
- 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)
148
156
  rc.plot(title=f'Portfolio {i+1}', ax=axes[i + 1], lw=1, rot=0)
149
157
 
150
158
  for j in range(self.n_portfolios + 1, len(axes)):
@@ -152,15 +160,15 @@ class EigenPortfolios(object):
152
160
 
153
161
  for i in range(self.n_portfolios + 1):
154
162
  axes[i].set_xlabel('')
155
-
163
+
156
164
  sns.despine()
157
165
  fig.tight_layout()
158
166
  plt.show()
159
-
167
+
160
168
  def optimize(self, portfolio: int = 1, optimizer: str = 'hrp', prices=None, freq=252, plot=True):
161
169
  """
162
170
  Optimizes the chosen eigenportfolio based on a specified optimization method.
163
-
171
+
164
172
  Parameters
165
173
  ----------
166
174
  portfolio : int, optional
@@ -173,17 +181,17 @@ class EigenPortfolios(object):
173
181
  Frequency of returns (e.g., 252 for daily returns).
174
182
  plot : bool, optional
175
183
  Whether to plot the performance of the optimized portfolio (default is True).
176
-
184
+
177
185
  Returns
178
186
  -------
179
187
  dict
180
188
  Dictionary of optimized asset weights.
181
-
189
+
182
190
  Raises
183
191
  ------
184
192
  ValueError
185
193
  If an unknown optimizer is specified, or if prices are not provided when using Markowitz optimization.
186
-
194
+
187
195
  Notes
188
196
  -----
189
197
  The optimization method varies based on risk-return assumptions, with options for traditional Markowitz optimization,
@@ -191,11 +199,12 @@ class EigenPortfolios(object):
191
199
  """
192
200
  portfolio = self.get_portfolios().iloc[portfolio - 1]
193
201
  returns = self.returns.loc[:, portfolio.index]
194
- returns = returns.loc[:, ~returns.columns.duplicated()]
202
+ returns = returns.loc[:, ~returns.columns.duplicated()]
195
203
  returns = returns.loc[~returns.index.duplicated(keep='first')]
196
204
  if optimizer == 'markowitz':
197
205
  if prices is None:
198
- raise ValueError("prices must be provided for markowitz optimization")
206
+ raise ValueError(
207
+ "prices must be provided for markowitz optimization")
199
208
  prices = prices.loc[:, returns.columns]
200
209
  weights = markowitz_weights(prices=prices, freq=freq)
201
210
  elif optimizer == 'hrp':
@@ -211,4 +220,4 @@ class EigenPortfolios(object):
211
220
  rc.plot(title=f'Optimized {portfolio.name}', lw=1, rot=0)
212
221
  sns.despine()
213
222
  plt.show()
214
- 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, timeframe=tf, start_pos=hmm_end, session_duration=sd, **kwargs)
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