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.

@@ -1,12 +1,12 @@
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
7
6
  from datetime import datetime
8
7
  from bbstrader.config import config_logger
9
8
  from bbstrader.btengine.event import SignalEvent
9
+ from bbstrader.btengine.event import FillEvent
10
10
  from bbstrader.btengine.data import DataHandler
11
11
  from bbstrader.metatrader.account import Account
12
12
  from bbstrader.metatrader.rates import Rates
@@ -17,6 +17,7 @@ from typing import (
17
17
  List,
18
18
  Literal
19
19
  )
20
+ from bbstrader.models.optimization import optimized_weights
20
21
 
21
22
  __all__ = ['Strategy', 'MT5Strategy']
22
23
 
@@ -38,6 +39,8 @@ class Strategy(metaclass=ABCMeta):
38
39
 
39
40
  The strategy hierarchy is relatively simple as it consists of an abstract
40
41
  base class with a single pure virtual method for generating `SignalEvent` objects.
42
+ Other methods are provided to check for pending orders, update trades from fills,
43
+ and get updates from the portfolio.
41
44
  """
42
45
 
43
46
  @abstractmethod
@@ -45,8 +48,9 @@ class Strategy(metaclass=ABCMeta):
45
48
  raise NotImplementedError(
46
49
  "Should implement calculate_signals()"
47
50
  )
48
-
49
- def check_pending_orders(self): ...
51
+ def check_pending_orders(self, *args, **kwargs): ...
52
+ def get_update_from_portfolio(self, *args, **kwargs): ...
53
+ def update_trades_from_fill(self, *args, **kwargs): ...
50
54
 
51
55
 
52
56
  class MT5Strategy(Strategy):
@@ -68,23 +72,110 @@ class MT5Strategy(Strategy):
68
72
  bars : The data handler object.
69
73
  mode : The mode of operation for the strategy (backtest or live).
70
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.
71
78
  """
72
79
  self.events = events
73
80
  self.data = bars
74
81
  self.symbols = symbol_list
75
82
  self.mode = mode
76
- self.volume = kwargs.get("volume")
77
- self.logger = kwargs.get("logger", config_logger("mt5_strategy.log"))
78
- self._construct_positions_and_orders()
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')
87
+ self.logger = kwargs.get("logger")
88
+ self._initialize_portfolio()
89
+
90
+ @property
91
+ def cash(self) -> float:
92
+ return self._porfolio_value
93
+
94
+ @cash.setter
95
+ def cash(self, value):
96
+ self._porfolio_value = value
97
+
98
+ @property
99
+ def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
100
+ return self._orders
101
+
102
+ @property
103
+ def trades(self) -> Dict[str, Dict[str, int]]:
104
+ return self._trades
79
105
 
80
- def _construct_positions_and_orders(self):
81
- self.positions: Dict[str, Dict[str, int]] = {}
82
- self.orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
106
+ @property
107
+ def positions(self) -> Dict[str, Dict[str, int|float]]:
108
+ return self._positions
109
+
110
+ @property
111
+ def holdings(self) -> Dict[str, float]:
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
126
+
127
+ def _initialize_portfolio(self):
83
128
  positions = ['LONG', 'SHORT']
84
129
  orders = ['BLMT', 'BSTP', 'BSTPLMT', 'SLMT', 'SSTP', 'SSTPLMT']
130
+ self._positions: Dict[str, Dict[str, int | float]] = {}
131
+ self._trades: Dict[str, Dict[str, int]] = {}
132
+ self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
133
+ for symbol in self.symbols:
134
+ self._positions[symbol] = {}
135
+ self._orders[symbol] = {}
136
+ self._trades[symbol] = {}
137
+ for position in positions:
138
+ self._trades[symbol][position] = 0
139
+ self._positions[symbol][position] = 0.0
140
+ for order in orders:
141
+ self._orders[symbol][order] = []
142
+ self._holdings = {s: 0.0 for s in self.symbols}
143
+
144
+ def get_update_from_portfolio(self, positions, holdings):
145
+ """
146
+ Update the positions and holdings for the strategy from the portfolio.
147
+
148
+ Positions are the number of shares of a security that are owned in long or short.
149
+ Holdings are the value (postions * price) of the security that are owned in long or short.
150
+
151
+ Args:
152
+ positions : The positions for the symbols in the strategy.
153
+ holdings : The holdings for the symbols in the strategy.
154
+ """
85
155
  for symbol in self.symbols:
