bbstrader 0.1.93__py3-none-any.whl → 0.2.0__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 +2 -2
- bbstrader/btengine/data.py +241 -40
- bbstrader/btengine/strategy.py +12 -8
- bbstrader/config.py +4 -0
- bbstrader/core/__init__.py +0 -0
- bbstrader/core/data.py +23 -0
- bbstrader/core/utils.py +0 -0
- bbstrader/ibkr/__init__.py +0 -0
- bbstrader/metatrader/account.py +66 -12
- bbstrader/metatrader/rates.py +24 -20
- bbstrader/metatrader/risk.py +6 -3
- bbstrader/metatrader/trade.py +31 -13
- bbstrader/models/__init__.py +1 -1
- bbstrader/models/factors.py +275 -0
- bbstrader/models/ml.py +1026 -0
- bbstrader/models/optimization.py +17 -16
- bbstrader/models/{portfolios.py → portfolio.py} +20 -11
- bbstrader/models/risk.py +10 -2
- bbstrader/trading/execution.py +67 -35
- bbstrader/trading/strategies.py +5 -5
- bbstrader/tseries.py +412 -63
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/METADATA +9 -3
- bbstrader-0.2.0.dist-info/RECORD +36 -0
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/WHEEL +1 -1
- bbstrader-0.1.93.dist-info/RECORD +0 -32
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/top_level.txt +0 -0
bbstrader/models/optimization.py
CHANGED
|
@@ -17,19 +17,19 @@ def markowitz_weights(prices=None, rfr=0.0, freq=252):
|
|
|
17
17
|
"""
|
|
18
18
|
Calculates optimal portfolio weights using Markowitz's mean-variance optimization (Max Sharpe Ratio) with multiple solvers.
|
|
19
19
|
|
|
20
|
-
Parameters
|
|
20
|
+
Parameters
|
|
21
21
|
----------
|
|
22
22
|
prices : pd.DataFrame, optional
|
|
23
23
|
Price data for assets, where rows represent time periods and columns represent assets.
|
|
24
24
|
freq : int, optional
|
|
25
25
|
Frequency of the data, such as 252 for daily returns in a year (default is 252).
|
|
26
26
|
|
|
27
|
-
Returns
|
|
27
|
+
Returns
|
|
28
28
|
-------
|
|
29
29
|
dict
|
|
30
30
|
Dictionary containing the optimal asset weights for maximizing the Sharpe ratio, normalized to sum to 1.
|
|
31
31
|
|
|
32
|
-
Notes
|
|
32
|
+
Notes
|
|
33
33
|
-----
|
|
34
34
|
This function attempts to maximize the Sharpe ratio by iterating through various solvers ('SCS', 'ECOS', 'OSQP')
|
|
35
35
|
from the PyPortfolioOpt library. If a solver fails, it proceeds to the next one. If none succeed, an error message
|
|
@@ -37,7 +37,7 @@ def markowitz_weights(prices=None, rfr=0.0, freq=252):
|
|
|
37
37
|
|
|
38
38
|
This function is useful for portfolio with a small number of assets, as it may not scale well for large portfolios.
|
|
39
39
|
|
|
40
|
-
Raises
|
|
40
|
+
Raises
|
|
41
41
|
------
|
|
42
42
|
Exception
|
|
43
43
|
If all solvers fail, each will print an exception error message during runtime.
|
|
@@ -61,7 +61,7 @@ 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.
|
|
63
63
|
|
|
64
|
-
Parameters
|
|
64
|
+
Parameters
|
|
65
65
|
----------
|
|
66
66
|
prices : pd.DataFrame, optional
|
|
67
67
|
Price data for assets; if provided, daily returns will be calculated.
|
|
@@ -70,17 +70,17 @@ def hierarchical_risk_parity(prices=None, returns=None, freq=252):
|
|
|
70
70
|
freq : int, optional
|
|
71
71
|
Number of days to consider in calculating portfolio weights (default is 252).
|
|
72
72
|
|
|
73
|
-
Returns
|
|
73
|
+
Returns
|
|
74
74
|
-------
|
|
75
75
|
dict
|
|
76
76
|
Optimized asset weights using the HRP method, with asset weights summing to 1.
|
|
77
77
|
|
|
78
|
-
Raises
|
|
78
|
+
Raises
|
|
79
79
|
------
|
|
80
80
|
ValueError
|
|
81
81
|
If neither `prices` nor `returns` are provided.
|
|
82
82
|
|
|
83
|
-
Notes
|
|
83
|
+
Notes
|
|
84
84
|
-----
|
|
85
85
|
Hierarchical Risk Parity is particularly useful for portfolios with a large number of assets,
|
|
86
86
|
as it mitigates issues of multicollinearity and estimation errors in covariance matrices by
|
|
@@ -101,7 +101,7 @@ def equal_weighted(prices=None, returns=None, round_digits=5):
|
|
|
101
101
|
"""
|
|
102
102
|
Generates an equal-weighted portfolio by assigning an equal proportion to each asset.
|
|
103
103
|
|
|
104
|
-
Parameters
|
|
104
|
+
Parameters
|
|
105
105
|
----------
|
|
106
106
|
prices : pd.DataFrame, optional
|
|
107
107
|
Price data for assets, where each column represents an asset.
|
|
@@ -110,21 +110,22 @@ def equal_weighted(prices=None, returns=None, round_digits=5):
|
|
|
110
110
|
round_digits : int, optional
|
|
111
111
|
Number of decimal places to round each weight to (default is 5).
|
|
112
112
|
|
|
113
|
-
Returns
|
|
113
|
+
Returns
|
|
114
114
|
-------
|
|
115
115
|
dict
|
|
116
116
|
Dictionary with equal weights assigned to each asset, summing to 1.
|
|
117
117
|
|
|
118
|
-
Raises
|
|
118
|
+
Raises
|
|
119
119
|
------
|
|
120
120
|
ValueError
|
|
121
121
|
If neither `prices` nor `returns` are provided.
|
|
122
122
|
|
|
123
|
-
Notes
|
|
123
|
+
Notes
|
|
124
124
|
-----
|
|
125
125
|
Equal weighting is a simple allocation method that assumes equal importance across all assets,
|
|
126
126
|
useful as a baseline model and when no strong views exist on asset return expectations or risk.
|
|
127
127
|
"""
|
|
128
|
+
|
|
128
129
|
if returns is None and prices is None:
|
|
129
130
|
raise ValueError("Either prices or returns must be provided")
|
|
130
131
|
if returns is None:
|
|
@@ -139,7 +140,7 @@ def optimized_weights(prices=None, returns=None, rfr=0.0, freq=252, method='equa
|
|
|
139
140
|
"""
|
|
140
141
|
Selects an optimization method to calculate portfolio weights based on user preference.
|
|
141
142
|
|
|
142
|
-
Parameters
|
|
143
|
+
Parameters
|
|
143
144
|
----------
|
|
144
145
|
prices : pd.DataFrame, optional
|
|
145
146
|
Price data for assets, required for certain methods.
|
|
@@ -150,17 +151,17 @@ def optimized_weights(prices=None, returns=None, rfr=0.0, freq=252, method='equa
|
|
|
150
151
|
method : str, optional
|
|
151
152
|
Optimization method to use ('markowitz', 'hrp', or 'equal') (default is 'markowitz').
|
|
152
153
|
|
|
153
|
-
Returns
|
|
154
|
+
Returns
|
|
154
155
|
-------
|
|
155
156
|
dict
|
|
156
157
|
Dictionary containing optimized asset weights based on the chosen method.
|
|
157
158
|
|
|
158
|
-
Raises
|
|
159
|
+
Raises
|
|
159
160
|
------
|
|
160
161
|
ValueError
|
|
161
162
|
If an unknown optimization method is specified.
|
|
162
163
|
|
|
163
|
-
Notes
|
|
164
|
+
Notes
|
|
164
165
|
-----
|
|
165
166
|
This function integrates different optimization methods:
|
|
166
167
|
- 'markowitz': mean-variance optimization with max Sharpe ratio
|
|
@@ -20,6 +20,15 @@ class EigenPortfolios(object):
|
|
|
20
20
|
to derive portfolios (eigenportfolios) that capture distinct risk factors in the asset returns. Each eigenportfolio
|
|
21
21
|
represents a principal component of the return covariance matrix, ordered by the magnitude of its eigenvalue. These
|
|
22
22
|
portfolios capture most of the variance in asset returns and are mutually uncorrelated.
|
|
23
|
+
|
|
24
|
+
Notes
|
|
25
|
+
-----
|
|
26
|
+
The implementation is inspired by the book "Machine Learning for Algorithmic Trading" by Stefan Jansen.
|
|
27
|
+
|
|
28
|
+
References
|
|
29
|
+
----------
|
|
30
|
+
Stefan Jansen (2020). Machine Learning for Algorithmic Trading - Second Edition.
|
|
31
|
+
chapter 13, Data-Driven Risk Factors and Asset Allocation with Unsupervised Learning.
|
|
23
32
|
|
|
24
33
|
"""
|
|
25
34
|
def __init__(self):
|
|
@@ -32,12 +41,12 @@ class EigenPortfolios(object):
|
|
|
32
41
|
"""
|
|
33
42
|
Returns the computed eigenportfolios (weights of assets in each portfolio).
|
|
34
43
|
|
|
35
|
-
Returns
|
|
44
|
+
Returns
|
|
36
45
|
-------
|
|
37
46
|
pd.DataFrame
|
|
38
47
|
DataFrame containing eigenportfolio weights for each asset.
|
|
39
48
|
|
|
40
|
-
Raises
|
|
49
|
+
Raises
|
|
41
50
|
------
|
|
42
51
|
ValueError
|
|
43
52
|
If `fit()` has not been called before retrieving portfolios.
|
|
@@ -50,19 +59,19 @@ class EigenPortfolios(object):
|
|
|
50
59
|
"""
|
|
51
60
|
Computes the eigenportfolios based on PCA of the asset returns' covariance matrix.
|
|
52
61
|
|
|
53
|
-
Parameters
|
|
62
|
+
Parameters
|
|
54
63
|
----------
|
|
55
64
|
returns : pd.DataFrame
|
|
56
65
|
Historical returns of assets to be used for PCA.
|
|
57
66
|
n_portfolios : int, optional
|
|
58
67
|
Number of eigenportfolios to compute (default is 4).
|
|
59
68
|
|
|
60
|
-
Returns
|
|
69
|
+
Returns
|
|
61
70
|
-------
|
|
62
71
|
pd.DataFrame
|
|
63
72
|
DataFrame containing normalized weights for each eigenportfolio.
|
|
64
73
|
|
|
65
|
-
Notes
|
|
74
|
+
Notes
|
|
66
75
|
-----
|
|
67
76
|
This method performs winsorization and normalization on returns to reduce the impact of outliers
|
|
68
77
|
and achieve zero mean and unit variance. It uses the first `n_portfolios` principal components
|
|
@@ -94,7 +103,7 @@ class EigenPortfolios(object):
|
|
|
94
103
|
"""
|
|
95
104
|
Plots the weights of each asset in each eigenportfolio as bar charts.
|
|
96
105
|
|
|
97
|
-
Notes
|
|
106
|
+
Notes
|
|
98
107
|
-----
|
|
99
108
|
Each subplot represents one eigenportfolio, showing the contribution of each asset.
|
|
100
109
|
"""
|
|
@@ -118,7 +127,7 @@ class EigenPortfolios(object):
|
|
|
118
127
|
"""
|
|
119
128
|
Plots the cumulative returns of each eigenportfolio over time.
|
|
120
129
|
|
|
121
|
-
Notes
|
|
130
|
+
Notes
|
|
122
131
|
-----
|
|
123
132
|
This method calculates the historical cumulative performance of each eigenportfolio
|
|
124
133
|
by weighting asset returns according to eigenportfolio weights.
|
|
@@ -152,7 +161,7 @@ class EigenPortfolios(object):
|
|
|
152
161
|
"""
|
|
153
162
|
Optimizes the chosen eigenportfolio based on a specified optimization method.
|
|
154
163
|
|
|
155
|
-
Parameters
|
|
164
|
+
Parameters
|
|
156
165
|
----------
|
|
157
166
|
portfolio : int, optional
|
|
158
167
|
Index of the eigenportfolio to optimize (default is 1).
|
|
@@ -165,17 +174,17 @@ class EigenPortfolios(object):
|
|
|
165
174
|
plot : bool, optional
|
|
166
175
|
Whether to plot the performance of the optimized portfolio (default is True).
|
|
167
176
|
|
|
168
|
-
Returns
|
|
177
|
+
Returns
|
|
169
178
|
-------
|
|
170
179
|
dict
|
|
171
180
|
Dictionary of optimized asset weights.
|
|
172
181
|
|
|
173
|
-
Raises
|
|
182
|
+
Raises
|
|
174
183
|
------
|
|
175
184
|
ValueError
|
|
176
185
|
If an unknown optimizer is specified, or if prices are not provided when using Markowitz optimization.
|
|
177
186
|
|
|
178
|
-
Notes
|
|
187
|
+
Notes
|
|
179
188
|
-----
|
|
180
189
|
The optimization method varies based on risk-return assumptions, with options for traditional Markowitz optimization,
|
|
181
190
|
Hierarchical Risk Parity, or equal weighting.
|
bbstrader/models/risk.py
CHANGED
|
@@ -310,7 +310,15 @@ class HMMRiskManager(RiskModel):
|
|
|
310
310
|
and data filtered up to the end date if specified.
|
|
311
311
|
"""
|
|
312
312
|
df = data_frame.copy()
|
|
313
|
-
|
|
313
|
+
if 'Returns' or 'returns' not in df.columns:
|
|
314
|
+
if 'Close' in df.columns:
|
|
315
|
+
df['Returns'] = df['Close'].pct_change()
|
|
316
|
+
elif 'Adj Close' in df.columns:
|
|
317
|
+
df['Returns'] = df['Adj Close'].pct_change()
|
|
318
|
+
else:
|
|
319
|
+
raise ValueError("No 'Close' or 'Adj Close' columns found.")
|
|
320
|
+
elif 'returns' in df.columns:
|
|
321
|
+
df.rename(columns={'returns': 'Returns'}, inplace=True)
|
|
314
322
|
if end is not None:
|
|
315
323
|
df = df[:end.strftime('%Y-%m-%d')]
|
|
316
324
|
df.dropna(inplace=True)
|
|
@@ -377,7 +385,7 @@ def build_hmm_models(symbol_list=None, **kwargs
|
|
|
377
385
|
hmm_models[symbol] = hmm
|
|
378
386
|
if mt5_data:
|
|
379
387
|
for symbol in symbols:
|
|
380
|
-
rates = Rates(symbol, tf, start_pos=hmm_end, session_duration=sd)
|
|
388
|
+
rates = Rates(symbol, timeframe=tf, start_pos=hmm_end, session_duration=sd, **kwargs)
|
|
381
389
|
data = rates.get_rates_from_pos()
|
|
382
390
|
assert data is not None, f"No data for {symbol}"
|
|
383
391
|
hmm = HMMRiskManager(
|
bbstrader/trading/execution.py
CHANGED
|
@@ -85,12 +85,17 @@ def _mt5_execution(
|
|
|
85
85
|
time_frame = kwargs.get('time_frame', '15m')
|
|
86
86
|
STRATEGY = kwargs.get('strategy_name')
|
|
87
87
|
mtrades = kwargs.get('max_trades')
|
|
88
|
+
check_max_trades = kwargs.get('check_max_trades', True)
|
|
88
89
|
notify = kwargs.get('notify', False)
|
|
89
90
|
if notify:
|
|
90
91
|
telegram = kwargs.get('telegram', False)
|
|
91
92
|
bot_token = kwargs.get('bot_token')
|
|
92
93
|
chat_id = kwargs.get('chat_id')
|
|
93
|
-
|
|
94
|
+
|
|
95
|
+
expert_ids = kwargs.get('expert_id')
|
|
96
|
+
if expert_ids is None:
|
|
97
|
+
expert_ids = set([trade.expert_id for trade in trades_instances.values()])
|
|
98
|
+
|
|
94
99
|
def update_risk(weights):
|
|
95
100
|
if weights is not None:
|
|
96
101
|
for symbol in symbols:
|
|
@@ -104,7 +109,9 @@ def _mt5_execution(
|
|
|
104
109
|
telegram=telegram, token=bot_token, chat_id=chat_id)
|
|
105
110
|
|
|
106
111
|
logger = trades_instances[symbols[0]].logger
|
|
107
|
-
max_trades = {symbol: mtrades[symbol]
|
|
112
|
+
max_trades = {symbol: mtrades[symbol]
|
|
113
|
+
if mtrades is not None and symbol in mtrades
|
|
114
|
+
else trades_instances[symbol].max_trade() for symbol in symbols}
|
|
108
115
|
if comment is None:
|
|
109
116
|
trade = trades_instances[symbols[0]]
|
|
110
117
|
comment = f"{trade.expert_name}@{trade.version}"
|
|
@@ -123,7 +130,7 @@ def _mt5_execution(
|
|
|
123
130
|
long_market = {symbol: False for symbol in symbols}
|
|
124
131
|
short_market = {symbol: False for symbol in symbols}
|
|
125
132
|
try:
|
|
126
|
-
check_mt5_connection()
|
|
133
|
+
check_mt5_connection(**kwargs)
|
|
127
134
|
strategy: MT5Strategy = strategy_cls(symbol_list=symbols, mode='live', **kwargs)
|
|
128
135
|
except Exception as e:
|
|
129
136
|
logger.error(f"Initializing strategy, {e}, STRATEGY={STRATEGY}")
|
|
@@ -133,7 +140,7 @@ def _mt5_execution(
|
|
|
133
140
|
|
|
134
141
|
while True:
|
|
135
142
|
try:
|
|
136
|
-
check_mt5_connection()
|
|
143
|
+
check_mt5_connection(**kwargs)
|
|
137
144
|
current_date = datetime.now()
|
|
138
145
|
today = current_date.strftime("%A").lower()
|
|
139
146
|
time.sleep(0.5)
|
|
@@ -142,8 +149,14 @@ def _mt5_execution(
|
|
|
142
149
|
positions_orders[type] = {}
|
|
143
150
|
for symbol in symbols:
|
|
144
151
|
positions_orders[type][symbol] = None
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
for id in expert_ids:
|
|
153
|
+
func = getattr(trades_instances[symbol], f"get_current_{type}")
|
|
154
|
+
func_value = func(id=id)
|
|
155
|
+
if func_value is not None:
|
|
156
|
+
if positions_orders[type][symbol] is None:
|
|
157
|
+
positions_orders[type][symbol] = func(id=id)
|
|
158
|
+
else:
|
|
159
|
+
positions_orders[type][symbol] += func(id=id)
|
|
147
160
|
buys = positions_orders['buys']
|
|
148
161
|
sells = positions_orders['sells']
|
|
149
162
|
for symbol in symbols:
|
|
@@ -153,41 +166,47 @@ def _mt5_execution(
|
|
|
153
166
|
logger.info(
|
|
154
167
|
f"Current {type.upper()} SYMBOL={symbol}: \
|
|
155
168
|
{positions_orders[type][symbol]}, STRATEGY={STRATEGY}")
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
if check_max_trades:
|
|
170
|
+
long_market = {
|
|
171
|
+
symbol: buys[symbol] is not None
|
|
172
|
+
and len(buys[symbol]) >= max_trades[symbol] for symbol in symbols
|
|
173
|
+
}
|
|
174
|
+
short_market = {
|
|
175
|
+
symbol: sells[symbol] is not None
|
|
176
|
+
and len(sells[symbol]) >= max_trades[symbol] for symbol in symbols
|
|
177
|
+
}
|
|
164
178
|
|
|
165
179
|
except Exception as e:
|
|
166
180
|
logger.error(f"Handling Positions and Orders, {e}, STRATEGY={STRATEGY}")
|
|
167
181
|
continue
|
|
168
182
|
time.sleep(0.5)
|
|
169
183
|
try:
|
|
170
|
-
check_mt5_connection()
|
|
184
|
+
check_mt5_connection(**kwargs)
|
|
171
185
|
signals = strategy.calculate_signals()
|
|
172
|
-
weights =
|
|
186
|
+
weights = None
|
|
187
|
+
if hasattr(strategy, 'apply_risk_management'):
|
|
188
|
+
weights = strategy.apply_risk_management(optimizer)
|
|
173
189
|
update_risk(weights)
|
|
174
190
|
except Exception as e:
|
|
175
191
|
logger.error(f"Calculating signal, {e}, STRATEGY={STRATEGY}")
|
|
176
192
|
continue
|
|
177
193
|
for symbol in symbols:
|
|
178
194
|
try:
|
|
179
|
-
check_mt5_connection()
|
|
195
|
+
check_mt5_connection(**kwargs)
|
|
180
196
|
trade: Trade = trades_instances[symbol]
|
|
181
197
|
tfmsg = f"Time Frame Not completed !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
|
|
182
198
|
riskmsg = f"Risk not allowed !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
|
|
183
199
|
signal = signals[symbol]
|
|
184
200
|
if isinstance(signal, dict):
|
|
185
|
-
|
|
186
|
-
price = signal
|
|
201
|
+
action = signal.get('action')
|
|
202
|
+
price = signal.get('price')
|
|
203
|
+
id = signal.get('id', trade.expert_id)
|
|
187
204
|
stoplimit = signal.get('stoplimit')
|
|
205
|
+
signal = action
|
|
188
206
|
elif isinstance(signal, str):
|
|
189
207
|
price = None
|
|
190
208
|
stoplimit = None
|
|
209
|
+
id = trade.expert_id
|
|
191
210
|
if trade.trading_time() and today in trading_days:
|
|
192
211
|
if signal is not None:
|
|
193
212
|
signal = 'BMKT' if signal == 'LONG' else signal
|
|
@@ -203,9 +222,9 @@ def _mt5_execution(
|
|
|
203
222
|
if notify:
|
|
204
223
|
_send_notification(info)
|
|
205
224
|
if position_type in POSITIONS_TYPES:
|
|
206
|
-
trade.close_positions(position_type=order_type)
|
|
225
|
+
trade.close_positions(position_type=order_type, id=id)
|
|
207
226
|
else:
|
|
208
|
-
trade.close_orders(order_type=order_type)
|
|
227
|
+
trade.close_orders(order_type=order_type, id=id)
|
|
209
228
|
elif signal in BUYS and not long_market[symbol]:
|
|
210
229
|
if use_trade_time:
|
|
211
230
|
if time_intervals % trade_time == 0 or buys[symbol] is None:
|
|
@@ -213,7 +232,7 @@ def _mt5_execution(
|
|
|
213
232
|
if notify:
|
|
214
233
|
_send_notification(info)
|
|
215
234
|
trade.open_buy_position(
|
|
216
|
-
action=signal, price=price, stoplimit=stoplimit, mm=mm, comment=comment)
|
|
235
|
+
action=signal, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
|
|
217
236
|
else:
|
|
218
237
|
logger.info(tfmsg)
|
|
219
238
|
check(buys[symbol], sells[symbol], symbol)
|
|
@@ -222,9 +241,11 @@ def _mt5_execution(
|
|
|
222
241
|
if notify:
|
|
223
242
|
_send_notification(info)
|
|
224
243
|
trade.open_buy_position(
|
|
225
|
-
action=signal, price=price, stoplimit=stoplimit, mm=mm, comment=comment)
|
|
226
|
-
elif signal in BUYS
|
|
227
|
-
|
|
244
|
+
action=signal, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
|
|
245
|
+
elif signal in BUYS:
|
|
246
|
+
if check_max_trades:
|
|
247
|
+
if long_market[symbol]:
|
|
248
|
+
logger.info(riskmsg)
|
|
228
249
|
check(buys[symbol], sells[symbol], symbol)
|
|
229
250
|
|
|
230
251
|
elif signal in SELLS and not short_market[symbol]:
|
|
@@ -234,7 +255,7 @@ def _mt5_execution(
|
|
|
234
255
|
if notify:
|
|
235
256
|
_send_notification(info)
|
|
236
257
|
trade.open_sell_position(
|
|
237
|
-
action=signal, price=price, stoplimit=stoplimit, mm=mm, comment=comment)
|
|
258
|
+
action=signal, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
|
|
238
259
|
else:
|
|
239
260
|
logger.info(tfmsg)
|
|
240
261
|
check(buys[symbol], sells[symbol], symbol)
|
|
@@ -243,9 +264,11 @@ def _mt5_execution(
|
|
|
243
264
|
if notify:
|
|
244
265
|
_send_notification(info)
|
|
245
266
|
trade.open_sell_position(
|
|
246
|
-
action=signal, price=price, stoplimit=stoplimit, mm=mm, comment=comment)
|
|
247
|
-
elif signal in SELLS
|
|
248
|
-
|
|
267
|
+
action=signal, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
|
|
268
|
+
elif signal in SELLS:
|
|
269
|
+
if check_max_trades:
|
|
270
|
+
if short_market[symbol]:
|
|
271
|
+
logger.info(riskmsg)
|
|
249
272
|
check(buys[symbol], sells[symbol], symbol)
|
|
250
273
|
else:
|
|
251
274
|
check(buys[symbol], sells[symbol], symbol)
|
|
@@ -269,7 +292,7 @@ def _mt5_execution(
|
|
|
269
292
|
)
|
|
270
293
|
try:
|
|
271
294
|
FRIDAY = 'friday'
|
|
272
|
-
check_mt5_connection()
|
|
295
|
+
check_mt5_connection(**kwargs)
|
|
273
296
|
day_end = all(trade.days_end() for trade in trades_instances.values())
|
|
274
297
|
if closing_pnl is not None:
|
|
275
298
|
closing = all(trade.positive_profit(id=trade.expert_id, th=closing_pnl)
|
|
@@ -284,7 +307,8 @@ def _mt5_execution(
|
|
|
284
307
|
for symbol in symbols:
|
|
285
308
|
trade = trades_instances[symbol]
|
|
286
309
|
if trade.days_end() and closing:
|
|
287
|
-
|
|
310
|
+
for id in expert_ids:
|
|
311
|
+
trade.close_positions(position_type='all', id=id, comment=comment)
|
|
288
312
|
logmsg("Day")
|
|
289
313
|
trade.statistics(save=True)
|
|
290
314
|
if day_end:
|
|
@@ -308,7 +332,8 @@ def _mt5_execution(
|
|
|
308
332
|
logmsg("Day")
|
|
309
333
|
|
|
310
334
|
elif trade.days_end() and today == FRIDAY and closing:
|
|
311
|
-
|
|
335
|
+
for id in expert_ids:
|
|
336
|
+
trade.close_positions(position_type='all', id=id, comment=comment)
|
|
312
337
|
logmsg("Week")
|
|
313
338
|
trade.statistics(save=True)
|
|
314
339
|
if day_end and today != FRIDAY:
|
|
@@ -337,7 +362,8 @@ def _mt5_execution(
|
|
|
337
362
|
and today == FRIDAY
|
|
338
363
|
and num_days/len(symbols) >= 20
|
|
339
364
|
) and closing:
|
|
340
|
-
|
|
365
|
+
for id in expert_ids:
|
|
366
|
+
trade.close_positions(position_type='all', id=id, comment=comment)
|
|
341
367
|
logmsg("Month")
|
|
342
368
|
trade.statistics(save=True)
|
|
343
369
|
if day_end and today != FRIDAY:
|
|
@@ -461,7 +487,7 @@ class MT5ExecutionEngine():
|
|
|
461
487
|
trading_days: Optional[List[str]] = TradingDays,
|
|
462
488
|
comment: Optional[str] = None,
|
|
463
489
|
**kwargs
|
|
464
|
-
|
|
490
|
+
):
|
|
465
491
|
"""
|
|
466
492
|
Args:
|
|
467
493
|
symbol_list : List of symbols to trade
|
|
@@ -505,6 +531,7 @@ class MT5ExecutionEngine():
|
|
|
505
531
|
|
|
506
532
|
4. All strategies must generate signals for backtesting and live trading.
|
|
507
533
|
See the `bbstrader.trading.strategies` module for more information on how to create custom strategies.
|
|
534
|
+
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
508
535
|
"""
|
|
509
536
|
self.symbol_list = symbol_list
|
|
510
537
|
self.trades_instances = trades_instances
|
|
@@ -525,8 +552,13 @@ class MT5ExecutionEngine():
|
|
|
525
552
|
self.comment = comment
|
|
526
553
|
self.kwargs = kwargs
|
|
527
554
|
|
|
555
|
+
def __repr__(self):
|
|
556
|
+
trades = self.trades_instances.keys()
|
|
557
|
+
s = self.strategy_cls.__name__
|
|
558
|
+
return f"MT5ExecutionEngine(Symbols={list(trades)}, Strategy={s})"
|
|
559
|
+
|
|
528
560
|
def run(self):
|
|
529
|
-
check_mt5_connection()
|
|
561
|
+
check_mt5_connection(**self.kwargs)
|
|
530
562
|
_mt5_execution(
|
|
531
563
|
self.symbol_list,
|
|
532
564
|
self.trades_instances,
|
bbstrader/trading/strategies.py
CHANGED
|
@@ -80,17 +80,17 @@ class SMAStrategy(Strategy):
|
|
|
80
80
|
self.symbol_list = symbol_list or self.bars.symbol_list
|
|
81
81
|
self.mode = mode
|
|
82
82
|
|
|
83
|
+
self.kwargs = kwargs
|
|
83
84
|
self.short_window = kwargs.get("short_window", 50)
|
|
84
85
|
self.long_window = kwargs.get("long_window", 200)
|
|
85
86
|
self.tf = kwargs.get("time_frame", 'D1')
|
|
86
87
|
self.qty = get_quantities(
|
|
87
88
|
kwargs.get('quantities', 100), self.symbol_list)
|
|
88
89
|
self.sd = kwargs.get("session_duration", 23.0)
|
|
89
|
-
self.risk_models = build_hmm_models(self.symbol_list, **kwargs)
|
|
90
|
+
self.risk_models = build_hmm_models(self.symbol_list, **self.kwargs)
|
|
90
91
|
self.risk_window = kwargs.get("risk_window", self.long_window)
|
|
91
92
|
self.bought = self._calculate_initial_bought()
|
|
92
93
|
|
|
93
|
-
|
|
94
94
|
def _calculate_initial_bought(self):
|
|
95
95
|
bought = {}
|
|
96
96
|
for s in self.symbol_list:
|
|
@@ -151,13 +151,13 @@ class SMAStrategy(Strategy):
|
|
|
151
151
|
signal = SignalEvent(
|
|
152
152
|
1, s, dt, 'SHORT', quantity=self.qty[s], price=price)
|
|
153
153
|
self.bought[s] = 'SHORT'
|
|
154
|
-
|
|
154
|
+
signals[s] = signal
|
|
155
155
|
return signals
|
|
156
156
|
|
|
157
157
|
def get_live_data(self):
|
|
158
158
|
symbol_data = {symbol: None for symbol in self.symbol_list}
|
|
159
159
|
for symbol in self.symbol_list:
|
|
160
|
-
sig_rate = Rates(symbol, self.tf, 0, self.risk_window)
|
|
160
|
+
sig_rate = Rates(symbol, self.tf, 0, self.risk_window+2, **self.kwargs)
|
|
161
161
|
hmm_data = sig_rate.returns.values
|
|
162
162
|
prices = sig_rate.close.values
|
|
163
163
|
current_regime = self.risk_models[symbol].which_trade_allowed(hmm_data)
|
|
@@ -404,7 +404,7 @@ class KalmanFilterStrategy(Strategy):
|
|
|
404
404
|
|
|
405
405
|
self.hmm_tiker = kwargs.get("hmm_tiker")
|
|
406
406
|
self._assert_tikers()
|
|
407
|
-
self.account = Account()
|
|
407
|
+
self.account = Account(**kwargs)
|
|
408
408
|
self.hmm_window = kwargs.get("hmm_window", 50)
|
|
409
409
|
self.qty = kwargs.get("quantity", 100)
|
|
410
410
|
self.tf = kwargs.get("time_frame", "D1")
|