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.
Files changed (45) hide show
  1. bbstrader/__init__.py +27 -0
  2. bbstrader/__main__.py +92 -0
  3. bbstrader/api/__init__.py +96 -0
  4. bbstrader/api/handlers.py +245 -0
  5. bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
  6. bbstrader/api/metatrader_client.pyi +624 -0
  7. bbstrader/assets/bbs_.png +0 -0
  8. bbstrader/assets/bbstrader.ico +0 -0
  9. bbstrader/assets/bbstrader.png +0 -0
  10. bbstrader/assets/qs_metrics_1.png +0 -0
  11. bbstrader/btengine/__init__.py +54 -0
  12. bbstrader/btengine/backtest.py +358 -0
  13. bbstrader/btengine/data.py +737 -0
  14. bbstrader/btengine/event.py +229 -0
  15. bbstrader/btengine/execution.py +287 -0
  16. bbstrader/btengine/performance.py +408 -0
  17. bbstrader/btengine/portfolio.py +393 -0
  18. bbstrader/btengine/strategy.py +588 -0
  19. bbstrader/compat.py +28 -0
  20. bbstrader/config.py +100 -0
  21. bbstrader/core/__init__.py +27 -0
  22. bbstrader/core/data.py +628 -0
  23. bbstrader/core/strategy.py +466 -0
  24. bbstrader/metatrader/__init__.py +48 -0
  25. bbstrader/metatrader/_copier.py +720 -0
  26. bbstrader/metatrader/account.py +865 -0
  27. bbstrader/metatrader/broker.py +418 -0
  28. bbstrader/metatrader/copier.py +1487 -0
  29. bbstrader/metatrader/rates.py +495 -0
  30. bbstrader/metatrader/risk.py +667 -0
  31. bbstrader/metatrader/trade.py +1692 -0
  32. bbstrader/metatrader/utils.py +402 -0
  33. bbstrader/models/__init__.py +39 -0
  34. bbstrader/models/nlp.py +932 -0
  35. bbstrader/models/optimization.py +182 -0
  36. bbstrader/scripts.py +665 -0
  37. bbstrader/trading/__init__.py +33 -0
  38. bbstrader/trading/execution.py +1159 -0
  39. bbstrader/trading/strategy.py +362 -0
  40. bbstrader/trading/utils.py +69 -0
  41. bbstrader-2.0.3.dist-info/METADATA +396 -0
  42. bbstrader-2.0.3.dist-info/RECORD +45 -0
  43. bbstrader-2.0.3.dist-info/WHEEL +5 -0
  44. bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
  45. 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