mathematical foundations

The mathematics of Drift

Full derivations for every formula in the 15-layer pipeline. Each section explains the problem, derives the solution from first principles, and notes how it maps to source code. Includes the Drift 2.0 upgrades: endogenous Ω, co-movement graph regimes, vectorized WST, and the corrected BL view construction.

Layer 1

Returns — data ingestion and contracts

Simple and log returns

For price P_{i,t}: simple return r_{i,t} = P_{i,t}/P_{i,t-1} − 1; log return ℓ_{i,t} = log(1 + r_{i,t}). Log returns are used wherever stationarity and additivity are required (regime features, wavelets). Simple returns measure outcomes.

The forward-return contract (Axiom 2)

The forward return R_{i,t,h} = P_{i,t+h}/P_{i,t} − 1 peeks into the future. By Axiom 2 it may only appear in evaluation code (IC analysis, backtests, matured view errors) — never in signal generation. The default horizon h = 21 trading days (≈ one month). A CI property test enforces this boundary.

The precomputed FeatureStore

Data is fetched via a fully asynchronous engine from Zerodha Kite, paced exactly at the broker rate limit. All screener math is precomputed at dawn and serialized into memory-mapped Arrow tables. A user request does zero network calls and zero pandas math — the OS maps the file into RAM directly. Fallback: if a snapshot is stale or the universe is custom, the live async pipeline runs without blocking other requests.

Key invariant: the DataLoader is the only file in the codebase that knows the broker exists. All layers above receive a clean (date, ticker) MultiIndex DataFrame. The async engine and FeatureStore are the only layer that knows about Zerodha Kite.
Layer 2

Factor Direction & Rank Transform — cross-sectional signal construction

The rank transform — derivation from first principles

We want a function mapping ranks r ∈ {1..N} linearly to scores s ∈ {−1,+1}. Write the linear form s = a·r + b. Two boundary conditions pin the two unknowns:

s(1) = −1 → a·1 + b = −1
s(N) = +1 → a·N + b = +1

Subtract the first equation from the second — b cancels:

a(N−1) = 2 → a = 2/(N−1)

Substitute back to find b, then combine over a common denominator:

sᵢ = (2rᵢ − N − 1) / (N − 1)
Verify: r=1 → (2−N−1)/(N−1) = −1 ✓ · r=N → (N−1)/(N−1) = +1 ✓
The rank transform is outlier-proof by construction. An outlier at +∞ gets the same score (+1.00) as a value at rank N. The z-score of that outlier would be +∞.

The seven factors

Fama-French 5 factors: Market beta (systematic risk), Size (−log mktcap), Value (B/M ratio), Profitability (ROE), Investment (−asset_growth). Plus Momentum (12-1 month return, skip most recent month per Jegadeesh & Titman 1993) and Quality (composite z-score of ROE, low leverage, low earnings volatility per Asness et al. 2019).

Layer 4

Regime-Aware Factor Weighting — HMM + co-movement graph

The computational problem

For K=3 states and T=252 trading days, brute-force enumeration of all state sequences requires K^T = 3^252 ≈ 10^120 operations. The forward algorithm reduces this to O(K²T) via dynamic programming.

The forward variable (forward-filtered, not smoothed)

Define α_t(k) = P(o₁,...,o_t, s_t = k | λ) — the probability of seeing observations up to t AND being in state k at t.

Initialisation: α₁(k) = π_k · B_k(o₁)
Recursion: α_{t+1}(j) = B_j(o_{t+1}) · Σ_k [α_t(k) · A_{kj}]
Termination: P(O|λ) = Σ_k α_T(k)
B_j is outside the sum — emission depends only on the destination state, not the path. This is the Markov property.
Drift 2.0 uses forward-filtered (not smoothed) probabilities. The legacy model used smoothed probabilities where every historical date knew the future. A CI property test enforces point-in-time integrity: appending future data must not change any earlier filtered probability.

The co-movement graph upgrade (6-dim embedding)

The most reliable crisis precursor is not the index falling — it is correlations tightening before it falls. Drift 2.0 builds a daily co-movement map of ~11 sectors and injects a 6-dimensional embedding g_t into the HMM emission space: (1) mean pairwise correlation ρ̄_t; (2) Absorption Ratio AR_t = Σ_{k=1}^m θ_k / Σ θ_k (Kritzman et al. 2011); (3) Fiedler value λ₂ of the normalized graph Laplacian — how hard the market network is to cut in two; (4) normalized MST length L̄_t (Mantegna 1999); (5) participation ratio PR_t = (Σ v_{1,i}⁴)⁻¹; (6) lead-lag asymmetry ‖Λ−Λᵀ‖_F / ‖Λ+Λᵀ‖_F.

