bbstrader 0.2.0__py3-none-any.whl → 0.2.1__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/__ini__.py +7 -7
- bbstrader/btengine/__init__.py +7 -7
- bbstrader/btengine/backtest.py +30 -26
- bbstrader/btengine/data.py +92 -81
- bbstrader/btengine/event.py +2 -1
- bbstrader/btengine/execution.py +18 -16
- bbstrader/btengine/performance.py +11 -7
- bbstrader/btengine/portfolio.py +35 -36
- bbstrader/btengine/strategy.py +113 -92
- bbstrader/config.py +12 -10
- bbstrader/core/data.py +4 -5
- bbstrader/core/utils.py +57 -0
- bbstrader/ibkr/utils.py +0 -0
- bbstrader/metatrader/__init__.py +5 -5
- bbstrader/metatrader/account.py +117 -121
- bbstrader/metatrader/rates.py +81 -78
- bbstrader/metatrader/risk.py +23 -37
- bbstrader/metatrader/trade.py +154 -138
- bbstrader/metatrader/utils.py +3 -3
- bbstrader/models/__init__.py +5 -5
- bbstrader/models/factors.py +17 -12
- bbstrader/models/ml.py +371 -305
- bbstrader/models/optimization.py +14 -12
- bbstrader/models/portfolio.py +44 -35
- bbstrader/models/risk.py +15 -9
- bbstrader/trading/__init__.py +2 -2
- bbstrader/trading/execution.py +245 -179
- bbstrader/trading/scripts.py +8 -4
- bbstrader/trading/strategies.py +78 -65
- bbstrader/tseries.py +124 -98
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/METADATA +2 -1
- bbstrader-0.2.1.dist-info/RECORD +37 -0
- bbstrader-0.2.0.dist-info/RECORD +0 -36
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/top_level.txt +0 -0
bbstrader/btengine/strategy.py
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
|
+
import string
|
|
1
2
|
from abc import ABCMeta, abstractmethod
|
|
2
|
-
import pytz
|
|
3
|
-
import pandas as pd
|
|
4
|
-
import numpy as np
|
|
5
|
-
from queue import Queue
|
|
6
3
|
from datetime import datetime
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
|
|
4
|
+
from queue import Queue
|
|
5
|
+
from typing import Dict, List, Literal, Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import pytz
|
|
10
|
+
|
|
10
11
|
from bbstrader.btengine.data import DataHandler
|
|
11
|
-
from bbstrader.
|
|
12
|
+
from bbstrader.btengine.event import FillEvent, SignalEvent
|
|
13
|
+
from bbstrader.metatrader.account import Account, AdmiralMarktsGroup
|
|
12
14
|
from bbstrader.metatrader.rates import Rates
|
|
13
|
-
from typing import (
|
|
14
|
-
Dict,
|
|
15
|
-
Union,
|
|
16
|
-
Any,
|
|
17
|
-
List,
|
|
18
|
-
Literal
|
|
19
|
-
)
|
|
20
15
|
from bbstrader.models.optimization import optimized_weights
|
|
16
|
+
from bbstrader.core.utils import TradeSignal
|
|
21
17
|
|
|
22
18
|
__all__ = ['Strategy', 'MT5Strategy']
|
|
23
19
|
|
|
20
|
+
|
|
24
21
|
class Strategy(metaclass=ABCMeta):
|
|
25
22
|
"""
|
|
26
23
|
A `Strategy()` object encapsulates all calculation on market data
|
|
@@ -44,13 +41,15 @@ class Strategy(metaclass=ABCMeta):
|
|
|
44
41
|
"""
|
|
45
42
|
|
|
46
43
|
@abstractmethod
|
|
47
|
-
def calculate_signals(self, *args, **kwargs) ->
|
|
44
|
+
def calculate_signals(self, *args, **kwargs) -> List[TradeSignal]:
|
|
48
45
|
raise NotImplementedError(
|
|
49
46
|
"Should implement calculate_signals()"
|
|
50
47
|
)
|
|
48
|
+
|
|
51
49
|
def check_pending_orders(self, *args, **kwargs): ...
|
|
52
50
|
def get_update_from_portfolio(self, *args, **kwargs): ...
|
|
53
51
|
def update_trades_from_fill(self, *args, **kwargs): ...
|
|
52
|
+
def perform_period_end_checks(self, *args, **kwargs): ...
|
|
54
53
|
|
|
55
54
|
|
|
56
55
|
class MT5Strategy(Strategy):
|
|
@@ -61,8 +60,8 @@ class MT5Strategy(Strategy):
|
|
|
61
60
|
for live trading and `MT5BacktestEngine` objects for backtesting.
|
|
62
61
|
"""
|
|
63
62
|
|
|
64
|
-
def __init__(self, events: Queue=None, symbol_list: List[str]=None,
|
|
65
|
-
bars: DataHandler=None, mode: str=None, **kwargs):
|
|
63
|
+
def __init__(self, events: Queue = None, symbol_list: List[str] = None,
|
|
64
|
+
bars: DataHandler = None, mode: str = None, **kwargs):
|
|
66
65
|
"""
|
|
67
66
|
Initialize the `MT5Strategy` object.
|
|
68
67
|
|
|
@@ -82,16 +81,18 @@ class MT5Strategy(Strategy):
|
|
|
82
81
|
self.mode = mode
|
|
83
82
|
self._porfolio_value = None
|
|
84
83
|
self.risk_budget = self._check_risk_budget(**kwargs)
|
|
85
|
-
self.max_trades = kwargs.get(
|
|
84
|
+
self.max_trades = kwargs.get(
|
|
85
|
+
"max_trades", {s: 1 for s in self.symbols})
|
|
86
86
|
self.tf = kwargs.get("time_frame", 'D1')
|
|
87
87
|
self.logger = kwargs.get("logger")
|
|
88
|
-
self.
|
|
88
|
+
if self.mode == 'backtest':
|
|
89
|
+
self._initialize_portfolio()
|
|
89
90
|
self.kwargs = kwargs
|
|
90
91
|
|
|
91
92
|
@property
|
|
92
93
|
def cash(self) -> float:
|
|
93
94
|
return self._porfolio_value
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
@cash.setter
|
|
96
97
|
def cash(self, value):
|
|
97
98
|
self._porfolio_value = value
|
|
@@ -99,28 +100,30 @@ class MT5Strategy(Strategy):
|
|
|
99
100
|
@property
|
|
100
101
|
def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
|
|
101
102
|
return self._orders
|
|
102
|
-
|
|
103
|
+
|
|
103
104
|
@property
|
|
104
105
|
def trades(self) -> Dict[str, Dict[str, int]]:
|
|
105
106
|
return self._trades
|
|
106
107
|
|
|
107
108
|
@property
|
|
108
|
-
def positions(self) -> Dict[str, Dict[str, int|float]]:
|
|
109
|
+
def positions(self) -> Dict[str, Dict[str, int | float]]:
|
|
109
110
|
return self._positions
|
|
110
|
-
|
|
111
|
+
|
|
111
112
|
@property
|
|
112
113
|
def holdings(self) -> Dict[str, float]:
|
|
113
114
|
return self._holdings
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
def _check_risk_budget(self, **kwargs):
|
|
116
117
|
weights = kwargs.get('risk_weights')
|
|
117
118
|
if weights is not None and isinstance(weights, dict):
|
|
118
119
|
for asset in self.symbols:
|
|
119
120
|
if asset not in weights:
|
|
120
|
-
raise ValueError(
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Risk budget for asset {asset} is missing.")
|
|
121
123
|
total_risk = sum(weights.values())
|
|
122
124
|
if not np.isclose(total_risk, 1.0):
|
|
123
|
-
raise ValueError(
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f'Risk budget weights must sum to 1. got {total_risk}')
|
|
124
127
|
return weights
|
|
125
128
|
elif isinstance(weights, str):
|
|
126
129
|
return weights
|
|
@@ -141,7 +144,7 @@ class MT5Strategy(Strategy):
|
|
|
141
144
|
for order in orders:
|
|
142
145
|
self._orders[symbol][order] = []
|
|
143
146
|
self._holdings = {s: 0.0 for s in self.symbols}
|
|
144
|
-
|
|
147
|
+
|
|
145
148
|
def get_update_from_portfolio(self, positions, holdings):
|
|
146
149
|
"""
|
|
147
150
|
Update the positions and holdings for the strategy from the portfolio.
|
|
@@ -164,7 +167,7 @@ class MT5Strategy(Strategy):
|
|
|
164
167
|
self._positions[symbol]['SHORT'] = 0
|
|
165
168
|
if symbol in holdings:
|
|
166
169
|
self._holdings[symbol] = holdings[symbol]
|
|
167
|
-
|
|
170
|
+
|
|
168
171
|
def update_trades_from_fill(self, event: FillEvent):
|
|
169
172
|
"""
|
|
170
173
|
This method updates the trades for the strategy based on the fill event.
|
|
@@ -178,34 +181,25 @@ class MT5Strategy(Strategy):
|
|
|
178
181
|
elif event.order == 'EXIT' and event.direction == 'SELL':
|
|
179
182
|
self._trades[event.symbol]['LONG'] = 0
|
|
180
183
|
|
|
181
|
-
def calculate_signals(self, *args, **kwargs
|
|
182
|
-
) -> Dict[str, Union[str, dict, None]] | None:
|
|
184
|
+
def calculate_signals(self, *args, **kwargs) -> List[TradeSignal]:
|
|
183
185
|
"""
|
|
184
186
|
Provides the mechanisms to calculate signals for the strategy.
|
|
185
|
-
This methods should return a
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
- ``
|
|
190
|
-
- ``
|
|
191
|
-
- ``EXIT``, ``EXIT_LONG``, ``EXIT_LONG_STOP``, ``EXIT_LONG_LIMIT``, ``EXIT_LONG_STOP_LIMIT`` for an exit signal (long).
|
|
192
|
-
- ``EXIT_SHORT``, ``EXIT_SHORT_STOP``, ``EXIT_SHORT_LIMIT``, ``EXIT_SHORT_STOP_LIMIT`` for an exit signal (short).
|
|
193
|
-
- ``EXIT_ALL_ORDERS`` for cancelling all orders.
|
|
194
|
-
- ``EXIT_ALL_POSITIONS`` for exiting all positions.
|
|
195
|
-
- ``EXIT_PROFITABLES`` for exiting all profitable positions.
|
|
196
|
-
- ``EXIT_LOSINGS`` for exiting all losing positions.
|
|
197
|
-
|
|
198
|
-
The signals could also be ``EXIT_STOP``, ``EXIT_LIMIT``, ``EXIT_STOP_LIMIT`` for exiting a position.
|
|
199
|
-
|
|
200
|
-
If a dictionary is used, it should be:
|
|
201
|
-
for each symbol, a dictionary with the following keys
|
|
202
|
-
- ``action``: The action to take for the symbol (LONG, SHORT, EXIT, etc.)
|
|
203
|
-
- ``price``: The price at which to execute the action.
|
|
204
|
-
- ``stoplimit``: The stop-limit price for STOP-LIMIT orders.
|
|
187
|
+
This methods should return a list of signals for the strategy.
|
|
188
|
+
|
|
189
|
+
Each signal must be a ``TradeSignal`` object with the following attributes:
|
|
190
|
+
- ``action``: The order to execute on the symbol (LONG, SHORT, EXIT, etc.), see `bbstrader.core.utils.TradeAction`.
|
|
191
|
+
- ``price``: The price at which to execute the action, used for pending orders.
|
|
192
|
+
- ``stoplimit``: The stop-limit price for STOP-LIMIT orders, used for pending stop limit orders.
|
|
205
193
|
- ``id``: The unique identifier for the strategy or order.
|
|
194
|
+
"""
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
def perform_period_end_checks(self, *args, **kwargs):
|
|
198
|
+
"""
|
|
199
|
+
Some strategies may require additional checks at the end of the period,
|
|
200
|
+
such as closing all positions or orders or tracking the performance of the strategy etc.
|
|
206
201
|
|
|
207
|
-
|
|
208
|
-
or for executing orders where the each order has a unique identifier.
|
|
202
|
+
This method is called at the end of the period to perform such checks.
|
|
209
203
|
"""
|
|
210
204
|
pass
|
|
211
205
|
|
|
@@ -217,13 +211,14 @@ class MT5Strategy(Strategy):
|
|
|
217
211
|
return None
|
|
218
212
|
symbols = symbols or self.symbols
|
|
219
213
|
prices = self.get_asset_values(
|
|
220
|
-
symbol_list=symbols, bars=self.data, mode=self.mode,
|
|
214
|
+
symbol_list=symbols, bars=self.data, mode=self.mode,
|
|
221
215
|
window=freq, value_type='close', array=False, tf=self.tf
|
|
222
216
|
)
|
|
223
217
|
prices = pd.DataFrame(prices)
|
|
224
218
|
prices = prices.dropna(axis=0, how='any')
|
|
225
219
|
try:
|
|
226
|
-
weights = optimized_weights(
|
|
220
|
+
weights = optimized_weights(
|
|
221
|
+
prices=prices, freq=freq, method=optimer)
|
|
227
222
|
return {symbol: weight for symbol, weight in weights.items()}
|
|
228
223
|
except Exception:
|
|
229
224
|
return {symbol: 0.0 for symbol in symbols}
|
|
@@ -241,17 +236,17 @@ class MT5Strategy(Strategy):
|
|
|
241
236
|
qty : The quantity to buy or sell for the symbol.
|
|
242
237
|
"""
|
|
243
238
|
if (self._porfolio_value is None or weight == 0 or
|
|
244
|
-
|
|
239
|
+
self._porfolio_value == 0 or np.isnan(self._porfolio_value)):
|
|
245
240
|
return 0
|
|
246
241
|
if volume is None:
|
|
247
242
|
volume = round(self._porfolio_value * weight)
|
|
248
243
|
if price is None:
|
|
249
244
|
price = self.data.get_latest_bar_value(symbol, 'close')
|
|
250
245
|
if (price is None or not isinstance(price, (int, float, np.number))
|
|
251
|
-
or volume is None or not isinstance(volume, (int, float, np.number))
|
|
246
|
+
or volume is None or not isinstance(volume, (int, float, np.number))
|
|
252
247
|
or np.isnan(float(price))
|
|
253
248
|
or np.isnan(float(volume))
|
|
254
|
-
|
|
249
|
+
):
|
|
255
250
|
if weight != 0:
|
|
256
251
|
return 1
|
|
257
252
|
return 0
|
|
@@ -260,7 +255,7 @@ class MT5Strategy(Strategy):
|
|
|
260
255
|
if maxqty is not None:
|
|
261
256
|
qty = min(qty, maxqty)
|
|
262
257
|
return max(round(qty, 2), 0)
|
|
263
|
-
|
|
258
|
+
|
|
264
259
|
def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
|
|
265
260
|
"""
|
|
266
261
|
Get the quantities to buy or sell for the symbols in the strategy.
|
|
@@ -275,7 +270,7 @@ class MT5Strategy(Strategy):
|
|
|
275
270
|
return quantities
|
|
276
271
|
elif isinstance(quantities, int):
|
|
277
272
|
return {symbol: quantities for symbol in self.symbols}
|
|
278
|
-
|
|
273
|
+
|
|
279
274
|
def _send_order(self, id, symbol: str, signal: str, strength: float, price: float,
|
|
280
275
|
quantity: int, dtime: datetime | pd.Timestamp):
|
|
281
276
|
|
|
@@ -294,8 +289,8 @@ class MT5Strategy(Strategy):
|
|
|
294
289
|
self.logger.info(
|
|
295
290
|
f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
|
|
296
291
|
|
|
297
|
-
def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
|
|
298
|
-
|
|
292
|
+
def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
|
|
293
|
+
strength: float = 1.0, dtime: datetime | pd.Timestamp = None):
|
|
299
294
|
"""
|
|
300
295
|
Open a long position
|
|
301
296
|
|
|
@@ -309,7 +304,8 @@ class MT5Strategy(Strategy):
|
|
|
309
304
|
|
|
310
305
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
311
306
|
"""
|
|
312
|
-
self._send_order(id, symbol, 'SHORT', strength,
|
|
307
|
+
self._send_order(id, symbol, 'SHORT', strength,
|
|
308
|
+
price, quantity, dtime)
|
|
313
309
|
|
|
314
310
|
def close_positions(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
315
311
|
"""
|
|
@@ -375,8 +371,8 @@ class MT5Strategy(Strategy):
|
|
|
375
371
|
quantity=quantity, strength=strength, price=price)
|
|
376
372
|
self._orders[symbol]['SLMT'].append(order)
|
|
377
373
|
|
|
378
|
-
def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
|
|
379
|
-
quantity: int, strength: float=1.0, dtime: datetime | pd.Timestamp = None):
|
|
374
|
+
def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
|
|
375
|
+
quantity: int, strength: float = 1.0, dtime: datetime | pd.Timestamp = None):
|
|
380
376
|
"""
|
|
381
377
|
Open a pending order to buy at a stop-limit price
|
|
382
378
|
|
|
@@ -416,52 +412,57 @@ class MT5Strategy(Strategy):
|
|
|
416
412
|
"""
|
|
417
413
|
for symbol in self.symbols:
|
|
418
414
|
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
419
|
-
|
|
415
|
+
|
|
416
|
+
def logmsg(order, type): return self.logger.info(
|
|
420
417
|
f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
421
418
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
422
419
|
for order in self._orders[symbol]['BLMT'].copy():
|
|
423
420
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
424
421
|
self.buy_mkt(order.strategy_id, symbol,
|
|
425
|
-
|
|
422
|
+
order.price, order.quantity, dtime)
|
|
426
423
|
try:
|
|
427
424
|
self._orders[symbol]['BLMT'].remove(order)
|
|
428
425
|
assert order not in self._orders[symbol]['BLMT']
|
|
429
426
|
logmsg(order, 'BUY LIMIT')
|
|
430
427
|
except AssertionError:
|
|
431
|
-
self._orders[symbol]['BLMT'] = [
|
|
428
|
+
self._orders[symbol]['BLMT'] = [
|
|
429
|
+
o for o in self._orders[symbol]['BLMT'] if o != order]
|
|
432
430
|
logmsg(order, 'BUY LIMIT')
|
|
433
431
|
for order in self._orders[symbol]['SLMT'].copy():
|
|
434
432
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
435
433
|
self.sell_mkt(order.strategy_id, symbol,
|
|
436
|
-
|
|
434
|
+
order.price, order.quantity, dtime)
|
|
437
435
|
try:
|
|
438
436
|
self._orders[symbol]['SLMT'].remove(order)
|
|
439
437
|
assert order not in self._orders[symbol]['SLMT']
|
|
440
438
|
logmsg(order, 'SELL LIMIT')
|
|
441
439
|
except AssertionError:
|
|
442
|
-
self._orders[symbol]['SLMT'] = [
|
|
440
|
+
self._orders[symbol]['SLMT'] = [
|
|
441
|
+
o for o in self._orders[symbol]['SLMT'] if o != order]
|
|
443
442
|
logmsg(order, 'SELL LIMIT')
|
|
444
443
|
for order in self._orders[symbol]['BSTP'].copy():
|
|
445
444
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
446
445
|
self.buy_mkt(order.strategy_id, symbol,
|
|
447
|
-
|
|
446
|
+
order.price, order.quantity, dtime)
|
|
448
447
|
try:
|
|
449
448
|
self._orders[symbol]['BSTP'].remove(order)
|
|
450
449
|
assert order not in self._orders[symbol]['BSTP']
|
|
451
450
|
logmsg(order, 'BUY STOP')
|
|
452
451
|
except AssertionError:
|
|
453
|
-
self._orders[symbol]['BSTP'] = [
|
|
452
|
+
self._orders[symbol]['BSTP'] = [
|
|
453
|
+
o for o in self._orders[symbol]['BSTP'] if o != order]
|
|
454
454
|
logmsg(order, 'BUY STOP')
|
|
455
455
|
for order in self._orders[symbol]['SSTP'].copy():
|
|
456
456
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
457
457
|
self.sell_mkt(order.strategy_id, symbol,
|
|
458
|
-
|
|
458
|
+
order.price, order.quantity, dtime)
|
|
459
459
|
try:
|
|
460
460
|
self._orders[symbol]['SSTP'].remove(order)
|
|
461
461
|
assert order not in self._orders[symbol]['SSTP']
|
|
462
462
|
logmsg(order, 'SELL STOP')
|
|
463
463
|
except AssertionError:
|
|
464
|
-
self._orders[symbol]['SSTP'] = [
|
|
464
|
+
self._orders[symbol]['SSTP'] = [
|
|
465
|
+
o for o in self._orders[symbol]['SSTP'] if o != order]
|
|
465
466
|
logmsg(order, 'SELL STOP')
|
|
466
467
|
for order in self._orders[symbol]['BSTPLMT'].copy():
|
|
467
468
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
@@ -472,7 +473,8 @@ class MT5Strategy(Strategy):
|
|
|
472
473
|
assert order not in self._orders[symbol]['BSTPLMT']
|
|
473
474
|
logmsg(order, 'BUY STOP LIMIT')
|
|
474
475
|
except AssertionError:
|
|
475
|
-
self._orders[symbol]['BSTPLMT'] = [
|
|
476
|
+
self._orders[symbol]['BSTPLMT'] = [
|
|
477
|
+
o for o in self._orders[symbol]['BSTPLMT'] if o != order]
|
|
476
478
|
logmsg(order, 'BUY STOP LIMIT')
|
|
477
479
|
for order in self._orders[symbol]['SSTPLMT'].copy():
|
|
478
480
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
@@ -483,21 +485,23 @@ class MT5Strategy(Strategy):
|
|
|
483
485
|
assert order not in self._orders[symbol]['SSTPLMT']
|
|
484
486
|
logmsg(order, 'SELL STOP LIMIT')
|
|
485
487
|
except AssertionError:
|
|
486
|
-
self._orders[symbol]['SSTPLMT'] = [
|
|
488
|
+
self._orders[symbol]['SSTPLMT'] = [
|
|
489
|
+
o for o in self._orders[symbol]['SSTPLMT'] if o != order]
|
|
487
490
|
logmsg(order, 'SELL STOP LIMIT')
|
|
488
491
|
|
|
489
|
-
|
|
492
|
+
@staticmethod
|
|
493
|
+
def calculate_pct_change(current_price, lh_price):
|
|
490
494
|
return ((current_price - lh_price) / lh_price) * 100
|
|
491
|
-
|
|
495
|
+
|
|
492
496
|
def get_asset_values(self,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
497
|
+
symbol_list: List[str],
|
|
498
|
+
window: int,
|
|
499
|
+
value_type: str = 'returns',
|
|
500
|
+
array: bool = True,
|
|
501
|
+
bars: DataHandler = None,
|
|
502
|
+
mode: Literal['backtest', 'live'] = 'backtest',
|
|
503
|
+
tf: str = 'D1'
|
|
504
|
+
) -> Dict[str, np.ndarray | pd.Series] | None:
|
|
501
505
|
"""
|
|
502
506
|
Get the historical OHLCV value or returns or custum value
|
|
503
507
|
based on the DataHandker of the assets in the symbol list.
|
|
@@ -513,7 +517,7 @@ class MT5Strategy(Strategy):
|
|
|
513
517
|
|
|
514
518
|
Returns:
|
|
515
519
|
asset_values : Historical values of the assets in the symbol list.
|
|
516
|
-
|
|
520
|
+
|
|
517
521
|
Note:
|
|
518
522
|
In Live mode, the `bbstrader.metatrader.rates.Rates` class is used to get the historical data
|
|
519
523
|
so the value_type must be 'returns', 'open', 'high', 'low', 'close', 'adjclose', 'volume'.
|
|
@@ -531,10 +535,11 @@ class MT5Strategy(Strategy):
|
|
|
531
535
|
asset_values[asset] = values[~np.isnan(values)]
|
|
532
536
|
else:
|
|
533
537
|
values = bars.get_latest_bars(asset, N=window)
|
|
534
|
-
asset_values[asset] = getattr(values, value_type)
|
|
538
|
+
asset_values[asset] = getattr(values, value_type)
|
|
535
539
|
elif mode == 'live':
|
|
536
540
|
for asset in symbol_list:
|
|
537
|
-
rates = Rates(asset, timeframe=tf,
|
|
541
|
+
rates = Rates(asset, timeframe=tf,
|
|
542
|
+
count=window + 1, **self.kwargs)
|
|
538
543
|
if array:
|
|
539
544
|
values = getattr(rates, value_type).values
|
|
540
545
|
asset_values[asset] = values[~np.isnan(values)]
|
|
@@ -546,7 +551,8 @@ class MT5Strategy(Strategy):
|
|
|
546
551
|
else:
|
|
547
552
|
return None
|
|
548
553
|
|
|
549
|
-
|
|
554
|
+
@staticmethod
|
|
555
|
+
def is_signal_time(period_count, signal_inverval) -> bool:
|
|
550
556
|
"""
|
|
551
557
|
Check if we can generate a signal based on the current period count.
|
|
552
558
|
We use the signal interval as a form of periodicity or rebalancing period.
|
|
@@ -614,10 +620,12 @@ class MT5Strategy(Strategy):
|
|
|
614
620
|
return prices
|
|
615
621
|
return np.array([])
|
|
616
622
|
|
|
617
|
-
|
|
623
|
+
@staticmethod
|
|
624
|
+
def get_current_dt(time_zone: str = 'US/Eastern') -> datetime:
|
|
618
625
|
return datetime.now(pytz.timezone(time_zone))
|
|
619
626
|
|
|
620
|
-
|
|
627
|
+
@staticmethod
|
|
628
|
+
def convert_time_zone(dt: datetime | int | pd.Timestamp,
|
|
621
629
|
from_tz: str = 'UTC',
|
|
622
630
|
to_tz: str = 'US/Eastern'
|
|
623
631
|
) -> pd.Timestamp:
|
|
@@ -645,6 +653,19 @@ class MT5Strategy(Strategy):
|
|
|
645
653
|
dt_to = dt.tz_convert(pytz.timezone(to_tz))
|
|
646
654
|
return dt_to
|
|
647
655
|
|
|
656
|
+
@staticmethod
|
|
657
|
+
def get_mt5_equivalent(symbols, type='STK', path: str = None) -> List[str]:
|
|
658
|
+
account = Account(path=path)
|
|
659
|
+
mt5_symbols = account.get_symbols(symbol_type=type)
|
|
660
|
+
mt5_equivalent = []
|
|
661
|
+
if account.broker == AdmiralMarktsGroup():
|
|
662
|
+
for s in mt5_symbols:
|
|
663
|
+
_s = s[1:] if s[0] in string.punctuation else s
|
|
664
|
+
for symbol in symbols:
|
|
665
|
+
if _s.split('.')[0] == symbol or _s.split('_')[0] == symbol:
|
|
666
|
+
mt5_equivalent.append(s)
|
|
667
|
+
return mt5_equivalent
|
|
668
|
+
|
|
648
669
|
|
|
649
670
|
class TWSStrategy(Strategy):
|
|
650
671
|
...
|
bbstrader/config.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import List
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from
|
|
5
|
-
|
|
3
|
+
from typing import List
|
|
6
4
|
|
|
7
5
|
ADMIRAL_PATH = "C:\\Program Files\\Admirals Group MT5 Terminal\\terminal64.exe"
|
|
8
|
-
FTMO_PATH
|
|
6
|
+
FTMO_PATH = "C:\\Program Files\\FTMO MetaTrader 5\\terminal64.exe"
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
|
|
9
|
+
def get_config_dir(name: str = ".bbstrader") -> Path:
|
|
11
10
|
"""
|
|
12
11
|
Get the path to the configuration directory.
|
|
13
12
|
|
|
@@ -22,6 +21,7 @@ def get_config_dir(name: str=".bbstrader") -> Path:
|
|
|
22
21
|
home_dir.mkdir()
|
|
23
22
|
return home_dir
|
|
24
23
|
|
|
24
|
+
|
|
25
25
|
BBSTRADER_DIR = get_config_dir()
|
|
26
26
|
|
|
27
27
|
|
|
@@ -47,7 +47,7 @@ class LogLevelFilter(logging.Filter):
|
|
|
47
47
|
True if the record's level is in the allowed levels, False otherwise.
|
|
48
48
|
"""
|
|
49
49
|
return record.levelno in self.levels
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
|
|
52
52
|
class CustomFormatter(logging.Formatter):
|
|
53
53
|
def formatTime(self, record, datefmt=None):
|
|
@@ -61,7 +61,7 @@ class CustomLogger(logging.Logger):
|
|
|
61
61
|
def __init__(self, name, level=logging.NOTSET):
|
|
62
62
|
super().__init__(name, level)
|
|
63
63
|
|
|
64
|
-
def _log(self, level, msg, args, exc_info=None,
|
|
64
|
+
def _log(self, level, msg, args, exc_info=None,
|
|
65
65
|
extra=None, stack_info=False, stacklevel=1, custom_time=None):
|
|
66
66
|
if extra is None:
|
|
67
67
|
extra = {}
|
|
@@ -77,13 +77,15 @@ class CustomLogger(logging.Logger):
|
|
|
77
77
|
self._log(logging.DEBUG, msg, args, custom_time=custom_time, **kwargs)
|
|
78
78
|
|
|
79
79
|
def warning(self, msg, *args, custom_time=None, **kwargs):
|
|
80
|
-
self._log(logging.WARNING, msg, args,
|
|
80
|
+
self._log(logging.WARNING, msg, args,
|
|
81
|
+
custom_time=custom_time, **kwargs)
|
|
81
82
|
|
|
82
83
|
def error(self, msg, *args, custom_time=None, **kwargs):
|
|
83
84
|
self._log(logging.ERROR, msg, args, custom_time=custom_time, **kwargs)
|
|
84
85
|
|
|
85
86
|
def critical(self, msg, *args, custom_time=None, **kwargs):
|
|
86
|
-
self._log(logging.CRITICAL, msg, args,
|
|
87
|
+
self._log(logging.CRITICAL, msg, args,
|
|
88
|
+
custom_time=custom_time, **kwargs)
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
def config_logger(log_file: str, console_log=True):
|
|
@@ -112,4 +114,4 @@ def config_logger(log_file: str, console_log=True):
|
|
|
112
114
|
console_handler.setFormatter(formatter)
|
|
113
115
|
logger.addHandler(console_handler)
|
|
114
116
|
|
|
115
|
-
return logger
|
|
117
|
+
return logger
|
bbstrader/core/data.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import pandas as pd
|
|
2
|
-
import numpy as np
|
|
3
1
|
from financetoolkit import Toolkit
|
|
4
2
|
|
|
5
|
-
|
|
6
3
|
__all__ = [
|
|
7
4
|
'FMP',
|
|
8
5
|
]
|
|
9
6
|
|
|
7
|
+
|
|
10
8
|
class FMP(Toolkit):
|
|
11
9
|
"""
|
|
12
10
|
FMPData class for fetching data from Financial Modeling Prep API
|
|
@@ -15,9 +13,10 @@ class FMP(Toolkit):
|
|
|
15
13
|
See `financetoolkit` for more details.
|
|
16
14
|
|
|
17
15
|
"""
|
|
18
|
-
|
|
16
|
+
|
|
17
|
+
def __init__(self, api_key: str = '', symbols: str | list = 'AAPL'):
|
|
19
18
|
super().__init__(tickers=symbols, api_key=api_key)
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class DataBendo:
|
|
23
|
-
...
|
|
22
|
+
...
|
bbstrader/core/utils.py
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TradeAction(Enum):
|
|
6
|
+
"""
|
|
7
|
+
An enumeration class for trade actions.
|
|
8
|
+
"""
|
|
9
|
+
BUY = "LONG"
|
|
10
|
+
LONG = "LONG"
|
|
11
|
+
SELL = "SHORT"
|
|
12
|
+
EXIT = "EXIT"
|
|
13
|
+
BMKT = "BMKT"
|
|
14
|
+
SMKT = "SMKT"
|
|
15
|
+
BLMT = "BLMT"
|
|
16
|
+
SLMT = "SLMT"
|
|
17
|
+
BSTP = "BSTP"
|
|
18
|
+
SSTP = "SSTP"
|
|
19
|
+
SHORT = "SHORT"
|
|
20
|
+
BSTPLMT = "BSTPLMT"
|
|
21
|
+
SSTPLMT = "SSTPLMT"
|
|
22
|
+
EXIT_LONG = "EXIT_LONG"
|
|
23
|
+
EXIT_SHORT = "EXIT_SHORT"
|
|
24
|
+
EXIT_STOP = "EXIT_STOP"
|
|
25
|
+
EXIT_LIMIT = "EXIT_LIMIT"
|
|
26
|
+
EXIT_LONG_STOP = "EXIT_LONG_STOP"
|
|
27
|
+
EXIT_LONG_LIMIT = "EXIT_LONG_LIMIT"
|
|
28
|
+
EXIT_SHORT_STOP = "EXIT_SHORT_STOP"
|
|
29
|
+
EXIT_SHORT_LIMIT = "EXIT_SHORT_LIMIT"
|
|
30
|
+
EXIT_LONG_STOP_LIMIT = "EXIT_LONG_STOP_LIMIT"
|
|
31
|
+
EXIT_SHORT_STOP_LIMIT = "EXIT_SHORT_STOP_LIMIT"
|
|
32
|
+
EXIT_PROFITABLES = "EXIT_PROFITABLES"
|
|
33
|
+
EXIT_LOSINGS = "EXIT_LOSINGS"
|
|
34
|
+
EXIT_ALL_POSITIONS = "EXIT_ALL_POSITIONS"
|
|
35
|
+
EXIT_ALL_LONGS = "EXIT_ALL_LONGS"
|
|
36
|
+
|
|
37
|
+
def __str__(self):
|
|
38
|
+
return self.value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass()
|
|
42
|
+
class TradeSignal:
|
|
43
|
+
"""
|
|
44
|
+
A dataclass for storing trading signal.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
id: int
|
|
48
|
+
symbol: str
|
|
49
|
+
action: TradeAction
|
|
50
|
+
price: float = None
|
|
51
|
+
stoplimit: float = None
|
|
52
|
+
|
|
53
|
+
def __repr__(self):
|
|
54
|
+
return (
|
|
55
|
+
f"TradeSignal(id={self.id}, symbol='{self.symbol}', "
|
|
56
|
+
f"action='{self.action.value}', price={self.price}, stoplimit={self.stoplimit})"
|
|
57
|
+
)
|
bbstrader/ibkr/utils.py
ADDED
|
File without changes
|
bbstrader/metatrader/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
from bbstrader.metatrader.account import *
|
|
3
|
-
from bbstrader.metatrader.rates import *
|
|
4
|
-
from bbstrader.metatrader.risk import *
|
|
5
|
-
from bbstrader.metatrader.trade import *
|
|
6
|
-
from bbstrader.metatrader.utils import *
|
|
2
|
+
from bbstrader.metatrader.account import * # noqa: F403
|
|
3
|
+
from bbstrader.metatrader.rates import * # noqa: F403
|
|
4
|
+
from bbstrader.metatrader.risk import * # noqa: F403
|
|
5
|
+
from bbstrader.metatrader.trade import * # noqa: F403
|
|
6
|
+
from bbstrader.metatrader.utils import * # noqa: F403
|