FourierResidueIdentity

Incremental, causal technical analysis documentation

Summary

FourierResidueIdentity is a streaming implementation of the Fourier-Residue Identity (FRI), which splits return autocorrelation into a direction (sign) channel and a magnitude channel that are individually testable and neither redundant.

It answers a question no scalar autocorrelation can: when a series mean-reverts, is the direction predictable, or only the size? The two cases call for opposite trades. A directional reversal says "go long after a down day". A magnitude-only reversal says "expect a smaller move tomorrow, of unknown sign" — a volatility signal, not a directional one, and a contrarian bet on it has no statistical warrant at all.

The motivating fact from the source paper: SPY's lag-1 autocorrelation is \(\hat\rho(1) = -0.081\), which is \(7.4\) standard errors below zero — one of the most significant regularities in empirical equity finance. Yet the FRI sign test on the same data returns \(z_{\mathrm{sign}} = -1.59\) (\(p = 0.11\)). Knowing SPY fell yesterday tells you essentially nothing about whether it rises or falls today. The bounce has no direction.

Reference

V. Portnaya, "The Bounce Has No Direction: Sign, Magnitude, and the Microstructure of Equity Return Predictability — Fourier-Residue Identities, Fejér Sums, and Evidence from US Equity and Cross-Asset Markets, 1993–2026", arXiv:2606.29591 (June 2026).

Update API

out = rtta.FourierResidueIdentity().update(close)

# OHLC-compatible overload (open/high/low ignored)
out = rtta.FourierResidueIdentity().update(open, high, low, close)

indicator = rtta.FourierResidueIdentity()
indicator.advance(close)          # no return value
out = indicator.last()
indicator.reset()

batch = rtta.FourierResidueIdentity().batch(close_array)

Constructor knobs:

Argument Default Role
max_lag 8 lags \(M\) tracked; raised to cover horizon - 1 and test_lag
horizon 2 variance-ratio horizon \(q\)
test_lag 1 lag \(m\) reported by the scalar outputs
span 512.0 EWMA memory in observations
median_window 256 rolling window for the median \(\lvert r\rvert\) that defines the \(k=4\) buckets
entry_z / exit_z 2.0 / 1.0 hysteresis on sign-channel evidence for signal
fillna True 0 vs NaN during warmup

Outputs

Field Meaning
rho scalar autocorrelation \(\hat\rho(m)\)
rho_sign sign channel \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)
rho_magnitude magnitude channel \(\operatorname{Re}\gamma_{1,4}(m)\)
z_rho Bartlett \(z\) for rho
z_sign binomial \(z\) for rho_sign
directional_share \(\lvert z_{\text{sign}}\rvert / (\lvert z_{\text{sign}}\rvert + \lvert z_\rho\rvert)\)
elliptical_ratio rho_sign divided by its Gaussian benchmark (see below)
variance_ratio \(\mathrm{VR}(q)\)
variance_ratio_sign \(\mathrm{VR}_2(q)\), direction channel
variance_ratio_magnitude \(\mathrm{VR}_4(q)\), magnitude channel
z_variance_ratio Lo–MacKinlay heteroskedasticity-robust \(z^*\)
persistence half-period ratio \(R_N\)
signal -1 / 0 / +1, gated on sign-channel significance
score continuous directional score in \([-1, 1]\)
magnitude_forecast conditional \(\mathbb{E}\lvert r_{t+1}\rvert\)

Theory Of Operation

Fejér / variance-ratio identity

The Lo–MacKinlay variance ratio admits an exact autocorrelation representation (Proposition 2.2):

\[\mathrm{VR}(q) \;=\; 1 + 2\sum_{m=1}^{q-1}\Bigl(1 - \tfrac{m}{q}\Bigr)\hat\rho(m) \;=\; 1 + 2\,\mathcal{C}_q\]

The Fejér weights \(w_m = 1 - m/q\) taper linearly to zero at lag \(q\), giving short lags — where microstructure lives — the most weight.

The FRI decomposition

Encode each return as a \(k\)-ary symbol \(s_t \in \{0,\dots,k-1\}\) and evaluate the characters of the cyclic group \(\mathbb{Z}/k\mathbb{Z}\) (Definition 2.4):

\[\gamma_{A,k}(m) \;=\; \frac{1}{N-m}\sum_t \omega^{A(s_t - s_{t+m})}, \qquad \omega = e^{2\pi i/k}\]

Sign channel (\(k=2\)). With \(s_t = \mathbb{1}[r_t > 0]\) and \(\omega = -1\), the character is \(+1\) when successive signs agree and \(-1\) when they disagree, collapsing to a closed form (Proposition 2.5):

