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

@@ -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 Callable, 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
@@ -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):
@@ -85,20 +85,31 @@ class MT5Strategy(Strategy):
85
85
  ID: int
86
86
 
87
87
  max_trades: Dict[str, int]
88
- risk_budget: Dict[str, float] | str | None
88
+ risk_budget: Optional[Union[Dict[str, float], str]]
89
89
 
90
90
  _orders: Dict[str, Dict[str, List[SignalEvent]]]
91
- _positions: Dict[str, Dict[str, int | float]]
91
+ _positions: Dict[str, Dict[str, Union[int, float]]]
92
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
93
104
 
94
105
  def __init__(
95
106
  self,
96
- events: Queue = None,
97
- symbol_list: List[str] = None,
98
- bars: DataHandler = None,
99
- mode: TradingMode = None,
100
- **kwargs,
101
- ):
107
+ events: "Queue[Union[SignalEvent, FillEvent]]",
108
+ symbol_list: List[str],
109
+ bars: DataHandler,
110
+ mode: TradingMode,
111
+ **kwargs: Any,
112
+ ) -> None:
102
113
  """
103
114
  Initialize the `MT5Strategy` object.
104
115
 
@@ -135,7 +146,7 @@ class MT5Strategy(Strategy):
135
146
  self.periodes = 0
136
147
 
137
148
  @property
138
- def account(self):
149
+ def account(self) -> Account:
139
150
  if self.mode != TradingMode.LIVE:
140
151
  raise ValueError("account attribute is only allowed in Live mode")
141
152
  return Account(**self.kwargs)
@@ -144,16 +155,18 @@ class MT5Strategy(Strategy):
144
155
  def cash(self) -> float:
145
156
  if self.mode == TradingMode.LIVE:
146
157
  return self.account.balance
147
- return self._porfolio_value
158
+ return self._porfolio_value or 0.0
148
159
 
149
160
  @cash.setter
150
- def cash(self, value):
161
+ def cash(self, value: float) -> None:
151
162
  if self.mode == TradingMode.LIVE:
152
163
  raise ValueError("Cannot set the account cash in live mode")
153
164
  self._porfolio_value = value
154
165
 
155
166
  @property
156
- def orders(self):
167
+ def orders(
168
+ self,
169
+ ) -> Union[List[TradeOrder], Dict[str, Dict[str, List[SignalEvent]]]]:
157
170
  if self.mode == TradingMode.LIVE:
158
171
  return self.account.get_orders() or []
159
172
  return self._orders
@@ -165,7 +178,7 @@ class MT5Strategy(Strategy):
165
178
  return self._trades
166
179
 
167
180
  @property
168
- def positions(self):
181
+ def positions(self) -> Union[List[Any], Dict[str, Dict[str, Union[int, float]]]]:
169
182
  if self.mode == TradingMode.LIVE:
170
183
  return self.account.get_positions() or []
171
184
  return self._positions
@@ -176,7 +189,9 @@ class MT5Strategy(Strategy):
176
189
  raise ValueError("Cannot call this methode in live mode")
177
190
  return self._holdings
178
191
 
179
- def _check_risk_budget(self, **kwargs):
192
+ def _check_risk_budget(
193
+ self, **kwargs: Any
194
+ ) -> Optional[Union[Dict[str, float], str]]:
180
195
  weights = kwargs.get("risk_weights")
181
196
  if weights is not None and isinstance(weights, dict):
182
197
  for asset in self.symbols:
@@ -188,8 +203,9 @@ class MT5Strategy(Strategy):
188
203
  return weights
189
204
  elif isinstance(weights, str):
190
205
  return weights
206
+ return None
191
207
 
192
- def _initialize_portfolio(self):
208
+ def _initialize_portfolio(self) -> None:
193
209
  self._orders = {}
194
210
  self._positions = {}
195
211
  self._trades = {}
@@ -204,7 +220,9 @@ class MT5Strategy(Strategy):
204
220
  self._orders[symbol][order] = []
205
221
  self._holdings = {s: 0.0 for s in self.symbols}
206
222
 
207
- 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:
208
226
  """
209
227
  Update the positions and holdings for the strategy from the portfolio.
210
228
 
@@ -227,20 +245,20 @@ class MT5Strategy(Strategy):
227
245
  if symbol in holdings:
228
246
  self._holdings[symbol] = holdings[symbol]
229
247
 
