This strategy is taken from Example 3.3 in Ernie Chan's book, Algorithmic Trading: Winning Strategies and Their Rationale. It's been posted here previously but that used constants for the linear regression coefficients.

In this case, a Kalman filter is used to dynamically update the linear regression coefficients between the EWA and EWC ETFs. It performs well up to 2009 but after that the performance degrades.

Clone Algorithm

3994

Loading...

There was an error loading this backtest.
Retry

Backtest from
to
with
initial capital

Cumulative performance:

Algorithm
Benchmark

Custom data:

Total Returns

--

Alpha

--

Beta

--

Sharpe

--

Sortino

--

Max Drawdown

--

Benchmark Returns

--

Volatility

--

Returns | 1 Month | 3 Month | 6 Month | 12 Month |

Alpha | 1 Month | 3 Month | 6 Month | 12 Month |

Beta | 1 Month | 3 Month | 6 Month | 12 Month |

Sharpe | 1 Month | 3 Month | 6 Month | 12 Month |

Sortino | 1 Month | 3 Month | 6 Month | 12 Month |

Volatility | 1 Month | 3 Month | 6 Month | 12 Month |

Max Drawdown | 1 Month | 3 Month | 6 Month | 12 Month |

import numpy as np import pytz def initialize(context): context.ewa = sid(14516) context.ewc = sid(14517) context.delta = 0.0001 context.Vw = context.delta / (1 - context.delta) * np.eye(2) context.Ve = 0.001 context.beta = np.zeros(2) context.P = np.zeros((2, 2)) context.R = None context.pos = None context.day = None set_slippage(slippage.FixedSlippage(spread=0)) set_commission(commission.PerShare(cost=0)) def handle_data(context, data): exchange_time = get_datetime().astimezone(pytz.timezone('US/Eastern')) # update Kalman filter and exectue a trade during the last 5 mins of trading each day if exchange_time.hour == 15 and exchange_time.minute >= 55: # only execute this once per day if context.day is not None and context.day == exchange_time.day: return context.day = exchange_time.day x = np.asarray([data[context.ewa].price, 1.0]).reshape((1, 2)) y = data[context.ewc].price # update Kalman filter with latest price if context.R is not None: context.R = context.P + context.Vw else: context.R = np.zeros((2, 2)) yhat = x.dot(context.beta) Q = x.dot(context.R).dot(x.T) + context.Ve sqrt_Q = np.sqrt(Q) e = y - yhat K = context.R.dot(x.T) / Q context.beta = context.beta + K.flatten() * e context.P = context.R - K * x.dot(context.R) record(beta=context.beta[0], alpha=context.beta[1]) if e < 5: record(spread=float(e), Q_upper=float(sqrt_Q), Q_lower=float(-sqrt_Q)) if context.pos is not None: if context.pos == 'long' and e > -sqrt_Q: #log.info('closing long') order_target(context.ewa, 0) order_target(context.ewc, 0) context.pos = None elif context.pos == 'short' and e < sqrt_Q: #log.info('closing short') order_target(context.ewa, 0) order_target(context.ewc, 0) context.pos = None if context.pos is None: if e < -sqrt_Q: #log.info('opening long') order(context.ewc, 1000) order(context.ewa, -1000 * context.beta[0]) context.pos = 'long' elif e > sqrt_Q: #log.info('opening short') order(context.ewc, -1000) order(context.ewa, 1000 * context.beta[0]) context.pos = 'short'

We have migrated this algorithm to work with a new version of the Quantopian API. The code is different than the original version, but the investment rationale of the algorithm has not changed. We've put everything you need to know here on one page.

This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.