edgekit

edgekit.factors

Regression & factor models — the linear-algebra layer that answers “is this return stream alpha, or just beta to something I already own?”. A from-scratch OLS core, CAPM, multi-factor exposures, rolling beta, and Newey-West standard errors, all in numpy.

What's inside. Everything sits on one engine: ols, a plain least-squares fit that returns coefficients with their t-stats and R². capm is the single-factor case that isolates alpha against a market; factor_exposures generalises it to a factor panel; rolling_beta tracks a time-varying loading; and newey_west_se gives you heteroskedasticity- and autocorrelation-robust standard errors when residuals are not i.i.d. (they rarely are).

t-stats are asymptotic, not exact
These are numpy least-squares fits — the reported tstat values assume large samples and (except via newey_west_se) i.i.d. residuals. Treat |t| > 2 as the rough significance bar, and reach for Newey-West standard errors whenever returns are overlapping or autocorrelated.

Core regression#

ols#

Ordinary least squares — regress y on X and get back coefficients, their standard errors and t-stats, fit quality, and the residuals. The primitive every other function in this module is built on.

ols(y, X, add_const=True) -> dict
ParamTypeDefaultMeaning
yarray (T,)The dependent variable (e.g. asset returns).
Xarray (T, k)The regressor matrix (one column per factor).
add_constboolTruePrepend an intercept column (the alpha term).

Returns: a dict with keys "beta" (coefficient vector, intercept first if add_const), "se" (standard errors), "tstat" (t-statistics), "r2", "adj_r2", "resid" (residuals), "fitted" (fitted values), and "n" (sample size).

import edgekit as ek
fit = ek.factors.ols(y, X)
fit["beta"], fit["tstat"], fit["adj_r2"]

Factor models#

capm#

The single-factor CAPM regression — regress an asset's excess return on the market's and read off alpha and beta with their significance. The first question to ask any new edge: does it have a positive, significant alpha once you strip out market exposure?

capm(asset_returns, market_returns, rf=0.0, periods_per_year=252.0) -> dict
ParamTypeDefaultMeaning
asset_returnsarray (T,)The asset / strategy return series.
market_returnsarray (T,)The market benchmark return series.
rffloat0.0Per-period risk-free rate subtracted from both.
periods_per_yearfloat252.0Annualisation factor for alpha_annual.

Returns: a dict with keys "alpha" (per-period intercept), "beta" (market loading), "alpha_t" and "beta_t" (their t-stats), "r2", and "alpha_annual" (alpha annualised by periods_per_year).

c = ek.factors.capm(strat_rets, mkt_rets, rf=0.0, periods_per_year=365)
c["alpha_annual"], c["alpha_t"], c["beta"]   # want alpha_t > 2 and beta near 0

factor_exposures#

Multi-factor regression — regress an asset on a panel of factors (market, size, value, momentum, …) to get its loading on each plus the residual alpha. The generalisation of capm to more than one systematic driver.

factor_exposures(asset_returns, factors) -> dict
ParamTypeDefaultMeaning
asset_returnsarray (T,)The asset / strategy return series.
factorsarray (T, k) / DataFrameThe factor-return panel (one column per factor).

Returns: a dict with keys "betas" (loading on each factor), "tstats" (their t-stats), "alpha" (the intercept), "alpha_t" (its t-stat), and "r2".

fx = ek.factors.factor_exposures(strat_rets, factor_panel)
fx["betas"], fx["alpha"], fx["alpha_t"]

Rolling & robust#

rolling_beta#

