edgekit.metrics
Performance metrics — one honest home for the stats/mets/st builder that appeared ~47 times across the research scripts, each subtly different. Everything speaks R-multiples first; dollars arrive only when you pass an account.
What’s inside. Two entry points do the heavy lifting: trade_stats (from an array of per-trade R-multiples) and equity_stats (from a daily P&L / return series). The scalar metrics that were re-derived inline everywhere — profit_factor, max_drawdown, max_drawdown_pct, sharpe, sortino — are exposed standalone. And dd_matched_size is the one place that turns an R-stream into a dollar-per-R scalar against a drawdown budget (the sizing layer delegates to it).
pf (profit factor) and mar (return / max-drawdown) alongside it. sharpe and sortino both return 0.0 on degenerate or too-short input rather than blowing up.Scalar metrics#
profit_factor#
Gross win divided by gross loss. The single most robust summary of an edge — it survives skew, doesn’t assume normality, and is the number you cost-stress against. Returns inf when there are no losing trades.
profit_factor(r) -> floatReturns: the ratio as a float; inf if gross loss is 0.
from edgekit import profit_factor
profit_factor([2.0, -1.0, -1.0]) # -> 1.0 (2 won / 2 lost)
profit_factor([1.0, 1.0]) # -> inf (no losers)max_drawdown#
Max peak-to-trough drawdown of an equity curve, as a positive number in the curve’s own units. Pass a cumulative-sum curve for R, or a dollar equity curve for dollars.
max_drawdown(equity) -> floatReturns: the largest peak-minus-trough gap (a positive number in the input units).
from edgekit import max_drawdown
import numpy as np
eq = np.array([0, 1, 2, 1, 0, 3]) # cumulative R; peak 2 -> trough 0
max_drawdown(eq) # -> 2.0max_drawdown_pct#
Max drawdown expressed as a fraction of the running peak, rather than in raw units.
max_drawdown_pct(equity) -> floatReturns: the worst drawdown as a fraction of its preceding peak.
sharpe#
Annualised Sharpe of a per-period return series. Read the caveat above — this understates trend-following.
sharpe(returns, periods_per_year: float = 252.0) -> float| Param | Type | Default | Meaning |
|---|---|---|---|
returns | array-like | — | Per-period return series (non-finite values dropped). |
periods_per_year | float | 252.0 | Annualisation factor (252 daily, 365 for calendar-daily crypto). |
Returns: annualised Sharpe; 0.0 if std is 0 or fewer than 3 points.
sortino#
Annualised Sortino — Sharpe using downside deviation only (penalises loss volatility, not gains).
sortino(returns, periods_per_year: float = 252.0) -> floatReturns: annualised Sortino; 0.0 when there is no downside deviation.
Summary builders#
trade_stats#
The universal per-trade summary in R-space — the consolidated replacement for the ~47 copy-pasted stats builders. Feed it per-trade net R; pass dates to unlock annualised metrics and hold to unlock holding-period metrics.
trade_stats(r, dates=None, hold=None) -> dict| Param | Type | Default | Meaning |
|---|---|---|---|
r | array-like | — | Per-trade net R-multiples. |
dates | array-like | None | None | Exit dates — enables annualised R, max DD, Sharpe, frequency. |
hold | array-like | None | None | Holding period per trade (bars or days) — enables hold stats. |
Returns (dict keys). The set grows with what you pass:
- Always:
n,total_r,ev_r,win_rate,pf,avg_win,avg_loss,best,worst,win_streak,loss_streak. - When
n == 0: onlyn,total_r,ev_r,win_rate,pf. - With
dates: addsyears,ann_r,max_dd_r,mar,sharpe,worst_day_r,trades_per_year. (The Sharpe here is annualised at 365 — daily R buckets.) - With
hold: addsavg_hold,median_hold.
from edgekit import trade_stats
st = trade_stats(trades.r, dates=trades.date)
print(st["pf"], st["mar"], st["ann_r"]) # judge the edge on PF and MAR, not Sharpe
print(st["win_rate"], st["ev_r"]) # ev_r is expectancy per trade, in Requity_stats#
Summary of a daily P&L (or return) series — the equity-curve counterpart to trade_stats. If account is given, daily_pnl is treated as dollars and normalised by it; otherwise it is treated as fractional returns.
equity_stats(daily_pnl, account: float | None = None, periods_per_year: float = 252.0) -> dict| Param | Type | Default | Meaning |
|---|---|---|---|
daily_pnl | array-like | — | Daily P&L (dollars if account given) or fractional returns. |
account | float | None | None | Account size; when set, daily_pnl is dollars and normalised by it. |
periods_per_year | float | 252.0 | Annualisation factor for CAGR / Sharpe / Sortino. |
Returns (all keys): total_return, cagr, sharpe, sortino, max_dd_pct, mar, worst_day, best_day.
from edgekit.metrics import equity_stats
es = equity_stats(daily_pnl_dollars, account=100_000)
print(es["cagr"], es["max_dd_pct"], es["mar"])dd_matched_size#
The dollar-per-R scalar that sizes an R-stream to a drawdown budget — the honest bridge from an R backtest to a dollar-risked plan. It sizes so the historical max drawdown equals dd_budget * account (e.g. 0.095 for a 10% prop limit with buffer). If daily_cap is given (e.g. 0.045 for a 5% daily limit), whichever of the two constraints binds tighter wins — the dual-constraint sizing.
dd_matched_size(daily_r, dd_budget: float, account: float, daily_cap: float | None = None) -> dict| Param | Type | Default | Meaning |
|---|---|---|---|
daily_r | array-like | — | Daily R-stream (per-day sum of trade R-multiples). |
dd_budget | float | — | Max-drawdown budget as a fraction of account (e.g. 0.095). |
account | float | — | Account size in dollars. |
daily_cap | float | None | None | Optional worst-day limit as a fraction of account (e.g. 0.045). |
Returns (all keys): dollar_per_r (the sizing scalar), binding ("max-dd" or "daily" — which constraint set the size), and max_dd_r (the historical max drawdown in R).
dd95 estimate.from edgekit import dd_matched_size
info = dd_matched_size(daily_r, dd_budget=0.10, account=100_000, daily_cap=0.03)
info["dollar_per_r"] # dollars to risk per 1R so historical DD == budget
info["binding"] # "daily" if the worst-day cap bound tighter than max-DDSee also#
- edgekit.sizing —
size_to_ddwrapsdd_matched_sizeand returns the sized dollar series. - edgekit.costs — feed
trade_statsintocost_stress. - The validation gauntlet — where these metrics get stress-tested.