86
- self.positions[symbol] = {position: 0 for position in positions}
87
- self.orders[symbol] = {order: [] for order in orders}
156
+ if symbol in positions:
157
+ if positions[symbol] > 0:
158
+ self._positions[symbol]['LONG'] = positions[symbol]
159
+ elif positions[symbol] < 0:
160
+ self._positions[symbol]['SHORT'] = positions[symbol]
161
+ else:
162
+ self._positions[symbol]['LONG'] = 0
163
+ self._positions[symbol]['SHORT'] = 0
164
+ if symbol in holdings:
165
+ self._holdings[symbol] = holdings[symbol]
166
+
167
+ def update_trades_from_fill(self, event: FillEvent):
168
+ """
169
+ This method updates the trades for the strategy based on the fill event.
170
+ It is used to keep track of the number of trades executed for each order.
171
+ """
172
+ if event.type == 'FILL':
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
88
179
 
89
180
  def calculate_signals(self, *args, **kwargs
90
181
  ) -> Dict[str, Union[str, dict, None]] | None:
@@ -115,7 +206,25 @@ class MT5Strategy(Strategy):
115
206
  """
116
207
  pass
117
208
 
118
- def get_quantity(self, symbol) -> 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:
119
228
  """
120
229
  Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
121
230
  The quantity calculated can be used to evalute a strategy's performance for each symbol
@@ -127,11 +236,26 @@ class MT5Strategy(Strategy):
127
236
  Returns:
128
237
  qty : The quantity to buy or sell for the symbol.
129
238
  """
130
- if self.volume is None:
131
- raise ValueError("Volume must be provided for the method.")
132
- current_price = self.data.get_latest_bar_value(symbol, 'close')
133
- qty = math.ceil(self.volume / current_price)
134
- return max(qty, 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)
135
259
 
136
260
  def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
137
261
  """
@@ -153,11 +277,20 @@ class MT5Strategy(Strategy):
153
277
 
154
278
  position = SignalEvent(id, symbol, dtime, signal,
155
279
  quantity=quantity, strength=strength, price=price)
156
- self.events.put(position)
157
- self.logger.info(
158
- f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
159
-
160
- def buy(self, id: int, symbol: str, price: float, quantity: int,
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)
292
+
293
+ def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
161
294
  strength: float=1.0, dtime: datetime | pd.Timestamp=None):
162
295
  """
163
296
  Open a long position
@@ -165,25 +298,22 @@ class MT5Strategy(Strategy):
165
298
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
166
299
  """
167
300
  self._send_order(id, symbol, 'LONG', strength, price, quantity, dtime)
168
- self.positions[symbol]['LONG'] += quantity
169
301
 
170
- def sell(self, id, symbol, price, quantity, strength=1.0, dtime=None):
302
+ def sell_mkt(self, id, symbol, price, quantity, strength=1.0, dtime=None):
171
303
  """
172
304
  Open a short position
173
305
 
174
306
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
175
307
  """
176
308
  self._send_order(id, symbol, 'SHORT', strength, price, quantity, dtime)
177
- self.positions[symbol]['SHORT'] += quantity
178
309
 
179
- def close(self, id, symbol, price, quantity, strength=1.0, dtime=None):
310
+ def close_positions(self, id, symbol, price, quantity, strength=1.0, dtime=None):
180
311
  """
181
- Close a position
312
+ Close a position or exit all positions
182
313
 
183
314
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
184
315
  """
