bbstrader 0.1.91__tar.gz → 0.1.92__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 (40) hide show
  1. {bbstrader-0.1.91/bbstrader.egg-info → bbstrader-0.1.92}/PKG-INFO +2 -1
  2. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/backtest.py +11 -4
  3. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/data.py +10 -2
  4. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/event.py +10 -4
  5. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/execution.py +6 -6
  6. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/portfolio.py +51 -25
  7. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/strategy.py +101 -42
  8. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/rates.py +50 -2
  9. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/trade.py +7 -4
  10. bbstrader-0.1.92/bbstrader/models/optimization.py +170 -0
  11. bbstrader-0.1.92/bbstrader/models/portfolios.py +202 -0
  12. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/trading/execution.py +11 -9
  13. {bbstrader-0.1.91 → bbstrader-0.1.92/bbstrader.egg-info}/PKG-INFO +2 -1
  14. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader.egg-info/SOURCES.txt +1 -0
  15. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader.egg-info/requires.txt +1 -0
  16. {bbstrader-0.1.91 → bbstrader-0.1.92}/requirements.txt +2 -1
  17. {bbstrader-0.1.91 → bbstrader-0.1.92}/setup.py +1 -1
  18. bbstrader-0.1.91/bbstrader/models/optimization.py +0 -0
  19. {bbstrader-0.1.91 → bbstrader-0.1.92}/LICENSE +0 -0
  20. {bbstrader-0.1.91 → bbstrader-0.1.92}/MANIFEST.in +0 -0
  21. {bbstrader-0.1.91 → bbstrader-0.1.92}/README.md +0 -0
  22. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/__ini__.py +0 -0
  23. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/__init__.py +0 -0
  24. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/performance.py +0 -0
  25. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/config.py +0 -0
  26. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/__init__.py +0 -0
  27. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/account.py +0 -0
  28. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/risk.py +0 -0
  29. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/utils.py +0 -0
  30. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/models/__init__.py +0 -0
  31. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/models/factors.py +0 -0
  32. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/models/ml.py +0 -0
  33. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/models/risk.py +0 -0
  34. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/trading/__init__.py +0 -0
  35. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/trading/scripts.py +0 -0
  36. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/trading/strategies.py +0 -0
  37. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/tseries.py +0 -0
  38. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader.egg-info/dependency_links.txt +0 -0
  39. {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader.egg-info/top_level.txt +0 -0
  40. {bbstrader-0.1.91 → bbstrader-0.1.92}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bbstrader
3
- Version: 0.1.91
3
+ Version: 0.1.92
4
4
  Summary: Simplified Investment & Trading Toolkit
5
5
  Home-page: https://github.com/bbalouki/bbstrader
6
6
  Download-URL: https://pypi.org/project/bbstrader/
@@ -46,6 +46,7 @@ Requires-Dist: tqdm
46
46
  Requires-Dist: scikit-learn
47
47
  Requires-Dist: notify-py
48
48
  Requires-Dist: python-telegram-bot
49
+ Requires-Dist: pyportfolioopt
49
50
  Provides-Extra: mt5
50
51
  Requires-Dist: MetaTrader5; extra == "mt5"
51
52
 
@@ -111,8 +111,9 @@ class BacktestEngine(Backtest):
111
111
  their class types.
112
112
  """
113
113
  print(
114
- f"\nStarting Backtest on {self.symbol_list} "
115
- f"with ${self.initial_capital} Initial Capital\n"
114
+ f"\n[======= STARTING BACKTEST =======]\n"
115
+ f"START DATE: {self.start_date} \n"
116
+ f"INITIAL CAPITAL: {self.initial_capital}\n"
116
117
  )
117
118
  self.data_handler: DataHandler = self.dh_cls(
118
119
  self.events, self.symbol_list, **self.kwargs
@@ -141,8 +142,9 @@ class BacktestEngine(Backtest):
141
142
  self.data_handler.update_bars()
142
143
  self.strategy.check_pending_orders()
143
144
  else:
144
- print("\n[======= BACKTEST COMPLETE =======]\n")
145
- print(f"Total bars: {i} ")
145
+ print("\n[======= BACKTEST COMPLETED =======]")
146
+ print(f"END DATE: {self.data_handler.get_latest_bar_datetime()}")
147
+ print(f"TOTAL BARS: {i} ")
146
148
  break
147
149
 
148
150
  # Handle the events
@@ -168,6 +170,11 @@ class BacktestEngine(Backtest):
168
170
  elif event.type == 'FILL':
169
171
  self.fills += 1
170
172
  self.portfolio.update_fill(event)
173
+ 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
+ )
171
178
 
172
179
  time.sleep(self.heartbeat)
173
180
 
@@ -272,7 +272,11 @@ class BaseCSVDataHandler(DataHandler):
272
272
  print("Symbol not available in the historical data set.")
273
273
  raise
274
274
  else:
275
- return getattr(bars_list[-1][1], val_type)
275
+ try:
276
+ return getattr(bars_list[-1][1], val_type)
277
+ except AttributeError:
278
+ print(f"Value type {val_type} not available in the historical data set.")
279
+ raise
276
280
 
277
281
  def get_latest_bars_values(self, symbol: str, val_type: str, N=1) -> np.ndarray:
278
282
  """
@@ -285,7 +289,11 @@ class BaseCSVDataHandler(DataHandler):
285
289
  print("That symbol is not available in the historical data set.")
286
290
  raise
287
291
  else:
288
- return np.array([getattr(b[1], val_type) for b in bars_list])
292
+ try:
293
+ return np.array([getattr(b[1], val_type) for b in bars_list])
294
+ except AttributeError:
295
+ print(f"Value type {val_type} not available in the historical data set.")
296
+ raise
289
297
 
290
298
  def update_bars(self):
291
299
  """
@@ -102,14 +102,15 @@ class OrderEvent(Event):
102
102
 
103
103
  def __init__(self,
104
104
  symbol: str,
105
- order_type: Literal['MKT', 'LMT'],
105
+ order_type: Literal['MKT', 'LMT', 'STP', 'STPLMT'],
106
106
  quantity: int | float,
107
107
  direction: Literal['BUY', 'SELL'],
108
- price: int | float = None
108
+ price: int | float = None,
109
+ signal: str = None
109
110
  ):
110
111
  """
111
112
  Initialises the order type, setting whether it is
112
- a Market order ('MKT') or Limit order ('LMT'), has
113
+ a Market order ('MKT') or Limit order ('LMT'), or Stop order ('STP').
113
114
  a quantity (integral or float) and its direction ('BUY' or 'SELL').
114
115
 
115
116
  Args:
@@ -118,6 +119,7 @@ class OrderEvent(Event):
118
119
  quantity (int | float): Non-negative number for quantity.
119
120
  direction (str): 'BUY' or 'SELL' for long or short.
120
121
  price (int | float): The price at which to order.
122
+ signal (str): The signal that generated the order.
121
123
  """
122
124
  self.type = 'ORDER'
123
125
  self.symbol = symbol
@@ -125,6 +127,7 @@ class OrderEvent(Event):
125
127
  self.quantity = quantity
126
128
  self.direction = direction
127
129
  self.price = price
130
+ self.signal = signal
128
131
 
129
132
  def print_order(self):
130
133
  """
@@ -162,7 +165,8 @@ class FillEvent(Event):
162
165
  quantity: int | float,
163
166
  direction: Literal['BUY', 'SELL'],
164
167
  fill_cost: int | float | None,
165
- commission: float | None = None
168
+ commission: float | None = None,
169
+ order: str = None
166
170
  ):
