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