bbstrader 0.1.8__py3-none-any.whl → 0.1.91__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 +4 -2
- bbstrader/btengine/__init__.py +5 -5
- bbstrader/btengine/backtest.py +51 -10
- bbstrader/btengine/data.py +147 -55
- bbstrader/btengine/event.py +4 -1
- bbstrader/btengine/execution.py +125 -23
- bbstrader/btengine/performance.py +4 -7
- bbstrader/btengine/portfolio.py +34 -13
- bbstrader/btengine/strategy.py +466 -6
- bbstrader/config.py +111 -0
- bbstrader/metatrader/__init__.py +4 -4
- bbstrader/metatrader/account.py +357 -55
- bbstrader/metatrader/rates.py +234 -31
- bbstrader/metatrader/risk.py +35 -24
- bbstrader/metatrader/trade.py +361 -173
- bbstrader/metatrader/utils.py +2 -53
- bbstrader/models/factors.py +0 -0
- bbstrader/models/ml.py +0 -0
- bbstrader/models/optimization.py +0 -0
- bbstrader/trading/__init__.py +1 -1
- bbstrader/trading/execution.py +329 -215
- bbstrader/trading/scripts.py +57 -0
- bbstrader/trading/strategies.py +49 -71
- bbstrader/tseries.py +274 -39
- {bbstrader-0.1.8.dist-info → bbstrader-0.1.91.dist-info}/METADATA +11 -3
- bbstrader-0.1.91.dist-info/RECORD +31 -0
- {bbstrader-0.1.8.dist-info → bbstrader-0.1.91.dist-info}/WHEEL +1 -1
- bbstrader-0.1.8.dist-info/RECORD +0 -26
- {bbstrader-0.1.8.dist-info → bbstrader-0.1.91.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.8.dist-info → bbstrader-0.1.91.dist-info}/top_level.txt +0 -0
bbstrader/btengine/strategy.py
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
from abc import ABCMeta, abstractmethod
|
|
2
|
-
|
|
2
|
+
import pytz
|
|
3
|
+
import math
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import numpy as np
|
|
6
|
+
from queue import Queue
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from bbstrader.config import config_logger
|
|
9
|
+
from bbstrader.btengine.event import SignalEvent
|
|
10
|
+
from bbstrader.btengine.data import DataHandler
|
|
11
|
+
from bbstrader.metatrader.account import Account
|
|
12
|
+
from bbstrader.metatrader.rates import Rates
|
|
13
|
+
from typing import (
|
|
14
|
+
Dict,
|
|
15
|
+
Union,
|
|
16
|
+
Any,
|
|
17
|
+
List,
|
|
18
|
+
Literal
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = ['Strategy', 'MT5Strategy']
|
|
3
22
|
|
|
4
23
|
class Strategy(metaclass=ABCMeta):
|
|
5
24
|
"""
|
|
@@ -22,11 +41,452 @@ class Strategy(metaclass=ABCMeta):
|
|
|
22
41
|
"""
|
|
23
42
|
|
|
24
43
|
@abstractmethod
|
|
25
|
-
def calculate_signals(self, *args, **kwargs) ->
|
|
44
|
+
def calculate_signals(self, *args, **kwargs) -> Any:
|
|
45
|
+
raise NotImplementedError(
|
|
46
|
+
"Should implement calculate_signals()"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def check_pending_orders(self): ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MT5Strategy(Strategy):
|
|
53
|
+
"""
|
|
54
|
+
A `MT5Strategy()` object is a subclass of `Strategy` that is used to
|
|
55
|
+
calculate signals for the MetaTrader 5 trading platform. The signals
|
|
56
|
+
are generated by the `MT5Strategy` object and sent to the the `MT5ExecutionEngine`
|
|
57
|
+
for live trading and `MT5BacktestEngine` objects for backtesting.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, events: Queue=None, symbol_list: List[str]=None,
|
|
61
|
+
bars: DataHandler=None, mode: str=None, **kwargs):
|
|
62
|
+
"""
|
|
63
|
+
Initialize the `MT5Strategy` object.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
events : The event queue.
|
|
67
|
+
symbol_list : The list of symbols for the strategy.
|
|
68
|
+
bars : The data handler object.
|
|
69
|
+
mode : The mode of operation for the strategy (backtest or live).
|
|
70
|
+
**kwargs : Additional keyword arguments for other classes (e.g, Portfolio, ExecutionHandler).
|
|
26
71
|
"""
|
|
27
|
-
|
|
72
|
+
self.events = events
|
|
73
|
+
self.data = bars
|
|
74
|
+
self.symbols = symbol_list
|
|
75
|
+
self.mode = mode
|
|
76
|
+
self.volume = kwargs.get("volume")
|
|
77
|
+
self.logger = kwargs.get("logger", config_logger("mt5_strategy.log"))
|
|
78
|
+
self._construct_positions_and_orders()
|
|
79
|
+
|
|
80
|
+
def _construct_positions_and_orders(self):
|
|
81
|
+
self.positions: Dict[str, Dict[str, int]] = {}
|
|
82
|
+
self.orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
|
|
83
|
+
positions = ['LONG', 'SHORT']
|
|
84
|
+
orders = ['BLMT', 'BSTP', 'BSTPLMT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
85
|
+
for symbol in self.symbols:
|
|
86
|
+
self.positions[symbol] = {position: 0 for position in positions}
|
|
87
|
+
self.orders[symbol] = {order: [] for order in orders}
|
|
88
|
+
|
|
89
|
+
def calculate_signals(self, *args, **kwargs
|
|
90
|
+
) -> Dict[str, Union[str, dict, None]] | None:
|
|
91
|
+
"""
|
|
92
|
+
Provides the mechanisms to calculate signals for the strategy.
|
|
28
93
|
This methods should return a dictionary of symbols and their respective signals.
|
|
94
|
+
The returned signals should be either string or dictionary objects.
|
|
95
|
+
|
|
96
|
+
If a string is used, it should be:
|
|
97
|
+
- ``LONG`` , ``BMKT``, ``BLMT``, ``BSTP``, ``BSTPLMT`` for a long signal (market, limit, stop, stop-limit).
|
|
98
|
+
- ``SHORT``, ``SMKT``, ``SLMT``, ``SSTP``, ``SSTPLMT`` for a short signal (market, limit, stop, stop-limit).
|
|
99
|
+
- ``EXIT``, ``EXIT_LONG``, ``EXIT_LONG_STOP``, ``EXIT_LONG_LIMIT``, ``EXIT_LONG_STOP_LIMIT`` for an exit signal (long).
|
|
100
|
+
- ``EXIT_SHORT``, ``EXIT_SHORT_STOP``, ``EXIT_SHORT_LIMIT``, ``EXIT_SHORT_STOP_LIMIT`` for an exit signal (short).
|
|
101
|
+
- ``EXIT_ALL_ORDERS`` for cancelling all orders.
|
|
102
|
+
- ``EXIT_ALL_POSITIONS`` for exiting all positions.
|
|
103
|
+
- ``EXIT_PROFITABLES`` for exiting all profitable positions.
|
|
104
|
+
- ``EXIT_LOSINGS`` for exiting all losing positions.
|
|
105
|
+
|
|
106
|
+
The signals could also be ``EXIT_STOP``, ``EXIT_LIMIT``, ``EXIT_STOP_LIMIT`` for exiting a position.
|
|
107
|
+
|
|
108
|
+
If a dictionary is used, it should be:
|
|
109
|
+
for each symbol, a dictionary with the following keys
|
|
110
|
+
- ``action``: The action to take for the symbol (LONG, SHORT, EXIT, etc.)
|
|
111
|
+
- ``price``: The price at which to execute the action.
|
|
112
|
+
- ``stoplimit``: The stop-limit price for STOP-LIMIT orders.
|
|
113
|
+
|
|
114
|
+
The dictionary can be use for pending orders (limit, stop, stop-limit) where the price is required.
|
|
29
115
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def get_quantity(self, symbol) -> int:
|
|
119
|
+
"""
|
|
120
|
+
Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
|
|
121
|
+
The quantity calculated can be used to evalute a strategy's performance for each symbol
|
|
122
|
+
given the fact that the dollar value is the same for all symbols.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
symbol : The symbol for the trade.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
qty : The quantity to buy or sell for the symbol.
|
|
129
|
+
"""
|
|
130
|
+
if self.volume is None:
|
|
131
|
+
raise ValueError("Volume must be provided for the method.")
|
|
132
|
+
current_price = self.data.get_latest_bar_value(symbol, 'close')
|
|
133
|
+
qty = math.ceil(self.volume / current_price)
|
|
134
|
+
return max(qty, 1)
|
|
135
|
+
|
|
136
|
+
def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
|
|
137
|
+
"""
|
|
138
|
+
Get the quantities to buy or sell for the symbols in the strategy.
|
|
139
|
+
This method is used when whe need to assign different quantities to the symbols.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
quantities : The quantities for the symbols in the strategy.
|
|
143
|
+
"""
|
|
144
|
+
if quantities is None:
|
|
145
|
+
return {symbol: None for symbol in self.symbols}
|
|
146
|
+
if isinstance(quantities, dict):
|
|
147
|
+
return quantities
|
|
148
|
+
elif isinstance(quantities, int):
|
|
149
|
+
return {symbol: quantities for symbol in self.symbols}
|
|
150
|
+
|
|
151
|
+
def _send_order(self, id, symbol: str, signal: str, strength: float, price: float,
|
|
152
|
+
quantity: int, dtime: datetime | pd.Timestamp):
|
|
153
|
+
|
|
154
|
+
position = SignalEvent(id, symbol, dtime, signal,
|
|
155
|
+
quantity=quantity, strength=strength, price=price)
|
|
156
|
+
self.events.put(position)
|
|
157
|
+
self.logger.info(
|
|
158
|
+
f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
|
|
159
|
+
|
|
160
|
+
def buy(self, id: int, symbol: str, price: float, quantity: int,
|
|
161
|
+
strength: float=1.0, dtime: datetime | pd.Timestamp=None):
|
|
162
|
+
"""
|
|
163
|
+
Open a long position
|
|
164
|
+
|
|
165
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
166
|
+
"""
|
|
167
|
+
self._send_order(id, symbol, 'LONG', strength, price, quantity, dtime)
|
|
168
|
+
self.positions[symbol]['LONG'] += quantity
|
|
169
|
+
|
|
170
|
+
def sell(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
171
|
+
"""
|
|
172
|
+
Open a short position
|
|
173
|
+
|
|
174
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
175
|
+
"""
|
|
176
|
+
self._send_order(id, symbol, 'SHORT', strength, price, quantity, dtime)
|
|
177
|
+
self.positions[symbol]['SHORT'] += quantity
|
|
178
|
+
|
|
179
|
+
def close(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
180
|
+
"""
|
|
181
|
+
Close a position
|
|
182
|
+
|
|
183
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
184
|
+
"""
|
|
185
|
+
self._send_order(id, symbol, 'EXIT', strength, price, quantity, dtime)
|
|
186
|
+
self.positions[symbol]['LONG'] -= quantity
|
|
187
|
+
|
|
188
|
+
def buy_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
189
|
+
"""
|
|
190
|
+
Open a pending order to buy at a stop price
|
|
191
|
+
|
|
192
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
193
|
+
"""
|
|
194
|
+
current_price = self.data.get_latest_bar_value(symbol, 'close')
|
|
195
|
+
if price <= current_price:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"The buy_stop price must be greater than the current price.")
|
|
198
|
+
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
199
|
+
quantity=quantity, strength=strength, price=price)
|
|
200
|
+
self.orders[symbol]['BSTP'].append(order)
|
|
201
|
+
|
|
202
|
+
def sell_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
203
|
+
"""
|
|
204
|
+
Open a pending order to sell at a stop price
|
|
205
|
+
|
|
206
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
207
|
+
"""
|
|
208
|
+
current_price = self.data.get_latest_bar_value(symbol, 'close')
|
|
209
|
+
if price >= current_price:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
"The sell_stop price must be less than the current price.")
|
|
212
|
+
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
213
|
+
quantity=quantity, strength=strength, price=price)
|
|
214
|
+
self.orders[symbol]['SSTP'].append(order)
|
|
215
|
+
|
|
216
|
+
def buy_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
217
|
+
"""
|
|
218
|
+
Open a pending order to buy at a limit price
|
|
219
|
+
|
|
220
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
221
|
+
"""
|
|
222
|
+
current_price = self.data.get_latest_bar_value(symbol, 'close')
|
|
223
|
+
if price >= current_price:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
"The buy_limit price must be less than the current price.")
|
|
226
|
+
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
227
|
+
quantity=quantity, strength=strength, price=price)
|
|
228
|
+
self.orders[symbol]['BLMT'].append(order)
|
|
229
|
+
|
|
230
|
+
def sell_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
231
|
+
"""
|
|
232
|
+
Open a pending order to sell at a limit price
|
|
233
|
+
|
|
234
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
235
|
+
"""
|
|
236
|
+
current_price = self.data.get_latest_bar_value(symbol, 'close')
|
|
237
|
+
if price <= current_price:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
"The sell_limit price must be greater than the current price.")
|
|
240
|
+
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
241
|
+
quantity=quantity, strength=strength, price=price)
|
|
242
|
+
self.orders[symbol]['SLMT'].append(order)
|
|
243
|
+
|
|
244
|
+
def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
|
|
245
|
+
quantity: int, strength: float=1.0, dtime: datetime | pd.Timestamp = None):
|
|
246
|
+
"""
|
|
247
|
+
Open a pending order to buy at a stop-limit price
|
|
248
|
+
|
|
249
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
250
|
+
"""
|
|
251
|
+
current_price = self.data.get_latest_bar_value(symbol, 'close')
|
|
252
|
+
if price <= current_price:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"The stop price {price} must be greater than the current price {current_price}.")
|
|
255
|
+
if price >= stoplimit:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
f"The stop-limit price {stoplimit} must be greater than the price {price}.")
|
|
258
|
+
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
259
|
+
quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
|
|
260
|
+
self.orders[symbol]['BSTPLMT'].append(order)
|
|
261
|
+
|
|
262
|
+
def sell_stop_limit(self, id, symbol, price, stoplimit, quantity, strength=1.0, dtime=None):
|
|
263
|
+
"""
|
|
264
|
+
Open a pending order to sell at a stop-limit price
|
|
265
|
+
|
|
266
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
267
|
+
"""
|
|
268
|
+
current_price = self.data.get_latest_bar_value(symbol, 'close')
|
|
269
|
+
if price >= current_price:
|
|
270
|
+
raise ValueError(
|
|
271
|
+
f"The stop price {price} must be less than the current price {current_price}.")
|
|
272
|
+
if price <= stoplimit:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
f"The stop-limit price {stoplimit} must be less than the price {price}.")
|
|
275
|
+
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
276
|
+
quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
|
|
277
|
+
self.orders[symbol]['SSTPLMT'].append(order)
|
|
278
|
+
|
|
279
|
+
def check_pending_orders(self):
|
|
280
|
+
"""
|
|
281
|
+
Check for pending orders and handle them accordingly.
|
|
282
|
+
"""
|
|
283
|
+
for symbol in self.symbols:
|
|
284
|
+
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
285
|
+
for order in self.orders[symbol]['BLMT'].copy():
|
|
286
|
+
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
287
|
+
self.buy(order.strategy_id, symbol,
|
|
288
|
+
order.price, order.quantity, dtime)
|
|
289
|
+
self.logger.info(
|
|
290
|
+
f"BUY LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
291
|
+
f"PRICE @ {order.price}", custom_time=dtime)
|
|
292
|
+
self.orders[symbol]['BLMT'].remove(order)
|
|
293
|
+
for order in self.orders[symbol]['SLMT'].copy():
|
|
294
|
+
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
295
|
+
self.sell(order.strategy_id, symbol,
|
|
296
|
+
order.price, order.quantity, dtime)
|
|
297
|
+
self.logger.info(
|
|
298
|
+
f"SELL LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
299
|
+
f"PRICE @ {order.price}", custom_time=dtime)
|
|
300
|
+
self.orders[symbol]['SLMT'].remove(order)
|
|
301
|
+
for order in self.orders[symbol]['BSTP'].copy():
|
|
302
|
+
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
303
|
+
self.buy(order.strategy_id, symbol,
|
|
304
|
+
order.price, order.quantity, dtime)
|
|
305
|
+
self.logger.info(
|
|
306
|
+
f"BUY STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
307
|
+
f"PRICE @ {order.price}", custom_time=dtime)
|
|
308
|
+
self.orders[symbol]['BSTP'].remove(order)
|
|
309
|
+
for order in self.orders[symbol]['SSTP'].copy():
|
|
310
|
+
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
311
|
+
self.sell(order.strategy_id, symbol,
|
|
312
|
+
order.price, order.quantity, dtime)
|
|
313
|
+
self.logger.info(
|
|
314
|
+
f"SELL STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
315
|
+
f"PRICE @ {order.price}", custom_time=dtime)
|
|
316
|
+
self.orders[symbol]['SSTP'].remove(order)
|
|
317
|
+
for order in self.orders[symbol]['BSTPLMT'].copy():
|
|
318
|
+
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
319
|
+
self.buy_limit(order.strategy_id, symbol,
|
|
320
|
+
order.stoplimit, order.quantity, dtime)
|
|
321
|
+
self.logger.info(
|
|
322
|
+
f"BUY STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
323
|
+
f"PRICE @ {order.price}", custom_time=dtime)
|
|
324
|
+
self.orders[symbol]['BSTPLMT'].remove(order)
|
|
325
|
+
for order in self.orders[symbol]['SSTPLMT'].copy():
|
|
326
|
+
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
327
|
+
self.sell_limit(order.strategy_id, symbol,
|
|
328
|
+
order.stoplimit, order.quantity, dtime)
|
|
329
|
+
self.logger.info(
|
|
330
|
+
f"SELL STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
331
|
+
f"PRICE @ {order.price}", custom_time=dtime)
|
|
332
|
+
self.orders[symbol]['SSTPLMT'].remove(order)
|
|
333
|
+
|
|
334
|
+
def get_asset_values(self,
|
|
335
|
+
symbol_list: List[str],
|
|
336
|
+
window: int,
|
|
337
|
+
value_type: str = 'returns',
|
|
338
|
+
array: bool = True,
|
|
339
|
+
bars: DataHandler = None,
|
|
340
|
+
mode: Literal['backtest', 'live'] = 'backtest',
|
|
341
|
+
tf: str = 'D1'
|
|
342
|
+
) -> Dict[str, np.ndarray | pd.Series] | None:
|
|
343
|
+
"""
|
|
344
|
+
Get the historical OHLCV value or returns or custum value
|
|
345
|
+
based on the DataHandker of the assets in the symbol list.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
bars : DataHandler for market data handling, required for backtest mode.
|
|
349
|
+
symbol_list : List of ticker symbols for the pairs trading strategy.
|
|
350
|
+
value_type : The type of value to get (e.g., returns, open, high, low, close, adjclose, volume).
|
|
351
|
+
array : If True, return the values as numpy arrays, otherwise as pandas Series.
|
|
352
|
+
mode : Mode of operation for the strategy.
|
|
353
|
+
window : The lookback period for resquesting the data.
|
|
354
|
+
tf : The time frame for the strategy.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
asset_values : Historical values of the assets in the symbol list.
|
|
358
|
+
|
|
359
|
+
Note:
|
|
360
|
+
In Live mode, the `bbstrader.metatrader.rates.Rates` class is used to get the historical data
|
|
361
|
+
so the value_type must be 'returns', 'open', 'high', 'low', 'close', 'adjclose', 'volume'.
|
|
362
|
+
"""
|
|
363
|
+
if mode not in ['backtest', 'live']:
|
|
364
|
+
raise ValueError('Mode must be either backtest or live.')
|
|
365
|
+
asset_values = {}
|
|
366
|
+
if mode == 'backtest':
|
|
367
|
+
if bars is None:
|
|
368
|
+
raise ValueError('DataHandler is required for backtest mode.')
|
|
369
|
+
for asset in symbol_list:
|
|
370
|
+
if array:
|
|
371
|
+
values = bars.get_latest_bars_values(
|
|
372
|
+
asset, value_type, N=window)
|
|
373
|
+
asset_values[asset] = values[~np.isnan(values)]
|
|
374
|
+
else:
|
|
375
|
+
values = bars.get_latest_bars(asset, N=window)
|
|
376
|
+
asset_values[asset] = getattr(values, value_type)
|
|
377
|
+
elif mode == 'live':
|
|
378
|
+
for asset in symbol_list:
|
|
379
|
+
rates = Rates(symbol=asset, time_frame=tf, count=window + 1)
|
|
380
|
+
if array:
|
|
381
|
+
values = getattr(rates, value_type).values
|
|
382
|
+
asset_values[asset] = values[~np.isnan(values)]
|
|
383
|
+
else:
|
|
384
|
+
values = getattr(rates, value_type)
|
|
385
|
+
asset_values[asset] = values
|
|
386
|
+
if all(len(values) >= window for values in asset_values.values()):
|
|
387
|
+
return {a: v[-window:] for a, v in asset_values.items()}
|
|
388
|
+
else:
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
def is_signal_time(self, period_count, signal_inverval) -> bool:
|
|
392
|
+
"""
|
|
393
|
+
Check if we can generate a signal based on the current period count.
|
|
394
|
+
We use the signal interval as a form of periodicity or rebalancing period.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
period_count : The current period count (e.g., number of bars).
|
|
398
|
+
signal_inverval : The signal interval for generating signals (e.g., every 5 bars).
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
bool : True if we can generate a signal, False otherwise
|
|
402
|
+
"""
|
|
403
|
+
if period_count == 0 or period_count is None:
|
|
404
|
+
return True
|
|
405
|
+
return period_count % signal_inverval == 0
|
|
406
|
+
|
|
407
|
+
def ispositions(self, symbol, strategy_id, position, max_trades, one_true=False, account=None) -> bool:
|
|
408
|
+
"""
|
|
409
|
+
This function is use for live trading to check if there are open positions
|
|
410
|
+
for a given symbol and strategy. It is used to prevent opening more trades
|
|
411
|
+
than the maximum allowed trades per symbol.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
symbol : The symbol for the trade.
|
|
415
|
+
strategy_id : The unique identifier for the strategy.
|
|
416
|
+
position : The position type (1: short, 0: long).
|
|
417
|
+
max_trades : The maximum number of trades allowed per symbol.
|
|
418
|
+
one_true : If True, return True if there is at least one open position.
|
|
419
|
+
account : The `bbstrader.metatrader.Account` object for the strategy.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
bool : True if there are open positions, False otherwise
|
|
423
|
+
"""
|
|
424
|
+
account = account or Account()
|
|
425
|
+
positions = account.get_positions(symbol=symbol)
|
|
426
|
+
if positions is not None:
|
|
427
|
+
open_positions = [
|
|
428
|
+
pos for pos in positions if pos.type == position
|
|
429
|
+
and pos.magic == strategy_id
|
|
430
|
+
]
|
|
431
|
+
if one_true:
|
|
432
|
+
return len(open_positions) in range(1, max_trades + 1)
|
|
433
|
+
return len(open_positions) >= max_trades
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
def get_positions_prices(self, symbol, strategy_id, position, account=None):
|
|
437
|
+
"""
|
|
438
|
+
Get the buy or sell prices for open positions of a given symbol and strategy.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
symbol : The symbol for the trade.
|
|
442
|
+
strategy_id : The unique identifier for the strategy.
|
|
443
|
+
position : The position type (1: short, 0: long).
|
|
444
|
+
account : The `bbstrader.metatrader.Account` object for the strategy.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
prices : numpy array of buy or sell prices for open positions if any or an empty array.
|
|
448
|
+
"""
|
|
449
|
+
account = account or Account()
|
|
450
|
+
positions = account.get_positions(symbol=symbol)
|
|
451
|
+
if positions is not None:
|
|
452
|
+
prices = np.array([
|
|
453
|
+
pos.price_open for pos in positions
|
|
454
|
+
if pos.type == position and pos.magic == strategy_id
|
|
455
|
+
])
|
|
456
|
+
return prices
|
|
457
|
+
return np.array([])
|
|
458
|
+
|
|
459
|
+
def get_current_dt(self, time_zone: str = 'US/Eastern') -> datetime:
|
|
460
|
+
return datetime.now(pytz.timezone(time_zone))
|
|
461
|
+
|
|
462
|
+
def convert_time_zone(self, dt: datetime | int | pd.Timestamp,
|
|
463
|
+
from_tz: str = 'UTC',
|
|
464
|
+
to_tz: str = 'US/Eastern'
|
|
465
|
+
) -> pd.Timestamp:
|
|
466
|
+
"""
|
|
467
|
+
Convert datetime from one timezone to another.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
dt : The datetime to convert.
|
|
471
|
+
from_tz : The timezone to convert from.
|
|
472
|
+
to_tz : The timezone to convert to.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
dt_to : The converted datetime.
|
|
476
|
+
"""
|
|
477
|
+
from_tz = pytz.timezone(from_tz)
|
|
478
|
+
if isinstance(dt, datetime):
|
|
479
|
+
dt = pd.to_datetime(dt, unit='s')
|
|
480
|
+
elif isinstance(dt, int):
|
|
481
|
+
dt = pd.to_datetime(dt, unit='s')
|
|
482
|
+
if dt.tzinfo is None:
|
|
483
|
+
dt = dt.tz_localize(from_tz)
|
|
484
|
+
else:
|
|
485
|
+
dt = dt.tz_convert(from_tz)
|
|
486
|
+
|
|
487
|
+
dt_to = dt.tz_convert(pytz.timezone(to_tz))
|
|
488
|
+
return dt_to
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class TWSStrategy(Strategy):
|
|
492
|
+
...
|
bbstrader/config.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import List
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
def get_config_dir(name: str=".bbstrader") -> Path:
|
|
7
|
+
"""
|
|
8
|
+
Get the path to the configuration directory.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
name: The name of the configuration directory.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
The path to the configuration directory.
|
|
15
|
+
"""
|
|
16
|
+
home_dir = Path.home() / name
|
|
17
|
+
if not home_dir.exists():
|
|
18
|
+
home_dir.mkdir()
|
|
19
|
+
return home_dir
|
|
20
|
+
|
|
21
|
+
BBSTRADER_DIR = get_config_dir()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LogLevelFilter(logging.Filter):
|
|
25
|
+
def __init__(self, levels: List[int]):
|
|
26
|
+
"""
|
|
27
|
+
Initializes the filter with specific logging levels.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
levels: A list of logging level values (integers) to include.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.levels = levels
|
|
34
|
+
|
|
35
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Filters log records based on their level.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
record: The log record to check.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if the record's level is in the allowed levels, False otherwise.
|
|
44
|
+
"""
|
|
45
|
+
return record.levelno in self.levels
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CustomFormatter(logging.Formatter):
|
|
49
|
+
def formatTime(self, record, datefmt=None):
|
|
50
|
+
if hasattr(record, 'custom_time'):
|
|
51
|
+
# Use the custom time if provided
|
|
52
|
+
record.created = record.custom_time.timestamp()
|
|
53
|
+
return super().formatTime(record, datefmt)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CustomLogger(logging.Logger):
|
|
57
|
+
def __init__(self, name, level=logging.NOTSET):
|
|
58
|
+
super().__init__(name, level)
|
|
59
|
+
|
|
60
|
+
def _log(self, level, msg, args, exc_info=None,
|
|
61
|
+
extra=None, stack_info=False, stacklevel=1, custom_time=None):
|
|
62
|
+
if extra is None:
|
|
63
|
+
extra = {}
|
|
64
|
+
# Add custom_time to the extra dictionary if provided
|
|
65
|
+
if custom_time:
|
|
66
|
+
extra['custom_time'] = custom_time
|
|
67
|
+
super()._log(level, msg, args, exc_info, extra, stack_info, stacklevel)
|
|
68
|
+
|
|
69
|
+
def info(self, msg, *args, custom_time=None, **kwargs):
|
|
70
|
+
self._log(logging.INFO, msg, args, custom_time=custom_time, **kwargs)
|
|
71
|
+
|
|
72
|
+
def debug(self, msg, *args, custom_time=None, **kwargs):
|
|
73
|
+
self._log(logging.DEBUG, msg, args, custom_time=custom_time, **kwargs)
|
|
74
|
+
|
|
75
|
+
def warning(self, msg, *args, custom_time=None, **kwargs):
|
|
76
|
+
self._log(logging.WARNING, msg, args, custom_time=custom_time, **kwargs)
|
|
77
|
+
|
|
78
|
+
def error(self, msg, *args, custom_time=None, **kwargs):
|
|
79
|
+
self._log(logging.ERROR, msg, args, custom_time=custom_time, **kwargs)
|
|
80
|
+
|
|
81
|
+
def critical(self, msg, *args, custom_time=None, **kwargs):
|
|
82
|
+
self._log(logging.CRITICAL, msg, args, custom_time=custom_time, **kwargs)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def config_logger(log_file: str, console_log=True):
|
|
86
|
+
|
|
87
|
+
# Use the CustomLogger
|
|
88
|
+
logging.setLoggerClass(CustomLogger)
|
|
89
|
+
logger = logging.getLogger(__name__)
|
|
90
|
+
logger.setLevel(logging.DEBUG)
|
|
91
|
+
|
|
92
|
+
# File handler
|
|
93
|
+
file_handler = logging.FileHandler(log_file)
|
|
94
|
+
file_handler.setLevel(logging.INFO)
|
|
95
|
+
|
|
96
|
+
# Custom formatter
|
|
97
|
+
formatter = CustomFormatter(
|
|
98
|
+
'%(asctime)s - %(levelname)s: %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
|
|
99
|
+
file_handler.setFormatter(formatter)
|
|
100
|
+
|
|
101
|
+
# Add the handler to the logger
|
|
102
|
+
logger.addHandler(file_handler)
|
|
103
|
+
|
|
104
|
+
if console_log:
|
|
105
|
+
# Handler for the console with a different level
|
|
106
|
+
console_handler = logging.StreamHandler()
|
|
107
|
+
console_handler.setLevel(logging.DEBUG)
|
|
108
|
+
console_handler.setFormatter(formatter)
|
|
109
|
+
logger.addHandler(console_handler)
|
|
110
|
+
|
|
111
|
+
return logger
|
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
|
|
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
6
|
from bbstrader.metatrader.utils import *
|