bbstrader 0.3.5__py3-none-any.whl → 0.3.7__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.

Files changed (45) hide show
  1. bbstrader/__init__.py +11 -2
  2. bbstrader/__main__.py +6 -1
  3. bbstrader/apps/_copier.py +43 -40
  4. bbstrader/btengine/backtest.py +33 -28
  5. bbstrader/btengine/data.py +105 -81
  6. bbstrader/btengine/event.py +21 -22
  7. bbstrader/btengine/execution.py +51 -24
  8. bbstrader/btengine/performance.py +23 -12
  9. bbstrader/btengine/portfolio.py +40 -30
  10. bbstrader/btengine/scripts.py +13 -12
  11. bbstrader/btengine/strategy.py +396 -134
  12. bbstrader/compat.py +4 -3
  13. bbstrader/config.py +20 -36
  14. bbstrader/core/data.py +76 -48
  15. bbstrader/core/scripts.py +22 -21
  16. bbstrader/core/utils.py +13 -12
  17. bbstrader/metatrader/account.py +51 -26
  18. bbstrader/metatrader/analysis.py +30 -16
  19. bbstrader/metatrader/copier.py +75 -40
  20. bbstrader/metatrader/trade.py +29 -39
  21. bbstrader/metatrader/utils.py +5 -4
  22. bbstrader/models/nlp.py +83 -66
  23. bbstrader/trading/execution.py +45 -22
  24. bbstrader/tseries.py +158 -166
  25. {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/METADATA +7 -21
  26. bbstrader-0.3.7.dist-info/RECORD +62 -0
  27. bbstrader-0.3.7.dist-info/top_level.txt +3 -0
  28. docs/conf.py +56 -0
  29. tests/__init__.py +0 -0
  30. tests/engine/__init__.py +1 -0
  31. tests/engine/test_backtest.py +58 -0
  32. tests/engine/test_data.py +536 -0
  33. tests/engine/test_events.py +300 -0
  34. tests/engine/test_execution.py +219 -0
  35. tests/engine/test_portfolio.py +308 -0
  36. tests/metatrader/__init__.py +0 -0
  37. tests/metatrader/test_account.py +1769 -0
  38. tests/metatrader/test_rates.py +292 -0
  39. tests/metatrader/test_risk_management.py +700 -0
  40. tests/metatrader/test_trade.py +439 -0
  41. bbstrader-0.3.5.dist-info/RECORD +0 -49
  42. bbstrader-0.3.5.dist-info/top_level.txt +0 -1
  43. {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/WHEEL +0 -0
  44. {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/entry_points.txt +0 -0
  45. {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,7 @@ import string
2
2
  from abc import ABCMeta, abstractmethod
3
3
  from datetime import datetime
4
4
  from queue import Queue
5
- from typing import Dict, List, Literal, Union
5
+ from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
6
6
 
7
7
  import numpy as np
8
8
  import pandas as pd
@@ -11,19 +11,19 @@ from loguru import logger
11
11
 
12
12
  from bbstrader.btengine.data import DataHandler
13
13
  from bbstrader.btengine.event import Events, FillEvent, SignalEvent
14
- from bbstrader.metatrader.trade import generate_signal, TradeAction
15
14
  from bbstrader.config import BBSTRADER_DIR
16
15
  from bbstrader.metatrader import (
17
16
  Account,
18
17
  AdmiralMarktsGroup,
19
18
  MetaQuotes,
20
19
  PepperstoneGroupLimited,
21
- TradeOrder,
22
20
  Rates,
23
- TradeSignal,
21
+ SymbolType,
22
+ TradeOrder,
23
+ TradeSignal,
24
24
  TradingMode,
25
- SymbolType
26
25
  )
26
+ from bbstrader.metatrader.trade import TradeAction, generate_signal
27
27
  from bbstrader.models.optimization import optimized_weights
28
28
 
29
29
  __all__ = ["Strategy", "MT5Strategy"]
@@ -59,13 +59,13 @@ class Strategy(metaclass=ABCMeta):
59
59
  """
60
60
 
61
61
  @abstractmethod
62
- def calculate_signals(self, *args, **kwargs) -> List[TradeSignal]:
63
- pass
62
+ def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal]:
63
+ raise NotImplementedError("Should implement calculate_signals()")
64
64
 
65
- def check_pending_orders(self, *args, **kwargs): ...
66
- def get_update_from_portfolio(self, *args, **kwargs): ...
67
- def update_trades_from_fill(self, *args, **kwargs): ...
68
- def perform_period_end_checks(self, *args, **kwargs): ...
65
+ def check_pending_orders(self, *args: Any, **kwargs: Any) -> None: ...
66
+ def get_update_from_portfolio(self, *args: Any, **kwargs: Any) -> None: ...
67
+ def update_trades_from_fill(self, *args: Any, **kwargs: Any) -> None: ...
68
+ def perform_period_end_checks(self, *args: Any, **kwargs: Any) -> None: ...
69
69
 
70
70
 
71
71
  class MT5Strategy(Strategy):
@@ -79,25 +79,37 @@ class MT5Strategy(Strategy):
79
79
  It is recommanded that every strategy specfic method to be a private method
80
80
  in order to avoid naming collusion.
81
81
  """
82
+
82
83
  tf: str
83
84
  id: int
84
85
  ID: int
85
-
86
+
86
87
  max_trades: Dict[str, int]
87
- risk_budget: Dict[str, float] | str | None
88
+ risk_budget: Optional[Union[Dict[str, float], str]]
88
89
 
89
90
  _orders: Dict[str, Dict[str, List[SignalEvent]]]
90
- _positions: Dict[str, Dict[str, int | float]]
91
+ _positions: Dict[str, Dict[str, Union[int, float]]]
91
92
  _trades: Dict[str, Dict[str, int]]
93
+ _holdings: Dict[str, float]
94
+ _porfolio_value: Optional[float]
95
+ events: "Queue[Union[SignalEvent, FillEvent]]"
96
+ data: DataHandler
97
+ symbols: List[str]
98
+ mode: TradingMode
99
+ logger: "logger" # type: ignore
100
+ kwargs: Dict[str, Any]
101
+ periodes: int
102
+ NAME: str
103
+ DESCRIPTION: str
92
104
 
93
105
  def __init__(
94
106
  self,
95
- events: Queue = None,
96
- symbol_list: List[str] = None,
97
- bars: DataHandler = None,
98
- mode: TradingMode = None,
99
- **kwargs,
100
- ):
107
+ events: "Queue[Union[SignalEvent, FillEvent]]",
108
+ symbol_list: List[str],
109
+ bars: DataHandler,
110
+ mode: TradingMode,
111
+ **kwargs: Any,
112
+ ) -> None:
101
113
  """
102
114
  Initialize the `MT5Strategy` object.
103
115
 
@@ -116,8 +128,10 @@ class MT5Strategy(Strategy):
116
128
  self.symbols = symbol_list
117
129
  self.mode = mode
118
130
  if self.mode not in [TradingMode.BACKTEST, TradingMode.LIVE]:
119
- raise ValueError(f"Mode must be an instance of {type(TradingMode)} not {type(self.mode)}")
120
-
131
+ raise ValueError(
132
+ f"Mode must be an instance of {type(TradingMode)} not {type(self.mode)}"
133
+ )
134
+
121
135
  self.risk_budget = self._check_risk_budget(**kwargs)
122
136
 
123
137
  self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
@@ -132,7 +146,7 @@ class MT5Strategy(Strategy):
132
146
  self.periodes = 0
133
147
 
134
148
  @property
135
- def account(self):
149
+ def account(self) -> Account:
136
150
  if self.mode != TradingMode.LIVE:
137
151
  raise ValueError("account attribute is only allowed in Live mode")
138
152
  return Account(**self.kwargs)
@@ -141,16 +155,18 @@ class MT5Strategy(Strategy):
141
155
  def cash(self) -> float:
142
156
  if self.mode == TradingMode.LIVE:
143
157
  return self.account.balance
144
- return self._porfolio_value
158
+ return self._porfolio_value or 0.0
145
159
 
146
160
  @cash.setter
147
- def cash(self, value):
161
+ def cash(self, value: float) -> None:
148
162
  if self.mode == TradingMode.LIVE:
149
163
  raise ValueError("Cannot set the account cash in live mode")
150
164
  self._porfolio_value = value
151
165
 
152
166
  @property
153
- def orders(self):
167
+ def orders(
168
+ self,
169
+ ) -> Union[List[TradeOrder], Dict[str, Dict[str, List[SignalEvent]]]]:
154
170
  if self.mode == TradingMode.LIVE:
155
171
  return self.account.get_orders() or []
156
172
  return self._orders
@@ -162,7 +178,7 @@ class MT5Strategy(Strategy):
162
178
  return self._trades
163
179
 
164
180
  @property
165
- def positions(self):
181
+ def positions(self) -> Union[List[Any], Dict[str, Dict[str, Union[int, float]]]]:
166
182
  if self.mode == TradingMode.LIVE:
167
183
  return self.account.get_positions() or []
168
184
  return self._positions
@@ -173,7 +189,9 @@ class MT5Strategy(Strategy):
173
189
  raise ValueError("Cannot call this methode in live mode")
174
190
  return self._holdings
175
191
 
176
- def _check_risk_budget(self, **kwargs):
192
+ def _check_risk_budget(
193
+ self, **kwargs: Any
194
+ ) -> Optional[Union[Dict[str, float], str]]:
177
195
  weights = kwargs.get("risk_weights")
178
196
  if weights is not None and isinstance(weights, dict):
179
197
  for asset in self.symbols:
@@ -185,11 +203,12 @@ class MT5Strategy(Strategy):
185
203
  return weights
186
204
  elif isinstance(weights, str):
187
205
  return weights
206
+ return None
188
207
 
189
- def _initialize_portfolio(self):
208
+ def _initialize_portfolio(self) -> None:
190
209
  self._orders = {}
191
210
  self._positions = {}
192
- self._trades = {}
211
+ self._trades = {}
193
212
  for symbol in self.symbols:
194
213
  self._positions[symbol] = {}
195
214
  self._orders[symbol] = {}
@@ -201,7 +220,9 @@ class MT5Strategy(Strategy):
201
220
  self._orders[symbol][order] = []
202
221
  self._holdings = {s: 0.0 for s in self.symbols}
203
222
 
204
- def get_update_from_portfolio(self, positions, holdings):
223
+ def get_update_from_portfolio(
224
+ self, positions: Dict[str, float], holdings: Dict[str, float]
225
+ ) -> None:
205
226
  """
206
227
  Update the positions and holdings for the strategy from the portfolio.
207
228
 
@@ -224,20 +245,20 @@ class MT5Strategy(Strategy):
224
245
  if symbol in holdings:
225
246
  self._holdings[symbol] = holdings[symbol]
226
247
 
227
- def update_trades_from_fill(self, event: FillEvent):
248
+ def update_trades_from_fill(self, event: FillEvent) -> None:
228
249
  """
229
250
  This method updates the trades for the strategy based on the fill event.
230
251
  It is used to keep track of the number of trades executed for each order.
231
252
  """
232
253
  if event.type == Events.FILL:
233
254
  if event.order != "EXIT":
234
- self._trades[event.symbol][event.order] += 1
255
+ self._trades[event.symbol][event.order] += 1 # type: ignore
235
256
  elif event.order == "EXIT" and event.direction == "BUY":
236
257
  self._trades[event.symbol]["SHORT"] = 0
237
258
  elif event.order == "EXIT" and event.direction == "SELL":
238
259
  self._trades[event.symbol]["LONG"] = 0
239
260
 
240
- def calculate_signals(self, *args, **kwargs) -> List[TradeSignal]:
261
+ def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal]:
241
262
  """
242
263
  Provides the mechanisms to calculate signals for the strategy.
243
264
  This methods should return a list of signals for the strategy.
@@ -249,35 +270,45 @@ class MT5Strategy(Strategy):
249
270
  - ``id``: The unique identifier for the strategy or order.
250
271
  - ``comment``: An optional comment or description related to the trade signal.
251
272
  """
252
- pass
273
+ raise NotImplementedError("Should implement calculate_signals()")
253
274
 
254
275
  def signal(self, signal: int, symbol: str) -> TradeSignal:
255
276
  """
256
277
  Generate a ``TradeSignal`` object based on the signal value.
257
- Args:
258
- signal : An integer value representing the signal type:
259
- 0: BUY
260
- 1: SELL
261
- 2: EXIT_LONG
262
- 3: EXIT_SHORT
263
- 4: EXIT_ALL_POSITIONS
264
- 5: EXIT_ALL_ORDERS
265
- 6: EXIT_STOP
266
- 7: EXIT_LIMIT
267
-
268
- symbol : The symbol for the trade.
269
278
 
270
- Returns:
271
- TradeSignal : A ``TradeSignal`` object representing the trade signal.
272
-
273
- Note:
274
- This generate only common signals. For more complex signals, use `generate_signal` directly.
275
-
276
- Raises:
277
- ValueError : If the signal value is not between 0 and 7.
279
+ Parameters
280
+ ----------
281
+ signal : int
282
+ An integer value representing the signal type:
283
+ * 0: BUY
284
+ * 1: SELL
285
+ * 2: EXIT_LONG
286
+ * 3: EXIT_SHORT
287
+ * 4: EXIT_ALL_POSITIONS
288
+ * 5: EXIT_ALL_ORDERS
289
+ * 6: EXIT_STOP
290
+ * 7: EXIT_LIMIT
291
+ symbol : str
292
+ The symbol for the trade.
293
+
294
+ Returns
295
+ -------
296
+ TradeSignal
297
+ A ``TradeSignal`` object representing the trade signal.
298
+
299
+ Raises
300
+ ------
301
+ ValueError
302
+ If the signal value is not between 0 and 7.
303
+
304
+ Notes
305
+ -----
306
+ This generates only common signals. For more complex signals, use
307
+ ``generate_signal`` directly.
278
308
  """
309
+
279
310
  signal_id = getattr(self, "id", None) or getattr(self, "ID")
280
-
311
+
281
312
  match signal:
282
313
  case 0:
283
314
  return generate_signal(signal_id, symbol, TradeAction.BUY)
@@ -288,7 +319,9 @@ class MT5Strategy(Strategy):
288
319
  case 3:
289
320
  return generate_signal(signal_id, symbol, TradeAction.EXIT_SHORT)
290
321
  case 4:
291
- return generate_signal(signal_id, symbol, TradeAction.EXIT_ALL_POSITIONS)
322
+ return generate_signal(
323
+ signal_id, symbol, TradeAction.EXIT_ALL_POSITIONS
324
+ )
292
325
  case 5:
293
326
  return generate_signal(signal_id, symbol, TradeAction.EXIT_ALL_ORDERS)
294
327
  case 6:
@@ -296,10 +329,66 @@ class MT5Strategy(Strategy):
296
329
  case 7:
297
330
  return generate_signal(signal_id, symbol, TradeAction.EXIT_LIMIT)
298
331
  case _:
299
- raise ValueError(f"Invalid signal value: {signal}. Must be an integer between 0 and 7.")
332
+ raise ValueError(
333
+ f"Invalid signal value: {signal}. Must be an integer between 0 and 7."
334
+ )
335
+
336
+ def send_trade_repport(self, perf_analyzer: Callable, **kwargs: Any) -> None:
337
+ """
338
+ Generates and sends a trade report message containing performance metrics for the current strategy.
339
+ This method retrieves the trade history for the current account, filters it by the strategy's ID,
340
+ computes performance metrics using the provided `perf_analyzer` callable, and formats the results
341
+ into a message. The message includes account information, strategy details, a timestamp, and
342
+ performance metrics. The message is then sent via Telegram using the specified bot token and chat ID.
343
+
344
+ Args:
345
+ perf_analyzer (Callable): A function or callable object that takes the filtered trade history
346
+ (as a DataFrame) and additional keyword arguments, and returns a DataFrame of performance metrics.
347
+ **kwargs: Additional keyword arguments, which may include
348
+ - Any other param requires by ``perf_analyzer``
349
+ """
350
+
351
+ from bbstrader.trading.utils import send_message
352
+
353
+ history = self.account.get_trades_history()
354
+ if history is None:
355
+ return
356
+
357
+ ID = getattr(self, "id", None) or getattr(self, "ID")
358
+ history = history[history["magic"] == ID]
359
+ performance = perf_analyzer(history, **kwargs)
300
360
 
361
+ account = self.kwargs.get("account", "MT5 Account")
362
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
301
363
 
302
- def perform_period_end_checks(self, *args, **kwargs):
364
+ header = (
365
+ f"==== TRADE REPORT =====\n\n"
366
+ f"ACCOUNT: {account}\n"
367
+ f"STRATEGY: {self.NAME}\n"
368
+ f"ID: {ID}\n"
369
+ f"DESCRIPTION: {self.DESCRIPTION}\n"
370
+ f"TIMESTAMP: {timestamp}\n\n"
371
+ f"📊 PERFORMANCE:\n"
372
+ )
373
+ metrics = performance.iloc[0].to_dict()
374
+
375
+ lines = []
376
+ for key, value in metrics.items():
377
+ if isinstance(value, float):
378
+ value = round(value, 4)
379
+ lines.append(f"{key:<15}: {value}")
380
+
381
+ performance_str = "\n".join(lines)
382
+ message = f"{header}{performance_str}"
383
+
384
+ send_message(
385
+ message=message,
386
+ telegram=True,
387
+ token=self.kwargs.get("bot_token"),
388
+ chat_id=self.kwargs.get("chat_id"),
389
+ )
390
+
391
+ def perform_period_end_checks(self, *args: Any, **kwargs: Any) -> None:
303
392
  """
304
393
  Some strategies may require additional checks at the end of the period,
305
394
  such as closing all positions or orders or tracking the performance of the strategy etc.
@@ -309,8 +398,11 @@ class MT5Strategy(Strategy):
309
398
  pass
310
399
 
311
400
  def apply_risk_management(
312
- self, optimer, symbols=None, freq=252
313
- ) -> Dict[str, float] | None:
401
+ self,
402
+ optimer: str,
403
+ symbols: Optional[List[str]] = None,
404
+ freq: int = 252,
405
+ ) -> Optional[Dict[str, float]]:
314
406
  """
315
407
  Apply risk management rules to the strategy.
316
408
  """
@@ -326,6 +418,8 @@ class MT5Strategy(Strategy):
326
418
  array=False,
327
419
  tf=self.tf,
328
420
  )
421
+ if prices is None:
422
+ return None
329
423
  prices = pd.DataFrame(prices)
330
424
  prices = prices.dropna(axis=0, how="any")
331
425
  try:
@@ -334,7 +428,14 @@ class MT5Strategy(Strategy):
334
428
  except Exception:
335
429
  return {symbol: 0.0 for symbol in symbols}
336
430
 
337
- def get_quantity(self, symbol, weight, price=None, volume=None, maxqty=None) -> int:
431
+ def get_quantity(
432
+ self,
433
+ symbol: str,
434
+ weight: float,
435
+ price: Optional[float] = None,
436
+ volume: Optional[float] = None,
437
+ maxqty: Optional[int] = None,
438
+ ) -> int:
338
439
  """
339
440
  Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
340
441
  The quantity calculated can be used to evalute a strategy's performance for each symbol
@@ -372,9 +473,11 @@ class MT5Strategy(Strategy):
372
473
  qty = max(qty, 0) / self.max_trades[symbol]
373
474
  if maxqty is not None:
374
475
  qty = min(qty, maxqty)
375
- return max(round(qty, 2), 0)
476
+ return int(max(round(qty, 2), 0))
376
477
 
377
- def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
478
+ def get_quantities(
479
+ self, quantities: Optional[Union[Dict[str, int], int]]
480
+ ) -> Dict[str, Optional[int]]:
378
481
  """
379
482
  Get the quantities to buy or sell for the symbols in the strategy.
380
483
  This method is used when whe need to assign different quantities to the symbols.
@@ -388,19 +491,26 @@ class MT5Strategy(Strategy):
388
491
  return quantities
389
492
  elif isinstance(quantities, int):
390
493
  return {symbol: quantities for symbol in self.symbols}
494
+ raise TypeError(f"Unsupported type for quantities: {type(quantities)}")
391
495
 
392
496
  def _send_order(
393
497
  self,
394
- id,
498
+ id: int,
395
499
  symbol: str,
396
500
  signal: str,
397
501
  strength: float,
398
502
  price: float,
399
503
  quantity: int,
400
- dtime: datetime | pd.Timestamp,
401
- ):
504
+ dtime: Union[datetime, pd.Timestamp],
505
+ ) -> None:
402
506
  position = SignalEvent(
403
- id, symbol, dtime, signal, quantity=quantity, strength=strength, price=price
507
+ id,
508
+ symbol,
509
+ dtime,
510
+ signal,
511
+ quantity=quantity,
512
+ strength=strength,
513
+ price=price, # type: ignore
404
514
  )
405
515
  log = False
406
516
  if signal in ["LONG", "SHORT"]:
@@ -427,32 +537,62 @@ class MT5Strategy(Strategy):
427
537
  price: float,
428
538
  quantity: int,
429
539
  strength: float = 1.0,
430
- dtime: datetime | pd.Timestamp = None,
431
- ):
540
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
541
+ ) -> None:
432
542
  """
433
543
  Open a long position
434
544
 
435
545
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
436
546
  """
547
+ if dtime is None:
548
+ dtime = self.get_current_dt()
437
549
  self._send_order(id, symbol, "LONG", strength, price, quantity, dtime)
438
550
 
439
- def sell_mkt(self, id, symbol, price, quantity, strength=1.0, dtime=None):
551
+ def sell_mkt(
552
+ self,
553
+ id: int,
554
+ symbol: str,
555
+ price: float,
556
+ quantity: int,
557
+ strength: float = 1.0,
558
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
559
+ ) -> None:
440
560
  """
441
561
  Open a short position
442
562
 
443
563
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
444
564
  """
565
+ if dtime is None:
566
+ dtime = self.get_current_dt()
445
567
  self._send_order(id, symbol, "SHORT", strength, price, quantity, dtime)
446
568
 
447
- def close_positions(self, id, symbol, price, quantity, strength=1.0, dtime=None):
569
+ def close_positions(
570
+ self,
571
+ id: int,
572
+ symbol: str,
573
+ price: float,
574
+ quantity: int,
575
+ strength: float = 1.0,
576
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
577
+ ) -> None:
448
578
  """
449
579
  Close a position or exit all positions
450
580
 
451
581
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
452
582
  """
583
+ if dtime is None:
584
+ dtime = self.get_current_dt()
453
585
  self._send_order(id, symbol, "EXIT", strength, price, quantity, dtime)
454
586
 
455
- def buy_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
587
+ def buy_stop(
588
+ self,
589
+ id: int,
590
+ symbol: str,
591
+ price: float,
592
+ quantity: int,
593
+ strength: float = 1.0,
594
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
595
+ ) -> None:
456
596
  """
457
597
  Open a pending order to buy at a stop price
458
598
 
@@ -463,12 +603,28 @@ class MT5Strategy(Strategy):
463
603
  raise ValueError(
464
604
  "The buy_stop price must be greater than the current price."
465
605
  )
606
+ if dtime is None:
607
+ dtime = self.get_current_dt()
466
608
  order = SignalEvent(
467
- id, symbol, dtime, "LONG", quantity=quantity, strength=strength, price=price
609
+ id,
610
+ symbol,
611
+ dtime,
612
+ "LONG",
613
+ quantity=quantity,
614
+ strength=strength,
615
+ price=price, # type: ignore
468
616
  )
469
617
  self._orders[symbol]["BSTP"].append(order)
470
618
 
471
- def sell_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
619
+ def sell_stop(
620
+ self,
621
+ id: int,
622
+ symbol: str,
623
+ price: float,
624
+ quantity: int,
625
+ strength: float = 1.0,
626
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
627
+ ) -> None:
472
628
  """
473
629
  Open a pending order to sell at a stop price
474
630
 
@@ -477,10 +633,12 @@ class MT5Strategy(Strategy):
477
633
  current_price = self.data.get_latest_bar_value(symbol, "close")
478
634
  if price >= current_price:
479
635
  raise ValueError("The sell_stop price must be less than the current price.")
636
+ if dtime is None:
637
+ dtime = self.get_current_dt()
480
638
  order = SignalEvent(
481
639
  id,
482
640
  symbol,
483
- dtime,
641
+ dtime, # type: ignore
484
642
  "SHORT",
485
643
  quantity=quantity,
486
644
  strength=strength,
@@ -488,7 +646,15 @@ class MT5Strategy(Strategy):
488
646
  )
489
647
  self._orders[symbol]["SSTP"].append(order)
490
648
 
491
- def buy_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
649
+ def buy_limit(
650
+ self,
651
+ id: int,
652
+ symbol: str,
653
+ price: float,
654
+ quantity: int,
655
+ strength: float = 1.0,
656
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
657
+ ) -> None:
492
658
  """
493
659
  Open a pending order to buy at a limit price
494
660
 
@@ -497,12 +663,28 @@ class MT5Strategy(Strategy):
497
663
  current_price = self.data.get_latest_bar_value(symbol, "close")
498
664
  if price >= current_price:
499
665
  raise ValueError("The buy_limit price must be less than the current price.")
666
+ if dtime is None:
667
+ dtime = self.get_current_dt()
500
668
  order = SignalEvent(
501
- id, symbol, dtime, "LONG", quantity=quantity, strength=strength, price=price
669
+ id,
670
+ symbol,
671
+ dtime,
672
+ "LONG",
673
+ quantity=quantity,
674
+ strength=strength,
675
+ price=price, # type: ignore
502
676
  )
503
677
  self._orders[symbol]["BLMT"].append(order)
504
678
 
505
- def sell_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
679
+ def sell_limit(
680
+ self,
681
+ id: int,
682
+ symbol: str,
683
+ price: float,
684
+ quantity: int,
685
+ strength: float = 1.0,
686
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
687
+ ) -> None:
506
688
  """
507
689
  Open a pending order to sell at a limit price
508
690
 
@@ -513,10 +695,12 @@ class MT5Strategy(Strategy):
513
695
  raise ValueError(
514
696
  "The sell_limit price must be greater than the current price."
515
697
  )
698
+ if dtime is None:
699
+ dtime = self.get_current_dt()
516
700
  order = SignalEvent(
517
701
  id,
518
702
  symbol,
519
- dtime,
703
+ dtime, # type: ignore
520
704
  "SHORT",
521
705
  quantity=quantity,
522
706
  strength=strength,
@@ -532,8 +716,8 @@ class MT5Strategy(Strategy):
532
716
  stoplimit: float,
533
717
  quantity: int,
534
718
  strength: float = 1.0,
535
- dtime: datetime | pd.Timestamp = None,
536
- ):
719
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
720
+ ) -> None:
537
721
  """
538
722
  Open a pending order to buy at a stop-limit price
539
723
 
@@ -548,10 +732,12 @@ class MT5Strategy(Strategy):
548
732
  raise ValueError(
549
733
  f"The stop-limit price {stoplimit} must be greater than the price {price}."
550
734
  )
735
+ if dtime is None:
736
+ dtime = self.get_current_dt()
551
737
  order = SignalEvent(
552
738
  id,
553
739
  symbol,
554
- dtime,
740
+ dtime, # type: ignore
555
741
  "LONG",
556
742
  quantity=quantity,
557
743
  strength=strength,
@@ -561,8 +747,15 @@ class MT5Strategy(Strategy):
561
747
  self._orders[symbol]["BSTPLMT"].append(order)
562
748
 
563
749
  def sell_stop_limit(
564
- self, id, symbol, price, stoplimit, quantity, strength=1.0, dtime=None
565
- ):
750
+ self,
751
+ id: int,
752
+ symbol: str,
753
+ price: float,
754
+ stoplimit: float,
755
+ quantity: int,
756
+ strength: float = 1.0,
757
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
758
+ ) -> None:
566
759
  """
567
760
  Open a pending order to sell at a stop-limit price
568
761
 
@@ -577,10 +770,12 @@ class MT5Strategy(Strategy):
577
770
  raise ValueError(
578
771
  f"The stop-limit price {stoplimit} must be less than the price {price}."
579
772
  )
773
+ if dtime is None:
774
+ dtime = self.get_current_dt()
580
775
  order = SignalEvent(
581
776
  id,
582
777
  symbol,
583
- dtime,
778
+ dtime, # type: ignore
584
779
  "SHORT",
585
780
  quantity=quantity,
586
781
  strength=strength,
@@ -589,19 +784,31 @@ class MT5Strategy(Strategy):
589
784
  )
590
785
  self._orders[symbol]["SSTPLMT"].append(order)
591
786
 
592
- def check_pending_orders(self):
787
+ def check_pending_orders(self) -> None:
593
788
  """
594
789
  Check for pending orders and handle them accordingly.
595
790
  """
596
791
 
597
- def logmsg(order, type, symbol, dtime):
598
- return self.logger.info(
792
+ def logmsg(
793
+ order: SignalEvent,
794
+ type: str,
795
+ symbol: str,
796
+ dtime: Union[datetime, pd.Timestamp],
797
+ ) -> None:
798
+ self.logger.info(
599
799
  f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
600
- f"PRICE @ {round(order.price, 5)}",
800
+ f"PRICE @ {round(order.price, 5)}", # type: ignore
601
801
  custom_time=dtime,
602
802
  )
603
803
 
604
- def process_orders(order_type, condition, execute_fn, log_label, symbol, dtime):
804
+ def process_orders(
805
+ order_type: str,
806
+ condition: Callable[[SignalEvent], bool],
807
+ execute_fn: Callable[[SignalEvent], None],
808
+ log_label: str,
809
+ symbol: str,
810
+ dtime: Union[datetime, pd.Timestamp],
811
+ ) -> None:
605
812
  for order in self._orders[symbol][order_type].copy():
606
813
  if condition(order):
607
814
  execute_fn(order)
@@ -620,9 +827,13 @@ class MT5Strategy(Strategy):
620
827
 
621
828
  process_orders(
622
829
  "BLMT",
623
- lambda o: latest_close <= o.price,
830
+ lambda o: latest_close <= o.price, # type: ignore
624
831
  lambda o: self.buy_mkt(
625
- o.strategy_id, symbol, o.price, o.quantity, dtime
832
+ o.strategy_id,
833
+ symbol,
834
+ o.price,
835
+ o.quantity,
836
+ dtime=dtime, # type: ignore
626
837
  ),
627
838
  "BUY LIMIT",
628
839
  symbol,
@@ -631,9 +842,13 @@ class MT5Strategy(Strategy):
631
842
 
632
843
  process_orders(
633
844
  "SLMT",
634
- lambda o: latest_close >= o.price,
845
+ lambda o: latest_close >= o.price, # type: ignore
635
846
  lambda o: self.sell_mkt(
636
- o.strategy_id, symbol, o.price, o.quantity, dtime
847
+ o.strategy_id,
848
+ symbol,
849
+ o.price,
850
+ o.quantity,
851
+ dtime=dtime, # type: ignore
637
852
  ),
638
853
  "SELL LIMIT",
639
854
  symbol,
@@ -642,9 +857,13 @@ class MT5Strategy(Strategy):
642
857
 
643
858
  process_orders(
644
859
  "BSTP",
645
- lambda o: latest_close >= o.price,
860
+ lambda o: latest_close >= o.price, # type: ignore
646
861
  lambda o: self.buy_mkt(
647
- o.strategy_id, symbol, o.price, o.quantity, dtime
862
+ o.strategy_id,
863
+ symbol,
864
+ o.price,
865
+ o.quantity,
866
+ dtime=dtime, # type: ignore
648
867
  ),
649
868
  "BUY STOP",
650
869
  symbol,
@@ -653,9 +872,13 @@ class MT5Strategy(Strategy):
653
872
 
654
873
  process_orders(
655
874
  "SSTP",
656
- lambda o: latest_close <= o.price,
875
+ lambda o: latest_close <= o.price, # type: ignore
657
876
  lambda o: self.sell_mkt(
658
- o.strategy_id, symbol, o.price, o.quantity, dtime
877
+ o.strategy_id,
878
+ symbol,
879
+ o.price,
880
+ o.quantity,
881
+ dtime=dtime, # type: ignore
659
882
  ),
660
883
  "SELL STOP",
661
884
  symbol,
@@ -664,9 +887,13 @@ class MT5Strategy(Strategy):
664
887
 
665
888
  process_orders(
666
889
  "BSTPLMT",
667
- lambda o: latest_close >= o.price,
890
+ lambda o: latest_close >= o.price, # type: ignore
668
891
  lambda o: self.buy_limit(
669
- o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
892
+ o.strategy_id,
893
+ symbol,
894
+ o.stoplimit,
895
+ o.quantity,
896
+ dtime=dtime, # type: ignore
670
897
  ),
671
898
  "BUY STOP LIMIT",
672
899
  symbol,
@@ -675,9 +902,13 @@ class MT5Strategy(Strategy):
675
902
 
676
903
  process_orders(
677
904
  "SSTPLMT",
678
- lambda o: latest_close <= o.price,
905
+ lambda o: latest_close <= o.price, # type: ignore
679
906
  lambda o: self.sell_limit(
680
- o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
907
+ o.strategy_id,
908
+ symbol,
909
+ o.stoplimit,
910
+ o.quantity,
911
+ dtime=dtime, # type: ignore
681
912
  ),
682
913
  "SELL STOP LIMIT",
683
914
  symbol,
@@ -685,7 +916,7 @@ class MT5Strategy(Strategy):
685
916
  )
686
917
 
687
918
  @staticmethod
688
- def calculate_pct_change(current_price, lh_price) -> float:
919
+ def calculate_pct_change(current_price: float, lh_price: float) -> float:
689
920
  return ((current_price - lh_price) / lh_price) * 100
690
921
 
691
922
  def get_asset_values(
@@ -694,11 +925,11 @@ class MT5Strategy(Strategy):
694
925
  window: int,
695
926
  value_type: str = "returns",
696
927
  array: bool = True,
697
- bars: DataHandler = None,
928
+ bars: Optional[DataHandler] = None,
698
929
  mode: TradingMode = TradingMode.BACKTEST,
699
930
  tf: str = "D1",
700
- error: Literal["ignore", "raise"] = None,
701
- ) -> Dict[str, np.ndarray | pd.Series] | None:
931
+ error: Optional[Literal["ignore", "raise"]] = None,
932
+ ) -> Optional[Dict[str, Union[np.ndarray, pd.Series]]]:
702
933
  """
703
934
  Get the historical OHLCV value or returns or custum value
704
935
  based on the DataHandker of the assets in the symbol list.
@@ -722,7 +953,7 @@ class MT5Strategy(Strategy):
722
953
  """
723
954
  if mode not in [TradingMode.BACKTEST, TradingMode.LIVE]:
724
955
  raise ValueError("Mode must be an instance of TradingMode")
725
- asset_values = {}
956
+ asset_values: Dict[str, Union[np.ndarray, pd.Series]] = {}
726
957
  if mode == TradingMode.BACKTEST:
727
958
  if bars is None:
728
959
  raise ValueError("DataHandler is required for backtest mode.")
@@ -731,8 +962,9 @@ class MT5Strategy(Strategy):
731
962
  values = bars.get_latest_bars_values(asset, value_type, N=window)
732
963
  asset_values[asset] = values[~np.isnan(values)]
733
964
  else:
734
- values = bars.get_latest_bars(asset, N=window)
735
- asset_values[asset] = getattr(values, value_type)
965
+ values_df = bars.get_latest_bars(asset, N=window)
966
+ if isinstance(values_df, pd.DataFrame):
967
+ asset_values[asset] = values_df[value_type]
736
968
  elif mode == TradingMode.LIVE:
737
969
  for asset in symbol_list:
738
970
  rates = Rates(asset, timeframe=tf, count=window + 1, **self.kwargs)
@@ -752,7 +984,7 @@ class MT5Strategy(Strategy):
752
984
  return None
753
985
 
754
986
  @staticmethod
755
- def is_signal_time(period_count, signal_inverval) -> bool:
987
+ def is_signal_time(period_count: Optional[int], signal_inverval: int) -> bool:
756
988
  """
757
989
  Check if we can generate a signal based on the current period count.
758
990
  We use the signal interval as a form of periodicity or rebalancing period.
@@ -771,11 +1003,17 @@ class MT5Strategy(Strategy):
771
1003
  @staticmethod
772
1004
  def stop_time(time_zone: str, stop_time: str) -> bool:
773
1005
  now = datetime.now(pytz.timezone(time_zone)).time()
774
- stop_time = datetime.strptime(stop_time, "%H:%M").time()
775
- return now >= stop_time
1006
+ stop_time_dt = datetime.strptime(stop_time, "%H:%M").time()
1007
+ return now >= stop_time_dt
776
1008
 
777
1009
  def ispositions(
778
- self, symbol, strategy_id, position, max_trades, one_true=False, account=None
1010
+ self,
1011
+ symbol: str,
1012
+ strategy_id: int,
1013
+ position: int,
1014
+ max_trades: int,
1015
+ one_true: bool = False,
1016
+ account: Optional[Account] = None,
779
1017
  ) -> bool:
780
1018
  """
781
1019
  This function is use for live trading to check if there are open positions
@@ -806,7 +1044,13 @@ class MT5Strategy(Strategy):
806
1044
  return len(open_positions) >= max_trades
807
1045
  return False
808
1046
 
809
- def get_positions_prices(self, symbol, strategy_id, position, account=None):
1047
+ def get_positions_prices(
1048
+ self,
1049
+ symbol: str,
1050
+ strategy_id: int,
1051
+ position: int,
1052
+ account: Optional[Account] = None,
1053
+ ) -> np.ndarray:
810
1054
  """
811
1055
  Get the buy or sell prices for open positions of a given symbol and strategy.
812
1056
 
@@ -831,8 +1075,10 @@ class MT5Strategy(Strategy):
831
1075
  )
832
1076
  return prices
833
1077
  return np.array([])
834
-
835
- def get_active_orders(self, symbol: str, strategy_id: int, order_type: int = None) -> List[TradeOrder]:
1078
+
1079
+ def get_active_orders(
1080
+ self, symbol: str, strategy_id: int, order_type: Optional[int] = None
1081
+ ) -> List[TradeOrder]:
836
1082
  """
837
1083
  Get the active orders for a given symbol and strategy.
838
1084
 
@@ -850,15 +1096,25 @@ class MT5Strategy(Strategy):
850
1096
  Returns:
851
1097
  List[TradeOrder] : A list of active orders for the given symbol and strategy.
852
1098
  """
853
- orders = [o for o in self.orders if o.symbol == symbol and o.magic == strategy_id]
1099
+ orders = [
1100
+ o
1101
+ for o in self.orders
1102
+ if isinstance(o, TradeOrder)
1103
+ and o.symbol == symbol
1104
+ and o.magic == strategy_id
1105
+ ]
854
1106
  if order_type is not None and len(orders) > 0:
855
1107
  orders = [o for o in orders if o.type == order_type]
856
1108
  return orders
857
1109
 
858
- def exit_positions(self, position, prices, asset, th: float = 0.01):
1110
+ def exit_positions(
1111
+ self, position: int, prices: np.ndarray, asset: str, th: float = 0.01
1112
+ ) -> bool:
859
1113
  if len(prices) == 0:
860
1114
  return False
861
1115
  tick_info = self.account.get_tick_info(asset)
1116
+ if tick_info is None:
1117
+ return False
862
1118
  bid, ask = tick_info.bid, tick_info.ask
863
1119
  price = None
864
1120
  if len(prices) == 1:
@@ -866,7 +1122,7 @@ class MT5Strategy(Strategy):
866
1122
  elif len(prices) in range(2, self.max_trades[asset] + 1):
867
1123
  price = np.mean(prices)
868
1124
  if price is not None:
869
- if position == 0:
1125
+ if position == 0:
870
1126
  return self.calculate_pct_change(ask, price) >= th
871
1127
  elif position == 1:
872
1128
  return self.calculate_pct_change(bid, price) <= -th
@@ -878,7 +1134,7 @@ class MT5Strategy(Strategy):
878
1134
 
879
1135
  @staticmethod
880
1136
  def convert_time_zone(
881
- dt: datetime | int | pd.Timestamp,
1137
+ dt: Union[datetime, int, pd.Timestamp],
882
1138
  from_tz: str = "UTC",
883
1139
  to_tz: str = "US/Eastern",
884
1140
  ) -> pd.Timestamp:
@@ -893,20 +1149,24 @@ class MT5Strategy(Strategy):
893
1149
  Returns:
894
1150
  dt_to : The converted datetime.
895
1151
  """
896
- from_tz = pytz.timezone(from_tz)
1152
+ from_tz_pytz = pytz.timezone(from_tz)
897
1153
  if isinstance(dt, (datetime, int)):
898
- dt = pd.to_datetime(dt, unit="s")
899
- if dt.tzinfo is None:
900
- dt = dt.tz_localize(from_tz)
1154
+ dt_ts = pd.to_datetime(dt, unit="s")
901
1155
  else:
902
- dt = dt.tz_convert(from_tz)
1156
+ dt_ts = dt
1157
+ if dt_ts.tzinfo is None:
1158
+ dt_ts = dt_ts.tz_localize(from_tz_pytz)
1159
+ else:
1160
+ dt_ts = dt_ts.tz_convert(from_tz_pytz)
903
1161
 
904
- dt_to = dt.tz_convert(pytz.timezone(to_tz))
1162
+ dt_to = dt_ts.tz_convert(pytz.timezone(to_tz))
905
1163
  return dt_to
906
1164
 
907
1165
  @staticmethod
908
1166
  def get_mt5_equivalent(
909
- symbols, symbol_type: str | SymbolType = SymbolType.STOCKS, **kwargs
1167
+ symbols: List[str],
1168
+ symbol_type: Union[str, SymbolType] = SymbolType.STOCKS,
1169
+ **kwargs: Any,
910
1170
  ) -> List[str]:
911
1171
  """
912
1172
  Get the MetaTrader 5 equivalent symbols for the symbols in the list.
@@ -920,20 +1180,20 @@ class MT5Strategy(Strategy):
920
1180
  Returns:
921
1181
  mt5_equivalent : The MetaTrader 5 equivalent symbols for the symbols in the list.
922
1182
  """
923
-
1183
+
924
1184
  account = Account(**kwargs)
925
1185
  mt5_symbols = account.get_symbols(symbol_type=symbol_type)
926
- mt5_equivalent = []
1186
+ mt5_equivalent: List[str] = []
927
1187
 
928
- def _get_admiral_symbols():
1188
+ def _get_admiral_symbols() -> None:
929
1189
  for s in mt5_symbols:
930
1190
  _s = s[1:] if s[0] in string.punctuation else s
931
1191
  for symbol in symbols:
932
1192
  if _s.split(".")[0] == symbol or _s.split("_")[0] == symbol:
933
1193
  mt5_equivalent.append(s)
934
1194
 
935
- def _get_pepperstone_symbols():
936
- for s in mt5_symbols:
1195
+ def _get_pepperstone_symbols() -> None:
1196
+ for s in mt5_symbols:
937
1197
  for symbol in symbols:
938
1198
  if s.split(".")[0] == symbol:
939
1199
  mt5_equivalent.append(s)
@@ -952,4 +1212,6 @@ class MT5Strategy(Strategy):
952
1212
  return mt5_equivalent
953
1213
 
954
1214
 
955
- class TWSStrategy(Strategy): ...
1215
+ class TWSStrategy(Strategy):
1216
+ def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal]:
1217
+ raise NotImplementedError("Should implement calculate_signals()")