185
316
  self._send_order(id, symbol, 'EXIT', strength, price, quantity, dtime)
186
- self.positions[symbol]['LONG'] -= quantity
187
317
 
188
318
  def buy_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
189
319
  """
@@ -197,7 +327,7 @@ class MT5Strategy(Strategy):
197
327
  "The buy_stop price must be greater than the current price.")
198
328
  order = SignalEvent(id, symbol, dtime, 'LONG',
199
329
  quantity=quantity, strength=strength, price=price)
200
- self.orders[symbol]['BSTP'].append(order)
330
+ self._orders[symbol]['BSTP'].append(order)
201
331
 
202
332
  def sell_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
203
333
  """
@@ -211,7 +341,7 @@ class MT5Strategy(Strategy):
211
341
  "The sell_stop price must be less than the current price.")
212
342
  order = SignalEvent(id, symbol, dtime, 'SHORT',
213
343
  quantity=quantity, strength=strength, price=price)
214
- self.orders[symbol]['SSTP'].append(order)
344
+ self._orders[symbol]['SSTP'].append(order)
215
345
 
216
346
  def buy_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
217
347
  """
@@ -225,7 +355,7 @@ class MT5Strategy(Strategy):
225
355
  "The buy_limit price must be less than the current price.")
226
356
  order = SignalEvent(id, symbol, dtime, 'LONG',
227
357
  quantity=quantity, strength=strength, price=price)
228
- self.orders[symbol]['BLMT'].append(order)
358
+ self._orders[symbol]['BLMT'].append(order)
229
359
 
230
360
  def sell_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
231
361
  """
@@ -239,7 +369,7 @@ class MT5Strategy(Strategy):
239
369
  "The sell_limit price must be greater than the current price.")
240
370
  order = SignalEvent(id, symbol, dtime, 'SHORT',
241
371
  quantity=quantity, strength=strength, price=price)
242
- self.orders[symbol]['SLMT'].append(order)
372
+ self._orders[symbol]['SLMT'].append(order)
243
373
 
244
374
  def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
245
375
  quantity: int, strength: float=1.0, dtime: datetime | pd.Timestamp = None):
@@ -257,7 +387,7 @@ class MT5Strategy(Strategy):
257
387
  f"The stop-limit price {stoplimit} must be greater than the price {price}.")
