bbstrader 0.1.9__py3-none-any.whl → 0.1.92__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.

@@ -33,61 +33,8 @@ __all__ = [
33
33
  "InternalFailTimeout",
34
34
  "trade_retcode_message",
35
35
  "raise_mt5_error",
36
- "config_logger",
37
36
  ]
38
37
 
39
- def config_logger(log_file: str, console_log=True):
40
- # Configure the logger
41
- logger = logging.getLogger(__name__)
42
- logger.setLevel(logging.DEBUG)
43
-
44
- # File handler
45
- file_handler = logging.FileHandler(log_file)
46
- file_handler.setLevel(logging.INFO)
47
-
48
- # Formatter
49
- formatter = logging.Formatter(
50
- '%(asctime)s - %(levelname)s: %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
51
- file_handler.setFormatter(formatter)
52
-
53
- # Add the handler to the logger
54
- logger.addHandler(file_handler)
55
-
56
- if console_log:
57
- # handler for the console with a different level
58
- console_handler = logging.StreamHandler()
59
- console_handler.setLevel(logging.DEBUG)
60
- console_handler.setFormatter(formatter)
61
- logger.addHandler(console_handler)
62
-
63
- return logger
64
-
65
-
66
- class LogLevelFilter(logging.Filter):
67
- def __init__(self, levels: List[int]):
68
- """
69
- Initializes the filter with specific logging levels.
70
-
71
- Args:
72
- levels: A list of logging level values (integers) to include.
73
- """
74
- super().__init__()
75
- self.levels = levels
76
-
77
- def filter(self, record: logging.LogRecord) -> bool:
78
- """
79
- Filters log records based on their level.
80
-
81
- Args:
82
- record: The log record to check.
83
-
84
- Returns:
85
- True if the record's level is in the allowed levels, False otherwise.
86
- """
87
- return record.levelno in self.levels
88
-
89
-
90
-
91
38
  # TIMEFRAME is an enumeration with possible chart period values
92
39
  # See https://www.mql5.com/en/docs/python_metatrader5/mt5copyratesfrom_py#timeframe
93
40
  TIMEFRAMES = {
@@ -141,6 +88,7 @@ class TimeFrame(Enum):
141
88
  W1 = "W1"
142
89
  MN1 = "MN1"
143
90
 
91
+
144
92
  class TerminalInfo(NamedTuple):
145
93
  """
146
94
  Represents general information about the trading terminal.
@@ -470,6 +418,7 @@ class TradeDeal(NamedTuple):
470
418
  comment: str
471
419
  external_id: str
472
420
 
421
+
473
422
  class InvalidBroker(Exception):
474
423
  """Exception raised for invalid broker errors."""
475
424
  def __init__(self, message="Invalid broker."):
File without changes
bbstrader/models/ml.py ADDED
File without changes
@@ -0,0 +1,170 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from pypfopt import risk_models
4
+ from pypfopt import expected_returns
5
+ from pypfopt.efficient_frontier import EfficientFrontier
6
+ from pypfopt.hierarchical_portfolio import HRPOpt
7
+ import warnings
8
+
9
+ def markowitz_weights(prices=None, freq=252):
10
+ """
11
+ Calculates optimal portfolio weights using Markowitz's mean-variance optimization (Max Sharpe Ratio) with multiple solvers.
12
+
13
+ Parameters:
14
+ ----------
15
+ prices : pd.DataFrame, optional
16
+ Price data for assets, where rows represent time periods and columns represent assets.
17
+ freq : int, optional
18
+ Frequency of the data, such as 252 for daily returns in a year (default is 252).
19
+
20
+ Returns:
21
+ -------
22
+ dict
23
+ Dictionary containing the optimal asset weights for maximizing the Sharpe ratio, normalized to sum to 1.
24
+
25
+ Notes:
26
+ -----
27
+ This function attempts to maximize the Sharpe ratio by iterating through various solvers ('SCS', 'ECOS', 'OSQP')
28
+ from the PyPortfolioOpt library. If a solver fails, it proceeds to the next one. If none succeed, an error message
29
+ is printed for each solver that fails.
30
+
31
+ This function is useful for portfolio with a small number of assets, as it may not scale well for large portfolios.
32
+
33
+ Raises:
34
+ ------
35
+ Exception
36
+ If all solvers fail, each will print an exception error message during runtime.
37
+ """
38
+ returns = expected_returns.mean_historical_return(prices, frequency=freq)
39
+ cov = risk_models.sample_cov(prices, frequency=freq)
40
+
41
+ # Try different solvers to maximize Sharpe ratio
42
+ for solver in ['SCS', 'ECOS', 'OSQP']:
43
+ ef = EfficientFrontier(expected_returns=returns,
44
+ cov_matrix=cov,
45
+ weight_bounds=(0, 1),
46
+ solver=solver)
47
+ try:
48
+ weights = ef.max_sharpe()
49
+ return ef.clean_weights()
50
+ except Exception as e:
51
+ print(f"Solver {solver} failed with error: {e}")
52
+
53
+ def hierarchical_risk_parity(prices=None, returns=None, freq=252):
54
+ """
55
+ Computes asset weights using Hierarchical Risk Parity (HRP) for risk-averse portfolio allocation.
56
+
57
+ Parameters:
58
+ ----------
59
+ prices : pd.DataFrame, optional
60
+ Price data for assets; if provided, daily returns will be calculated.
61
+ returns : pd.DataFrame, optional
62
+ Daily returns for assets. One of `prices` or `returns` must be provided.
63
+ freq : int, optional
64
+ Number of days to consider in calculating portfolio weights (default is 252).
65
+
66
+ Returns:
67
+ -------
68
+ dict
69
+ Optimized asset weights using the HRP method, with asset weights summing to 1.
70
+
71
+ Raises:
72
+ ------
73
+ ValueError
74
+ If neither `prices` nor `returns` are provided.
75
+
76
+ Notes:
77
+ -----
78
+ Hierarchical Risk Parity is particularly useful for portfolios with a large number of assets,
79
+ as it mitigates issues of multicollinearity and estimation errors in covariance matrices by
80
+ using hierarchical clustering.
81
+ """
82
+ warnings.filterwarnings("ignore")
83
+ if returns is None and prices is None:
84
+ raise ValueError("Either prices or returns must be provided")
85
+ if returns is None:
86
+ returns = prices.pct_change().dropna()
87
+ # Remove duplicate columns and index
88
+ returns = returns.loc[:, ~returns.columns.duplicated()]
89
+ returns = returns.loc[~returns.index.duplicated(keep='first')]
90
+ hrp = HRPOpt(returns=returns.iloc[-freq:])
91
+ return hrp.optimize()
92
+
93
+ def equal_weighted(prices=None, returns=None, round_digits=5):
94
+ """
95
+ Generates an equal-weighted portfolio by assigning an equal proportion to each asset.
96
+
97
+ Parameters:
98
+ ----------
99
+ prices : pd.DataFrame, optional
100
+ Price data for assets, where each column represents an asset.
101
+ returns : pd.DataFrame, optional
102
+ Return data for assets. One of `prices` or `returns` must be provided.
103
+ round_digits : int, optional
104
+ Number of decimal places to round each weight to (default is 5).
105
+
106
+ Returns:
107
+ -------
108
+ dict
109
+ Dictionary with equal weights assigned to each asset, summing to 1.
110
+
111
+ Raises:
112
+ ------
113
+ ValueError
114
+ If neither `prices` nor `returns` are provided.
115
+
116
+ Notes:
117
+ -----
118
+ Equal weighting is a simple allocation method that assumes equal importance across all assets,
119
+ useful as a baseline model and when no strong views exist on asset return expectations or risk.
120
+ """
121
+ if returns is None and prices is None:
122
+ raise ValueError("Either prices or returns must be provided")
123
+ if returns is None:
124
+ n = len(prices.columns)
125
+ columns = prices.columns
126
+ else:
127
+ n = len(returns.columns)
128
+ columns = returns.columns
129
+ return {col: round(1/n, round_digits) for col in columns}
130
+
131
+ def optimized_weights(prices=None, returns=None, freq=252, method='markowitz'):
132
+ """
133
+ Selects an optimization method to calculate portfolio weights based on user preference.
134
+
135
+ Parameters:
136
+ ----------
137
+ prices : pd.DataFrame, optional
138
+ Price data for assets, required for certain methods.
139
+ returns : pd.DataFrame, optional
140
+ Returns data for assets, an alternative input for certain methods.
141
+ freq : int, optional
142
+ Number of days for calculating portfolio weights, such as 252 for a year's worth of daily returns (default is 252).
143
+ method : str, optional
144
+ Optimization method to use ('markowitz', 'hrp', or 'equal') (default is 'markowitz').
145
+
146
+ Returns:
147
+ -------
148
+ dict
149
+ Dictionary containing optimized asset weights based on the chosen method.
150
+
151
+ Raises:
152
+ ------
153
+ ValueError
154
+ If an unknown optimization method is specified.
155
+
156
+ Notes:
157
+ -----
158
+ This function integrates different optimization methods:
159
+ - 'markowitz': mean-variance optimization with max Sharpe ratio
160
+ - 'hrp': Hierarchical Risk Parity, for risk-based clustering of assets
161
+ - 'equal': Equal weighting across all assets
162
+ """
163
+ if method == 'markowitz':
164
+ return markowitz_weights(prices=prices, freq=freq)
165
+ elif method == 'hrp':
166
+ return hierarchical_risk_parity(prices=prices, returns=returns, freq=freq)
167
+ elif method == 'equal':
168
+ return equal_weighted(prices=prices, returns=returns)
169
+ else:
170
+ raise ValueError(f"Unknown method: {method}")
@@ -0,0 +1,202 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import seaborn as sns
4
+ import matplotlib.pyplot as plt
5
+ from sklearn.decomposition import PCA
6
+ from sklearn.preprocessing import scale
7
+ from bbstrader.models.optimization import (
8
+ markowitz_weights,
9
+ hierarchical_risk_parity,
10
+ equal_weighted
11
+ )
12
+
13
+
14
+ class EigenPortfolios(object):
15
+ """
16
+ The `EigenPortfolios` class applies Principal Component Analysis (PCA) to a covariance matrix of normalized asset returns
17
+ to derive portfolios (eigenportfolios) that capture distinct risk factors in the asset returns. Each eigenportfolio
18
+ represents a principal component of the return covariance matrix, ordered by the magnitude of its eigenvalue. These
19
+ portfolios capture most of the variance in asset returns and are mutually uncorrelated.
20
+
21
+ """
22
+ def __init__(self):
23
+ self.returns = None
24
+ self.n_portfolios = None
25
+ self._portfolios = None
26
+ self._fit_called = False
27
+
28
+ def get_portfolios(self) -> pd.DataFrame:
29
+ """
30
+ Returns the computed eigenportfolios (weights of assets in each portfolio).
31
+
32
+ Returns:
33
+ -------
34
+ pd.DataFrame
35
+ DataFrame containing eigenportfolio weights for each asset.
36
+
37
+ Raises:
38
+ ------
39
+ ValueError
40
+ If `fit()` has not been called before retrieving portfolios.
41
+ """
42
+ if not self._fit_called:
43
+ raise ValueError("fit() must be called first")
44
+ return self._portfolios
45
+
46
+ def fit(self, returns: pd.DataFrame, n_portfolios: int=4) -> pd.DataFrame:
47
+ """
48
+ Computes the eigenportfolios based on PCA of the asset returns' covariance matrix.
49
+
50
+ Parameters:
51
+ ----------
52
+ returns : pd.DataFrame
53
+ Historical returns of assets to be used for PCA.
54
+ n_portfolios : int, optional
55
+ Number of eigenportfolios to compute (default is 4).
56
+
57
+ Returns:
58
+ -------
59
+ pd.DataFrame
60
+ DataFrame containing normalized weights for each eigenportfolio.
61
+
62
+ Notes:
63
+ -----
64
+ This method performs winsorization and normalization on returns to reduce the impact of outliers
65
+ and achieve zero mean and unit variance. It uses the first `n_portfolios` principal components
66
+ as portfolio weights.
67
+ """
68
+ # Winsorize and normalize the returns
69
+ normed_returns = scale(returns
70
+ .clip(lower=returns.quantile(q=.025),
71
+ upper=returns.quantile(q=.975),
72
+ axis=1)
73
+ .apply(lambda x: x.sub(x.mean()).div(x.std())))
74
+ returns = returns.dropna(thresh=int(normed_returns.shape[0] * .95), axis=1)
75
+ returns = returns.dropna(thresh=int(normed_returns.shape[1] * .95))
76
+
77
+ cov = returns.cov()
78
+ cov.columns = cov.columns.astype(str)
79
+ pca = PCA()
80
+ pca.fit(cov)
81
+
82
+ top_portfolios = pd.DataFrame(pca.components_[:n_portfolios], columns=cov.columns)
83
+ eigen_portfolios = top_portfolios.div(top_portfolios.sum(axis=1), axis=0)
84
+ eigen_portfolios.index = [f"Portfolio {i}" for i in range(1, n_portfolios + 1)]
85
+ self._portfolios = eigen_portfolios
86
+ self.returns = returns
87
+ self.n_portfolios = n_portfolios
88
+ self._fit_called = True
89
+
90
+ def plot_weights(self):
91
+ """
92
+ Plots the weights of each asset in each eigenportfolio as bar charts.
93
+
94
+ Notes:
95
+ -----
96
+ Each subplot represents one eigenportfolio, showing the contribution of each asset.
97
+ """
98
+ eigen_portfolios = self.get_portfolios()
99
+ n_cols = 2
100
+ n_rows = (self.n_portfolios + 1) // n_cols
101
+ figsize = (n_cols * 10, n_rows * 5)
102
+ axes = eigen_portfolios.T.plot.bar(subplots=True,
103
+ layout=(n_rows, n_cols),
104
+ figsize=figsize,
105
+ legend=False)
106
+ for ax in axes.flatten():
107
+ ax.set_ylabel('Portfolio Weight')
108
+ ax.set_xlabel('')
109
+
110
+ sns.despine()
111
+ plt.tight_layout()
112
+ plt.show()
113
+
114
+ def plot_performance(self):
115
+ """
116
+ Plots the cumulative returns of each eigenportfolio over time.
117
+
118
+ Notes:
119
+ -----
120
+ This method calculates the historical cumulative performance of each eigenportfolio
121
+ by weighting asset returns according to eigenportfolio weights.
122
+ """
123
+ eigen_portfolios = self.get_portfolios()
124
+ returns = self.returns.copy()
125
+
126
+ n_cols = 2
127
+ n_rows = (self.n_portfolios + 1 + n_cols - 1) // n_cols
128
+ figsize = (n_cols * 10, n_rows * 5)
129
+ fig, axes = plt.subplots(nrows=n_rows, ncols=n_cols,
130
+ figsize=figsize, sharex=True)
131
+ axes = axes.flatten()
132
+ returns.mean(1).add(1).cumprod().sub(1).plot(title='The Market', ax=axes[0])
133
+
134
+ for i in range(self.n_portfolios):
135
+ rc = returns.mul(eigen_portfolios.iloc[i]).sum(1).add(1).cumprod().sub(1)
136
+ rc.plot(title=f'Portfolio {i+1}', ax=axes[i + 1], lw=1, rot=0)
137
+
138
+ for j in range(self.n_portfolios + 1, len(axes)):
139
+ fig.delaxes(axes[j])
140
+
141
+ for i in range(self.n_portfolios + 1):
142
+ axes[i].set_xlabel('')
143
+
144
+ sns.despine()
145
+ fig.tight_layout()
146
+ plt.show()
147
+
148
+ def optimize(self, portfolio: int = 1, optimizer: str = 'hrp', prices=None, freq=252, plot=True):
149
+ """
150
+ Optimizes the chosen eigenportfolio based on a specified optimization method.
151
+
152
+ Parameters:
153
+ ----------
154
+ portfolio : int, optional
155
+ Index of the eigenportfolio to optimize (default is 1).
156
+ optimizer : str, optional
157
+ Optimization method: 'markowitz', 'hrp' (Hierarchical Risk Parity), or 'equal' (default is 'hrp').
158
+ prices : pd.DataFrame, optional
159
+ Asset prices used for Markowitz optimization (required if optimizer is 'markowitz').
160
+ freq : int, optional
161
+ Frequency of returns (e.g., 252 for daily returns).
162
+ plot : bool, optional
163
+ Whether to plot the performance of the optimized portfolio (default is True).
164
+
165
+ Returns:
166
+ -------
167
+ dict
168
+ Dictionary of optimized asset weights.
169
+
170
+ Raises:
171
+ ------
172
+ ValueError
173
+ If an unknown optimizer is specified, or if prices are not provided when using Markowitz optimization.
174
+
175
+ Notes:
176
+ -----
177
+ The optimization method varies based on risk-return assumptions, with options for traditional Markowitz optimization,
178
+ Hierarchical Risk Parity, or equal weighting.
179
+ """
180
+ portfolio = self.get_portfolios().iloc[portfolio - 1]
181
+ returns = self.returns.loc[:, portfolio.index]
182
+ returns = returns.loc[:, ~returns.columns.duplicated()]
183
+ returns = returns.loc[~returns.index.duplicated(keep='first')]
184
+ if optimizer == 'markowitz':
185
+ if prices is None:
186
+ raise ValueError("prices must be provided for markowitz optimization")
187
+ prices = prices.loc[:, returns.columns]
188
+ weights = markowitz_weights(prices=prices, freq=freq)
189
+ elif optimizer == 'hrp':
190
+ weights = hierarchical_risk_parity(returns=returns, freq=freq)
191
+ elif optimizer == 'equal':
192
+ weights = equal_weighted(returns=returns)
193
+ else:
194
+ raise ValueError(f"Unknown optimizer: {optimizer}")
195
+ if plot:
196
+ # plot the optimized potfolio performance
197
+ returns = returns.filter(weights.keys())
198
+ rc = returns.mul(weights).sum(1).add(1).cumprod().sub(1)
199
+ rc.plot(title=f'Optimized {portfolio.name}', lw=1, rot=0)
200
+ sns.despine()
201
+ plt.show()
202
+ return weights
@@ -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 ExecutionEngine
10
+ from bbstrader.trading.execution import *
11
11
  from bbstrader.trading.strategies import *