167
171
  """
168
172
  Initialises the FillEvent object. Sets the symbol, exchange,
@@ -181,6 +185,7 @@ class FillEvent(Event):
181
185
  direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
182
186
  fill_cost (int | float): Price of the shares when filled.
183
187
  commission (float | None): An optional commission sent from IB.
188
+ order (str): The order that this fill is related
184
189
  """
185
190
  self.type = 'FILL'
186
191
  self.timeindex = timeindex
@@ -194,6 +199,7 @@ class FillEvent(Event):
194
199
  self.commission = self.calculate_ib_commission()
195
200
  else:
196
201
  self.commission = commission
202
+ self.order = order
197
203
 
198
204
  def calculate_ib_commission(self):
199
205
  """
@@ -4,7 +4,6 @@ from abc import ABCMeta, abstractmethod
4
4
  from bbstrader.btengine.event import FillEvent, OrderEvent
5
5
  from bbstrader.btengine.data import DataHandler
6
6
  from bbstrader.metatrader.account import Account
7
- from bbstrader.config import config_logger
8
7
 
9
8
  __all__ = [
10
9
  "ExecutionHandler",
@@ -26,7 +25,8 @@ class ExecutionHandler(metaclass=ABCMeta):
26
25
 
27
26
  The ExecutionHandler described here is exceedingly simple,
28
27
  since it fills all orders at the current market price.
29
- This is highly unrealistic, but serves as a good baseline for improvement.
28
+ This is highly unrealistic, for other markets thant ``CFDs``
29
+ but serves as a good baseline for improvement.
30
30
  """
