In this article we will:
Construct a mean-reverting portfolio
Backtest a simple mean-reverting strategy
Download and Visualise Data
data = yf.download(['AAPL', 'GOOG'])['Adj Close'].dropna()["2012-01-01":]
data.plot(logy=True)
Fit a Linear Model on the Data
x = data['GOOG']
y = data['AAPL']
y = sm.add_constant(y)
model = sm.OLS(x, y).fit()
print(model.summary())
intercept = model.params[0]
slope = model.params[1]
print("Intercept:", intercept)
print("Slope:", slope)
Visualise the Linear Model Fit
plt.scatter(data['AAPL'], data['GOOG'], color='blue', label='Data', )
x_pred = sm.add_constant(data['AAPL'])
y_pred = model.predict(x_pred)
plt.plot(data['AAPL'], y_pred, color='red', label='Linear Regression')
plt.xlabel('AAPL')
plt.ylabel('GOOG')
plt.legend()
plt.show()
Construct a Mean-Reverting Portfolio using the Linear Regression fit
stationary_train = data['GOOG'] - slope * data['AAPL']
stationary_train.plot()
Backtest: Simple Mean-Reversion strategy
Rolling Linear Regression Hedge Ratio
#add constant column to regress with intercept
data['const'] = 1
#fit
model = RollingOLS(
endog=data['AAPL'].values ,
exog=data[['const','GOOG']],
window=30
)
rres = model.fit()
First, we fit a linear regression model on a rolling window of 30 days worth of data.
Mean Reverting Portfolio Construction
df = rres.params
df['slope'] = df['GOOG']
df['AAPL'] = data['AAPL']
df['GOOG'] = data['GOOG']
df['portfolio'] = df['GOOG'] - df['slope'] * df['AAPL']
df['portfolio daily return'] = np.log(df['portfolio'] / df['portfolio'].shift(1))
Next, we construct a mean-reverting portfolio. Note that we are now using a rolling hedge ratio.
Trading Signal:
\(p_i = - \frac{x_i-\mu_i}{\sigma_i}\)
We use the z-score as our signal. The z-score is a measure of the number of standard deviations our portfolio is away from the mean.
df['position'] = - (df['portfolio'] - df['portfolio'].rolling(30).mean()) / df['portfolio'].rolling(30).std()
Backtest Results
df['daily pnl'] = df['portfolio daily return'] * df['position'].shift(1)
df['cum pnl'] = df['daily pnl'].cumsum()
df['cum pnl'].plot()
The backtest shows a good performance from 2012 to 2020 with consistent positive pnl.
However, the strategy has been flat since 2020 which seems to imply that the alpha has decayed from 2020 onwards.