bbstrader 2.0.3__cp312-cp312-macosx_11_0_arm64.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.
- bbstrader/__init__.py +27 -0
- bbstrader/__main__.py +92 -0
- bbstrader/api/__init__.py +96 -0
- bbstrader/api/handlers.py +245 -0
- bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
- bbstrader/api/metatrader_client.pyi +624 -0
- bbstrader/assets/bbs_.png +0 -0
- bbstrader/assets/bbstrader.ico +0 -0
- bbstrader/assets/bbstrader.png +0 -0
- bbstrader/assets/qs_metrics_1.png +0 -0
- bbstrader/btengine/__init__.py +54 -0
- bbstrader/btengine/backtest.py +358 -0
- bbstrader/btengine/data.py +737 -0
- bbstrader/btengine/event.py +229 -0
- bbstrader/btengine/execution.py +287 -0
- bbstrader/btengine/performance.py +408 -0
- bbstrader/btengine/portfolio.py +393 -0
- bbstrader/btengine/strategy.py +588 -0
- bbstrader/compat.py +28 -0
- bbstrader/config.py +100 -0
- bbstrader/core/__init__.py +27 -0
- bbstrader/core/data.py +628 -0
- bbstrader/core/strategy.py +466 -0
- bbstrader/metatrader/__init__.py +48 -0
- bbstrader/metatrader/_copier.py +720 -0
- bbstrader/metatrader/account.py +865 -0
- bbstrader/metatrader/broker.py +418 -0
- bbstrader/metatrader/copier.py +1487 -0
- bbstrader/metatrader/rates.py +495 -0
- bbstrader/metatrader/risk.py +667 -0
- bbstrader/metatrader/trade.py +1692 -0
- bbstrader/metatrader/utils.py +402 -0
- bbstrader/models/__init__.py +39 -0
- bbstrader/models/nlp.py +932 -0
- bbstrader/models/optimization.py +182 -0
- bbstrader/scripts.py +665 -0
- bbstrader/trading/__init__.py +33 -0
- bbstrader/trading/execution.py +1159 -0
- bbstrader/trading/strategy.py +362 -0
- bbstrader/trading/utils.py +69 -0
- bbstrader-2.0.3.dist-info/METADATA +396 -0
- bbstrader-2.0.3.dist-info/RECORD +45 -0
- bbstrader-2.0.3.dist-info/WHEEL +5 -0
- bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
- bbstrader-2.0.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
from abc import ABCMeta, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import pytz
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
13
|
+
from bbstrader.models.optimization import optimized_weights
|
|
14
|
+
|
|
15
|
+
logger.add(
|
|
16
|
+
f"{BBSTRADER_DIR}/logs/strategy.log",
|
|
17
|
+
enqueue=True,
|
|
18
|
+
level="INFO",
|
|
19
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"TradeAction",
|
|
25
|
+
"TradeSignal",
|
|
26
|
+
"TradingMode",
|
|
27
|
+
"generate_signal",
|
|
28
|
+
"Strategy",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TradeAction(Enum):
|
|
33
|
+
"""
|
|
34
|
+
An enumeration class for trade actions.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
BUY = "LONG"
|
|
38
|
+
SELL = "SHORT"
|
|
39
|
+
LONG = "LONG"
|
|
40
|
+
SHORT = "SHORT"
|
|
41
|
+
BMKT = "BMKT"
|
|
42
|
+
SMKT = "SMKT"
|
|
43
|
+
BLMT = "BLMT"
|
|
44
|
+
SLMT = "SLMT"
|
|
45
|
+
BSTP = "BSTP"
|
|
46
|
+
SSTP = "SSTP"
|
|
47
|
+
BSTPLMT = "BSTPLMT"
|
|
48
|
+
SSTPLMT = "SSTPLMT"
|
|
49
|
+
EXIT = "EXIT"
|
|
50
|
+
EXIT_LONG = "EXIT_LONG"
|
|
51
|
+
EXIT_SHORT = "EXIT_SHORT"
|
|
52
|
+
EXIT_STOP = "EXIT_STOP"
|
|
53
|
+
EXIT_LIMIT = "EXIT_LIMIT"
|
|
54
|
+
EXIT_LONG_STOP = "EXIT_LONG_STOP"
|
|
55
|
+
EXIT_LONG_LIMIT = "EXIT_LONG_LIMIT"
|
|
56
|
+
EXIT_SHORT_STOP = "EXIT_SHORT_STOP"
|
|
57
|
+
EXIT_SHORT_LIMIT = "EXIT_SHORT_LIMIT"
|
|
58
|
+
EXIT_LONG_STOP_LIMIT = "EXIT_LONG_STOP_LIMIT"
|
|
59
|
+
EXIT_SHORT_STOP_LIMIT = "EXIT_SHORT_STOP_LIMIT"
|
|
60
|
+
EXIT_PROFITABLES = "EXIT_PROFITABLES"
|
|
61
|
+
EXIT_LOSINGS = "EXIT_LOSINGS"
|
|
62
|
+
EXIT_ALL_POSITIONS = "EXIT_ALL_POSITIONS"
|
|
63
|
+
EXIT_ALL_ORDERS = "EXIT_ALL_ORDERS"
|
|
64
|
+
|
|
65
|
+
def __str__(self):
|
|
66
|
+
return self.value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass()
|
|
70
|
+
class TradeSignal:
|
|
71
|
+
"""
|
|
72
|
+
Represents a trading signal generated by a trading system or strategy.
|
|
73
|
+
|
|
74
|
+
Notes
|
|
75
|
+
-----
|
|
76
|
+
Attributes:
|
|
77
|
+
|
|
78
|
+
- id (int): A unique identifier for the trade signal or the strategy.
|
|
79
|
+
- symbol (str): The trading symbol (e.g., stock ticker, forex pair, crypto asset).
|
|
80
|
+
- action (TradeAction): The trading action to perform. Must be an instance of the ``TradeAction`` enum (e.g., BUY, SELL).
|
|
81
|
+
- price (float, optional): The price at which the trade should be executed.
|
|
82
|
+
- stoplimit (float, optional): A stop-limit price for the trade. Must not be set without specifying a price.
|
|
83
|
+
- sl (float, optional): A stop loss price for the trade.
|
|
84
|
+
- tp (float, optional): A take profit price for the trade.
|
|
85
|
+
- comment (str, optional): An optional comment or description related to the trade signal.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
id: int
|
|
89
|
+
symbol: str
|
|
90
|
+
action: TradeAction
|
|
91
|
+
price: float = None
|
|
92
|
+
stoplimit: float = None
|
|
93
|
+
sl: float = None
|
|
94
|
+
tp: float = None
|
|
95
|
+
comment: str = None
|
|
96
|
+
|
|
97
|
+
def __post_init__(self):
|
|
98
|
+
if not isinstance(self.action, TradeAction):
|
|
99
|
+
raise TypeError(
|
|
100
|
+
f"action must be of type TradeAction, not {type(self.action)}"
|
|
101
|
+
)
|
|
102
|
+
if self.stoplimit is not None and self.price is None:
|
|
103
|
+
raise ValueError("stoplimit cannot be set without price")
|
|
104
|
+
|
|
105
|
+
def __repr__(self):
|
|
106
|
+
return (
|
|
107
|
+
f"TradeSignal(id={self.id}, symbol='{self.symbol}', action='{self.action.value}', "
|
|
108
|
+
f"price={self.price}, stoplimit={self.stoplimit}, sl={self.sl}, tp={self.tp}, comment='{self.comment or ''}')"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def generate_signal(
|
|
113
|
+
id: int,
|
|
114
|
+
symbol: str,
|
|
115
|
+
action: TradeAction,
|
|
116
|
+
price: float = None,
|
|
117
|
+
stoplimit: float = None,
|
|
118
|
+
sl: float = None,
|
|
119
|
+
tp: float = None,
|
|
120
|
+
comment: str = None,
|
|
121
|
+
) -> TradeSignal:
|
|
122
|
+
"""
|
|
123
|
+
Generates a trade signal for MetaTrader 5.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
id (int): Unique identifier for the trade signal.
|
|
127
|
+
symbol (str): The symbol for which the trade signal is generated.
|
|
128
|
+
action (TradeAction): The action to be taken (e.g., BUY, SELL).
|
|
129
|
+
price (float, optional): The price at which to execute the trade.
|
|
130
|
+
stoplimit (float, optional): The stop limit price for the trade.
|
|
131
|
+
sl (float, optional): The stop loss price for the trade.
|
|
132
|
+
tp (float, optional): The take profit price for the trade.
|
|
133
|
+
comment (str, optional): Additional comments for the trade.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
TradeSignal: A TradeSignal object containing the details of the trade signal.
|
|
137
|
+
"""
|
|
138
|
+
return TradeSignal(
|
|
139
|
+
id=id,
|
|
140
|
+
symbol=symbol,
|
|
141
|
+
action=action,
|
|
142
|
+
price=price,
|
|
143
|
+
stoplimit=stoplimit,
|
|
144
|
+
sl=sl,
|
|
145
|
+
tp=tp,
|
|
146
|
+
comment=comment,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TradingMode(Enum):
|
|
151
|
+
BACKTEST = "BACKTEST"
|
|
152
|
+
LIVE = "LIVE"
|
|
153
|
+
|
|
154
|
+
def isbacktest(self) -> bool:
|
|
155
|
+
return self == TradingMode.BACKTEST
|
|
156
|
+
|
|
157
|
+
def islive(self) -> bool:
|
|
158
|
+
return self == TradingMode.LIVE
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Strategy(metaclass=ABCMeta):
|
|
162
|
+
"""
|
|
163
|
+
A `Strategy()` object encapsulates all calculation on market data
|
|
164
|
+
that generate advisory signals to a `Portfolio` object. Thus all of
|
|
165
|
+
the "strategy logic" resides within this class. We opted to separate
|
|
166
|
+
out the `Strategy` and `Portfolio` objects for this backtester,
|
|
167
|
+
since we believe this is more amenable to the situation of multiple
|
|
168
|
+
strategies feeding "ideas" to a larger `Portfolio`, which then can handle
|
|
169
|
+
its own risk (such as sector allocation, leverage). In higher frequency trading,
|
|
170
|
+
the strategy and portfolio concepts will be tightly coupled and extremely
|
|
171
|
+
hardware dependent.
|
|
172
|
+
|
|
173
|
+
At this stage in the event-driven backtester development there is no concept of
|
|
174
|
+
an indicator or filter, such as those found in technical trading. These are also
|
|
175
|
+
good candidates for creating a class hierarchy.
|
|
176
|
+
|
|
177
|
+
The strategy hierarchy is relatively simple as it consists of an abstract
|
|
178
|
+
base class with a single pure virtual method for generating `SignalEvent` objects.
|
|
179
|
+
Other methods are provided to check for pending orders, update trades from fills,
|
|
180
|
+
and get updates from the portfolio.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
@abstractmethod
|
|
184
|
+
def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal] | None:
|
|
185
|
+
raise NotImplementedError("Should implement calculate_signals()")
|
|
186
|
+
|
|
187
|
+
def check_pending_orders(self, *args: Any, **kwargs: Any) -> None: ...
|
|
188
|
+
def get_update_from_portfolio(self, *args: Any, **kwargs: Any) -> None: ...
|
|
189
|
+
def update_trades_from_fill(self, *args: Any, **kwargs: Any) -> None: ...
|
|
190
|
+
def perform_period_end_checks(self, *args: Any, **kwargs: Any) -> None: ...
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class BaseStrategy(Strategy):
|
|
194
|
+
"""
|
|
195
|
+
Base class containing shared logic for both Backtest and Live MT5 strategies.
|
|
196
|
+
This class handles configuration, logging, and common utility calculations.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
tf: str
|
|
200
|
+
id: int
|
|
201
|
+
ID: int
|
|
202
|
+
max_trades: Dict[str, int]
|
|
203
|
+
risk_budget: Optional[Union[Dict[str, float], str]]
|
|
204
|
+
symbols: List[str]
|
|
205
|
+
logger: "logger" # type: ignore
|
|
206
|
+
kwargs: Dict[str, Any]
|
|
207
|
+
periodes: int
|
|
208
|
+
NAME: str
|
|
209
|
+
DESCRIPTION: str
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
symbol_list: List[str],
|
|
214
|
+
**kwargs: Any,
|
|
215
|
+
) -> None:
|
|
216
|
+
self.symbols = symbol_list
|
|
217
|
+
self.risk_budget = self._check_risk_budget(**kwargs)
|
|
218
|
+
self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
|
|
219
|
+
self.tf = kwargs.get("time_frame", "D1")
|
|
220
|
+
self.logger = kwargs.get("logger") or logger
|
|
221
|
+
self.kwargs = kwargs
|
|
222
|
+
self.periodes = 0
|
|
223
|
+
|
|
224
|
+
def _check_risk_budget(
|
|
225
|
+
self, **kwargs: Any
|
|
226
|
+
) -> Optional[Union[Dict[str, float], str]]:
|
|
227
|
+
weights = kwargs.get("risk_weights")
|
|
228
|
+
if weights is not None and isinstance(weights, dict):
|
|
229
|
+
for asset in self.symbols:
|
|
230
|
+
if asset not in weights:
|
|
231
|
+
raise ValueError(f"Risk budget for asset {asset} is missing.")
|
|
232
|
+
total_risk = float(round(sum(weights.values())))
|
|
233
|
+
if not np.isclose(total_risk, 1.0):
|
|
234
|
+
raise ValueError(f"Risk budget weights must sum to 1. got {total_risk}")
|
|
235
|
+
return weights
|
|
236
|
+
elif isinstance(weights, str):
|
|
237
|
+
return weights
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
@abstractmethod
|
|
242
|
+
def cash(self) -> float:
|
|
243
|
+
"""Returns the available cash (virtual or real)."""
|
|
244
|
+
raise NotImplementedError
|
|
245
|
+
|
|
246
|
+
def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal] | None:
|
|
247
|
+
"""
|
|
248
|
+
Provides the mechanisms to calculate signals for the strategy.
|
|
249
|
+
This methods should return a list of signals for the strategy For Live mode and None For Backtest mode.
|
|
250
|
+
|
|
251
|
+
Each signal must be a ``TradeSignal`` object with the following attributes:
|
|
252
|
+
- ``id``: The unique identifier for the strategy or order.
|
|
253
|
+
- ``action``: The order to execute on the symbol (LONG, SHORT, EXIT, etc.), see `bbstrader.core.utils.TradeAction`.
|
|
254
|
+
- ``symbol``: The trading symbol (e.g., stock ticker, forex pair, crypto asset).
|
|
255
|
+
- See ``bbstrader.core.strategy.TradeSignal`` for other optionnal arguments.
|
|
256
|
+
"""
|
|
257
|
+
raise NotImplementedError("Should implement calculate_signals()")
|
|
258
|
+
|
|
259
|
+
def perform_period_end_checks(self, *args: Any, **kwargs: Any) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Some strategies may require additional checks at the end of the period,
|
|
262
|
+
such as closing all positions or orders or tracking the performance of the strategy etc.
|
|
263
|
+
|
|
264
|
+
This method is called at the end of the period to perform such checks.
|
|
265
|
+
"""
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
@abstractmethod
|
|
269
|
+
def get_asset_values(
|
|
270
|
+
self,
|
|
271
|
+
symbol_list: List[str],
|
|
272
|
+
window: int,
|
|
273
|
+
value_type: str = "returns",
|
|
274
|
+
array: bool = True,
|
|
275
|
+
**kwargs,
|
|
276
|
+
) -> Optional[Dict[str, Union[np.ndarray, pd.Series]]]:
|
|
277
|
+
"""
|
|
278
|
+
Get the historical OHLCV value or returns or custum value
|
|
279
|
+
based on the DataHandker of the assets in the symbol list.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
bars : DataHandler for market data handling, required for backtest mode.
|
|
283
|
+
symbol_list : List of ticker symbols for the pairs trading strategy.
|
|
284
|
+
value_type : The type of value to get (e.g., returns, open, high, low, close, adjclose, volume).
|
|
285
|
+
array : If True, return the values as numpy arrays, otherwise as pandas Series.
|
|
286
|
+
mode : Mode of operation for the strategy.
|
|
287
|
+
window : The lookback period for resquesting the data.
|
|
288
|
+
tf : The time frame for the strategy.
|
|
289
|
+
error : The error handling method for the function.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
asset_values : Historical values of the assets in the symbol list.
|
|
293
|
+
|
|
294
|
+
Note:
|
|
295
|
+
In Live mode, the `bbstrader.metatrader.rates.Rates` class is used to get the historical data
|
|
296
|
+
so the value_type must be 'returns', 'open', 'high', 'low', 'close', 'adjclose', 'volume'.
|
|
297
|
+
"""
|
|
298
|
+
raise NotImplementedError
|
|
299
|
+
|
|
300
|
+
def apply_risk_management(
|
|
301
|
+
self,
|
|
302
|
+
optimizer: str,
|
|
303
|
+
symbols: Optional[List[str]] = None,
|
|
304
|
+
freq: int = 252,
|
|
305
|
+
) -> Optional[Dict[str, float]]:
|
|
306
|
+
"""Apply risk management optimization."""
|
|
307
|
+
if optimizer is None:
|
|
308
|
+
return None
|
|
309
|
+
symbols = symbols or self.symbols
|
|
310
|
+
|
|
311
|
+
prices = self.get_asset_values(
|
|
312
|
+
symbol_list=symbols,
|
|
313
|
+
window=freq,
|
|
314
|
+
value_type="close",
|
|
315
|
+
array=False,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if prices is None:
|
|
319
|
+
return None
|
|
320
|
+
prices = pd.DataFrame(prices)
|
|
321
|
+
prices = prices.dropna(axis=0, how="any")
|
|
322
|
+
try:
|
|
323
|
+
weights = optimized_weights(prices=prices, freq=freq, method=optimizer)
|
|
324
|
+
return {symbol: abs(weight) for symbol, weight in weights.items()}
|
|
325
|
+
except Exception:
|
|
326
|
+
return {symbol: 0.0 for symbol in symbols}
|
|
327
|
+
|
|
328
|
+
def get_quantity(
|
|
329
|
+
self,
|
|
330
|
+
symbol: str,
|
|
331
|
+
weight: float,
|
|
332
|
+
price: Optional[float] = None,
|
|
333
|
+
volume: Optional[float] = None,
|
|
334
|
+
maxqty: Optional[int] = None,
|
|
335
|
+
) -> int:
|
|
336
|
+
"""
|
|
337
|
+
Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
|
|
338
|
+
The quantity calculated can be used to evalute a strategy's performance for each symbol
|
|
339
|
+
given the fact that the dollar value is the same for all symbols.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
symbol : The symbol for the trade.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
qty : The quantity to buy or sell for the symbol.
|
|
346
|
+
"""
|
|
347
|
+
current_cash = self.cash
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
current_cash is None
|
|
351
|
+
or weight == 0
|
|
352
|
+
or current_cash == 0
|
|
353
|
+
or np.isnan(current_cash)
|
|
354
|
+
):
|
|
355
|
+
return 0
|
|
356
|
+
if price is None:
|
|
357
|
+
vals = self.get_asset_values(
|
|
358
|
+
[symbol], window=1, value_type="close", array=True
|
|
359
|
+
)
|
|
360
|
+
if vals and symbol in vals and len(vals[symbol]) > 0:
|
|
361
|
+
price = float(vals[symbol][-1])
|
|
362
|
+
else:
|
|
363
|
+
price = None
|
|
364
|
+
|
|
365
|
+
if volume is None:
|
|
366
|
+
volume = round(current_cash * weight)
|
|
367
|
+
|
|
368
|
+
if (
|
|
369
|
+
price is None
|
|
370
|
+
or not isinstance(price, (int, float, np.number))
|
|
371
|
+
or volume is None
|
|
372
|
+
or not isinstance(volume, (int, float, np.number))
|
|
373
|
+
or np.isnan(float(price))
|
|
374
|
+
or np.isnan(float(volume))
|
|
375
|
+
):
|
|
376
|
+
if weight != 0:
|
|
377
|
+
return 1
|
|
378
|
+
return 0
|
|
379
|
+
|
|
380
|
+
qty = round(volume / price, 2)
|
|
381
|
+
qty = max(qty, 0) / self.max_trades.get(symbol, 1)
|
|
382
|
+
if maxqty is not None:
|
|
383
|
+
qty = min(qty, maxqty)
|
|
384
|
+
return int(max(round(qty, 2), 0))
|
|
385
|
+
|
|
386
|
+
def get_quantities(
|
|
387
|
+
self, quantities: Optional[Union[Dict[str, int], int]]
|
|
388
|
+
) -> Dict[str, Optional[int]]:
|
|
389
|
+
"""
|
|
390
|
+
Get the quantities to buy or sell for the symbols in the strategy.
|
|
391
|
+
This method is used when whe need to assign different quantities to the symbols.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
quantities : The quantities for the symbols in the strategy.
|
|
395
|
+
"""
|
|
396
|
+
if quantities is None:
|
|
397
|
+
return {symbol: None for symbol in self.symbols}
|
|
398
|
+
if isinstance(quantities, dict):
|
|
399
|
+
return quantities
|
|
400
|
+
elif isinstance(quantities, int):
|
|
401
|
+
return {symbol: quantities for symbol in self.symbols}
|
|
402
|
+
raise TypeError(f"Unsupported type for quantities: {type(quantities)}")
|
|
403
|
+
|
|
404
|
+
@staticmethod
|
|
405
|
+
def calculate_pct_change(current_price: float, lh_price: float) -> float:
|
|
406
|
+
return ((current_price - lh_price) / lh_price) * 100
|
|
407
|
+
|
|
408
|
+
@staticmethod
|
|
409
|
+
def is_signal_time(period_count: Optional[int], signal_inverval: int) -> bool:
|
|
410
|
+
"""
|
|
411
|
+
Check if we can generate a signal based on the current period count.
|
|
412
|
+
We use the signal interval as a form of periodicity or rebalancing period.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
period_count : The current period count (e.g., number of bars).
|
|
416
|
+
signal_inverval : The signal interval for generating signals (e.g., every 5 bars).
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
bool : True if we can generate a signal, False otherwise
|
|
420
|
+
"""
|
|
421
|
+
if period_count == 0 or period_count is None:
|
|
422
|
+
return True
|
|
423
|
+
return period_count % signal_inverval == 0
|
|
424
|
+
|
|
425
|
+
@staticmethod
|
|
426
|
+
def get_current_dt(time_zone: str = "US/Eastern") -> datetime:
|
|
427
|
+
return datetime.now(pytz.timezone(time_zone))
|
|
428
|
+
|
|
429
|
+
@staticmethod
|
|
430
|
+
def convert_time_zone(
|
|
431
|
+
dt: Union[datetime, int, pd.Timestamp],
|
|
432
|
+
from_tz: str = "UTC",
|
|
433
|
+
to_tz: str = "US/Eastern",
|
|
434
|
+
) -> pd.Timestamp:
|
|
435
|
+
"""
|
|
436
|
+
Convert datetime from one timezone to another.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
dt : The datetime to convert.
|
|
440
|
+
from_tz : The timezone to convert from.
|
|
441
|
+
to_tz : The timezone to convert to.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
dt_to : The converted datetime.
|
|
445
|
+
"""
|
|
446
|
+
from_tz_pytz = pytz.timezone(from_tz)
|
|
447
|
+
if isinstance(dt, (datetime, int)):
|
|
448
|
+
dt_ts = pd.to_datetime(dt, unit="s")
|
|
449
|
+
else:
|
|
450
|
+
dt_ts = dt
|
|
451
|
+
if dt_ts.tzinfo is None:
|
|
452
|
+
dt_ts = dt_ts.tz_localize(from_tz_pytz)
|
|
453
|
+
else:
|
|
454
|
+
dt_ts = dt_ts.tz_convert(from_tz_pytz)
|
|
455
|
+
return dt_ts.tz_convert(pytz.timezone(to_tz))
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def stop_time(time_zone: str, stop_time: str) -> bool:
|
|
459
|
+
now = datetime.now(pytz.timezone(time_zone)).time()
|
|
460
|
+
stop_time_dt = datetime.strptime(stop_time, "%H:%M").time()
|
|
461
|
+
return now >= stop_time_dt
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class TWSStrategy(Strategy):
|
|
465
|
+
def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal]:
|
|
466
|
+
raise NotImplementedError("Should implement calculate_signals()")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Overview
|
|
3
|
+
========
|
|
4
|
+
|
|
5
|
+
This MetaTrader Module provides a direct interface to the MetaTrader 5 trading platform,
|
|
6
|
+
enabling seamless integration of Python-based trading strategies with a live trading environment.
|
|
7
|
+
It offers a comprehensive set of tools for account management, trade execution, market data retrieval,
|
|
8
|
+
and risk management, all tailored for the MetaTrader 5 platform.
|
|
9
|
+
|
|
10
|
+
Features
|
|
11
|
+
========
|
|
12
|
+
|
|
13
|
+
- **Direct MetaTrader 5 Integration**: Connects to the MetaTrader 5 terminal to access its full range of trading functionalities.
|
|
14
|
+
- **Account and Trade Management**: Provides tools for querying account information, managing open positions, and executing trades.
|
|
15
|
+
- **Market Data Retrieval**: Fetches historical and real-time market data, including rates and ticks, directly from MetaTrader 5.
|
|
16
|
+
- **Risk Management**: Includes utilities for managing risk, such as setting stop-loss and take-profit levels.
|
|
17
|
+
- **Trade Copying**: Functionality to copy trades between different MetaTrader 5 accounts.
|
|
18
|
+
|
|
19
|
+
Components
|
|
20
|
+
==========
|
|
21
|
+
|
|
22
|
+
- **Account**: Manages account information, including balance, equity, and margin.
|
|
23
|
+
- **Broker**: Handles the connection to the MetaTrader 5 terminal.
|
|
24
|
+
- **Copier**: Copies trades between accounts.
|
|
25
|
+
- **Rates**: Retrieves historical and current market rates.
|
|
26
|
+
- **Risk**: Provides risk management functionalities.
|
|
27
|
+
- **Trade**: Manages trade execution and position management.
|
|
28
|
+
- **Utils**: Contains utility functions for the MetaTrader module.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
========
|
|
32
|
+
|
|
33
|
+
>>> from bbstrader.metatrader import Account
|
|
34
|
+
>>> account = Account()
|
|
35
|
+
>>> print(account.get_account_info())
|
|
36
|
+
|
|
37
|
+
Notes
|
|
38
|
+
=====
|
|
39
|
+
|
|
40
|
+
This module requires the MetaTrader 5 terminal to be installed and running.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from bbstrader.metatrader.account import * # noqa: F403
|
|
44
|
+
from bbstrader.metatrader.rates import * # noqa: F403
|
|
45
|
+
from bbstrader.metatrader.risk import * # noqa: F403
|
|
46
|
+
from bbstrader.metatrader.trade import * # noqa: F403
|
|
47
|
+
from bbstrader.metatrader.utils import * # noqa: F403
|
|
48
|
+
from bbstrader.metatrader.copier import * # noqa: F403
|