bbstrader 0.1.91__py3-none-any.whl → 0.1.93__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 +26 -13
- bbstrader/btengine/data.py +10 -2
- bbstrader/btengine/event.py +10 -4
- bbstrader/btengine/execution.py +8 -7
- bbstrader/btengine/performance.py +2 -0
- bbstrader/btengine/portfolio.py +50 -28
- bbstrader/btengine/strategy.py +223 -69
- bbstrader/metatrader/account.py +1 -1
- bbstrader/metatrader/rates.py +50 -2
- bbstrader/metatrader/risk.py +28 -3
- bbstrader/metatrader/trade.py +7 -4
- bbstrader/models/__init__.py +5 -1
- bbstrader/models/optimization.py +177 -0
- bbstrader/models/portfolios.py +205 -0
- bbstrader/trading/execution.py +31 -16
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.93.dist-info}/METADATA +2 -1
- bbstrader-0.1.93.dist-info/RECORD +32 -0
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.93.dist-info}/WHEEL +1 -1
- bbstrader-0.1.91.dist-info/RECORD +0 -31
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.93.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.93.dist-info}/top_level.txt +0 -0
bbstrader/btengine/strategy.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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
|
|
7
6
|
from datetime import datetime
|
|
8
7
|
from bbstrader.config import config_logger
|
|
9
8
|
from bbstrader.btengine.event import SignalEvent
|
|
9
|
+
from bbstrader.btengine.event import FillEvent
|
|
10
10
|
from bbstrader.btengine.data import DataHandler
|
|
11
11
|
from bbstrader.metatrader.account import Account
|
|
12
12
|
from bbstrader.metatrader.rates import Rates
|
|
@@ -17,6 +17,7 @@ from typing import (
|
|
|
17
17
|
List,
|
|
18
18
|
Literal
|
|
19
19
|
)
|
|
20
|
+
from bbstrader.models.optimization import optimized_weights
|
|
20
21
|
|
|
21
22
|
__all__ = ['Strategy', 'MT5Strategy']
|
|
22
23
|
|
|
@@ -38,6 +39,8 @@ class Strategy(metaclass=ABCMeta):
|
|
|
38
39
|
|
|
39
40
|
The strategy hierarchy is relatively simple as it consists of an abstract
|
|
40
41
|
base class with a single pure virtual method for generating `SignalEvent` objects.
|
|
42
|
+
Other methods are provided to check for pending orders, update trades from fills,
|
|
43
|
+
and get updates from the portfolio.
|
|
41
44
|
"""
|
|
42
45
|
|
|
43
46
|
@abstractmethod
|
|
@@ -45,8 +48,9 @@ class Strategy(metaclass=ABCMeta):
|
|
|
45
48
|
raise NotImplementedError(
|
|
46
49
|
"Should implement calculate_signals()"
|
|
47
50
|
)
|
|
48
|
-
|
|
49
|
-
def
|
|
51
|
+
def check_pending_orders(self, *args, **kwargs): ...
|
|
52
|
+
def get_update_from_portfolio(self, *args, **kwargs): ...
|
|
53
|
+
def update_trades_from_fill(self, *args, **kwargs): ...
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
class MT5Strategy(Strategy):
|
|
@@ -68,23 +72,110 @@ class MT5Strategy(Strategy):
|
|
|
68
72
|
bars : The data handler object.
|
|
69
73
|
mode : The mode of operation for the strategy (backtest or live).
|
|
70
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.
|
|
71
78
|
"""
|
|
72
79
|
self.events = events
|
|
73
80
|
self.data = bars
|
|
74
81
|
self.symbols = symbol_list
|
|
75
82
|
self.mode = mode
|
|
76
|
-
self.
|
|
77
|
-
self.
|
|
78
|
-
self.
|
|
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')
|
|
87
|
+
self.logger = kwargs.get("logger")
|
|
88
|
+
self._initialize_portfolio()
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def cash(self) -> float:
|
|
92
|
+
return self._porfolio_value
|
|
93
|
+
|
|
94
|
+
@cash.setter
|
|
95
|
+
def cash(self, value):
|
|
96
|
+
self._porfolio_value = value
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
|
|
100
|
+
return self._orders
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def trades(self) -> Dict[str, Dict[str, int]]:
|
|
104
|
+
return self._trades
|
|
79
105
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self.
|
|
106
|
+
@property
|
|
107
|
+
def positions(self) -> Dict[str, Dict[str, int|float]]:
|
|
108
|
+
return self._positions
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def holdings(self) -> Dict[str, float]:
|
|
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
|
|
126
|
+
|
|
127
|
+
def _initialize_portfolio(self):
|
|
83
128
|
positions = ['LONG', 'SHORT']
|
|
84
129
|
orders = ['BLMT', 'BSTP', 'BSTPLMT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
130
|
+
self._positions: Dict[str, Dict[str, int | float]] = {}
|
|
131
|
+
self._trades: Dict[str, Dict[str, int]] = {}
|
|
132
|
+
self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
|
|
133
|
+
for symbol in self.symbols:
|
|
134
|
+
self._positions[symbol] = {}
|
|
135
|
+
self._orders[symbol] = {}
|
|
136
|
+
self._trades[symbol] = {}
|
|
137
|
+
for position in positions:
|
|
138
|
+
self._trades[symbol][position] = 0
|
|
139
|
+
self._positions[symbol][position] = 0.0
|
|
140
|
+
for order in orders:
|
|
141
|
+
self._orders[symbol][order] = []
|
|
142
|
+
self._holdings = {s: 0.0 for s in self.symbols}
|
|
143
|
+
|
|
144
|
+
def get_update_from_portfolio(self, positions, holdings):
|
|
145
|
+
"""
|
|
146
|
+
Update the positions and holdings for the strategy from the portfolio.
|
|
147
|
+
|
|
148
|
+
Positions are the number of shares of a security that are owned in long or short.
|
|
149
|
+
Holdings are the value (postions * price) of the security that are owned in long or short.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
positions : The positions for the symbols in the strategy.
|
|
153
|
+
holdings : The holdings for the symbols in the strategy.
|
|
154
|
+
"""
|
|
85
155
|
for symbol in self.symbols:
|
|
86
|
-
|
|
87
|
-
|
|
156
|
+
if symbol in positions:
|
|
157
|
+
if positions[symbol] > 0:
|
|
158
|
+
self._positions[symbol]['LONG'] = positions[symbol]
|
|
159
|
+
elif positions[symbol] < 0:
|
|
160
|
+
self._positions[symbol]['SHORT'] = positions[symbol]
|
|
161
|
+
else:
|
|
162
|
+
self._positions[symbol]['LONG'] = 0
|
|
163
|
+
self._positions[symbol]['SHORT'] = 0
|
|
164
|
+
if symbol in holdings:
|
|
165
|
+
self._holdings[symbol] = holdings[symbol]
|
|
166
|
+
|
|
167
|
+
def update_trades_from_fill(self, event: FillEvent):
|
|
168
|
+
"""
|
|
169
|
+
This method updates the trades for the strategy based on the fill event.
|
|
170
|
+
It is used to keep track of the number of trades executed for each order.
|
|
171
|
+
"""
|
|
172
|
+
if event.type == 'FILL':
|
|
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
|
|
88
179
|
|
|
89
180
|
def calculate_signals(self, *args, **kwargs
|
|
90
181
|
) -> Dict[str, Union[str, dict, None]] | None:
|
|
@@ -115,7 +206,25 @@ class MT5Strategy(Strategy):
|
|
|
115
206
|
"""
|
|
116
207
|
pass
|
|
117
208
|
|
|
118
|
-
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:
|
|
119
228
|
"""
|
|
120
229
|
Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
|
|
121
230
|
The quantity calculated can be used to evalute a strategy's performance for each symbol
|
|
@@ -127,11 +236,26 @@ class MT5Strategy(Strategy):
|
|
|
127
236
|
Returns:
|
|
128
237
|
qty : The quantity to buy or sell for the symbol.
|
|
129
238
|
"""
|
|
130
|
-
if self.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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)
|
|
135
259
|
|
|
136
260
|
def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
|
|
137
261
|
"""
|
|
@@ -153,11 +277,20 @@ class MT5Strategy(Strategy):
|
|
|
153
277
|
|
|
154
278
|
position = SignalEvent(id, symbol, dtime, signal,
|
|
155
279
|
quantity=quantity, strength=strength, price=price)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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)
|
|
292
|
+
|
|
293
|
+
def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
|
|
161
294
|
strength: float=1.0, dtime: datetime | pd.Timestamp=None):
|
|
162
295
|
"""
|
|
163
296
|
Open a long position
|
|
@@ -165,25 +298,22 @@ class MT5Strategy(Strategy):
|
|
|
165
298
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
166
299
|
"""
|
|
167
300
|
self._send_order(id, symbol, 'LONG', strength, price, quantity, dtime)
|
|
168
|
-
self.positions[symbol]['LONG'] += quantity
|
|
169
301
|
|
|
170
|
-
def
|
|
302
|
+
def sell_mkt(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
171
303
|
"""
|
|
172
304
|
Open a short position
|
|
173
305
|
|
|
174
306
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
175
307
|
"""
|
|
176
308
|
self._send_order(id, symbol, 'SHORT', strength, price, quantity, dtime)
|
|
177
|
-
self.positions[symbol]['SHORT'] += quantity
|
|
178
309
|
|
|
179
|
-
def
|
|
310
|
+
def close_positions(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
180
311
|
"""
|
|
181
|
-
Close a position
|
|
312
|
+
Close a position or exit all positions
|
|
182
313
|
|
|
183
314
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
184
315
|
"""
|
|
185
316
|
self._send_order(id, symbol, 'EXIT', strength, price, quantity, dtime)
|
|
186
|
-
self.positions[symbol]['LONG'] -= quantity
|
|
187
317
|
|
|
188
318
|
def buy_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
189
319
|
"""
|
|
@@ -197,7 +327,7 @@ class MT5Strategy(Strategy):
|
|
|
197
327
|
"The buy_stop price must be greater than the current price.")
|
|
198
328
|
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
199
329
|
quantity=quantity, strength=strength, price=price)
|
|
200
|
-
self.
|
|
330
|
+
self._orders[symbol]['BSTP'].append(order)
|
|
201
331
|
|
|
202
332
|
def sell_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
203
333
|
"""
|
|
@@ -211,7 +341,7 @@ class MT5Strategy(Strategy):
|
|
|
211
341
|
"The sell_stop price must be less than the current price.")
|
|
212
342
|
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
213
343
|
quantity=quantity, strength=strength, price=price)
|
|
214
|
-
self.
|
|
344
|
+
self._orders[symbol]['SSTP'].append(order)
|
|
215
345
|
|
|
216
346
|
def buy_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
217
347
|
"""
|
|
@@ -225,7 +355,7 @@ class MT5Strategy(Strategy):
|
|
|
225
355
|
"The buy_limit price must be less than the current price.")
|
|
226
356
|
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
227
357
|
quantity=quantity, strength=strength, price=price)
|
|
228
|
-
self.
|
|
358
|
+
self._orders[symbol]['BLMT'].append(order)
|
|
229
359
|
|
|
230
360
|
def sell_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
231
361
|
"""
|
|
@@ -239,7 +369,7 @@ class MT5Strategy(Strategy):
|
|
|
239
369
|
"The sell_limit price must be greater than the current price.")
|
|
240
370
|
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
241
371
|
quantity=quantity, strength=strength, price=price)
|
|
242
|
-
self.
|
|
372
|
+
self._orders[symbol]['SLMT'].append(order)
|
|
243
373
|
|
|
244
374
|
def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
|
|
245
375
|
quantity: int, strength: float=1.0, dtime: datetime | pd.Timestamp = None):
|
|
@@ -257,7 +387,7 @@ class MT5Strategy(Strategy):
|
|
|
257
387
|
f"The stop-limit price {stoplimit} must be greater than the price {price}.")
|
|
258
388
|
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
259
389
|
quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
|
|
260
|
-
self.
|
|
390
|
+
self._orders[symbol]['BSTPLMT'].append(order)
|
|
261
391
|
|
|
262
392
|
def sell_stop_limit(self, id, symbol, price, stoplimit, quantity, strength=1.0, dtime=None):
|
|
263
393
|
"""
|
|
@@ -274,7 +404,7 @@ class MT5Strategy(Strategy):
|
|
|
274
404
|
f"The stop-limit price {stoplimit} must be less than the price {price}.")
|
|
275
405
|
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
276
406
|
quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
|
|
277
|
-
self.
|
|
407
|
+
self._orders[symbol]['SSTPLMT'].append(order)
|
|
278
408
|
|
|
279
409
|
def check_pending_orders(self):
|
|
280
410
|
"""
|
|
@@ -282,55 +412,79 @@ class MT5Strategy(Strategy):
|
|
|
282
412
|
"""
|
|
283
413
|
for symbol in self.symbols:
|
|
284
414
|
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
285
|
-
|
|
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)
|
|
418
|
+
for order in self._orders[symbol]['BLMT'].copy():
|
|
286
419
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
287
|
-
self.
|
|
420
|
+
self.buy_mkt(order.strategy_id, symbol,
|
|
288
421
|
order.price, order.quantity, dtime)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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')
|
|
429
|
+
for order in self._orders[symbol]['SLMT'].copy():
|
|
294
430
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
295
|
-
self.
|
|
431
|
+
self.sell_mkt(order.strategy_id, symbol,
|
|
296
432
|
order.price, order.quantity, dtime)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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')
|
|
440
|
+
for order in self._orders[symbol]['BSTP'].copy():
|
|
302
441
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
303
|
-
self.
|
|
442
|
+
self.buy_mkt(order.strategy_id, symbol,
|
|
304
443
|
order.price, order.quantity, dtime)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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')
|
|
451
|
+
for order in self._orders[symbol]['SSTP'].copy():
|
|
310
452
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
311
|
-
self.
|
|
453
|
+
self.sell_mkt(order.strategy_id, symbol,
|
|
312
454
|
order.price, order.quantity, dtime)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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')
|
|
462
|
+
for order in self._orders[symbol]['BSTPLMT'].copy():
|
|
318
463
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
319
464
|
self.buy_limit(order.strategy_id, symbol,
|
|
320
465
|
order.stoplimit, order.quantity, dtime)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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')
|
|
473
|
+
for order in self._orders[symbol]['SSTPLMT'].copy():
|
|
326
474
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
327
475
|
self.sell_limit(order.strategy_id, symbol,
|
|
328
476
|
order.stoplimit, order.quantity, dtime)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
|
|
334
488
|
def get_asset_values(self,
|
|
335
489
|
symbol_list: List[str],
|
|
336
490
|
window: int,
|
bbstrader/metatrader/account.py
CHANGED
|
@@ -688,7 +688,7 @@ class Account(object):
|
|
|
688
688
|
"minors": r"\b(Minors?)\b",
|
|
689
689
|
"exotics": r"\b(Exotics?)\b",
|
|
690
690
|
}
|
|
691
|
-
return self._get_symbols_by_category('
|
|
691
|
+
return self._get_symbols_by_category('FX', category, fx_categories)
|
|
692
692
|
|
|
693
693
|
def get_stocks_from_country(self, country_code: str = 'USA', etf=True) -> List[str]:
|
|
694
694
|
"""
|
bbstrader/metatrader/rates.py
CHANGED
|
@@ -178,8 +178,10 @@ class Rates(object):
|
|
|
178
178
|
return TIMEFRAMES[time_frame]
|
|
179
179
|
|
|
180
180
|
def _fetch_data(
|
|
181
|
-
self,
|
|
182
|
-
|
|
181
|
+
self,
|
|
182
|
+
start: Union[int, datetime, pd.Timestamp],
|
|
183
|
+
count: Union[int, datetime, pd.Timestamp],
|
|
184
|
+
lower_colnames=False, utc=False,
|
|
183
185
|
) -> Union[pd.DataFrame, None]:
|
|
184
186
|
"""Fetches data from MT5 and returns a DataFrame or None."""
|
|
185
187
|
try:
|
|
@@ -187,6 +189,13 @@ class Rates(object):
|
|
|
187
189
|
rates = Mt5.copy_rates_from_pos(
|
|
188
190
|
self.symbol, self.time_frame, start, count
|
|
189
191
|
)
|
|
192
|
+
elif (
|
|
193
|
+
isinstance(start, (datetime, pd.Timestamp)) and
|
|
194
|
+
isinstance(count, int)
|
|
195
|
+
):
|
|
196
|
+
rates = Mt5.copy_rates_from(
|
|
197
|
+
self.symbol, self.time_frame, start, count
|
|
198
|
+
)
|
|
190
199
|
elif (
|
|
191
200
|
isinstance(start, (datetime, pd.Timestamp)) and
|
|
192
201
|
isinstance(count, (datetime, pd.Timestamp))
|
|
@@ -320,6 +329,35 @@ class Rates(object):
|
|
|
320
329
|
return self._filter_data(df, fill_na=fill_na)
|
|
321
330
|
return df
|
|
322
331
|
|
|
332
|
+
def get_rates_from(self, date_from: datetime | pd.Timestamp, count: int=MAX_BARS,
|
|
333
|
+
filter=False, fill_na=False, lower_colnames=False, utc=False) -> Union[pd.DataFrame, None]:
|
|
334
|
+
"""
|
|
335
|
+
Retrieves historical data within a specified date range.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
date_from : Starting date for data retrieval.
|
|
339
|
+
The data will be retrieved from this date going to the past.
|
|
340
|
+
|
|
341
|
+
count : Number of bars to retrieve.
|
|
342
|
+
|
|
343
|
+
filter : See `Rates.get_historical_data` for more details.
|
|
344
|
+
fill_na : See `Rates.get_historical_data` for more details.
|
|
345
|
+
lower_colnames : If True, the column names will be converted to lowercase.
|
|
346
|
+
utc (bool, optional): If True, the data will be in UTC timezone.
|
|
347
|
+
Defaults to False.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Union[pd.DataFrame, None]: A DataFrame containing historical
|
|
351
|
+
data if successful, otherwise None.
|
|
352
|
+
"""
|
|
353
|
+
utc = self._check_filter(filter, utc)
|
|
354
|
+
df = self._fetch_data(date_from, count, lower_colnames=lower_colnames, utc=utc)
|
|
355
|
+
if df is None:
|
|
356
|
+
return None
|
|
357
|
+
if filter:
|
|
358
|
+
return self._filter_data(df, fill_na=fill_na)
|
|
359
|
+
return df
|
|
360
|
+
|
|
323
361
|
@property
|
|
324
362
|
def open(self):
|
|
325
363
|
return self.__data['Open']
|
|
@@ -459,4 +497,14 @@ def get_data_from_pos(symbol, time_frame, start_pos=0, fill_na=False,
|
|
|
459
497
|
rates = Rates(symbol, time_frame, start_pos, count, session_duration)
|
|
460
498
|
data = rates.get_rates_from_pos(filter=filter, fill_na=fill_na,
|
|
461
499
|
lower_colnames=lower_colnames, utc=utc)
|
|
500
|
+
return data
|
|
501
|
+
|
|
502
|
+
def get_data_from_date(symbol, time_frame, date_from, count=MAX_BARS, fill_na=False,
|
|
503
|
+
lower_colnames=False, utc=False, filter=False):
|
|
504
|
+
"""Get historical data from a specific date.
|
|
505
|
+
See `Rates.get_rates_from` for more details.
|
|
506
|
+
"""
|
|
507
|
+
rates = Rates(symbol, time_frame)
|
|
508
|
+
data = rates.get_rates_from(date_from, count, filter=filter, fill_na=fill_na,
|
|
509
|
+
lower_colnames=lower_colnames, utc=utc)
|
|
462
510
|
return data
|
bbstrader/metatrader/risk.py
CHANGED
|
@@ -156,6 +156,22 @@ class RiskManagement(Account):
|
|
|
156
156
|
self.symbol_info = super().get_symbol_info(self.symbol)
|
|
157
157
|
|
|
158
158
|
self._tf = time_frame
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def dailydd(self) -> float:
|
|
162
|
+
return self.daily_dd
|
|
163
|
+
|
|
164
|
+
@dailydd.setter
|
|
165
|
+
def dailydd(self, value: float):
|
|
166
|
+
self.daily_dd = value
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def maxrisk(self) -> float:
|
|
170
|
+
return self.max_risk
|
|
171
|
+
|
|
172
|
+
@maxrisk.setter
|
|
173
|
+
def maxrisk(self, value: float):
|
|
174
|
+
self.max_risk = value
|
|
159
175
|
|
|
160
176
|
def _convert_time_frame(self, tf: str) -> int:
|
|
161
177
|
"""Convert time frame to minutes"""
|
|
@@ -437,14 +453,23 @@ class RiskManagement(Account):
|
|
|
437
453
|
if trade_risk > 0:
|
|
438
454
|
currency_risk = round(self.var_loss_value(), 5)
|
|
439
455
|
volume = round(currency_risk*laverage)
|
|
440
|
-
|
|
456
|
+
try:
|
|
457
|
+
_lot = round((volume / (contract_size * av_price)), 2)
|
|
458
|
+
except ZeroDivisionError:
|
|
459
|
+
_lot = 0.0
|
|
441
460
|
lot = self._check_lot(_lot)
|
|
442
461
|
if COMD and contract_size > 1:
|
|
443
462
|
# lot = volume / av_price / contract_size
|
|
444
|
-
|
|
463
|
+
try:
|
|
464
|
+
lot = volume / av_price / contract_size
|
|
465
|
+
except ZeroDivisionError:
|
|
466
|
+
lot = 0.0
|
|
445
467
|
lot = self._check_lot(_lot)
|
|
446
468
|
if FX:
|
|
447
|
-
|
|
469
|
+
try:
|
|
470
|
+
__lot = round((volume / contract_size), 2)
|
|
471
|
+
except ZeroDivisionError:
|
|
472
|
+
__lot = 0.0
|
|
448
473
|
lot = self._check_lot(__lot)
|
|
449
474
|
|
|
450
475
|
tick_value = s_info.trade_tick_value
|
bbstrader/metatrader/trade.py
CHANGED
|
@@ -838,14 +838,16 @@ class Trade(RiskManagement):
|
|
|
838
838
|
elif account and id is not None:
|
|
839
839
|
# All open positions for a specific strategy or expert no matter the symbol
|
|
840
840
|
positions = self.get_positions()
|
|
841
|
-
|
|
841
|
+
if positions is not None:
|
|
842
|
+
positions = [position for position in positions if position.magic == id]
|
|
842
843
|
elif not account and id is None:
|
|
843
844
|
# All open positions for the current symbol no matter the strategy or expert
|
|
844
845
|
positions = self.get_positions(symbol=self.symbol)
|
|
845
846
|
elif not account and id is not None:
|
|
846
847
|
# All open positions for the current symbol and a specific strategy or expert
|
|
847
848
|
positions = self.get_positions(symbol=self.symbol)
|
|
848
|
-
|
|
849
|
+
if positions is not None:
|
|
850
|
+
positions = [position for position in positions if position.magic == id]
|
|
849
851
|
profit = 0.0
|
|
850
852
|
balance = self.get_account_info().balance
|
|
851
853
|
target = round((balance * self.target)/100, 2)
|
|
@@ -1026,8 +1028,9 @@ class Trade(RiskManagement):
|
|
|
1026
1028
|
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
1027
1029
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1028
1030
|
msg = trade_retcode_message(result.retcode)
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
+
if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
1032
|
+
self.logger.error(
|
|
1033
|
+
f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
1031
1034
|
tries = 0
|
|
1032
1035
|
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
|
|
1033
1036
|
if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
|
bbstrader/models/__init__.py
CHANGED
|
@@ -3,4 +3,8 @@ The `models` module provides a foundational framework for implementing various q
|
|
|
3
3
|
|
|
4
4
|
It is designed to be a versatile base module for different types of models used in financial analysis and trading.
|
|
5
5
|
"""
|
|
6
|
-
from bbstrader.models.risk import *
|
|
6
|
+
from bbstrader.models.risk import *
|
|
7
|
+
from bbstrader.models.optimization import *
|
|
8
|
+
from bbstrader.models.portfolios import *
|
|
9
|
+
from bbstrader.models.factors import *
|
|
10
|
+
from bbstrader.models.ml import *
|