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.
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:
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.

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.
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.
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.
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 .
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 explainsRegress 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.”
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.

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.
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 togetherA 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.
| Quantity | Formula | Reads as | edgekit key |
|---|---|---|---|
| OLS estimator | Best-fit coefficients | beta | |
| Standard error | Estimate uncertainty | se | |
| t-statistic | Significant if |t| ≳ 2 | tstat / alpha_t | |
| Fit | Variance explained | r2 | |
| Skill | Return net of factors | alpha |
Next: we can measure risk and attribute return — now we choose the weights that trade one against the other. Optimization & portfolios.