230
- def update_trades_from_fill(self, event: FillEvent):
248
+ def update_trades_from_fill(self, event: FillEvent) -> None:
231
249
  """
232
250
  This method updates the trades for the strategy based on the fill event.
233
251
  It is used to keep track of the number of trades executed for each order.
234
252
  """
235
253
  if event.type == Events.FILL:
236
254
  if event.order != "EXIT":
237
- self._trades[event.symbol][event.order] += 1
255
+ self._trades[event.symbol][event.order] += 1 # type: ignore
238
256
  elif event.order == "EXIT" and event.direction == "BUY":
239
257
  self._trades[event.symbol]["SHORT"] = 0
240
258
  elif event.order == "EXIT" and event.direction == "SELL":
241
259
  self._trades[event.symbol]["LONG"] = 0
242
260
 
243
- def calculate_signals(self, *args, **kwargs) -> List[TradeSignal]:
261
+ def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal]:
244
262
  """
245
263
  Provides the mechanisms to calculate signals for the strategy.
246
264
  This methods should return a list of signals for the strategy.
@@ -252,7 +270,7 @@ class MT5Strategy(Strategy):
252
270
  - ``id``: The unique identifier for the strategy or order.
253
271
  - ``comment``: An optional comment or description related to the trade signal.
254
272
  """
255
- pass
273
+ raise NotImplementedError("Should implement calculate_signals()")
256
274
 
257
275
  def signal(self, signal: int, symbol: str) -> TradeSignal:
258
276
  """
@@ -315,7 +333,7 @@ class MT5Strategy(Strategy):
315
333
  f"Invalid signal value: {signal}. Must be an integer between 0 and 7."
316
334
  )
317
335
 
318
- def send_trade_repport(self, perf_analyzer: Callable, **kwargs):
336
+ def send_trade_repport(self, perf_analyzer: Callable, **kwargs: Any) -> None:
319
337
  """
320
338
  Generates and sends a trade report message containing performance metrics for the current strategy.
321
339
  This method retrieves the trade history for the current account, filters it by the strategy's ID,
@@ -329,7 +347,7 @@ class MT5Strategy(Strategy):
329
347
  **kwargs: Additional keyword arguments, which may include
330
348
  - Any other param requires by ``perf_analyzer``
331
349
  """
332
-
350
+
333
351
  from bbstrader.trading.utils import send_message
334
352
 
335
353
  history = self.account.get_trades_history()
@@ -370,7 +388,7 @@ class MT5Strategy(Strategy):
370
388
  chat_id=self.kwargs.get("chat_id"),
371
389
  )
372
390
 
373
- def perform_period_end_checks(self, *args, **kwargs):
391
+ def perform_period_end_checks(self, *args: Any, **kwargs: Any) -> None:
374
392
  """
375
393
  Some strategies may require additional checks at the end of the period,
376
394
  such as closing all positions or orders or tracking the performance of the strategy etc.
@@ -380,8 +398,11 @@ class MT5Strategy(Strategy):
380
398
  pass
381
399
 
382
400
  def apply_risk_management(
383
- self, optimer, symbols=None, freq=252
384
- ) -> Dict[str, float] | None:
401
+ self,
402
+ optimer: str,
403
+ symbols: Optional[List[str]] = None,
404
+ freq: int = 252,
405
+ ) -> Optional[Dict[str, float]]:
385
406
  """
386
407
  Apply risk management rules to the strategy.
387
408
  """
@@ -397,6 +418,8 @@ class MT5Strategy(Strategy):
397
418
  array=False,
398
419
  tf=self.tf,
399
420
  )
421
+ if prices is None:
422
+ return None
400
423
  prices = pd.DataFrame(prices)
401
424
  prices = prices.dropna(axis=0, how="any")
402
425
  try:
@@ -405,7 +428,14 @@ class MT5Strategy(Strategy):
405
428
  except Exception:
406
429
  return {symbol: 0.0 for symbol in symbols}
407
430
 
408
- 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:
409
439
  """
410
440
  Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
411
441
  The quantity calculated can be used to evalute a strategy's performance for each symbol
@@ -443,9 +473,11 @@ class MT5Strategy(Strategy):
443
473
  qty = max(qty, 0) / self.max_trades[symbol]
444
474
  if maxqty is not None:
445
475
  qty = min(qty, maxqty)
446
- return max(round(qty, 2), 0)
476
+ return int(max(round(qty, 2), 0))
447
477
 
448
- 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]]:
449
481
  """
450
482
  Get the quantities to buy or sell for the symbols in the strategy.
451
483
  This method is used when whe need to assign different quantities to the symbols.
