Estimate a hedge ratio

Fit QQQ implied volatility on SPY implied volatility with ols in the Calculator, take the slope as the hedge ratio, and rank today's residual spread.

Goal

Regress QQQ 30-day implied volatility on SPY 30-day implied volatility in the Calculator. Take the slope as the hedge ratio, build the residual spread of QQQ minus the slope times SPY, and compute the z-score of today's spread.

Before you start

Open Trader Dash. Compare two or more series covers the correlation and the plain spread of the same two histories. This guide weights SPY by the slope of a fit before it subtracts.

ols(y, x) fits y on x by least squares and returns a column of coefficients: the intercept in row 0, the slope in row 1. iloc(m, i, j) returns the number in row i, column j. The full rules for both are in Matrices and regression.

Steps

  1. Click Add widget and select Calculator.

  2. Type the fit, with QQQ first and SPY second. The result line shows two rows: the intercept on top and the slope below it.

    ols(iv30.series("QQQ"), iv30.series("SPY")) => a 2 by 1 Matrix
    
  3. Wrap the fit in iloc with row 1 and column 0. The result line shows the slope alone.

    iloc(ols(iv30.series("QQQ"), iv30.series("SPY")), 1, 0) => a number of QQQ points per SPY point
    
  4. Replace the formula with the residual spread. Each side of the subtraction is cut to the other's dates with align. The result line shows 1163 samples, and a chart of the spread opens under it.

    align(iv30.series("QQQ"), iv30.series("SPY")) - iloc(ols(iv30.series("QQQ"), iv30.series("SPY")), 1, 0) * align(iv30.series("SPY"), iv30.series("QQQ")) => a Series of daily residual spreads
    
  5. Wrap the spread in zscore with a window of 252 days. The chart closes and the result line shows one number.

    zscore(align(iv30.series("QQQ"), iv30.series("SPY")) - iloc(ols(iv30.series("QQQ"), iv30.series("SPY")), 1, 0) * align(iv30.series("SPY"), iv30.series("QQQ")), 252) => a number
    

Result

  • Intercept: 0.0408, the QQQ implied volatility the fit predicts when SPY implied volatility is 0.
  • Slope: 1.0888, the hedge ratio. QQQ implied volatility moves 1.0888 points for each point of SPY implied volatility. A QQQ volatility position of one unit is hedged with 1.0888 units of SPY volatility.
  • Residual spread: the part of QQQ implied volatility that SPY does not explain, on each shared date.
  • Z-score: 0.21, today's spread compared with the last 252 trading days. Above 2 or below -2 means today's spread is rare for this pair.

The spread leaves out the intercept. A constant moves the spread and its mean by the same amount, so the z-score is the same with or without it.

The slope comes from every date both series have, the last 252 days included. Each formula uses history up to the previous trading day's close.

Other ways

Check the fit

ols.r2 returns the share of the variance of QQQ implied volatility that the fit explains, from 0 to 1. For this pair it is 0.927. A low R squared means the slope is a poor hedge ratio and the spread is mostly QQQ's own movement.

ols.r2(iv30.series("QQQ"), iv30.series("SPY")) => a number from 0 to 1

With one x, the slope equals the covariance of the two series over the variance of SPY. Use it to check the slope against cov.

iloc(cov(iv30.series("QQQ"), iv30.series("SPY")), 0, 1) / iloc(cov(iv30.series("QQQ"), iv30.series("SPY")), 1, 1) => a number equal to the ols slope

Rank the spread on every date

Wrap the spread in roll inside zscore to chart a z-score per date instead of today's alone. Series describes rolling windows.

zscore(roll(align(iv30.series("QQQ"), iv30.series("SPY")) - iloc(ols(iv30.series("QQQ"), iv30.series("SPY")), 1, 0) * align(iv30.series("SPY"), iv30.series("QQQ")), 252)) => a Series of daily z-scores

If something goes wrong

  • The slope is in SPY points per QQQ point: the arguments are in the wrong order. ols takes y first. Put QQQ first to get QQQ points per SPY point.
  • iloc returns E_ARITY: a matrix needs both positions. Write iloc(m, 1, 0), with the column.
  • E_DIM on the spread: one side of the subtraction is missing align. Wrap both sides, each with the other series as the second argument, as in step 4.
  • E_NO_DATA: the site has no history for that symbol, or the data has not loaded yet. Check the symbol, then edit the formula once to run it again.