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.
- bbstrader/__ini__.py +7 -7
- bbstrader/btengine/__init__.py +7 -7
- bbstrader/btengine/backtest.py +30 -26
- bbstrader/btengine/data.py +92 -81
- bbstrader/btengine/event.py +2 -1
- bbstrader/btengine/execution.py +18 -16
- bbstrader/btengine/performance.py +11 -7
- bbstrader/btengine/portfolio.py +35 -36
- bbstrader/btengine/strategy.py +113 -92
- bbstrader/config.py +12 -10
- bbstrader/core/data.py +4 -5
- bbstrader/core/utils.py +57 -0
- bbstrader/ibkr/utils.py +0 -0
- bbstrader/metatrader/__init__.py +5 -5
- bbstrader/metatrader/account.py +117 -121
- bbstrader/metatrader/rates.py +81 -78
- bbstrader/metatrader/risk.py +23 -37
- bbstrader/metatrader/trade.py +154 -138
- bbstrader/metatrader/utils.py +3 -3
- bbstrader/models/__init__.py +5 -5
- bbstrader/models/factors.py +17 -12
- bbstrader/models/ml.py +371 -305
- bbstrader/models/optimization.py +14 -12
- bbstrader/models/portfolio.py +44 -35
- bbstrader/models/risk.py +15 -9
- bbstrader/trading/__init__.py +2 -2
- bbstrader/trading/execution.py +245 -179
- bbstrader/trading/scripts.py +8 -4
- bbstrader/trading/strategies.py +78 -65
- bbstrader/tseries.py +124 -98
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/METADATA +2 -1
- bbstrader-0.2.1.dist-info/RECORD +37 -0
- bbstrader-0.2.0.dist-info/RECORD +0 -36
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/top_level.txt +0 -0
bbstrader/models/optimization.py
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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
|
-
|
|
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.
|
bbstrader/models/portfolio.py
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
9
|
-
hierarchical_risk_parity,
|
|
10
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
returns = returns.dropna(thresh=int(
|
|
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(
|
|
95
|
-
|
|
96
|
-
eigen_portfolios
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
8
|
-
from matplotlib import
|
|
9
|
-
from matplotlib.dates import
|
|
10
|
-
|
|
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'
|
|
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 =
|
|
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,
|
|
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
|
bbstrader/trading/__init__.py
CHANGED
|
@@ -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
|