Fourier analysis & cycles
Any series can be rewritten as a sum of sinusoids, and the periodogram tells you how much variance lives at each frequency. That makes Fourier analysis a genuine detector of seasonality — intraday volume rhythms, day-of-week effects — and simultaneously one of the great engines of self-deception in trading, because pure noise always produces impressive-looking peaks. This chapter builds the periodogram from the DFT, explains why it must be smoothed to estimate a spectrum, and draws the line between a cycle that survives a permutation test and a cycle that is just the noise you went looking for.
A prism splits light into its component colours and shows you the energy at each wavelength. The discrete Fourier transform does the same for a time series: it re-expresses the data as a sum of sines and cosines and reports the amplitude at each frequency. A genuine cycle — something in the world that repeats, like the opening and closing rush of trading volume — concentrates variance at its frequency and shows up as a sharp, reproducible spectral line. The trap is that white noise does not produce a flat-looking periodogram on any finite sample: it produces a jagged forest of spurious peaks. The skill is not finding peaks — anyone can find peaks — it is knowing which ones would survive on data that has no cycles at all.
The periodogram, from the DFT#
Take a demeaned series sampled once per bar. Its discrete Fourier transform evaluates the correlation of the data with a complex sinusoid at each Fourier frequency , in units of cycles per bar:
The periodogram is the squared magnitude, normalised by the sample size:
Read it as a variance budget: summing across frequencies recovers (up to convention) the sample variance, so each ordinate is the share of variance explained by a sinusoid at that frequency. A frequency of cycles/bar means a period of bars; the highest resolvable frequency is the Nyquist limit — one full cycle every two bars. For real-valued data the negative frequencies are redundant, which is why periodogram uses the real FFT and returns only .

Spectral density — and why raw periodograms lie#
The object the periodogram is trying to estimate is the spectral density — the Fourier transform of the autocovariance function, describing how the process distributes variance over frequency in expectation. Two facts anchor all interpretation:
- White noise is flat: for i.i.d. noise with variance , the spectral density is constant, for all — every frequency carries equal variance, which is precisely why it is called white. A flat spectrum is the frequency-domain twin of the zero-autocorrelation null you test with an ACF plot.
- The raw periodogram is inconsistent: at any fixed frequency, is approximately distributed as — an exponential-type variable whose standard deviation equals its mean — and this does not improve with more data. More observations buy you more frequencies, not less noise per frequency. The estimator’s variance never shrinks.

At frequency the DFT is a sum of terms; under mild conditions its real and imaginary parts are asymptotically independent normals with variance each. The periodogram divides the squared magnitude by , so
a two-degree-of-freedom quantity regardless of — the s cancel. Doubling the sample halves the frequency spacing but leaves each ordinate just as noisy. The repair is to trade resolution for stability: average the periodogram over neighbouring frequencies (a Daniell window), which sums approximately independent variables into a — relative standard error . That smoothing is exactly what spectral_density does, and it is the difference between an estimator and a random-number generator.
Prepare the series first — the red-spectrum trap#
One preprocessing mistake produces more fake cycles than any other: transforming the wrong series. A random walk — prices under the martingale null — has a spectrum proportional to : enormous power at low frequencies, decaying smoothly. Feed raw prices to the periodogram and you get a mountain range at the left edge that looks like a majestic long-period cycle and is nothing but accumulated noise. The same applies to any trending or slowly wandering series.
- Analyse returns (or differences), not levels. Differencing is a high-pass filter that flattens the random-walk spectrum back to white — the correct null for the search.
- Demean always (a nonzero mean is a spike at that leaks into neighbouring frequencies), which
periodogramdoes for you. - Mind the sampling rate. Frequencies are in cycles per bar: a cycle faster than two bars is aliased — folded back onto a lower frequency — and invisible at your resolution.
Real cycles versus noise peaks#
What does a genuine cycle look like? Three signatures separate it from a noise spike:
- It survives smoothing. A true spectral line keeps its prominence when the periodogram is Daniell-smoothed; a noise spike is averaged away with its neighbours.
- It has a mechanism and a stable frequency. Intraday volume peaks at the open and close because auctions, hedging flows, and order deadlines happen at clock times. The cycle sits at exactly one bar-count per session, every session, in every subsample.
- It replicates out of sample. Split the data: a real cycle appears at the same frequency in both halves. A noise peak relocates.

