edgekit

Regression & factor models

Every strategy’s return can be split into a part explained by things you did not choose — the market, a sector, a style — and a residual that is genuinely yours. Regression is the tool that does the splitting, and the CAPM is the simplest useful example: it separates skill (alpha) from riding the market (beta). This chapter derives ordinary least squares from first principles, shows where its standard errors and t-stats come from, and wires the theory straight into ek.factors.

Intuition — why a trader cares before any algebra

Your crypto strategy returned last year. Impressive — until you notice Bitcoin itself was up . So how much of that was you, and how much was just being in the room while the market rose? Regression answers exactly that. It draws the best straight line through “my return” versus “the market’s return,” and the line has two numbers: a slope (how much you move when the market moves — the part you did not earn) and a height where the market return is zero (what you made when the market did nothing — the part that might be skill). Splitting those two apart, and asking whether the second is real or a fluke, is the whole game. Everything below is the machinery that draws that line honestly.

The linear model#

We posit that a response (say, a strategy’s daily returns) is a linear combination of explanatory variables, plus noise. Stacking observations into a vector and the regressors into a design matrix (one column per variable, usually including a column of ones for the intercept):

The unknown coefficient vector is what we want to estimate, and is the part of the regressors cannot explain. The assumptions on the right — zero-mean, constant-variance, uncorrelated errors — are what make least squares the best linear unbiased estimator (the Gauss-Markov theorem); we will flag where financial data violates them.

Ordinary least squares — the normal equations#

OLS chooses the that minimises the sum of squared residuals (SSR) — the total squared vertical distance between the data and the fitted line:

Derivation — minimise the SSR to get β = (XᵀX)⁻¹Xᵀy

Expand the objective, using (both scalars, equal to their own transpose):

This is a convex quadratic in (the Hessian is PSD), so the unique minimum is where the gradient vanishes. Differentiating with the matrix-calculus rules and :

Those are the normal equations. When is invertible (the regressors are not collinear), solve for the estimator:

Geometric reading. The fitted values are the orthogonal projection of onto the column space of ; the normal equations are just the statement that the residual is perpendicular to every regressor, . Least squares is the closest point in the span of your explanatory variables — nothing more.

A scatter of points with the least-squares regression line through them and vertical residual segments from each point to the line
Ordinary least squares in one picture. The line is the beta that minimises the summed squares of the vertical residual segments; the normal equations are exactly the condition that no other line makes those segments jointly shorter. The fit is the orthogonal projection of the data onto the regressors.

edgekit computes exactly this in ek.factors.ols, using numpy.linalg.lstsq (which solves the normal equations stably via a matrix factorisation rather than forming naively). Set add_const=True to prepend the intercept column automatically.

ols.py
import edgekit as ek

res = ek.factors.ols(y, X, add_const=True)   # X without a constant column
res["beta"]     # coefficients, intercept first
res["se"]       # standard errors
res["tstat"]    # beta / se
res["r2"]       # coefficient of determination
res["resid"]    # y - fitted (the unexplained part)

Goodness of fit: R²#

How much of the variation in did the model capture? The residual is orthogonal to the fit, so the total variation splits cleanly (Pythagoras in dimensions) into explained plus unexplained:

The coefficient of determination is the explained fraction:

An of 0.7 in a CAPM regression means 70% of the strategy’s return variance is just the market moving. Adding regressors can only raise (a projection onto a bigger space can only get closer), which is why edgekit also reports adj_r2 — it penalises extra columns and can fall when a regressor adds nothing.

!High R² is not high skill
A strategy with against the market has almost no independent signal — it is the market with a bit of noise. For finding alpha you want a low on the systematic factors and a significant intercept. Fit quality and edge are different questions.

Standard errors and t-stats#

A point estimate is useless without a sense of its uncertainty. Under the model assumptions the estimator’s sampling covariance follows from being a linear function of the random :

The standard error of coefficient is the square root of the -th diagonal entry, and the t-statistic asks how many standard errors the estimate sits away from zero:

A is the rough “significant at 5%” bar: the coefficient is unlikely to be that far from zero by chance. edgekit returns se and tstat from ols using the unbiased denominator.

!Return data breaks the plain-OLS standard errors
The formula assumes errors are uncorrelated and equal-variance. Financial returns are neither — volatility clusters (heteroskedasticity) and overlapping holds induce autocorrelation, which makes plain SEs too small and t-stats too optimistic. edgekit provides ek.factors.newey_west_se (a HAC sandwich estimator) for exactly this; treat a on serially-correlated returns with suspicion.

The CAPM: alpha vs beta#

The Capital Asset Pricing Model is a one-regressor OLS with a famous interpretation. Regress an asset’s excess return (over the risk-free rate ) on the market’s excess return:

The two coefficients answer two different questions:

  • Beta () is market exposure. The slope is — how much the asset moves per unit of market move. A trend-follower that is secretly just long the index shows . Beta is the return you could have earned with an index fund; it is not skill.
  • Alpha () is skill. The intercept is the average excess return left over after stripping out the market. A positive, statistically significant is the evidence that a strategy adds something the market did not give away for free.