\[\gamma_{1,2}(m) \;=\; 2p_{m,0} - 1 \;=:\; \hat\rho_{\mathrm{sign}}(m)\]

where \(p_{m,0}\) is the probability of closing on the same side of zero \(m\) periods apart. Under the random-walk null \(p_{m,0} = \tfrac12\). This is a magnitude-free test of directional dependence: positive means momentum, negative means genuine contrarian reversal.

Magnitude channel (\(k=4\)). Returns are bucketed at the median \(\lvert r\rvert\) into a signed size ladder \(\{\text{large-down},\text{small-down},\text{small-up},\text{large-up}\} = \{0,1,2,3\}\) with \(A = 1\), \(\omega = i\). This measures whether the size bucket persists, independently of whether direction agrees.

Applying the Fejér identity per channel gives \(\mathrm{VR}_2(q)\) and \(\mathrm{VR}_4(q)\) (Equation 5). The channels are nonnested: a series with sign momentum but no magnitude clustering has \(\mathrm{VR}_2 > 1\) and \(\mathrm{VR}_4 \approx 1\), and vice versa.

Which mechanism is which

Mechanism Sign \(\mathrm{VR}_2\) Magn. \(\hat\rho(1) = -0.081\)0 Lag range
Bid-ask bounce \(\hat\rho(1) = -0.081\)1 \(\hat\rho(1) = -0.081\)2 lag 1 only
Non-synchronous trading \(\hat\rho(1) = -0.081\)3 \(\hat\rho(1) = -0.081\)4 lags 1–3
Dealer inventory \(\hat\rho(1) = -0.081\)5 \(\hat\rho(1) = -0.081\)6 lags 1–2
Adverse selection \(\hat\rho(1) = -0.081\)7 \(\hat\rho(1) = -0.081\)8 lags 2–5
Partial price adjustment \(\hat\rho(1) = -0.081\)9 \(7.4\)0 lags 2–7
Volatility clustering \(7.4\)1 \(7.4\)2 all lags

Only the mechanisms with \(7.4\)3 are directionally tradeable.

Subsample persistence

The half-period ratio (Definition 2.6) answers whether a detected deviation will survive out of sample:

\[G_N = \max_{1\le m\le M}\lvert\hat\rho_N(m)\rvert, \qquad R_N = G_{N/2}\,/\,G_N\]

Under IID noise \(7.4\)4; under genuine serial dependence \(7.4\)5 (Proposition 2.7). Halving the sample inflates a noise maximum by \(7.4\)6 but barely moves a structural one.

Streaming form

The paper estimates full-sample; this implementation is bounded-memory and online. Sample means become debiased EWMAs of span span (so early updates behave like an expanding sample rather than a biased ramp), and \(7.4\)7 is replaced by the effective sample size \(7.4\)8.

persistence is computed by running a second parallel estimator at half the span, which is the streaming analogue of the \(7.4\)9 construction. It is only meaningful for a finite span; with an effectively infinite span both estimators coincide and the ratio degenerates to 1.

z_variance_ratio uses the Lo–MacKinlay M2 statistic with \(z_{\mathrm{sign}} = -1.59\)0 in its standard \(z_{\mathrm{sign}} = -1.59\)1 normalisation, so \(z_{\mathrm{sign}} = -1.59\)2 collapses to \(z_{\mathrm{sign}} = -1.59\)3 under an IID null. Daily equity returns have strong GARCH effects, and the homoskedastic \(z_{\mathrm{sign}} = -1.59\)4 over-rejects at 10–12% where the robust \(z_{\mathrm{sign}} = -1.59\)5 holds its 5% size.

The elliptical benchmark (extension beyond the paper)

The sign channel is not free of \(z_{\mathrm{sign}} = -1.59\)6 — it has a predictable null. For a bivariate normal pair, Grothendieck's identity gives

\[\mathbb{E}[\operatorname{sgn}X \operatorname{sgn}Y] = \tfrac{2}{\pi}\arcsin\rho\]

so any elliptical process with autocorrelation \(z_{\mathrm{sign}} = -1.59\)7 must show a sign channel of about \(z_{\mathrm{sign}} = -1.59\)8. elliptical_ratio divides the observed rho_sign by that benchmark, giving a scale-free diagnostic with a null of 1:

This matters because it sharpens the paper's own conclusion. A simulated pure Roll bounce scores 0.95 here, not 0 — a bounce does leak into the sign channel, because sign correlation is pinned to \(p = 0.11\)2 for near-Gaussian data. What makes SPY genuinely unusual is that its pair \(p = 0.11\)3 scores 0.34: far less directional than any elliptical process with that \(p = 0.11\)4. The reversal is concentrated in large moves — which dominate the covariance — while a typical day's direction stays a coin flip.

