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,358 @@
|
|
|
1
|
+
import queue
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, List, Literal, Optional, Type
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from tabulate import tabulate
|
|
8
|
+
|
|
9
|
+
from bbstrader.btengine.data import DataHandler
|
|
10
|
+
from bbstrader.btengine.event import Events
|
|
11
|
+
from bbstrader.btengine.execution import ExecutionHandler, SimExecutionHandler
|
|
12
|
+
from bbstrader.btengine.portfolio import Portfolio
|
|
13
|
+
from bbstrader.core.strategy import Strategy
|
|
14
|
+
|
|
15
|
+
__all__ = ["BacktestEngine", "run_backtest"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BacktestEngine:
|
|
19
|
+
"""
|
|
20
|
+
The `BacktestEngine()` object encapsulates the event-handling logic and essentially
|
|
21
|
+
ties together all of the other classes.
|
|
22
|
+
|
|
23
|
+
The BacktestEngine object is designed to carry out a nested while-loop event-driven system
|
|
24
|
+
in order to handle the events placed on the `Event` Queue object.
|
|
25
|
+
The outer while-loop is known as the "heartbeat loop" and decides the temporal resolution of
|
|
26
|
+
the backtesting system. In a live environment this value will be a positive number,
|
|
27
|
+
such as 600 seconds (every ten minutes). Thus the market data and positions
|
|
28
|
+
will only be updated on this timeframe.
|
|
29
|
+
|
|
30
|
+
For the backtester described here the "heartbeat" can be set to zero,
|
|
31
|
+
irrespective of the strategy frequency, since the data is already available by virtue of
|
|
32
|
+
the fact it is historical! We can run the backtest at whatever speed we like,
|
|
33
|
+
since the event-driven system is agnostic to when the data became available,
|
|
34
|
+
so long as it has an associated timestamp.
|
|
35
|
+
|
|
36
|
+
The inner while-loop actually processes the signals and sends them to the correct
|
|
37
|
+
component depending upon the event type. Thus the Event Queue is continually being
|
|
38
|
+
populated and depopulated with events. This is what it means for a system to be event-driven.
|
|
39
|
+
|
|
40
|
+
The initialisation of the BacktestEngine object requires the full `symbol list` of traded symbols,
|
|
41
|
+
the `initial capital`, the `heartbeat` time in milliseconds, the `start datetime` stamp
|
|
42
|
+
of the backtest as well as the `DataHandler`, `ExecutionHandler`, `Strategy` objects
|
|
43
|
+
and additionnal `kwargs` based on the `ExecutionHandler`, the `DataHandler`, and the `Strategy` used.
|
|
44
|
+
|
|
45
|
+
A Queue is used to hold the events. The signals, orders and fills are counted.
|
|
46
|
+
For a `MarketEvent`, the `Strategy` object is told to recalculate new signals,
|
|
47
|
+
while the `Portfolio` object is told to reindex the time. If a `SignalEvent`
|
|
48
|
+
object is received the `Portfolio` is told to handle the new signal and convert it into a
|
|
49
|
+
set of `OrderEvents`, if appropriate. If an `OrderEvent` is received the `ExecutionHandler`
|
|
50
|
+
is sent the order to be transmitted to the broker (if in a real trading setting).
|
|
51
|
+
Finally, if a `FillEvent` is received, the Portfolio will update itself to be aware of
|
|
52
|
+
the new positions.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
symbol_list: List[str],
|
|
59
|
+
initial_capital: float,
|
|
60
|
+
heartbeat: float,
|
|
61
|
+
start_date: datetime,
|
|
62
|
+
data_handler: Type[DataHandler],
|
|
63
|
+
execution_handler: Type[ExecutionHandler],
|
|
64
|
+
strategy: Type[Strategy],
|
|
65
|
+
/,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Initialises the backtest.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
symbol_list (List[str]): The list of symbol strings.
|
|
73
|
+
intial_capital (float): The starting capital for the portfolio.
|
|
74
|
+
heartbeat (float): Backtest "heartbeat" in seconds
|
|
75
|
+
start_date (datetime): The start datetime of the strategy.
|
|
76
|
+
data_handler (DataHandler) : Handles the market data feed.
|
|
77
|
+
execution_handler (ExecutionHandler) : Handles the orders/fills for trades.
|
|
78
|
+
strategy (Strategy): Generates signals based on market data.
|
|
79
|
+
kwargs : Additional parameters based on the `ExecutionHandler`,
|
|
80
|
+
the `DataHandler`, the `Strategy` used and the `Portfolio`.
|
|
81
|
+
- show_equity (bool): Show the equity curve of the portfolio.
|
|
82
|
+
- stats_file (str): File to save the summary stats.
|
|
83
|
+
"""
|
|
84
|
+
self.symbol_list = symbol_list
|
|
85
|
+
self.initial_capital = initial_capital
|
|
86
|
+
self.heartbeat = heartbeat
|
|
87
|
+
self.start_date = start_date
|
|
88
|
+
|
|
89
|
+
self.dh_cls = data_handler
|
|
90
|
+
self.eh_cls = execution_handler
|
|
91
|
+
self.strategy_cls = strategy
|
|
92
|
+
self.kwargs = kwargs
|
|
93
|
+
|
|
94
|
+
self.events: "queue.Queue[Events]" = queue.Queue()
|
|
95
|
+
|
|
96
|
+
self.signals = 0
|
|
97
|
+
self.orders = 0
|
|
98
|
+
self.fills = 0
|
|
99
|
+
|
|
100
|
+
self._generate_trading_instances()
|
|
101
|
+
self.show_equity = kwargs.get("show_equity", False)
|
|
102
|
+
self.stats_file = kwargs.get("stats_file", None)
|
|
103
|
+
|
|
104
|
+
def _generate_trading_instances(self) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Generates the trading instance objects from
|
|
107
|
+
their class types.
|
|
108
|
+
"""
|
|
109
|
+
print(
|
|
110
|
+
f"\n[======= STARTING BACKTEST =======]\n"
|
|
111
|
+
f"START DATE: {self.start_date} \n"
|
|
112
|
+
f"INITIAL CAPITAL: {self.initial_capital}\n"
|
|
113
|
+
)
|
|
114
|
+
self.data_handler: DataHandler = self.dh_cls(
|
|
115
|
+
self.events, self.symbol_list, **self.kwargs
|
|
116
|
+
)
|
|
117
|
+
self.strategy: Strategy = self.strategy_cls(
|
|
118
|
+
self.events, self.symbol_list, self.data_handler, **self.kwargs
|
|
119
|
+
)
|
|
120
|
+
self.portfolio: Portfolio = Portfolio(
|
|
121
|
+
self.data_handler,
|
|
122
|
+
self.events,
|
|
123
|
+
self.start_date,
|
|
124
|
+
self.initial_capital,
|
|
125
|
+
**self.kwargs,
|
|
126
|
+
)
|
|
127
|
+
self.execution_handler: ExecutionHandler = self.eh_cls(
|
|
128
|
+
self.events, self.data_handler, **self.kwargs
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _run_backtest(self) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Executes the backtest.
|
|
134
|
+
"""
|
|
135
|
+
i = 0
|
|
136
|
+
while True:
|
|
137
|
+
i += 1
|
|
138
|
+
value = self.portfolio.all_holdings[-1]["Total"]
|
|
139
|
+
if self.data_handler.continue_backtest is True:
|
|
140
|
+
# Update the market bars
|
|
141
|
+
self.data_handler.update_bars()
|
|
142
|
+
self.strategy.check_pending_orders()
|
|
143
|
+
self.strategy.get_update_from_portfolio(
|
|
144
|
+
self.portfolio.current_positions, self.portfolio.current_holdings
|
|
145
|
+
)
|
|
146
|
+
self.strategy.cash = value
|
|
147
|
+
else:
|
|
148
|
+
print("\n[======= BACKTEST COMPLETED =======]")
|
|
149
|
+
dt = self.data_handler.get_latest_bar_datetime(self.symbol_list[0])
|
|
150
|
+
if dt:
|
|
151
|
+
print(f"END DATE: {dt}")
|
|
152
|
+
print(f"TOTAL BARS: {i} ")
|
|
153
|
+
print(f"PORFOLIO VALUE: {round(value, 2)}")
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
# Handle the events
|
|
157
|
+
while True:
|
|
158
|
+
try:
|
|
159
|
+
event = self.events.get(False)
|
|
160
|
+
except queue.Empty:
|
|
161
|
+
break
|
|
162
|
+
else:
|
|
163
|
+
if event is not None:
|
|
164
|
+
if event.type == Events.MARKET:
|
|
165
|
+
self.strategy.calculate_signals(event)
|
|
166
|
+
self.portfolio.update_timeindex(event)
|
|
167
|
+
|
|
168
|
+
elif event.type == Events.SIGNAL:
|
|
169
|
+
self.signals += 1
|
|
170
|
+
self.portfolio.update_signal(event)
|
|
171
|
+
|
|
172
|
+
elif event.type == Events.ORDER:
|
|
173
|
+
self.orders += 1
|
|
174
|
+
self.execution_handler.execute_order(event)
|
|
175
|
+
|
|
176
|
+
elif event.type == Events.FILL:
|
|
177
|
+
self.fills += 1
|
|
178
|
+
self.portfolio.update_fill(event)
|
|
179
|
+
self.strategy.update_trades_from_fill(event)
|
|
180
|
+
|
|
181
|
+
time.sleep(self.heartbeat)
|
|
182
|
+
|
|
183
|
+
def _output_performance(self) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Outputs the strategy performance from the backtest.
|
|
186
|
+
"""
|
|
187
|
+
self.portfolio.create_equity_curve_dataframe()
|
|
188
|
+
|
|
189
|
+
print("\nCreating summary stats...")
|
|
190
|
+
stats = self.portfolio.output_summary_stats()
|
|
191
|
+
print("[======= Summary Stats =======]")
|
|
192
|
+
stat2 = {}
|
|
193
|
+
stat2["Signals"] = self.signals
|
|
194
|
+
stat2["Orders"] = self.orders
|
|
195
|
+
stat2["Fills"] = self.fills
|
|
196
|
+
stats.extend(stat2.items())
|
|
197
|
+
tab_stats = tabulate(stats, headers=["Metric", "Value"], tablefmt="outline")
|
|
198
|
+
print(tab_stats, "\n")
|
|
199
|
+
if self.stats_file:
|
|
200
|
+
with open(self.stats_file, "a") as f:
|
|
201
|
+
f.write("\n[======= Summary Stats =======]\n")
|
|
202
|
+
f.write(tab_stats)
|
|
203
|
+
f.write("\n")
|
|
204
|
+
|
|
205
|
+
if self.show_equity:
|
|
206
|
+
print("\nCreating equity curve...")
|
|
207
|
+
print("\n[======= PORTFOLIO SUMMARY =======]")
|
|
208
|
+
print(
|
|
209
|
+
tabulate(
|
|
210
|
+
self.portfolio.equity_curve.tail(10),
|
|
211
|
+
headers="keys",
|
|
212
|
+
tablefmt="outline",
|
|
213
|
+
),
|
|
214
|
+
"\n",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def simulate_trading(self) -> pd.DataFrame:
|
|
218
|
+
"""
|
|
219
|
+
Simulates the backtest and outputs portfolio performance.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
pd.DataFrame: The portfilio values over time (capital, equity, returns etc.)
|
|
223
|
+
"""
|
|
224
|
+
self._run_backtest()
|
|
225
|
+
self._output_performance()
|
|
226
|
+
return self.portfolio.equity_curve
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def run_backtest(
|
|
230
|
+
symbol_list: List[str],
|
|
231
|
+
start_date: datetime,
|
|
232
|
+
data_handler: Type[DataHandler],
|
|
233
|
+
strategy: Type[Strategy],
|
|
234
|
+
exc_handler: Optional[Type[ExecutionHandler]] = None,
|
|
235
|
+
initial_capital: float = 100000.0,
|
|
236
|
+
heartbeat: float = 0.0,
|
|
237
|
+
**kwargs: Any,
|
|
238
|
+
) -> pd.DataFrame:
|
|
239
|
+
"""
|
|
240
|
+
Runs a backtest simulation based on a `DataHandler`, `Strategy`, and `ExecutionHandler`.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
symbol_list (List[str]): List of symbol strings for the assets to be backtested.
|
|
244
|
+
|
|
245
|
+
start_date (datetime): Start date of the backtest.
|
|
246
|
+
|
|
247
|
+
data_handler (DataHandler): A subclass of the `DataHandler` class, responsible for managing
|
|
248
|
+
and processing market data. Available options include `CSVDataHandler`,
|
|
249
|
+
`MT5DataHandler`, and `YFDataHandler`.
|
|
250
|
+
|
|
251
|
+
strategy (Strategy): The trading strategy to be employed during the backtest.
|
|
252
|
+
The strategy must be a subclass of `Strategy` and should include the following attributes:
|
|
253
|
+
- `bars` (DataHandler): The `DataHandler` class for the strategy.
|
|
254
|
+
- `events` (Queue): Queue instance for managing events.
|
|
255
|
+
- `symbol_list` (List[str]): List of symbols to trade.
|
|
256
|
+
- `mode` (str): 'live' or 'backtest'.
|
|
257
|
+
|
|
258
|
+
Additional parameters specific to the strategy should be passed in `**kwargs`.
|
|
259
|
+
The strategy class must implement a `calculate_signals` method to generate `SignalEvent`.
|
|
260
|
+
|
|
261
|
+
exc_handler (ExecutionHandler, optional): The execution handler for managing order executions.
|
|
262
|
+
If not provided, a `SimulatedExecutionHandler` will be used by default. This handler must
|
|
263
|
+
implement an `execute_order` method to process `OrderEvent` in the `Backtest` class.
|
|
264
|
+
|
|
265
|
+
initial_capital (float, optional): The initial capital for the portfolio in the backtest.
|
|
266
|
+
Default is 100,000.
|
|
267
|
+
|
|
268
|
+
heartbeat (float, optional): Time delay (in seconds) between iterations of the event-driven
|
|
269
|
+
backtest loop. Default is 0.0, allowing the backtest to run as fast as possible. This could
|
|
270
|
+
also be used as a time frame in live trading (e.g., 1m, 5m, 15m) with a live `DataHandler`.
|
|
271
|
+
|
|
272
|
+
**kwargs: Additional parameters passed to the `Backtest` instance, which may include strategy-specific,
|
|
273
|
+
data handler, portfolio, or execution handler options.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
pd.DataFrame: The portfolio values over time (capital, equities, returns etc.).
|
|
277
|
+
|
|
278
|
+
Notes:
|
|
279
|
+
This function generates three outputs:
|
|
280
|
+
- A performance summary saved as an HTML file.
|
|
281
|
+
- An equity curve of the portfolio saved as a CSV file.
|
|
282
|
+
- Monthly returns saved as a PNG image.
|
|
283
|
+
|
|
284
|
+
Example:
|
|
285
|
+
>>> from examples.strategies import StockIndexSTBOTrading
|
|
286
|
+
>>> from bbstrader.config import config_logger
|
|
287
|
+
>>> from bbstrader.btengine.data import MT5DataHandler
|
|
288
|
+
>>> from bbstrader.btengine.execution import MT5ExecutionHandler
|
|
289
|
+
>>> from datetime import datetime
|
|
290
|
+
>>>
|
|
291
|
+
>>> logger = config_logger('index_trade.log', console_log=True)
|
|
292
|
+
>>> symbol_list = ['[SP500]', 'GERMANY40', '[DJI30]', '[NQ100]']
|
|
293
|
+
>>> start = datetime(2010, 6, 1, 2, 0, 0)
|
|
294
|
+
>>> kwargs = {
|
|
295
|
+
... 'expected_returns': {'[NQ100]': 1.5, '[SP500]': 1.5, '[DJI30]': 1.0, 'GERMANY40': 1.0},
|
|
296
|
+
... 'quantities': {'[NQ100]': 15, '[SP500]': 30, '[DJI30]': 5, 'GERMANY40': 10},
|
|
297
|
+
... 'max_trades': {'[NQ100]': 3, '[SP500]': 3, '[DJI30]': 3, 'GERMANY40': 3},
|
|
298
|
+
... 'mt5_start': start,
|
|
299
|
+
... 'time_frame': '15m',
|
|
300
|
+
... 'strategy_name': 'SISTBO',
|
|
301
|
+
... }
|
|
302
|
+
>>> run_backtest(
|
|
303
|
+
... symbol_list=symbol_list,
|
|
304
|
+
... start_date=start,
|
|
305
|
+
... data_handler=MT5DataHandler,
|
|
306
|
+
... strategy=StockIndexSTBOTrading,
|
|
307
|
+
... exc_handler=MT5ExecutionHandler,
|
|
308
|
+
... initial_capital=100000.0,
|
|
309
|
+
... heartbeat=0.0,
|
|
310
|
+
... **kwargs
|
|
311
|
+
... )
|
|
312
|
+
"""
|
|
313
|
+
if exc_handler is None:
|
|
314
|
+
execution_handler: Type[ExecutionHandler] = SimExecutionHandler
|
|
315
|
+
else:
|
|
316
|
+
execution_handler = exc_handler
|
|
317
|
+
engine = BacktestEngine(
|
|
318
|
+
symbol_list,
|
|
319
|
+
initial_capital,
|
|
320
|
+
heartbeat,
|
|
321
|
+
start_date,
|
|
322
|
+
data_handler,
|
|
323
|
+
execution_handler,
|
|
324
|
+
strategy,
|
|
325
|
+
**kwargs,
|
|
326
|
+
)
|
|
327
|
+
portfolio = engine.simulate_trading()
|
|
328
|
+
return portfolio
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class CerebroEngine: ...
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class ZiplineEngine: ...
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def run_backtest_with(
|
|
338
|
+
engine: Literal["bbstrader", "cerebro", "zipline"], **kwargs: Any
|
|
339
|
+
) -> Optional[pd.DataFrame]:
|
|
340
|
+
""" """
|
|
341
|
+
if engine == "bbstrader":
|
|
342
|
+
return run_backtest(
|
|
343
|
+
symbol_list=kwargs.get("symbol_list"), # type: ignore
|
|
344
|
+
start_date=kwargs.get("start_date"), # type: ignore
|
|
345
|
+
data_handler=kwargs.get("data_handler"), # type: ignore
|
|
346
|
+
strategy=kwargs.get("strategy"), # type: ignore
|
|
347
|
+
exc_handler=kwargs.get("exc_handler"),
|
|
348
|
+
initial_capital=kwargs.get("initial_capital", 100000.0),
|
|
349
|
+
heartbeat=kwargs.get("heartbeat", 0.0),
|
|
350
|
+
**kwargs,
|
|
351
|
+
)
|
|
352
|
+
elif engine == "cerebro":
|
|
353
|
+
# TODO:
|
|
354
|
+
raise NotImplementedError("cerebro engine is not supported yet")
|
|
355
|
+
elif engine == "zipline":
|
|
356
|
+
# TODO:
|
|
357
|
+
raise NotImplementedError("zipline engine is not supported yet")
|
|
358
|
+
return None
|