The test of skill is therefore a t-test on the intercept. edgekit’s ek.factors.capm returns alpha, beta, the t-stat on alpha (alpha_t), the t-stat on beta (beta_t), the fit (r2), and an annualised intercept (alpha_annual) for a readable number. The bar you want to clear is a positive alpha with .

capm.py
import edgekit as ek

cm = ek.factors.capm(asset_returns, market_returns, rf=0.0,
                     periods_per_year=252)
cm["beta"]          # market exposure (1.0 = moves with the index)
cm["alpha"]         # per-period skill, net of the market
cm["alpha_t"]       # t-stat on alpha: is the skill real?  (|t| > 2)
cm["alpha_annual"]  # alpha scaled to a yearly number
cm["r2"]            # fraction of variance the market explains
Scenario — splitting a crypto strategy's return into alpha and beta

Regress a trend strategy’s monthly excessreturns on BTC’s monthly excess returns over 36 months. Suppose OLS hands back , with , and . Over that window BTC’s excess return averaged . Decompose the strategy’s average month:

Annualised, that is of alpha versus you could have earned with units of a BTC index fund. More than half the “edge” was beta. Now the honesty check: — it just clears the bar, but only just. And because monthly returns overlap and cluster, the plain SE is optimistic; run ek.factors.newey_west_se and a HAC correction of even pushes to — no longer significant. The point-estimate looked like skill; the inference says “plausibly, but unproven.”

This is the honest version of is_it_beta
The gauntlet’s ek.validation.is_it_beta gives a quick annualised intercept with np.polyfit. ek.factors.capm is the inference-carrying version: it hands you the t-stat, so you can distinguish “alpha looks positive” from “alpha is statistically distinguishable from zero.” A positive point-estimate with is not skill — it is noise. See Alpha vs beta for the trading interpretation.

The security market line#

CAPM makes an equilibrium prediction: across assets, expected excess return should be a straight line in beta — the security market line (SML), . An asset with no skill sits on the line; its whole return is compensation for the market risk it carries. An asset with genuine alpha sits above it — the vertical gap is the alpha.

The security market line: expected excess return plotted against beta, a straight line through the origin, with points above the line marked as positive alpha
The security market line. Beta is on the x-axis, expected excess return on the y. The line is what the market pays for risk alone; a point sitting above the line has positive alpha — return the market did not require it to earn. Alpha is a vertical distance from this line, not a level of return.

Multi-factor models#

A single market factor rarely explains everything. The Fama-French insight was that a couple more systematic factors — a size spread (small minus big, SMB) and a value spread (high minus low book-to-market, HML) — capture return variation the market misses. The regression just grows more columns:

Same normal equations, wider . Now each is a loading — how much of the strategy’s return is explained by each known factor — and the intercept is the return left after controlling for allof them. This is the harder, fairer bar: a “market-neutral” strategy can have yet be quietly loaded on value, and its apparent alpha vanishes once HML is in the regression.

edgekit’s ek.factors.factor_exposures runs this multivariate OLS. Pass the asset returns and a DataFrame of factor returns; it returns betas and tstats as dicts keyed by your factor column names, plus alpha, alpha_t, and r2.

factors.py
import edgekit as ek
import pandas as pd

factors = pd.DataFrame({"mkt": mkt_ret, "smb": smb_ret, "hml": hml_ret})
fx = ek.factors.factor_exposures(asset_returns, factors)

fx["betas"]     # {"mkt": 0.08, "smb": 0.41, "hml": -0.22}  loadings by name
fx["tstats"]    # {"mkt": 0.7, "smb": 3.9, "hml": -2.1}     significance by name
fx["alpha"]     # intercept AFTER controlling for every factor
fx["alpha_t"]   # is the leftover alpha real?
fx["r2"]        # variance spanned by the factors together
Scenario — a 'market-neutral' alpha that was really a value tilt

A long/short equity book reports a CAPM fit of (looks genuinely neutral) and with — apparently real skill uncorrelated to the index. Add the Fama-French value factor and re-run:

The HML loading is with — the book is quietly long cheap stocks and short expensive ones. With that captured, the intercept collapses to at . The “alpha” was never neutral skill; it was compensation for bearing value risk, a factor you can rent for a few basis points. Market-neutral in the sense is not factor-neutral. This is why the fair bar is a significant intercept after all the obvious factors are in the regression, not just after the market.

QuantityFormulaReads asedgekit key
OLS estimatorBest-fit coefficientsbeta
Standard errorEstimate uncertaintyse
t-statisticSignificant if |t| ≳ 2tstat / alpha_t
FitVariance explainedr2
SkillReturn net of factorsalpha
The regression discipline
Whenever a strategy looks good, the first question is: against what? Regress it on the obvious systematic factors and look at the intercept. If the alpha survives with a real t-stat, you may have something; if it collapses into a beta loading, you have re-discovered the market with extra steps. This is the same skepticism the gauntlet applies with permutation tests.

Next: we can measure risk and attribute return — now we choose the weights that trade one against the other. Optimization & portfolios.