Beta of y on x estimated in a trailing window— a time-varying loading that shows when an exposure drifts (a strategy's market beta creeping up in a trend, say). Leading positions are NaN until the window fills.

rolling_beta(y, x, window) -> np.ndarray
ParamTypeDefaultMeaning
yarray (T,)Dependent series (e.g. strategy returns).
xarray (T,)Explanatory series (e.g. market returns).
windowintRolling lookback in bars.

Returns: a numpy array of rolling betas aligned to the inputs (leading positions NaN).

b = ek.factors.rolling_beta(strat_rets, mkt_rets, window=60)

newey_west_se#

Newey-West HAC standard errors — heteroskedasticity- and autocorrelation-consistent errors for a fitted regression. Use them whenever residuals are autocorrelated (overlapping returns, persistent series); the plain OLS standard errors understate uncertainty there and make weak edges look significant.

newey_west_se(X, resid, lags) -> np.ndarray
ParamTypeDefaultMeaning
Xarray (T, k)The regressor matrix used in the fit (include the constant column).
residarray (T,)The regression residuals (e.g. ols()["resid"]).
lagsintBandwidth: number of autocorrelation lags to correct for.

Returns: a numpy array of HAC standard errors, one per coefficient in X.

fit = ek.factors.ols(y, X)
se_hac = ek.factors.newey_west_se(fit["X"] if "X" in fit else X, fit["resid"], lags=5)
t_hac = fit["beta"] / se_hac   # robust t-stats

Cross-section & events#

The regressions above run down a single time series. This block runs across assets and around events: newey_west is the HAC standard error of a mean (the scalar companion to newey_west_se), fama_macbeth estimates factor premia from period-by-period cross-sectional regressions, and event_study measures the abnormal return around a list of event dates.

newey_west#

HAC standard error of the mean of a series — the one number you divide a mean return by to get a t-stat that survives autocorrelation. It is what fama_macbethuses to test premia, exposed standalone because “is this mean significantly non-zero?” comes up constantly with overlapping or persistent series.

newey_west(x, lags=None) -> float
ParamTypeDefaultMeaning
xarray (T,)The series whose mean you are testing.
lagsint | NoneNoneHAC bandwidth; default = the standard automatic rule from T.

Returns: a float — the HAC standard error of mean(x).

se = ek.factors.newey_west(monthly_premium)
t = monthly_premium.mean() / se   # autocorrelation-robust t-stat

fama_macbeth#

Fama-MacBeth two-pass regression — each period, regress the cross-section of asset returns on the assets’ characteristics; the time-series average of the per-period slopes is the premium earned per unit of each characteristic, tested with Newey-West errors. The standard way to ask “does this characteristic actually price the cross-section?”.

fama_macbeth(returns, characteristics) -> dict
ParamTypeDefaultMeaning
returnsDataFrame (T, N)Asset returns, one column per asset.
characteristicsdict[str, DataFrame]Name → T×N characteristic panel, each ALREADY lagged by you.

Returns: a dict with keys "premia" (Series — the average slope per characteristic), "t_nw" (their Newey-West t-stats), and "by_period" (DataFrame of the per-period slopes).

Lag the characteristics yourself
Each characteristic panel must hold information known before the return it is regressed on — shift it a period before passing it in. Feeding a same-period characteristic is the cross-sectional version of look-ahead, and it manufactures premia out of nothing.
chars = {"momentum": mom.shift(1), "size": size.shift(1)}   # lagged by the caller
fm = ek.factors.fama_macbeth(rets_panel, chars)
fm["premia"], fm["t_nw"]   # want |t_nw| > 2 on the premium you care about

event_study#

Average abnormal return around a list of events — for each event, returns in the window around it are market-adjusted by the mean over a preceding estimationwindow, then averaged across events into the AAR and its cumulative sum (CAR). The honest test of “does anything actually happen on these dates?”.

event_study(returns, events, window=(-5, 10), estimation=60) -> dict
ParamTypeDefaultMeaning
returnspd.SeriesThe asset's return series (indexed by date).
eventslistEvent timestamps (must exist in / align to the index).
windowtuple(-5, 10)Relative-day window around each event (inclusive).
estimationint60Bars before the window used to estimate the normal return.

Returns: a dict with keys "aar" (Series of average abnormal return, indexed by relative day), "car" (its cumulative sum), "t_car" (t-stat of the final CAR), and "n_events" (events actually used).

es = ek.factors.event_study(rets, earnings_dates, window=(-5, 10), estimation=60)
es["car"].plot()          # the cumulative abnormal-return path
es["t_car"], es["n_events"]

See also#