@@ -459,19 +491,26 @@ class MT5Strategy(Strategy):
459
491
  return quantities
460
492
  elif isinstance(quantities, int):
461
493
  return {symbol: quantities for symbol in self.symbols}
494
+ raise TypeError(f"Unsupported type for quantities: {type(quantities)}")
462
495
 
463
496
  def _send_order(
464
497
  self,
465
- id,
498
+ id: int,
466
499
  symbol: str,
467
500
  signal: str,
468
501
  strength: float,
469
502
  price: float,
470
503
  quantity: int,
471
- dtime: datetime | pd.Timestamp,
472
- ):
504
+ dtime: Union[datetime, pd.Timestamp],
505
+ ) -> None:
473
506
  position = SignalEvent(
474
- 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
475
514
  )
476
515
  log = False
477
516
  if signal in ["LONG", "SHORT"]:
@@ -498,32 +537,62 @@ class MT5Strategy(Strategy):
498
537
  price: float,
499
538
  quantity: int,
500
539
  strength: float = 1.0,
501
- dtime: datetime | pd.Timestamp = None,
502
- ):
540
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
541
+ ) -> None:
503
542
  """
504
543
  Open a long position
505
544
 
506
545
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
507
546
  """
547
+ if dtime is None:
548
+ dtime = self.get_current_dt()
508
549
  self._send_order(id, symbol, "LONG", strength, price, quantity, dtime)
509
550
 
510
- 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:
511
560
  """
512
561
  Open a short position
513
562
 
514
563
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
515
564
  """
565
+ if dtime is None:
566
+ dtime = self.get_current_dt()
516
567
  self._send_order(id, symbol, "SHORT", strength, price, quantity, dtime)
517
568
 
518
- 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:
519
578
  """
520
579
  Close a position or exit all positions
521
580
 
522
581
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
523
582
  """
583
+ if dtime is None:
584
+ dtime = self.get_current_dt()
524
585
  self._send_order(id, symbol, "EXIT", strength, price, quantity, dtime)
525
586
 
526
- 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:
527
596
  """
528
597
  Open a pending order to buy at a stop price
529
598
 
@@ -534,12 +603,28 @@ class MT5Strategy(Strategy):
534
603
  raise ValueError(
535
604
  "The buy_stop price must be greater than the current price."
536
605
  )
606
+ if dtime is None:
607
+ dtime = self.get_current_dt()
537
608
  order = SignalEvent(
538
- 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
539
616
  )
540
617
  self._orders[symbol]["BSTP"].append(order)
541
618
 
542
- 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:
543
628
  """
544
629
  Open a pending order to sell at a stop price
545
630
 
@@ -548,10 +633,12 @@ class MT5Strategy(Strategy):
548
633
  current_price = self.data.get_latest_bar_value(symbol, "close")
549
634
  if price >= current_price:
550
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()
551
638
  order = SignalEvent(
552
639
  id,
553
640
  symbol,
554
- dtime,
641
+ dtime, # type: ignore
555
642
  "SHORT",
556
643
  quantity=quantity,
557
644
  strength=strength,
@@ -559,7 +646,15 @@ class MT5Strategy(Strategy):
559
646
  )
560
647
  self._orders[symbol]["SSTP"].append(order)
561
648
 
562
- 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:
563
658
  """
564
659
  Open a pending order to buy at a limit price
565
660
 
@@ -568,12 +663,28 @@ class MT5Strategy(Strategy):
568
663
  current_price = self.data.get_latest_bar_value(symbol, "close")
569
664
  if price >= current_price:
570
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()
571
668
  order = SignalEvent(
572
- 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
573
676
  )
574
677
  self._orders[symbol]["BLMT"].append(order)
575
678
 
576
- 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:
577
688
  """
578
689
  Open a pending order to sell at a limit price
579
690
 
@@ -584,10 +695,12 @@ class MT5Strategy(Strategy):
584
695
  raise ValueError(
585
696
  "The sell_limit price must be greater than the current price."
586
697
  )