Only interpret elliptical_ratio when the scalar ACF is itself detectable (\(p = 0.11\)5 large); it is returned as NaN when \(p = 0.11\)6 is too close to zero for the ratio to be stable.

Recurrence

State: previous close; a ring buffer of the last \(p = 0.11\)7 returns with their signs and \(p = 0.11\)8 codes; a rolling median of \(p = 0.11\)9; debiased EWMA pairs \(M\)0 for \(M\)1, \(M\)2, \(M\)3, \(M\)4; and per lag \(M\)5 the EWMAs \(M\)6 (cross-product), \(M\)7 (sign agreement), \(M\)8 (\(M\)9 magnitude character), \(q\)0 (\(q\)1 cross-product) and \(q\)2 (quartic, for \(q\)3). A parallel \(q\)4 set runs at half the span for \(q\)5.

Each debiased EWMA accumulates \(q\)6 and \(q\)7, reporting \(q\)8.

  1. \(q\)9; \(m\)0 as \(m\)1; \(m\)2; code \(m\)3 from \(m\)4.
  2. For \(m\)5, against \(m\)6 held at ring slot \(m\)7: push \(m\)8 into \(m\)9; \(\lvert r\rvert\)0 into \(\lvert r\rvert\)1; \(\lvert r\rvert\)2 into \(\lvert r\rvert\)3 via a 4-entry table on \(\lvert r\rvert\)4; \(\lvert r\rvert\)5 into \(\lvert r\rvert\)6; and \(\lvert r\rvert\)7 into \(\lvert r\rvert\)8.
  3. Push \(\lvert r\rvert\)9 into the global moment EWMAs, then into the ring buffer.
  4. \(k=4\)0; \(k=4\)1; \(k=4\)2; \(k=4\)3.
  5. \(k=4\)4; \(k=4\)5; \(k=4\)6.
  6. Fejér-weight lags \(k=4\)7 into \(k=4\)8, \(k=4\)9, \(\hat\rho(m)\)0; accumulate \(\hat\rho(m)\)1 from \(\hat\rho(m)\)2.
  7. \(\hat\rho(m)\)3 and \(\hat\rho(m)\)4 are the running maxima of \(\hat\rho(m)\)5 over the full- and half-span sets; \(\hat\rho(m)\)6.
  8. Score \(\hat\rho(m)\)7; arm/disarm on \(\hat\rho(m)\)8 against entry_z/exit_z; emit the signed score when armed.

Each update is \(\hat\rho(m)\)9 with \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)0 max_lag (default 8) and causal. The rolling median dominates at \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)1 via nth_element; its scratch buffer reaches full size during warmup and is not reallocated afterwards, so the steady-state hot path is allocation-free. Lower median_window if the \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)2 bucket boundary does not need that much history.

Trading Interpretation

signal is non-zero only while the sign channel itself clears entry_z, with hysteresis at exit_z:

magnitude_forecast carries the content that remains statistically warranted even when direction does not: a conditional forecast of the next absolute return, for volatility sizing, straddle/strangle timing, or scaling a delta-hedged book.

A practical reading of the two together:

z_rho z_sign Reading
large large, same sign genuine directional dependence — trade the direction
large small magnitude-only — size positions, do not bet on direction
small large direction pattern hidden from the scalar ACF by offsetting magnitudes
small small no exploitable structure

What this does not do

Be precise about the limits of the sign gate. Simulating a pure Roll bounce (martingale efficient price, IID trade direction, half-spread \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)4) produces \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)5 and \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)6 at \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)7 — a thoroughly significant sign channel. So signal will fire on a simulated bid-ask bounce; it is not a bounce filter. Nothing computed from close prices alone can be, because the observed series genuinely does reverse — what makes the bounce untradeable is the spread you cross, which is not in the data.

What the sign channel does deliver is the separation itself: when \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)8 is large and \(\gamma_{1,2}(m) = 2p_{m,0} - 1\)9 is not, you know the direction of a typical bar is a coin flip and only sizing is warranted. Real SPY is that case; a simulated Roll bounce is not.

The actionable consequence of a low elliptical_ratio is where the edge sits. On a simulated magnitude-carried reversal (elliptical_ratio = 0.37), taking the contrarian stance only after an above-median move retains 97% of the gross P&L while trading half as many bars, nearly doubling per-bar edge. On a uniform directional reversal (elliptical_ratio = 1.00) the same restriction retains only 78%. A low ratio tells you to concentrate risk on large moves rather than to trade every bar.

Notes