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.
- bbstrader/__init__.py +1 -1
- bbstrader/__main__.py +2 -2
- bbstrader/apps/_copier.py +40 -37
- bbstrader/btengine/backtest.py +33 -28
- bbstrader/btengine/data.py +105 -81
- bbstrader/btengine/event.py +21 -22
- bbstrader/btengine/execution.py +51 -24
- bbstrader/btengine/performance.py +23 -12
- bbstrader/btengine/portfolio.py +40 -30
- bbstrader/btengine/scripts.py +13 -12
- bbstrader/btengine/strategy.py +288 -101
- bbstrader/compat.py +4 -3
- bbstrader/config.py +20 -36
- bbstrader/core/data.py +76 -48
- bbstrader/core/scripts.py +22 -21
- bbstrader/core/utils.py +13 -12
- bbstrader/trading/execution.py +6 -0
- bbstrader/tseries.py +55 -39
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/METADATA +3 -3
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/RECORD +25 -25
- tests/engine/test_portfolio.py +3 -2
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.6.dist-info → bbstrader-0.3.7.dist-info}/top_level.txt +0 -0
bbstrader/btengine/strategy.py
CHANGED
|
@@ -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
|
-
|
|
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]
|
|
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
|
|
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
|
|
97
|
-
symbol_list: List[str]
|
|
98
|
-
bars: DataHandler
|
|
99
|
-
mode: TradingMode
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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,
|
|
384
|
-
|
|
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(
|
|
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(
|
|
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
|
|
472
|
-
):
|
|
504
|
+
dtime: Union[datetime, pd.Timestamp],
|
|
505
|
+
) -> None:
|
|
473
506
|
position = SignalEvent(
|
|
474
|
-
id,
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
|
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,
|
|
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(
|
|
669
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
806
|
-
|
|
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
|
-
|
|
846
|
-
return now >=
|
|
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,
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
1152
|
+
from_tz_pytz = pytz.timezone(from_tz)
|
|
972
1153
|
if isinstance(dt, (datetime, int)):
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
1160
|
+
dt_ts = dt_ts.tz_convert(from_tz_pytz)
|
|
978
1161
|
|
|
979
|
-
dt_to =
|
|
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
|
|
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()")
|