bbstrader 0.1.5__py3-none-any.whl → 0.1.7__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 +0 -1
- bbstrader/btengine/__init__.py +12 -9
- bbstrader/btengine/backtest.py +100 -702
- bbstrader/btengine/data.py +25 -12
- bbstrader/btengine/event.py +18 -11
- bbstrader/btengine/execution.py +67 -7
- bbstrader/btengine/performance.py +34 -1
- bbstrader/btengine/portfolio.py +24 -14
- bbstrader/btengine/strategy.py +4 -3
- bbstrader/metatrader/account.py +18 -6
- bbstrader/metatrader/rates.py +35 -12
- bbstrader/metatrader/trade.py +54 -38
- bbstrader/metatrader/utils.py +3 -2
- bbstrader/models/risk.py +39 -2
- bbstrader/trading/__init__.py +8 -1
- bbstrader/trading/execution.py +344 -923
- bbstrader/trading/strategies.py +838 -0
- bbstrader/tseries.py +603 -19
- {bbstrader-0.1.5.dist-info → bbstrader-0.1.7.dist-info}/METADATA +15 -7
- bbstrader-0.1.7.dist-info/RECORD +26 -0
- {bbstrader-0.1.5.dist-info → bbstrader-0.1.7.dist-info}/WHEEL +1 -1
- bbstrader/strategies.py +0 -681
- bbstrader/trading/run.py +0 -131
- bbstrader/trading/utils.py +0 -153
- bbstrader-0.1.5.dist-info/RECORD +0 -28
- {bbstrader-0.1.5.dist-info → bbstrader-0.1.7.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.5.dist-info → bbstrader-0.1.7.dist-info}/top_level.txt +0 -0
bbstrader/btengine/backtest.py
CHANGED
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
import pprint
|
|
2
2
|
import queue
|
|
3
3
|
import time
|
|
4
|
-
import numpy as np
|
|
5
|
-
import pandas as pd
|
|
6
4
|
import yfinance as yf
|
|
7
5
|
from queue import Queue
|
|
8
6
|
from datetime import datetime
|
|
9
|
-
from seaborn import saturate
|
|
10
7
|
from bbstrader.btengine.data import *
|
|
11
8
|
from bbstrader.btengine.execution import *
|
|
12
9
|
from bbstrader.btengine.portfolio import Portfolio
|
|
13
|
-
from bbstrader.btengine.strategy import Strategy
|
|
14
10
|
from bbstrader.btengine.event import SignalEvent
|
|
15
|
-
from bbstrader.
|
|
16
|
-
from filterpy.kalman import KalmanFilter
|
|
17
|
-
from bbstrader.strategies import OrnsteinUhlenbeck
|
|
18
|
-
from bbstrader.tseries import load_and_prepare_data
|
|
19
|
-
from bbstrader.tseries import get_prediction
|
|
11
|
+
from bbstrader.btengine.strategy import Strategy
|
|
20
12
|
from typing import Literal, Optional, List
|
|
13
|
+
from tabulate import tabulate
|
|
21
14
|
|
|
22
15
|
__all__ = [
|
|
23
16
|
"Backtest",
|
|
24
|
-
"
|
|
25
|
-
"KLFStrategyBacktester",
|
|
26
|
-
"OUStrategyBacktester",
|
|
27
|
-
"ArimaGarchStrategyBacktester",
|
|
17
|
+
"BacktestEngine",
|
|
28
18
|
"run_backtest"
|
|
29
19
|
]
|
|
30
20
|
|
|
31
|
-
|
|
32
21
|
class Backtest(object):
|
|
33
22
|
"""
|
|
34
23
|
The `Backtest()` object encapsulates the event-handling logic and essentially
|
|
@@ -124,7 +113,7 @@ class Backtest(object):
|
|
|
124
113
|
self.events, self.symbol_list, **self.kwargs
|
|
125
114
|
)
|
|
126
115
|
self.strategy: Strategy = self.strategy_cls(
|
|
127
|
-
self.data_handler, self.events, **self.kwargs
|
|
116
|
+
bars=self.data_handler, events=self.events, **self.kwargs
|
|
128
117
|
)
|
|
129
118
|
self.portfolio = Portfolio(
|
|
130
119
|
self.data_handler,
|
|
@@ -132,7 +121,8 @@ class Backtest(object):
|
|
|
132
121
|
self.start_date,
|
|
133
122
|
self.initial_capital, **self.kwargs
|
|
134
123
|
)
|
|
135
|
-
self.execution_handler: ExecutionHandler = self.eh_cls(
|
|
124
|
+
self.execution_handler: ExecutionHandler = self.eh_cls(
|
|
125
|
+
self.events, **self.kwargs)
|
|
136
126
|
|
|
137
127
|
def _run_backtest(self):
|
|
138
128
|
"""
|
|
@@ -172,7 +162,7 @@ class Backtest(object):
|
|
|
172
162
|
self.fills += 1
|
|
173
163
|
self.portfolio.update_fill(event)
|
|
174
164
|
|
|
175
|
-
|
|
165
|
+
time.sleep(self.heartbeat)
|
|
176
166
|
|
|
177
167
|
def _output_performance(self):
|
|
178
168
|
"""
|
|
@@ -182,17 +172,30 @@ class Backtest(object):
|
|
|
182
172
|
|
|
183
173
|
print("\nCreating summary stats...")
|
|
184
174
|
stats = self.portfolio.output_summary_stats()
|
|
185
|
-
|
|
186
|
-
print("\nCreating equity curve...")
|
|
187
|
-
print(f"{self.portfolio.equity_curve.tail(10)}\n")
|
|
188
|
-
print("==== Summary Stats ====")
|
|
189
|
-
pprint.pprint(stats)
|
|
175
|
+
print("[======= Summary Stats =======]")
|
|
190
176
|
stat2 = {}
|
|
191
177
|
stat2['Signals'] = self.signals
|
|
192
178
|
stat2['Orders'] = self.orders
|
|
193
179
|
stat2['Fills'] = self.fills
|
|
194
|
-
|
|
180
|
+
stats.extend(stat2.items())
|
|
181
|
+
print(
|
|
182
|
+
tabulate(
|
|
183
|
+
stats,
|
|
184
|
+
headers=["Metric", "Value"],
|
|
185
|
+
tablefmt="outline"),
|
|
186
|
+
"\n"
|
|
187
|
+
)
|
|
195
188
|
|
|
189
|
+
print("\nCreating equity curve...")
|
|
190
|
+
print("\n[======= EQUITY CURVE =======]")
|
|
191
|
+
print(
|
|
192
|
+
tabulate(
|
|
193
|
+
self.portfolio.equity_curve.tail(10),
|
|
194
|
+
headers="keys",
|
|
195
|
+
tablefmt="outline"),
|
|
196
|
+
"\n"
|
|
197
|
+
)
|
|
198
|
+
|
|
196
199
|
def simulate_trading(self):
|
|
197
200
|
"""
|
|
198
201
|
Simulates the backtest and outputs portfolio performance.
|
|
@@ -200,701 +203,96 @@ class Backtest(object):
|
|
|
200
203
|
self._run_backtest()
|
|
201
204
|
self._output_performance()
|
|
202
205
|
|
|
203
|
-
|
|
204
|
-
class SMAStrategyBacktester(Strategy):
|
|
205
|
-
"""
|
|
206
|
-
Carries out a basic Moving Average Crossover strategy bactesting with a
|
|
207
|
-
short/long simple weighted moving average. Default short/long
|
|
208
|
-
windows are 50/200 periods respectively and uses Hiden Markov Model
|
|
209
|
-
as risk Managment system for filteering signals.
|
|
210
|
-
|
|
211
|
-
The trading strategy for this class is exceedingly simple and is used to bettter
|
|
212
|
-
understood. The important issue is the risk management aspect (the Hmm model)
|
|
213
|
-
|
|
214
|
-
The Long-term trend following strategy is of the classic moving average crossover type.
|
|
215
|
-
The rules are simple:
|
|
216
|
-
- At every bar calculate the 50-day and 200-day simple moving averages (SMA)
|
|
217
|
-
- If the 50-day SMA exceeds the 200-day SMA and the strategy is not invested, then go long
|
|
218
|
-
- If the 200-day SMA exceeds the 50-day SMA and the strategy is invested, then close the position
|
|
219
|
-
"""
|
|
220
|
-
|
|
221
|
-
def __init__(
|
|
222
|
-
self, bars: DataHandler, events: Queue, /, **kwargs
|
|
223
|
-
):
|
|
224
|
-
"""
|
|
225
|
-
Args:
|
|
226
|
-
bars (DataHandler): A data handler object that provides market data.
|
|
227
|
-
events (Queue): An event queue object where generated signals are placed.
|
|
228
|
-
short_window (int, optional): The period for the short moving average.
|
|
229
|
-
long_window (int, optional): The period for the long moving average.
|
|
230
|
-
hmm_model (optional): The risk management model to be used.
|
|
231
|
-
quantity (int, optional): The default quantity of assets to trade.
|
|
232
|
-
"""
|
|
233
|
-
self.bars = bars
|
|
234
|
-
self.symbol_list = self.bars.symbol_list
|
|
235
|
-
self.events = events
|
|
236
|
-
|
|
237
|
-
self.short_window = kwargs.get("short_window", 50)
|
|
238
|
-
self.long_window = kwargs.get("long_window", 200)
|
|
239
|
-
self.hmm_model = kwargs.get("hmm_model")
|
|
240
|
-
self.qty = kwargs.get("quantity", 100)
|
|
241
|
-
|
|
242
|
-
self.bought = self._calculate_initial_bought()
|
|
243
|
-
|
|
244
|
-
def _calculate_initial_bought(self):
|
|
245
|
-
bought = {}
|
|
246
|
-
for s in self.symbol_list:
|
|
247
|
-
bought[s] = 'OUT'
|
|
248
|
-
return bought
|
|
249
|
-
|
|
250
|
-
def get_data(self):
|
|
251
|
-
for s in self.symbol_list:
|
|
252
|
-
bar_date = self.bars.get_latest_bar_datetime(s)
|
|
253
|
-
bars = self.bars.get_latest_bars_values(
|
|
254
|
-
s, "Adj Close", N=self.long_window
|
|
255
|
-
)
|
|
256
|
-
returns_val = self.bars.get_latest_bars_values(
|
|
257
|
-
s, "Returns", N=self.long_window
|
|
258
|
-
)
|
|
259
|
-
if len(bars) >= self.long_window and len(returns_val) >= self.long_window:
|
|
260
|
-
regime = self.hmm_model.which_trade_allowed(returns_val)
|
|
261
|
-
|
|
262
|
-
short_sma = np.mean(bars[-self.short_window:])
|
|
263
|
-
long_sma = np.mean(bars[-self.long_window:])
|
|
264
|
-
|
|
265
|
-
return short_sma, long_sma, regime, s, bar_date
|
|
266
|
-
else:
|
|
267
|
-
return None
|
|
268
|
-
|
|
269
|
-
def create_signal(self):
|
|
270
|
-
signal = None
|
|
271
|
-
data = self.get_data()
|
|
272
|
-
if data is not None:
|
|
273
|
-
short_sma, long_sma, regime, s, bar_date = data
|
|
274
|
-
dt = bar_date
|
|
275
|
-
if regime == "LONG":
|
|
276
|
-
# Bulliqh regime
|
|
277
|
-
if short_sma < long_sma and self.bought[s] == "LONG":
|
|
278
|
-
print(f"EXIT: {bar_date}")
|
|
279
|
-
signal = SignalEvent(1, s, dt, 'EXIT')
|
|
280
|
-
self.bought[s] = 'OUT'
|
|
281
|
-
|
|
282
|
-
elif short_sma > long_sma and self.bought[s] == "OUT":
|
|
283
|
-
print(f"LONG: {bar_date}")
|
|
284
|
-
signal = SignalEvent(
|
|
285
|
-
1, s, dt, 'LONG', quantity=self.qty)
|
|
286
|
-
self.bought[s] = 'LONG'
|
|
287
|
-
|
|
288
|
-
elif regime == "SHORT":
|
|
289
|
-
# Bearish regime
|
|
290
|
-
if short_sma > long_sma and self.bought[s] == "SHORT":
|
|
291
|
-
print(f"EXIT: {bar_date}")
|
|
292
|
-
signal = SignalEvent(1, s, dt, 'EXIT')
|
|
293
|
-
self.bought[s] = 'OUT'
|
|
294
|
-
|
|
295
|
-
elif short_sma < long_sma and self.bought[s] == "OUT":
|
|
296
|
-
print(f"SHORT: {bar_date}")
|
|
297
|
-
signal = SignalEvent(
|
|
298
|
-
1, s, dt, 'SHORT', quantity=self.qty)
|
|
299
|
-
self.bought[s] = 'SHORT'
|
|
300
|
-
return signal
|
|
301
|
-
|
|
302
|
-
def calculate_signals(self, event):
|
|
303
|
-
if event.type == 'MARKET':
|
|
304
|
-
signal = self.create_signal()
|
|
305
|
-
if signal is not None:
|
|
306
|
-
self.events.put(signal)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
class KLFStrategyBacktester(Strategy):
|
|
310
|
-
"""
|
|
311
|
-
The `KLFStrategyBacktester` class implements a backtesting framework for a
|
|
312
|
-
[pairs trading](https://en.wikipedia.org/wiki/Pairs_trade) strategy using
|
|
313
|
-
Kalman Filter for signals and Hidden Markov Models (HMM) for risk management.
|
|
314
|
-
This document outlines the structure and usage of the `KLFStrategyBacktester`,
|
|
315
|
-
including initialization parameters, main functions, and an example of how to run a backtest.
|
|
316
|
-
"""
|
|
317
|
-
|
|
318
|
-
def __init__(
|
|
319
|
-
self,
|
|
320
|
-
bars: DataHandler, events_queue: Queue, **kwargs
|
|
321
|
-
):
|
|
322
|
-
"""
|
|
323
|
-
Args:
|
|
324
|
-
`bars`: `DataHandler` for market data handling.
|
|
325
|
-
`events_queue`: A queue for managing events.
|
|
326
|
-
kwargs : Additional keyword arguments including
|
|
327
|
-
- `tickers`: List of ticker symbols involved in the pairs trading strategy.
|
|
328
|
-
- `quantity`: Quantity of assets to trade. Default is 100.
|
|
329
|
-
- `delta`: Delta parameter for the Kalman Filter. Default is `1e-4`.
|
|
330
|
-
- `vt`: Measurement noise covariance for the Kalman Filter. Default is `1e-3`.
|
|
331
|
-
- `hmm_model`: Instance of `HMMRiskManager` for managing trading risks.
|
|
332
|
-
- `hmm_window`: Window size for calculating returns for the HMM. Default is 50.
|
|
333
|
-
- `hmm_tiker`: Ticker symbol used by the HMM for risk management.
|
|
334
|
-
"""
|
|
335
|
-
self.bars = bars
|
|
336
|
-
self.symbol_list = self.bars.symbol_list
|
|
337
|
-
self.events_queue = events_queue
|
|
338
|
-
|
|
339
|
-
self.tickers = kwargs.get("tickers")
|
|
340
|
-
self.hmm_tiker = kwargs.get("hmm_tiker")
|
|
341
|
-
self.hmm_model = kwargs.get("hmm_model")
|
|
342
|
-
self._assert_tikers()
|
|
343
|
-
self.hmm_window = kwargs.get("hmm_window", 50)
|
|
344
|
-
self.qty = kwargs.get("quantity", 100)
|
|
345
|
-
|
|
346
|
-
self.latest_prices = np.array([-1.0, -1.0])
|
|
347
|
-
self.delta = kwargs.get("delta", 1e-4)
|
|
348
|
-
self.wt = self.delta/(1-self.delta) * np.eye(2)
|
|
349
|
-
self.vt = kwargs.get("vt", 1e-3)
|
|
350
|
-
self.theta = np.zeros(2)
|
|
351
|
-
self.P = np.zeros((2, 2))
|
|
352
|
-
self.R = None
|
|
353
|
-
self.kf = self._init_kalman()
|
|
354
|
-
|
|
355
|
-
self.long_market = False
|
|
356
|
-
self.short_market = False
|
|
357
|
-
|
|
358
|
-
def _assert_tikers(self):
|
|
359
|
-
if self.tickers is None:
|
|
360
|
-
raise ValueError(
|
|
361
|
-
"A list of 2 Tickers must be provide for this strategy")
|
|
362
|
-
if self.hmm_tiker is None:
|
|
363
|
-
raise ValueError(
|
|
364
|
-
"You need to provide a ticker used by the HMM for risk management")
|
|
365
|
-
|
|
366
|
-
def _init_kalman(self):
|
|
367
|
-
kf = KalmanFilter(dim_x=2, dim_z=1)
|
|
368
|
-
kf.x = np.zeros((2, 1)) # Initial state
|
|
369
|
-
kf.P = self.P # Initial covariance
|
|
370
|
-
kf.F = np.eye(2) # State transition matrix
|
|
371
|
-
kf.Q = self.wt # Process noise covariance
|
|
372
|
-
kf.R = 1. # Scalar measurement noise covariance
|
|
373
|
-
|
|
374
|
-
return kf
|
|
375
|
-
|
|
376
|
-
def calc_slope_intercep(self, prices):
|
|
377
|
-
kf = self.kf
|
|
378
|
-
kf.H = np.array([[prices[1], 1.0]])
|
|
379
|
-
kf.predict()
|
|
380
|
-
kf.update(prices[0])
|
|
381
|
-
slope = kf.x.copy().flatten()[0]
|
|
382
|
-
intercept = kf.x.copy().flatten()[1]
|
|
383
|
-
|
|
384
|
-
return slope, intercept
|
|
385
|
-
|
|
386
|
-
def calculate_xy_signals(self, et, sqrt_Qt, regime, dt):
|
|
387
|
-
# Make sure there is no position open
|
|
388
|
-
if et >= -sqrt_Qt and self.long_market:
|
|
389
|
-
print("CLOSING LONG: %s" % dt)
|
|
390
|
-
y_signal = SignalEvent(1, self.tickers[1], dt, "EXIT")
|
|
391
|
-
x_signal = SignalEvent(1, self.tickers[0], dt, "EXIT")
|
|
392
|
-
self.events_queue.put(y_signal)
|
|
393
|
-
self.events_queue.put(x_signal)
|
|
394
|
-
self.long_market = False
|
|
395
|
-
|
|
396
|
-
elif et <= sqrt_Qt and self.short_market:
|
|
397
|
-
print("CLOSING SHORT: %s" % dt)
|
|
398
|
-
y_signal = SignalEvent(1, self.tickers[1], dt, "EXIT")
|
|
399
|
-
x_signal = SignalEvent(1, self.tickers[0], dt, "EXIT")
|
|
400
|
-
self.events_queue.put(y_signal)
|
|
401
|
-
self.events_queue.put(x_signal)
|
|
402
|
-
self.short_market = False
|
|
403
|
-
|
|
404
|
-
# Long Entry
|
|
405
|
-
if regime == "LONG":
|
|
406
|
-
if et <= -sqrt_Qt and not self.long_market:
|
|
407
|
-
print("LONG: %s" % dt)
|
|
408
|
-
y_signal = SignalEvent(
|
|
409
|
-
1, self.tickers[1], dt, "LONG", self.qty, 1.0)
|
|
410
|
-
x_signal = SignalEvent(
|
|
411
|
-
1, self.tickers[0], dt, "SHORT", self.qty, self.theta[0])
|
|
412
|
-
self.events_queue.put(y_signal)
|
|
413
|
-
self.events_queue.put(x_signal)
|
|
414
|
-
self.long_market = True
|
|
415
|
-
|
|
416
|
-
# Short Entry
|
|
417
|
-
elif regime == "SHORT":
|
|
418
|
-
if et >= sqrt_Qt and not self.short_market:
|
|
419
|
-
print("SHORT: %s" % dt)
|
|
420
|
-
y_signal = SignalEvent(
|
|
421
|
-
1, self.tickers[1], dt, "SHORT", self.qty, 1.0)
|
|
422
|
-
x_signal = SignalEvent(
|
|
423
|
-
1, self.tickers[0], "LONG", self.qty, self.theta[0])
|
|
424
|
-
self.events_queue.put(y_signal)
|
|
425
|
-
self.events_queue.put(x_signal)
|
|
426
|
-
self.short_market = True
|
|
427
|
-
|
|
428
|
-
def calculate_signals_for_pairs(self):
|
|
429
|
-
p0, p1 = self.tickers[0], self.tickers[1]
|
|
430
|
-
dt = self.bars.get_latest_bar_datetime(p0)
|
|
431
|
-
_x = self.bars.get_latest_bars_values(
|
|
432
|
-
p0, "Close", N=1
|
|
433
|
-
)
|
|
434
|
-
_y = self.bars.get_latest_bars_values(
|
|
435
|
-
p1, "Close", N=1
|
|
436
|
-
)
|
|
437
|
-
returns = self.bars.get_latest_bars_values(
|
|
438
|
-
self.hmm_tiker, "Returns", N=self.hmm_window
|
|
439
|
-
)
|
|
440
|
-
if len(returns) >= self.hmm_window:
|
|
441
|
-
self.latest_prices[0] = _x[-1]
|
|
442
|
-
self.latest_prices[1] = _y[-1]
|
|
443
|
-
|
|
444
|
-
if all(self.latest_prices > -1.0):
|
|
445
|
-
slope, intercept = self.calc_slope_intercep(self.latest_prices)
|
|
446
|
-
|
|
447
|
-
self.theta[0] = slope
|
|
448
|
-
self.theta[1] = intercept
|
|
449
|
-
|
|
450
|
-
# Create the observation matrix of the latest prices
|
|
451
|
-
# of Y and the intercept value (1.0) as well as the
|
|
452
|
-
# scalar value of the latest price from X
|
|
453
|
-
F = np.asarray([self.latest_prices[0], 1.0]).reshape((1, 2))
|
|
454
|
-
y = self.latest_prices[1]
|
|
455
|
-
|
|
456
|
-
# The prior value of the states \theta_t is
|
|
457
|
-
# distributed as a multivariate Gaussian with
|
|
458
|
-
# mean a_t and variance-covariance R_t
|
|
459
|
-
if self.R is not None:
|
|
460
|
-
self.R = self.C + self.wt
|
|
461
|
-
else:
|
|
462
|
-
self.R = np.zeros((2, 2))
|
|
463
|
-
|
|
464
|
-
# Calculate the Kalman Filter update
|
|
465
|
-
# ---------------------------------
|
|
466
|
-
# Calculate prediction of new observation
|
|
467
|
-
# as well as forecast error of that prediction
|
|
468
|
-
yhat = F.dot(self.theta)
|
|
469
|
-
et = y - yhat
|
|
470
|
-
|
|
471
|
-
# Q_t is the variance of the prediction of
|
|
472
|
-
# observations and hence sqrt_Qt is the
|
|
473
|
-
# standard deviation of the predictions
|
|
474
|
-
Qt = F.dot(self.R).dot(F.T) + self.vt
|
|
475
|
-
sqrt_Qt = np.sqrt(Qt)
|
|
476
|
-
|
|
477
|
-
# The posterior value of the states \theta_t is
|
|
478
|
-
# distributed as a multivariate Gaussian with mean
|
|
479
|
-
# m_t and variance-covariance C_t
|
|
480
|
-
At = self.R.dot(F.T) / Qt
|
|
481
|
-
self.theta = self.theta + At.flatten() * et
|
|
482
|
-
self.C = self.R - At * F.dot(self.R)
|
|
483
|
-
regime = self.hmm_model.which_trade_allowed(returns)
|
|
484
|
-
|
|
485
|
-
self.calculate_xy_signals(et, sqrt_Qt, regime, dt)
|
|
486
|
-
|
|
487
|
-
def calculate_signals(self, event):
|
|
488
|
-
"""
|
|
489
|
-
Calculate the Kalman Filter strategy.
|
|
490
|
-
"""
|
|
491
|
-
if event.type == "MARKET":
|
|
492
|
-
self.calculate_signals_for_pairs()
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
class OUStrategyBacktester(Strategy):
|
|
496
|
-
"""
|
|
497
|
-
The `OUBacktester` class is a specialized trading strategy that implements
|
|
498
|
-
the Ornstein-Uhlenbeck (OU) process for mean-reverting financial time series.
|
|
499
|
-
This class extends the generic `Strategy` class provided by a backtesting framework
|
|
500
|
-
allowing it to integrate seamlessly with the ecosystem of data handling, signal generation
|
|
501
|
-
event management, and execution handling components of the framework.
|
|
502
|
-
The strategy is designed to operate on historical market data, specifically
|
|
503
|
-
targeting a single financial instrument (or a list of instruments)
|
|
504
|
-
for which the trading signals are to be generated.
|
|
505
|
-
|
|
506
|
-
Note:
|
|
507
|
-
This strategy is based on a stochastic process, so it is normal that every time
|
|
508
|
-
you run the backtest you get different results.
|
|
509
|
-
"""
|
|
510
|
-
|
|
511
|
-
def __init__(self, bars: DataHandler, events: Queue, **kwargs):
|
|
512
|
-
"""
|
|
513
|
-
Args:
|
|
514
|
-
`bars`: DataHandler
|
|
515
|
-
`events`: event queue
|
|
516
|
-
`ticker`: Symbol of the financial instrument.
|
|
517
|
-
`p`: Lookback period for the OU process.
|
|
518
|
-
`n`: Minimum number of observations for signal generation.
|
|
519
|
-
`quantity`: Quantity of assets to trade.
|
|
520
|
-
`ou_data`: DataFrame used to estimate Ornstein-Uhlenbeck process params
|
|
521
|
-
(drift `(θ)`, volatility `(σ)`, and long-term mean `(μ)`).
|
|
522
|
-
`hmm_model`: HMM risk management model.
|
|
523
|
-
`hmm_window`: Lookback period for HMM.
|
|
524
|
-
"""
|
|
525
|
-
self.bars = bars
|
|
526
|
-
self.symbol_list = self.bars.symbol_list
|
|
527
|
-
self.events = events
|
|
528
|
-
|
|
529
|
-
self.ticker = kwargs.get('tiker')
|
|
530
|
-
self.p = kwargs.get('p', 20)
|
|
531
|
-
self.n = kwargs.get('n', 10)
|
|
532
|
-
self.qty = kwargs.get('quantity', 1000)
|
|
533
|
-
|
|
534
|
-
self.data = kwargs.get("ou_data")
|
|
535
|
-
self.ou_data = self._get_data(self.data)
|
|
536
|
-
self.ou = OrnsteinUhlenbeck(self.ou_data["Close"].values)
|
|
537
|
-
|
|
538
|
-
self.hmm = kwargs.get('hmm_model')
|
|
539
|
-
self.window = kwargs.get('hmm_window', 50)
|
|
540
|
-
|
|
541
|
-
self.LONG = False
|
|
542
|
-
self.SHORT = False
|
|
543
|
-
|
|
544
|
-
def _get_data(self, data):
|
|
545
|
-
if isinstance(data, pd.DataFrame):
|
|
546
|
-
return data
|
|
547
|
-
if isinstance(data, str):
|
|
548
|
-
return self._read_csv(data)
|
|
549
|
-
|
|
550
|
-
def _read_csv(self, csv_file):
|
|
551
|
-
df = pd.read_csv(csv_file, header=0,
|
|
552
|
-
names=["Date", "Open", "High", "Low",
|
|
553
|
-
"Close", "Adj Close", "Volume"],
|
|
554
|
-
index_col="Date", parse_dates=True)
|
|
555
|
-
return df
|
|
556
|
-
|
|
557
|
-
def create_signal(self):
|
|
558
|
-
returns = self.bars.get_latest_bars_values(
|
|
559
|
-
self.ticker, "Returns", N=self.p
|
|
560
|
-
)
|
|
561
|
-
hmm_returns = self.bars.get_latest_bars_values(
|
|
562
|
-
self.ticker, "Returns", N=self.window
|
|
563
|
-
)
|
|
564
|
-
dt = self.bars.get_latest_bar_datetime(self.ticker)
|
|
565
|
-
if len(returns) >= self.p and len(hmm_returns) >= self.window:
|
|
566
|
-
action = self.ou.calculate_signals(
|
|
567
|
-
rts=returns, p=self.p, n=self.n, th=1)
|
|
568
|
-
regime = self.hmm.which_trade_allowed(hmm_returns)
|
|
569
|
-
|
|
570
|
-
if action == "SHORT" and self.LONG:
|
|
571
|
-
signal = SignalEvent(1, self.ticker, dt, "EXIT")
|
|
572
|
-
self.events.put(signal)
|
|
573
|
-
print(dt, "EXIT LONG")
|
|
574
|
-
self.LONG = False
|
|
575
|
-
|
|
576
|
-
elif action == "LONG" and self.SHORT:
|
|
577
|
-
signal = SignalEvent(1, self.ticker, dt, "EXIT")
|
|
578
|
-
self.events.put(signal)
|
|
579
|
-
print(dt, "EXIT SHORT")
|
|
580
|
-
self.SHORT = False
|
|
581
|
-
|
|
582
|
-
if regime == "LONG":
|
|
583
|
-
if action == "LONG" and not self.LONG:
|
|
584
|
-
self.LONG = True
|
|
585
|
-
signal = SignalEvent(1, self.ticker, dt, "LONG", self.qty)
|
|
586
|
-
self.events.put(signal)
|
|
587
|
-
print(dt, "LONG")
|
|
588
|
-
|
|
589
|
-
elif regime == 'SHORT':
|
|
590
|
-
if action == "SHORT" and not self.SHORT:
|
|
591
|
-
self.SHORT = True
|
|
592
|
-
signal = SignalEvent(1, self.ticker, dt, "SHORT", self.qty)
|
|
593
|
-
self.events.put(signal)
|
|
594
|
-
print(dt, "SHORT")
|
|
595
|
-
|
|
596
|
-
def calculate_signals(self, event):
|
|
597
|
-
if event.type == "MARKET":
|
|
598
|
-
self.create_signal()
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
class ArimaGarchStrategyBacktester(Strategy):
|
|
602
|
-
"""
|
|
603
|
-
The `ArimaGarchStrategyBacktester` class extends the `Strategy`
|
|
604
|
-
class to implement a backtesting framework for trading strategies based on
|
|
605
|
-
ARIMA-GARCH models, incorporating a Hidden Markov Model (HMM) for risk management.
|
|
606
|
-
|
|
607
|
-
Features
|
|
608
|
-
========
|
|
609
|
-
- **ARIMA-GARCH Model**: Utilizes ARIMA for time series forecasting and GARCH for volatility forecasting, aimed at predicting market movements.
|
|
610
|
-
|
|
611
|
-
- **HMM Risk Management**: Employs a Hidden Markov Model to manage risks, determining safe trading regimes.
|
|
612
|
-
|
|
613
|
-
- **Event-Driven Backtesting**: Capable of simulating real-time trading conditions by processing market data and signals sequentially.
|
|
614
|
-
|
|
615
|
-
Key Methods
|
|
616
|
-
===========
|
|
617
|
-
- `get_data()`: Retrieves and prepares the data required for ARIMA-GARCH model predictions.
|
|
618
|
-
- `create_signal()`: Generates trading signals based on model predictions and current market positions.
|
|
619
|
-
- `calculate_signals(event)`: Listens for market events and triggers signal creation and event placement.
|
|
620
|
-
|
|
621
|
-
"""
|
|
622
|
-
|
|
623
|
-
def __init__(self, bars: DataHandler, events: Queue, **kwargs):
|
|
624
|
-
"""
|
|
625
|
-
Args:
|
|
626
|
-
`bars`: DataHandler
|
|
627
|
-
`events`: event queue
|
|
628
|
-
`ticker`: Symbol of the financial instrument.
|
|
629
|
-
`arima_window`: The window size for rolling prediction in backtesting.
|
|
630
|
-
`quantity`: Quantity of assets to trade.
|
|
631
|
-
`hmm_window`: Lookback period for HMM.
|
|
632
|
-
`hmm_model`: HMM risk management model.
|
|
633
|
-
|
|
634
|
-
"""
|
|
635
|
-
self.bars = bars
|
|
636
|
-
self.symbol_list = self.bars.symbol_list
|
|
637
|
-
self.events = events
|
|
638
|
-
|
|
639
|
-
self.tiker = kwargs.get('tiker')
|
|
640
|
-
self.arima_window = kwargs.get('arima_window', 252)
|
|
641
|
-
|
|
642
|
-
self.qty = kwargs.get('qauntity', 100)
|
|
643
|
-
self.hmm_window = kwargs.get("hmm_window", 50)
|
|
644
|
-
self.hmm_model = kwargs.get("hmm_model")
|
|
645
|
-
|
|
646
|
-
self.long_market = False
|
|
647
|
-
self.short_market = False
|
|
648
|
-
|
|
649
|
-
def get_data(self):
|
|
650
|
-
symbol = self.tiker
|
|
651
|
-
M = self.arima_window
|
|
652
|
-
N = self.hmm_window
|
|
653
|
-
dt = self.bars.get_latest_bar_datetime(self.tiker)
|
|
654
|
-
bars = self.bars.get_latest_bars_values(
|
|
655
|
-
symbol, "Close", N=self.arima_window
|
|
656
|
-
)
|
|
657
|
-
returns = self.bars.get_latest_bars_values(
|
|
658
|
-
symbol, 'Returns', N=self.hmm_window
|
|
659
|
-
)
|
|
660
|
-
df = pd.DataFrame()
|
|
661
|
-
df['Close'] = bars[-M:]
|
|
662
|
-
df = df.dropna()
|
|
663
|
-
data = load_and_prepare_data(df)
|
|
664
|
-
if len(data) >= M and len(returns) >= N:
|
|
665
|
-
return data, returns[-N:], dt
|
|
666
|
-
return None, None, None
|
|
667
|
-
|
|
668
|
-
def create_signal(self):
|
|
669
|
-
data, returns, dt = self.get_data()
|
|
670
|
-
signal = None
|
|
671
|
-
if data is not None and returns is not None:
|
|
672
|
-
prediction = get_prediction(data['diff_log_return'])
|
|
673
|
-
regime = self.hmm_model.which_trade_allowed(returns)
|
|
674
|
-
|
|
675
|
-
# If we are short the market, check for an exit
|
|
676
|
-
if prediction > 0 and self.short_market:
|
|
677
|
-
signal = SignalEvent(1, self.tiker, dt, "EXIT")
|
|
678
|
-
print(f"{dt}: EXIT SHORT")
|
|
679
|
-
self.short_market = False
|
|
680
|
-
|
|
681
|
-
# If we are long the market, check for an exit
|
|
682
|
-
elif prediction < 0 and self.long_market:
|
|
683
|
-
signal = SignalEvent(1, self.tiker, dt, "EXIT")
|
|
684
|
-
print(f"{dt}: EXIT LONG")
|
|
685
|
-
self.long_market = False
|
|
686
|
-
|
|
687
|
-
if regime == "LONG":
|
|
688
|
-
# If we are not in the market, go long
|
|
689
|
-
if prediction > 0 and not self.long_market:
|
|
690
|
-
signal = SignalEvent(
|
|
691
|
-
1, self.tiker, dt, "LONG", quantity=self.qty)
|
|
692
|
-
print(f"{dt}: LONG")
|
|
693
|
-
self.long_market = True
|
|
694
|
-
|
|
695
|
-
elif regime == "SHORT":
|
|
696
|
-
# If we are not in the market, go short
|
|
697
|
-
if prediction < 0 and not self.short_market:
|
|
698
|
-
signal = SignalEvent(
|
|
699
|
-
1, self.tiker, dt, "SHORT", quantity=self.qty)
|
|
700
|
-
print(f"{dt}: SHORT")
|
|
701
|
-
self.short_market = True
|
|
702
|
-
|
|
703
|
-
return signal
|
|
704
|
-
|
|
705
|
-
def calculate_signals(self, event):
|
|
706
|
-
if event.type == 'MARKET':
|
|
707
|
-
signal = self.create_signal()
|
|
708
|
-
if signal is not None:
|
|
709
|
-
self.events.put(signal)
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
def _run_backtest(
|
|
713
|
-
strategy_name: str,
|
|
714
|
-
capital: float, symbol_list: list, kwargs: dict):
|
|
715
|
-
"""
|
|
716
|
-
Executes a backtest of the specified strategy
|
|
717
|
-
integrating a Hidden Markov Model (HMM) for risk management.
|
|
718
|
-
"""
|
|
719
|
-
hmm_data = yf.download(
|
|
720
|
-
kwargs.get("hmm_tiker", symbol_list[0]),
|
|
721
|
-
start=kwargs.get("hmm_start"), end=kwargs.get("hmm_end")
|
|
722
|
-
)
|
|
723
|
-
kwargs["hmm_model"] = HMMRiskManager(data=hmm_data, verbose=True)
|
|
724
|
-
kwargs["strategy_name"] = strategy_name
|
|
725
|
-
|
|
726
|
-
engine = Backtest(
|
|
727
|
-
symbol_list, capital, 0.0, datetime.strptime(
|
|
728
|
-
kwargs['yf_start'], "%Y-%m-%d"),
|
|
729
|
-
kwargs.get("data_handler", YFHistoricDataHandler),
|
|
730
|
-
SimulatedExecutionHandler, kwargs.pop('backtester_class'), **kwargs
|
|
731
|
-
)
|
|
732
|
-
engine.simulate_trading()
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
def _run_arch_backtest(
|
|
736
|
-
capital: float = 100000.0,
|
|
737
|
-
quantity: int = 1000
|
|
738
|
-
):
|
|
739
|
-
kwargs = {
|
|
740
|
-
'tiker': 'SPY',
|
|
741
|
-
'quantity': quantity,
|
|
742
|
-
'yf_start': "2004-01-02",
|
|
743
|
-
'backtester_class': ArimaGarchStrategyBacktester
|
|
744
|
-
}
|
|
745
|
-
_run_backtest("ARIMA+GARCH & HMM", capital, ["SPY"], kwargs)
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
def _run_ou_backtest(
|
|
749
|
-
capital: float = 100000.0,
|
|
750
|
-
quantity: int = 2000
|
|
751
|
-
):
|
|
752
|
-
kwargs = {
|
|
753
|
-
"tiker": 'GLD',
|
|
754
|
-
'quantity': quantity,
|
|
755
|
-
"n": 5,
|
|
756
|
-
"p": 5,
|
|
757
|
-
"hmm_window": 50,
|
|
758
|
-
"yf_start": "2015-01-02",
|
|
759
|
-
'backtester_class': OUStrategyBacktester,
|
|
760
|
-
'ou_data': yf.download("GLD", start="2010-01-04", end="2014-12-31"),
|
|
761
|
-
'hmm_end': "2014-12-31"
|
|
762
|
-
}
|
|
763
|
-
_run_backtest("Ornstein-Uhlenbeck & HMM", capital, ['GLD'], kwargs)
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
def _run_kf_backtest(
|
|
767
|
-
capital: float = 100000.0,
|
|
768
|
-
quantity: int = 2000
|
|
769
|
-
):
|
|
770
|
-
symbol_list = ["IEI", "TLT"]
|
|
771
|
-
kwargs = {
|
|
772
|
-
"tickers": symbol_list,
|
|
773
|
-
"quantity": quantity,
|
|
774
|
-
"benchmark": "TLT",
|
|
775
|
-
"yf_start": "2009-08-03",
|
|
776
|
-
"hmm_tiker": "TLT",
|
|
777
|
-
'backtester_class': KLFStrategyBacktester,
|
|
778
|
-
'hmm_end': "2009-07-28"
|
|
779
|
-
}
|
|
780
|
-
_run_backtest("Kalman Filter & HMM", capital, symbol_list, kwargs)
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
def _run_sma_backtest(
|
|
784
|
-
capital: float = 100000.0,
|
|
785
|
-
quantity: int = 100
|
|
786
|
-
):
|
|
787
|
-
kwargs = {
|
|
788
|
-
"quantity": quantity,
|
|
789
|
-
"hmm_end": "2009-12-31",
|
|
790
|
-
"hmm_tiker": "^GSPC",
|
|
791
|
-
"yf_start": "2010-01-01",
|
|
792
|
-
"hmm_start": "1990-01-01",
|
|
793
|
-
"start_pos": "2023-01-01",
|
|
794
|
-
"session_duration": 23.0,
|
|
795
|
-
"backtester_class": SMAStrategyBacktester,
|
|
796
|
-
"data_handler": MT5HistoricDataHandler
|
|
797
|
-
}
|
|
798
|
-
_run_backtest("SMA & HMM", capital, ["[SP500]"], kwargs)
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
_BACKTESTS = {
|
|
802
|
-
'ou': _run_ou_backtest,
|
|
803
|
-
'sma': _run_sma_backtest,
|
|
804
|
-
'klf': _run_kf_backtest,
|
|
805
|
-
'arch': _run_arch_backtest
|
|
806
|
-
}
|
|
807
|
-
|
|
206
|
+
BacktestEngine = Backtest
|
|
808
207
|
|
|
809
208
|
def run_backtest(
|
|
810
|
-
symbol_list:
|
|
811
|
-
start_date:
|
|
812
|
-
data_handler:
|
|
813
|
-
strategy:
|
|
814
|
-
exc_handler:
|
|
815
|
-
initial_capital:
|
|
816
|
-
heartbeat:
|
|
817
|
-
test_mode: Optional[bool] = True,
|
|
818
|
-
test_strategy: Literal['ou', 'sma', 'klf', 'arch'] = 'sma',
|
|
819
|
-
test_quantity: Optional[int] = 1000,
|
|
209
|
+
symbol_list: List[str],
|
|
210
|
+
start_date: datetime,
|
|
211
|
+
data_handler: DataHandler,
|
|
212
|
+
strategy: Strategy,
|
|
213
|
+
exc_handler: Optional[ExecutionHandler] = None,
|
|
214
|
+
initial_capital: float = 100000.0,
|
|
215
|
+
heartbeat: float = 0.0,
|
|
820
216
|
**kwargs
|
|
821
217
|
):
|
|
822
218
|
"""
|
|
823
|
-
Runs a backtest simulation based on a `DataHandler`, `Strategy
|
|
219
|
+
Runs a backtest simulation based on a `DataHandler`, `Strategy`, and `ExecutionHandler`.
|
|
824
220
|
|
|
825
221
|
Args:
|
|
826
|
-
symbol_list (List[str]): List of symbol strings for the assets to be backtested.
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
222
|
+
symbol_list (List[str]): List of symbol strings for the assets to be backtested.
|
|
223
|
+
|
|
224
|
+
start_date (datetime): Start date of the backtest.
|
|
225
|
+
|
|
831
226
|
data_handler (DataHandler): An instance of the `DataHandler` class, responsible for managing
|
|
832
|
-
and processing market data.
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
See each of this class documentation for more details.
|
|
836
|
-
You can create your `CustumDataHandler` but it must be a subclass of `DataHandler`.
|
|
837
|
-
|
|
838
|
-
strategy (Strategy): The trading strategy to be employed during the backtest. Required when `test_mode` is False.
|
|
839
|
-
The strategy must be an instance of `Strategy` and it must have `DataHandler` and `event queue`
|
|
840
|
-
as required positional arguments to be used int the `Backtest` class. All other argument needed to be pass
|
|
841
|
-
to the strategy class must be in `**kwargs`. The strategy class must have `calculate_signals`
|
|
842
|
-
methods to generate `SignalEvent` in the backtest class.
|
|
843
|
-
|
|
844
|
-
exc_handler (ExecutionHandler): The execution handler for managing order executions. If not provided,
|
|
845
|
-
a `SimulatedExecutionHandler` will be used by default. Required when `test_mode` is False.
|
|
846
|
-
The `exc_handler` must be an instance of `ExecutionHandler` and must have `execute_order`method
|
|
847
|
-
used to handle `OrderEvent` in events queue in the `Backtest` class.
|
|
227
|
+
and processing market data. Available options include `HistoricCSVDataHandler`,
|
|
228
|
+
`MT5HistoricDataHandler`, and `YFHistoricDataHandler`. Ensure that the `DataHandler`
|
|
229
|
+
instance is initialized before passing it to the function.
|
|
848
230
|
|
|
849
|
-
|
|
231
|
+
strategy (Strategy): The trading strategy to be employed during the backtest.
|
|
232
|
+
The strategy must be an instance of `Strategy` and should include the following attributes:
|
|
233
|
+
- `bars` (DataHandler): The `DataHandler` instance for the strategy.
|
|
234
|
+
- `events` (Queue): Queue instance for managing events.
|
|
235
|
+
- `symbol_list` (List[str]): List of symbols to trade.
|
|
236
|
+
- `mode` (str): 'live' or 'backtest'.
|
|
850
237
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
It could be also used as time frame in live trading engine (e.g 1m, 5m, 15m etc.) when listening
|
|
854
|
-
to a live market `DataHandler`.
|
|
238
|
+
Additional parameters specific to the strategy should be passed in `**kwargs`.
|
|
239
|
+
The strategy class must implement a `calculate_signals` method to generate `SignalEvent`.
|
|
855
240
|
|
|
856
|
-
|
|
857
|
-
|
|
241
|
+
exc_handler (ExecutionHandler, optional): The execution handler for managing order executions.
|
|
242
|
+
If not provided, a `SimulatedExecutionHandler` will be used by default. This handler must
|
|
243
|
+
implement an `execute_order` method to process `OrderEvent` in the `Backtest` class.
|
|
858
244
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
- `sma` Execute `SMAStrategyBacktester`, for more detail see this class documentation.
|
|
862
|
-
- `klf` Execute `KLFStrategyBacktester`, for more detail see this class documentation.
|
|
863
|
-
- `arch` Execute `ArimaGarchStrategyBacktester`, for more detail see this class documentation.
|
|
245
|
+
initial_capital (float, optional): The initial capital for the portfolio in the backtest.
|
|
246
|
+
Default is 100,000.
|
|
864
247
|
|
|
865
|
-
|
|
248
|
+
heartbeat (float, optional): Time delay (in seconds) between iterations of the event-driven
|
|
249
|
+
backtest loop. Default is 0.0, allowing the backtest to run as fast as possible. This could
|
|
250
|
+
also be used as a time frame in live trading (e.g., 1m, 5m, 15m) with a live `DataHandler`.
|
|
866
251
|
|
|
867
252
|
**kwargs: Additional parameters passed to the `Backtest` instance, which may include strategy-specific,
|
|
868
|
-
data handler,
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
>>>
|
|
878
|
-
|
|
253
|
+
data handler, portfolio, or execution handler options.
|
|
254
|
+
|
|
255
|
+
Notes:
|
|
256
|
+
This function generates three outputs:
|
|
257
|
+
- A performance summary saved as an HTML file.
|
|
258
|
+
- An equity curve of the portfolio saved as a CSV file.
|
|
259
|
+
- Monthly returns saved as a PNG image.
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
>>> from bbstrader.trading.strategies import StockIndexSTBOTrading
|
|
263
|
+
>>> from bbstrader.metatrader.utils import config_logger
|
|
264
|
+
>>> from bbstrader.datahandlers import MT5HistoricDataHandler
|
|
265
|
+
>>> from bbstrader.execution import MT5ExecutionHandler
|
|
266
|
+
>>> from datetime import datetime
|
|
267
|
+
>>>
|
|
268
|
+
>>> logger = config_logger('index_trade.log', console_log=True)
|
|
269
|
+
>>> symbol_list = ['[SP500]', 'GERMANY40', '[DJI30]', '[NQ100]']
|
|
270
|
+
>>> start = datetime(2010, 6, 1, 2, 0, 0)
|
|
271
|
+
>>> kwargs = {
|
|
272
|
+
... 'expected_returns': {'[NQ100]': 1.5, '[SP500]': 1.5, '[DJI30]': 1.0, 'GERMANY40': 1.0},
|
|
273
|
+
... 'quantities': {'[NQ100]': 15, '[SP500]': 30, '[DJI30]': 5, 'GERMANY40': 10},
|
|
274
|
+
... 'max_trades': {'[NQ100]': 3, '[SP500]': 3, '[DJI30]': 3, 'GERMANY40': 3},
|
|
275
|
+
... 'mt5_start': start,
|
|
276
|
+
... 'time_frame': '15m',
|
|
277
|
+
... 'strategy_name': 'SISTBO',
|
|
278
|
+
... }
|
|
879
279
|
>>> run_backtest(
|
|
880
|
-
... symbol_list=
|
|
881
|
-
... start_date=
|
|
882
|
-
... data_handler=
|
|
883
|
-
... strategy=
|
|
884
|
-
... exc_handler=
|
|
885
|
-
... initial_capital=
|
|
886
|
-
... heartbeat=
|
|
280
|
+
... symbol_list=symbol_list,
|
|
281
|
+
... start_date=start,
|
|
282
|
+
... data_handler=MT5HistoricDataHandler(),
|
|
283
|
+
... strategy=StockIndexSTBOTrading(),
|
|
284
|
+
... exc_handler=MT5ExecutionHandler(),
|
|
285
|
+
... initial_capital=100000.0,
|
|
286
|
+
... heartbeat=0.0,
|
|
287
|
+
... **kwargs
|
|
887
288
|
... )
|
|
888
289
|
"""
|
|
889
|
-
if
|
|
890
|
-
|
|
891
|
-
capital=initial_capital,
|
|
892
|
-
quantity=test_quantity
|
|
893
|
-
)
|
|
290
|
+
if exc_handler is None:
|
|
291
|
+
execution_handler = SimulatedExecutionHandler
|
|
894
292
|
else:
|
|
895
|
-
execution_handler =
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
293
|
+
execution_handler = exc_handler
|
|
294
|
+
engine = BacktestEngine(
|
|
295
|
+
symbol_list, initial_capital, heartbeat, start_date,
|
|
296
|
+
data_handler, execution_handler, strategy, **kwargs
|
|
297
|
+
)
|
|
298
|
+
engine.simulate_trading()
|