31
31
 
32
32
  @abstractmethod
@@ -64,7 +64,7 @@ class SimExecutionHandler(ExecutionHandler):
64
64
  """
65
65
  self.events = events
66
66
  self.bardata = data
67
- self.logger = kwargs.get("logger", config_logger("execution.log"))
67
+ self.logger = kwargs.get("logger")
68
68
 
69
69
  def execute_order(self, event: OrderEvent):
70
70
  """
@@ -78,7 +78,7 @@ class SimExecutionHandler(ExecutionHandler):
78
78
  dtime = self.bardata.get_latest_bar_datetime(event.symbol)
79
79
  fill_event = FillEvent(
80
80
  dtime, event.symbol,
81
- 'ARCA', event.quantity, event.direction, None
81
+ 'ARCA', event.quantity, event.direction, order=event.signal
82
82
  )
83
83
  self.events.put(fill_event)
84
84
  self.logger.info(
@@ -121,7 +121,7 @@ class MT5ExecutionHandler(ExecutionHandler):
121
121
  """
122
122
  self.events = events
123
123
  self.bardata = data
124
- self.logger = kwargs.get("logger", config_logger("execution.log"))
124
+ self.logger = kwargs.get("logger")
125
125
  self.__account = Account()
126
126
 
127
127
  def _calculate_lot(self, symbol, quantity, price):
@@ -232,7 +232,7 @@ class MT5ExecutionHandler(ExecutionHandler):
232
232
  fill_event = FillEvent(
233
233
  timeindex=dtime, symbol=symbol,
234
234
  exchange='MT5', quantity=quantity, direction=direction,
235
- fill_cost=None, commission=fees
235
+ fill_cost=None, commission=fees, order=event.signal
236
236
  )
237
237
  self.events.put(fill_event)
238
238
  self.logger.info(
@@ -31,8 +31,6 @@ class Portfolio(object):
31
31
  """
32
32
  This describes a `Portfolio()` object that keeps track of the positions
33
33
  within a portfolio and generates orders of a fixed quantity of stock based on signals.
34
- More sophisticated portfolio objects could include risk management and position
35
- sizing tools (such as the `Kelly Criterion`).
36
34
 
37
35
  The portfolio order management system is possibly the most complex component of
38
36
  an event driven backtester. Its role is to keep track of all current market positions
@@ -57,10 +55,9 @@ class Portfolio(object):
57
55
  value (defaulting to `100,000 USD`) and others parameter based on the `Strategy` requirement.
58
56
 
59
57
  The `Portfolio` is designed to handle position sizing and current holdings,
60
- but will carry out trading orders in a "dumb" manner by simply sending them directly
61
- to the brokerage with a predetermined fixed quantity size, irrespective of cash held.
62
- These are all unrealistic assumptions, but they help to outline how a portfolio order
63
- management system (OMS) functions in an eventdriven fashion.
58
+ but will carry out trading orders by simply them to the brokerage with a predetermined
59
+ fixed quantity size, if the portfolio has enough cash to place the order.
60
+
64
61
 
65
62
  The portfolio contains the `all_positions` and `current_positions` members.
66
63
  The former stores a list of all previous positions recorded at the timestamp of a market data event.
@@ -108,7 +105,7 @@ class Portfolio(object):
108
105
  self.initial_capital = initial_capital
109
106
 
110
107
  self.timeframe = kwargs.get("time_frame", "D1")
111
- self.trading_hours = kwargs.get("session_duration", 6.5)
108
+ self.trading_hours = kwargs.get("session_duration", 23)
112
109
  self.benchmark = kwargs.get('benchmark', 'SPY')
113
110
  self.output_dir = kwargs.get('output_dir', None)
114
111
  self.strategy_name = kwargs.get('strategy_name', '')
@@ -174,6 +171,23 @@ class Portfolio(object):
174
171
  d['Commission'] = 0.0
175
172
  d['Total'] = self.initial_capital
176
173
  return d
174
+
175
+ def _get_price(self, symbol: str) -> float:
176
+ try:
177
+ price = self.bars.get_latest_bar_value(
178
+ symbol, "adj_close"
179
+ )
180
+ return price
181
+ except AttributeError:
182
+ try:
183
+ price = self.bars.get_latest_bar_value(
184
+ symbol, "close"
185
+ )
186
+ return price
187
+ except AttributeError:
188
+ raise AttributeError(
189
+ f"Bars object must have 'adj_close' or 'close' prices"
190
+ )
177
191
 
178
192
  def update_timeindex(self, event: MarketEvent):
179
193
  """
@@ -203,8 +217,8 @@ class Portfolio(object):
203
217
  dh['Total'] = self.current_holdings['Cash']
204
218
  for s in self.symbol_list:
205
219
  # Approximation to the real value
206
- market_value = self.current_positions[s] * \
207
- self.bars.get_latest_bar_value(s, "adj_close")
220
+ price = self._get_price(s)
221
+ market_value = self.current_positions[s] * price
208
222
  dh[s] = market_value
209
223
  dh['Total'] += market_value
210
224
 
@@ -245,9 +259,7 @@ class Portfolio(object):
245
259
  fill_dir = -1
246
260
 
247
261
  # Update holdings list with new quantities
248
- price = self.bars.get_latest_bar_value(
249
- fill.symbol, "adj_close"
250
- )
262
+ price = self._get_price(fill.symbol)
251
263
  cost = fill_dir * price * fill.quantity
252
264
  self.current_holdings[fill.symbol] += cost
253
265
  self.current_holdings['Commission'] += fill.commission
@@ -263,14 +275,16 @@ class Portfolio(object):
263
275
  self.update_positions_from_fill(event)
264
276
  self.update_holdings_from_fill(event)
265
277
 
266
- def generate_naive_order(self, signal: SignalEvent):
278
+ def generate_order(self, signal: SignalEvent):
267
279
  """
268
- Simply files an Order object as a constant quantity
269
- sizing of the signal object, without risk management or
270
- position sizing considerations.
280
+ Check if the portfolio has enough cash to place an order
281
+ and generate an OrderEvent, else return None.
271
282
 
272
283
  Args:
273
284
  signal (SignalEvent): The tuple containing Signal information.
285
+
286
+ Returns:
287
+ OrderEvent: The OrderEvent to be executed.
274
288
  """
275
289
  order = None
276
290
 
@@ -278,21 +292,33 @@ class Portfolio(object):
278
292
  direction = signal.signal_type
279
293
  quantity = signal.quantity
280
294
  strength = signal.strength
281
- price = signal.price
295
+ price = signal.price or self._get_price(symbol)
282
296
  cur_quantity = self.current_positions[symbol]
297
+ cash = self.current_holdings['Cash']
283
298
 
284
- order_type = 'MKT'
299
+ if direction in ['LONG', 'SHORT', 'EXIT']:
300
+ order_type = 'MKT'
301
+ else:
302
+ order_type = direction
285
303
  mkt_quantity = round(quantity * strength)
304
+ cost = mkt_quantity * price
305
+
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
286
312
 
287
- if direction == 'LONG' and cur_quantity == 0:
288
- order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY', price)
289
- if direction == 'SHORT' and cur_quantity == 0:
290
- order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL', price)
313
+ if new_quantity > 0 and direction == 'LONG':
314
+ order = OrderEvent(symbol, order_type, new_quantity, 'BUY', price, direction)
315
+ if new_quantity > 0 and direction == 'SHORT':
316
+ order = OrderEvent(symbol, order_type, new_quantity, 'SELL', price, direction)
291
317
 
292
318
  if direction == 'EXIT' and cur_quantity > 0:
293
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
319
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price, direction)
294
320
  if direction == 'EXIT' and cur_quantity < 0:
295
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
321
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price, direction)
296
322
 
297
323
  return order
298
324
 
@@ -302,7 +328,7 @@ class Portfolio(object):
302
328
  based on the portfolio logic.
303
329
  """
304
330
  if event.type == 'SIGNAL':
305
- order_event = self.generate_naive_order(event)
331
+ order_event = self.generate_order(event)
306
332
  self.events.put(order_event)
307
333
 
308
334
  def create_equity_curve_dataframe(self):
@@ -7,6 +7,7 @@ from queue import Queue
7
7
  from datetime import datetime
8
8
  from bbstrader.config import config_logger
9
9
  from bbstrader.btengine.event import SignalEvent
10
+ from bbstrader.btengine.event import FillEvent
10
11
  from bbstrader.btengine.data import DataHandler
11
12
  from bbstrader.metatrader.account import Account
12
13
  from bbstrader.metatrader.rates import Rates
@@ -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):
@@ -74,17 +78,71 @@ class MT5Strategy(Strategy):
74
78
  self.symbols = symbol_list
