Fit the spread on formation data
Engle–Granger tests the residual for a unit root using critical values that account for having estimated and . Its null is no cointegration. A low p-value supports a stationary spread under the selected specification and sample. It also matters which series is chosen as the dependent variable in finite samples. Select the orientation and deterministic terms before interpreting the test.
Translate a spread signal into actual positions
Suppose a pair has , a formation spread mean of zero and a standard deviation of 0.03. Today's spread of 0.075 is 2.5 standard deviations above its fitted mean. With a positive hedge coefficient, an illustrative short-spread position would short one unit of dollar notional in A and buy 1.2 units in B. If the spread narrows to 0.03 before the exit, the 0.045 log spread change helps explain the direction of gross P&L. Actual P&L depends on both legs' percentage moves, positions, timing, financing, commissions, and borrow costs.
The hedge coefficient is fitted in log-price units. If you use the spread as a signal, you still need to define the dollar exposure, convert it to shares at executable prices, and handle the cost of holding the short. Re-estimating every day can change the reported spread even when neither price moves. Treat a refit as an explicit model decision and charge for any resulting trade. In validation, compare a frozen formation fit with your scheduled refitting policy so you can see whether the policy itself is helping.
Run the Engle–Granger test and freeze the fit
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.tsa.stattools import coint
rng = np.random.default_rng(7)
x = 4 + np.cumsum(rng.normal(0, 0.015, 600))
e = np.zeros(600)
for t in range(1, len(e)):
e[t] = 0.7 * e[t - 1] + rng.normal(0, 0.015)
logs = pd.DataFrame({"x": x, "y": 0.4 + 1.2 * x + e})
formation, later = logs.iloc[:400], logs.iloc[400:]
_, p_value, _ = coint(formation.y, formation.x, trend="c")
fit = sm.OLS(formation.y, sm.add_constant(formation.x)).fit()
later_spread = later.y - fit.predict(sm.add_constant(later.x))
z = (later_spread - fit.resid.mean()) / fit.resid.std()
print("Formation test p-value:", p_value)
print("First later signals:", z.head().round(2).tolist())This forms signals from synthetic log prices. A trading simulation would timestamp orders after those signals and model fills on both legs.
What to check
- Using ordinary ADF critical values on the fitted spread.
- Selecting a pair using future data or testing hundreds of pairs without tracking multiple comparisons.
- Re-estimating the hedge ratio with data that arrives after the signal.