Software companion

Spectral Signatures in SPY and BTC Intraday Returns

Overview

The study asks how intraday return variance distributes across timescales in an equity-index proxy and in bitcoin, so the software is built around one requirement: the two assets and the two decompositions must be treated identically. Loading, session alignment, return construction, band aggregation, and resampling are shared code. The only things that vary between arms are which series enters and which transform is applied to it.

That symmetry is what allowed the headline result — that the two decompositions disagree about the low-frequency band by more than the cross-asset difference being measured — to register as a methodological finding rather than a suspected bug. src/crosscheck.py exists to quantify the disagreement directly, and src/headline_method_choice.md records the resulting decision: publish the STFT 30-minute band share as the headline, carry the symmetric-mode wavelet series beside it, and never average the two into a single number.

Implementation

Core libraries
NumPy, pandas, SciPy, PyWavelets, pandas-market-calendars, Matplotlib
Data sources
yfinance 1-minute SPY bars, the Binance public klines endpoint for 1-minute BTCUSDT, and FRED VIXCLS for the stress split
Decomposition settings
30-minute Hann STFT with 15/30/60-minute sensitivity windows; a db4 discrete wavelet to five levels with symmetric, periodic and zero boundary sensitivity
Bands
Four edges in minutes — 1–5, 5–15, 15–60, and 60 and above
Configuration
A single frozen Config dataclass carrying dates, symbols, session hours, transform settings, band edges, stress cutoffs, the KS alpha and its Bonferroni divisor of four, and the run seed
Tests
18 pytest modules, roughly one per source module

Components

src/loaders/
SPY, BTC and VIX acquisition, each writing a hashed parquet snapshot through an atomic temp-file rename
src/alignment.py
Regular-trading-hours calendar construction, matched-session alignment, and the separate 24-hour BTC series
src/returns.py
Log returns and the sparse-session rule that drops days with too few observations
src/stft.py, src/dwt.py
The two decompositions, each returning per-band energy through a common interface
src/aggregation.py
Band shares from raw energies, pooled and rolling
src/bootstrap.py
Date-block resampling, so intraday dependence is not broken by the resampler
src/ks.py
Two-sample Kolmogorov–Smirnov tests at the Bonferroni-corrected alpha
src/stress.py
The stress overlay: a VIX median split for SPY, a realized-volatility quantile for BTC
src/crosscheck.py
STFT-versus-wavelet agreement, reported rather than reconciled
src/hashing.py, src/runtime_metadata.py
SHA-256 file digests and the run manifest recording environment and inputs
src/figures/
Nine figure builders, each reading only from processed artifacts

Pipeline

  1. src/loaders/* Fetch and snapshot raw SPY, BTC and VIX, recording a digest for each file.
  2. build_processed.py Align to the trading calendar, build log returns, and drop sparse sessions.
  3. stft.py / dwt.py Decompose both series under both transforms with the configured sensitivity settings.
  4. aggregation.py Reduce coefficient energies to band shares, pooled and rolling.
  5. build_ks_results.py Bootstrap the band-share distributions and run the corrected KS comparisons.
  6. crosscheck.py Quantify STFT-versus-wavelet disagreement on the same input.
  7. figures/ Render the nine report figures from processed artifacts only.
  8. build_report.py Assemble the report against the manifest, so a stale input cannot be published silently.

Reproducibility and validation

  • Every raw and processed artifact is hashed into a run manifest, and the report builder reads figures and tables through that manifest rather than from the filesystem directly.
  • Sensitivity settings are declared in configuration rather than run ad hoc: three STFT window lengths and three wavelet boundary modes are part of the standing result, not a follow-up.
  • Parquet writes go to a temporary file and are renamed into place, so an interrupted run cannot leave a half-written input behind.
  • The multiple-comparison correction lives in configuration — ks_alpha over a Bonferroni divisor of four, one per band — rather than being applied by hand at reporting time.

Availability

Not publicly released. Both price sources are public, but neither redistributes cleanly, and the repository carries hashed snapshots rather than the vendor data itself. A release would need the loaders repointed at a source whose terms allow the cached bars to be republished.

Companion research

Two decompositions of the same intraday series disagree about the low-frequency band by more than the cross-asset difference they are being used to measure.

Read the research report