bbstrader 0.1.94__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.

@@ -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
@@ -385,7 +385,7 @@ def build_hmm_models(symbol_list=None, **kwargs
385
385
  hmm_models[symbol] = hmm
386
386
  if mt5_data:
387
387
  for symbol in symbols:
388
- 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)
389
389
  data = rates.get_rates_from_pos()
390
390
  assert data is not None, f"No data for {symbol}"
391
391
  hmm = HMMRiskManager(
@@ -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:
@@ -144,8 +149,14 @@ def _mt5_execution(
144
149
  positions_orders[type] = {}
145
150
  for symbol in symbols:
146
151
  positions_orders[type][symbol] = None
147
- func = getattr(trades_instances[symbol], f"get_current_{type}")
148
- positions_orders[type][symbol] = func()
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)
149
160
  buys = positions_orders['buys']
150
161
  sells = positions_orders['sells']
151
162
  for symbol in symbols:
@@ -155,14 +166,15 @@ def _mt5_execution(
155
166
  logger.info(
156
167
  f"Current {type.upper()} SYMBOL={symbol}: \
157
168
  {positions_orders[type][symbol]}, STRATEGY={STRATEGY}")
158
- long_market = {
159
- symbol: buys[symbol] is not None
160
- and len(buys[symbol]) >= max_trades[symbol] for symbol in symbols
161
- }
162
- short_market = {
163
- symbol: sells[symbol] is not None
164
- and len(sells[symbol]) >= max_trades[symbol] for symbol in symbols
165
- }
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
+ }
166
178
 
167
179
  except Exception as e:
168
180
  logger.error(f"Handling Positions and Orders, {e}, STRATEGY={STRATEGY}")
@@ -186,12 +198,15 @@ def _mt5_execution(
186
198
  riskmsg = f"Risk not allowed !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
187
199
  signal = signals[symbol]
188
200
  if isinstance(signal, dict):
189
- signal = signal['action']
190
- price = signal['price']
201
+ action = signal.get('action')
202
+ price = signal.get('price')
203
+ id = signal.get('id', trade.expert_id)
191
204
  stoplimit = signal.get('stoplimit')
205
+ signal = action
192
206
  elif isinstance(signal, str):
193
207
  price = None
194
208
  stoplimit = None
209
+ id = trade.expert_id
195
210
  if trade.trading_time() and today in trading_days:
196
211
  if signal is not None:
197
212
  signal = 'BMKT' if signal == 'LONG' else signal
@@ -207,9 +222,9 @@ def _mt5_execution(
207
222
  if notify:
208
223
  _send_notification(info)
209
224
  if position_type in POSITIONS_TYPES:
210
- trade.close_positions(position_type=order_type)
225
+ trade.close_positions(position_type=order_type, id=id)
211
226
  else:
212
- trade.close_orders(order_type=order_type)
227
+ trade.close_orders(order_type=order_type, id=id)
213
228
  elif signal in BUYS and not long_market[symbol]:
214
229
  if use_trade_time:
215
230
  if time_intervals % trade_time == 0 or buys[symbol] is None:
@@ -217,7 +232,7 @@ def _mt5_execution(
217
232
  if notify:
218
233
  _send_notification(info)
219
234
  trade.open_buy_position(
220
- action=signal, price=price, stoplimit=stoplimit, mm=mm, comment=comment)
235
+ action=signal, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
221
236
  else:
222
237
  logger.info(tfmsg)
223
238
  check(buys[symbol], sells[symbol], symbol)
@@ -226,9 +241,11 @@ def _mt5_execution(
226
241
  if notify:
227
242
  _send_notification(info)
228
243
  trade.open_buy_position(
229
- action=signal, price=price, stoplimit=stoplimit, mm=mm, comment=comment)
230
- elif signal in BUYS and long_market[symbol]:
231
- logger.info(riskmsg)
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)
232
249
  check(buys[symbol], sells[symbol], symbol)
233
250
 
234
251
  elif signal in SELLS and not short_market[symbol]:
@@ -238,7 +255,7 @@ def _mt5_execution(
238
255
  if notify:
239
256
  _send_notification(info)
240
257
  trade.open_sell_position(
241
- action=signal, price=price, stoplimit=stoplimit, mm=mm, comment=comment)
258
+ action=signal, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
242
259
  else:
243
260
  logger.info(tfmsg)
244
261
  check(buys[symbol], sells[symbol], symbol)
@@ -247,9 +264,11 @@ def _mt5_execution(
247
264
  if notify:
248
265
  _send_notification(info)
249
266
  trade.open_sell_position(
250
- action=signal, price=price, stoplimit=stoplimit, mm=mm, comment=comment)
251
- elif signal in SELLS and short_market[symbol]:
252
- logger.info(riskmsg)
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)
253
272
  check(buys[symbol], sells[symbol], symbol)
254
273
  else:
255
274
  check(buys[symbol], sells[symbol], symbol)
@@ -288,7 +307,8 @@ def _mt5_execution(
288
307
  for symbol in symbols:
289
308
  trade = trades_instances[symbol]
290
309
  if trade.days_end() and closing:
291
- trade.close_positions(position_type='all', comment=comment)
310
+ for id in expert_ids:
311
+ trade.close_positions(position_type='all', id=id, comment=comment)
292
312
  logmsg("Day")
293
313
  trade.statistics(save=True)
294
314
  if day_end:
@@ -312,7 +332,8 @@ def _mt5_execution(
312
332
  logmsg("Day")
313
333
 
314
334
  elif trade.days_end() and today == FRIDAY and closing:
315
- trade.close_positions(position_type='all', comment=comment)
335
+ for id in expert_ids:
336
+ trade.close_positions(position_type='all', id=id, comment=comment)
316
337
  logmsg("Week")
317
338
  trade.statistics(save=True)
318
339
  if day_end and today != FRIDAY:
@@ -341,7 +362,8 @@ def _mt5_execution(
341
362
  and today == FRIDAY
342
363
  and num_days/len(symbols) >= 20
343
364
  ) and closing:
344
- trade.close_positions(position_type='all', comment=comment)
365
+ for id in expert_ids:
366
+ trade.close_positions(position_type='all', id=id, comment=comment)
345
367
  logmsg("Month")
346
368
  trade.statistics(save=True)
347
369
  if day_end and today != FRIDAY:
@@ -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")