bbstrader 0.1.92__py3-none-any.whl → 0.1.94__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/btengine/backtest.py +20 -14
- bbstrader/btengine/data.py +230 -39
- bbstrader/btengine/execution.py +3 -2
- bbstrader/btengine/performance.py +2 -0
- bbstrader/btengine/portfolio.py +12 -16
- bbstrader/btengine/strategy.py +140 -45
- bbstrader/metatrader/account.py +67 -13
- bbstrader/metatrader/rates.py +22 -18
- bbstrader/metatrader/risk.py +34 -6
- bbstrader/metatrader/trade.py +14 -9
- bbstrader/models/__init__.py +5 -1
- bbstrader/models/optimization.py +12 -5
- bbstrader/models/portfolios.py +3 -0
- bbstrader/models/risk.py +9 -1
- bbstrader/trading/execution.py +38 -15
- bbstrader/trading/strategies.py +4 -4
- {bbstrader-0.1.92.dist-info → bbstrader-0.1.94.dist-info}/METADATA +5 -3
- bbstrader-0.1.94.dist-info/RECORD +32 -0
- {bbstrader-0.1.92.dist-info → bbstrader-0.1.94.dist-info}/WHEEL +1 -1
- bbstrader-0.1.92.dist-info/RECORD +0 -32
- {bbstrader-0.1.92.dist-info → bbstrader-0.1.94.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.92.dist-info → bbstrader-0.1.94.dist-info}/top_level.txt +0 -0
bbstrader/btengine/strategy.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from abc import ABCMeta, abstractmethod
|
|
2
2
|
import pytz
|
|
3
|
-
import math
|
|
4
3
|
import pandas as pd
|
|
5
4
|
import numpy as np
|
|
6
5
|
from queue import Queue
|
|
@@ -18,6 +17,7 @@ from typing import (
|
|
|
18
17
|
List,
|
|
19
18
|
Literal
|
|
20
19
|
)
|
|
20
|
+
from bbstrader.models.optimization import optimized_weights
|
|
21
21
|
|
|
22
22
|
__all__ = ['Strategy', 'MT5Strategy']
|
|
23
23
|
|
|
@@ -72,17 +72,29 @@ class MT5Strategy(Strategy):
|
|
|
72
72
|
bars : The data handler object.
|
|
73
73
|
mode : The mode of operation for the strategy (backtest or live).
|
|
74
74
|
**kwargs : Additional keyword arguments for other classes (e.g, Portfolio, ExecutionHandler).
|
|
75
|
+
- max_trades : The maximum number of trades allowed per symbol.
|
|
76
|
+
- time_frame : The time frame for the strategy.
|
|
77
|
+
- logger : The logger object for the strategy.
|
|
75
78
|
"""
|
|
76
79
|
self.events = events
|
|
77
80
|
self.data = bars
|
|
78
81
|
self.symbols = symbol_list
|
|
79
82
|
self.mode = mode
|
|
80
|
-
self.
|
|
81
|
-
self.
|
|
82
|
-
|
|
83
|
+
self._porfolio_value = None
|
|
84
|
+
self.risk_budget = self._check_risk_budget(**kwargs)
|
|
85
|
+
self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
|
|
86
|
+
self.tf = kwargs.get("time_frame", 'D1')
|
|
83
87
|
self.logger = kwargs.get("logger")
|
|
84
88
|
self._initialize_portfolio()
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def cash(self) -> float:
|
|
92
|
+
return self._porfolio_value
|
|
85
93
|
|
|
94
|
+
@cash.setter
|
|
95
|
+
def cash(self, value):
|
|
96
|
+
self._porfolio_value = value
|
|
97
|
+
|
|
86
98
|
@property
|
|
87
99
|
def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
|
|
88
100
|
return self._orders
|
|
@@ -98,11 +110,24 @@ class MT5Strategy(Strategy):
|
|
|
98
110
|
@property
|
|
99
111
|
def holdings(self) -> Dict[str, float]:
|
|
100
112
|
return self._holdings
|
|
113
|
+
|
|
114
|
+
def _check_risk_budget(self, **kwargs):
|
|
115
|
+
weights = kwargs.get('risk_weights')
|
|
116
|
+
if weights is not None and isinstance(weights, dict):
|
|
117
|
+
for asset in self.symbols:
|
|
118
|
+
if asset not in weights:
|
|
119
|
+
raise ValueError(f"Risk budget for asset {asset} is missing.")
|
|
120
|
+
total_risk = sum(weights.values())
|
|
121
|
+
if not np.isclose(total_risk, 1.0):
|
|
122
|
+
raise ValueError(f'Risk budget weights must sum to 1. got {total_risk}')
|
|
123
|
+
return weights
|
|
124
|
+
elif isinstance(weights, str):
|
|
125
|
+
return weights
|
|
101
126
|
|
|
102
127
|
def _initialize_portfolio(self):
|
|
103
128
|
positions = ['LONG', 'SHORT']
|
|
104
129
|
orders = ['BLMT', 'BSTP', 'BSTPLMT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
105
|
-
self._positions: Dict[str, Dict[str, int]] = {}
|
|
130
|
+
self._positions: Dict[str, Dict[str, int | float]] = {}
|
|
106
131
|
self._trades: Dict[str, Dict[str, int]] = {}
|
|
107
132
|
self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
|
|
108
133
|
for symbol in self.symbols:
|
|
@@ -111,7 +136,7 @@ class MT5Strategy(Strategy):
|
|
|
111
136
|
self._trades[symbol] = {}
|
|
112
137
|
for position in positions:
|
|
113
138
|
self._trades[symbol][position] = 0
|
|
114
|
-
self._positions[symbol][position] = 0
|
|
139
|
+
self._positions[symbol][position] = 0.0
|
|
115
140
|
for order in orders:
|
|
116
141
|
self._orders[symbol][order] = []
|
|
117
142
|
self._holdings = {s: 0.0 for s in self.symbols}
|
|
@@ -133,6 +158,9 @@ class MT5Strategy(Strategy):
|
|
|
133
158
|
self._positions[symbol]['LONG'] = positions[symbol]
|
|
134
159
|
elif positions[symbol] < 0:
|
|
135
160
|
self._positions[symbol]['SHORT'] = positions[symbol]
|
|
161
|
+
else:
|
|
162
|
+
self._positions[symbol]['LONG'] = 0
|
|
163
|
+
self._positions[symbol]['SHORT'] = 0
|
|
136
164
|
if symbol in holdings:
|
|
137
165
|
self._holdings[symbol] = holdings[symbol]
|
|
138
166
|
|
|
@@ -142,7 +170,12 @@ class MT5Strategy(Strategy):
|
|
|
142
170
|
It is used to keep track of the number of trades executed for each order.
|
|
143
171
|
"""
|
|
144
172
|
if event.type == 'FILL':
|
|
145
|
-
|
|
173
|
+
if event.order != 'EXIT':
|
|
174
|
+
self._trades[event.symbol][event.order] += 1
|
|
175
|
+
elif event.order == 'EXIT' and event.direction == 'BUY':
|
|
176
|
+
self._trades[event.symbol]['SHORT'] = 0
|
|
177
|
+
elif event.order == 'EXIT' and event.direction == 'SELL':
|
|
178
|
+
self._trades[event.symbol]['LONG'] = 0
|
|
146
179
|
|
|
147
180
|
def calculate_signals(self, *args, **kwargs
|
|
148
181
|
) -> Dict[str, Union[str, dict, None]] | None:
|
|
@@ -173,7 +206,25 @@ class MT5Strategy(Strategy):
|
|
|
173
206
|
"""
|
|
174
207
|
pass
|
|
175
208
|
|
|
176
|
-
def
|
|
209
|
+
def apply_risk_management(self, optimer, freq=252) -> Dict[str, float] | None:
|
|
210
|
+
"""
|
|
211
|
+
Apply risk management rules to the strategy.
|
|
212
|
+
"""
|
|
213
|
+
if optimer is None:
|
|
214
|
+
return None
|
|
215
|
+
prices = self.get_asset_values(
|
|
216
|
+
symbol_list=self.symbols, bars=self.data, mode=self.mode,
|
|
217
|
+
window=freq, value_type='close', array=False, tf=self.tf
|
|
218
|
+
)
|
|
219
|
+
prices = pd.DataFrame(prices)
|
|
220
|
+
prices = prices.dropna(axis=0, how='any')
|
|
221
|
+
try:
|
|
222
|
+
weights = optimized_weights(prices=prices, freq=freq, method=optimer)
|
|
223
|
+
return {symbol: weight for symbol, weight in weights.items()}
|
|
224
|
+
except Exception:
|
|
225
|
+
return {symbol: 0.0 for symbol in self.symbols}
|
|
226
|
+
|
|
227
|
+
def get_quantity(self, symbol, weight, price=None, volume=None, maxqty=None) -> int:
|
|
177
228
|
"""
|
|
178
229
|
Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
|
|
179
230
|
The quantity calculated can be used to evalute a strategy's performance for each symbol
|
|
@@ -185,15 +236,26 @@ class MT5Strategy(Strategy):
|
|
|
185
236
|
Returns:
|
|
186
237
|
qty : The quantity to buy or sell for the symbol.
|
|
187
238
|
"""
|
|
188
|
-
if self.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
239
|
+
if (self._porfolio_value is None or weight == 0 or
|
|
240
|
+
self._porfolio_value == 0 or np.isnan(self._porfolio_value)):
|
|
241
|
+
return 0
|
|
242
|
+
if volume is None:
|
|
243
|
+
volume = round(self._porfolio_value * weight)
|
|
244
|
+
if price is None:
|
|
245
|
+
price = self.data.get_latest_bar_value(symbol, 'close')
|
|
246
|
+
if (price is None or not isinstance(price, (int, float, np.number))
|
|
247
|
+
or volume is None or not isinstance(volume, (int, float, np.number))
|
|
248
|
+
or np.isnan(float(price))
|
|
249
|
+
or np.isnan(float(volume))
|
|
250
|
+
):
|
|
251
|
+
if weight != 0:
|
|
252
|
+
return 1
|
|
253
|
+
return 0
|
|
254
|
+
qty = round(volume / price, 2)
|
|
255
|
+
qty = max(qty, 0) / self.max_trades[symbol]
|
|
256
|
+
if maxqty is not None:
|
|
257
|
+
qty = min(qty, maxqty)
|
|
258
|
+
return max(round(qty, 2), 0)
|
|
197
259
|
|
|
198
260
|
def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
|
|
199
261
|
"""
|
|
@@ -215,9 +277,18 @@ class MT5Strategy(Strategy):
|
|
|
215
277
|
|
|
216
278
|
position = SignalEvent(id, symbol, dtime, signal,
|
|
217
279
|
quantity=quantity, strength=strength, price=price)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
280
|
+
log = False
|
|
281
|
+
if signal in ['LONG', 'SHORT']:
|
|
282
|
+
if self._trades[symbol][signal] < self.max_trades[symbol] and quantity > 0:
|
|
283
|
+
self.events.put(position)
|
|
284
|
+
log = True
|
|
285
|
+
elif signal == 'EXIT':
|
|
286
|
+
if self._positions[symbol]['LONG'] > 0 or self._positions[symbol]['SHORT'] < 0:
|
|
287
|
+
self.events.put(position)
|
|
288
|
+
log = True
|
|
289
|
+
if log:
|
|
290
|
+
self.logger.info(
|
|
291
|
+
f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
|
|
221
292
|
|
|
222
293
|
def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
|
|
223
294
|
strength: float=1.0, dtime: datetime | pd.Timestamp=None):
|
|
@@ -341,55 +412,79 @@ class MT5Strategy(Strategy):
|
|
|
341
412
|
"""
|
|
342
413
|
for symbol in self.symbols:
|
|
343
414
|
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
415
|
+
logmsg = lambda order, type: self.logger.info(
|
|
416
|
+
f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
417
|
+
f"PRICE @ {order.price}", custom_time=dtime)
|
|
344
418
|
for order in self._orders[symbol]['BLMT'].copy():
|
|
345
419
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
346
420
|
self.buy_mkt(order.strategy_id, symbol,
|
|
347
421
|
order.price, order.quantity, dtime)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
422
|
+
try:
|
|
423
|
+
self._orders[symbol]['BLMT'].remove(order)
|
|
424
|
+
assert order not in self._orders[symbol]['BLMT']
|
|
425
|
+
logmsg(order, 'BUY LIMIT')
|
|
426
|
+
except AssertionError:
|
|
427
|
+
self._orders[symbol]['BLMT'] = [o for o in self._orders[symbol]['BLMT'] if o != order]
|
|
428
|
+
logmsg(order, 'BUY LIMIT')
|
|
352
429
|
for order in self._orders[symbol]['SLMT'].copy():
|
|
353
430
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
354
431
|
self.sell_mkt(order.strategy_id, symbol,
|
|
355
432
|
order.price, order.quantity, dtime)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
433
|
+
try:
|
|
434
|
+
self._orders[symbol]['SLMT'].remove(order)
|
|
435
|
+
assert order not in self._orders[symbol]['SLMT']
|
|
436
|
+
logmsg(order, 'SELL LIMIT')
|
|
437
|
+
except AssertionError:
|
|
438
|
+
self._orders[symbol]['SLMT'] = [o for o in self._orders[symbol]['SLMT'] if o != order]
|
|
439
|
+
logmsg(order, 'SELL LIMIT')
|
|
360
440
|
for order in self._orders[symbol]['BSTP'].copy():
|
|
361
441
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
362
442
|
self.buy_mkt(order.strategy_id, symbol,
|
|
363
443
|
order.price, order.quantity, dtime)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
444
|
+
try:
|
|
445
|
+
self._orders[symbol]['BSTP'].remove(order)
|
|
446
|
+
assert order not in self._orders[symbol]['BSTP']
|
|
447
|
+
logmsg(order, 'BUY STOP')
|
|
448
|
+
except AssertionError:
|
|
449
|
+
self._orders[symbol]['BSTP'] = [o for o in self._orders[symbol]['BSTP'] if o != order]
|
|
450
|
+
logmsg(order, 'BUY STOP')
|
|
368
451
|
for order in self._orders[symbol]['SSTP'].copy():
|
|
369
452
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
370
453
|
self.sell_mkt(order.strategy_id, symbol,
|
|
371
454
|
order.price, order.quantity, dtime)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
455
|
+
try:
|
|
456
|
+
self._orders[symbol]['SSTP'].remove(order)
|
|
457
|
+
assert order not in self._orders[symbol]['SSTP']
|
|
458
|
+
logmsg(order, 'SELL STOP')
|
|
459
|
+
except AssertionError:
|
|
460
|
+
self._orders[symbol]['SSTP'] = [o for o in self._orders[symbol]['SSTP'] if o != order]
|
|
461
|
+
logmsg(order, 'SELL STOP')
|
|
376
462
|
for order in self._orders[symbol]['BSTPLMT'].copy():
|
|
377
463
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
378
464
|
self.buy_limit(order.strategy_id, symbol,
|
|
379
465
|
order.stoplimit, order.quantity, dtime)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
466
|
+
try:
|
|
467
|
+
self._orders[symbol]['BSTPLMT'].remove(order)
|
|
468
|
+
assert order not in self._orders[symbol]['BSTPLMT']
|
|
469
|
+
logmsg(order, 'BUY STOP LIMIT')
|
|
470
|
+
except AssertionError:
|
|
471
|
+
self._orders[symbol]['BSTPLMT'] = [o for o in self._orders[symbol]['BSTPLMT'] if o != order]
|
|
472
|
+
logmsg(order, 'BUY STOP LIMIT')
|
|
384
473
|
for order in self._orders[symbol]['SSTPLMT'].copy():
|
|
385
474
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
386
475
|
self.sell_limit(order.strategy_id, symbol,
|
|
387
476
|
order.stoplimit, order.quantity, dtime)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
477
|
+
try:
|
|
478
|
+
self._orders[symbol]['SSTPLMT'].remove(order)
|
|
479
|
+
assert order not in self._orders[symbol]['SSTPLMT']
|
|
480
|
+
logmsg(order, 'SELL STOP LIMIT')
|
|
481
|
+
except AssertionError:
|
|
482
|
+
self._orders[symbol]['SSTPLMT'] = [o for o in self._orders[symbol]['SSTPLMT'] if o != order]
|
|
483
|
+
logmsg(order, 'SELL STOP LIMIT')
|
|
484
|
+
|
|
485
|
+
def calculate_pct_change(self, current_price, lh_price):
|
|
486
|
+
return ((current_price - lh_price) / lh_price) * 100
|
|
487
|
+
|
|
393
488
|
def get_asset_values(self,
|
|
394
489
|
symbol_list: List[str],
|
|
395
490
|
window: int,
|
bbstrader/metatrader/account.py
CHANGED
|
@@ -125,9 +125,57 @@ AMG_EXCHANGES = {
|
|
|
125
125
|
'XSWX': r"Switzerland.*\(SWX\)"
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
def check_mt5_connection():
|
|
128
|
+
def check_mt5_connection(**kwargs):
|
|
129
|
+
"""
|
|
130
|
+
Initialize the connection to the MetaTrader 5 terminal.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
path (str, optional): The path to the MetaTrader 5 terminal executable file.
|
|
134
|
+
Defaults to None (e.g., "C:\\Program Files\\MetaTrader 5\\terminal64.exe").
|
|
135
|
+
login (int, optional): The login ID of the trading account. Defaults to None.
|
|
136
|
+
password (str, optional): The password of the trading account. Defaults to None.
|
|
137
|
+
server (str, optional): The name of the trade server to which the client terminal is connected.
|
|
138
|
+
Defaults to None.
|
|
139
|
+
timeout (int, optional): Connection timeout in milliseconds. Defaults to 60_000.
|
|
140
|
+
portable (bool, optional): If True, the portable mode of the terminal is used.
|
|
141
|
+
Defaults to False (See https://www.metatrader5.com/en/terminal/help/start_advanced/start#portable).
|
|
142
|
+
|
|
143
|
+
Notes:
|
|
144
|
+
If you want to lunch multiple terminal instances:
|
|
145
|
+
- Follow these instructions to lunch each terminal in portable mode first:
|
|
146
|
+
https://www.metatrader5.com/en/terminal/help/start_advanced/start#configuration_file
|
|
147
|
+
"""
|
|
148
|
+
path = kwargs.get('path', None)
|
|
149
|
+
login = kwargs.get('login', None)
|
|
150
|
+
password = kwargs.get('password', None)
|
|
151
|
+
server = kwargs.get('server', None)
|
|
152
|
+
timeout = kwargs.get('timeout', 60_000)
|
|
153
|
+
portable = kwargs.get('portable', False)
|
|
154
|
+
|
|
155
|
+
if path is None and (login or password or server):
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"You must provide a path to the terminal executable file"
|
|
158
|
+
f"when providing login, password or server"
|
|
159
|
+
)
|
|
129
160
|
try:
|
|
130
|
-
|
|
161
|
+
if path is not None:
|
|
162
|
+
if (
|
|
163
|
+
login is not None and
|
|
164
|
+
password is not None and
|
|
165
|
+
server is not None
|
|
166
|
+
):
|
|
167
|
+
init = mt5.initialize(
|
|
168
|
+
path=path,
|
|
169
|
+
login=login,
|
|
170
|
+
password=password,
|
|
171
|
+
server=server,
|
|
172
|
+
timeout=timeout,
|
|
173
|
+
portable=portable
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
init = mt5.initialize(path=path)
|
|
177
|
+
else:
|
|
178
|
+
init = mt5.initialize()
|
|
131
179
|
if not init:
|
|
132
180
|
raise_mt5_error(INIT_MSG)
|
|
133
181
|
except Exception:
|
|
@@ -135,9 +183,9 @@ def check_mt5_connection():
|
|
|
135
183
|
|
|
136
184
|
|
|
137
185
|
class Broker(object):
|
|
138
|
-
def __init__(self, name: str=None):
|
|
186
|
+
def __init__(self, name: str=None, **kwargs):
|
|
139
187
|
if name is None:
|
|
140
|
-
check_mt5_connection()
|
|
188
|
+
check_mt5_connection(**kwargs)
|
|
141
189
|
self._name = mt5.account_info().company
|
|
142
190
|
else:
|
|
143
191
|
self._name = name
|
|
@@ -157,8 +205,8 @@ class Broker(object):
|
|
|
157
205
|
|
|
158
206
|
|
|
159
207
|
class AdmiralMarktsGroup(Broker):
|
|
160
|
-
def __init__(self):
|
|
161
|
-
super().__init__("Admirals Group AS")
|
|
208
|
+
def __init__(self, **kwargs):
|
|
209
|
+
super().__init__("Admirals Group AS", **kwargs)
|
|
162
210
|
|
|
163
211
|
@property
|
|
164
212
|
def timezone(self) -> str:
|
|
@@ -166,8 +214,8 @@ class AdmiralMarktsGroup(Broker):
|
|
|
166
214
|
|
|
167
215
|
|
|
168
216
|
class JustGlobalMarkets(Broker):
|
|
169
|
-
def __init__(self):
|
|
170
|
-
super().__init__("Just Global Markets Ltd.")
|
|
217
|
+
def __init__(self, **kwargs):
|
|
218
|
+
super().__init__("Just Global Markets Ltd.", **kwargs)
|
|
171
219
|
|
|
172
220
|
@property
|
|
173
221
|
def timezone(self) -> str:
|
|
@@ -175,8 +223,8 @@ class JustGlobalMarkets(Broker):
|
|
|
175
223
|
|
|
176
224
|
|
|
177
225
|
class FTMO(Broker):
|
|
178
|
-
def __init__(self):
|
|
179
|
-
super().__init__("FTMO S.R.O.")
|
|
226
|
+
def __init__(self, **kwargs):
|
|
227
|
+
super().__init__("FTMO S.R.O.", **kwargs)
|
|
180
228
|
|
|
181
229
|
@property
|
|
182
230
|
def timezone(self) -> str:
|
|
@@ -230,8 +278,14 @@ class Account(object):
|
|
|
230
278
|
>>> trade_history = account.get_trade_history(from_date, to_date)
|
|
231
279
|
"""
|
|
232
280
|
|
|
233
|
-
def __init__(self):
|
|
234
|
-
|
|
281
|
+
def __init__(self, **kwargs):
|
|
282
|
+
"""
|
|
283
|
+
Initialize the Account class.
|
|
284
|
+
|
|
285
|
+
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
check_mt5_connection(**kwargs)
|
|
235
289
|
self._check_brokers()
|
|
236
290
|
|
|
237
291
|
def _check_brokers(self):
|
|
@@ -688,7 +742,7 @@ class Account(object):
|
|
|
688
742
|
"minors": r"\b(Minors?)\b",
|
|
689
743
|
"exotics": r"\b(Exotics?)\b",
|
|
690
744
|
}
|
|
691
|
-
return self._get_symbols_by_category('
|
|
745
|
+
return self._get_symbols_by_category('FX', category, fx_categories)
|
|
692
746
|
|
|
693
747
|
def get_stocks_from_country(self, country_code: str = 'USA', etf=True) -> List[str]:
|
|
694
748
|
"""
|
bbstrader/metatrader/rates.py
CHANGED
|
@@ -82,6 +82,8 @@ class Rates(object):
|
|
|
82
82
|
2. The `open, high, low, close, adjclose, returns,
|
|
83
83
|
volume` properties returns data in Broker's timezone by default.
|
|
84
84
|
|
|
85
|
+
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
86
|
+
|
|
85
87
|
Example:
|
|
86
88
|
>>> rates = Rates("EURUSD", "1h")
|
|
87
89
|
>>> df = rates.get_historical_data(
|
|
@@ -94,17 +96,18 @@ class Rates(object):
|
|
|
94
96
|
def __init__(
|
|
95
97
|
self,
|
|
96
98
|
symbol: str,
|
|
97
|
-
|
|
99
|
+
timeframe: TimeFrame = 'D1',
|
|
98
100
|
start_pos: Union[int , str] = 0,
|
|
99
101
|
count: Optional[int] = MAX_BARS,
|
|
100
|
-
session_duration: Optional[float] = None
|
|
102
|
+
session_duration: Optional[float] = None,
|
|
103
|
+
**kwargs
|
|
101
104
|
):
|
|
102
105
|
"""
|
|
103
106
|
Initializes a new Rates instance.
|
|
104
107
|
|
|
105
108
|
Args:
|
|
106
109
|
symbol (str): Financial instrument symbol (e.g., "EURUSD").
|
|
107
|
-
|
|
110
|
+
timeframe (str): Timeframe string (e.g., "D1", "1h", "5m").
|
|
108
111
|
start_pos (int, | str): Starting index (int) or date (str) for data retrieval.
|
|
109
112
|
count (int, optional): Number of bars to retrieve default is
|
|
110
113
|
the maximum bars availble in the MT5 terminal.
|
|
@@ -118,16 +121,17 @@ class Rates(object):
|
|
|
118
121
|
For `session_duration` check your broker symbols details
|
|
119
122
|
"""
|
|
120
123
|
self.symbol = symbol
|
|
121
|
-
|
|
124
|
+
tf = kwargs.get('time_frame')
|
|
125
|
+
self.time_frame = self._validate_time_frame(timeframe)
|
|
122
126
|
self.sd = session_duration
|
|
123
|
-
self.start_pos = self._get_start_pos(start_pos,
|
|
127
|
+
self.start_pos = self._get_start_pos(start_pos, timeframe)
|
|
124
128
|
self.count = count
|
|
125
|
-
self._mt5_initialized()
|
|
126
|
-
self.__account = Account()
|
|
129
|
+
self._mt5_initialized(**kwargs)
|
|
130
|
+
self.__account = Account(**kwargs)
|
|
127
131
|
self.__data = self.get_rates_from_pos()
|
|
128
132
|
|
|
129
|
-
def _mt5_initialized(self):
|
|
130
|
-
check_mt5_connection()
|
|
133
|
+
def _mt5_initialized(self, **kwargs):
|
|
134
|
+
check_mt5_connection(**kwargs)
|
|
131
135
|
|
|
132
136
|
def _get_start_pos(self, index, time_frame):
|
|
133
137
|
if isinstance(index, int):
|
|
@@ -471,13 +475,13 @@ class Rates(object):
|
|
|
471
475
|
df.to_csv(f"{self.symbol}.csv")
|
|
472
476
|
return df
|
|
473
477
|
|
|
474
|
-
def download_historical_data(symbol,
|
|
478
|
+
def download_historical_data(symbol, timeframe, date_from,
|
|
475
479
|
date_to=pd.Timestamp.now(),lower_colnames=True,
|
|
476
|
-
utc=False, filter=False, fill_na=False, save_csv=False):
|
|
480
|
+
utc=False, filter=False, fill_na=False, save_csv=False, **kwargs):
|
|
477
481
|
"""Download historical data from MetaTrader 5 terminal.
|
|
478
482
|
See `Rates.get_historical_data` for more details.
|
|
479
483
|
"""
|
|
480
|
-
rates = Rates(symbol,
|
|
484
|
+
rates = Rates(symbol, timeframe, **kwargs)
|
|
481
485
|
data = rates.get_historical_data(
|
|
482
486
|
date_from=date_from,
|
|
483
487
|
date_to=date_to,
|
|
@@ -488,23 +492,23 @@ def download_historical_data(symbol, time_frame, date_from,
|
|
|
488
492
|
)
|
|
489
493
|
return data
|
|
490
494
|
|
|
491
|
-
def get_data_from_pos(symbol,
|
|
495
|
+
def get_data_from_pos(symbol, timeframe, start_pos=0, fill_na=False,
|
|
492
496
|
count=MAX_BARS, lower_colnames=False, utc=False, filter=False,
|
|
493
|
-
session_duration=23.0):
|
|
497
|
+
session_duration=23.0, **kwargs):
|
|
494
498
|
"""Get historical data from a specific position.
|
|
495
499
|
See `Rates.get_rates_from_pos` for more details.
|
|
496
500
|
"""
|
|
497
|
-
rates = Rates(symbol,
|
|
501
|
+
rates = Rates(symbol, timeframe, start_pos, count, session_duration, **kwargs)
|
|
498
502
|
data = rates.get_rates_from_pos(filter=filter, fill_na=fill_na,
|
|
499
503
|
lower_colnames=lower_colnames, utc=utc)
|
|
500
504
|
return data
|
|
501
505
|
|
|
502
|
-
def get_data_from_date(symbol,
|
|
503
|
-
lower_colnames=False, utc=False, filter=False):
|
|
506
|
+
def get_data_from_date(symbol, timeframe, date_from, count=MAX_BARS, fill_na=False,
|
|
507
|
+
lower_colnames=False, utc=False, filter=False, **kwargs):
|
|
504
508
|
"""Get historical data from a specific date.
|
|
505
509
|
See `Rates.get_rates_from` for more details.
|
|
506
510
|
"""
|
|
507
|
-
rates = Rates(symbol,
|
|
511
|
+
rates = Rates(symbol, timeframe, **kwargs)
|
|
508
512
|
data = rates.get_rates_from(date_from, count, filter=filter, fill_na=fill_na,
|
|
509
513
|
lower_colnames=lower_colnames, utc=utc)
|
|
510
514
|
return data
|
bbstrader/metatrader/risk.py
CHANGED
|
@@ -118,8 +118,10 @@ class RiskManagement(Account):
|
|
|
118
118
|
tp (int, optional): Take Profit in points, Must be a positive number.
|
|
119
119
|
be (int, optional): Break Even in points, Must be a positive number.
|
|
120
120
|
rr (float, optional): Risk reward ratio, Must be a positive number. Defaults to 1.5.
|
|
121
|
+
|
|
122
|
+
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
121
123
|
"""
|
|
122
|
-
super().__init__()
|
|
124
|
+
super().__init__(**kwargs)
|
|
123
125
|
|
|
124
126
|
# Validation
|
|
125
127
|
if daily_risk is not None and daily_risk < 0:
|
|
@@ -137,6 +139,7 @@ class RiskManagement(Account):
|
|
|
137
139
|
if var_time_frame not in TIMEFRAMES:
|
|
138
140
|
raise ValueError("Unsupported time frame {}".format(var_time_frame))
|
|
139
141
|
|
|
142
|
+
self.kwargs = kwargs
|
|
140
143
|
self.symbol = symbol
|
|
141
144
|
self.start_time = start_time
|
|
142
145
|
self.finishing_time = finishing_time
|
|
@@ -156,6 +159,22 @@ class RiskManagement(Account):
|
|
|
156
159
|
self.symbol_info = super().get_symbol_info(self.symbol)
|
|
157
160
|
|
|
158
161
|
self._tf = time_frame
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def dailydd(self) -> float:
|
|
165
|
+
return self.daily_dd
|
|
166
|
+
|
|
167
|
+
@dailydd.setter
|
|
168
|
+
def dailydd(self, value: float):
|
|
169
|
+
self.daily_dd = value
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def maxrisk(self) -> float:
|
|
173
|
+
return self.max_risk
|
|
174
|
+
|
|
175
|
+
@maxrisk.setter
|
|
176
|
+
def maxrisk(self, value: float):
|
|
177
|
+
self.max_risk = value
|
|
159
178
|
|
|
160
179
|
def _convert_time_frame(self, tf: str) -> int:
|
|
161
180
|
"""Convert time frame to minutes"""
|
|
@@ -263,7 +282,7 @@ class RiskManagement(Account):
|
|
|
263
282
|
tf_int = self._convert_time_frame(self._tf)
|
|
264
283
|
interval = round((minutes / tf_int) * 252)
|
|
265
284
|
|
|
266
|
-
rate = Rates(self.symbol, self._tf, 0, interval)
|
|
285
|
+
rate = Rates(self.symbol, self._tf, 0, interval, **self.kwargs)
|
|
267
286
|
returns = rate.returns*100
|
|
268
287
|
std = returns.std()
|
|
269
288
|
point = self.get_symbol_info(self.symbol).point
|
|
@@ -317,7 +336,7 @@ class RiskManagement(Account):
|
|
|
317
336
|
tf_int = self._convert_time_frame(tf)
|
|
318
337
|
interval = round((minutes / tf_int) * 252)
|
|
319
338
|
|
|
320
|
-
rate = Rates(self.symbol, tf, 0, interval)
|
|
339
|
+
rate = Rates(self.symbol, tf, 0, interval, **self.kwargs)
|
|
321
340
|
returns = rate.returns*100
|
|
322
341
|
p = self.get_account_info().margin_free
|
|
323
342
|
mu = returns.mean()
|
|
@@ -437,14 +456,23 @@ class RiskManagement(Account):
|
|
|
437
456
|
if trade_risk > 0:
|
|
438
457
|
currency_risk = round(self.var_loss_value(), 5)
|
|
439
458
|
volume = round(currency_risk*laverage)
|
|
440
|
-
|
|
459
|
+
try:
|
|
460
|
+
_lot = round((volume / (contract_size * av_price)), 2)
|
|
461
|
+
except ZeroDivisionError:
|
|
462
|
+
_lot = 0.0
|
|
441
463
|
lot = self._check_lot(_lot)
|
|
442
464
|
if COMD and contract_size > 1:
|
|
443
465
|
# lot = volume / av_price / contract_size
|
|
444
|
-
|
|
466
|
+
try:
|
|
467
|
+
lot = volume / av_price / contract_size
|
|
468
|
+
except ZeroDivisionError:
|
|
469
|
+
lot = 0.0
|
|
445
470
|
lot = self._check_lot(_lot)
|
|
446
471
|
if FX:
|
|
447
|
-
|
|
472
|
+
try:
|
|
473
|
+
__lot = round((volume / contract_size), 2)
|
|
474
|
+
except ZeroDivisionError:
|
|
475
|
+
__lot = 0.0
|
|
448
476
|
lot = self._check_lot(__lot)
|
|
449
477
|
|
|
450
478
|
tick_value = s_info.trade_tick_value
|