Spread#
Hedge ratio estimators and spread series diagnostics.
SpreadSeries#
- class spreadpy.spread.spreadSeries.SpreadSeries(y: PriceTimeSeries, x: PriceTimeSeries, hedge_ratio_ts: Series, estimator_name: str = '')[source]#
Residual spread series \(s_t = y_t - \beta_t \cdot x_t\).
Given aligned price series y, x and a time-varying hedge ratio β_t, the spread is computed bar-by-bar. Provides stationarity diagnostics (Ornstein-Uhlenbeck half-life, ADF test) and a rolling z-score.
- Parameters:
y (PriceTimeSeries) – Dependent leg price series.
x (PriceTimeSeries) – Independent leg price series.
hedge_ratio_ts (pd.Series) – Time series of hedge ratios β_t, aligned (or reindexed + forward-filled) to
y.index.estimator_name (str) – Label for the estimator used (for display and repr).
- adf_statistic() Tuple[float, float][source]#
Compute the Augmented Dickey-Fuller test statistic for the spread.
Tests the null hypothesis \(H_0\) of a unit root (non-stationarity) against the alternative \(H_1\) of stationarity (mean-reversion). The ADF regression with one lag is:
\[\Delta s_t = \rho \cdot s_{t-1} + c + \delta \cdot \Delta s_{t-1} + \varepsilon_t\]A t-statistic on \(\rho\) that is sufficiently negative (p-value < 0.05) rejects \(H_0\) and supports cointegration of the pair.
Requires
statsmodels.- Returns:
Tuple
(adf_statistic, p_value).- Return type:
Tuple[float, float]
- half_life() float[source]#
Estimate the mean-reversion half-life of the spread via an Ornstein-Uhlenbeck OLS regression.
The discrete-time OU model is:
\[\Delta s_t = \lambda \cdot s_{t-1} + c + \varepsilon_t, \quad \varepsilon_t \sim \mathcal{N}(0, \sigma^2)\]where \(\lambda < 0\) indicates mean-reversion. The half-life is the time (in bars) for a deviation from equilibrium to decay by half:
\[\tau_{1/2} = -\frac{\ln 2}{\lambda}\]\(\lambda\) is estimated by regressing \(\Delta s_t\) on \(s_{t-1}\) (with constant) using OLS. If \(\lambda \geq 0\), the spread is non-stationary and the method returns
float('inf')with a warning.- Returns:
Mean-reversion half-life in bars.
float('inf')if the spread does not mean-revert.- Return type:
float
- rolling_adf(window: int) Series[source]#
Compute rolling ADF p-values with no lookahead.
At each bar t, runs the ADF test (one lag, constant term) on the preceding
windowbars of spread residuals. A p-value below 0.05 rejects the unit-root null and supports stationarity at that point.Bars with fewer than
windowpredecessors areNaN.- Parameters:
window (int) – Rolling window length (in bars).
- Returns:
Series of ADF p-values aligned with
self.index.- Return type:
pd.Series
- rolling_zscore(window: int) Series[source]#
Compute a rolling z-score of the spread with no lookahead.
At each bar \(t\), the z-score standardises \(s_t\) using statistics estimated over the preceding window of \(w\) bars:
\[\begin{split}\mu_{t,w} &= \frac{1}{w} \sum_{j=0}^{w-1} s_{t-j} \\ \sigma_{t,w} &= \mathrm{std}(s_{t-w+1}, \ldots, s_t) \quad (\mathrm{ddof}=1) \\ z_t &= \frac{s_t - \mu_{t,w}}{\sigma_{t,w}}\end{split}\]Bars \(t < w\) are set to NaN (insufficient history). This is the signal used by
ZScoreSignal.- Parameters:
window (int) – Rolling window length w (in bars).
- Returns:
z-score series aligned with
self.index.- Return type:
pd.Series
- slice(start, end) SpreadSeries[source]#
Return a new
SpreadSeriesrestricted to the closed interval [start, end].The underlying y, x, and β_t series are all sliced consistently, so the returned object is a self-contained SpreadSeries over the sub-period with no reference to the original data outside [start, end].
- Parameters:
start – Inclusive start timestamp (pd.Timestamp or compatible).
end – Inclusive end timestamp (pd.Timestamp or compatible).
- Returns:
Spread sub-series over [start, end].
- Return type:
HedgeRatioEstimator#
Abstract base class for all hedge ratio estimators.
- class spreadpy.spread.hedgeRatioEstimator.HedgeRatioEstimator[source]#
Abstract base class for hedge ratio estimators.
Given two price series \(y\) (dependent leg) and \(x\) (independent leg), an estimator produces a time series \(\beta_t\) such that the spread
\[s_t = y_t - \beta_t \cdot x_t\]is (ideally) stationary. All concrete subclasses must implement
fit(), which returns apd.Seriesof hedge ratios aligned withy.index.- compute_spread(y: PriceTimeSeries, x: PriceTimeSeries) SpreadSeries[source]#
Fit the estimator and return the residual spread in a single call.
Equivalent to
SpreadSeries(y, x, self.fit(y, x)). The spread is defined bar-by-bar as:\[s_t = y_t - \beta_t \cdot x_t\]- Parameters:
y (PriceTimeSeries) – Dependent-leg price series.
x (PriceTimeSeries) – Independent-leg price series.
- Returns:
Residual spread series with diagnostics.
- Return type:
- abstractmethod fit(y: PriceTimeSeries, x: PriceTimeSeries) Series[source]#
Estimate the hedge ratio series β_t from price series y and x.
The returned series satisfies:
\[s_t = y_t - \beta_t \cdot x_t\]where \(s_t\) is (ideally) stationary. Implementations must ensure \(\beta_t\) is free of lookahead bias: at each bar \(t\), \(\beta_t\) may only depend on observations \(\{(y_s, x_s) : s \leq t\}\).
- Parameters:
y (PriceTimeSeries) – Dependent-leg price series.
x (PriceTimeSeries) – Independent-leg price series.
- Returns:
Time series of hedge ratios aligned with
y.index.- Return type:
pd.Series
ConstantOLS#
- class spreadpy.spread.hedgeRatio.constantOLS.ConstantOLS(add_intercept: bool = True)[source]#
Full-sample OLS hedge ratio estimator.
Fits a single linear regression over the entire supplied period:
\[y_t = \alpha + \beta \cdot x_t + \varepsilon_t\]and returns the constant slope \(\beta\) as the hedge ratio for all bars. Suitable as a baseline when the cointegration relationship is stable.
After calling
fit(), the fitted values are available asbeta_,alpha_, andr_squared_.- Parameters:
add_intercept (bool) – If True (default), fits with an intercept \(\alpha\). If False, forces the regression through the origin (\(\alpha = 0\)).
- fit(y: PriceTimeSeries, x: PriceTimeSeries) Series[source]#
Estimate a single hedge ratio \(\beta\) via full-sample OLS and return it as a constant series.
With intercept (default): solves the normal equations
\[[\beta,\, \alpha]^\top = (X^\top X)^{-1} X^\top y, \quad X = [x \mid \mathbf{1}]\]Without intercept: uses the closed-form projection
\[\beta = \frac{x^\top y}{x^\top x}\]After fitting,
beta_,alpha_, andr_squared_are set. The coefficient of determination is:\[R^2 = 1 - \frac{SS_{\mathrm{res}}}{SS_{\mathrm{tot}}}, \quad SS_{\mathrm{res}} = \|y - \hat{\beta}\, x - \hat{\alpha}\|^2, \quad SS_{\mathrm{tot}} = \|y - \bar{y}\|^2\]- Parameters:
y (PriceTimeSeries) – Dependent-leg price series.
x (PriceTimeSeries) – Independent-leg price series.
- Returns:
Constant hedge ratio \(\beta\) broadcast over
y.index.- Return type:
pd.Series
RollingOLS#
- class spreadpy.spread.hedgeRatio.rollingOLS.RollingOLS(window: int = 60, add_intercept: bool = True)[source]#
Rolling OLS hedge ratio estimator.
At each bar \(t\), fits OLS on the most recent
windowobservations:\[y_s = \alpha + \beta_t \cdot x_s + \varepsilon_s, \quad s \in [t - w + 1,\; t]\]This adapts to structural shifts in the cointegration relationship without the complexity of a state-space model. Bars before the first complete window are filled with the first available estimate (forward-fill).
- Parameters:
window (int) – Number of bars in each rolling regression (must be \(\geq 2\)).
add_intercept (bool) – If True (default), includes an intercept term \(\alpha\).
- fit(y: PriceTimeSeries, x: PriceTimeSeries) Series[source]#
Estimate a time-varying hedge ratio \(\beta_t\) via rolling OLS.
At each bar \(t \geq w - 1\), the hedge ratio is the OLS slope over the most recent \(w\) observations:
\[\beta_t = \operatorname{argmin}_{\beta,\, \alpha} \sum_{s=t-w+1}^{t} (y_s - \alpha - \beta\, x_s)^2\]whose solution is:
\[\beta_t = \frac{\mathrm{Cov}_w[x,\, y]}{\mathrm{Var}_w[x]}\]where \(\mathrm{Cov}_w\) and \(\mathrm{Var}_w\) denote the sample covariance and variance over the rolling window. For the first \(w - 1\) bars (warm-up), the first available estimate is forward-filled.
- Parameters:
y (PriceTimeSeries) – Dependent-leg price series.
x (PriceTimeSeries) – Independent-leg price series.
- Returns:
Time series of hedge ratios \(\beta_t\) aligned with
y.index.- Return type:
pd.Series
KalmanFilter#
- class spreadpy.spread.hedgeRatio.kalmanFilter.KalmanFilterParams(sigma2_eps: float, sigma2_mu: float, sigma2_gam: float, mu0: float, gam0: float, P0: ndarray)[source]#
Hyperparameters for the
KalmanFilterstate-space model.Derived from an OLS bootstrap on the first
ls_windowbars, following the heuristic of Palomar (2024), §15.6.3.- Parameters:
sigma2_eps (float) – Observation noise variance \(\sigma^2_\varepsilon\).
sigma2_mu (float) – State noise variance for the intercept, \(\sigma^2_\mu = \alpha \cdot \sigma^2_\varepsilon\).
sigma2_gam (float) – State noise variance for the hedge ratio, \(\sigma^2_\gamma = \alpha \cdot \sigma^2_\varepsilon / \mathrm{Var}[x]\).
mu0 (float) – Initial intercept estimate \(\mu_1\) (from OLS).
gam0 (float) – Initial hedge ratio estimate \(\gamma_1\) (from OLS).
P0 (np.ndarray) – Initial state covariance matrix (\(2 \times 2\), diagonal).
- class spreadpy.spread.hedgeRatio.kalmanFilter.KalmanFilter(alpha: float = 1e-05, ls_window: int | None = None, add_intercept: bool = True)[source]#
Kalman filter hedge ratio estimator with two latent states \([\mu_t,\, \gamma_t]\).
Implements the pairs-trading state-space model of Palomar (2024), §15.6.3, eq. (15.3).
Observation equation:
\[y_t = \mu_t + \gamma_t x_t + \varepsilon_t, \quad \varepsilon_t \sim \mathcal{N}(0, \sigma^2_\varepsilon)\]Transition equations (independent random walks):
\[\begin{split}\mu_{t+1} &= \mu_t + \eta_{\mu,t}, \quad \eta_{\mu,t} \sim \mathcal{N}(0, \sigma^2_\mu) \\ \gamma_{t+1} &= \gamma_t + \eta_{\gamma,t}, \quad \eta_{\gamma,t} \sim \mathcal{N}(0, \sigma^2_\gamma)\end{split}\]The hedge ratio returned by
fit()is the one-step-ahead predictive \(\gamma_{t|t-1}\), free of lookahead bias. The normalised spread is:\[z_t = \frac{y_t - \gamma_{t|t-1}\, x_t - \mu_{t|t-1}}{1 + \gamma_{t|t-1}}\]Hyperparameters are initialised via an OLS bootstrap on the first
ls_windowbars (Palomar, §15.6.3):\[\sigma^2_\varepsilon = \mathrm{Var}[\varepsilon^{\mathrm{OLS}}], \quad \sigma^2_\mu = \alpha \cdot \sigma^2_\varepsilon, \quad \sigma^2_\gamma = \frac{\alpha \cdot \sigma^2_\varepsilon}{\mathrm{Var}[x]}\]- Parameters:
alpha (float) – Controls state noise relative to observation noise. Smaller values → slower adaptation of \(\mu\) and \(\gamma\). Typical range: 1e-6 to 1e-4. Palomar uses 1e-5 for this model.
ls_window (Optional[int]) – Number of bars for the OLS initialisation. None uses the entire series (recommended: pass
train_sizefrom the engine).add_intercept (bool) – If
False, the intercept state \(\mu_t\) is pinned to zero for the entire filter run (mu0=0,P0[0,0]=0,sigma2_mu=0). The Kalman gain on \(\mu\) is then identically zero at every step, so the state vector effectively reduces to a 1-state model without any structural change to_run_filter. Useful when the relationship betweenyandxis expected to be purely proportional (e.g. realised-variance series where a constant offset is not meaningful). Default:True.
Note
Use log-prices. The filter assumes constant observation noise \(\sigma^2_\varepsilon\). With raw prices, \(\sigma^2_\varepsilon \propto \text{price}^2\), making the Kalman gain \(K_t\) systematically mis-calibrated when prices drift. Passing
log(y),log(x)makes \(\sigma^2_\varepsilon\) approximately homoscedastic and gives more stable hedge ratio estimates. InBacktestEngine, setlog_prices=Trueto apply this automatically while keeping actual prices for P&L accounting.- fit(y: PriceTimeSeries, x: PriceTimeSeries) Series[source]#
Run the Kalman filter on (y, x) and return the one-step-ahead predictive hedge ratio \(\gamma_{t|t-1}\).
Using the predictive state rather than the filtered state \(\gamma_{t|t}\) ensures no lookahead bias: at bar \(t\), \(\gamma_{t|t-1}\) is computed from observations \(\{(y_s, x_s) : s \leq t-1\}\) only.
After calling this method the following attributes are set:
params_— fittedKalmanFilterParamsmu_ts_— filtered intercept series \(\mu_{t|t}\)normalized_spread_— lookahead-free normalised spread\[z_t = \frac{y_t - \gamma_{t|t-1}\, x_t - \mu_{t|t-1}}{1 + \gamma_{t|t-1}}\]
- Parameters:
y (PriceTimeSeries) – Dependent-leg price series.
x (PriceTimeSeries) – Independent-leg price series.
- Returns:
Predictive hedge ratio series \(\gamma_{t|t-1}\) aligned with
y.index.- Return type:
pd.Series
KalmanFilterWithVelocity#
- class spreadpy.spread.hedgeRatio.kalmanFilterWithVelocity.KalmanFilterWithVelocityParams(sigma2_eps: float, sigma2_mu: float, sigma2_gam: float, sigma2_dgam: float, mu0: float, gam0: float, P0: ndarray)[source]#
Hyperparameters for the
KalmanFilterWithVelocitystate-space model.Derived from an OLS bootstrap following Palomar (2024), §15.6.3, eq. (15.4).
- Parameters:
sigma2_eps (float) – Observation noise variance \(\sigma^2_\varepsilon\).
sigma2_mu (float) – State noise variance for the intercept \(\mu_t\).
sigma2_gam (float) – State noise variance for the hedge ratio \(\gamma_t\).
sigma2_dgam (float) – State noise variance for the velocity \(\dot{\gamma}_t\).
mu0 (float) – Initial intercept estimate \(\mu_1\) (from OLS).
gam0 (float) – Initial hedge ratio estimate \(\gamma_1\) (from OLS).
P0 (np.ndarray) – Initial state covariance matrix (\(3 \times 3\), diagonal).
- class spreadpy.spread.hedgeRatio.kalmanFilterWithVelocity.KalmanFilterWithVelocity(alpha: float = 1e-06, alpha_dgam: float | None = None, ls_window: int | None = None, add_intercept: bool = True)[source]#
Kalman filter hedge ratio estimator with augmented state \([\mu_t,\, \gamma_t,\, \dot{\gamma}_t]\).
Extends
KalmanFilterwith a velocity state \(\dot{\gamma}_t\) that allows the hedge ratio to follow a locally linear trend. Implements Palomar (2024), §15.6.3, eq. (15.4).Observation equation:
\[y_t = \mu_t + \gamma_t x_t + \varepsilon_t, \quad \varepsilon_t \sim \mathcal{N}(0, \sigma^2_\varepsilon)\]Transition equations:
\[\begin{split}\mu_{t+1} &= \mu_t + \eta_{\mu,t} \\ \gamma_{t+1} &= \gamma_t + \dot{\gamma}_t + \eta_{\gamma,t} \\ \dot{\gamma}_{t+1} &= \dot{\gamma}_t + \eta_{\dot{\gamma},t}\end{split}\]\(\gamma\) follows a locally linear trend driven by its velocity \(\dot{\gamma}\). The velocity is a slow random walk (\(\sigma^2_{\dot{\gamma}} \ll \sigma^2_\gamma\)).
The predictive hedge ratio \(\gamma_{t|t-1}\) is returned by
fit(). The normalised spread (no lookahead) is:\[z_t = \frac{y_t - \gamma_{t|t-1}\, x_t - \mu_{t|t-1}}{1 + \gamma_{t|t-1}}\]Hyperparameters are initialised via OLS bootstrap (same heuristic as
KalmanFilter), with an additional term for \(\sigma^2_{\dot{\gamma}}\):\[\sigma^2_{\dot{\gamma}} = \alpha_{\dot{\gamma}} \cdot \sigma^2_\varepsilon / \mathrm{Var}[x]\]- Parameters:
alpha (float) – State noise scale for \(\mu\) and \(\gamma\). Palomar recommends 1e-6 for this model.
alpha_dgam (Optional[float]) – State noise scale for \(\dot{\gamma}\). Should satisfy
alpha_dgam < alphaso that velocity varies slowly. Defaults toalpha / 10.ls_window (Optional[int]) – Bars used for OLS initialisation. None uses the full series.
add_intercept (bool) – If
False, the intercept state \(\mu_t\) is pinned to zero for the entire filter run (mu0=0,P0[0,0]=0,sigma2_mu=0). The Kalman gain on \(\mu\) is then identically zero at every step, so the state vector effectively reduces to a 2-state \([\gamma_t, \dot{\gamma}_t]\) model without any structural change to_run_filter. Useful when the relationship betweenyandxis expected to be purely proportional (e.g. realised-variance series). Default:True.
Note
Use log-prices. The filter assumes constant observation noise \(\sigma^2_\varepsilon\). With raw prices, \(\sigma^2_\varepsilon \propto \text{price}^2\), making the Kalman gain \(K_t\) systematically mis-calibrated when prices drift. Passing
log(y),log(x)makes \(\sigma^2_\varepsilon\) approximately homoscedastic and gives more stable hedge ratio estimates. InBacktestEngine, setlog_prices=Trueto apply this automatically while keeping actual prices for P&L accounting.- fit(y: PriceTimeSeries, x: PriceTimeSeries) Series[source]#
Run the Kalman filter on (y, x) and return the one-step-ahead predictive hedge ratio \(\gamma_{t|t-1}\).
Using the predictive state rather than the filtered state \(\gamma_{t|t}\) ensures no lookahead bias: at bar \(t\), \(\gamma_{t|t-1}\) is computed from observations \(\{(y_s, x_s) : s \leq t-1\}\) only.
After calling this method the following attributes are set:
params_— fittedKalmanFilterWithVelocityParamsmu_ts_— filtered intercept series \(\mu_{t|t}\)velocity_ts_— filtered velocity series \(\dot{\gamma}_{t|t}\)normalized_spread_— lookahead-free normalised spread\[z_t = \frac{y_t - \gamma_{t|t-1}\, x_t - \mu_{t|t-1}}{1 + \gamma_{t|t-1}}\]
- Parameters:
y (PriceTimeSeries) – Dependent-leg price series.
x (PriceTimeSeries) – Independent-leg price series.
- Returns:
Predictive hedge ratio series \(\gamma_{t|t-1}\) aligned with
y.index.- Return type:
pd.Series