75
79
  self.mode = mode
76
80
  self.volume = kwargs.get("volume")
77
- self.logger = kwargs.get("logger", config_logger("mt5_strategy.log"))
78
- self._construct_positions_and_orders()
81
+ self.max_trades = kwargs.get("max_trades",
82
+ {symbol: 1 for symbol in self.symbols})
83
+ self.logger = kwargs.get("logger")
84
+ self._initialize_portfolio()
85
+
86
+ @property
87
+ def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
88
+ return self._orders
89
+
90
+ @property
91
+ def trades(self) -> Dict[str, Dict[str, int]]:
92
+ return self._trades
93
+
94
+ @property
95
+ def positions(self) -> Dict[str, Dict[str, int|float]]:
96
+ return self._positions
97
+
98
+ @property
99
+ def holdings(self) -> Dict[str, float]:
100
+ return self._holdings
79
101
 
80
- def _construct_positions_and_orders(self):
81
- self.positions: Dict[str, Dict[str, int]] = {}
82
- self.orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
102
+ def _initialize_portfolio(self):
83
103
  positions = ['LONG', 'SHORT']
84
104
  orders = ['BLMT', 'BSTP', 'BSTPLMT', 'SLMT', 'SSTP', 'SSTPLMT']
105
+ self._positions: Dict[str, Dict[str, int]] = {}
106
+ self._trades: Dict[str, Dict[str, int]] = {}
107
+ self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
85
108
  for symbol in self.symbols:
86
- self.positions[symbol] = {position: 0 for position in positions}
87
- self.orders[symbol] = {order: [] for order in orders}
109
+ self._positions[symbol] = {}
110
+ self._orders[symbol] = {}
111
+ self._trades[symbol] = {}
112
+ for position in positions:
113
+ self._trades[symbol][position] = 0
114
+ self._positions[symbol][position] = 0
115
+ for order in orders:
116
+ self._orders[symbol][order] = []
117
+ self._holdings = {s: 0.0 for s in self.symbols}
118
+
119
+ def get_update_from_portfolio(self, positions, holdings):
120
+ """
121
+ Update the positions and holdings for the strategy from the portfolio.
122
+
123
+ Positions are the number of shares of a security that are owned in long or short.
124
+ Holdings are the value (postions * price) of the security that are owned in long or short.
125
+
126
+ Args:
127
+ positions : The positions for the symbols in the strategy.
128
+ holdings : The holdings for the symbols in the strategy.
129
+ """
130
+ for symbol in self.symbols:
131
+ if symbol in positions:
132
+ if positions[symbol] > 0:
133
+ self._positions[symbol]['LONG'] = positions[symbol]
134
+ elif positions[symbol] < 0:
135
+ self._positions[symbol]['SHORT'] = positions[symbol]
136
+ if symbol in holdings:
137
+ self._holdings[symbol] = holdings[symbol]
138
+
139
+ def update_trades_from_fill(self, event: FillEvent):
140
+ """
141
+ This method updates the trades for the strategy based on the fill event.
142
+ It is used to keep track of the number of trades executed for each order.
143
+ """
144
+ if event.type == 'FILL':
145
+ self._trades[event.symbol][event.order] += 1
88
146
 
89
147
  def calculate_signals(self, *args, **kwargs
90
148
  ) -> Dict[str, Union[str, dict, None]] | None:
@@ -115,7 +173,7 @@ class MT5Strategy(Strategy):
115
173
  """
116
174
  pass
117
175
 
118
- def get_quantity(self, symbol) -> int:
176
+ def get_quantity(self, symbol, volume=None) -> int:
119
177
  """
120
178
  Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
121
179
  The quantity calculated can be used to evalute a strategy's performance for each symbol
@@ -127,11 +185,15 @@ class MT5Strategy(Strategy):
127
185
  Returns:
128
186
  qty : The quantity to buy or sell for the symbol.