Regime-blended factor weights

w_{f,t} = 0.60 · w_f^{ICIR} + 0.40 · w_{f,t}^{regime}
Leg 1 (long-run reliability): ICIR-proportional weights. Leg 2 (current-regime preference): state-conditional mean IC, shrunk with James–Stein pseudo-count.

Why StandardScaler is mandatory

log_return, realised_vol, vol_of_vol, and the 6 co-movement features differ in scale by orders of magnitude. Without scaling, Baum-Welch EM collapses all observations into one state. That state's covariance is well-estimated; the minority states have near-zero observations, their covariance matrices become rank-deficient, and the Cholesky decomposition inside the Gaussian emission fails.

Layer 6

Wavelet Scattering Stability — path-stability score

Why the modulus is essential

A complex wavelet ψ_j is a bandpass filter. The raw convolution x★ψ_j oscillates at the centre frequency with zero mean — averaging gives nothing. The modulus |x★ψ_j| strips the oscillation and gives the instantaneous amplitude envelope — slowly varying, non-negative, convolvable at the next scale.

Without modulus: S₂ = x★ψ_j₁★ψ_j₂ is just another bandpass filter (composition of linear operators = linear operator). With modulus: S₂ captures how energy at scale j₁ modulates energy at scale j₂ — a genuinely non-linear feature.

Cauchy vs Morlet

Morlet: ψ̂(ω) ∝ exp(−(ω−ω₀)²/2σ²) (Gaussian decay, fast)
Cauchy: ψ̂(ω) ∝ ω^α · exp(−ω/ω_j) (algebraic decay, slower)
α=4 in Drift · higher α → closer to Morlet · lower α → heavier tails, more sensitive to large events

Financial returns are fat-tailed. A Morlet wavelet damps a 10σ return the same as a 3σ return at the same frequency. A Cauchy wavelet retains sensitivity to the 10σ event — exactly the information that matters for systematic risk.

The scattering cascade

S₀ = ⟨|x|⟩ → 1 coefficient
S₁(j) = ⟨|x★ψ_j|⟩ → J=8 coefficients
S₂(j₁,j₂) = ⟨||x★ψ_j₁|★ψ_j₂|⟩ → C(8,2)=28 coefficients (j₁ < j₂)
Total: 1 + 8 + 28 = 37 · Each coefficient is translation-invariant: same volatility pattern at any point in time → same feature

Path-stability score (production output)

In the production pipeline, the scattering coefficients are distilled into a single path-stability score in [−1,+1]: +1 = smooth, trending path; −1 = erratic, jumpy behavior. The computation now runs as a single 3D tensor batch operation with a cached filter bank and reflection padding — correcting two bugs present in the legacy per-ticker loop implementation.

The vectorized WST batch eliminates thousands of interpreter round-trips. The two fixed bugs: incorrect boundary conditions (now reflection padding) and an odd-length filter crash — both had silently corrupted historical stability scores.
Layer 3

Factor Reliability — IC, ICIR, and signal combination

Why Spearman, not Pearson

A single earnings shock (+30% on one stock, ±2% everywhere else) produces a near-zero Pearson IC even if the factor correctly ranked all other stocks. Spearman rank correlation is Pearson correlation of the rank vectors — a single outlier gets rank N, not a 15σ influence on the correlation.

IC_t = Spearman(s_t, r_{t+h})
s_t are already rank-transformed → rank(s_t) preserves the same ordering

ICIR as signal Sharpe ratio

ICIR = IC̄ / σ_IC
t-stat = ICIR · √T
ICIR 0.3, T=252: t=4.8 (highly significant) · ICIR 0.05: t=0.8 (noise)

IC-weighted combination

Zᵢ = Σ_k w_k · s_{i,k} / Σ_k w_k
w_k = max(ICIR_k, 0) · negative ICIR → zero weight
Regime conditioning: momentum weight → 0 in bear regimes (Barroso & Santa-Clara 2015: momentum crashes occur specifically during market reversals following bear regimes).
Layer 8

Black-Litterman — self-calibrating Bayesian posterior

Step 1 — Reverse optimisation

CAPM says market-cap weights w_mkt are mean-variance optimal. Running the formula backwards gives the implied equilibrium expected returns:

μ_eq = λ · Σ · w_mkt
λ = market risk aversion ≈ 2.5

Step 2 — Views as active tilts above equilibrium (corrected)

The legacy implementation set q = Z · scale(μ_eq), which dragged expected-return levels toward zero. The corrected formula treats the composite alpha Z as an active tilt above the equilibrium prior:

P = I_N (absolute views on each stock)
q = π + Z · scale(μ_eq) (equilibrium prior + active tilt)
π = μ_eq is the CAPM prior. The tilt Z·scale anchors expected-return levels correctly.