698
+ if dtime is None:
699
+ dtime = self.get_current_dt()
587
700
  order = SignalEvent(
588
701
  id,
589
702
  symbol,
590
- dtime,
703
+ dtime, # type: ignore
591
704
  "SHORT",
592
705
  quantity=quantity,
593
706
  strength=strength,
@@ -603,8 +716,8 @@ class MT5Strategy(Strategy):
603
716
  stoplimit: float,
604
717
  quantity: int,
605
718
  strength: float = 1.0,
606
- dtime: datetime | pd.Timestamp = None,
607
- ):
719
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
720
+ ) -> None:
608
721
  """
609
722
  Open a pending order to buy at a stop-limit price
610
723
 
@@ -619,10 +732,12 @@ class MT5Strategy(Strategy):
619
732
  raise ValueError(
620
733
  f"The stop-limit price {stoplimit} must be greater than the price {price}."
621
734
  )
735
+ if dtime is None:
736
+ dtime = self.get_current_dt()
622
737
  order = SignalEvent(
623
738
  id,
624
739
  symbol,
625
- dtime,
740
+ dtime, # type: ignore
626
741
  "LONG",
627
742
  quantity=quantity,
628
743
  strength=strength,
@@ -632,8 +747,15 @@ class MT5Strategy(Strategy):
632
747
  self._orders[symbol]["BSTPLMT"].append(order)
633
748
 
634
749
  def sell_stop_limit(
635
- self, id, symbol, price, stoplimit, quantity, strength=1.0, dtime=None
636
- ):
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:
637
759
  """
638
760
  Open a pending order to sell at a stop-limit price
639
761
 
@@ -648,10 +770,12 @@ class MT5Strategy(Strategy):
648
770
  raise ValueError(
649
771
  f"The stop-limit price {stoplimit} must be less than the price {price}."
650
772
  )
773
+ if dtime is None:
774
+ dtime = self.get_current_dt()
651
775
  order = SignalEvent(
652
776
  id,
653
777
  symbol,
654
- dtime,
778
+ dtime, # type: ignore
655
779
  "SHORT",
656
780
  quantity=quantity,
657
781
  strength=strength,
@@ -660,19 +784,31 @@ class MT5Strategy(Strategy):
660
784
  )
661
785
  self._orders[symbol]["SSTPLMT"].append(order)
662
786
 
663
- def check_pending_orders(self):
787
+ def check_pending_orders(self) -> None:
664
788
  """
665
789
  Check for pending orders and handle them accordingly.
666
790
  """
667
791
 
668
- def logmsg(order, type, symbol, dtime):
669
- 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(
670
799
  f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
671
- f"PRICE @ {round(order.price, 5)}",
800
+ f"PRICE @ {round(order.price, 5)}", # type: ignore
672
801
  custom_time=dtime,
673
802
  )
674
803
 
675
- 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:
676
812
  for order in self._orders[symbol][order_type].copy():
677
813
  if condition(order):
678
814
  execute_fn(order)
@@ -691,9 +827,13 @@ class MT5Strategy(Strategy):
691
827
 
