bbstrader 0.3.1__py3-none-any.whl → 0.3.2__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/__init__.py +1 -1
- bbstrader/__main__.py +2 -0
- bbstrader/btengine/backtest.py +7 -8
- bbstrader/btengine/execution.py +2 -2
- bbstrader/btengine/strategy.py +68 -17
- bbstrader/config.py +2 -2
- bbstrader/metatrader/account.py +77 -6
- bbstrader/metatrader/copier.py +530 -207
- bbstrader/metatrader/risk.py +1 -0
- bbstrader/metatrader/scripts.py +35 -9
- bbstrader/metatrader/trade.py +58 -41
- bbstrader/metatrader/utils.py +2 -0
- bbstrader/models/__init__.py +0 -1
- bbstrader/models/ml.py +55 -26
- bbstrader/models/nlp.py +145 -80
- bbstrader/models/optimization.py +1 -1
- bbstrader/models/risk.py +16 -386
- bbstrader/trading/execution.py +20 -12
- bbstrader/trading/strategies.py +9 -592
- bbstrader/tseries.py +39 -709
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.2.dist-info}/METADATA +35 -40
- bbstrader-0.3.2.dist-info/RECORD +47 -0
- bbstrader-0.3.1.dist-info/RECORD +0 -47
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.2.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.2.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.2.dist-info}/top_level.txt +0 -0
bbstrader/trading/strategies.py
CHANGED
|
@@ -20,25 +20,17 @@ from datetime import datetime
|
|
|
20
20
|
from queue import Queue
|
|
21
21
|
from typing import Dict, List, Literal, Optional, Union
|
|
22
22
|
|
|
23
|
-
import numpy as np
|
|
24
|
-
import pandas as pd
|
|
25
|
-
import yfinance as yf
|
|
26
|
-
|
|
27
23
|
from bbstrader.btengine.backtest import BacktestEngine
|
|
28
24
|
from bbstrader.btengine.data import DataHandler, MT5DataHandler, YFDataHandler
|
|
29
25
|
from bbstrader.btengine.event import Events, SignalEvent
|
|
30
26
|
from bbstrader.btengine.execution import MT5ExecutionHandler, SimExecutionHandler
|
|
31
27
|
from bbstrader.btengine.strategy import Strategy
|
|
32
28
|
from bbstrader.metatrader.account import Account
|
|
33
|
-
|
|
29
|
+
|
|
34
30
|
from bbstrader.metatrader.trade import TradingMode
|
|
35
|
-
|
|
36
|
-
from bbstrader.tseries import ArimaGarchModel, KalmanFilterModel
|
|
31
|
+
|
|
37
32
|
|
|
38
33
|
__all__ = [
|
|
39
|
-
"SMAStrategy",
|
|
40
|
-
"ArimaGarchStrategy",
|
|
41
|
-
"KalmanFilterStrategy",
|
|
42
34
|
"StockIndexSTBOTrading",
|
|
43
35
|
"test_strategy",
|
|
44
36
|
"get_quantities",
|
|
@@ -52,528 +44,6 @@ def get_quantities(quantities, symbol_list):
|
|
|
52
44
|
return {symbol: quantities for symbol in symbol_list}
|
|
53
45
|
|
|
54
46
|
|
|
55
|
-
class SMAStrategy(Strategy):
|
|
56
|
-
"""
|
|
57
|
-
Carries out a basic Moving Average Crossover strategy bactesting with a
|
|
58
|
-
short/long simple weighted moving average. Default short/long
|
|
59
|
-
windows are 50/200 periods respectively and uses Hiden Markov Model
|
|
60
|
-
as risk Managment system for filteering signals.
|
|
61
|
-
|
|
62
|
-
The trading strategy for this class is exceedingly simple and is used to bettter
|
|
63
|
-
understood. The important issue is the risk management aspect (the Hmm model)
|
|
64
|
-
|
|
65
|
-
The Long-term trend following strategy is of the classic moving average crossover type.
|
|
66
|
-
The rules are simple:
|
|
67
|
-
- At every bar calculate the 50-day and 200-day simple moving averages (SMA)
|
|
68
|
-
- If the 50-day SMA exceeds the 200-day SMA and the strategy is not invested, then go long
|
|
69
|
-
- If the 200-day SMA exceeds the 50-day SMA and the strategy is invested, then close the position
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
def __init__(
|
|
73
|
-
self,
|
|
74
|
-
bars: DataHandler = None,
|
|
75
|
-
events: Queue = None,
|
|
76
|
-
symbol_list: List[str] = None,
|
|
77
|
-
mode: TradingMode = TradingMode.BACKTEST,
|
|
78
|
-
**kwargs,
|
|
79
|
-
):
|
|
80
|
-
"""
|
|
81
|
-
Args:
|
|
82
|
-
bars (DataHandler): A data handler object that provides market data.
|
|
83
|
-
events (Queue): An event queue object where generated signals are placed.
|
|
84
|
-
symbol_list (List[str]): A list of symbols to consider for trading.
|
|
85
|
-
mode TradingMode: The mode of operation for the strategy.
|
|
86
|
-
short_window (int, optional): The period for the short moving average.
|
|
87
|
-
long_window (int, optional): The period for the long moving average.
|
|
88
|
-
time_frame (str, optional): The time frame for the data.
|
|
89
|
-
session_duration (float, optional): The duration of the trading session.
|
|
90
|
-
risk_window (int, optional): The window size for the risk model.
|
|
91
|
-
quantities (int, dict | optional): The default quantities of each asset to trade.
|
|
92
|
-
"""
|
|
93
|
-
self.bars = bars
|
|
94
|
-
self.events = events
|
|
95
|
-
self.symbol_list = symbol_list or self.bars.symbol_list
|
|
96
|
-
self.mode = mode
|
|
97
|
-
|
|
98
|
-
self.kwargs = kwargs
|
|
99
|
-
self.short_window = kwargs.get("short_window", 50)
|
|
100
|
-
self.long_window = kwargs.get("long_window", 200)
|
|
101
|
-
self.tf = kwargs.get("time_frame", "D1")
|
|
102
|
-
self.qty = get_quantities(kwargs.get("quantities", 100), self.symbol_list)
|
|
103
|
-
self.sd = kwargs.get("session_duration", 23.0)
|
|
104
|
-
self.risk_models = build_hmm_models(self.symbol_list, **self.kwargs)
|
|
105
|
-
self.risk_window = kwargs.get("risk_window", self.long_window)
|
|
106
|
-
self.bought = self._calculate_initial_bought()
|
|
107
|
-
|
|
108
|
-
def _calculate_initial_bought(self):
|
|
109
|
-
bought = {}
|
|
110
|
-
for s in self.symbol_list:
|
|
111
|
-
bought[s] = "OUT"
|
|
112
|
-
return bought
|
|
113
|
-
|
|
114
|
-
def get_backtest_data(self):
|
|
115
|
-
symbol_data = {symbol: None for symbol in self.symbol_list}
|
|
116
|
-
for s in self.symbol_list:
|
|
117
|
-
bar_date = self.bars.get_latest_bar_datetime(s)
|
|
118
|
-
bars = self.bars.get_latest_bars_values(s, "adj_close", N=self.long_window)
|
|
119
|
-
returns_val = self.bars.get_latest_bars_values(
|
|
120
|
-
s, "returns", N=self.risk_window
|
|
121
|
-
)
|
|
122
|
-
if len(bars) >= self.long_window and len(returns_val) >= self.risk_window:
|
|
123
|
-
regime = self.risk_models[s].which_trade_allowed(returns_val)
|
|
124
|
-
|
|
125
|
-
short_sma = np.mean(bars[-self.short_window :])
|
|
126
|
-
long_sma = np.mean(bars[-self.long_window :])
|
|
127
|
-
|
|
128
|
-
symbol_data[s] = (short_sma, long_sma, regime, bar_date)
|
|
129
|
-
return symbol_data
|
|
130
|
-
|
|
131
|
-
def create_backtest_signals(self):
|
|
132
|
-
signals = {symbol: None for symbol in self.symbol_list}
|
|
133
|
-
symbol_data = self.get_backtest_data()
|
|
134
|
-
for s, data in symbol_data.items():
|
|
135
|
-
signal = None
|
|
136
|
-
if data is not None:
|
|
137
|
-
price = self.bars.get_latest_bar_value(s, "adj_close")
|
|
138
|
-
short_sma, long_sma, regime, bar_date = data
|
|
139
|
-
dt = bar_date
|
|
140
|
-
if regime == "LONG":
|
|
141
|
-
# Bulliqh regime
|
|
142
|
-
if short_sma < long_sma and self.bought[s] == "LONG":
|
|
143
|
-
print(f"EXIT: {bar_date}")
|
|
144
|
-
signal = SignalEvent(1, s, dt, "EXIT", price=price)
|
|
145
|
-
self.bought[s] = "OUT"
|
|
146
|
-
|
|
147
|
-
elif short_sma > long_sma and self.bought[s] == "OUT":
|
|
148
|
-
print(f"LONG: {bar_date}")
|
|
149
|
-
signal = SignalEvent(
|
|
150
|
-
1, s, dt, "LONG", quantity=self.qty[s], price=price
|
|
151
|
-
)
|
|
152
|
-
self.bought[s] = "LONG"
|
|
153
|
-
|
|
154
|
-
elif regime == "SHORT":
|
|
155
|
-
# Bearish regime
|
|
156
|
-
if short_sma > long_sma and self.bought[s] == "SHORT":
|
|
157
|
-
print(f"EXIT: {bar_date}")
|
|
158
|
-
signal = SignalEvent(1, s, dt, "EXIT", price=price)
|
|
159
|
-
self.bought[s] = "OUT"
|
|
160
|
-
|
|
161
|
-
elif short_sma < long_sma and self.bought[s] == "OUT":
|
|
162
|
-
print(f"SHORT: {bar_date}")
|
|
163
|
-
signal = SignalEvent(
|
|
164
|
-
1, s, dt, "SHORT", quantity=self.qty[s], price=price
|
|
165
|
-
)
|
|
166
|
-
self.bought[s] = "SHORT"
|
|
167
|
-
signals[s] = signal
|
|
168
|
-
return signals
|
|
169
|
-
|
|
170
|
-
def get_live_data(self):
|
|
171
|
-
symbol_data = {symbol: None for symbol in self.symbol_list}
|
|
172
|
-
for symbol in self.symbol_list:
|
|
173
|
-
sig_rate = Rates(symbol, self.tf, 0, self.risk_window + 2, **self.kwargs)
|
|
174
|
-
hmm_data = sig_rate.returns.values
|
|
175
|
-
prices = sig_rate.close.values
|
|
176
|
-
current_regime = self.risk_models[symbol].which_trade_allowed(hmm_data)
|
|
177
|
-
assert len(prices) >= self.long_window and len(hmm_data) >= self.risk_window
|
|
178
|
-
short_sma = np.mean(prices[-self.short_window :])
|
|
179
|
-
long_sma = np.mean(prices[-self.long_window :])
|
|
180
|
-
short_sma, long_sma, current_regime
|
|
181
|
-
symbol_data[symbol] = (short_sma, long_sma, current_regime)
|
|
182
|
-
return symbol_data
|
|
183
|
-
|
|
184
|
-
def create_live_signals(self):
|
|
185
|
-
signals = {symbol: None for symbol in self.symbol_list}
|
|
186
|
-
symbol_data = self.get_live_data()
|
|
187
|
-
for symbol, data in symbol_data.items():
|
|
188
|
-
signal = None
|
|
189
|
-
short_sma, long_sma, regime = data
|
|
190
|
-
if regime == "LONG":
|
|
191
|
-
if short_sma > long_sma:
|
|
192
|
-
signal = "LONG"
|
|
193
|
-
elif regime == "SHORT":
|
|
194
|
-
if short_sma < long_sma:
|
|
195
|
-
signal = "SHORT"
|
|
196
|
-
signals[symbol] = signal
|
|
197
|
-
return signals
|
|
198
|
-
|
|
199
|
-
def calculate_signals(self, event=None):
|
|
200
|
-
if self.mode == TradingMode.BACKTEST and event is not None:
|
|
201
|
-
if event.type == Events.MARKET:
|
|
202
|
-
signals = self.create_backtest_signals()
|
|
203
|
-
for signal in signals.values():
|
|
204
|
-
if signal is not None:
|
|
205
|
-
self.events.put(signal)
|
|
206
|
-
elif self.mode == TradingMode.LIVE:
|
|
207
|
-
signals = self.create_live_signals()
|
|
208
|
-
return signals
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
class ArimaGarchStrategy(Strategy):
|
|
212
|
-
"""
|
|
213
|
-
The `ArimaGarchStrategy` class extends the `Strategy`
|
|
214
|
-
class to implement a backtesting framework for trading strategies based on
|
|
215
|
-
ARIMA-GARCH models, incorporating a Hidden Markov Model (HMM) for risk management.
|
|
216
|
-
|
|
217
|
-
Features
|
|
218
|
-
========
|
|
219
|
-
- **ARIMA-GARCH Model**: Utilizes ARIMA for time series forecasting and GARCH for volatility forecasting, aimed at predicting market movements.
|
|
220
|
-
|
|
221
|
-
- **HMM Risk Management**: Employs a Hidden Markov Model to manage risks, determining safe trading regimes.
|
|
222
|
-
|
|
223
|
-
- **Event-Driven Backtesting**: Capable of simulating real-time trading conditions by processing market data and signals sequentially.
|
|
224
|
-
|
|
225
|
-
- **Live Trading**: Supports real-time trading by generating signals based on live ARIMA-GARCH predictions and HMM risk management.
|
|
226
|
-
|
|
227
|
-
Key Methods
|
|
228
|
-
===========
|
|
229
|
-
- `get_backtest_data()`: Retrieves historical data for backtesting.
|
|
230
|
-
- `create_backtest_signal()`: Generates trading signals based on ARIMA-GARCH predictions and HMM risk management.
|
|
231
|
-
- `get_live_data()`: Retrieves live data for real-time trading.
|
|
232
|
-
- `create_live_signals()`: Generates trading signals based on live ARIMA-GARCH predictions and HMM risk management.
|
|
233
|
-
- `calculate_signals()`: Determines the trading signals based on the mode of operation (backtest or live).
|
|
234
|
-
|
|
235
|
-
"""
|
|
236
|
-
|
|
237
|
-
def __init__(
|
|
238
|
-
self,
|
|
239
|
-
bars: DataHandler = None,
|
|
240
|
-
events: Queue = None,
|
|
241
|
-
symbol_list: List[str] = None,
|
|
242
|
-
mode: TradingMode = TradingMode.BACKTEST,
|
|
243
|
-
**kwargs,
|
|
244
|
-
):
|
|
245
|
-
"""
|
|
246
|
-
Args:
|
|
247
|
-
`bars`: A data handler object that provides market data.
|
|
248
|
-
`events`: An event queue object where generated signals are placed.
|
|
249
|
-
`symbol_list`: A list of symbols to consider for trading.
|
|
250
|
-
`mode`: The mode of operation for the strategy.
|
|
251
|
-
`arima_window`: The window size for rolling prediction in backtesting.
|
|
252
|
-
`time_frame`: The time frame for the data.
|
|
253
|
-
`quantities`: Quantity of each assets to trade.
|
|
254
|
-
`hmm_window`: Lookback period for HMM.
|
|
255
|
-
"""
|
|
256
|
-
self.bars = bars
|
|
257
|
-
self.events = events
|
|
258
|
-
self.symbol_list = symbol_list or self.bars.symbol_list
|
|
259
|
-
self.mode = mode
|
|
260
|
-
|
|
261
|
-
self.qty = get_quantities(kwargs.get("quantities", 100), self.symbol_list)
|
|
262
|
-
self.arima_window = kwargs.get("arima_window", 252)
|
|
263
|
-
self.tf = kwargs.get("time_frame", "D1")
|
|
264
|
-
self.sd = kwargs.get("session_duration", 23.0)
|
|
265
|
-
self.risk_window = kwargs.get("hmm_window", 50)
|
|
266
|
-
self.risk_models = build_hmm_models(self.symbol_list, **kwargs)
|
|
267
|
-
self.arima_models = self._build_arch_models(**kwargs)
|
|
268
|
-
|
|
269
|
-
self.long_market = {s: False for s in self.symbol_list}
|
|
270
|
-
self.short_market = {s: False for s in self.symbol_list}
|
|
271
|
-
|
|
272
|
-
def _build_arch_models(self, **kwargs) -> Dict[str, ArimaGarchModel]:
|
|
273
|
-
arch_models = {symbol: None for symbol in self.symbol_list}
|
|
274
|
-
for symbol in self.symbol_list:
|
|
275
|
-
try:
|
|
276
|
-
rates = Rates(symbol, self.tf, 0)
|
|
277
|
-
data = rates.get_rates_from_pos()
|
|
278
|
-
assert data is not None, f"No data for {symbol}"
|
|
279
|
-
except AssertionError:
|
|
280
|
-
data = yf.download(symbol, start=kwargs.get("yf_start"))
|
|
281
|
-
arch = ArimaGarchModel(symbol, data, k=self.arima_window)
|
|
282
|
-
arch_models[symbol] = arch
|
|
283
|
-
return arch_models
|
|
284
|
-
|
|
285
|
-
def get_backtest_data(self):
|
|
286
|
-
symbol_data = {symbol: None for symbol in self.symbol_list}
|
|
287
|
-
for symbol in self.symbol_list:
|
|
288
|
-
M = self.arima_window
|
|
289
|
-
N = self.risk_window
|
|
290
|
-
dt = self.bars.get_latest_bar_datetime(symbol)
|
|
291
|
-
bars = self.bars.get_latest_bars_values(
|
|
292
|
-
symbol, "close", N=self.arima_window
|
|
293
|
-
)
|
|
294
|
-
returns = self.bars.get_latest_bars_values(
|
|
295
|
-
symbol, "returns", N=self.risk_window
|
|
296
|
-
)
|
|
297
|
-
df = pd.DataFrame()
|
|
298
|
-
df["Close"] = bars[-M:]
|
|
299
|
-
df = df.dropna()
|
|
300
|
-
arch_returns = self.arima_models[symbol].load_and_prepare_data(df)
|
|
301
|
-
data = arch_returns["diff_log_return"].iloc[-self.arima_window :]
|
|
302
|
-
if len(data) >= M and len(returns) >= N:
|
|
303
|
-
symbol_data[symbol] = (data, returns[-N:], dt)
|
|
304
|
-
return symbol_data
|
|
305
|
-
|
|
306
|
-
def create_backtest_signal(self):
|
|
307
|
-
signals = {symbol: None for symbol in self.symbol_list}
|
|
308
|
-
for symbol in self.symbol_list:
|
|
309
|
-
symbol_data = self.get_backtest_data()[symbol]
|
|
310
|
-
if symbol_data is not None:
|
|
311
|
-
data, returns, dt = symbol_data
|
|
312
|
-
signal = None
|
|
313
|
-
prediction = self.arima_models[symbol].get_prediction(data)
|
|
314
|
-
regime = self.risk_models[symbol].which_trade_allowed(returns)
|
|
315
|
-
price = self.bars.get_latest_bar_value(symbol, "adj_close")
|
|
316
|
-
|
|
317
|
-
# If we are short the market, check for an exit
|
|
318
|
-
if prediction > 0 and self.short_market[symbol]:
|
|
319
|
-
signal = SignalEvent(1, symbol, dt, "EXIT", price=price)
|
|
320
|
-
print(f"{dt}: EXIT SHORT")
|
|
321
|
-
self.short_market[symbol] = False
|
|
322
|
-
|
|
323
|
-
# If we are long the market, check for an exit
|
|
324
|
-
elif prediction < 0 and self.long_market[symbol]:
|
|
325
|
-
signal = SignalEvent(1, symbol, dt, "EXIT", price=price)
|
|
326
|
-
print(f"{dt}: EXIT LONG")
|
|
327
|
-
self.long_market[symbol] = False
|
|
328
|
-
|
|
329
|
-
if regime == "LONG":
|
|
330
|
-
# If we are not in the market, go long
|
|
331
|
-
if prediction > 0 and not self.long_market[symbol]:
|
|
332
|
-
signal = SignalEvent(
|
|
333
|
-
1,
|
|
334
|
-
symbol,
|
|
335
|
-
dt,
|
|
336
|
-
"LONG",
|
|
337
|
-
quantity=self.qty[symbol],
|
|
338
|
-
price=price,
|
|
339
|
-
)
|
|
340
|
-
print(f"{dt}: LONG")
|
|
341
|
-
self.long_market[symbol] = True
|
|
342
|
-
|
|
343
|
-
elif regime == "SHORT":
|
|
344
|
-
# If we are not in the market, go short
|
|
345
|
-
if prediction < 0 and not self.short_market[symbol]:
|
|
346
|
-
signal = SignalEvent(
|
|
347
|
-
1,
|
|
348
|
-
symbol,
|
|
349
|
-
dt,
|
|
350
|
-
"SHORT",
|
|
351
|
-
quantity=self.qty[symbol],
|
|
352
|
-
price=price,
|
|
353
|
-
)
|
|
354
|
-
print(f"{dt}: SHORT")
|
|
355
|
-
self.short_market[symbol] = True
|
|
356
|
-
signals[symbol] = signal
|
|
357
|
-
return signals
|
|
358
|
-
|
|
359
|
-
def get_live_data(self):
|
|
360
|
-
symbol_data = {symbol: None for symbol in self.symbol_list}
|
|
361
|
-
for symbol in self.symbol_list:
|
|
362
|
-
arch_data = Rates(symbol, self.tf, 0, self.arima_window)
|
|
363
|
-
rates = arch_data.get_rates_from_pos()
|
|
364
|
-
arch_returns = self.arima_models[symbol].load_and_prepare_data(rates)
|
|
365
|
-
window_data = arch_returns["diff_log_return"].iloc[-self.arima_window :]
|
|
366
|
-
hmm_returns = arch_data.returns.values[-self.risk_window :]
|
|
367
|
-
symbol_data[symbol] = (window_data, hmm_returns)
|
|
368
|
-
return symbol_data
|
|
369
|
-
|
|
370
|
-
def create_live_signals(self):
|
|
371
|
-
signals = {symbol: None for symbol in self.symbol_list}
|
|
372
|
-
data = self.get_live_data()
|
|
373
|
-
for symbol in self.symbol_list:
|
|
374
|
-
symbol_data = data[symbol]
|
|
375
|
-
if symbol_data is not None:
|
|
376
|
-
window_data, hmm_returns = symbol_data
|
|
377
|
-
prediction = self.arima_models[symbol].get_prediction(window_data)
|
|
378
|
-
regime = self.risk_models[symbol].which_trade_allowed(hmm_returns)
|
|
379
|
-
if regime == "LONG":
|
|
380
|
-
if prediction > 0:
|
|
381
|
-
signals[symbol] = "LONG"
|
|
382
|
-
elif regime == "SHORT":
|
|
383
|
-
if prediction < 0:
|
|
384
|
-
signals[symbol] = "SHORT"
|
|
385
|
-
return signals
|
|
386
|
-
|
|
387
|
-
def calculate_signals(self, event=None):
|
|
388
|
-
if self.mode == TradingMode.BACKTEST and event is not None:
|
|
389
|
-
if event.type == Events.MARKET:
|
|
390
|
-
signals = self.create_backtest_signal()
|
|
391
|
-
for signal in signals.values():
|
|
392
|
-
if signal is not None:
|
|
393
|
-
self.events.put(signal)
|
|
394
|
-
elif self.mode == TradingMode.LIVE:
|
|
395
|
-
return self.create_live_signals()
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
class KalmanFilterStrategy(Strategy):
|
|
399
|
-
"""
|
|
400
|
-
The `KalmanFilterStrategy` class implements a backtesting framework for a
|
|
401
|
-
[pairs trading](https://en.wikipedia.org/wiki/Pairs_trade) strategy using
|
|
402
|
-
Kalman Filter for signals and Hidden Markov Models (HMM) for risk management.
|
|
403
|
-
This document outlines the structure and usage of the `KalmanFilterStrategy`,
|
|
404
|
-
including initialization parameters, main functions, and an example of how to run a backtest.
|
|
405
|
-
"""
|
|
406
|
-
|
|
407
|
-
def __init__(
|
|
408
|
-
self,
|
|
409
|
-
bars: DataHandler = None,
|
|
410
|
-
events: Queue = None,
|
|
411
|
-
symbol_list: List[str] = None,
|
|
412
|
-
mode: TradingMode = TradingMode.BACKTEST,
|
|
413
|
-
**kwargs,
|
|
414
|
-
):
|
|
415
|
-
"""
|
|
416
|
-
Args:
|
|
417
|
-
`bars`: `DataHandler` for market data handling.
|
|
418
|
-
`events`: A queue for managing events.
|
|
419
|
-
`symbol_list`: List of ticker symbols for the pairs trading strategy.
|
|
420
|
-
`mode`: Mode of operation for the strategy.
|
|
421
|
-
kwargs : Additional keyword arguments including
|
|
422
|
-
- `quantity`: Quantity of assets to trade. Default is 100.
|
|
423
|
-
- `hmm_window`: Window size for calculating returns for the HMM. Default is 50.
|
|
424
|
-
- `hmm_tiker`: Ticker symbol used by the HMM for risk management.
|
|
425
|
-
- `time_frame`: Time frame for the data. Default is 'D1'.
|
|
426
|
-
- `session_duration`: Duration of the trading session. Default is 6.5.
|
|
427
|
-
"""
|
|
428
|
-
self.bars = bars
|
|
429
|
-
self.events_queue = events
|
|
430
|
-
self.symbol_list = symbol_list or self.bars.symbol_list
|
|
431
|
-
self.mode = mode
|
|
432
|
-
|
|
433
|
-
self.hmm_tiker = kwargs.get("hmm_tiker")
|
|
434
|
-
self._assert_tikers()
|
|
435
|
-
self.account = Account(**kwargs)
|
|
436
|
-
self.hmm_window = kwargs.get("hmm_window", 50)
|
|
437
|
-
self.qty = kwargs.get("quantity", 100)
|
|
438
|
-
self.tf = kwargs.get("time_frame", "D1")
|
|
439
|
-
self.sd = kwargs.get("session_duration", 6.5)
|
|
440
|
-
|
|
441
|
-
self.risk_model = build_hmm_models(self.symbol_list, **kwargs)
|
|
442
|
-
self.kl_model = KalmanFilterModel(self.tickers, **kwargs)
|
|
443
|
-
|
|
444
|
-
self.long_market = False
|
|
445
|
-
self.short_market = False
|
|
446
|
-
|
|
447
|
-
def _assert_tikers(self):
|
|
448
|
-
if self.symbol_list is None or len(self.symbol_list) != 2:
|
|
449
|
-
raise ValueError("A list of 2 Tickers must be provide for this strategy")
|
|
450
|
-
self.tickers = self.symbol_list
|
|
451
|
-
if self.hmm_tiker is None:
|
|
452
|
-
raise ValueError(
|
|
453
|
-
"You need to provide a ticker used by the HMM for risk management"
|
|
454
|
-
)
|
|
455
|
-
|
|
456
|
-
def calculate_btxy(self, etqt, regime, dt):
|
|
457
|
-
# Make sure there is no position open
|
|
458
|
-
if etqt is None:
|
|
459
|
-
return
|
|
460
|
-
et, sqrt_Qt = etqt
|
|
461
|
-
theta = self.kl_model.theta
|
|
462
|
-
p1 = self.bars.get_latest_bar_value(self.tickers[1], "adj_close")
|
|
463
|
-
p0 = self.bars.get_latest_bar_value(self.tickers[0], "adj_close")
|
|
464
|
-
if et >= -sqrt_Qt and self.long_market:
|
|
465
|
-
print("CLOSING LONG: %s" % dt)
|
|
466
|
-
y_signal = SignalEvent(1, self.tickers[1], dt, "EXIT", price=p1)
|
|
467
|
-
x_signal = SignalEvent(1, self.tickers[0], dt, "EXIT", price=p0)
|
|
468
|
-
self.events_queue.put(y_signal)
|
|
469
|
-
self.events_queue.put(x_signal)
|
|
470
|
-
self.long_market = False
|
|
471
|
-
|
|
472
|
-
elif et <= sqrt_Qt and self.short_market:
|
|
473
|
-
print("CLOSING SHORT: %s" % dt)
|
|
474
|
-
y_signal = SignalEvent(1, self.tickers[1], dt, "EXIT", price=p1)
|
|
475
|
-
x_signal = SignalEvent(1, self.tickers[0], dt, "EXIT", price=p0)
|
|
476
|
-
self.events_queue.put(y_signal)
|
|
477
|
-
self.events_queue.put(x_signal)
|
|
478
|
-
self.short_market = False
|
|
479
|
-
|
|
480
|
-
# Long Entry
|
|
481
|
-
if regime == "LONG":
|
|
482
|
-
if et <= -sqrt_Qt and not self.long_market:
|
|
483
|
-
print("LONG: %s" % dt)
|
|
484
|
-
y_signal = SignalEvent(
|
|
485
|
-
1, self.tickers[1], dt, "LONG", self.qty, 1.0, price=p1
|
|
486
|
-
)
|
|
487
|
-
x_signal = SignalEvent(
|
|
488
|
-
1, self.tickers[0], dt, "SHORT", self.qty, theta[0], price=p0
|
|
489
|
-
)
|
|
490
|
-
self.events_queue.put(y_signal)
|
|
491
|
-
self.events_queue.put(x_signal)
|
|
492
|
-
self.long_market = True
|
|
493
|
-
|
|
494
|
-
# Short Entry
|
|
495
|
-
elif regime == "SHORT":
|
|
496
|
-
if et >= sqrt_Qt and not self.short_market:
|
|
497
|
-
print("SHORT: %s" % dt)
|
|
498
|
-
y_signal = SignalEvent(
|
|
499
|
-
1, self.tickers[1], dt, "SHORT", self.qty, 1.0, price=p1
|
|
500
|
-
)
|
|
501
|
-
x_signal = SignalEvent(
|
|
502
|
-
1, self.tickers[0], "LONG", self.qty, theta[0], price=p0
|
|
503
|
-
)
|
|
504
|
-
self.events_queue.put(y_signal)
|
|
505
|
-
self.events_queue.put(x_signal)
|
|
506
|
-
self.short_market = True
|
|
507
|
-
|
|
508
|
-
def calculate_livexy(self):
|
|
509
|
-
signals = {symbol: None for symbol in self.symbol_list}
|
|
510
|
-
p0_price = self.account.get_tick_info(self.tickers[0]).ask
|
|
511
|
-
p1_price = self.account.get_tick_info(self.tickers[1]).ask
|
|
512
|
-
prices = np.array([p0_price, p1_price])
|
|
513
|
-
et_std = self.kl_model.calculate_etqt(prices)
|
|
514
|
-
if et_std is not None:
|
|
515
|
-
et, std = et_std
|
|
516
|
-
y_signal = None
|
|
517
|
-
x_signal = None
|
|
518
|
-
|
|
519
|
-
if et >= -std or et <= std:
|
|
520
|
-
y_signal = "EXIT"
|
|
521
|
-
x_signal = "EXIT"
|
|
522
|
-
|
|
523
|
-
if et <= -std:
|
|
524
|
-
y_signal = "LONG"
|
|
525
|
-
x_signal = "SHORT"
|
|
526
|
-
|
|
527
|
-
if et >= std:
|
|
528
|
-
y_signal = "SHORT"
|
|
529
|
-
x_signal = "LONG"
|
|
530
|
-
|
|
531
|
-
signals[self.tickers[0]] = x_signal
|
|
532
|
-
signals[self.tickers[1]] = y_signal
|
|
533
|
-
return signals
|
|
534
|
-
|
|
535
|
-
def calculate_backtest_signals(self):
|
|
536
|
-
p0, p1 = self.tickers[0], self.tickers[1]
|
|
537
|
-
dt = self.bars.get_latest_bar_datetime(p0)
|
|
538
|
-
x = self.bars.get_latest_bar_value(p0, "close")
|
|
539
|
-
y = self.bars.get_latest_bar_value(p1, "close")
|
|
540
|
-
returns = self.bars.get_latest_bars_values(
|
|
541
|
-
self.hmm_tiker, "returns", N=self.hmm_window
|
|
542
|
-
)
|
|
543
|
-
latest_prices = np.array([-1.0, -1.0])
|
|
544
|
-
if len(returns) >= self.hmm_window:
|
|
545
|
-
latest_prices[0] = x
|
|
546
|
-
latest_prices[1] = y
|
|
547
|
-
et_qt = self.kl_model.calculate_etqt(latest_prices)
|
|
548
|
-
regime = self.risk_model[self.hmm_tiker].which_trade_allowed(returns)
|
|
549
|
-
self.calculate_btxy(et_qt, regime, dt)
|
|
550
|
-
|
|
551
|
-
def calculate_live_signals(self):
|
|
552
|
-
# Data Retrieval
|
|
553
|
-
signals = {symbol: None for symbol in self.symbol_list}
|
|
554
|
-
initial_signals = self.calculate_livexy()
|
|
555
|
-
hmm_data = Rates(self.hmm_ticker, self.tf, 0, self.hmm_window)
|
|
556
|
-
returns = hmm_data.returns.values
|
|
557
|
-
current_regime = self.risk_model[self.hmm_tiker].which_trade_allowed(returns)
|
|
558
|
-
for symbol in self.symbol_list:
|
|
559
|
-
if symbol in initial_signals:
|
|
560
|
-
signal = initial_signals[symbol]
|
|
561
|
-
if signal == "LONG" and current_regime == "LONG":
|
|
562
|
-
signals[symbol] = "LONG"
|
|
563
|
-
elif signal == "SHORT" and current_regime == "SHORT":
|
|
564
|
-
signals[symbol] = "SHORT"
|
|
565
|
-
return signals
|
|
566
|
-
|
|
567
|
-
def calculate_signals(self, event=None):
|
|
568
|
-
"""
|
|
569
|
-
Calculate the Kalman Filter strategy.
|
|
570
|
-
"""
|
|
571
|
-
if self.mode == TradingMode.BACKTEST and event is not None:
|
|
572
|
-
if event.type == Events.MARKET:
|
|
573
|
-
self.calculate_backtest_signals()
|
|
574
|
-
elif self.mode == TradingMode.LIVE:
|
|
575
|
-
return self.calculate_live_signals()
|
|
576
|
-
|
|
577
47
|
|
|
578
48
|
class StockIndexSTBOTrading(Strategy):
|
|
579
49
|
"""
|
|
@@ -778,50 +248,6 @@ def _run_backtest(strategy_name: str, capital: float, symbol_list: list, kwargs:
|
|
|
778
248
|
engine.simulate_trading()
|
|
779
249
|
|
|
780
250
|
|
|
781
|
-
def _run_arch_backtest(capital: float = 100000.0, quantity: int = 1000):
|
|
782
|
-
hmm_data = yf.download("^GSPC", start="1990-01-01", end="2009-12-31")
|
|
783
|
-
kwargs = {
|
|
784
|
-
"quantity": quantity,
|
|
785
|
-
"yf_start": "2010-01-04",
|
|
786
|
-
"hmm_data": hmm_data,
|
|
787
|
-
"backtester_class": ArimaGarchStrategy,
|
|
788
|
-
"data_handler": YFDataHandler,
|
|
789
|
-
}
|
|
790
|
-
_run_backtest("ARIMA+GARCH & HMM", capital, ["^GSPC"], kwargs)
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
def _run_kf_backtest(capital: float = 100000.0, quantity: int = 2000):
|
|
794
|
-
symbol_list = ["IEI", "TLT"]
|
|
795
|
-
tlt = yf.download("TLT", end="2008-07-09")
|
|
796
|
-
iei = yf.download("IEI", end="2008-07-09")
|
|
797
|
-
kwargs = {
|
|
798
|
-
"quantity": quantity,
|
|
799
|
-
"yf_start": "2009-08-03",
|
|
800
|
-
"hmm_data": {"IEI": iei, "TLT": tlt},
|
|
801
|
-
"hmm_tiker": "TLT",
|
|
802
|
-
"session_duration": 6.5,
|
|
803
|
-
"backtester_class": KalmanFilterStrategy,
|
|
804
|
-
"data_handler": YFDataHandler,
|
|
805
|
-
}
|
|
806
|
-
_run_backtest("Kalman Filter & HMM", capital, symbol_list, kwargs)
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
def _run_sma_backtest(capital: float = 100000.0, quantity: int = 1):
|
|
810
|
-
spx_data = yf.download("^GSPC", start="1990-01-01", end="2009-12-31")
|
|
811
|
-
kwargs = {
|
|
812
|
-
"quantities": quantity,
|
|
813
|
-
"hmm_end": "2009-12-31",
|
|
814
|
-
"yf_start": "2010-01-04",
|
|
815
|
-
"hmm_data": spx_data,
|
|
816
|
-
"mt5_start": datetime(2010, 1, 1),
|
|
817
|
-
"mt5_end": datetime(2023, 1, 1),
|
|
818
|
-
"backtester_class": SMAStrategy,
|
|
819
|
-
"data_handler": MT5DataHandler,
|
|
820
|
-
"exc_handler": MT5ExecutionHandler,
|
|
821
|
-
}
|
|
822
|
-
_run_backtest("SMA & HMM", capital, ["[SP500]"], kwargs)
|
|
823
|
-
|
|
824
|
-
|
|
825
251
|
def _run_sistbo_backtest(capital: float = 100000.0, quantity: int = None):
|
|
826
252
|
ndx = "[NQ100]"
|
|
827
253
|
spx = "[SP500]"
|
|
@@ -845,16 +271,8 @@ def _run_sistbo_backtest(capital: float = 100000.0, quantity: int = None):
|
|
|
845
271
|
_run_backtest("Stock Index Short Term Buy Only ", capital, symbol_list, kwargs)
|
|
846
272
|
|
|
847
273
|
|
|
848
|
-
_BACKTESTS = {
|
|
849
|
-
"sma": _run_sma_backtest,
|
|
850
|
-
"klf": _run_kf_backtest,
|
|
851
|
-
"arch": _run_arch_backtest,
|
|
852
|
-
"sistbo": _run_sistbo_backtest,
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
|
|
856
274
|
def test_strategy(
|
|
857
|
-
strategy: Literal["
|
|
275
|
+
strategy: Literal["sistbo"] = "sistbo",
|
|
858
276
|
quantity: Optional[int] = 100,
|
|
859
277
|
):
|
|
860
278
|
"""
|
|
@@ -862,14 +280,13 @@ def test_strategy(
|
|
|
862
280
|
|
|
863
281
|
Args:
|
|
864
282
|
strategy : The strategy to use in test mode. Default is `sma`.
|
|
865
|
-
- `sma` Execute `SMAStrategy`, for more detail see this class documentation.
|
|
866
|
-
- `klf` Execute `KalmanFilterStrategy`, for more detail see this class documentation.
|
|
867
|
-
- `arch` Execute `ArimaGarchStrategy`, for more detail see this class documentation.
|
|
868
283
|
- `sistbo` Execute `StockIndexSTBOTrading`, for more detail see this class documentation.
|
|
869
284
|
quantity : The quantity of assets to be used in the test backtest. Default is 1000.
|
|
870
285
|
|
|
871
286
|
"""
|
|
872
|
-
if strategy
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
287
|
+
if strategy != "sistbo":
|
|
288
|
+
raise ValueError(
|
|
289
|
+
"Only 'sistbo' strategy is available for testing at the moment."
|
|
290
|
+
)
|
|
291
|
+
_run_sistbo_backtest(quantity=quantity)
|
|
292
|
+
|