bbstrader 2.0.3__cp312-cp312-macosx_11_0_arm64.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.
- bbstrader/__init__.py +27 -0
- bbstrader/__main__.py +92 -0
- bbstrader/api/__init__.py +96 -0
- bbstrader/api/handlers.py +245 -0
- bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
- bbstrader/api/metatrader_client.pyi +624 -0
- bbstrader/assets/bbs_.png +0 -0
- bbstrader/assets/bbstrader.ico +0 -0
- bbstrader/assets/bbstrader.png +0 -0
- bbstrader/assets/qs_metrics_1.png +0 -0
- bbstrader/btengine/__init__.py +54 -0
- bbstrader/btengine/backtest.py +358 -0
- bbstrader/btengine/data.py +737 -0
- bbstrader/btengine/event.py +229 -0
- bbstrader/btengine/execution.py +287 -0
- bbstrader/btengine/performance.py +408 -0
- bbstrader/btengine/portfolio.py +393 -0
- bbstrader/btengine/strategy.py +588 -0
- bbstrader/compat.py +28 -0
- bbstrader/config.py +100 -0
- bbstrader/core/__init__.py +27 -0
- bbstrader/core/data.py +628 -0
- bbstrader/core/strategy.py +466 -0
- bbstrader/metatrader/__init__.py +48 -0
- bbstrader/metatrader/_copier.py +720 -0
- bbstrader/metatrader/account.py +865 -0
- bbstrader/metatrader/broker.py +418 -0
- bbstrader/metatrader/copier.py +1487 -0
- bbstrader/metatrader/rates.py +495 -0
- bbstrader/metatrader/risk.py +667 -0
- bbstrader/metatrader/trade.py +1692 -0
- bbstrader/metatrader/utils.py +402 -0
- bbstrader/models/__init__.py +39 -0
- bbstrader/models/nlp.py +932 -0
- bbstrader/models/optimization.py +182 -0
- bbstrader/scripts.py +665 -0
- bbstrader/trading/__init__.py +33 -0
- bbstrader/trading/execution.py +1159 -0
- bbstrader/trading/strategy.py +362 -0
- bbstrader/trading/utils.py +69 -0
- bbstrader-2.0.3.dist-info/METADATA +396 -0
- bbstrader-2.0.3.dist-info/RECORD +45 -0
- bbstrader-2.0.3.dist-info/WHEEL +5 -0
- bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
- bbstrader-2.0.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from queue import Queue
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from bbstrader.btengine.data import DataHandler
|
|
11
|
+
from bbstrader.btengine.event import Events, FillEvent, MarketEvent, SignalEvent
|
|
12
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
13
|
+
from bbstrader.core.strategy import BaseStrategy, TradingMode
|
|
14
|
+
|
|
15
|
+
logger.add(
|
|
16
|
+
f"{BBSTRADER_DIR}/logs/strategy.log",
|
|
17
|
+
enqueue=True,
|
|
18
|
+
level="INFO",
|
|
19
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = ["BacktestStrategy"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BacktestStrategy(BaseStrategy):
|
|
26
|
+
"""
|
|
27
|
+
Strategy implementation specifically for Backtesting.
|
|
28
|
+
Handles internal state for orders, positions, trades, and cash.
|
|
29
|
+
Simulates order execution and pending orders.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_orders: Dict[str, Dict[str, List[SignalEvent]]]
|
|
33
|
+
_positions: Dict[str, Dict[str, Union[int, float]]]
|
|
34
|
+
_trades: Dict[str, Dict[str, int]]
|
|
35
|
+
_holdings: Dict[str, float]
|
|
36
|
+
_portfolio_value: Optional[float]
|
|
37
|
+
events: "Queue[Union[SignalEvent, FillEvent]]"
|
|
38
|
+
data: DataHandler
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
events: "Queue[Union[SignalEvent, FillEvent]]",
|
|
43
|
+
symbol_list: List[str],
|
|
44
|
+
bars: DataHandler,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initialize the `BacktestStrategy` object.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
events : The event queue.
|
|
52
|
+
symbol_list : The list of symbols for the strategy.
|
|
53
|
+
bars : The data handler object.
|
|
54
|
+
**kwargs : Additional keyword arguments for other classes (e.g, Portfolio, ExecutionHandler).
|
|
55
|
+
- max_trades : The maximum number of trades allowed per symbol.
|
|
56
|
+
- time_frame : The time frame for the strategy.
|
|
57
|
+
- logger : The logger object for the strategy.
|
|
58
|
+
"""
|
|
59
|
+
super().__init__(symbol_list, **kwargs)
|
|
60
|
+
self.events = events
|
|
61
|
+
self.data = bars
|
|
62
|
+
self.mode = TradingMode.BACKTEST
|
|
63
|
+
self._portfolio_value = None
|
|
64
|
+
self._initialize_portfolio()
|
|
65
|
+
|
|
66
|
+
def _initialize_portfolio(self) -> None:
|
|
67
|
+
self._orders = {}
|
|
68
|
+
self._positions = {}
|
|
69
|
+
self._trades = {}
|
|
70
|
+
for symbol in self.symbols:
|
|
71
|
+
self._positions[symbol] = {}
|
|
72
|
+
self._orders[symbol] = {}
|
|
73
|
+
self._trades[symbol] = {}
|
|
74
|
+
for position in ["LONG", "SHORT"]:
|
|
75
|
+
self._trades[symbol][position] = 0
|
|
76
|
+
self._positions[symbol][position] = 0.0
|
|
77
|
+
for order in ["BLMT", "BSTP", "BSTPLMT", "SLMT", "SSTP", "SSTPLMT"]:
|
|
78
|
+
self._orders[symbol][order] = []
|
|
79
|
+
self._holdings = {s: 0.0 for s in self.symbols}
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def cash(self) -> float:
|
|
83
|
+
return self._portfolio_value or 0.0
|
|
84
|
+
|
|
85
|
+
@cash.setter
|
|
86
|
+
def cash(self, value: float) -> None:
|
|
87
|
+
self._portfolio_value = value
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
|
|
91
|
+
return self._orders
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def trades(self) -> Dict[str, Dict[str, int]]:
|
|
95
|
+
return self._trades
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def positions(self) -> Dict[str, Dict[str, Union[int, float]]]:
|
|
99
|
+
return self._positions
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def holdings(self) -> Dict[str, float]:
|
|
103
|
+
return self._holdings
|
|
104
|
+
|
|
105
|
+
def get_update_from_portfolio(
|
|
106
|
+
self, positions: Dict[str, float], holdings: Dict[str, float]
|
|
107
|
+
) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Update the positions and holdings for the strategy from the portfolio.
|
|
110
|
+
|
|
111
|
+
Positions are the number of shares of a security that are owned in long or short.
|
|
112
|
+
Holdings are the value (postions * price) of the security that are owned in long or short.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
positions : The positions for the symbols in the strategy.
|
|
116
|
+
holdings : The holdings for the symbols in the strategy.
|
|
117
|
+
"""
|
|
118
|
+
for symbol in self.symbols:
|
|
119
|
+
if symbol in positions:
|
|
120
|
+
if positions[symbol] > 0:
|
|
121
|
+
self._positions[symbol]["LONG"] = positions[symbol]
|
|
122
|
+
elif positions[symbol] < 0:
|
|
123
|
+
self._positions[symbol]["SHORT"] = positions[symbol]
|
|
124
|
+
else:
|
|
125
|
+
self._positions[symbol]["LONG"] = 0
|
|
126
|
+
self._positions[symbol]["SHORT"] = 0
|
|
127
|
+
if symbol in holdings:
|
|
128
|
+
self._holdings[symbol] = holdings[symbol]
|
|
129
|
+
|
|
130
|
+
def update_trades_from_fill(self, event: FillEvent) -> None:
|
|
131
|
+
"""
|
|
132
|
+
This method updates the trades for the strategy based on the fill event.
|
|
133
|
+
It is used to keep track of the number of trades executed for each order.
|
|
134
|
+
"""
|
|
135
|
+
if event.type == Events.FILL:
|
|
136
|
+
if event.order != "EXIT":
|
|
137
|
+
self._trades[event.symbol][event.order] += 1 # type: ignore
|
|
138
|
+
elif event.order == "EXIT" and event.direction == "BUY":
|
|
139
|
+
self._trades[event.symbol]["SHORT"] = 0
|
|
140
|
+
elif event.order == "EXIT" and event.direction == "SELL":
|
|
141
|
+
self._trades[event.symbol]["LONG"] = 0
|
|
142
|
+
|
|
143
|
+
def get_asset_values(
|
|
144
|
+
self,
|
|
145
|
+
symbol_list: List[str],
|
|
146
|
+
window: int,
|
|
147
|
+
value_type: str = "returns",
|
|
148
|
+
array: bool = True,
|
|
149
|
+
**kwargs,
|
|
150
|
+
) -> Optional[Dict[str, Union[np.ndarray, pd.Series]]]:
|
|
151
|
+
asset_values: Dict[str, Union[np.ndarray, pd.Series]] = {}
|
|
152
|
+
for asset in symbol_list:
|
|
153
|
+
if array:
|
|
154
|
+
values = self.data.get_latest_bars_values(asset, value_type, N=window)
|
|
155
|
+
asset_values[asset] = values[~np.isnan(values)]
|
|
156
|
+
else:
|
|
157
|
+
values_df = self.data.get_latest_bars(asset, N=window)
|
|
158
|
+
if isinstance(values_df, pd.DataFrame):
|
|
159
|
+
asset_values[asset] = values_df[value_type]
|
|
160
|
+
|
|
161
|
+
if all(len(values) >= window for values in asset_values.values()):
|
|
162
|
+
return {a: v[-window:] for a, v in asset_values.items()}
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
@abstractmethod
|
|
166
|
+
def calculate_signals(self, event: MarketEvent) -> None: ...
|
|
167
|
+
|
|
168
|
+
def _send_order(
|
|
169
|
+
self,
|
|
170
|
+
id: int,
|
|
171
|
+
symbol: str,
|
|
172
|
+
signal: str,
|
|
173
|
+
strength: float,
|
|
174
|
+
price: float,
|
|
175
|
+
quantity: int,
|
|
176
|
+
dtime: Union[datetime, pd.Timestamp],
|
|
177
|
+
) -> None:
|
|
178
|
+
position = SignalEvent(
|
|
179
|
+
id,
|
|
180
|
+
symbol,
|
|
181
|
+
dtime,
|
|
182
|
+
signal,
|
|
183
|
+
quantity=quantity,
|
|
184
|
+
strength=strength,
|
|
185
|
+
price=price, # type: ignore
|
|
186
|
+
)
|
|
187
|
+
log = False
|
|
188
|
+
if signal in ["LONG", "SHORT"]:
|
|
189
|
+
if self._trades[symbol][signal] < self.max_trades[symbol] and quantity > 0:
|
|
190
|
+
self.events.put(position)
|
|
191
|
+
log = True
|
|
192
|
+
elif signal == "EXIT":
|
|
193
|
+
if (
|
|
194
|
+
self._positions[symbol]["LONG"] > 0
|
|
195
|
+
or self._positions[symbol]["SHORT"] < 0
|
|
196
|
+
):
|
|
197
|
+
self.events.put(position)
|
|
198
|
+
log = True
|
|
199
|
+
if log:
|
|
200
|
+
self.logger.info(
|
|
201
|
+
f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{round(price, 5)}",
|
|
202
|
+
custom_time=dtime,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def buy_mkt(
|
|
206
|
+
self,
|
|
207
|
+
id: int,
|
|
208
|
+
symbol: str,
|
|
209
|
+
price: float,
|
|
210
|
+
quantity: int,
|
|
211
|
+
strength: float = 1.0,
|
|
212
|
+
dtime: Optional[Union[datetime, pd.Timestamp]] = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Open a long position
|
|
216
|
+
|
|
217
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
218
|
+
"""
|
|
219
|
+
if dtime is None:
|
|
220
|
+
dtime = self.get_current_dt()
|
|
221
|
+
self._send_order(id, symbol, "LONG", strength, price, quantity, dtime)
|
|
222
|
+
|
|
223
|
+
def sell_mkt(
|
|
224
|
+
self,
|
|
225
|
+
id: int,
|
|
226
|
+
symbol: str,
|
|
227
|
+
price: float,
|
|
228
|
+
quantity: int,
|
|
229
|
+
strength: float = 1.0,
|
|
230
|
+
dtime: Optional[Union[datetime, pd.Timestamp]] = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Open a short position
|
|
234
|
+
|
|
235
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
236
|
+
"""
|
|
237
|
+
if dtime is None:
|
|
238
|
+
dtime = self.get_current_dt()
|
|
239
|
+
self._send_order(id, symbol, "SHORT", strength, price, quantity, dtime)
|
|
240
|
+
|
|
241
|
+
def close_positions(
|
|
242
|
+
self,
|
|
243
|
+
id: int,
|
|
244
|
+
symbol: str,
|
|
245
|
+
price: float,
|
|
246
|
+
quantity: int,
|
|
247
|
+
strength: float = 1.0,
|
|
248
|
+
dtime: Optional[Union[datetime, pd.Timestamp]] = None,
|
|
249
|
+
) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Close a position or exit all positions
|
|
252
|
+
|
|
253
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
254
|
+
"""
|
|
255
|
+
if dtime is None:
|
|
256
|
+
dtime = self.get_current_dt()
|
|
257
|
+
self._send_order(id, symbol, "EXIT", strength, price, quantity, dtime)
|
|
258
|
+
|
|
259
|
+
def buy_stop(
|
|
260
|
+
self,
|
|
261
|
+
id: int,
|
|
262
|
+
symbol: str,
|
|
263
|
+
price: float,
|
|
264
|
+
quantity: int,
|
|
265
|
+
strength: float = 1.0,
|
|
266
|
+
dtime: Optional[Union[datetime, pd.Timestamp]] = None,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""
|
|
269
|
+
Open a pending order to buy at a stop price
|
|
270
|
+
|
|
271
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
272
|
+
"""
|
|
273
|
+
current_price = self.data.get_latest_bar_value(symbol, "close")
|
|
274
|
+
if price <= current_price:
|
|
275
|
+
raise ValueError(
|
|
276
|
+
"The buy_stop price must be greater than the current price."
|
|
277
|
+
)
|
|
278
|
+
if dtime is None:
|
|
279
|
+
dtime = self.get_current_dt()
|
|
280
|
+
order = SignalEvent(
|
|
281
|
+
id,
|
|
282
|
+
symbol,
|
|
283
|
+
dtime,
|
|
284
|
+
"LONG",
|
|
285
|
+
quantity=quantity,
|
|
286
|
+
strength=strength,
|
|
287
|
+
price=price, # type: ignore
|
|
288
|
+
)
|
|
289
|
+
self._orders[symbol]["BSTP"].append(order)
|
|
290
|
+
|
|
291
|
+
def sell_stop(
|
|
292
|
+
self,
|
|
293
|
+
id: int,
|
|
294
|
+
symbol: str,
|
|
295
|
+
price: float,
|
|
296
|
+
quantity: int,
|
|
297
|
+
strength: float = 1.0,
|
|
298
|
+
dtime: Optional[Union[datetime, pd.Timestamp]] = None,
|
|
299
|
+
) -> None:
|
|
300
|
+
"""
|
|
301
|
+
Open a pending order to sell at a stop price
|
|
302
|
+
|
|
303
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
304
|
+
"""
|
|
305
|
+
current_price = self.data.get_latest_bar_value(symbol, "close")
|
|
306
|
+
if price >= current_price:
|
|
307
|
+
raise ValueError("The sell_stop price must be less than the current price.")
|
|
308
|
+
if dtime is None:
|
|
309
|
+
dtime = self.get_current_dt()
|
|
310
|
+
order = SignalEvent(
|
|
311
|
+
id,
|
|
312
|
+
symbol,
|
|
313
|
+
dtime, # type: ignore
|
|
314
|
+
"SHORT",
|
|
315
|
+
quantity=quantity,
|
|
316
|
+
strength=strength,
|
|
317
|
+
price=price,
|
|
318
|
+
)
|
|
319
|
+
self._orders[symbol]["SSTP"].append(order)
|
|
320
|
+
|
|
321
|
+
def buy_limit(
|
|
322
|
+
self,
|
|
323
|
+
id: int,
|
|
324
|
+
symbol: str,
|
|
325
|
+
price: float,
|
|
326
|
+
quantity: int,
|
|
327
|
+
strength: float = 1.0,
|
|
328
|
+
dtime: Optional[Union[datetime, pd.Timestamp]] = None,
|
|
329
|
+
) -> None:
|
|
330
|
+
"""
|
|
331
|
+
Open a pending order to buy at a limit price
|
|
332
|
+
|
|
333
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
334
|
+
"""
|
|
335
|
+
current_price = self.data.get_latest_bar_value(symbol, "close")
|
|
336
|
+
if price >= current_price:
|
|
337
|
+
raise ValueError("The buy_limit price must be less than the current price.")
|
|
338
|
+
if dtime is None:
|
|
339
|
+
dtime = self.get_current_dt()
|
|
340
|
+
order = SignalEvent(
|
|
341
|
+
id,
|
|
342
|
+
symbol,
|
|
343
|
+
dtime,
|
|
344
|
+
"LONG",
|
|
345
|
+
quantity=quantity,
|
|
346
|
+
strength=strength,
|
|
347
|
+
price=price, # type: ignore
|
|
348
|
+
)
|
|
349
|
+
self._orders[symbol]["BLMT"].append(order)
|
|
350
|
+
|
|
351
|
+
def sell_limit(
|
|
352
|
+
self,
|
|
353
|
+
id: int,
|
|
354
|
+
symbol: str,
|
|
355
|
+
price: float,
|
|
356
|
+
quantity: int,
|
|
357
|
+
strength: float = 1.0,
|
|
358
|
+
dtime: Optional[Union[datetime, pd.Timestamp]] = None,
|
|
359
|
+
) -> None:
|
|
360
|
+
"""
|
|
361
|
+
Open a pending order to sell at a limit price
|
|
362
|
+
|
|
363
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
364
|
+
"""
|
|
365
|
+
current_price = self.data.get_latest_bar_value(symbol, "close")
|
|
366
|
+
if price <= current_price:
|
|
367
|
+
raise ValueError(
|
|
368
|
+
"The sell_limit price must be greater than the current price."
|
|
369
|
+
)
|
|
370
|
+
if dtime is None:
|
|
371
|
+
dtime = self.get_current_dt()
|
|
372
|
+
order = SignalEvent(
|
|
373
|
+
id,
|
|
374
|
+
symbol,
|
|
375
|
+
dtime, # type: ignore
|
|
376
|
+
"SHORT",
|
|
377
|
+
quantity=quantity,
|
|
378
|
+
strength=strength,
|
|
379
|
+
price=price,
|
|
380
|
+
)
|
|
381
|
+
self._orders[symbol]["SLMT"].append(order)
|
|
382
|
+
|
|
383
|
+
def buy_stop_limit(
|
|
384
|
+
self,
|
|
385
|
+
id: int,
|
|
386
|
+
symbol: str,
|
|
387
|
+
price: float,
|
|
388
|
+
stoplimit: float,
|
|
389
|
+
quantity: int,
|
|
390
|
+
strength: float = 1.0,
|
|
391
|
+
dtime: Optional[Union[datetime, pd.Timestamp]] = None,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""
|
|
394
|
+
Open a pending order to buy at a stop-limit price
|
|
395
|
+
|
|
396
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
397
|
+
"""
|
|
398
|
+
current_price = self.data.get_latest_bar_value(symbol, "close")
|
|
399
|
+
if price <= current_price:
|
|
400
|
+
raise ValueError(
|
|
401
|
+
f"The stop price {price} must be greater than the current price {current_price}."
|
|
402
|
+
)
|
|
403
|
+
if price >= stoplimit:
|
|
404
|
+
raise ValueError(
|
|
405
|
+
f"The stop-limit price {stoplimit} must be greater than the price {price}."
|
|
406
|
+
)
|
|
407
|
+
if dtime is None:
|
|
408
|
+
dtime = self.get_current_dt()
|
|
409
|
+
order = SignalEvent(
|
|
410
|
+
id,
|
|
411
|
+
symbol,
|
|
412
|
+
dtime, # type: ignore
|
|
413
|
+
"LONG",
|
|
414
|
+
quantity=quantity,
|
|
415
|
+
strength=strength,
|
|
416
|
+
price=price,
|
|
417
|
+
stoplimit=stoplimit,
|
|
418
|
+
)
|
|
419
|
+
self._orders[symbol]["BSTPLMT"].append(order)
|
|
420
|
+
|
|
421
|
+
def sell_stop_limit(
|
|
422
|
+
self,
|
|
423
|
+
id: int,
|
|
424
|
+
symbol: str,
|
|
425
|
+
price: float,
|
|
426
|
+
stoplimit: float,
|
|
427
|
+
quantity: int,
|
|
428
|
+
strength: float = 1.0,
|
|
429
|
+
dtime: Optional[Union[datetime, pd.Timestamp]] = None,
|
|
430
|
+
) -> None:
|
|
431
|
+
"""
|
|
432
|
+
Open a pending order to sell at a stop-limit price
|
|
433
|
+
|
|
434
|
+
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
435
|
+
"""
|
|
436
|
+
current_price = self.data.get_latest_bar_value(symbol, "close")
|
|
437
|
+
if price >= current_price:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
f"The stop price {price} must be less than the current price {current_price}."
|
|
440
|
+
)
|
|
441
|
+
if price <= stoplimit:
|
|
442
|
+
raise ValueError(
|
|
443
|
+
f"The stop-limit price {stoplimit} must be less than the price {price}."
|
|
444
|
+
)
|
|
445
|
+
if dtime is None:
|
|
446
|
+
dtime = self.get_current_dt()
|
|
447
|
+
order = SignalEvent(
|
|
448
|
+
id,
|
|
449
|
+
symbol,
|
|
450
|
+
dtime, # type: ignore
|
|
451
|
+
"SHORT",
|
|
452
|
+
quantity=quantity,
|
|
453
|
+
strength=strength,
|
|
454
|
+
price=price,
|
|
455
|
+
stoplimit=stoplimit,
|
|
456
|
+
)
|
|
457
|
+
self._orders[symbol]["SSTPLMT"].append(order)
|
|
458
|
+
|
|
459
|
+
def check_pending_orders(self) -> None:
|
|
460
|
+
"""
|
|
461
|
+
Check for pending orders and handle them accordingly.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
def logmsg(
|
|
465
|
+
order: SignalEvent,
|
|
466
|
+
type: str,
|
|
467
|
+
symbol: str,
|
|
468
|
+
dtime: Union[datetime, pd.Timestamp],
|
|
469
|
+
) -> None:
|
|
470
|
+
self.logger.info(
|
|
471
|
+
f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
472
|
+
f"PRICE @ {round(order.price, 5)}", # type: ignore
|
|
473
|
+
custom_time=dtime,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
def process_orders(
|
|
477
|
+
order_type: str,
|
|
478
|
+
condition: Callable[[SignalEvent], bool],
|
|
479
|
+
execute_fn: Callable[[SignalEvent], None],
|
|
480
|
+
log_label: str,
|
|
481
|
+
symbol: str,
|
|
482
|
+
dtime: Union[datetime, pd.Timestamp],
|
|
483
|
+
) -> None:
|
|
484
|
+
for order in self._orders[symbol][order_type].copy():
|
|
485
|
+
if condition(order):
|
|
486
|
+
execute_fn(order)
|
|
487
|
+
try:
|
|
488
|
+
self._orders[symbol][order_type].remove(order)
|
|
489
|
+
assert order not in self._orders[symbol][order_type]
|
|
490
|
+
except AssertionError:
|
|
491
|
+
self._orders[symbol][order_type] = [
|
|
492
|
+
o for o in self._orders[symbol][order_type] if o != order
|
|
493
|
+
]
|
|
494
|
+
logmsg(order, log_label, symbol, dtime)
|
|
495
|
+
|
|
496
|
+
for symbol in self.symbols:
|
|
497
|
+
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
498
|
+
latest_close = self.data.get_latest_bar_value(symbol, "close")
|
|
499
|
+
|
|
500
|
+
process_orders(
|
|
501
|
+
"BLMT",
|
|
502
|
+
lambda o: latest_close <= o.price, # type: ignore
|
|
503
|
+
lambda o: self.buy_mkt(
|
|
504
|
+
o.strategy_id,
|
|
505
|
+
symbol,
|
|
506
|
+
o.price,
|
|
507
|
+
o.quantity,
|
|
508
|
+
dtime=dtime, # type: ignore
|
|
509
|
+
),
|
|
510
|
+
"BUY LIMIT",
|
|
511
|
+
symbol,
|
|
512
|
+
dtime,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
process_orders(
|
|
516
|
+
"SLMT",
|
|
517
|
+
lambda o: latest_close >= o.price, # type: ignore
|
|
518
|
+
lambda o: self.sell_mkt(
|
|
519
|
+
o.strategy_id,
|
|
520
|
+
symbol,
|
|
521
|
+
o.price,
|
|
522
|
+
o.quantity,
|
|
523
|
+
dtime=dtime, # type: ignore
|
|
524
|
+
),
|
|
525
|
+
"SELL LIMIT",
|
|
526
|
+
symbol,
|
|
527
|
+
dtime,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
process_orders(
|
|
531
|
+
"BSTP",
|
|
532
|
+
lambda o: latest_close >= o.price, # type: ignore
|
|
533
|
+
lambda o: self.buy_mkt(
|
|
534
|
+
o.strategy_id,
|
|
535
|
+
symbol,
|
|
536
|
+
o.price,
|
|
537
|
+
o.quantity,
|
|
538
|
+
dtime=dtime, # type: ignore
|
|
539
|
+
),
|
|
540
|
+
"BUY STOP",
|
|
541
|
+
symbol,
|
|
542
|
+
dtime,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
process_orders(
|
|
546
|
+
"SSTP",
|
|
547
|
+
lambda o: latest_close <= o.price, # type: ignore
|
|
548
|
+
lambda o: self.sell_mkt(
|
|
549
|
+
o.strategy_id,
|
|
550
|
+
symbol,
|
|
551
|
+
o.price,
|
|
552
|
+
o.quantity,
|
|
553
|
+
dtime=dtime, # type: ignore
|
|
554
|
+
),
|
|
555
|
+
"SELL STOP",
|
|
556
|
+
symbol,
|
|
557
|
+
dtime,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
process_orders(
|
|
561
|
+
"BSTPLMT",
|
|
562
|
+
lambda o: latest_close >= o.price, # type: ignore
|
|
563
|
+
lambda o: self.buy_limit(
|
|
564
|
+
o.strategy_id,
|
|
565
|
+
symbol,
|
|
566
|
+
o.stoplimit,
|
|
567
|
+
o.quantity,
|
|
568
|
+
dtime=dtime, # type: ignore
|
|
569
|
+
),
|
|
570
|
+
"BUY STOP LIMIT",
|
|
571
|
+
symbol,
|
|
572
|
+
dtime,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
process_orders(
|
|
576
|
+
"SSTPLMT",
|
|
577
|
+
lambda o: latest_close <= o.price, # type: ignore
|
|
578
|
+
lambda o: self.sell_limit(
|
|
579
|
+
o.strategy_id,
|
|
580
|
+
symbol,
|
|
581
|
+
o.stoplimit,
|
|
582
|
+
o.quantity,
|
|
583
|
+
dtime=dtime, # type: ignore
|
|
584
|
+
),
|
|
585
|
+
"SELL STOP LIMIT",
|
|
586
|
+
symbol,
|
|
587
|
+
dtime,
|
|
588
|
+
)
|
bbstrader/compat.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_mock_modules() -> None:
|
|
7
|
+
"""Mock some modules not available on some OS to prevent import errors."""
|
|
8
|
+
from unittest.mock import MagicMock
|
|
9
|
+
|
|
10
|
+
class Mock(MagicMock):
|
|
11
|
+
@classmethod
|
|
12
|
+
def __getattr__(cls, name: str) -> Any:
|
|
13
|
+
return MagicMock()
|
|
14
|
+
|
|
15
|
+
MOCK_MODULES = []
|
|
16
|
+
|
|
17
|
+
# Mock Metatrader5 on Linux and MacOS
|
|
18
|
+
if platform.system() != "Windows":
|
|
19
|
+
MOCK_MODULES.append("MetaTrader5")
|
|
20
|
+
|
|
21
|
+
# Mock posix On windows
|
|
22
|
+
if platform.system() == "Windows":
|
|
23
|
+
MOCK_MODULES.append("posix")
|
|
24
|
+
|
|
25
|
+
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
setup_mock_modules()
|
bbstrader/config.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_config_dir(name: str = ".bbstrader") -> Path:
|
|
8
|
+
"""
|
|
9
|
+
Get the path to the configuration directory.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
name: The name of the configuration directory.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
The path to the configuration directory.
|
|
16
|
+
"""
|
|
17
|
+
home_dir = Path.home() / name
|
|
18
|
+
if not home_dir.exists():
|
|
19
|
+
home_dir.mkdir()
|
|
20
|
+
return home_dir
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
BBSTRADER_DIR = get_config_dir()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LogLevelFilter(logging.Filter):
|
|
27
|
+
def __init__(self, levels: List[int]) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initializes the filter with specific logging levels.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
levels: A list of logging level values (integers) to include.
|
|
33
|
+
"""
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.levels = levels
|
|
36
|
+
|
|
37
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
38
|
+
"""
|
|
39
|
+
Filters log records based on their level.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
record: The log record to check.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True if the record's level is in the allowed levels, False otherwise.
|
|
46
|
+
"""
|
|
47
|
+
return record.levelno in self.levels
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CustomFormatter(logging.Formatter):
|
|
51
|
+
def formatTime(
|
|
52
|
+
self, record: logging.LogRecord, datefmt: Optional[str] = None
|
|
53
|
+
) -> str:
|
|
54
|
+
if hasattr(record, "custom_time"):
|
|
55
|
+
# Use the custom time if provided
|
|
56
|
+
record.created = record.custom_time.timestamp() # type: ignore
|
|
57
|
+
return super().formatTime(record, datefmt)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CustomLogger(logging.Logger):
|
|
61
|
+
def __init__(self, name: str, level: int = logging.NOTSET) -> None:
|
|
62
|
+
super().__init__(name, level)
|
|
63
|
+
|
|
64
|
+
def log(
|
|
65
|
+
self,
|
|
66
|
+
level: int,
|
|
67
|
+
msg: object,
|
|
68
|
+
*args: object,
|
|
69
|
+
custom_time: Optional[datetime] = None,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> None:
|
|
72
|
+
if custom_time:
|
|
73
|
+
if "extra" not in kwargs or kwargs["extra"] is None:
|
|
74
|
+
kwargs["extra"] = {}
|
|
75
|
+
kwargs["extra"]["custom_time"] = custom_time
|
|
76
|
+
super().log(level, msg, *args, **kwargs)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def config_logger(log_file: str, console_log: bool = True) -> logging.Logger:
|
|
80
|
+
logging.setLoggerClass(CustomLogger)
|
|
81
|
+
logger = logging.getLogger(__name__)
|
|
82
|
+
logger.setLevel(logging.DEBUG)
|
|
83
|
+
|
|
84
|
+
file_handler = logging.FileHandler(log_file)
|
|
85
|
+
file_handler.setLevel(logging.INFO)
|
|
86
|
+
|
|
87
|
+
formatter = CustomFormatter(
|
|
88
|
+
"%(asctime)s - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
89
|
+
)
|
|
90
|
+
file_handler.setFormatter(formatter)
|
|
91
|
+
|
|
92
|
+
logger.addHandler(file_handler)
|
|
93
|
+
|
|
94
|
+
if console_log:
|
|
95
|
+
console_handler = logging.StreamHandler()
|
|
96
|
+
console_handler.setLevel(logging.DEBUG)
|
|
97
|
+
console_handler.setFormatter(formatter)
|
|
98
|
+
logger.addHandler(console_handler)
|
|
99
|
+
|
|
100
|
+
return logger
|