bbstrader 0.1.94__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 +9 -9
- bbstrader/btengine/__init__.py +7 -7
- bbstrader/btengine/backtest.py +30 -26
- bbstrader/btengine/data.py +100 -79
- 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 +119 -94
- bbstrader/config.py +14 -8
- bbstrader/core/__init__.py +0 -0
- bbstrader/core/data.py +22 -0
- bbstrader/core/utils.py +57 -0
- bbstrader/ibkr/__init__.py +0 -0
- bbstrader/ibkr/utils.py +0 -0
- bbstrader/metatrader/__init__.py +5 -5
- bbstrader/metatrader/account.py +117 -121
- bbstrader/metatrader/rates.py +83 -80
- bbstrader/metatrader/risk.py +23 -37
- bbstrader/metatrader/trade.py +169 -140
- bbstrader/metatrader/utils.py +3 -3
- bbstrader/models/__init__.py +5 -5
- bbstrader/models/factors.py +280 -0
- bbstrader/models/ml.py +1092 -0
- bbstrader/models/optimization.py +31 -28
- bbstrader/models/{portfolios.py → portfolio.py} +64 -46
- bbstrader/models/risk.py +15 -9
- bbstrader/trading/__init__.py +2 -2
- bbstrader/trading/execution.py +252 -164
- bbstrader/trading/scripts.py +8 -4
- bbstrader/trading/strategies.py +79 -66
- bbstrader/tseries.py +482 -107
- {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
- {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/METADATA +6 -1
- bbstrader-0.2.1.dist-info/RECORD +37 -0
- bbstrader-0.1.94.dist-info/RECORD +0 -32
- {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
- {bbstrader-0.1.94.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,15 +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()
|
|
90
|
+
self.kwargs = kwargs
|
|
89
91
|
|
|
90
92
|
@property
|
|
91
93
|
def cash(self) -> float:
|
|
92
94
|
return self._porfolio_value
|
|
93
|
-
|
|
95
|
+
|
|
94
96
|
@cash.setter
|
|
95
97
|
def cash(self, value):
|
|
96
98
|
self._porfolio_value = value
|
|
@@ -98,28 +100,30 @@ class MT5Strategy(Strategy):
|
|
|
98
100
|
@property
|
|
99
101
|
def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
|
|
100
102
|
return self._orders
|
|
101
|
-
|
|
103
|
+
|
|
102
104
|
@property
|
|
103
105
|
def trades(self) -> Dict[str, Dict[str, int]]:
|
|
104
106
|
return self._trades
|
|
105
107
|
|
|
106
108
|
@property
|
|
107
|
-
def positions(self) -> Dict[str, Dict[str, int|float]]:
|
|
109
|
+
def positions(self) -> Dict[str, Dict[str, int | float]]:
|
|
108
110
|
return self._positions
|
|
109
|
-
|
|
111
|
+
|
|
110
112
|
@property
|
|
111
113
|
def holdings(self) -> Dict[str, float]:
|
|
112
114
|
return self._holdings
|
|
113
|
-
|
|
115
|
+
|
|
114
116
|
def _check_risk_budget(self, **kwargs):
|
|
115
117
|
weights = kwargs.get('risk_weights')
|
|
116
118
|
if weights is not None and isinstance(weights, dict):
|
|
117
119
|
for asset in self.symbols:
|
|
118
120
|
if asset not in weights:
|
|
119
|
-
raise ValueError(
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Risk budget for asset {asset} is missing.")
|
|
120
123
|
total_risk = sum(weights.values())
|
|
121
124
|
if not np.isclose(total_risk, 1.0):
|
|
122
|
-
raise ValueError(
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f'Risk budget weights must sum to 1. got {total_risk}')
|
|
123
127
|
return weights
|
|
124
128
|
elif isinstance(weights, str):
|
|
125
129
|
return weights
|
|
@@ -140,7 +144,7 @@ class MT5Strategy(Strategy):
|
|
|
140
144
|
for order in orders:
|
|
141
145
|
self._orders[symbol][order] = []
|
|
142
146
|
self._holdings = {s: 0.0 for s in self.symbols}
|
|
143
|
-
|
|
147
|
+
|
|
144
148
|
def get_update_from_portfolio(self, positions, holdings):
|
|
145
149
|
"""
|
|
146
150
|
Update the positions and holdings for the strategy from the portfolio.
|
|
@@ -163,7 +167,7 @@ class MT5Strategy(Strategy):
|
|
|
163
167
|
self._positions[symbol]['SHORT'] = 0
|
|
164
168
|
if symbol in holdings:
|
|
165
169
|
self._holdings[symbol] = holdings[symbol]
|
|
166
|
-
|
|
170
|
+
|
|
167
171
|
def update_trades_from_fill(self, event: FillEvent):
|
|
168
172
|
"""
|
|
169
173
|
This method updates the trades for the strategy based on the fill event.
|
|
@@ -177,52 +181,47 @@ class MT5Strategy(Strategy):
|
|
|
177
181
|
elif event.order == 'EXIT' and event.direction == 'SELL':
|
|
178
182
|
self._trades[event.symbol]['LONG'] = 0
|
|
179
183
|
|
|
180
|
-
def calculate_signals(self, *args, **kwargs
|
|
181
|
-
) -> Dict[str, Union[str, dict, None]] | None:
|
|
184
|
+
def calculate_signals(self, *args, **kwargs) -> List[TradeSignal]:
|
|
182
185
|
"""
|
|
183
186
|
Provides the mechanisms to calculate signals for the strategy.
|
|
184
|
-
This methods should return a
|
|
185
|
-
The returned signals should be either string or dictionary objects.
|
|
187
|
+
This methods should return a list of signals for the strategy.
|
|
186
188
|
|
|
187
|
-
|
|
188
|
-
- ``
|
|
189
|
-
- ``
|
|
190
|
-
- ``
|
|
191
|
-
- ``
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
- ``EXIT_PROFITABLES`` for exiting all profitable positions.
|
|
195
|
-
- ``EXIT_LOSINGS`` for exiting all losing positions.
|
|
196
|
-
|
|
197
|
-
The signals could also be ``EXIT_STOP``, ``EXIT_LIMIT``, ``EXIT_STOP_LIMIT`` for exiting a position.
|
|
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.
|
|
193
|
+
- ``id``: The unique identifier for the strategy or order.
|
|
194
|
+
"""
|
|
195
|
+
pass
|
|
198
196
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
- ``stoplimit``: The stop-limit price for STOP-LIMIT orders.
|
|
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.
|
|
204
201
|
|
|
205
|
-
|
|
202
|
+
This method is called at the end of the period to perform such checks.
|
|
206
203
|
"""
|
|
207
204
|
pass
|
|
208
205
|
|
|
209
|
-
def apply_risk_management(self, optimer, freq=252) -> Dict[str, float] | None:
|
|
206
|
+
def apply_risk_management(self, optimer, symbols=None, freq=252) -> Dict[str, float] | None:
|
|
210
207
|
"""
|
|
211
208
|
Apply risk management rules to the strategy.
|
|
212
209
|
"""
|
|
213
210
|
if optimer is None:
|
|
214
211
|
return None
|
|
212
|
+
symbols = symbols or self.symbols
|
|
215
213
|
prices = self.get_asset_values(
|
|
216
|
-
symbol_list=
|
|
214
|
+
symbol_list=symbols, bars=self.data, mode=self.mode,
|
|
217
215
|
window=freq, value_type='close', array=False, tf=self.tf
|
|
218
216
|
)
|
|
219
217
|
prices = pd.DataFrame(prices)
|
|
220
218
|
prices = prices.dropna(axis=0, how='any')
|
|
221
219
|
try:
|
|
222
|
-
weights = optimized_weights(
|
|
220
|
+
weights = optimized_weights(
|
|
221
|
+
prices=prices, freq=freq, method=optimer)
|
|
223
222
|
return {symbol: weight for symbol, weight in weights.items()}
|
|
224
223
|
except Exception:
|
|
225
|
-
return {symbol: 0.0 for symbol in
|
|
224
|
+
return {symbol: 0.0 for symbol in symbols}
|
|
226
225
|
|
|
227
226
|
def get_quantity(self, symbol, weight, price=None, volume=None, maxqty=None) -> int:
|
|
228
227
|
"""
|
|
@@ -237,17 +236,17 @@ class MT5Strategy(Strategy):
|
|
|
237
236
|
qty : The quantity to buy or sell for the symbol.
|
|
238
237
|
"""
|
|
239
238
|
if (self._porfolio_value is None or weight == 0 or
|
|
240
|
-
|
|
239
|
+
self._porfolio_value == 0 or np.isnan(self._porfolio_value)):
|
|
241
240
|
return 0
|
|
242
241
|
if volume is None:
|
|
243
242
|
volume = round(self._porfolio_value * weight)
|
|
244
243
|
if price is None:
|
|
245
244
|
price = self.data.get_latest_bar_value(symbol, 'close')
|
|
246
245
|
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))
|
|
246
|
+
or volume is None or not isinstance(volume, (int, float, np.number))
|
|
248
247
|
or np.isnan(float(price))
|
|
249
248
|
or np.isnan(float(volume))
|
|
250
|
-
|
|
249
|
+
):
|
|
251
250
|
if weight != 0:
|
|
252
251
|
return 1
|
|
253
252
|
return 0
|
|
@@ -256,7 +255,7 @@ class MT5Strategy(Strategy):
|
|
|
256
255
|
if maxqty is not None:
|
|
257
256
|
qty = min(qty, maxqty)
|
|
258
257
|
return max(round(qty, 2), 0)
|
|
259
|
-
|
|
258
|
+
|
|
260
259
|
def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
|
|
261
260
|
"""
|
|
262
261
|
Get the quantities to buy or sell for the symbols in the strategy.
|
|
@@ -271,7 +270,7 @@ class MT5Strategy(Strategy):
|
|
|
271
270
|
return quantities
|
|
272
271
|
elif isinstance(quantities, int):
|
|
273
272
|
return {symbol: quantities for symbol in self.symbols}
|
|
274
|
-
|
|
273
|
+
|
|
275
274
|
def _send_order(self, id, symbol: str, signal: str, strength: float, price: float,
|
|
276
275
|
quantity: int, dtime: datetime | pd.Timestamp):
|
|
277
276
|
|
|
@@ -290,8 +289,8 @@ class MT5Strategy(Strategy):
|
|
|
290
289
|
self.logger.info(
|
|
291
290
|
f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
|
|
292
291
|
|
|
293
|
-
def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
|
|
294
|
-
|
|
292
|
+
def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
|
|
293
|
+
strength: float = 1.0, dtime: datetime | pd.Timestamp = None):
|
|
295
294
|
"""
|
|
296
295
|
Open a long position
|
|
297
296
|
|
|
@@ -305,7 +304,8 @@ class MT5Strategy(Strategy):
|
|
|
305
304
|
|
|
306
305
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
307
306
|
"""
|
|
308
|
-
self._send_order(id, symbol, 'SHORT', strength,
|
|
307
|
+
self._send_order(id, symbol, 'SHORT', strength,
|
|
308
|
+
price, quantity, dtime)
|
|
309
309
|
|
|
310
310
|
def close_positions(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
311
311
|
"""
|
|
@@ -371,8 +371,8 @@ class MT5Strategy(Strategy):
|
|
|
371
371
|
quantity=quantity, strength=strength, price=price)
|
|
372
372
|
self._orders[symbol]['SLMT'].append(order)
|
|
373
373
|
|
|
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):
|
|
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):
|
|
376
376
|
"""
|
|
377
377
|
Open a pending order to buy at a stop-limit price
|
|
378
378
|
|
|
@@ -412,52 +412,57 @@ class MT5Strategy(Strategy):
|
|
|
412
412
|
"""
|
|
413
413
|
for symbol in self.symbols:
|
|
414
414
|
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
415
|
-
|
|
415
|
+
|
|
416
|
+
def logmsg(order, type): return self.logger.info(
|
|
416
417
|
f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
417
418
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
418
419
|
for order in self._orders[symbol]['BLMT'].copy():
|
|
419
420
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
420
421
|
self.buy_mkt(order.strategy_id, symbol,
|
|
421
|
-
|
|
422
|
+
order.price, order.quantity, dtime)
|
|
422
423
|
try:
|
|
423
424
|
self._orders[symbol]['BLMT'].remove(order)
|
|
424
425
|
assert order not in self._orders[symbol]['BLMT']
|
|
425
426
|
logmsg(order, 'BUY LIMIT')
|
|
426
427
|
except AssertionError:
|
|
427
|
-
self._orders[symbol]['BLMT'] = [
|
|
428
|
+
self._orders[symbol]['BLMT'] = [
|
|
429
|
+
o for o in self._orders[symbol]['BLMT'] if o != order]
|
|
428
430
|
logmsg(order, 'BUY LIMIT')
|
|
429
431
|
for order in self._orders[symbol]['SLMT'].copy():
|
|
430
432
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
431
433
|
self.sell_mkt(order.strategy_id, symbol,
|
|
432
|
-
|
|
434
|
+
order.price, order.quantity, dtime)
|
|
433
435
|
try:
|
|
434
436
|
self._orders[symbol]['SLMT'].remove(order)
|
|
435
437
|
assert order not in self._orders[symbol]['SLMT']
|
|
436
438
|
logmsg(order, 'SELL LIMIT')
|
|
437
439
|
except AssertionError:
|
|
438
|
-
self._orders[symbol]['SLMT'] = [
|
|
440
|
+
self._orders[symbol]['SLMT'] = [
|
|
441
|
+
o for o in self._orders[symbol]['SLMT'] if o != order]
|
|
439
442
|
logmsg(order, 'SELL LIMIT')
|
|
440
443
|
for order in self._orders[symbol]['BSTP'].copy():
|
|
441
444
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
442
445
|
self.buy_mkt(order.strategy_id, symbol,
|
|
443
|
-
|
|
446
|
+
order.price, order.quantity, dtime)
|
|
444
447
|
try:
|
|
445
448
|
self._orders[symbol]['BSTP'].remove(order)
|
|
446
449
|
assert order not in self._orders[symbol]['BSTP']
|
|
447
450
|
logmsg(order, 'BUY STOP')
|
|
448
451
|
except AssertionError:
|
|
449
|
-
self._orders[symbol]['BSTP'] = [
|
|
452
|
+
self._orders[symbol]['BSTP'] = [
|
|
453
|
+
o for o in self._orders[symbol]['BSTP'] if o != order]
|
|
450
454
|
logmsg(order, 'BUY STOP')
|
|
451
455
|
for order in self._orders[symbol]['SSTP'].copy():
|
|
452
456
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
453
457
|
self.sell_mkt(order.strategy_id, symbol,
|
|
454
|
-
|
|
458
|
+
order.price, order.quantity, dtime)
|
|
455
459
|
try:
|
|
456
460
|
self._orders[symbol]['SSTP'].remove(order)
|
|
457
461
|
assert order not in self._orders[symbol]['SSTP']
|
|
458
462
|
logmsg(order, 'SELL STOP')
|
|
459
463
|
except AssertionError:
|
|
460
|
-
self._orders[symbol]['SSTP'] = [
|
|
464
|
+
self._orders[symbol]['SSTP'] = [
|
|
465
|
+
o for o in self._orders[symbol]['SSTP'] if o != order]
|
|
461
466
|
logmsg(order, 'SELL STOP')
|
|
462
467
|
for order in self._orders[symbol]['BSTPLMT'].copy():
|
|
463
468
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
@@ -468,7 +473,8 @@ class MT5Strategy(Strategy):
|
|
|
468
473
|
assert order not in self._orders[symbol]['BSTPLMT']
|
|
469
474
|
logmsg(order, 'BUY STOP LIMIT')
|
|
470
475
|
except AssertionError:
|
|
471
|
-
self._orders[symbol]['BSTPLMT'] = [
|
|
476
|
+
self._orders[symbol]['BSTPLMT'] = [
|
|
477
|
+
o for o in self._orders[symbol]['BSTPLMT'] if o != order]
|
|
472
478
|
logmsg(order, 'BUY STOP LIMIT')
|
|
473
479
|
for order in self._orders[symbol]['SSTPLMT'].copy():
|
|
474
480
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
@@ -479,21 +485,23 @@ class MT5Strategy(Strategy):
|
|
|
479
485
|
assert order not in self._orders[symbol]['SSTPLMT']
|
|
480
486
|
logmsg(order, 'SELL STOP LIMIT')
|
|
481
487
|
except AssertionError:
|
|
482
|
-
self._orders[symbol]['SSTPLMT'] = [
|
|
488
|
+
self._orders[symbol]['SSTPLMT'] = [
|
|
489
|
+
o for o in self._orders[symbol]['SSTPLMT'] if o != order]
|
|
483
490
|
logmsg(order, 'SELL STOP LIMIT')
|
|
484
491
|
|
|
485
|
-
|
|
492
|
+
@staticmethod
|
|
493
|
+
def calculate_pct_change(current_price, lh_price):
|
|
486
494
|
return ((current_price - lh_price) / lh_price) * 100
|
|
487
|
-
|
|
495
|
+
|
|
488
496
|
def get_asset_values(self,
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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:
|
|
497
505
|
"""
|
|
498
506
|
Get the historical OHLCV value or returns or custum value
|
|
499
507
|
based on the DataHandker of the assets in the symbol list.
|
|
@@ -509,7 +517,7 @@ class MT5Strategy(Strategy):
|
|
|
509
517
|
|
|
510
518
|
Returns:
|
|
511
519
|
asset_values : Historical values of the assets in the symbol list.
|
|
512
|
-
|
|
520
|
+
|
|
513
521
|
Note:
|
|
514
522
|
In Live mode, the `bbstrader.metatrader.rates.Rates` class is used to get the historical data
|
|
515
523
|
so the value_type must be 'returns', 'open', 'high', 'low', 'close', 'adjclose', 'volume'.
|
|
@@ -527,10 +535,11 @@ class MT5Strategy(Strategy):
|
|
|
527
535
|
asset_values[asset] = values[~np.isnan(values)]
|
|
528
536
|
else:
|
|
529
537
|
values = bars.get_latest_bars(asset, N=window)
|
|
530
|
-
asset_values[asset] = getattr(values, value_type)
|
|
538
|
+
asset_values[asset] = getattr(values, value_type)
|
|
531
539
|
elif mode == 'live':
|
|
532
540
|
for asset in symbol_list:
|
|
533
|
-
rates = Rates(
|
|
541
|
+
rates = Rates(asset, timeframe=tf,
|
|
542
|
+
count=window + 1, **self.kwargs)
|
|
534
543
|
if array:
|
|
535
544
|
values = getattr(rates, value_type).values
|
|
536
545
|
asset_values[asset] = values[~np.isnan(values)]
|
|
@@ -542,7 +551,8 @@ class MT5Strategy(Strategy):
|
|
|
542
551
|
else:
|
|
543
552
|
return None
|
|
544
553
|
|
|
545
|
-
|
|
554
|
+
@staticmethod
|
|
555
|
+
def is_signal_time(period_count, signal_inverval) -> bool:
|
|
546
556
|
"""
|
|
547
557
|
Check if we can generate a signal based on the current period count.
|
|
548
558
|
We use the signal interval as a form of periodicity or rebalancing period.
|
|
@@ -575,11 +585,11 @@ class MT5Strategy(Strategy):
|
|
|
575
585
|
Returns:
|
|
576
586
|
bool : True if there are open positions, False otherwise
|
|
577
587
|
"""
|
|
578
|
-
account = account or Account()
|
|
588
|
+
account = account or Account(**self.kwargs)
|
|
579
589
|
positions = account.get_positions(symbol=symbol)
|
|
580
590
|
if positions is not None:
|
|
581
591
|
open_positions = [
|
|
582
|
-
pos for pos in positions if pos.type == position
|
|
592
|
+
pos.ticket for pos in positions if pos.type == position
|
|
583
593
|
and pos.magic == strategy_id
|
|
584
594
|
]
|
|
585
595
|
if one_true:
|
|
@@ -600,7 +610,7 @@ class MT5Strategy(Strategy):
|
|
|
600
610
|
Returns:
|
|
601
611
|
prices : numpy array of buy or sell prices for open positions if any or an empty array.
|
|
602
612
|
"""
|
|
603
|
-
account = account or Account()
|
|
613
|
+
account = account or Account(**self.kwargs)
|
|
604
614
|
positions = account.get_positions(symbol=symbol)
|
|
605
615
|
if positions is not None:
|
|
606
616
|
prices = np.array([
|
|
@@ -610,10 +620,12 @@ class MT5Strategy(Strategy):
|
|
|
610
620
|
return prices
|
|
611
621
|
return np.array([])
|
|
612
622
|
|
|
613
|
-
|
|
623
|
+
@staticmethod
|
|
624
|
+
def get_current_dt(time_zone: str = 'US/Eastern') -> datetime:
|
|
614
625
|
return datetime.now(pytz.timezone(time_zone))
|
|
615
626
|
|
|
616
|
-
|
|
627
|
+
@staticmethod
|
|
628
|
+
def convert_time_zone(dt: datetime | int | pd.Timestamp,
|
|
617
629
|
from_tz: str = 'UTC',
|
|
618
630
|
to_tz: str = 'US/Eastern'
|
|
619
631
|
) -> pd.Timestamp:
|
|
@@ -641,6 +653,19 @@ class MT5Strategy(Strategy):
|
|
|
641
653
|
dt_to = dt.tz_convert(pytz.timezone(to_tz))
|
|
642
654
|
return dt_to
|
|
643
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
|
+
|
|
644
669
|
|
|
645
670
|
class TWSStrategy(Strategy):
|
|
646
671
|
...
|
bbstrader/config.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import List
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
ADMIRAL_PATH = "C:\\Program Files\\Admirals Group MT5 Terminal\\terminal64.exe"
|
|
6
|
+
FTMO_PATH = "C:\\Program Files\\FTMO MetaTrader 5\\terminal64.exe"
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
def get_config_dir(name: str = ".bbstrader") -> Path:
|
|
7
10
|
"""
|
|
8
11
|
Get the path to the configuration directory.
|
|
9
12
|
|
|
@@ -18,6 +21,7 @@ def get_config_dir(name: str=".bbstrader") -> Path:
|
|
|
18
21
|
home_dir.mkdir()
|
|
19
22
|
return home_dir
|
|
20
23
|
|
|
24
|
+
|
|
21
25
|
BBSTRADER_DIR = get_config_dir()
|
|
22
26
|
|
|
23
27
|
|
|
@@ -43,7 +47,7 @@ class LogLevelFilter(logging.Filter):
|
|
|
43
47
|
True if the record's level is in the allowed levels, False otherwise.
|
|
44
48
|
"""
|
|
45
49
|
return record.levelno in self.levels
|
|
46
|
-
|
|
50
|
+
|
|
47
51
|
|
|
48
52
|
class CustomFormatter(logging.Formatter):
|
|
49
53
|
def formatTime(self, record, datefmt=None):
|
|
@@ -57,7 +61,7 @@ class CustomLogger(logging.Logger):
|
|
|
57
61
|
def __init__(self, name, level=logging.NOTSET):
|
|
58
62
|
super().__init__(name, level)
|
|
59
63
|
|
|
60
|
-
def _log(self, level, msg, args, exc_info=None,
|
|
64
|
+
def _log(self, level, msg, args, exc_info=None,
|
|
61
65
|
extra=None, stack_info=False, stacklevel=1, custom_time=None):
|
|
62
66
|
if extra is None:
|
|
63
67
|
extra = {}
|
|
@@ -73,13 +77,15 @@ class CustomLogger(logging.Logger):
|
|
|
73
77
|
self._log(logging.DEBUG, msg, args, custom_time=custom_time, **kwargs)
|
|
74
78
|
|
|
75
79
|
def warning(self, msg, *args, custom_time=None, **kwargs):
|
|
76
|
-
self._log(logging.WARNING, msg, args,
|
|
80
|
+
self._log(logging.WARNING, msg, args,
|
|
81
|
+
custom_time=custom_time, **kwargs)
|
|
77
82
|
|
|
78
83
|
def error(self, msg, *args, custom_time=None, **kwargs):
|
|
79
84
|
self._log(logging.ERROR, msg, args, custom_time=custom_time, **kwargs)
|
|
80
85
|
|
|
81
86
|
def critical(self, msg, *args, custom_time=None, **kwargs):
|
|
82
|
-
self._log(logging.CRITICAL, msg, args,
|
|
87
|
+
self._log(logging.CRITICAL, msg, args,
|
|
88
|
+
custom_time=custom_time, **kwargs)
|
|
83
89
|
|
|
84
90
|
|
|
85
91
|
def config_logger(log_file: str, console_log=True):
|
|
@@ -108,4 +114,4 @@ def config_logger(log_file: str, console_log=True):
|
|
|
108
114
|
console_handler.setFormatter(formatter)
|
|
109
115
|
logger.addHandler(console_handler)
|
|
110
116
|
|
|
111
|
-
return logger
|
|
117
|
+
return logger
|
|
File without changes
|
bbstrader/core/data.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from financetoolkit import Toolkit
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
'FMP',
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FMP(Toolkit):
|
|
9
|
+
"""
|
|
10
|
+
FMPData class for fetching data from Financial Modeling Prep API
|
|
11
|
+
using the Toolkit class from financetoolkit package.
|
|
12
|
+
|
|
13
|
+
See `financetoolkit` for more details.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, api_key: str = '', symbols: str | list = 'AAPL'):
|
|
18
|
+
super().__init__(tickers=symbols, api_key=api_key)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DataBendo:
|
|
22
|
+
...
|
bbstrader/core/utils.py
ADDED
|
@@ -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
|
+
)
|
|
File without changes
|
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
|