bbstrader 0.1.92__tar.gz → 0.1.93__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bbstrader might be problematic. Click here for more details.

Files changed (39) hide show
  1. {bbstrader-0.1.92/bbstrader.egg-info → bbstrader-0.1.93}/PKG-INFO +1 -1
  2. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/btengine/backtest.py +20 -14
  3. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/btengine/execution.py +3 -2
  4. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/btengine/performance.py +2 -0
  5. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/btengine/portfolio.py +12 -16
  6. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/btengine/strategy.py +140 -45
  7. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/metatrader/account.py +1 -1
  8. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/metatrader/risk.py +28 -3
  9. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/models/__init__.py +5 -1
  10. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/models/optimization.py +12 -5
  11. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/models/portfolios.py +3 -0
  12. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/trading/execution.py +20 -7
  13. {bbstrader-0.1.92 → bbstrader-0.1.93/bbstrader.egg-info}/PKG-INFO +1 -1
  14. {bbstrader-0.1.92 → bbstrader-0.1.93}/setup.py +1 -1
  15. {bbstrader-0.1.92 → bbstrader-0.1.93}/LICENSE +0 -0
  16. {bbstrader-0.1.92 → bbstrader-0.1.93}/MANIFEST.in +0 -0
  17. {bbstrader-0.1.92 → bbstrader-0.1.93}/README.md +0 -0
  18. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/__ini__.py +0 -0
  19. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/btengine/__init__.py +0 -0
  20. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/btengine/data.py +0 -0
  21. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/btengine/event.py +0 -0
  22. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/config.py +0 -0
  23. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/metatrader/__init__.py +0 -0
  24. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/metatrader/rates.py +0 -0
  25. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/metatrader/trade.py +0 -0
  26. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/metatrader/utils.py +0 -0
  27. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/models/factors.py +0 -0
  28. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/models/ml.py +0 -0
  29. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/models/risk.py +0 -0
  30. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/trading/__init__.py +0 -0
  31. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/trading/scripts.py +0 -0
  32. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/trading/strategies.py +0 -0
  33. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader/tseries.py +0 -0
  34. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader.egg-info/SOURCES.txt +0 -0
  35. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader.egg-info/dependency_links.txt +0 -0
  36. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader.egg-info/requires.txt +0 -0
  37. {bbstrader-0.1.92 → bbstrader-0.1.93}/bbstrader.egg-info/top_level.txt +0 -0
  38. {bbstrader-0.1.92 → bbstrader-0.1.93}/requirements.txt +0 -0
  39. {bbstrader-0.1.92 → bbstrader-0.1.93}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bbstrader
3
- Version: 0.1.92
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/
@@ -85,6 +85,8 @@ class BacktestEngine(Backtest):
85
85
  strategy (Strategy): Generates signals based on market data.
86
86
  kwargs : Additional parameters based on the `ExecutionHandler`,
87
87
  the `DataHandler`, the `Strategy` used and the `Portfolio`.
88
+ - show_equity (bool): Show the equity curve of the portfolio.
89
+ - stats_file (str): File to save the summary stats.
88
90
  """
89
91
  self.symbol_list = symbol_list
90
92
  self.initial_capital = initial_capital
@@ -104,6 +106,7 @@ class BacktestEngine(Backtest):
104
106
 
105
107
  self._generate_trading_instances()
106
108
  self.show_equity = kwargs.get("show_equity", False)
109
+ self.stats_file = kwargs.get("stats_file", None)
107
110
 
108
111
  def _generate_trading_instances(self):
109
112
  """
@@ -137,14 +140,21 @@ class BacktestEngine(Backtest):
137
140
  i = 0
138
141
  while True:
139
142
  i += 1
140
- # Update the market bars
143
+ value = self.portfolio.all_holdings[-1]['Total']
141
144
  if self.data_handler.continue_backtest == True:
145
+ # Update the market bars
142
146
  self.data_handler.update_bars()
143
147
  self.strategy.check_pending_orders()
148
+ self.strategy.get_update_from_portfolio(
149
+ self.portfolio.current_positions,
150
+ self.portfolio.current_holdings
151
+ )
152
+ self.strategy.cash = value
144
153
  else:
145
154
  print("\n[======= BACKTEST COMPLETED =======]")
