RollingSpread#
Description#
RollingSpread computes the rolling hedge-adjusted residual of x against y. At each time step it computes the rolling regression slope β_w[t] = cov(x, y) / var(y) over the window and returns x[t] - β_w[t] · y[t].
This is the building block for pairs trading: RollingSpread(price_a, price_b, w) is the residual of price_a after removing its rolling-best-fit linear exposure to price_b. A mean-reverting spread is the prototypical pairs-trading signal.
Equation:
with the sums in cov and var taken over the most recent window_size samples ending at t.
Parameters:
window_size(int, ≥ 2): size of the rolling window.start_policy(str, default"strict"): controls warmup behavior.
Input shape: two parallel streams, identical to RollingCorr.
Return value: a real number, the residual. Returns NaN during warmup or when y has zero variance within the window.
NaN handling#
Policy: ignore. A NaN in any input at index t causes the function to skip that step: output at t is NaN and internal state is unchanged. Subsequent finite samples are processed as if step t had not occurred.
Examples#
Usage example#
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from screamer import RollingSpread
np.random.seed(0)
N = 400
common = np.cumsum(np.random.normal(size=N))
a = common + 0.3 * np.cumsum(np.random.normal(size=N))
b = 1.2 * common + 0.3 * np.cumsum(np.random.normal(size=N))
# Inject a transient mispricing in a between samples 200 and 240.
a[200:240] += 5.0
spread_60 = RollingSpread(window_size=60)(a, b)
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
row_heights=[0.5, 0.5], vertical_spacing=0.05)
fig.add_trace(go.Scatter(y=a, mode="lines", name="a"), row=1, col=1)
fig.add_trace(go.Scatter(y=b, mode="lines", name="b"), row=1, col=1)
fig.add_trace(go.Scatter(y=spread_60, mode="lines",
name="RollingSpread(60)"), row=2, col=1)
fig.update_layout(
title="Hedge-adjusted spread of a against b (window=60)",
margin=dict(l=20, r=20, t=60, b=20),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
)
fig.update_yaxes(title_text="price", row=1, col=1)
fig.update_yaxes(title_text="spread", row=2, col=1)
fig.show()
Implementation Details#
Same four detail::RollingSum buffers as RollingBeta, O(1) per step. The spread at each step combines the rolling β with the current (x, y) pair.
Time:
O(1)per new element.Space:
O(window_size).Reference: parity with
x - RollingBeta(x, y, w) · yverified intests/test_rolling_two_input.py.