Step 3 — Endogenous view uncertainty Ω (the key upgrade)

In the legacy model, Ω = τ · diag(PΣPᵀ) was a single hand-set constant (He-Litterman uncertainty). Drift 2.0 makes Ω endogenous — computed from the model's own measured IC accuracy and realized view errors:

Ω = f(IC_history, realized_errors) (endogenous, per-asset)
Poor recent accuracy → Ω inflates → portfolio drifts toward market baseline. Self-deflating risk gate.

Step 4 — Posterior (conjugate Gaussian update)

M = [(τΣ)⁻¹ + PᵀΩ⁻¹P]⁻¹ (posterior covariance)
μ_BL = M · [(τΣ)⁻¹μ_eq + PᵀΩ⁻¹q] (posterior mean)
Precision-weighted average: high-confidence views pull μ_BL toward q; when Ω is large (low confidence), μ_BL stays near μ_eq
Layer 10

Hierarchical Risk Parity — clustering-based base allocation

Step 1 — Distance matrix

d_{ij} = √((1 − ρ_{ij}) / 2)
ρ=1→d=0 · ρ=0→d=0.707 · ρ=−1→d=1 · satisfies triangle inequality (unlike d=1−ρ)

Step 2 — Ward linkage and quasi-diagonalisation

Ward linkage minimises within-cluster variance at each merge. The dendrogram leaf order (quasi-diagonalisation) reorders the covariance matrix so similar assets are adjacent.

Step 3 — Recursive bisection

Var(cluster) = w_c^T Σ w_c (equal weight within cluster)
α = Var(right) / (Var(left) + Var(right))
w_left ← α · w_parent · w_right ← (1−α) · w_parent
HRP is the default optimiser — it never fails numerically. BL requires a well-conditioned covariance; HRP works even when T < N.
Layer 14

CVaR — the Rockafellar-Uryasev linear programme

The non-smooth problem

CVaR_α(w) = min_z [ z + 1/((1−α)T) · Σ_t max(−w^T r_t − z, 0) ]

The max() makes this non-smooth. Standard gradient methods fail. Rockafellar-Uryasev (2000) showed it reformulates as a linear programme:

min_{w,z,u} z + 1/((1−α)T) · Σ_t u_t
s.t. u_t ≥ −w^T r_t − z ∀ t
u_t ≥ 0, Σwᵢ = 1, 0 ≤ wᵢ ≤ max_weight
At optimality: z = VaR_α · objective = CVaR_α · u_t captures loss in excess of VaR
Layer 9

Covariance & Risk Model — Ledoit-Wolf shrinkage and BARRA decomposition

Why sample covariance fails

An N×N covariance matrix has N(N+1)/2 unique parameters. When T approaches N, the condition number (max/min eigenvalue) explodes. Inverting such a matrix amplifies noise by the condition number — 10³ or more for a 100-asset portfolio with 2 years of daily data.

Ledoit-Wolf shrinkage

Σ̂ = α · F + (1−α) · S
F = (tr(S)/N)·I (scaled identity target) · α computed analytically from S, no cross-validation

BARRA decomposition

rᵢ = Σ_k Xᵢ_k · f_k + uᵢ
Var(r_p) = w^T (X F X^T + Δ) w
= w^T X F X^T w + w^T Δ w
X = factor exposures (N×K) · F = factor covariance (K×K) · Δ = diagonal specific variance
Layer 15

Backtest Validation — walk-forward, PSR, and DSR

Sharpe ratio variance under non-normality

Var(SR̂) ≈ (1/T) · [1 − γ₃ · SR̂ + (γ₄−1)/4 · SR̂²]
γ₃ = skewness · γ₄ = excess kurtosis · negative skew inflates variance → SR̂ less reliable

Probabilistic Sharpe Ratio

PSR(SR*) = Φ( (SR̂ − SR*) · √(T−1) / √(1 − γ₃·SR̂ + (γ₄−1)/4·SR̂²) )
Φ = standard normal CDF · negative skew → larger denominator → lower PSR → correctly penalises tail-risk harvesting

Deflated Sharpe Ratio

When N strategies are tested and the best is selected, E[max SR] under the null rises with N. DSR adjusts the benchmark SR* to account for selection bias:

SR*_DSR = SR*₀ + σ_{SR̂} · E[max(N)]
E[max(N)] ≈ (1−ρ̄)·Φ⁻¹(1−1/N) + ρ̄·Φ⁻¹(1−1/Ne) · for N=50 independent trials: E[max]≈2.0
PSR(88.6%) means there is an 88.6% probability the true Sharpe exceeds zero — not just that the observed Sharpe is positive. DSR further deducts for how many times you looked before selecting this strategy.