bbstrader 2.0.7__tar.gz → 2.0.8__tar.gz
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.
- {bbstrader-2.0.7 → bbstrader-2.0.8}/PKG-INFO +2 -1
- bbstrader-2.0.8/VERSION.txt +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/pyproject.toml +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/api/handlers.py +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/btengine/performance.py +75 -1
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/metatrader/broker.py +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/metatrader/copier.py +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/metatrader/rates.py +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/metatrader/risk.py +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/metatrader/trade.py +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/metatrader/utils.py +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/models/optimization.py +79 -5
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/trading/execution.py +1 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tcopier.iss +1 -1
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/api/test_metatrader_client.py +1 -0
- bbstrader-2.0.8/tests/btengine/test_performance.py +58 -0
- bbstrader-2.0.8/tests/models/test_optimization.py +68 -0
- bbstrader-2.0.7/VERSION.txt +0 -1
- {bbstrader-2.0.7 → bbstrader-2.0.8}/.clang-format +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/.clang-tidy +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/.github/workflows/build.yml +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/.github/workflows/python.yml +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/.gitignore +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/.readthedocs.yaml +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/CMakeLists.txt +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/LICENSE +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/README.md +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/cmake/Helpers.cmake +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/cmake/Versions.cmake +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/Doxyfile.in +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/Makefile +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/bbstrader.api.rst +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/bbstrader.btengine.rst +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/bbstrader.core.rst +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/bbstrader.metatrader.rst +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/bbstrader.models.rst +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/bbstrader.rst +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/bbstrader.trading.rst +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/conf.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/index.rst +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/make.bat +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/modules.rst +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/docs/requirements.txt +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/examples/strategies.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/include/bbstrader/metatrader.hpp +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/include/bbstrader/objects.hpp +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/CMakeLists.txt +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/__main__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/api/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/api/client.pyi +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/assets/bbs_.png +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/assets/bbstrader.ico +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/assets/bbstrader.png +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/assets/qs_metrics_1.png +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/btengine/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/btengine/backtest.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/btengine/data.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/btengine/event.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/btengine/execution.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/btengine/portfolio.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/btengine/strategy.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/compat.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/config.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/core/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/core/data.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/core/strategy.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/metatrader/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/metatrader/_copier.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/metatrader/account.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/models/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/models/nlp.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/scripts.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/trading/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/trading/strategy.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/bbstrader/trading/utils.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/cmake/BbstraderConfig.cmake.in +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/cpp/bbstrader.cpp +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/src/cpp/metatrader.cpp +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tcopier.spec +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/CMakeLists.txt +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/api/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/btengine/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/btengine/test_backtest.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/btengine/test_data.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/btengine/test_events.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/btengine/test_execution.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/btengine/test_portfolio.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/cpp/test_metatrader_client.cpp +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/metatrader/__init__.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/metatrader/test_account.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/metatrader/test_rates.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/metatrader/test_risk_management.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/tests/metatrader/test_trade.py +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/uv.lock +0 -0
- {bbstrader-2.0.7 → bbstrader-2.0.8}/vcpkg.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.8
|
|
4
4
|
Summary: Simplified Investment & Trading Toolkit with Python & C++
|
|
5
5
|
Keywords: Finance,Toolkit,Financial,Analysis,Fundamental,Quantitative,Database,Equities,Currencies,Economics,ETFs,Funds,Indices,Moneymarkets,Commodities,Futures,CFDs,Derivatives,Trading,Investing,Portfolio,Optimization,Performance
|
|
6
6
|
Author-Email: Bertin Balouki SIMYELI <bertin@bbs-trading.com>
|
|
@@ -28,6 +28,7 @@ Requires-Dist: ipython>=9.5.0
|
|
|
28
28
|
Requires-Dist: nltk>=3.9.1
|
|
29
29
|
Requires-Dist: notify_py>=0.3.43
|
|
30
30
|
Requires-Dist: numpy>=2.2.6
|
|
31
|
+
Requires-Dist: plotly>=5.24.1
|
|
31
32
|
Requires-Dist: praw>=7.8.1
|
|
32
33
|
Requires-Dist: pybind11>=3.0.1
|
|
33
34
|
Requires-Dist: pyfiglet>=1.0.4
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.0.8
|
|
@@ -17,6 +17,10 @@ __all__ = [
|
|
|
17
17
|
"plot_performance",
|
|
18
18
|
"create_sharpe_ratio",
|
|
19
19
|
"create_sortino_ratio",
|
|
20
|
+
"create_omega_ratio",
|
|
21
|
+
"create_calmar_ratio",
|
|
22
|
+
"create_tail_ratio",
|
|
23
|
+
"calculate_risk_metrics",
|
|
20
24
|
"plot_returns_and_dd",
|
|
21
25
|
"plot_monthly_yearly_returns",
|
|
22
26
|
"show_qs_stats",
|
|
@@ -111,6 +115,72 @@ def create_sortino_ratio(returns: pd.Series, periods: int = 252) -> float:
|
|
|
111
115
|
return qs.stats.sortino(returns, periods=periods)
|
|
112
116
|
|
|
113
117
|
|
|
118
|
+
def create_omega_ratio(
|
|
119
|
+
returns: pd.Series, periods: int = 252, rf: float = 0.0
|
|
120
|
+
) -> float:
|
|
121
|
+
"""
|
|
122
|
+
Create the Omega ratio for the strategy.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
returns : A pandas Series representing period percentage returns.
|
|
126
|
+
periods (int): Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc.
|
|
127
|
+
rf (float): Risk-free rate.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
float: Omega ratio
|
|
131
|
+
"""
|
|
132
|
+
return qs.stats.omega(returns, rf=rf)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def create_calmar_ratio(returns: pd.Series, periods: int = 252) -> float:
|
|
136
|
+
"""
|
|
137
|
+
Create the Calmar ratio for the strategy.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
returns : A pandas Series representing period percentage returns.
|
|
141
|
+
periods (int): Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
float: Calmar ratio
|
|
145
|
+
"""
|
|
146
|
+
return qs.stats.calmar(returns)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def create_tail_ratio(returns: pd.Series) -> float:
|
|
150
|
+
"""
|
|
151
|
+
Create the Tail ratio for the strategy.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
returns : A pandas Series representing period percentage returns.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
float: Tail ratio
|
|
158
|
+
"""
|
|
159
|
+
return qs.stats.tail_ratio(returns)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def calculate_risk_metrics(
|
|
163
|
+
returns: pd.Series, benchmark_returns: pd.Series, periods: int = 252
|
|
164
|
+
) -> Dict[str, float]:
|
|
165
|
+
"""
|
|
166
|
+
Calculate Alpha, Beta and Volatility for the strategy.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
returns : A pandas Series representing period percentage returns.
|
|
170
|
+
benchmark_returns : A pandas Series representing benchmark period percentage returns.
|
|
171
|
+
periods (int): Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Dict[str, float]: Alpha, Beta, Volatility
|
|
175
|
+
"""
|
|
176
|
+
g_beta = qs.stats.greeks(returns, benchmark_returns)
|
|
177
|
+
alpha = g_beta["alpha"]
|
|
178
|
+
beta = g_beta["beta"]
|
|
179
|
+
volatility = qs.stats.volatility(returns, periods=periods)
|
|
180
|
+
|
|
181
|
+
return {"alpha": alpha, "beta": beta, "volatility": volatility}
|
|
182
|
+
|
|
183
|
+
|
|
114
184
|
def create_drawdowns(pnl: pd.Series) -> Tuple[pd.Series, float, float]:
|
|
115
185
|
"""
|
|
116
186
|
Calculate the largest peak-to-trough drawdown of the PnL curve
|
|
@@ -125,6 +195,8 @@ def create_drawdowns(pnl: pd.Series) -> Tuple[pd.Series, float, float]:
|
|
|
125
195
|
"""
|
|
126
196
|
# Calculate the cumulative returns curve
|
|
127
197
|
# and set up the High Water Mark
|
|
198
|
+
if pnl.empty:
|
|
199
|
+
return pd.Series(dtype=float), 0.0, 0.0
|
|
128
200
|
hwm = pd.Series(index=pnl.index)
|
|
129
201
|
hwm.iloc[0] = 0
|
|
130
202
|
|
|
@@ -139,7 +211,9 @@ def create_drawdowns(pnl: pd.Series) -> Tuple[pd.Series, float, float]:
|
|
|
139
211
|
drawdown.iloc[t] = hwm.iloc[t] - pnl.iloc[t]
|
|
140
212
|
duration.iloc[t] = 0 if drawdown.iloc[t] == 0 else duration.iloc[t - 1] + 1
|
|
141
213
|
|
|
142
|
-
|
|
214
|
+
max_drawdown = drawdown.max() if not drawdown.empty else 0.0
|
|
215
|
+
max_duration = duration.max() if not duration.empty else 0.0
|
|
216
|
+
return drawdown, max_drawdown, max_duration
|
|
143
217
|
|
|
144
218
|
|
|
145
219
|
def plot_performance(df: pd.DataFrame, title: str) -> None:
|
|
@@ -3,30 +3,36 @@ import warnings
|
|
|
3
3
|
from pypfopt import expected_returns, risk_models
|
|
4
4
|
from pypfopt.efficient_frontier import EfficientFrontier
|
|
5
5
|
from pypfopt.hierarchical_portfolio import HRPOpt
|
|
6
|
+
from pypfopt.black_litterman import BlackLittermanModel
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
8
9
|
"markowitz_weights",
|
|
9
10
|
"hierarchical_risk_parity",
|
|
11
|
+
"black_litterman_weights",
|
|
10
12
|
"equal_weighted",
|
|
11
13
|
"optimized_weights",
|
|
12
14
|
]
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
def markowitz_weights(prices=None, rfr=0.0, freq=252):
|
|
17
|
+
def markowitz_weights(prices=None, rfr=0.0, freq=252, min_vol=False):
|
|
16
18
|
"""
|
|
17
|
-
Calculates optimal portfolio weights using Markowitz's mean-variance optimization (Max Sharpe Ratio) with multiple solvers.
|
|
19
|
+
Calculates optimal portfolio weights using Markowitz's mean-variance optimization (Max Sharpe Ratio or Min Volatility) with multiple solvers.
|
|
18
20
|
|
|
19
21
|
Parameters
|
|
20
22
|
----------
|
|
21
23
|
prices : pd.DataFrame, optional
|
|
22
24
|
Price data for assets, where rows represent time periods and columns represent assets.
|
|
25
|
+
rfr : float, optional
|
|
26
|
+
Risk-free rate (default is 0.0).
|
|
23
27
|
freq : int, optional
|
|
24
28
|
Frequency of the data, such as 252 for daily returns in a year (default is 252).
|
|
29
|
+
min_vol : bool, optional
|
|
30
|
+
If True, optimizes for minimum volatility instead of maximum Sharpe ratio (default is False).
|
|
25
31
|
|
|
26
32
|
Returns
|
|
27
33
|
-------
|
|
28
34
|
dict
|
|
29
|
-
Dictionary containing the optimal asset weights for maximizing the Sharpe ratio, normalized to sum to 1.
|
|
35
|
+
Dictionary containing the optimal asset weights for maximizing the Sharpe ratio or minimizing volatility, normalized to sum to 1.
|
|
30
36
|
|
|
31
37
|
Notes
|
|
32
38
|
-----
|
|
@@ -53,10 +59,15 @@ def markowitz_weights(prices=None, rfr=0.0, freq=252):
|
|
|
53
59
|
solver=solver,
|
|
54
60
|
)
|
|
55
61
|
try:
|
|
56
|
-
|
|
62
|
+
if min_vol:
|
|
63
|
+
ef.min_volatility()
|
|
64
|
+
else:
|
|
65
|
+
ef.max_sharpe(risk_free_rate=rfr)
|
|
57
66
|
return ef.clean_weights()
|
|
58
67
|
except Exception as e:
|
|
59
68
|
print(f"Solver {solver} failed with error: {e}")
|
|
69
|
+
# Default to equal weighted if all solvers fail
|
|
70
|
+
return equal_weighted(prices=prices)
|
|
60
71
|
|
|
61
72
|
|
|
62
73
|
def hierarchical_risk_parity(prices=None, returns=None, freq=252):
|
|
@@ -140,7 +151,66 @@ def equal_weighted(prices=None, returns=None, round_digits=5):
|
|
|
140
151
|
return {col: round(1 / n, round_digits) for col in columns}
|
|
141
152
|
|
|
142
153
|
|
|
143
|
-
def
|
|
154
|
+
def black_litterman_weights(
|
|
155
|
+
prices=None,
|
|
156
|
+
rfr=0.0,
|
|
157
|
+
freq=252,
|
|
158
|
+
views=None,
|
|
159
|
+
view_confidences=None,
|
|
160
|
+
pi=None,
|
|
161
|
+
market_caps=None,
|
|
162
|
+
):
|
|
163
|
+
"""
|
|
164
|
+
Computes portfolio weights using the Black-Litterman model.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
prices : pd.DataFrame
|
|
169
|
+
Price data for assets.
|
|
170
|
+
rfr : float, optional
|
|
171
|
+
Risk-free rate (default is 0.0).
|
|
172
|
+
freq : int, optional
|
|
173
|
+
Frequency of the data (default is 252).
|
|
174
|
+
views : dict, optional
|
|
175
|
+
Investor's views on asset returns.
|
|
176
|
+
view_confidences : list or np.array, optional
|
|
177
|
+
Confidence levels for each view.
|
|
178
|
+
pi : pd.Series, optional
|
|
179
|
+
Market-implied prior returns.
|
|
180
|
+
market_caps : pd.Series, optional
|
|
181
|
+
Market capitalization of assets.
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
dict
|
|
186
|
+
Optimal asset weights based on the Black-Litterman model.
|
|
187
|
+
"""
|
|
188
|
+
cov_matrix = risk_models.sample_cov(prices, frequency=freq)
|
|
189
|
+
if pi is None:
|
|
190
|
+
if market_caps is not None:
|
|
191
|
+
# If market caps are provided, we can use them to compute the prior
|
|
192
|
+
# This requires a benchmark, which we don't have here easily.
|
|
193
|
+
# For simplicity, we use the mean historical return if pi is not provided.
|
|
194
|
+
pi = expected_returns.mean_historical_return(prices, frequency=freq)
|
|
195
|
+
else:
|
|
196
|
+
pi = expected_returns.mean_historical_return(prices, frequency=freq)
|
|
197
|
+
|
|
198
|
+
bl = BlackLittermanModel(
|
|
199
|
+
cov_matrix,
|
|
200
|
+
pi=pi,
|
|
201
|
+
absolute_views=views,
|
|
202
|
+
omega=None,
|
|
203
|
+
view_confidences=view_confidences,
|
|
204
|
+
)
|
|
205
|
+
ret_bl = bl.bl_returns()
|
|
206
|
+
ef = EfficientFrontier(ret_bl, cov_matrix)
|
|
207
|
+
ef.max_sharpe(risk_free_rate=rfr)
|
|
208
|
+
return ef.clean_weights()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def optimized_weights(
|
|
212
|
+
prices=None, returns=None, rfr=0.0, freq=252, method="equal", **kwargs
|
|
213
|
+
):
|
|
144
214
|
"""
|
|
145
215
|
Selects an optimization method to calculate portfolio weights based on user preference.
|
|
146
216
|
|
|
@@ -174,8 +244,12 @@ def optimized_weights(prices=None, returns=None, rfr=0.0, freq=252, method="equa
|
|
|
174
244
|
"""
|
|
175
245
|
if method == "markowitz":
|
|
176
246
|
return markowitz_weights(prices=prices, rfr=rfr, freq=freq)
|
|
247
|
+
elif method == "min_vol":
|
|
248
|
+
return markowitz_weights(prices=prices, rfr=rfr, freq=freq, min_vol=True)
|
|
177
249
|
elif method == "hrp":
|
|
178
250
|
return hierarchical_risk_parity(prices=prices, returns=returns, freq=freq)
|
|
251
|
+
elif method == "black_litterman":
|
|
252
|
+
return black_litterman_weights(prices=prices, rfr=rfr, freq=freq, **kwargs)
|
|
179
253
|
elif method == "equal":
|
|
180
254
|
return equal_weighted(prices=prices, returns=returns)
|
|
181
255
|
else:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from bbstrader.btengine.performance import (
|
|
6
|
+
calculate_risk_metrics,
|
|
7
|
+
create_calmar_ratio,
|
|
8
|
+
create_drawdowns,
|
|
9
|
+
create_omega_ratio,
|
|
10
|
+
create_sharpe_ratio,
|
|
11
|
+
create_sortino_ratio,
|
|
12
|
+
create_tail_ratio,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def sample_returns():
|
|
18
|
+
dates = pd.date_range("2020-01-01", periods=100)
|
|
19
|
+
returns = pd.Series(np.random.normal(0.001, 0.02, 100), index=dates)
|
|
20
|
+
return returns
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def sample_benchmark():
|
|
25
|
+
dates = pd.date_range("2020-01-01", periods=100)
|
|
26
|
+
returns = pd.Series(np.random.normal(0.0005, 0.015, 100), index=dates)
|
|
27
|
+
return returns
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_create_drawdowns(sample_returns):
|
|
31
|
+
drawdown, max_dd, max_duration = create_drawdowns(sample_returns)
|
|
32
|
+
assert isinstance(drawdown, pd.Series)
|
|
33
|
+
assert isinstance(max_dd, float)
|
|
34
|
+
assert isinstance(max_duration, (float, int))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_create_drawdowns_empty():
|
|
38
|
+
empty_returns = pd.Series([], dtype=float)
|
|
39
|
+
drawdown, max_dd, max_duration = create_drawdowns(empty_returns)
|
|
40
|
+
assert drawdown.empty
|
|
41
|
+
assert max_dd == 0.0
|
|
42
|
+
assert max_duration == 0.0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_ratios(sample_returns):
|
|
46
|
+
assert isinstance(create_sharpe_ratio(sample_returns), (float, np.float64))
|
|
47
|
+
assert isinstance(create_sortino_ratio(sample_returns), (float, np.float64))
|
|
48
|
+
assert isinstance(create_omega_ratio(sample_returns), (float, np.float64))
|
|
49
|
+
assert isinstance(create_calmar_ratio(sample_returns), (float, np.float64))
|
|
50
|
+
assert isinstance(create_tail_ratio(sample_returns), (float, np.float64))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_risk_metrics(sample_returns, sample_benchmark):
|
|
54
|
+
metrics = calculate_risk_metrics(sample_returns, sample_benchmark)
|
|
55
|
+
assert isinstance(metrics, dict)
|
|
56
|
+
assert "alpha" in metrics
|
|
57
|
+
assert "beta" in metrics
|
|
58
|
+
assert "volatility" in metrics
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
from bbstrader.models.optimization import (
|
|
5
|
+
markowitz_weights,
|
|
6
|
+
hierarchical_risk_parity,
|
|
7
|
+
equal_weighted,
|
|
8
|
+
optimized_weights,
|
|
9
|
+
black_litterman_weights,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def sample_prices():
|
|
15
|
+
dates = pd.date_range("2020-01-01", periods=100)
|
|
16
|
+
data = {
|
|
17
|
+
"AAPL": np.linspace(100, 150, 100) + np.random.normal(0, 2, 100),
|
|
18
|
+
"MSFT": np.linspace(200, 250, 100) + np.random.normal(0, 2, 100),
|
|
19
|
+
"GOOG": np.linspace(1000, 1100, 100) + np.random.normal(0, 5, 100),
|
|
20
|
+
}
|
|
21
|
+
return pd.DataFrame(data, index=dates)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_markowitz_weights(sample_prices):
|
|
25
|
+
weights = markowitz_weights(sample_prices)
|
|
26
|
+
assert isinstance(weights, dict)
|
|
27
|
+
assert len(weights) == 3
|
|
28
|
+
assert np.isclose(sum(weights.values()), 1.0)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_markowitz_min_vol(sample_prices):
|
|
32
|
+
weights = markowitz_weights(sample_prices, min_vol=True)
|
|
33
|
+
assert isinstance(weights, dict)
|
|
34
|
+
assert len(weights) == 3
|
|
35
|
+
assert np.isclose(sum(weights.values()), 1.0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_hrp_weights(sample_prices):
|
|
39
|
+
weights = hierarchical_risk_parity(prices=sample_prices)
|
|
40
|
+
assert isinstance(weights, dict)
|
|
41
|
+
assert len(weights) == 3
|
|
42
|
+
assert np.isclose(sum(weights.values()), 1.0)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_equal_weighted(sample_prices):
|
|
46
|
+
weights = equal_weighted(prices=sample_prices)
|
|
47
|
+
assert isinstance(weights, dict)
|
|
48
|
+
assert len(weights) == 3
|
|
49
|
+
assert all(np.isclose(w, 1 / 3) for w in weights.values())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_black_litterman_weights(sample_prices):
|
|
53
|
+
views = {"AAPL": 0.05, "MSFT": 0.02}
|
|
54
|
+
weights = black_litterman_weights(sample_prices, views=views)
|
|
55
|
+
assert isinstance(weights, dict)
|
|
56
|
+
assert len(weights) == 3
|
|
57
|
+
assert np.isclose(sum(weights.values()), 1.0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_optimized_weights_methods(sample_prices):
|
|
61
|
+
methods = ["markowitz", "min_vol", "hrp", "equal", "black_litterman"]
|
|
62
|
+
for method in methods:
|
|
63
|
+
kwargs = {}
|
|
64
|
+
if method == "black_litterman":
|
|
65
|
+
kwargs = {"views": {"AAPL": 0.05}}
|
|
66
|
+
weights = optimized_weights(prices=sample_prices, method=method, **kwargs)
|
|
67
|
+
assert isinstance(weights, dict)
|
|
68
|
+
assert np.isclose(sum(weights.values()), 1.0)
|
bbstrader-2.0.7/VERSION.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.0.7
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|