bbstrader 0.1.91__py3-none-any.whl → 0.1.93__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/btengine/backtest.py +26 -13
- bbstrader/btengine/data.py +10 -2
- bbstrader/btengine/event.py +10 -4
- bbstrader/btengine/execution.py +8 -7
- bbstrader/btengine/performance.py +2 -0
- bbstrader/btengine/portfolio.py +50 -28
- bbstrader/btengine/strategy.py +223 -69
- bbstrader/metatrader/account.py +1 -1
- bbstrader/metatrader/rates.py +50 -2
- bbstrader/metatrader/risk.py +28 -3
- bbstrader/metatrader/trade.py +7 -4
- bbstrader/models/__init__.py +5 -1
- bbstrader/models/optimization.py +177 -0
- bbstrader/models/portfolios.py +205 -0
- bbstrader/trading/execution.py +31 -16
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.93.dist-info}/METADATA +2 -1
- bbstrader-0.1.93.dist-info/RECORD +32 -0
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.93.dist-info}/WHEEL +1 -1
- bbstrader-0.1.91.dist-info/RECORD +0 -31
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.93.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.93.dist-info}/top_level.txt +0 -0
bbstrader/models/optimization.py
CHANGED
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
__all__ = [
|
|
10
|
+
'markowitz_weights',
|
|
11
|
+
'hierarchical_risk_parity',
|
|
12
|
+
'equal_weighted',
|
|
13
|
+
'optimized_weights'
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
def markowitz_weights(prices=None, rfr=0.0, freq=252):
|
|
17
|
+
"""
|
|
18
|
+
Calculates optimal portfolio weights using Markowitz's mean-variance optimization (Max Sharpe Ratio) with multiple solvers.
|
|
19
|
+
|
|
20
|
+
Parameters:
|
|
21
|
+
----------
|
|
22
|
+
prices : pd.DataFrame, optional
|
|
23
|
+
Price data for assets, where rows represent time periods and columns represent assets.
|
|
24
|
+
freq : int, optional
|
|
25
|
+
Frequency of the data, such as 252 for daily returns in a year (default is 252).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
-------
|
|
29
|
+
dict
|
|
30
|
+
Dictionary containing the optimal asset weights for maximizing the Sharpe ratio, normalized to sum to 1.
|
|
31
|
+
|
|
32
|
+
Notes:
|
|
33
|
+
-----
|
|
34
|
+
This function attempts to maximize the Sharpe ratio by iterating through various solvers ('SCS', 'ECOS', 'OSQP')
|
|
35
|
+
from the PyPortfolioOpt library. If a solver fails, it proceeds to the next one. If none succeed, an error message
|
|
36
|
+
is printed for each solver that fails.
|
|
37
|
+
|
|
38
|
+
This function is useful for portfolio with a small number of assets, as it may not scale well for large portfolios.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
------
|
|
42
|
+
Exception
|
|
43
|
+
If all solvers fail, each will print an exception error message during runtime.
|
|
44
|
+
"""
|
|
45
|
+
returns = expected_returns.mean_historical_return(prices, frequency=freq)
|
|
46
|
+
cov = risk_models.sample_cov(prices, frequency=freq)
|
|
47
|
+
|
|
48
|
+
# Try different solvers to maximize Sharpe ratio
|
|
49
|
+
for solver in ['SCS', 'ECOS', 'OSQP']:
|
|
50
|
+
ef = EfficientFrontier(expected_returns=returns,
|
|
51
|
+
cov_matrix=cov,
|
|
52
|
+
weight_bounds=(0, 1),
|
|
53
|
+
solver=solver)
|
|
54
|
+
try:
|
|
55
|
+
weights = ef.max_sharpe(risk_free_rate=rfr)
|
|
56
|
+
return ef.clean_weights()
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"Solver {solver} failed with error: {e}")
|
|
59
|
+
|
|
60
|
+
def hierarchical_risk_parity(prices=None, returns=None, freq=252):
|
|
61
|
+
"""
|
|
62
|
+
Computes asset weights using Hierarchical Risk Parity (HRP) for risk-averse portfolio allocation.
|
|
63
|
+
|
|
64
|
+
Parameters:
|
|
65
|
+
----------
|
|
66
|
+
prices : pd.DataFrame, optional
|
|
67
|
+
Price data for assets; if provided, daily returns will be calculated.
|
|
68
|
+
returns : pd.DataFrame, optional
|
|
69
|
+
Daily returns for assets. One of `prices` or `returns` must be provided.
|
|
70
|
+
freq : int, optional
|
|
71
|
+
Number of days to consider in calculating portfolio weights (default is 252).
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
-------
|
|
75
|
+
dict
|
|
76
|
+
Optimized asset weights using the HRP method, with asset weights summing to 1.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
------
|
|
80
|
+
ValueError
|
|
81
|
+
If neither `prices` nor `returns` are provided.
|
|
82
|
+
|
|
83
|
+
Notes:
|
|
84
|
+
-----
|
|
85
|
+
Hierarchical Risk Parity is particularly useful for portfolios with a large number of assets,
|
|
86
|
+
as it mitigates issues of multicollinearity and estimation errors in covariance matrices by
|
|
87
|
+
using hierarchical clustering.
|
|
88
|
+
"""
|
|
89
|
+
warnings.filterwarnings("ignore")
|
|
90
|
+
if returns is None and prices is None:
|
|
91
|
+
raise ValueError("Either prices or returns must be provided")
|
|
92
|
+
if returns is None:
|
|
93
|
+
returns = prices.pct_change().dropna(how='all')
|
|
94
|
+
# Remove duplicate columns and index
|
|
95
|
+
returns = returns.loc[:, ~returns.columns.duplicated()]
|
|
96
|
+
returns = returns.loc[~returns.index.duplicated(keep='first')]
|
|
97
|
+
hrp = HRPOpt(returns=returns.iloc[-freq:])
|
|
98
|
+
return hrp.optimize()
|
|
99
|
+
|
|
100
|
+
def equal_weighted(prices=None, returns=None, round_digits=5):
|
|
101
|
+
"""
|
|
102
|
+
Generates an equal-weighted portfolio by assigning an equal proportion to each asset.
|
|
103
|
+
|
|
104
|
+
Parameters:
|
|
105
|
+
----------
|
|
106
|
+
prices : pd.DataFrame, optional
|
|
107
|
+
Price data for assets, where each column represents an asset.
|
|
108
|
+
returns : pd.DataFrame, optional
|
|
109
|
+
Return data for assets. One of `prices` or `returns` must be provided.
|
|
110
|
+
round_digits : int, optional
|
|
111
|
+
Number of decimal places to round each weight to (default is 5).
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
-------
|
|
115
|
+
dict
|
|
116
|
+
Dictionary with equal weights assigned to each asset, summing to 1.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
------
|
|
120
|
+
ValueError
|
|
121
|
+
If neither `prices` nor `returns` are provided.
|
|
122
|
+
|
|
123
|
+
Notes:
|
|
124
|
+
-----
|
|
125
|
+
Equal weighting is a simple allocation method that assumes equal importance across all assets,
|
|
126
|
+
useful as a baseline model and when no strong views exist on asset return expectations or risk.
|
|
127
|
+
"""
|
|
128
|
+
if returns is None and prices is None:
|
|
129
|
+
raise ValueError("Either prices or returns must be provided")
|
|
130
|
+
if returns is None:
|
|
131
|
+
n = len(prices.columns)
|
|
132
|
+
columns = prices.columns
|
|
133
|
+
else:
|
|
134
|
+
n = len(returns.columns)
|
|
135
|
+
columns = returns.columns
|
|
136
|
+
return {col: round(1/n, round_digits) for col in columns}
|
|
137
|
+
|
|
138
|
+
def optimized_weights(prices=None, returns=None, rfr=0.0, freq=252, method='equal'):
|
|
139
|
+
"""
|
|
140
|
+
Selects an optimization method to calculate portfolio weights based on user preference.
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
----------
|
|
144
|
+
prices : pd.DataFrame, optional
|
|
145
|
+
Price data for assets, required for certain methods.
|
|
146
|
+
returns : pd.DataFrame, optional
|
|
147
|
+
Returns data for assets, an alternative input for certain methods.
|
|
148
|
+
freq : int, optional
|
|
149
|
+
Number of days for calculating portfolio weights, such as 252 for a year's worth of daily returns (default is 252).
|
|
150
|
+
method : str, optional
|
|
151
|
+
Optimization method to use ('markowitz', 'hrp', or 'equal') (default is 'markowitz').
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
-------
|
|
155
|
+
dict
|
|
156
|
+
Dictionary containing optimized asset weights based on the chosen method.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
------
|
|
160
|
+
ValueError
|
|
161
|
+
If an unknown optimization method is specified.
|
|
162
|
+
|
|
163
|
+
Notes:
|
|
164
|
+
-----
|
|
165
|
+
This function integrates different optimization methods:
|
|
166
|
+
- 'markowitz': mean-variance optimization with max Sharpe ratio
|
|
167
|
+
- 'hrp': Hierarchical Risk Parity, for risk-based clustering of assets
|
|
168
|
+
- 'equal': Equal weighting across all assets
|
|
169
|
+
"""
|
|
170
|
+
if method == 'markowitz':
|
|
171
|
+
return markowitz_weights(prices=prices, rfr=rfr, freq=freq)
|
|
172
|
+
elif method == 'hrp':
|
|
173
|
+
return hierarchical_risk_parity(prices=prices, returns=returns, freq=freq)
|
|
174
|
+
elif method == 'equal':
|
|
175
|
+
return equal_weighted(prices=prices, returns=returns)
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError(f"Unknown method: {method}")
|
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
__all__ = [
|
|
14
|
+
'EigenPortfolios'
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
class EigenPortfolios(object):
|
|
18
|
+
"""
|
|
19
|
+
The `EigenPortfolios` class applies Principal Component Analysis (PCA) to a covariance matrix of normalized asset returns
|
|
20
|
+
to derive portfolios (eigenportfolios) that capture distinct risk factors in the asset returns. Each eigenportfolio
|
|
21
|
+
represents a principal component of the return covariance matrix, ordered by the magnitude of its eigenvalue. These
|
|
22
|
+
portfolios capture most of the variance in asset returns and are mutually uncorrelated.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.returns = None
|
|
27
|
+
self.n_portfolios = None
|
|
28
|
+
self._portfolios = None
|
|
29
|
+
self._fit_called = False
|
|
30
|
+
|
|
31
|
+
def get_portfolios(self) -> pd.DataFrame:
|
|
32
|
+
"""
|
|
33
|
+
Returns the computed eigenportfolios (weights of assets in each portfolio).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
-------
|
|
37
|
+
pd.DataFrame
|
|
38
|
+
DataFrame containing eigenportfolio weights for each asset.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
------
|
|
42
|
+
ValueError
|
|
43
|
+
If `fit()` has not been called before retrieving portfolios.
|
|
44
|
+
"""
|
|
45
|
+
if not self._fit_called:
|
|
46
|
+
raise ValueError("fit() must be called first")
|
|
47
|
+
return self._portfolios
|
|
48
|
+
|
|
49
|
+
def fit(self, returns: pd.DataFrame, n_portfolios: int=4) -> pd.DataFrame:
|
|
50
|
+
"""
|
|
51
|
+
Computes the eigenportfolios based on PCA of the asset returns' covariance matrix.
|
|
52
|
+
|
|
53
|
+
Parameters:
|
|
54
|
+
----------
|
|
55
|
+
returns : pd.DataFrame
|
|
56
|
+
Historical returns of assets to be used for PCA.
|
|
57
|
+
n_portfolios : int, optional
|
|
58
|
+
Number of eigenportfolios to compute (default is 4).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
-------
|
|
62
|
+
pd.DataFrame
|
|
63
|
+
DataFrame containing normalized weights for each eigenportfolio.
|
|
64
|
+
|
|
65
|
+
Notes:
|
|
66
|
+
-----
|
|
67
|
+
This method performs winsorization and normalization on returns to reduce the impact of outliers
|
|
68
|
+
and achieve zero mean and unit variance. It uses the first `n_portfolios` principal components
|
|
69
|
+
as portfolio weights.
|
|
70
|
+
"""
|
|
71
|
+
# Winsorize and normalize the returns
|
|
72
|
+
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)
|
|
78
|
+
returns = returns.dropna(thresh=int(normed_returns.shape[1] * .95))
|
|
79
|
+
|
|
80
|
+
cov = returns.cov()
|
|
81
|
+
cov.columns = cov.columns.astype(str)
|
|
82
|
+
pca = PCA()
|
|
83
|
+
pca.fit(cov)
|
|
84
|
+
|
|
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)]
|
|
88
|
+
self._portfolios = eigen_portfolios
|
|
89
|
+
self.returns = returns
|
|
90
|
+
self.n_portfolios = n_portfolios
|
|
91
|
+
self._fit_called = True
|
|
92
|
+
|
|
93
|
+
def plot_weights(self):
|
|
94
|
+
"""
|
|
95
|
+
Plots the weights of each asset in each eigenportfolio as bar charts.
|
|
96
|
+
|
|
97
|
+
Notes:
|
|
98
|
+
-----
|
|
99
|
+
Each subplot represents one eigenportfolio, showing the contribution of each asset.
|
|
100
|
+
"""
|
|
101
|
+
eigen_portfolios = self.get_portfolios()
|
|
102
|
+
n_cols = 2
|
|
103
|
+
n_rows = (self.n_portfolios + 1) // n_cols
|
|
104
|
+
figsize = (n_cols * 10, n_rows * 5)
|
|
105
|
+
axes = eigen_portfolios.T.plot.bar(subplots=True,
|
|
106
|
+
layout=(n_rows, n_cols),
|
|
107
|
+
figsize=figsize,
|
|
108
|
+
legend=False)
|
|
109
|
+
for ax in axes.flatten():
|
|
110
|
+
ax.set_ylabel('Portfolio Weight')
|
|
111
|
+
ax.set_xlabel('')
|
|
112
|
+
|
|
113
|
+
sns.despine()
|
|
114
|
+
plt.tight_layout()
|
|
115
|
+
plt.show()
|
|
116
|
+
|
|
117
|
+
def plot_performance(self):
|
|
118
|
+
"""
|
|
119
|
+
Plots the cumulative returns of each eigenportfolio over time.
|
|
120
|
+
|
|
121
|
+
Notes:
|
|
122
|
+
-----
|
|
123
|
+
This method calculates the historical cumulative performance of each eigenportfolio
|
|
124
|
+
by weighting asset returns according to eigenportfolio weights.
|
|
125
|
+
"""
|
|
126
|
+
eigen_portfolios = self.get_portfolios()
|
|
127
|
+
returns = self.returns.copy()
|
|
128
|
+
|
|
129
|
+
n_cols = 2
|
|
130
|
+
n_rows = (self.n_portfolios + 1 + n_cols - 1) // n_cols
|
|
131
|
+
figsize = (n_cols * 10, n_rows * 5)
|
|
132
|
+
fig, axes = plt.subplots(nrows=n_rows, ncols=n_cols,
|
|
133
|
+
figsize=figsize, sharex=True)
|
|
134
|
+
axes = axes.flatten()
|
|
135
|
+
returns.mean(1).add(1).cumprod().sub(1).plot(title='The Market', ax=axes[0])
|
|
136
|
+
|
|
137
|
+
for i in range(self.n_portfolios):
|
|
138
|
+
rc = returns.mul(eigen_portfolios.iloc[i]).sum(1).add(1).cumprod().sub(1)
|
|
139
|
+
rc.plot(title=f'Portfolio {i+1}', ax=axes[i + 1], lw=1, rot=0)
|
|
140
|
+
|
|
141
|
+
for j in range(self.n_portfolios + 1, len(axes)):
|
|
142
|
+
fig.delaxes(axes[j])
|
|
143
|
+
|
|
144
|
+
for i in range(self.n_portfolios + 1):
|
|
145
|
+
axes[i].set_xlabel('')
|
|
146
|
+
|
|
147
|
+
sns.despine()
|
|
148
|
+
fig.tight_layout()
|
|
149
|
+
plt.show()
|
|
150
|
+
|
|
151
|
+
def optimize(self, portfolio: int = 1, optimizer: str = 'hrp', prices=None, freq=252, plot=True):
|
|
152
|
+
"""
|
|
153
|
+
Optimizes the chosen eigenportfolio based on a specified optimization method.
|
|
154
|
+
|
|
155
|
+
Parameters:
|
|
156
|
+
----------
|
|
157
|
+
portfolio : int, optional
|
|
158
|
+
Index of the eigenportfolio to optimize (default is 1).
|
|
159
|
+
optimizer : str, optional
|
|
160
|
+
Optimization method: 'markowitz', 'hrp' (Hierarchical Risk Parity), or 'equal' (default is 'hrp').
|
|
161
|
+
prices : pd.DataFrame, optional
|
|
162
|
+
Asset prices used for Markowitz optimization (required if optimizer is 'markowitz').
|
|
163
|
+
freq : int, optional
|
|
164
|
+
Frequency of returns (e.g., 252 for daily returns).
|
|
165
|
+
plot : bool, optional
|
|
166
|
+
Whether to plot the performance of the optimized portfolio (default is True).
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
-------
|
|
170
|
+
dict
|
|
171
|
+
Dictionary of optimized asset weights.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
------
|
|
175
|
+
ValueError
|
|
176
|
+
If an unknown optimizer is specified, or if prices are not provided when using Markowitz optimization.
|
|
177
|
+
|
|
178
|
+
Notes:
|
|
179
|
+
-----
|
|
180
|
+
The optimization method varies based on risk-return assumptions, with options for traditional Markowitz optimization,
|
|
181
|
+
Hierarchical Risk Parity, or equal weighting.
|
|
182
|
+
"""
|
|
183
|
+
portfolio = self.get_portfolios().iloc[portfolio - 1]
|
|
184
|
+
returns = self.returns.loc[:, portfolio.index]
|
|
185
|
+
returns = returns.loc[:, ~returns.columns.duplicated()]
|
|
186
|
+
returns = returns.loc[~returns.index.duplicated(keep='first')]
|
|
187
|
+
if optimizer == 'markowitz':
|
|
188
|
+
if prices is None:
|
|
189
|
+
raise ValueError("prices must be provided for markowitz optimization")
|
|
190
|
+
prices = prices.loc[:, returns.columns]
|
|
191
|
+
weights = markowitz_weights(prices=prices, freq=freq)
|
|
192
|
+
elif optimizer == 'hrp':
|
|
193
|
+
weights = hierarchical_risk_parity(returns=returns, freq=freq)
|
|
194
|
+
elif optimizer == 'equal':
|
|
195
|
+
weights = equal_weighted(returns=returns)
|
|
196
|
+
else:
|
|
197
|
+
raise ValueError(f"Unknown optimizer: {optimizer}")
|
|
198
|
+
if plot:
|
|
199
|
+
# plot the optimized potfolio performance
|
|
200
|
+
returns = returns.filter(weights.keys())
|
|
201
|
+
rc = returns.mul(weights).sum(1).add(1).cumprod().sub(1)
|
|
202
|
+
rc.plot(title=f'Optimized {portfolio.name}', lw=1, rot=0)
|
|
203
|
+
sns.despine()
|
|
204
|
+
plt.show()
|
|
205
|
+
return weights
|
bbstrader/trading/execution.py
CHANGED
|
@@ -78,10 +78,11 @@ EXIT_SIGNAL_ACTIONS = {
|
|
|
78
78
|
|
|
79
79
|
def _mt5_execution(
|
|
80
80
|
symbol_list, trades_instances, strategy_cls, /,
|
|
81
|
-
mm, trail, stop_trail, trail_after_points, be_plus_points, show_positions_orders,
|
|
82
|
-
|
|
81
|
+
mm, optimizer, trail, stop_trail, trail_after_points, be_plus_points, show_positions_orders,
|
|
82
|
+
iter_time, use_trade_time, period, period_end_action, closing_pnl, trading_days,
|
|
83
83
|
comment, **kwargs):
|
|
84
84
|
symbols = symbol_list.copy()
|
|
85
|
+
time_frame = kwargs.get('time_frame', '15m')
|
|
85
86
|
STRATEGY = kwargs.get('strategy_name')
|
|
86
87
|
mtrades = kwargs.get('max_trades')
|
|
87
88
|
notify = kwargs.get('notify', False)
|
|
@@ -89,8 +90,16 @@ def _mt5_execution(
|
|
|
89
90
|
telegram = kwargs.get('telegram', False)
|
|
90
91
|
bot_token = kwargs.get('bot_token')
|
|
91
92
|
chat_id = kwargs.get('chat_id')
|
|
93
|
+
|
|
94
|
+
def update_risk(weights):
|
|
95
|
+
if weights is not None:
|
|
96
|
+
for symbol in symbols:
|
|
97
|
+
if symbol not in weights:
|
|
98
|
+
continue
|
|
99
|
+
trade = trades_instances[symbol]
|
|
100
|
+
trade.dailydd = round(weights[symbol], 5)
|
|
92
101
|
|
|
93
|
-
def _send_notification(
|
|
102
|
+
def _send_notification(signal):
|
|
94
103
|
send_message(message=signal, notify_me=notify,
|
|
95
104
|
telegram=telegram, token=bot_token, chat_id=chat_id)
|
|
96
105
|
|
|
@@ -104,8 +113,6 @@ def _mt5_execution(
|
|
|
104
113
|
if not mm:
|
|
105
114
|
return
|
|
106
115
|
if buys is not None or sells is not None:
|
|
107
|
-
logger.info(
|
|
108
|
-
f"Checking for Break even, SYMBOL={symbol}...STRATEGY={STRATEGY}")
|
|
109
116
|
trades_instances[symbol].break_even(
|
|
110
117
|
mm=mm, trail=trail, stop_trail=stop_trail,
|
|
111
118
|
trail_after_points=trail_after_points, be_plus_points=be_plus_points)
|
|
@@ -119,10 +126,10 @@ def _mt5_execution(
|
|
|
119
126
|
check_mt5_connection()
|
|
120
127
|
strategy: MT5Strategy = strategy_cls(symbol_list=symbols, mode='live', **kwargs)
|
|
121
128
|
except Exception as e:
|
|
122
|
-
logger.error(f"
|
|
129
|
+
logger.error(f"Initializing strategy, {e}, STRATEGY={STRATEGY}")
|
|
123
130
|
return
|
|
124
131
|
logger.info(
|
|
125
|
-
f'Running {STRATEGY} Strategy
|
|
132
|
+
f'Running {STRATEGY} Strategy in {time_frame} Interval ...')
|
|
126
133
|
|
|
127
134
|
while True:
|
|
128
135
|
try:
|
|
@@ -132,7 +139,9 @@ def _mt5_execution(
|
|
|
132
139
|
time.sleep(0.5)
|
|
133
140
|
positions_orders = {}
|
|
134
141
|
for type in POSITIONS_TYPES + ORDERS_TYPES:
|
|
142
|
+
positions_orders[type] = {}
|
|
135
143
|
for symbol in symbols:
|
|
144
|
+
positions_orders[type][symbol] = None
|
|
136
145
|
func = getattr(trades_instances[symbol], f"get_current_{type}")
|
|
137
146
|
positions_orders[type][symbol] = func()
|
|
138
147
|
buys = positions_orders['buys']
|
|
@@ -154,12 +163,14 @@ def _mt5_execution(
|
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
except Exception as e:
|
|
157
|
-
logger.error(f"{e}, STRATEGY={STRATEGY}")
|
|
166
|
+
logger.error(f"Handling Positions and Orders, {e}, STRATEGY={STRATEGY}")
|
|
158
167
|
continue
|
|
159
168
|
time.sleep(0.5)
|
|
160
169
|
try:
|
|
161
170
|
check_mt5_connection()
|
|
162
171
|
signals = strategy.calculate_signals()
|
|
172
|
+
weights = strategy.apply_risk_management(optimizer)
|
|
173
|
+
update_risk(weights)
|
|
163
174
|
except Exception as e:
|
|
164
175
|
logger.error(f"Calculating signal, {e}, STRATEGY={STRATEGY}")
|
|
165
176
|
continue
|
|
@@ -181,9 +192,10 @@ def _mt5_execution(
|
|
|
181
192
|
if signal is not None:
|
|
182
193
|
signal = 'BMKT' if signal == 'LONG' else signal
|
|
183
194
|
signal = 'SMKT' if signal == 'SHORT' else signal
|
|
184
|
-
info = f"SIGNAL = {signal}, SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
|
|
195
|
+
info = f"SIGNAL = {signal}, SYMBOL={trade.symbol}, STRATEGY={STRATEGY}, TIMEFRAME={time_frame}"
|
|
185
196
|
msg = f"Sending {signal} Order ... SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
|
|
186
|
-
|
|
197
|
+
if signal not in EXIT_SIGNAL_ACTIONS:
|
|
198
|
+
logger.info(info)
|
|
187
199
|
if signal in EXIT_SIGNAL_ACTIONS:
|
|
188
200
|
for exit_signal, actions in EXIT_SIGNAL_ACTIONS.items():
|
|
189
201
|
for position_type, order_type in actions.items():
|
|
@@ -235,13 +247,15 @@ def _mt5_execution(
|
|
|
235
247
|
elif signal in SELLS and short_market[symbol]:
|
|
236
248
|
logger.info(riskmsg)
|
|
237
249
|
check(buys[symbol], sells[symbol], symbol)
|
|
250
|
+
else:
|
|
251
|
+
check(buys[symbol], sells[symbol], symbol)
|
|
238
252
|
else:
|
|
239
253
|
logger.info(
|
|
240
254
|
f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}")
|
|
241
255
|
check(buys[symbol], sells[symbol], symbol)
|
|
242
256
|
|
|
243
257
|
except Exception as e:
|
|
244
|
-
logger.error(f"{e}, SYMBOL={symbol}, STRATEGY={STRATEGY}")
|
|
258
|
+
logger.error(f"Handling Signals {e}, SYMBOL={symbol}, STRATEGY={STRATEGY}")
|
|
245
259
|
continue
|
|
246
260
|
time.sleep((60 * iter_time) - 1.0)
|
|
247
261
|
if iter_time == 1:
|
|
@@ -253,7 +267,6 @@ def _mt5_execution(
|
|
|
253
267
|
f"iter_time must be a multiple of the {time_frame} !!!"
|
|
254
268
|
f"(e.g; if time_frame is 15m, iter_time must be 1.5, 3, 3, 15 etc)"
|
|
255
269
|
)
|
|
256
|
-
print()
|
|
257
270
|
try:
|
|
258
271
|
FRIDAY = 'friday'
|
|
259
272
|
check_mt5_connection()
|
|
@@ -434,12 +447,12 @@ class MT5ExecutionEngine():
|
|
|
434
447
|
strategy_cls: Strategy,
|
|
435
448
|
/,
|
|
436
449
|
mm: bool = True,
|
|
450
|
+
optimizer: str = 'equal',
|
|
437
451
|
trail: bool = True,
|
|
438
452
|
stop_trail: Optional[int] = None,
|
|
439
453
|
trail_after_points: Optional[int] = None,
|
|
440
454
|
be_plus_points: Optional[int] = None,
|
|
441
455
|
show_positions_orders: bool = False,
|
|
442
|
-
time_frame: str = '15m',
|
|
443
456
|
iter_time: int | float = 5,
|
|
444
457
|
use_trade_time: bool = True,
|
|
445
458
|
period: Literal['day', 'week', 'month'] = 'week',
|
|
@@ -455,8 +468,9 @@ class MT5ExecutionEngine():
|
|
|
455
468
|
trades_instances : Dictionary of Trade instances
|
|
456
469
|
strategy_cls : Strategy class to use for trading
|
|
457
470
|
mm : Enable Money Management. Defaults to False.
|
|
471
|
+
optimizer : Risk management optimizer. Defaults to 'equal'.
|
|
472
|
+
See `bbstrader.models.optimization` module for more information.
|
|
458
473
|
show_positions_orders : Print open positions and orders. Defaults to False.
|
|
459
|
-
time_frame : Time frame to trade. Defaults to '15m'.
|
|
460
474
|
iter_time : Interval to check for signals and `mm`. Defaults to 5.
|
|
461
475
|
use_trade_time : Open trades after the time is completed. Defaults to True.
|
|
462
476
|
period : Period to trade. Defaults to 'week'.
|
|
@@ -466,6 +480,7 @@ class MT5ExecutionEngine():
|
|
|
466
480
|
trading_days : Trading days in a week. Defaults to monday to friday.
|
|
467
481
|
comment: Comment for trades. Defaults to None.
|
|
468
482
|
**kwargs: Additional keyword arguments
|
|
483
|
+
_ time_frame : Time frame to trade. Defaults to '15m'.
|
|
469
484
|
- strategy_name (Optional[str]): Strategy name. Defaults to None.
|
|
470
485
|
- max_trades (Dict[str, int]): Maximum trades per symbol. Defaults to None.
|
|
471
486
|
- notify (bool): Enable notifications. Defaults to False.
|
|
@@ -495,12 +510,12 @@ class MT5ExecutionEngine():
|
|
|
495
510
|
self.trades_instances = trades_instances
|
|
496
511
|
self.strategy_cls = strategy_cls
|
|
497
512
|
self.mm = mm
|
|
513
|
+
self.optimizer = optimizer
|
|
498
514
|
self.trail = trail
|
|
499
515
|
self.stop_trail = stop_trail
|
|
500
516
|
self.trail_after_points = trail_after_points
|
|
501
517
|
self.be_plus_points = be_plus_points
|
|
502
518
|
self.show_positions_orders = show_positions_orders
|
|
503
|
-
self.time_frame = time_frame
|
|
504
519
|
self.iter_time = iter_time
|
|
505
520
|
self.use_trade_time = use_trade_time
|
|
506
521
|
self.period = period
|
|
@@ -517,12 +532,12 @@ class MT5ExecutionEngine():
|
|
|
517
532
|
self.trades_instances,
|
|
518
533
|
self.strategy_cls,
|
|
519
534
|
mm=self.mm,
|
|
535
|
+
optimizer=self.optimizer,
|
|
520
536
|
trail=self.trail,
|
|
521
537
|
stop_trail=self.stop_trail,
|
|
522
538
|
trail_after_points=self.trail_after_points,
|
|
523
539
|
be_plus_points=self.be_plus_points,
|
|
524
540
|
show_positions_orders=self.show_positions_orders,
|
|
525
|
-
time_frame=self.time_frame,
|
|
526
541
|
iter_time=self.iter_time,
|
|
527
542
|
use_trade_time=self.use_trade_time,
|
|
528
543
|
period=self.period,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.93
|
|
4
4
|
Summary: Simplified Investment & Trading Toolkit
|
|
5
5
|
Home-page: https://github.com/bbalouki/bbstrader
|
|
6
6
|
Download-URL: https://pypi.org/project/bbstrader/
|
|
@@ -46,6 +46,7 @@ Requires-Dist: tqdm
|
|
|
46
46
|
Requires-Dist: scikit-learn
|
|
47
47
|
Requires-Dist: notify-py
|
|
48
48
|
Requires-Dist: python-telegram-bot
|
|
49
|
+
Requires-Dist: pyportfolioopt
|
|
49
50
|
Provides-Extra: mt5
|
|
50
51
|
Requires-Dist: MetaTrader5 ; extra == 'mt5'
|
|
51
52
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
bbstrader/__ini__.py,sha256=rCTy-3g2RlDAgIZ7cSET9-I74MwuCXpp-xGVTFS8NNc,482
|
|
2
|
+
bbstrader/config.py,sha256=_AD_Cd-w5zyabm1CBPNGhzcZuSjThB7jyzTcjbrIlUQ,3618
|
|
3
|
+
bbstrader/tseries.py,sha256=qJKLxHnPOjB7dXon-ITK7vU1fAuvl8evzET6lSSnijQ,53572
|
|
4
|
+
bbstrader/btengine/__init__.py,sha256=OaXZTjgDwqWrjPq-CNE4kJkmriKXt9t5pIghW1MDTeo,2911
|
|
5
|
+
bbstrader/btengine/backtest.py,sha256=A3S84jpGTE_zhguOEGoGu6H_4ws4Iq5sf0n7TZaUYfQ,14615
|
|
6
|
+
bbstrader/btengine/data.py,sha256=A6jUqDnjl-w1OSzbLLPfS1WfJ8Se25AqigJs9pbe0wc,17966
|
|
7
|
+
bbstrader/btengine/event.py,sha256=zF_ST4tcjV5uJJVV1IbRXQgCLbca2R2fmE7A2MaIno4,8748
|
|
8
|
+
bbstrader/btengine/execution.py,sha256=Fs6Hk64DxEOEVzAjsQ3CIVvYifWLLgkDjOixSh_Ghsc,10282
|
|
9
|
+
bbstrader/btengine/performance.py,sha256=WTYzB50lUD5aShPIEebbQPlaC2NVW6VfxdgGHjcIIAw,10707
|
|
10
|
+
bbstrader/btengine/portfolio.py,sha256=wCRmGxaZvihUPlXIlZp9cQo9fqPP-Tk5oALjknMfnos,16055
|
|
11
|
+
bbstrader/btengine/strategy.py,sha256=6IN1KQ-a-IQgbCEOflKTtGh-ouztwsVjik6TuMg6CY0,30210
|
|
12
|
+
bbstrader/metatrader/__init__.py,sha256=OLVOB_EieEb1P72I8V4Vem8kQWJ__D_L3c_wfwqY-9k,211
|
|
13
|
+
bbstrader/metatrader/account.py,sha256=3KeGLZ397kctf3EW_y8n9ENswAMU0tBQJuX_L0VXMrI,53909
|
|
14
|
+
bbstrader/metatrader/rates.py,sha256=1dJHbVqoT41m3EhF0wRe7dSGe5Kf3o5Maskkw-i5qsQ,20810
|
|
15
|
+
bbstrader/metatrader/risk.py,sha256=8NnH1kRvWd_JLieCpVty6hHKz2awrIQV2c8oykxELh0,26596
|
|
16
|
+
bbstrader/metatrader/trade.py,sha256=uigDah9n_rVJiwSslTAArLP94sde1dxYyGyRVIPPgb4,70210
|
|
17
|
+
bbstrader/metatrader/utils.py,sha256=BTaZun4DKWpCxBBzY0SLQqqz7n_7F_R1F59APfyaa3E,17666
|
|
18
|
+
bbstrader/models/__init__.py,sha256=mpxtXYEcE8hwNDbzJf8MRqnBIa2T1voraEk0U0ri53c,437
|
|
19
|
+
bbstrader/models/factors.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
bbstrader/models/ml.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
bbstrader/models/optimization.py,sha256=JlMsda9L-ADSgw4YPE4o3CsL1Yyxfeahf9kUb-EZqqM,6699
|
|
22
|
+
bbstrader/models/portfolios.py,sha256=dFTZ3maRVY_O3UOIoRlLCbAow3SiLTQYt1q5DNaRUxE,8223
|
|
23
|
+
bbstrader/models/risk.py,sha256=Pm_WoGI-vtPW75fwo_7ptF2Br-xQYBwrAAOIgqDQmy8,15120
|
|
24
|
+
bbstrader/trading/__init__.py,sha256=3CCzV5rQbH8NthjDJhD0_2FABvpiCmkeC9cVeoW7bi4,438
|
|
25
|
+
bbstrader/trading/execution.py,sha256=p_SKUziBBDuGiOmuxBsgBvxWu5nDVGZmtQBKw8OoZgE,25967
|
|
26
|
+
bbstrader/trading/scripts.py,sha256=rQmnG_4F_MuUEc96RXpAQT4kXrC-FkscsgHKgDAR_-Y,1902
|
|
27
|
+
bbstrader/trading/strategies.py,sha256=ztKNL4Nmlb-4N8_cq0OJyn3E2cRcdKdKu3FeTbZrHsU,36402
|
|
28
|
+
bbstrader-0.1.93.dist-info/LICENSE,sha256=1EudjwwP2oTJy8Vh0e-Kzv8VZZU95y-t6c3DYhR51uc,1115
|
|
29
|
+
bbstrader-0.1.93.dist-info/METADATA,sha256=XByGtsQ885U8oZpZOR9DPhQjdfi5eHbScJyGlcbbjxM,9932
|
|
30
|
+
bbstrader-0.1.93.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
|
31
|
+
bbstrader-0.1.93.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
|
|
32
|
+
bbstrader-0.1.93.dist-info/RECORD,,
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
bbstrader/__ini__.py,sha256=rCTy-3g2RlDAgIZ7cSET9-I74MwuCXpp-xGVTFS8NNc,482
|
|
2
|
-
bbstrader/config.py,sha256=_AD_Cd-w5zyabm1CBPNGhzcZuSjThB7jyzTcjbrIlUQ,3618
|
|
3
|
-
bbstrader/tseries.py,sha256=qJKLxHnPOjB7dXon-ITK7vU1fAuvl8evzET6lSSnijQ,53572
|
|
4
|
-
bbstrader/btengine/__init__.py,sha256=OaXZTjgDwqWrjPq-CNE4kJkmriKXt9t5pIghW1MDTeo,2911
|
|
5
|
-
bbstrader/btengine/backtest.py,sha256=HDuCNUETLUBZ0R8AVLeT5gZVVk8UQ6oeReUtDvUFhIk,13681
|
|
6
|
-
bbstrader/btengine/data.py,sha256=zeF3O7_vPT3NvAxlAcUS9OoxF09XhypDhGiVRml3U1A,17622
|
|
7
|
-
bbstrader/btengine/event.py,sha256=DQTmUQ4l4yZgL47hW1dGv__CecNF-rDqsyFst5kCQvA,8453
|
|
8
|
-
bbstrader/btengine/execution.py,sha256=gijnRvknNi921TjtF9wSyBG_nI0e2cda2NKIm3LJh1k,10222
|
|
9
|
-
bbstrader/btengine/performance.py,sha256=bKwj1_CSygvggLKTXPASp2eWhDdwyCf06ayUaXwdh4E,10655
|
|
10
|
-
bbstrader/btengine/portfolio.py,sha256=uf1fx0RuTTQNNxiYmCYW_sh6sBWbtNq4uIRusKA5MdY,15399
|
|
11
|
-
bbstrader/btengine/strategy.py,sha256=ktTzlJzSTo9zhZsILwj2vtPHjAkI-aSnKA8PcT3nF6Y,23206
|
|
12
|
-
bbstrader/metatrader/__init__.py,sha256=OLVOB_EieEb1P72I8V4Vem8kQWJ__D_L3c_wfwqY-9k,211
|
|
13
|
-
bbstrader/metatrader/account.py,sha256=hVH83vnAdfMOzUsF9PiWelqxa7HaLSTpCVlUEePnSZg,53912
|
|
14
|
-
bbstrader/metatrader/rates.py,sha256=xWTsM8PMPGdgwoPXcIRvHpJ1FKRIXHJcn4qrgmA7XRg,18737
|
|
15
|
-
bbstrader/metatrader/risk.py,sha256=8FcLY8pgV8_rxAcjx179sdqaMu66wl-fDFPZvdihfUw,25953
|
|
16
|
-
bbstrader/metatrader/trade.py,sha256=68hYIA01OPApRgGNdS1YFgM83ZemkCmv6mTXOCS_kWc,70052
|
|
17
|
-
bbstrader/metatrader/utils.py,sha256=BTaZun4DKWpCxBBzY0SLQqqz7n_7F_R1F59APfyaa3E,17666
|
|
18
|
-
bbstrader/models/__init__.py,sha256=6tAj9V9vgwesgPVMKznwRB3k8-Ec8Q73Di5p2UO0qlA,274
|
|
19
|
-
bbstrader/models/factors.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
bbstrader/models/ml.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
bbstrader/models/optimization.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
bbstrader/models/risk.py,sha256=Pm_WoGI-vtPW75fwo_7ptF2Br-xQYBwrAAOIgqDQmy8,15120
|
|
23
|
-
bbstrader/trading/__init__.py,sha256=3CCzV5rQbH8NthjDJhD0_2FABvpiCmkeC9cVeoW7bi4,438
|
|
24
|
-
bbstrader/trading/execution.py,sha256=5mGanLIB1nPfbXA9r_uKGDCkcV-ilVgZmssF7Oh4SeI,25186
|
|
25
|
-
bbstrader/trading/scripts.py,sha256=rQmnG_4F_MuUEc96RXpAQT4kXrC-FkscsgHKgDAR_-Y,1902
|
|
26
|
-
bbstrader/trading/strategies.py,sha256=ztKNL4Nmlb-4N8_cq0OJyn3E2cRcdKdKu3FeTbZrHsU,36402
|
|
27
|
-
bbstrader-0.1.91.dist-info/LICENSE,sha256=1EudjwwP2oTJy8Vh0e-Kzv8VZZU95y-t6c3DYhR51uc,1115
|
|
28
|
-
bbstrader-0.1.91.dist-info/METADATA,sha256=ZwHR-DYmIhULp_5dh48oQ_lGR0VYQnkGG8GcaQe1KP4,9901
|
|
29
|
-
bbstrader-0.1.91.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
30
|
-
bbstrader-0.1.91.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
|
|
31
|
-
bbstrader-0.1.91.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|