Metrics
Standalone financial metric functions operating on Polars return series.
No dependencies beyond polars — included in the base pip install mktlib.
Overview
Two usage patterns:
Direct functions — call individual metric functions with a
pl.SeriesDispatcher — use
calculate_metric()with aMetricenum value
from mktlib.metrics import sharpe, cumulative_return, drawdown_series
sr = sharpe(returns, rf=0.05)
cr = cumulative_return(returns)
dd = drawdown_series(returns)
from mktlib.metrics import calculate_metric, Metric
sr = calculate_metric(Metric.SHARPE, returns, rf=0.05)
Metric Enum
- class mktlib.metrics.Metric(*values)[source]
Enumeration of standalone financial metrics.
- CUMULATIVE_RETURN = 'cumulative_return'
- CAGR = 'cagr'
- ANNUALIZED_VOLATILITY = 'annualized_volatility'
- MAX_DRAWDOWN = 'max_drawdown'
- AVG_DRAWDOWN = 'avg_drawdown'
- LONGEST_DRAWDOWN_DAYS = 'longest_drawdown_days'
- SHARPE = 'sharpe'
- SORTINO = 'sortino'
- CALMAR = 'calmar'
- ROMAD = 'romad'
- OMEGA = 'omega'
- VAR = 'var'
- CVAR = 'cvar'
- WIN_RATE = 'win_rate'
- PAYOFF_RATIO = 'payoff_ratio'
- PROFIT_FACTOR = 'profit_factor'
- KELLY_CRITERION = 'kelly_criterion'
Dispatcher
- mktlib.metrics.calculate_metric(metric, ret, *, dd=None, dates=None, compounded=True, ppy=252, rf=0.0, alpha=0.05)[source]
Compute a single metric by enum value (historical / empirical only).
For forward-looking parametric estimators of VaR / CVaR (
"gaussian","monte_carlo"), usesimulate_metric()instead. Splitting the two keeps this dispatcher cheap and free of simulation kwargs.- Parameters:
metric (
Metric) – Which metric to compute.ret (
Series) – Bar-level return series.dd (
Series|None) – Pre-computed drawdown series. Lazily computed from ret whenNoneand the metric requires it.dates (
Series|None) – Date series, required forLONGEST_DRAWDOWN_DAYS.compounded (
bool) – Whether returns compound (affects cumulative return and drawdown).ppy (
int) – Periods per year for annualisation (default 252).rf (
float) – Annual risk-free rate.alpha (
float) – Tail-risk quantile for VaR/CVaR.
- Return type:
Return Metrics
- mktlib.metrics.cumulative_return(ret, compounded=True)[source]
Total cumulative return over the full series.
Answers “if I invested $1 at the start, how much did I gain or lose?” A value of 0.25 means a 25 % gain; −0.10 means a 10 % loss.
When compounded is
True: \(R = \prod(1 + r_i) - 1\). WhenFalse, returns are summed (arithmetic).
- mktlib.metrics.cagr(ret, compounded=True, ppy=252)[source]
Compound annual growth rate.
CAGR converts the total cumulative return into a smooth annualized rate, answering “what constant yearly return would produce the same final wealth?”
\(\text{CAGR} = (1 + R)^{1/n_{\text{years}}} - 1\)
Higher is better. A CAGR of 0.08 means 8 % per year. Returns 0.0 when the series is empty or the cumulative return is −100 % or worse.
- Parameters:
- Returns:
Annualized growth rate as a decimal.
- Return type:
- mktlib.metrics.annualized_volatility(ret, ppy=252)[source]
Annualized standard deviation of returns.
Volatility quantifies the dispersion of returns and is the most widely used measure of investment risk. Higher values indicate larger typical swings, both up and down.
\(\sigma_{\text{ann}} = \text{std}(r) \cdot \sqrt{N}\)
Typical daily-return volatility for US equities is 0.15–0.25 (15–25 % annualized). The annualization assumes returns are approximately i.i.d., which breaks down for assets with strong serial correlation.
Risk-Adjusted Ratios
- mktlib.metrics.sharpe(ret, ppy=252, rf=0.0)[source]
Annualized Sharpe ratio.
The Sharpe ratio measures risk-adjusted return: how much excess return (above the risk-free rate) you earn per unit of total volatility.
\(S = \frac{\bar{r} - r_f}{\sigma} \cdot \sqrt{N}\)
A Sharpe above 1.0 is generally considered good; above 2.0 is excellent. Values near or below 0 indicate the strategy barely beats (or underperforms) the risk-free rate after accounting for volatility.
The risk-free rate rf is specified as an annual rate and is converted internally to a per-bar rate by dividing by ppy.
- Parameters:
- Returns:
Annualized Sharpe ratio.
- Return type:
- mktlib.metrics.sortino(ret, ppy=252, rf=0.0)[source]
Annualized Sortino ratio.
Like the Sharpe ratio, but penalizes only downside volatility instead of total volatility. This is more appropriate when the return distribution is skewed, because upside surprises should not count against a strategy.
\(\text{Sortino} = \frac{\bar{r} - r_f}{\sigma_{\text{down}}} \cdot \sqrt{N}\)
where \(\sigma_{\text{down}} = \sqrt{\text{mean}(\min(r - r_f, 0)^2)}\).
A Sortino of 2.0+ is strong. Because it ignores upside variance, the Sortino is typically higher than the Sharpe for positively skewed strategies.
- Parameters:
- Returns:
Annualized Sortino ratio.
- Return type:
- mktlib.metrics.omega(ret, ppy=252, rf=0.0)[source]
Omega ratio.
The Omega ratio is the probability-weighted ratio of gains over losses relative to a threshold (the per-bar risk-free rate). Unlike Sharpe and Sortino, it captures the entire return distribution — all moments, not just mean and variance.
\(\Omega = \frac{\sum \max(r_i - \tau, 0)}{\sum \max(\tau - r_i, 0)}\)
An Omega of 1.0 means gains and losses are balanced. Values above 1 indicate net positive excess returns; higher is better. Returns
infwhen there are gains but zero losses, and 0.0 for an empty series.- Parameters:
- Returns:
Omega ratio (>= 0;
infwhen losses are zero).- Return type:
Tail Risk
The mktlib.metrics.var() and mktlib.metrics.cvar() functions
support three estimators via the method= kwarg:
"historical"(default) — empirical α-quantile of the input series. Non-parametric, no distributional assumption, no horizon — answers “how bad were the worst α of bars?”."gaussian"— closed-form parametric estimator under fitted GBM. Cheap and exact under Gaussian innovations."monte_carlo"— simulation-based; required for non-Gaussian innovations or when path-dependent extensions are added.
Closed-form Gaussian VaR / CVaR (the math). Fit \(\hat{\mu} = \overline{\log(1+r_t)} / \Delta t\) and \(\hat{\sigma} = \mathrm{std}(\log(1+r_t)) / \sqrt{\Delta t}\) from the input series, then the \(H\)-bar log-return is
so the analytic VaR / CVaR (in simple-return units) are
where \(\Phi^{-1}\) is the standard-normal inverse CDF (computed
via statistics.NormalDist), \(\phi\) is the standard-normal
PDF, and \(z_\alpha = \Phi^{-1}(\alpha)\). Both are typically negative.
Square-root-of-time scaling. The variance of the cumulative log-return is linear in \(H\) under i.i.d. innovations (\(\mathrm{Var}(\sum r_t) = H \sigma^2 \Delta t\)), so the volatility component scales as \(\sqrt{H}\) — Basel’s “square-root-of-time” rule. Drift scales linearly in \(H\). At short horizons \(\hat{\sigma}\sqrt{H \Delta t}\) dominates; at long horizons drift catches up and the signal-to-noise ratio improves as \(\sqrt{H}\cdot \hat{\mu} / \hat{\sigma}\).
Choosing between "gaussian" and "monte_carlo". Under pure
GBM with Gaussian innovations the two estimators agree at the population
level: MC converges to the closed-form quantity at rate
\(\mathcal{O}(1/\sqrt{N})\). This agreement is a feature, not a
redundancy — running method="monte_carlo" against
method="gaussian" is the canonical way to validate a simulator
implementation, and the simulated paths themselves carry information
the analytic VaR number alone discards (full forecast distribution,
percentile bands for visualisation, the ability to ask
“what fraction of paths breach −10 %?”, and so on).
That said, when all you need is the scalar tail number and the
innovations really are Gaussian, "gaussian" is the cheaper estimator
of choice — it computes the same answer in microseconds via
statistics.NormalDist.
The simulation path becomes necessary (not just useful) when:
Innovations stop being Gaussian — Student-t (heavier tails), bootstrapped empirical residuals, or any callable noise source. Sums of Student-t aren’t Student-t (only the Gaussian is stable under summation), so no analytic form exists.
Path-dependent measures — drawdown over horizon, time-to-barrier, expected shortfall conditional on a within-horizon event. These are functionals of the whole path, not the endpoint.
Autocorrelated returns / fBm — variance of the H-bar sum is no longer \(H \sigma^2\). Under fBm with Hurst \(h\), variance scales as \(H^{2h}\): sub-diffusive (\(h < 0.5\)) tightens long-horizon tails; super-diffusive (\(h > 0.5\)) fattens them.
Practical caveats.
Single-bar VaR (
horizon=1) under Gaussian innovations: the"monte_carlo"and"gaussian"paths converge to the same number. Pick"gaussian"for the scalar; pick"monte_carlo"when you also want the full sample distribution behind it.Tail-size rule of thumb: at small \(\alpha\) (e.g.
0.01) usen_simulations >= 200/alphaso the CVaR tail (the worst \(\alpha\) fraction of paths) is well-populated.Identical seed values across
var()andcvar()calls produce identical sample paths under the perf path (independent_streams=False). Pass the same seed to both calls when you want their tail numbers to come from the same simulation.
- mktlib.metrics.var(ret, alpha=0.05, *, method='historical', horizon=1, n_simulations=10000, dt=0.003968253968253968, innovations=None, df=None, seed=None)[source]
Value at Risk at the alpha confidence level.
VaR answers “what is the worst return I can expect in all but the worst alpha fraction of bars?” At
alpha=0.05, this is the 5th-percentile return — on 95 % of bars, the return was at least this good.\(\text{VaR}_{\alpha} = Q_{\alpha}(r)\)
The result is typically negative. A VaR of −0.02 at
alpha=0.05means losses exceeded 2 % only about 5 % of the time.- Parameters:
ret (
Series) – Bar-level return series (simple returns; e.g. close-to-close pct changes).alpha (
float) – Tail probability (default 0.05 = 5th percentile).method (
Literal['historical','gaussian','monte_carlo']) –"historical"(default) — empirical quantile of ret."gaussian"— closed-form parametric VaR under GBM, fitted from ret. Uses \(\mu \cdot H \Delta t + \sigma \sqrt{H \Delta t}\,\Phi^{-1}(\alpha)\)."monte_carlo"— simulation-based; required when innovations is non-Gaussian or when path-dependent extensions are added. UnderInnovations.GAUSSIANandhorizon=1the result matches"gaussian"modulo sampling noise — the simulation pays compute for nothing there.
horizon (
int) – Forecast horizon in bars. Only used by"gaussian"/"monte_carlo". Athorizon=1MC under Gaussian innovations returns the closed-form Gaussian VaR.n_simulations (
int) – Monte Carlo path count. Rule of thumb:n_simulations >= 200/alphakeeps the CVaR tail well-populated.dt (
float) – Time step for the SDE (default1/252≈ daily under the standard 252-trading-day year).innovations (
Innovations|None) – Noise distribution formethod="monte_carlo". Defaults toInnovations.GAUSSIAN. Seemktlib.data.Innovations.df (
float|None) – Degrees of freedom forInnovations.STUDENT_T(must be > 2).seed (
int|None) – RNG seed for reproducibility (Monte Carlo path only). Pass the same seed tovar()andcvar()to make their simulated paths match exactly.
- Returns:
The α-quantile return (usually negative).
- Return type:
- mktlib.metrics.cvar(ret, alpha=0.05, *, method='historical', horizon=1, n_simulations=10000, dt=0.003968253968253968, innovations=None, df=None, seed=None)[source]
Conditional Value at Risk (Expected Shortfall) at alpha.
CVaR answers “when losses do exceed VaR, how bad are they on average?” It is the mean of all returns at or below the VaR threshold and is always at least as extreme as VaR itself.
\(\text{CVaR}_{\alpha} = E[r \mid r \leq \text{VaR}_{\alpha}]\)
CVaR is preferred over VaR by many risk frameworks (including Basel III) because it is coherent — it does not underestimate the risk of concentrated tail events.
A CVaR of −0.035 at
alpha=0.05means that in the worst 5 % of bars, the average loss was 3.5 %.:param Same as
var().:
Forward-Looking Estimators
- mktlib.metrics.simulate_metric(metric, ret, *, alpha=0.05, method='monte_carlo', horizon=1, n_simulations=10000, dt=0.003968253968253968, innovations=None, df=None, seed=None)[source]
Forward-looking parametric estimator for VaR / CVaR.
Companion to
calculate_metric()— this dispatcher hosts the simulation-aware estimators ("gaussian"closed-form,"monte_carlo") so thatcalculate_metric()stays lean for the historical / empirical metric set.Currently supports
Metric.VARandMetric.CVAR. Other members raiseValueError— no simulatable definition exists for them in this release.- Parameters:
metric (
Metric) – Which metric to simulate. Must be one ofMetric.VARorMetric.CVAR.ret (
Series) – Historical bar-level return series used to fit the parametric model (μ̂, σ̂ via_gbm_log_return_moments(); standardized residuals forInnovations.BOOTSTRAP).alpha (
float) – Tail probability (default0.05).method (
Literal['historical','gaussian','monte_carlo']) –"gaussian"for the closed-form parametric estimator or"monte_carlo"(default) for the simulation-based path."historical"is rejected — callcalculate_metric()for that.horizon (
int) – Forwarded tovar()/cvar(). Pass the same seed to both metrics if you want their simulated paths to match exactly (this is what Reports does internally so the chart and the displayed VaR / CVaR numbers agree).n_simulations (
int) – Forwarded tovar()/cvar(). Pass the same seed to both metrics if you want their simulated paths to match exactly (this is what Reports does internally so the chart and the displayed VaR / CVaR numbers agree).dt (
float) – Forwarded tovar()/cvar(). Pass the same seed to both metrics if you want their simulated paths to match exactly (this is what Reports does internally so the chart and the displayed VaR / CVaR numbers agree).innovations (
Innovations|None) – Forwarded tovar()/cvar(). Pass the same seed to both metrics if you want their simulated paths to match exactly (this is what Reports does internally so the chart and the displayed VaR / CVaR numbers agree).df (
float|None) – Forwarded tovar()/cvar(). Pass the same seed to both metrics if you want their simulated paths to match exactly (this is what Reports does internally so the chart and the displayed VaR / CVaR numbers agree).seed (
int|None) – Forwarded tovar()/cvar(). Pass the same seed to both metrics if you want their simulated paths to match exactly (this is what Reports does internally so the chart and the displayed VaR / CVaR numbers agree).
- Returns:
VaR or CVaR at the requested confidence and horizon.
- Return type:
- mktlib.metrics.monte_carlo_paths(ret, *, horizon, n_simulations=10000, dt=0.003968253968253968, innovations=None, df=None, seed=None)[source]
Run one MC GBM batch fitted to ret; return the full simulation frame.
Companion entry point for callers — typically reporting code — that need the full price paths (e.g. for charting). Internally:
Fits
mu_hat, sigma_hatfrom the log-returns of ret.Derives standardized residuals when
innovations=Innovations.BOOTSTRAP.Runs
monte_carlo(Process.GBM, ..., independent_streams=False)— the perf path; statistically i.i.d. by construction.
- Parameters:
ret (
Series) – Historical bar-level return series used to fit the GBM model.horizon (
int) – Forecast horizon in bars (number of SDE steps; the returned frame hashorizon + 1price points per simulation including the base value at step 0).n_simulations (
int) – Forwarded tomktlib.data.monte_carlo(). Seemktlib.data.Innovationsfor the innovation contract.dt (
float) – Forwarded tomktlib.data.monte_carlo(). Seemktlib.data.Innovationsfor the innovation contract.innovations (
Innovations|None) – Forwarded tomktlib.data.monte_carlo(). Seemktlib.data.Innovationsfor the innovation contract.df (
float|None) – Forwarded tomktlib.data.monte_carlo(). Seemktlib.data.Innovationsfor the innovation contract.seed (
int|None) – Forwarded tomktlib.data.monte_carlo(). Seemktlib.data.Innovationsfor the innovation contract.
- Returns:
Long-form frame with columns
simulation,seed,step,price(base = 1.0; multiply by initial portfolio equity for absolute units).- Return type:
DataFrame
Notes
Callers who want the chart and the VaR / CVaR numbers to come from the same simulation paths should pass a fixed
seedto this call and to the subsequentvar()/cvar()calls — identical seeds produce identical paths underindependent_streams=False, so all three artefacts are mutually consistent. At the perf-path defaults (10k × 22 ≈ 10–15 ms per batch) running MC three times per report is invisible inside the typical tearsheet render.
The monte_carlo_paths() helper is the entry point used by
Reports to render the Monte Carlo simulation-paths chart. It
runs MC and returns the full sims frame (long-form
simulation, seed, step, price — base price 1.0, scale by initial
equity for absolute units). Callers who want the chart paths and a
downstream VaR / CVaR to come from the same simulation pass an
identical seed to every call: deterministic seeding produces
byte-for-byte identical samples. At the perf-path defaults
(~10–15 ms per 10 k × 22 batch) re-running MC for each metric is
cheap, so there is no shared-batch machinery to thread through.
Win/Loss
- mktlib.metrics.win_rate(ret)[source]
Fraction of positive-return bars.
Win rate alone says little about profitability — a strategy can win on 90 % of bars but still lose money if the average loss far exceeds the average gain. Always pair with
payoff_ratioorprofit_factorfor a complete picture.Returns a value in [0, 1]. A win rate of 0.55 means 55 % of bars had a positive return.
- Return type:
- Parameters:
ret (Series)
- mktlib.metrics.payoff_ratio(ret)[source]
Average win divided by average loss.
Measures the magnitude of the typical winning bar relative to the typical losing bar.
\(\text{payoff} = \frac{\text{mean}(r^+)}{|\text{mean}(r^-)|}\)
A payoff ratio above 1.0 means winners are larger than losers on average. Combined with
win_rate, it determines whether a strategy is profitable: a low win rate can still be profitable if the payoff ratio is high enough (trend-following), and vice versa (mean-reversion).Returns 0.0 when there are no wins or no losses.
- Return type:
- Parameters:
ret (Series)
- mktlib.metrics.profit_factor(ret)[source]
Sum of gains divided by sum of losses.
While
payoff_ratiocompares averages, profit factor compares totals — it answers “for every dollar lost, how many dollars were gained?”\(\text{PF} = \frac{\sum r^+}{|\sum r^-|}\)
A profit factor above 1.0 means the strategy is net profitable. Values above 2.0 are strong. Returns
infwhen there are gains but no losses, and 0.0 for an empty or all-negative series.- Return type:
- Parameters:
ret (Series)
- mktlib.metrics.kelly_criterion(ret)[source]
Kelly criterion from bar-level returns.
The Kelly criterion gives the theoretically optimal fraction of capital to risk per bar in order to maximize the long-run geometric growth rate, assuming i.i.d. returns.
\(K = w - \frac{1 - w}{P}\)
where w is the win rate and P is the payoff ratio.
A positive Kelly means the edge is positive; the magnitude suggests how aggressively to size positions (though practitioners typically use a fraction of full Kelly — “half Kelly” — to reduce variance). A negative value means the strategy has negative expected geometric growth.
Returns 0.0 when
payoff_ratiois zero (no wins or no losses).- Return type:
- Parameters:
ret (Series)
Drawdown
- mktlib.metrics.drawdown_series(ret, compounded=True)[source]
Compute drawdown series from returns.
A drawdown measures the decline from the most recent equity peak at each point in time. Values are zero when equity is at a new high and negative when below one. The series is useful for visualizing underwater periods and as input to
avg_drawdownandlongest_drawdown_days.When compounded is
True(the default), equity is built via cumulative product; otherwise via cumulative sum (arithmetic returns).- Parameters:
ret (
Series) – Bar-level return series (e.g. daily close-to-close percent changes).compounded (
bool) – IfTrue, compound returns geometrically.
- Returns:
Series named
"drawdown"with values in (-1, 0].- Return type:
Series
- mktlib.metrics.avg_drawdown(dd)[source]
Average of drawdown values during drawdown episodes.
While max drawdown captures the worst single decline, average drawdown gives a sense of the typical underwater experience. A strategy with a deep max drawdown but shallow average drawdown had one bad episode; one where both are close had persistently poor periods.
Only bars where the drawdown is strictly negative are included. Returns 0.0 when the series is always at or above its high-water mark.
- Parameters:
dd (
Series) – Pre-computed drawdown series (output ofdrawdown_series). Values should be <= 0.- Returns:
Mean drawdown (a negative number, or 0.0 if no drawdown occurred).
- Return type:
- mktlib.metrics.longest_drawdown_days(dd, dates)[source]
Longest drawdown duration in calendar days.
- Returns:
Duration in calendar days.
- Return type:
- Raises:
ValueError – If dates is not provided (None passed via calculate_metric).
- Parameters:
dd (Series)
dates (Series)