146
- print(f"END DATE: {self.data_handler.get_latest_bar_datetime()}")
155
+ print(f"END DATE: {self.data_handler.get_latest_bar_datetime(self.symbol_list[0])}")
147
156
  print(f"TOTAL BARS: {i} ")
157
+ print(f"PORFOLIO VALUE: {round(value, 2)}")
148
158
  break
149
159
 
150
160
  # Handle the events
@@ -171,10 +181,6 @@ class BacktestEngine(Backtest):
171
181
  self.fills += 1
172
182
  self.portfolio.update_fill(event)
173
183
  self.strategy.update_trades_from_fill(event)
174
- self.strategy.get_update_from_portfolio(
175
- self.portfolio.current_positions,
176
- self.portfolio.current_holdings
177
- )
178
184
 
179
185
  time.sleep(self.heartbeat)
180
186
 
@@ -192,13 +198,13 @@ class BacktestEngine(Backtest):
192
198
  stat2['Orders'] = self.orders
193
199
  stat2['Fills'] = self.fills
194
200
  stats.extend(stat2.items())
195
- print(
196
- tabulate(
197
- stats,
198
- headers=["Metric", "Value"],
199
- tablefmt="outline"),
200
- "\n"
201
- )
201
+ tab_stats = tabulate(stats, headers=["Metric", "Value"], tablefmt="outline")
202
+ print(tab_stats, "\n")
203
+ if self.stats_file:
204
+ with open(self.stats_file, 'a') as f:
205
+ f.write("\n[======= Summary Stats =======]\n")
206
+ f.write(tab_stats)
207
+ f.write("\n")
202
208
 
203
209
  if self.show_equity:
204
210
  print("\nCreating equity curve...")
@@ -336,7 +342,7 @@ def run_backtest_with(engine: Literal["bbstrader", "cerebro", "zipline"], **kwar
336
342
  data_handler=kwargs.get("data_handler"),
337
343
  strategy=kwargs.get("strategy"),
338
344
  exc_handler=kwargs.get("exc_handler"),
339
- initial_capital=kwargs.get("initial_capital"),
345
+ initial_capital=kwargs.get("initial_capital", 100000.0),
340
346
  heartbeat=kwargs.get("heartbeat", 0.0),
341
347
  **kwargs
342
348
  )
@@ -77,8 +77,9 @@ class SimExecutionHandler(ExecutionHandler):
77
77
  if event.type == 'ORDER':
78
78
  dtime = self.bardata.get_latest_bar_datetime(event.symbol)
79
79
  fill_event = FillEvent(
80
- dtime, event.symbol,
81
- 'ARCA', event.quantity, event.direction, order=event.signal
80
+ timeindex=dtime, symbol=event.symbol,
81
+ exchange='ARCA', quantity=event.quantity, direction=event.direction,
82
+ fill_cost=None, commission=None, order=event.signal
82
83
  )
83
84
  self.events.put(fill_event)