258
388
  order = SignalEvent(id, symbol, dtime, 'LONG',
259
389
  quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
260
- self.orders[symbol]['BSTPLMT'].append(order)
390
+ self._orders[symbol]['BSTPLMT'].append(order)
261
391
 
262
392
  def sell_stop_limit(self, id, symbol, price, stoplimit, quantity, strength=1.0, dtime=None):
263
393
  """
@@ -274,7 +404,7 @@ class MT5Strategy(Strategy):
274
404
  f"The stop-limit price {stoplimit} must be less than the price {price}.")
275
405
  order = SignalEvent(id, symbol, dtime, 'SHORT',
276
406
  quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
277
- self.orders[symbol]['SSTPLMT'].append(order)
407
+ self._orders[symbol]['SSTPLMT'].append(order)
278
408
 
279
409
  def check_pending_orders(self):
280
410
  """
@@ -282,55 +412,79 @@ class MT5Strategy(Strategy):
282
412
  """
283
413
  for symbol in self.symbols:
284
414
  dtime = self.data.get_latest_bar_datetime(symbol)
285
- for order in self.orders[symbol]['BLMT'].copy():
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)
418
+ for order in self._orders[symbol]['BLMT'].copy():
286
419
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
287
- self.buy(order.strategy_id, symbol,
420
+ self.buy_mkt(order.strategy_id, symbol,
288
421
  order.price, order.quantity, dtime)
289
- self.logger.info(
290
- f"BUY LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
291
- f"PRICE @ {order.price}", custom_time=dtime)
292
- self.orders[symbol]['BLMT'].remove(order)
293
- for order in self.orders[symbol]['SLMT'].copy():
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')
429
+ for order in self._orders[symbol]['SLMT'].copy():
294
430
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
295
- self.sell(order.strategy_id, symbol,
431
+ self.sell_mkt(order.strategy_id, symbol,
296
432
  order.price, order.quantity, dtime)
297
- self.logger.info(
298
- f"SELL LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
299
- f"PRICE @ {order.price}", custom_time=dtime)
300
- self.orders[symbol]['SLMT'].remove(order)
301
- for order in self.orders[symbol]['BSTP'].copy():
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')
440
+ for order in self._orders[symbol]['BSTP'].copy():
302
441
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
303
- self.buy(order.strategy_id, symbol,
442
+ self.buy_mkt(order.strategy_id, symbol,
304
443
  order.price, order.quantity, dtime)
305
- self.logger.info(
306
- f"BUY STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
307
- f"PRICE @ {order.price}", custom_time=dtime)
308
- self.orders[symbol]['BSTP'].remove(order)
309
- for order in self.orders[symbol]['SSTP'].copy():
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')
451
+ for order in self._orders[symbol]['SSTP'].copy():
310
452
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
311
- self.sell(order.strategy_id, symbol,
453
+ self.sell_mkt(order.strategy_id, symbol,
312
454
  order.price, order.quantity, dtime)
313
- self.logger.info(
314
- f"SELL STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
315
- f"PRICE @ {order.price}", custom_time=dtime)
316
- self.orders[symbol]['SSTP'].remove(order)
317
- for order in self.orders[symbol]['BSTPLMT'].copy():
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')
462
+ for order in self._orders[symbol]['BSTPLMT'].copy():
318
463
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
319
464
  self.buy_limit(order.strategy_id, symbol,
320
465
  order.stoplimit, order.quantity, dtime)
321
- self.logger.info(
322
- f"BUY STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
323
- f"PRICE @ {order.price}", custom_time=dtime)
324
- self.orders[symbol]['BSTPLMT'].remove(order)
325
- for order in self.orders[symbol]['SSTPLMT'].copy():
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')
473
+ for order in self._orders[symbol]['SSTPLMT'].copy():
326
474
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
327
475
  self.sell_limit(order.strategy_id, symbol,
328
476
  order.stoplimit, order.quantity, dtime)
329
- self.logger.info(
330
- f"SELL STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
331
- f"PRICE @ {order.price}", custom_time=dtime)
332
- self.orders[symbol]['SSTPLMT'].remove(order)
333
-
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
+
334
488
  def get_asset_values(self,
335
489
  symbol_list: List[str],
336
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
  """
@@ -178,8 +178,10 @@ class Rates(object):
178
178
  return TIMEFRAMES[time_frame]
179
179
 
180
180
  def _fetch_data(
181
- self, start: Union[int, datetime , pd.Timestamp],
182
- count: Union[int, datetime, pd.Timestamp], lower_colnames=False, utc=False,
181
+ self,
182
+ start: Union[int, datetime, pd.Timestamp],
183
+ count: Union[int, datetime, pd.Timestamp],
184
+ lower_colnames=False, utc=False,
183
185
  ) -> Union[pd.DataFrame, None]:
184
186
  """Fetches data from MT5 and returns a DataFrame or None."""
185
187
  try:
@@ -187,6 +189,13 @@ class Rates(object):
187
189
  rates = Mt5.copy_rates_from_pos(
188
190
  self.symbol, self.time_frame, start, count
189
191
  )
192
+ elif (
193
+ isinstance(start, (datetime, pd.Timestamp)) and
194
+ isinstance(count, int)
195
+ ):
196
+ rates = Mt5.copy_rates_from(
197
+ self.symbol, self.time_frame, start, count
198
+ )
190
199
  elif (
191
200
  isinstance(start, (datetime, pd.Timestamp)) and
192
201
  isinstance(count, (datetime, pd.Timestamp))
@@ -320,6 +329,35 @@ class Rates(object):
320
329
  return self._filter_data(df, fill_na=fill_na)
321
330
  return df
322
331
 
332
+ def get_rates_from(self, date_from: datetime | pd.Timestamp, count: int=MAX_BARS,
333
+ filter=False, fill_na=False, lower_colnames=False, utc=False) -> Union[pd.DataFrame, None]:
334
+ """
335
+ Retrieves historical data within a specified date range.
336
+
337
+ Args:
338
+ date_from : Starting date for data retrieval.
339
+ The data will be retrieved from this date going to the past.
340
+
341
+ count : Number of bars to retrieve.
342
+
343
+ filter : See `Rates.get_historical_data` for more details.
344
+ fill_na : See `Rates.get_historical_data` for more details.
345
+ lower_colnames : If True, the column names will be converted to lowercase.
346
+ utc (bool, optional): If True, the data will be in UTC timezone.
347
+ Defaults to False.
348
+
349
+ Returns:
350
+ Union[pd.DataFrame, None]: A DataFrame containing historical
351
+ data if successful, otherwise None.
352
+ """
353
+ utc = self._check_filter(filter, utc)
354
+ df = self._fetch_data(date_from, count, lower_colnames=lower_colnames, utc=utc)
355
+ if df is None:
356
+ return None
357
+ if filter:
358
+ return self._filter_data(df, fill_na=fill_na)
359
+ return df
360
+
323
361
  @property
324
362
  def open(self):
325
363
  return self.__data['Open']
@@ -459,4 +497,14 @@ def get_data_from_pos(symbol, time_frame, start_pos=0, fill_na=False,
459
497
  rates = Rates(symbol, time_frame, start_pos, count, session_duration)
460
498
  data = rates.get_rates_from_pos(filter=filter, fill_na=fill_na,
461
499
  lower_colnames=lower_colnames, utc=utc)
500
+ return data
501
+
502
+ def get_data_from_date(symbol, time_frame, date_from, count=MAX_BARS, fill_na=False,
503
+ lower_colnames=False, utc=False, filter=False):
504
+ """Get historical data from a specific date.
505
+ See `Rates.get_rates_from` for more details.
506
+ """
507
+ rates = Rates(symbol, time_frame)
508
+ data = rates.get_rates_from(date_from, count, filter=filter, fill_na=fill_na,
509
+ lower_colnames=lower_colnames, utc=utc)
462
510
  return data
@@ -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
@@ -838,14 +838,16 @@ class Trade(RiskManagement):
838
838
  elif account and id is not None:
839
839
  # All open positions for a specific strategy or expert no matter the symbol
840
840
  positions = self.get_positions()
841
- positions = [position for position in positions if position.magic == id]
841
+ if positions is not None:
842
+ positions = [position for position in positions if position.magic == id]
842
843
  elif not account and id is None:
843
844
  # All open positions for the current symbol no matter the strategy or expert
844
845
  positions = self.get_positions(symbol=self.symbol)
845
846
  elif not account and id is not None:
846
847
  # All open positions for the current symbol and a specific strategy or expert
847
848
  positions = self.get_positions(symbol=self.symbol)
848
- positions = [position for position in positions if position.magic == id]
849
+ if positions is not None:
850
+ positions = [position for position in positions if position.magic == id]
849
851
  profit = 0.0
850
852
  balance = self.get_account_info().balance
851
853
  target = round((balance * self.target)/100, 2)
@@ -1026,8 +1028,9 @@ class Trade(RiskManagement):
1026
1028
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1027
1029
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
1028
1030
  msg = trade_retcode_message(result.retcode)
1029
- self.logger.error(
1030
- f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1031
+ if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
1032
+ self.logger.error(
1033
+ f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1031
1034
  tries = 0
1032
1035
  while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
1033
1036
  if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
@@ -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 *