The honest test: a permutation null#
The cleanest defence needs no distributional assumptions. Shuffling the observations in time destroys any cyclical structure while exactly preserving the marginal distribution — fat tails and all. So: compute your statistic (say, the maximum periodogram ordinate, or the power at the claimed frequency) on the real data, then on many shuffles, and ask how often the shuffles beat reality.
Using the maximum ordinate as the statistic bakes the multiple-comparisons correction into the null: each shuffle also gets to show off its own tallest peak, so the comparison is search-against-search — the same logic as the Monte-Carlo permutation tests that guard the gauntlet.
Worked scenario — a real cycle and a fake one#
Two claims on 5-minute data, 78 bars per session, bars (50 sessions). Claim 1: volume is seasonal within the day. The smoothed spectrum of volume shows a peak at cycles/bar rising ~40× above the background, with a clear harmonic at . Permutation test on the max ordinate: none of 1000 shuffles comes close — . Both halves of the sample show the peak at the identical frequency. Real, strong, mechanistic — the U-shaped volume curve every intraday trader knows. Claim 2: returns have a 35-bar cycle (a peak spotted at ). The raw ordinate is 6× the average level — impressive until you note the scan covered 1950 frequencies, and the expected maximum under white noise is times the mean. The permutation null agrees: 62% of shuffles produce a tallest peak at least this large, . Smoothing flattens it; the second half of the sample puts its best peak near 23 bars instead. The 35-bar cycle is the noise’s tallest tree, photographed and named.
import numpy as np
import edgekit as ek
# Raw periodogram: frequencies in cycles/bar, power per frequency
freq, power = ek.timeseries.periodogram(volume) # demeaned, rfft-based
print(freq[np.argmax(power)], 1 / freq[np.argmax(power)]) # peak freq and period in bars
# Consistent estimate: Daniell-smoothed spectral density
freq_s, dens = ek.timeseries.spectral_density(volume, n_smooth=7)
# Permutation null for the MAX ordinate (search-vs-search, multiplicity-aware)
rng = np.random.default_rng(11)
t_real = power.max()
t_null = np.empty(1000)
for i in range(1000):
_, p_sh = ek.timeseries.periodogram(np.asarray(volume)[rng.permutation(len(volume))])
t_null[i] = p_sh.max()
p_value = (1 + (t_null >= t_real).sum()) / (1 + len(t_null))
print(f"max-peak permutation p = {p_value:.3f}")Assumptions versus reality#
| Spectral analysis assumes | Reality | Consequence |
|---|---|---|
| Stationarity over the sample | Regimes, trends, drifting volatility | Power smears across frequencies; detrend and difference first |
| Fixed, persistent cycle frequency | Market rhythms drift with the calendar and microstructure | Yesterday's peak frequency mislocates tomorrow's |
| One test at one frequency | You scanned every frequency for the best peak | Naive p-values are fiction — test the max, or use Fisher's g |
| A peak implies a tradeable signal | Volume/vol seasonality is real; price cycles rarely survive costs | Spectral edges must still run the full gauntlet |
The honest summary: Fourier methods are excellent at confirming structure you have a mechanism for — seasonality in volume, volatility, spreads — and dangerous as a discovery tool for price cycles, because the estimator is noisy, the search is wide, and the null is never flat-looking. Treat the periodogram like a backtest: it only means something relative to what noise alone would have shown you.
Next: this closes Part IX — dependence, extremes, jumps, and cycles. The next part changes altitude: from the behaviour of prices to the cross-section of expected returns — factors, premia, and how to test whether a characteristic is actually paid. Factor investing.