129
187
  """
130
- if self.volume is None:
188
+ if self.volume is None and volume is None:
131
189
  raise ValueError("Volume must be provided for the method.")
132
190
  current_price = self.data.get_latest_bar_value(symbol, 'close')
133
- qty = math.ceil(self.volume / current_price)
134
- return max(qty, 1)
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
135
197
 
136
198
  def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
137
199
  """
@@ -157,7 +219,7 @@ class MT5Strategy(Strategy):
157
219
  self.logger.info(
158
220
  f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
159
221
 
160
- def buy(self, id: int, symbol: str, price: float, quantity: int,
222
+ def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
161
223
  strength: float=1.0, dtime: datetime | pd.Timestamp=None):
162
224
  """
163
225
  Open a long position
@@ -165,25 +227,22 @@ class MT5Strategy(Strategy):
165
227
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
166
228
  """
167
229
  self._send_order(id, symbol, 'LONG', strength, price, quantity, dtime)
168
- self.positions[symbol]['LONG'] += quantity
169
230
 
170
- def sell(self, id, symbol, price, quantity, strength=1.0, dtime=None):
231
+ def sell_mkt(self, id, symbol, price, quantity, strength=1.0, dtime=None):
171
232
  """
172
233
  Open a short position
173
234
 
174
235
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
175
236
  """
176
237
  self._send_order(id, symbol, 'SHORT', strength, price, quantity, dtime)
177
- self.positions[symbol]['SHORT'] += quantity
178
238
 
179
- def close(self, id, symbol, price, quantity, strength=1.0, dtime=None):
239
+ def close_positions(self, id, symbol, price, quantity, strength=1.0, dtime=None):
180
240
  """
181
- Close a position
241
+ Close a position or exit all positions
182
242
 
183
243
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
184
244
  """
185
245
  self._send_order(id, symbol, 'EXIT', strength, price, quantity, dtime)
186
- self.positions[symbol]['LONG'] -= quantity
187
246
 
188
247
  def buy_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
189
248
  """
@@ -197,7 +256,7 @@ class MT5Strategy(Strategy):
197
256
  "The buy_stop price must be greater than the current price.")
198
257
  order = SignalEvent(id, symbol, dtime, 'LONG',
199
258
  quantity=quantity, strength=strength, price=price)
200
- self.orders[symbol]['BSTP'].append(order)
259
+ self._orders[symbol]['BSTP'].append(order)
201
260
 
202
261
  def sell_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
203
262
  """
@@ -211,7 +270,7 @@ class MT5Strategy(Strategy):
211
270
  "The sell_stop price must be less than the current price.")
212
271
  order = SignalEvent(id, symbol, dtime, 'SHORT',
213
272
  quantity=quantity, strength=strength, price=price)
214
- self.orders[symbol]['SSTP'].append(order)
273
+ self._orders[symbol]['SSTP'].append(order)
215
274
 
216
275
  def buy_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
217
276
  """
@@ -225,7 +284,7 @@ class MT5Strategy(Strategy):
225
284
  "The buy_limit price must be less than the current price.")
226
285
  order = SignalEvent(id, symbol, dtime, 'LONG',
227
286
  quantity=quantity, strength=strength, price=price)
228
- self.orders[symbol]['BLMT'].append(order)
287
+ self._orders[symbol]['BLMT'].append(order)
229
288
 
230
289
  def sell_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
231
290
  """
@@ -239,7 +298,7 @@ class MT5Strategy(Strategy):
239
298
  "The sell_limit price must be greater than the current price.")
240
299
  order = SignalEvent(id, symbol, dtime, 'SHORT',
241
300
  quantity=quantity, strength=strength, price=price)
242
- self.orders[symbol]['SLMT'].append(order)
301
+ self._orders[symbol]['SLMT'].append(order)
243
302
 
244
303
  def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
245
304
  quantity: int, strength: float=1.0, dtime: datetime | pd.Timestamp = None):
@@ -257,7 +316,7 @@ class MT5Strategy(Strategy):
257
316
  f"The stop-limit price {stoplimit} must be greater than the price {price}.")