84
85
  self.logger.info(
@@ -6,6 +6,8 @@ from scipy.stats import mstats
6
6
  import matplotlib.pyplot as plt
7
7
  from matplotlib.ticker import MaxNLocator
8
8
  import quantstats as qs
9
+ import warnings
10
+ warnings.filterwarnings("ignore")
9
11
 
10
12
  sns.set_theme()
11
13
 
@@ -93,16 +93,20 @@ class Portfolio(object):
93
93
  initial_capital (float): The starting capital in USD.
94
94
 
95
95
  kwargs (dict): Additional arguments
96
+ - `leverage`: The leverage to apply to the portfolio.
96
97
  - `time_frame`: The time frame of the bars.
97
- - `trading_hours`: The number of trading hours in a day.
98
+ - `session_duration`: The number of trading hours in a day.
98
99
  - `benchmark`: The benchmark symbol to compare the portfolio.
99
- - `strategy_name`: The name of the strategy (the name must not include 'Strategy' in it).
100
+ - `output_dir`: The directory to save the backtest results.
101
+ - `strategy_name`: The name of the strategy (the name must not include 'Strategy' in it).
102
+ - `print_stats`: Whether to print the backtest statistics.
100
103
  """
101
104
  self.bars = bars
102
105
  self.events = events
103
106
  self.symbol_list = self.bars.symbol_list
104
107
  self.start_date = start_date
105
108
  self.initial_capital = initial_capital
109
+ self._leverage = kwargs.get('leverage', 1)
106
110
 
107
111
  self.timeframe = kwargs.get("time_frame", "D1")
108
112
  self.trading_hours = kwargs.get("session_duration", 23)
@@ -277,8 +281,7 @@ class Portfolio(object):
277
281
 
278
282
  def generate_order(self, signal: SignalEvent):
279
283
  """
280
- Check if the portfolio has enough cash to place an order
281
- and generate an OrderEvent, else return None.
284
+ Turns a SignalEvent into an OrderEvent.
282
285
 
283
286
  Args:
284
287
  signal (SignalEvent): The tuple containing Signal information.
@@ -294,25 +297,17 @@ class Portfolio(object):
294
297
  strength = signal.strength
295
298
  price = signal.price or self._get_price(symbol)
296
299
  cur_quantity = self.current_positions[symbol]
297
- cash = self.current_holdings['Cash']
300
+ mkt_quantity = round(quantity * strength, 2)
301
+ new_quantity = mkt_quantity * self._leverage
298
302
 
299
303
  if direction in ['LONG', 'SHORT', 'EXIT']:
300
304
  order_type = 'MKT'
301
305
  else:
302
306
  order_type = direction
303
- mkt_quantity = round(quantity * strength)
304
- cost = mkt_quantity * price
305
307
 
306
- if cash >= cost:
307
- new_quantity = mkt_quantity
308
- elif cash < cost and cash > 0:
309
- new_quantity = round(cash // price)
310
- else:
311
- new_quantity = 0
312
-
313
- if new_quantity > 0 and direction == 'LONG':
308
+ if direction == 'LONG' and new_quantity > 0:
314
309
  order = OrderEvent(symbol, order_type, new_quantity, 'BUY', price, direction)
315
- if new_quantity > 0 and direction == 'SHORT':
310
+ if direction == 'SHORT' and new_quantity > 0:
316
311
  order = OrderEvent(symbol, order_type, new_quantity, 'SELL', price, direction)
317
312
 
318
313
  if direction == 'EXIT' and cur_quantity > 0:
@@ -322,6 +317,7 @@ class Portfolio(object):
322
317
 
323
318
  return order
324
319
 
320
+
325
321
  def update_signal(self, event: SignalEvent):
326
322
  """
327
323
  Acts on a SignalEvent to generate new orders
@@ -1,6 +1,5 @@
1
1
  from abc import ABCMeta, abstractmethod
2
2
  import pytz
3
- import math
4
3
  import pandas as pd
5
4
  import numpy as np
6
5
  from queue import Queue
@@ -18,6 +17,7 @@ from typing import (
18
17
  List,
19
18
  Literal
20
19
  )
20
+ from bbstrader.models.optimization import optimized_weights
21
21
 
22
22
  __all__ = ['Strategy', 'MT5Strategy']
23
23
 
@@ -72,17 +72,29 @@ class MT5Strategy(Strategy):
72
72
  bars : The data handler object.
73
73
  mode : The mode of operation for the strategy (backtest or live).
74
74
  **kwargs : Additional keyword arguments for other classes (e.g, Portfolio, ExecutionHandler).
75
+ - max_trades : The maximum number of trades allowed per symbol.
76
+ - time_frame : The time frame for the strategy.
77
+ - logger : The logger object for the strategy.
75
78
  """
76
79
  self.events = events
77
80
  self.data = bars
78
81
  self.symbols = symbol_list
79
82
  self.mode = mode
80
- self.volume = kwargs.get("volume")
81
- self.max_trades = kwargs.get("max_trades",
82
- {symbol: 1 for symbol in self.symbols})
83
+ self._porfolio_value = None
84
+ self.risk_budget = self._check_risk_budget(**kwargs)
85
+ self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
86
+ self.tf = kwargs.get("time_frame", 'D1')
83
87
  self.logger = kwargs.get("logger")
84
88
  self._initialize_portfolio()
89
+
90
+ @property
91
+ def cash(self) -> float:
92
+ return self._porfolio_value
85
93
 
94
+ @cash.setter
95
+ def cash(self, value):
96
+ self._porfolio_value = value
97
+
86
98
  @property
87
99
  def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
88
100
  return self._orders
@@ -98,11 +110,24 @@ class MT5Strategy(Strategy):
98
110
  @property
99
111
  def holdings(self) -> Dict[str, float]:
100
112
  return self._holdings
113
+
114
+ def _check_risk_budget(self, **kwargs):
115
+ weights = kwargs.get('risk_weights')
116
+ if weights is not None and isinstance(weights, dict):
117
+ for asset in self.symbols:
118
+ if asset not in weights:
119
+ raise ValueError(f"Risk budget for asset {asset} is missing.")
120
+ total_risk = sum(weights.values())
121
+ if not np.isclose(total_risk, 1.0):
122
+ raise ValueError(f'Risk budget weights must sum to 1. got {total_risk}')
123
+ return weights
124
+ elif isinstance(weights, str):
125
+ return weights
101
126
 
102
127
  def _initialize_portfolio(self):
103
128
  positions = ['LONG', 'SHORT']
104
129
  orders = ['BLMT', 'BSTP', 'BSTPLMT', 'SLMT', 'SSTP', 'SSTPLMT']
105
- self._positions: Dict[str, Dict[str, int]] = {}
130
+ self._positions: Dict[str, Dict[str, int | float]] = {}
106
131
  self._trades: Dict[str, Dict[str, int]] = {}
107
132
  self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
108
133
  for symbol in self.symbols:
@@ -111,7 +136,7 @@ class MT5Strategy(Strategy):
111
136
  self._trades[symbol] = {}
112
137
  for position in positions:
113
138
  self._trades[symbol][position] = 0
114
- self._positions[symbol][position] = 0
139
+ self._positions[symbol][position] = 0.0
115
140
  for order in orders:
116
141
  self._orders[symbol][order] = []
117
142
  self._holdings = {s: 0.0 for s in self.symbols}
@@ -133,6 +158,9 @@ class MT5Strategy(Strategy):
133
158
  self._positions[symbol]['LONG'] = positions[symbol]
134
159
  elif positions[symbol] < 0:
135
160
  self._positions[symbol]['SHORT'] = positions[symbol]
161
+ else:
162
+ self._positions[symbol]['LONG'] = 0
163
+ self._positions[symbol]['SHORT'] = 0
136
164
  if symbol in holdings:
137
165
  self._holdings[symbol] = holdings[symbol]
138
166
 
@@ -142,7 +170,12 @@ class MT5Strategy(Strategy):
142
170
  It is used to keep track of the number of trades executed for each order.
143
171
  """
144
172
  if event.type == 'FILL':
145
- self._trades[event.symbol][event.order] += 1
173
+ if event.order != 'EXIT':
174
+ self._trades[event.symbol][event.order] += 1
175
+ elif event.order == 'EXIT' and event.direction == 'BUY':
176
+ self._trades[event.symbol]['SHORT'] = 0
177
+ elif event.order == 'EXIT' and event.direction == 'SELL':
178
+ self._trades[event.symbol]['LONG'] = 0
146
179
 
147
180
  def calculate_signals(self, *args, **kwargs
148
181
  ) -> Dict[str, Union[str, dict, None]] | None:
@@ -173,7 +206,25 @@ class MT5Strategy(Strategy):
173
206
  """
174
207
  pass
175
208
 
176
- def get_quantity(self, symbol, volume=None) -> int:
209
+ def apply_risk_management(self, optimer, freq=252) -> Dict[str, float] | None:
210
+ """
211
+ Apply risk management rules to the strategy.
212
+ """
213
+ if optimer is None:
214
+ return None
215
+ prices = self.get_asset_values(
216
+ symbol_list=self.symbols, bars=self.data, mode=self.mode,
217
+ window=freq, value_type='close', array=False, tf=self.tf
218
+ )
219
+ prices = pd.DataFrame(prices)
220
+ prices = prices.dropna(axis=0, how='any')
221
+ try:
222
+ weights = optimized_weights(prices=prices, freq=freq, method=optimer)
223
+ return {symbol: weight for symbol, weight in weights.items()}
224
+ except Exception:
225
+ return {symbol: 0.0 for symbol in self.symbols}
226
+
227
+ def get_quantity(self, symbol, weight, price=None, volume=None, maxqty=None) -> int:
177
228
  """
178
229
  Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
179
230
  The quantity calculated can be used to evalute a strategy's performance for each symbol
@@ -185,15 +236,26 @@ class MT5Strategy(Strategy):
185
236
  Returns:
186
237
  qty : The quantity to buy or sell for the symbol.
187
238
  """
188
- if self.volume is None and volume is None:
189
- raise ValueError("Volume must be provided for the method.")
190
- current_price = self.data.get_latest_bar_value(symbol, 'close')
191
- try:
192
- qty = math.ceil(self.volume or volume / current_price)
193
- qty = max(qty, 1) / self.max_trades[symbol]
194
- return max(math.ceil(qty), 1)
195
- except Exception:
196
- return 1
239
+ if (self._porfolio_value is None or weight == 0 or
240
+ self._porfolio_value == 0 or np.isnan(self._porfolio_value)):
241
+ return 0
242
+ if volume is None:
243
+ volume = round(self._porfolio_value * weight)
244
+ if price is None:
245
+ price = self.data.get_latest_bar_value(symbol, 'close')
246
+ if (price is None or not isinstance(price, (int, float, np.number))
247
+ or volume is None or not isinstance(volume, (int, float, np.number))
248
+ or np.isnan(float(price))
249
+ or np.isnan(float(volume))
250
+ ):
251
+ if weight != 0:
252
+ return 1
253
+ return 0
254
+ qty = round(volume / price, 2)
255
+ qty = max(qty, 0) / self.max_trades[symbol]
256
+ if maxqty is not None:
257
+ qty = min(qty, maxqty)
258
+ return max(round(qty, 2), 0)
197
259
 
198
260
  def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
199
261
  """
@@ -215,9 +277,18 @@ class MT5Strategy(Strategy):
215
277
 
216
278
  position = SignalEvent(id, symbol, dtime, signal,
217
279
  quantity=quantity, strength=strength, price=price)
218
- self.events.put(position)
219
- self.logger.info(
220
- f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
280
+ log = False
281
+ if signal in ['LONG', 'SHORT']:
282
+ if self._trades[symbol][signal] < self.max_trades[symbol] and quantity > 0:
283
+ self.events.put(position)
284
+ log = True
285
+ elif signal == 'EXIT':
286
+ if self._positions[symbol]['LONG'] > 0 or self._positions[symbol]['SHORT'] < 0:
287
+ self.events.put(position)
288
+ log = True
289
+ if log:
290
+ self.logger.info(
291
+ f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
221
292
 
222
293
  def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
223
294
  strength: float=1.0, dtime: datetime | pd.Timestamp=None):
@@ -341,55 +412,79 @@ class MT5Strategy(Strategy):
341
412
  """
342
413
  for symbol in self.symbols:
343
414
  dtime = self.data.get_latest_bar_datetime(symbol)
415
+ logmsg = lambda order, type: self.logger.info(
416
+ f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
417
+ f"PRICE @ {order.price}", custom_time=dtime)
344
418
  for order in self._orders[symbol]['BLMT'].copy():
345
419
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
346
420
  self.buy_mkt(order.strategy_id, symbol,
347
421
  order.price, order.quantity, dtime)
348
- self.logger.info(
349
- f"BUY LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
350
- f"PRICE @ {order.price}", custom_time=dtime)
351
- self._orders[symbol]['BLMT'].remove(order)
422
+ try:
423
+ self._orders[symbol]['BLMT'].remove(order)
424
+ assert order not in self._orders[symbol]['BLMT']
425
+ logmsg(order, 'BUY LIMIT')
426
+ except AssertionError:
427
+ self._orders[symbol]['BLMT'] = [o for o in self._orders[symbol]['BLMT'] if o != order]
428
+ logmsg(order, 'BUY LIMIT')
352
429
  for order in self._orders[symbol]['SLMT'].copy():
353
430
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
354
431
  self.sell_mkt(order.strategy_id, symbol,
355
432
  order.price, order.quantity, dtime)
356
- self.logger.info(
357
- f"SELL LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
358
- f"PRICE @ {order.price}", custom_time=dtime)
359
- self._orders[symbol]['SLMT'].remove(order)
433
+ try:
434
+ self._orders[symbol]['SLMT'].remove(order)
435
+ assert order not in self._orders[symbol]['SLMT']
436
+ logmsg(order, 'SELL LIMIT')
437
+ except AssertionError:
438
+ self._orders[symbol]['SLMT'] = [o for o in self._orders[symbol]['SLMT'] if o != order]
439
+ logmsg(order, 'SELL LIMIT')
360
440
  for order in self._orders[symbol]['BSTP'].copy():
361
441
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
362
442
  self.buy_mkt(order.strategy_id, symbol,
363
443
  order.price, order.quantity, dtime)
364
- self.logger.info(
365
- f"BUY STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
366
- f"PRICE @ {order.price}", custom_time=dtime)
367
- self._orders[symbol]['BSTP'].remove(order)
444
+ try:
445
+ self._orders[symbol]['BSTP'].remove(order)
446
+ assert order not in self._orders[symbol]['BSTP']
447
+ logmsg(order, 'BUY STOP')
448
+ except AssertionError:
449
+ self._orders[symbol]['BSTP'] = [o for o in self._orders[symbol]['BSTP'] if o != order]
450
+ logmsg(order, 'BUY STOP')
368
451
  for order in self._orders[symbol]['SSTP'].copy():
369
452
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
370
453
  self.sell_mkt(order.strategy_id, symbol,
371
454
  order.price, order.quantity, dtime)
372
- self.logger.info(
373
- f"SELL STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
374
- f"PRICE @ {order.price}", custom_time=dtime)
375
- self._orders[symbol]['SSTP'].remove(order)
455
+ try:
456
+ self._orders[symbol]['SSTP'].remove(order)
457
+ assert order not in self._orders[symbol]['SSTP']
458
+ logmsg(order, 'SELL STOP')
459
+ except AssertionError:
460
+ self._orders[symbol]['SSTP'] = [o for o in self._orders[symbol]['SSTP'] if o != order]
461
+ logmsg(order, 'SELL STOP')
376
462
  for order in self._orders[symbol]['BSTPLMT'].copy():
377
463
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
378
464
  self.buy_limit(order.strategy_id, symbol,
379
465
  order.stoplimit, order.quantity, dtime)
380
- self.logger.info(
381
- f"BUY STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
382
- f"PRICE @ {order.price}", custom_time=dtime)
383
- self._orders[symbol]['BSTPLMT'].remove(order)
466
+ try:
467
+ self._orders[symbol]['BSTPLMT'].remove(order)
468
+ assert order not in self._orders[symbol]['BSTPLMT']
469
+ logmsg(order, 'BUY STOP LIMIT')
470
+ except AssertionError:
471
+ self._orders[symbol]['BSTPLMT'] = [o for o in self._orders[symbol]['BSTPLMT'] if o != order]
472
+ logmsg(order, 'BUY STOP LIMIT')
384
473
  for order in self._orders[symbol]['SSTPLMT'].copy():
385
474
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
386
475
  self.sell_limit(order.strategy_id, symbol,
387
476
  order.stoplimit, order.quantity, dtime)
388
- self.logger.info(
389
- f"SELL STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
390
- f"PRICE @ {order.price}", custom_time=dtime)
391
- self._orders[symbol]['SSTPLMT'].remove(order)
392
-
477
+ try:
478
+ self._orders[symbol]['SSTPLMT'].remove(order)
479
+ assert order not in self._orders[symbol]['SSTPLMT']
480
+ logmsg(order, 'SELL STOP LIMIT')
481
+ except AssertionError:
482
+ self._orders[symbol]['SSTPLMT'] = [o for o in self._orders[symbol]['SSTPLMT'] if o != order]
483
+ logmsg(order, 'SELL STOP LIMIT')
484
+
485
+ def calculate_pct_change(self, current_price, lh_price):
486
+ return ((current_price - lh_price) / lh_price) * 100
487
+
393
488
  def get_asset_values(self,
394
489
  symbol_list: List[str],
395
490
  window: int,
@@ -688,7 +688,7 @@ class Account(object):
688
688
  "minors": r"\b(Minors?)\b",
689
689
  "exotics": r"\b(Exotics?)\b",
690
690
  }
691
- return self._get_symbols_by_category('forex', category, fx_categories)
691
+ return self._get_symbols_by_category('FX', category, fx_categories)
692
692
 
693
693
  def get_stocks_from_country(self, country_code: str = 'USA', etf=True) -> List[str]:
694
694
  """
@@ -156,6 +156,22 @@ class RiskManagement(Account):
156
156
  self.symbol_info = super().get_symbol_info(self.symbol)
157
157
 
158
158
  self._tf = time_frame
159
+
160
+ @property
161
+ def dailydd(self) -> float:
162
+ return self.daily_dd
163
+
164
+ @dailydd.setter
165
+ def dailydd(self, value: float):
166
+ self.daily_dd = value
167
+
168
+ @property
169
+ def maxrisk(self) -> float:
170
+ return self.max_risk
171
+
172
+ @maxrisk.setter
173
+ def maxrisk(self, value: float):
174
+ self.max_risk = value
159
175
 
160
176
  def _convert_time_frame(self, tf: str) -> int:
161
177
  """Convert time frame to minutes"""
@@ -437,14 +453,23 @@ class RiskManagement(Account):
437
453
  if trade_risk > 0:
438
454
  currency_risk = round(self.var_loss_value(), 5)
439
455
  volume = round(currency_risk*laverage)
440
- _lot = round((volume / (contract_size * av_price)), 2)
456
+ try:
457
+ _lot = round((volume / (contract_size * av_price)), 2)
458
+ except ZeroDivisionError:
459
+ _lot = 0.0
441
460
  lot = self._check_lot(_lot)
442
461
  if COMD and contract_size > 1:
443
462
  # lot = volume / av_price / contract_size
444
- lot = volume / av_price / contract_size
463
+ try:
464
+ lot = volume / av_price / contract_size
465
+ except ZeroDivisionError:
466
+ lot = 0.0
445
467
  lot = self._check_lot(_lot)
446
468
  if FX:
447
- __lot = round((volume / contract_size), 2)
469
+ try:
470
+ __lot = round((volume / contract_size), 2)
471
+ except ZeroDivisionError:
472
+ __lot = 0.0
448
473
  lot = self._check_lot(__lot)
449
474
 
450
475
  tick_value = s_info.trade_tick_value
@@ -3,4 +3,8 @@ The `models` module provides a foundational framework for implementing various q
3
3
 
4
4
  It is designed to be a versatile base module for different types of models used in financial analysis and trading.
5
5
  """
6
- from bbstrader.models.risk import *
6
+ from bbstrader.models.risk import *
7
+ from bbstrader.models.optimization import *
8
+ from bbstrader.models.portfolios import *
9
+ from bbstrader.models.factors import *
10
+ from bbstrader.models.ml import *
@@ -6,7 +6,14 @@ from pypfopt.efficient_frontier import EfficientFrontier
6
6
  from pypfopt.hierarchical_portfolio import HRPOpt
7
7
  import warnings
8
8
 
9
- def markowitz_weights(prices=None, freq=252):
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):
10
17
  """
11
18
  Calculates optimal portfolio weights using Markowitz's mean-variance optimization (Max Sharpe Ratio) with multiple solvers.
12
19
 
@@ -45,7 +52,7 @@ def markowitz_weights(prices=None, freq=252):
45
52
  weight_bounds=(0, 1),
46
53
  solver=solver)
47
54
  try:
48
- weights = ef.max_sharpe()
55
+ weights = ef.max_sharpe(risk_free_rate=rfr)
49
56
  return ef.clean_weights()
50
57
  except Exception as e:
51
58
  print(f"Solver {solver} failed with error: {e}")
@@ -83,7 +90,7 @@ def hierarchical_risk_parity(prices=None, returns=None, freq=252):
83
90
  if returns is None and prices is None:
84
91
  raise ValueError("Either prices or returns must be provided")
85
92
  if returns is None:
86
- returns = prices.pct_change().dropna()
93
+ returns = prices.pct_change().dropna(how='all')
87
94
  # Remove duplicate columns and index
88
95
  returns = returns.loc[:, ~returns.columns.duplicated()]
89
96
  returns = returns.loc[~returns.index.duplicated(keep='first')]
@@ -128,7 +135,7 @@ def equal_weighted(prices=None, returns=None, round_digits=5):
128
135
  columns = returns.columns
129
136
  return {col: round(1/n, round_digits) for col in columns}
130
137
 
131
- def optimized_weights(prices=None, returns=None, freq=252, method='markowitz'):
138
+ def optimized_weights(prices=None, returns=None, rfr=0.0, freq=252, method='equal'):
132
139
  """
133
140
  Selects an optimization method to calculate portfolio weights based on user preference.
134
141
 
@@ -161,7 +168,7 @@ def optimized_weights(prices=None, returns=None, freq=252, method='markowitz'):
161
168
  - 'equal': Equal weighting across all assets
162
169
  """
163
170
  if method == 'markowitz':
164
- return markowitz_weights(prices=prices, freq=freq)
171
+ return markowitz_weights(prices=prices, rfr=rfr, freq=freq)
165
172
  elif method == 'hrp':
166
173
  return hierarchical_risk_parity(prices=prices, returns=returns, freq=freq)
167
174
  elif method == 'equal':
@@ -10,6 +10,9 @@ from bbstrader.models.optimization import (
10
10
  equal_weighted
11
11
  )
12
12
 
13
+ __all__ = [
14
+ 'EigenPortfolios'
15
+ ]
13
16
 
14
17
  class EigenPortfolios(object):
15
18
  """
@@ -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
- time_frame, iter_time, use_trade_time, period, period_end_action, closing_pnl, trading_days,
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,6 +90,14 @@ 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
102
  def _send_notification(signal):
94
103
  send_message(message=signal, notify_me=notify,
@@ -160,6 +169,8 @@ def _mt5_execution(
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,7 +192,7 @@ 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:
187
198
  logger.info(info)
@@ -436,12 +447,12 @@ class MT5ExecutionEngine():
436
447
  strategy_cls: Strategy,
437
448
  /,
438
449
  mm: bool = True,
450
+ optimizer: str = 'equal',
439
451
  trail: bool = True,
440
452
  stop_trail: Optional[int] = None,
441
453
  trail_after_points: Optional[int] = None,
442
454
  be_plus_points: Optional[int] = None,
443
455
  show_positions_orders: bool = False,
444
- time_frame: str = '15m',
445
456
  iter_time: int | float = 5,
446
457
  use_trade_time: bool = True,
447
458
  period: Literal['day', 'week', 'month'] = 'week',
@@ -457,8 +468,9 @@ class MT5ExecutionEngine():
457
468
  trades_instances : Dictionary of Trade instances
458
469
  strategy_cls : Strategy class to use for trading
459
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.
460
473
  show_positions_orders : Print open positions and orders. Defaults to False.
461
- time_frame : Time frame to trade. Defaults to '15m'.
462
474
  iter_time : Interval to check for signals and `mm`. Defaults to 5.
463
475
  use_trade_time : Open trades after the time is completed. Defaults to True.
464
476
  period : Period to trade. Defaults to 'week'.
@@ -468,6 +480,7 @@ class MT5ExecutionEngine():
468
480
  trading_days : Trading days in a week. Defaults to monday to friday.
469
481
  comment: Comment for trades. Defaults to None.
470
482
  **kwargs: Additional keyword arguments
483
+ _ time_frame : Time frame to trade. Defaults to '15m'.
471
484
  - strategy_name (Optional[str]): Strategy name. Defaults to None.
472
485
  - max_trades (Dict[str, int]): Maximum trades per symbol. Defaults to None.
473
486
  - notify (bool): Enable notifications. Defaults to False.
@@ -497,12 +510,12 @@ class MT5ExecutionEngine():
497
510
  self.trades_instances = trades_instances
498
511
  self.strategy_cls = strategy_cls
499
512
  self.mm = mm
513
+ self.optimizer = optimizer
500
514
  self.trail = trail
501
515
  self.stop_trail = stop_trail
502
516
  self.trail_after_points = trail_after_points
503
517
  self.be_plus_points = be_plus_points
504
518
  self.show_positions_orders = show_positions_orders
505
- self.time_frame = time_frame
506
519
  self.iter_time = iter_time
507
520
  self.use_trade_time = use_trade_time
508
521
  self.period = period
@@ -519,12 +532,12 @@ class MT5ExecutionEngine():
519
532
  self.trades_instances,
520
533
  self.strategy_cls,
521
534
  mm=self.mm,
535
+ optimizer=self.optimizer,
522
536
  trail=self.trail,
523
537
  stop_trail=self.stop_trail,
524
538
  trail_after_points=self.trail_after_points,
525
539
  be_plus_points=self.be_plus_points,
526
540
  show_positions_orders=self.show_positions_orders,
527
- time_frame=self.time_frame,
528
541
  iter_time=self.iter_time,
529
542
  use_trade_time=self.use_trade_time,
530
543
  period=self.period,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bbstrader
3
- Version: 0.1.92
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/
@@ -17,7 +17,7 @@ with io.open(path.join(here, 'README.md'), encoding='utf-8') as f:
17
17
  with io.open(path.join(here, 'requirements.txt'), encoding='utf-8') as f:
18
18
  REQUIREMENTS = [line.rstrip() for line in f]
19
19
 
20
- VERSION = '0.1.92'
20
+ VERSION = '0.1.93'
21
21
  DESCRIPTION = 'Simplified Investment & Trading Toolkit'
22
22
 
23
23
  KEYWORDS = [
File without changes
File without changes
File without changes
File without changes
File without changes