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:

  1. Direct functions — call individual metric functions with a pl.Series

  2. Dispatcher — use calculate_metric() with a Metric enum 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"), use simulate_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 when None and the metric requires it.

  • dates (Series | None) – Date series, required for LONGEST_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:

float

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\). When False, returns are summed (arithmetic).

Parameters:
  • ret (Series) – Bar-level return series.

  • compounded (bool) – If True, compound returns geometrically.

Returns:

Cumulative return as a decimal (not percent).

Return type:

float

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:
  • ret (Series) – Bar-level return series.

  • compounded (bool) – If True, compound returns geometrically.

  • ppy (int) – Periods per year — must match the bar frequency of ret. Use 252 for daily, 52 for weekly, 12 for monthly, 252 * 390 for minute bars, etc.

Returns:

Annualized growth rate as a decimal.

Return type:

float

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.

Parameters:
  • ret (Series) – Bar-level return series.

  • ppy (int) – Periods per year — must match the bar frequency of ret. Use 252 for daily, 52 for weekly, 12 for monthly, 252 * 390 for minute bars, etc.

Returns:

Annualized volatility as a decimal.

Return type:

float

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:
  • ret (Series) – Bar-level return series.

  • ppy (int) – Periods per year — must match the bar frequency of ret. Use 252 for daily, 52 for weekly, 12 for monthly, 252 * 390 for minute bars, etc.

  • rf (float) – Annual risk-free rate (e.g. 0.05 for 5 %).

Returns:

Annualized Sharpe ratio.

Return type:

float

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:
  • ret (Series) – Bar-level return series.

  • ppy (int) – Periods per year — must match the bar frequency of ret. Use 252 for daily, 52 for weekly, 12 for monthly, 252 * 390 for minute bars, etc.

  • rf (float) – Annual risk-free rate (e.g. 0.05 for 5 %).

Returns:

Annualized Sortino ratio.

Return type:

float

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 inf when there are gains but zero losses, and 0.0 for an empty series.

Parameters:
  • ret (Series) – Bar-level return series.

  • ppy (int) – Periods per year — must match the bar frequency of ret. Used only to convert the annual rf to a per-bar threshold.

  • rf (float) – Annual risk-free rate (e.g. 0.05 for 5 %).

Returns:

Omega ratio (>= 0; inf when losses are zero).

Return type:

float

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

\[\log(1 + R_H) \;\sim\; N\!\bigl(\hat{\mu} \, H \, \Delta t, \; \hat{\sigma}^2 \, H \, \Delta t\bigr)\]

so the analytic VaR / CVaR (in simple-return units) are

\[\begin{split}\mathrm{VaR}_\alpha &= \exp\!\bigl(\hat{\mu} H \Delta t + \hat{\sigma} \sqrt{H \Delta t} \, \Phi^{-1}(\alpha)\bigr) - 1 \\ \mathrm{CVaR}_\alpha &= \exp\!\bigl(\hat{\mu} H \Delta t - \hat{\sigma} \sqrt{H \Delta t} \; \tfrac{\phi(z_\alpha)}{\alpha}\bigr) - 1\end{split}\]

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:

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

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

  3. 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) use n_simulations >= 200/alpha so the CVaR tail (the worst \(\alpha\) fraction of paths) is well-populated.

  • Identical seed values across var() and cvar() 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.05 means 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. Under Innovations.GAUSSIAN and horizon=1 the 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". At horizon=1 MC under Gaussian innovations returns the closed-form Gaussian VaR.

  • n_simulations (int) – Monte Carlo path count. Rule of thumb: n_simulations >= 200/alpha keeps the CVaR tail well-populated.

  • dt (float) – Time step for the SDE (default 1/252 ≈ daily under the standard 252-trading-day year).

  • innovations (Innovations | None) – Noise distribution for method="monte_carlo". Defaults to Innovations.GAUSSIAN. See mktlib.data.Innovations.

  • df (float | None) – Degrees of freedom for Innovations.STUDENT_T (must be > 2).

  • seed (int | None) – RNG seed for reproducibility (Monte Carlo path only). Pass the same seed to var() and cvar() to make their simulated paths match exactly.

Returns:

The α-quantile return (usually negative).

Return type:

float

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.05 means that in the worst 5 % of bars, the average loss was 3.5 %.

:param Same as var().:

Returns:

Mean return in the worst alpha fraction (usually negative).

Return type:

float

Parameters:

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 that calculate_metric() stays lean for the historical / empirical metric set.

Currently supports Metric.VAR and Metric.CVAR. Other members raise ValueError — no simulatable definition exists for them in this release.

Parameters:
  • metric (Metric) – Which metric to simulate. Must be one of Metric.VAR or Metric.CVAR.

  • ret (Series) – Historical bar-level return series used to fit the parametric model (μ̂, σ̂ via _gbm_log_return_moments(); standardized residuals for Innovations.BOOTSTRAP).

  • alpha (float) – Tail probability (default 0.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 — call calculate_metric() for that.

  • horizon (int) – Forwarded to var() / 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 to var() / 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 to var() / 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 to var() / 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 to var() / 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 to var() / 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:

float

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:

  1. Fits mu_hat, sigma_hat from the log-returns of ret.

  2. Derives standardized residuals when innovations=Innovations.BOOTSTRAP.

  3. Runs monte_carlo(Process.GBM, ..., independent_streams=False) — the perf path; statistically i.i.d. by construction.

Parameters:
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 seed to this call and to the subsequent var() / cvar() calls — identical seeds produce identical paths under independent_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_ratio or profit_factor for 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:

float

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:

float

Parameters:

ret (Series)

mktlib.metrics.profit_factor(ret)[source]

Sum of gains divided by sum of losses.

While payoff_ratio compares 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 inf when there are gains but no losses, and 0.0 for an empty or all-negative series.

Return type:

float

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_ratio is zero (no wins or no losses).

Return type:

float

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_drawdown and longest_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) – If True, 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 of drawdown_series). Values should be <= 0.

Returns:

Mean drawdown (a negative number, or 0.0 if no drawdown occurred).

Return type:

float

mktlib.metrics.longest_drawdown_days(dd, dates)[source]

Longest drawdown duration in calendar days.

Returns:

Duration in calendar days.

Return type:

float

Raises:

ValueError – If dates is not provided (None passed via calculate_metric).

Parameters:
  • dd (Series)

  • dates (Series)