258
317
  order = SignalEvent(id, symbol, dtime, 'LONG',
259
318
  quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
260
- self.orders[symbol]['BSTPLMT'].append(order)
319
+ self._orders[symbol]['BSTPLMT'].append(order)
261
320
 
262
321
  def sell_stop_limit(self, id, symbol, price, stoplimit, quantity, strength=1.0, dtime=None):
263
322
  """
@@ -274,7 +333,7 @@ class MT5Strategy(Strategy):
274
333
  f"The stop-limit price {stoplimit} must be less than the price {price}.")
275
334
  order = SignalEvent(id, symbol, dtime, 'SHORT',
276
335
  quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
277
- self.orders[symbol]['SSTPLMT'].append(order)
336
+ self._orders[symbol]['SSTPLMT'].append(order)
278
337
 
279
338
  def check_pending_orders(self):
280
339
  """
@@ -282,54 +341,54 @@ class MT5Strategy(Strategy):
282
341
  """
283
342
  for symbol in self.symbols:
284
343
  dtime = self.data.get_latest_bar_datetime(symbol)
285
- for order in self.orders[symbol]['BLMT'].copy():
344
+ for order in self._orders[symbol]['BLMT'].copy():
286
345
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
287
- self.buy(order.strategy_id, symbol,
346
+ self.buy_mkt(order.strategy_id, symbol,
288
347
  order.price, order.quantity, dtime)
289
348
  self.logger.info(
290
349
  f"BUY LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
291
350
  f"PRICE @ {order.price}", custom_time=dtime)
292
- self.orders[symbol]['BLMT'].remove(order)
293
- for order in self.orders[symbol]['SLMT'].copy():
351
+ self._orders[symbol]['BLMT'].remove(order)
352
+ for order in self._orders[symbol]['SLMT'].copy():
294
353
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
295
- self.sell(order.strategy_id, symbol,
354
+ self.sell_mkt(order.strategy_id, symbol,
296
355
  order.price, order.quantity, dtime)
297
356
  self.logger.info(
298
357
  f"SELL LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
299
358
  f"PRICE @ {order.price}", custom_time=dtime)
300
- self.orders[symbol]['SLMT'].remove(order)
301
- for order in self.orders[symbol]['BSTP'].copy():
359
+ self._orders[symbol]['SLMT'].remove(order)
360
+ for order in self._orders[symbol]['BSTP'].copy():
302
361
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
303
- self.buy(order.strategy_id, symbol,
362
+ self.buy_mkt(order.strategy_id, symbol,
304
363
  order.price, order.quantity, dtime)
305
364
  self.logger.info(
306
365
  f"BUY STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
307
366
  f"PRICE @ {order.price}", custom_time=dtime)
308
- self.orders[symbol]['BSTP'].remove(order)
309
- for order in self.orders[symbol]['SSTP'].copy():
367
+ self._orders[symbol]['BSTP'].remove(order)
368
+ for order in self._orders[symbol]['SSTP'].copy():
310
369
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
311
- self.sell(order.strategy_id, symbol,
370
+ self.sell_mkt(order.strategy_id, symbol,
312
371
  order.price, order.quantity, dtime)
313
372
  self.logger.info(
314
373
  f"SELL STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
315
374
  f"PRICE @ {order.price}", custom_time=dtime)
316
- self.orders[symbol]['SSTP'].remove(order)
317
- for order in self.orders[symbol]['BSTPLMT'].copy():
375
+ self._orders[symbol]['SSTP'].remove(order)
376
+ for order in self._orders[symbol]['BSTPLMT'].copy():
318
377
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
319
378
  self.buy_limit(order.strategy_id, symbol,
320
379
  order.stoplimit, order.quantity, dtime)
321
380
  self.logger.info(
322
381
  f"BUY STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
323
382
  f"PRICE @ {order.price}", custom_time=dtime)
324
- self.orders[symbol]['BSTPLMT'].remove(order)
325
- for order in self.orders[symbol]['SSTPLMT'].copy():
383
+ self._orders[symbol]['BSTPLMT'].remove(order)
384
+ for order in self._orders[symbol]['SSTPLMT'].copy():
326
385
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
327
386
  self.sell_limit(order.strategy_id, symbol,
328
387
  order.stoplimit, order.quantity, dtime)
329
388
  self.logger.info(
330
389
  f"SELL STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
331
390
  f"PRICE @ {order.price}", custom_time=dtime)
332
- self.orders[symbol]['SSTPLMT'].remove(order)
391
+ self._orders[symbol]['SSTPLMT'].remove(order)
333
392
 
334
393
  def get_asset_values(self,
335
394
  symbol_list: List[str],