692
828
  process_orders(
693
829
  "BLMT",
694
- lambda o: latest_close <= o.price,
830
+ lambda o: latest_close <= o.price, # type: ignore
695
831
  lambda o: self.buy_mkt(
696
- 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
697
837
  ),
698
838
  "BUY LIMIT",
699
839
  symbol,
@@ -702,9 +842,13 @@ class MT5Strategy(Strategy):
702
842
 
703
843
  process_orders(
704
844
  "SLMT",
705
- lambda o: latest_close >= o.price,
845
+ lambda o: latest_close >= o.price, # type: ignore
706
846
  lambda o: self.sell_mkt(
707
- 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
708
852
  ),
709
853
  "SELL LIMIT",
710
854
  symbol,
@@ -713,9 +857,13 @@ class MT5Strategy(Strategy):
713
857
 
714
858
  process_orders(
715
859
  "BSTP",
716
- lambda o: latest_close >= o.price,
860
+ lambda o: latest_close >= o.price, # type: ignore
717
861
  lambda o: self.buy_mkt(
718
- 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
719
867
  ),
720
868
  "BUY STOP",
721
869
  symbol,
@@ -724,9 +872,13 @@ class MT5Strategy(Strategy):
724
872
 
725
873
  process_orders(
726
874
  "SSTP",
727
- lambda o: latest_close <= o.price,
875
+ lambda o: latest_close <= o.price, # type: ignore
728
876
  lambda o: self.sell_mkt(
729
- 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
730
882
  ),
731
883
  "SELL STOP",
732
884
  symbol,
@@ -735,9 +887,13 @@ class MT5Strategy(Strategy):
735
887
 
736
888
  process_orders(
737
889
  "BSTPLMT",
738
- lambda o: latest_close >= o.price,
890
+ lambda o: latest_close >= o.price, # type: ignore
739
891
  lambda o: self.buy_limit(
740
- 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
741
897
  ),
742
898
  "BUY STOP LIMIT",
743
899
  symbol,
@@ -746,9 +902,13 @@ class MT5Strategy(Strategy):
746
902
 
747
903
  process_orders(
748
904
  "SSTPLMT",
749
- lambda o: latest_close <= o.price,
905
+ lambda o: latest_close <= o.price, # type: ignore
750
906
  lambda o: self.sell_limit(
751
- 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
752
912
  ),
753
913
  "SELL STOP LIMIT",
754
914
  symbol,
@@ -756,7 +916,7 @@ class MT5Strategy(Strategy):
756
916
  )
757
917
 
758
918
  @staticmethod
759
- def calculate_pct_change(current_price, lh_price) -> float:
919
+ def calculate_pct_change(current_price: float, lh_price: float) -> float:
760
920
  return ((current_price - lh_price) / lh_price) * 100
761
921
 
762
922
  def get_asset_values(
@@ -765,11 +925,11 @@ class MT5Strategy(Strategy):
765
925
  window: int,
766
926
  value_type: str = "returns",
767
927
  array: bool = True,
768
- bars: DataHandler = None,
928
+ bars: Optional[DataHandler] = None,
769
929
  mode: TradingMode = TradingMode.BACKTEST,
770
930
  tf: str = "D1",
771
- error: Literal["ignore", "raise"] = None,
772
- ) -> Dict[str, np.ndarray | pd.Series] | None:
931
+ error: Optional[Literal["ignore", "raise"]] = None,
932
+ ) -> Optional[Dict[str, Union[np.ndarray, pd.Series]]]:
773
933
  """
774
934
  Get the historical OHLCV value or returns or custum value
775
935
  based on the DataHandker of the assets in the symbol list.
@@ -793,7 +953,7 @@ class MT5Strategy(Strategy):
793
953
  """
794
954
  if mode not in [TradingMode.BACKTEST, TradingMode.LIVE]:
795
955
  raise ValueError("Mode must be an instance of TradingMode")
796
- asset_values = {}
956
+ asset_values: Dict[str, Union[np.ndarray, pd.Series]] = {}
797
957
  if mode == TradingMode.BACKTEST:
798
958
  if bars is None:
799
959
  raise ValueError("DataHandler is required for backtest mode.")
@@ -802,8 +962,9 @@ class MT5Strategy(Strategy):
802
962
  values = bars.get_latest_bars_values(asset, value_type, N=window)
803
963
  asset_values[asset] = values[~np.isnan(values)]
804
964
  else:
805
- values = bars.get_latest_bars(asset, N=window)
806
- 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]
807
968
  elif mode == TradingMode.LIVE:
808
969
  for asset in symbol_list:
809
970
  rates = Rates(asset, timeframe=tf, count=window + 1, **self.kwargs)
@@ -823,7 +984,7 @@ class MT5Strategy(Strategy):
823
984
  return None
824
985
 
825
986
  @staticmethod
826
- def is_signal_time(period_count, signal_inverval) -> bool:
987
+ def is_signal_time(period_count: Optional[int], signal_inverval: int) -> bool:
827
988
  """
828
989
  Check if we can generate a signal based on the current period count.
829
990
  We use the signal interval as a form of periodicity or rebalancing period.
@@ -842,11 +1003,17 @@ class MT5Strategy(Strategy):
842
1003
  @staticmethod
843
1004
  def stop_time(time_zone: str, stop_time: str) -> bool:
844
1005
  now = datetime.now(pytz.timezone(time_zone)).time()
845
- stop_time = datetime.strptime(stop_time, "%H:%M").time()
846
- return now >= stop_time
1006
+ stop_time_dt = datetime.strptime(stop_time, "%H:%M").time()
1007
+ return now >= stop_time_dt
847
1008
 
848
1009
  def ispositions(
849
- 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,
850
1017
  ) -> bool:
851
1018
  """
852
1019
  This function is use for live trading to check if there are open positions
@@ -877,7 +1044,13 @@ class MT5Strategy(Strategy):
877
1044
  return len(open_positions) >= max_trades
878
1045
  return False
879
1046
 
880
- 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:
881
1054
  """
882
1055
  Get the buy or sell prices for open positions of a given symbol and strategy.
883
1056
 
@@ -904,7 +1077,7 @@ class MT5Strategy(Strategy):
904
1077
  return np.array([])
905
1078
 
906
1079
  def get_active_orders(
907
- self, symbol: str, strategy_id: int, order_type: int = None
1080
+ self, symbol: str, strategy_id: int, order_type: Optional[int] = None
908
1081
  ) -> List[TradeOrder]:
909
1082
  """
910
1083
  Get the active orders for a given symbol and strategy.
@@ -924,16 +1097,24 @@ class MT5Strategy(Strategy):
924
1097
  List[TradeOrder] : A list of active orders for the given symbol and strategy.
925
1098
  """
926
1099
  orders = [
927
- o for o in self.orders if o.symbol == symbol and o.magic == strategy_id
1100
+ o
1101
+ for o in self.orders
1102
+ if isinstance(o, TradeOrder)
1103
+ and o.symbol == symbol
1104
+ and o.magic == strategy_id
928
1105
  ]
929
1106
  if order_type is not None and len(orders) > 0:
930
1107
  orders = [o for o in orders if o.type == order_type]
931
1108
  return orders
932
1109
 
933
- 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:
934
1113
  if len(prices) == 0:
935
1114
  return False
936
1115
  tick_info = self.account.get_tick_info(asset)
1116
+ if tick_info is None:
1117
+ return False
937
1118
  bid, ask = tick_info.bid, tick_info.ask
938
1119
  price = None
939
1120
  if len(prices) == 1:
@@ -953,7 +1134,7 @@ class MT5Strategy(Strategy):
953
1134
 
954
1135
  @staticmethod
955
1136
  def convert_time_zone(
956
- dt: datetime | int | pd.Timestamp,
1137
+ dt: Union[datetime, int, pd.Timestamp],
957
1138
  from_tz: str = "UTC",
958
1139
  to_tz: str = "US/Eastern",
959
1140
  ) -> pd.Timestamp:
@@ -968,20 +1149,24 @@ class MT5Strategy(Strategy):
968
1149
  Returns:
969
1150
  dt_to : The converted datetime.
970
1151
  """
971
- from_tz = pytz.timezone(from_tz)
1152
+ from_tz_pytz = pytz.timezone(from_tz)
972
1153
  if isinstance(dt, (datetime, int)):
973
- dt = pd.to_datetime(dt, unit="s")
974
- if dt.tzinfo is None:
975
- dt = dt.tz_localize(from_tz)
1154
+ dt_ts = pd.to_datetime(dt, unit="s")
1155
+ else:
1156
+ dt_ts = dt
1157
+ if dt_ts.tzinfo is None:
1158
+ dt_ts = dt_ts.tz_localize(from_tz_pytz)
976
1159
  else:
977
- dt = dt.tz_convert(from_tz)
1160
+ dt_ts = dt_ts.tz_convert(from_tz_pytz)
978
1161
 
979
- dt_to = dt.tz_convert(pytz.timezone(to_tz))
1162
+ dt_to = dt_ts.tz_convert(pytz.timezone(to_tz))
980
1163
  return dt_to
981
1164
 
982
1165
  @staticmethod
983
1166
  def get_mt5_equivalent(
984
- symbols, symbol_type: str | SymbolType = SymbolType.STOCKS, **kwargs
1167
+ symbols: List[str],
1168
+ symbol_type: Union[str, SymbolType] = SymbolType.STOCKS,
1169
+ **kwargs: Any,
985
1170
  ) -> List[str]:
986
1171
  """
987
1172
  Get the MetaTrader 5 equivalent symbols for the symbols in the list.
@@ -998,16 +1183,16 @@ class MT5Strategy(Strategy):
998
1183
 
999
1184
  account = Account(**kwargs)
1000
1185
  mt5_symbols = account.get_symbols(symbol_type=symbol_type)
1001
- mt5_equivalent = []
1186
+ mt5_equivalent: List[str] = []
1002
1187
 
1003
- def _get_admiral_symbols():
1188
+ def _get_admiral_symbols() -> None:
1004
1189
  for s in mt5_symbols:
1005
1190
  _s = s[1:] if s[0] in string.punctuation else s
1006
1191
  for symbol in symbols:
1007
1192
  if _s.split(".")[0] == symbol or _s.split("_")[0] == symbol:
1008
1193
  mt5_equivalent.append(s)
1009
1194
 
1010
- def _get_pepperstone_symbols():
1195
+ def _get_pepperstone_symbols() -> None:
1011
1196
  for s in mt5_symbols:
1012
1197
  for symbol in symbols:
1013
1198
  if s.split(".")[0] == symbol:
@@ -1027,4 +1212,6 @@ class MT5Strategy(Strategy):
1027
1212
  return mt5_equivalent
1028
1213
 
1029
1214
 
1030
- 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()")