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.
- bbstrader/__init__.py +11 -2
- bbstrader/__main__.py +6 -1
- bbstrader/apps/_copier.py +43 -40
- 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 +396 -134
- 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/metatrader/account.py +51 -26
- bbstrader/metatrader/analysis.py +30 -16
- bbstrader/metatrader/copier.py +75 -40
- bbstrader/metatrader/trade.py +29 -39
- bbstrader/metatrader/utils.py +5 -4
- bbstrader/models/nlp.py +83 -66
- bbstrader/trading/execution.py +45 -22
- bbstrader/tseries.py +158 -166
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/METADATA +7 -21
- bbstrader-0.3.7.dist-info/RECORD +62 -0
- bbstrader-0.3.7.dist-info/top_level.txt +3 -0
- docs/conf.py +56 -0
- tests/__init__.py +0 -0
- tests/engine/__init__.py +1 -0
- tests/engine/test_backtest.py +58 -0
- tests/engine/test_data.py +536 -0
- tests/engine/test_events.py +300 -0
- tests/engine/test_execution.py +219 -0
- tests/engine/test_portfolio.py +308 -0
- tests/metatrader/__init__.py +0 -0
- tests/metatrader/test_account.py +1769 -0
- tests/metatrader/test_rates.py +292 -0
- tests/metatrader/test_risk_management.py +700 -0
- tests/metatrader/test_trade.py +439 -0
- bbstrader-0.3.5.dist-info/RECORD +0 -49
- bbstrader-0.3.5.dist-info/top_level.txt +0 -1
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/licenses/LICENSE +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 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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
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
|
|
96
|
-
symbol_list: List[str]
|
|
97
|
-
bars: DataHandler
|
|
98
|
-
mode: TradingMode
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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,
|
|
313
|
-
|
|
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(
|
|
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(
|
|
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
|
|
401
|
-
):
|
|
504
|
+
dtime: Union[datetime, pd.Timestamp],
|
|
505
|
+
) -> None:
|
|
402
506
|
position = SignalEvent(
|
|
403
|
-
id,
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
|
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,
|
|
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(
|
|
598
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
735
|
-
|
|
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
|
-
|
|
775
|
-
return now >=
|
|
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,
|
|
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(
|
|
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(
|
|
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 = [
|
|
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(
|
|
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
|
|
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
|
-
|
|
1152
|
+
from_tz_pytz = pytz.timezone(from_tz)
|
|
897
1153
|
if isinstance(dt, (datetime, int)):
|
|
898
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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()")
|