edgekit.risk
Value-at-Risk & tail risk — how bad a normal-ish bad day looks (VaR), how bad the days beyond it look (CVaR / expected shortfall), and the drawdown-shape diagnostics that a single loss number misses. Three VaR methods, two ES methods, and the ulcer / tail-ratio pair.
What's inside. value_at_risk and expected_shortfall are the two headline estimators (with cvar as an alias for the latter); var_cvar returns both in one call. Below the quantile, drawdown_series reconstructs the full underwater curve, ulcer_index summarises its depth-and-duration pain, and tail_ratio compares the right tail to the left.
Both are reported as positive magnitudes of loss — a 5% VaR of 0.021 means “on the worst 5% of days you lose about 2.1%”, not -0.021. Because CVaR averages the losses beyond the VaR threshold, it is always at least as large: cvar >= var. If either comes back negative, your input is a gain at that quantile.
Value-at-Risk & expected shortfall#
value_at_risk#
The loss you should not exceed on all but the worst alpha fraction of periods — the standard one-number risk budget. Three methods trade off distributional assumptions: historical (empirical quantile, no assumption), gaussian (normal closed form), and cornish_fisher (normal adjusted for skew and kurtosis — better for fat tails).
value_at_risk(returns, alpha=0.05, method="historical") -> float| Param | Type | Default | Meaning |
|---|---|---|---|
returns | array-like | — | Per-period return series. |
alpha | float | 0.05 | Tail probability (0.05 = 95% VaR). |
method | str | "historical" | "historical", "gaussian", or "cornish_fisher". |
Returns: a float — the VaR as a positive loss fraction.
import edgekit as ek
ek.risk.value_at_risk(rets, alpha=0.05) # historical 95% VaR
ek.risk.value_at_risk(rets, alpha=0.01, method="cornish_fisher") # fat-tail-aware 99%expected_shortfall#
Expected shortfall (a.k.a. CVaR) — the averageloss on the days worse than the VaR threshold. Answers the question VaR ducks: “when it does break, how bad is it?”. Two methods: historical and gaussian. cvar is an exported alias for this function.
expected_shortfall(returns, alpha=0.05, method="historical") -> float
cvar(returns, alpha=0.05, method="historical") -> float # alias| Param | Type | Default | Meaning |
|---|---|---|---|
returns | array-like | — | Per-period return series. |
alpha | float | 0.05 | Tail probability. |
method | str | "historical" | "historical" or "gaussian". |
Returns: a float — the average tail loss as a positive fraction (always >= value_at_risk).
ek.risk.expected_shortfall(rets, alpha=0.05)
ek.risk.cvar(rets, alpha=0.05) # same thingvar_cvar#
Both risk numbers in a single call, sharing the same method and alpha — the convenience form when you report them together (which you should).
var_cvar(returns, alpha=0.05, method="historical") -> dict| Param | Type | Default | Meaning |
|---|---|---|---|
returns | array-like | — | Per-period return series. |
alpha | float | 0.05 | Tail probability. |
method | str | "historical" | VaR method ("historical" / "gaussian" / "cornish_fisher"; ES falls back to "gaussian" for cornish_fisher). |
Returns: a dict with keys "var" and "cvar", both positive loss numbers with cvar >= var.
r = ek.risk.var_cvar(rets, alpha=0.05)
r["var"], r["cvar"]
Drawdown shape#
drawdown_series#
The full underwater curve — the fractional distance below the running peak at every point. Where max_drawdown gives you the single worst gap, this gives you the whole shape: how often, how deep, and how long you were underwater.
drawdown_series(equity) -> np.ndarrayequity— a cumulative equity / wealth curve.
Returns: a numpy array of drawdowns (0 at new highs, negative or positive-magnitude below — same length as equity).
dd = ek.risk.drawdown_series(equity)ulcer_index#
The Ulcer Index — the root-mean-square of the drawdown series, so it penalises deep and prolonged drawdowns rather than just the single worst point. A pain metric that rewards curves which recover quickly.
ulcer_index(equity) -> floatequity— a cumulative equity curve.
Returns: a float — the RMS drawdown (lower is smoother).
tail_ratio#
The ratio of the right-tail quantile to the (absolute) left-tail quantile — how big the good extremes are relative to the bad ones. > 1 means the upside tail dominates (the positively-skewed profile trend-following wants); < 1 means losses tail harder than gains.
tail_ratio(returns, q=0.05) -> float # >1 upside tail dominates| Param | Type | Default | Meaning |
|---|---|---|---|
returns | array-like | — | Per-period return series. |
q | float | 0.05 | Tail fraction to compare on each side. |
Returns: a float — right-tail / |left-tail| ratio.
ek.risk.tail_ratio(rets, q=0.05) # >1 is the shape a trend edge should haveExtreme value theory#
Historical VaR runs out of data exactly where it matters — there are only a handful of observations in the far tail. Extreme value theory fits a parametric model to the exceedances over a high threshold (peaks-over-threshold), letting you extrapolate to quantiles beyond the sample. gpd_fit fits the generalized Pareto tail, evt_var_es turns it into VaR/ES numbers, and hill is the classic tail-index estimator.
gpd_fit#
Fit a generalized Pareto distribution to the losses exceeding the q-quantile threshold — the peaks-over-threshold (POT) method, estimated by probability-weighted moments (no scipy). The shape xi is the number to stare at: xi > 0 means a genuinely fat (power-law) tail; near 0 is exponential; negative is a bounded tail.
gpd_fit(losses, q=0.95) -> dict| Param | Type | Default | Meaning |
|---|---|---|---|
losses | array-like | — | Loss series as positive magnitudes (e.g. -returns clipped to losses). |
q | float | 0.95 | Threshold quantile — losses above it are the exceedances fitted. |
Returns: a dict with keys "xi" (GPD shape), "beta" (GPD scale), "threshold" (the loss level at q), and "n_exceed" (how many losses exceeded it).
import edgekit as ek
fit = ek.risk.gpd_fit(losses, q=0.95)
fit["xi"] # > 0 => power-law tail; historical VaR will understate the extremesevt_var_es#
EVT-based VaR and expected shortfall at level alpha, from a GPD tail fitted above the q-threshold. The point of the exercise: an honest 99% or 99.9% number even when the sample barely contains any observations that deep. Follows the module’s sign convention — both come back as positive loss magnitudes.
evt_var_es(returns, alpha=0.01, q=0.95) -> tuple[float, float]| Param | Type | Default | Meaning |
|---|---|---|---|
returns | array-like | — | Per-period return series (losses extracted internally). |
alpha | float | 0.01 | Tail probability of the VaR/ES level (0.01 = 99%). |
q | float | 0.95 | POT threshold quantile for the tail fit. |
Returns: a (var, es) tuple of floats — positive loss numbers with es >= var, same convention as value_at_risk / expected_shortfall.
var99, es99 = ek.risk.evt_var_es(rets, alpha=0.01, q=0.95)
# compare against the historical estimate — EVT usually reads higher, and is usually right
ek.risk.value_at_risk(rets, alpha=0.01)hill#
The Hill estimator of the tail index alpha from the klargest losses — the classic answer to “how fat is this tail?”. Smaller alpha = fatter tail; roughly, moments above order alphado not exist (alpha < 4 and kurtosis is meaningless, alpha < 2 and even the variance is suspect). Note it estimates alpha ≈ 1/xi, the reciprocal of the GPD shape.
hill(losses, k=None) -> float| Param | Type | Default | Meaning |
|---|---|---|---|
losses | array-like | — | Loss series as positive magnitudes. |
k | int | None | None | Number of top order statistics to use (default ~sqrt(n)). |
Returns: a float tail index (smaller = fatter tail).
a = ek.risk.hill(losses) # ~3 is typical for daily equity returns
a = ek.risk.hill(losses, k=100) # sensitivity-check across k before trusting itSee also#
- edgekit.metrics —
max_drawdown/sharpe/sortinoalongside these tail numbers. - edgekit.optimize — the portfolio whose tail you are measuring.
- edgekit.sizing — turn a drawdown budget into a position size.
