Skip to content

Metrics

All single-series metrics (sharpe, sortino, calmar, omega, max_drawdown, ulcer_index, value_at_risk, cvar) take a returns Series and a small number of keyword arguments (periods_per_year, risk_free, alpha, target) with sensible defaults for daily data. The batch_* variants accept a dict of named series or a wide DataFrame and dispatch to the Rust-accelerated kernel when available — see Rust kernels. returns_stats / batch_summary produce the same Series / DataFrame shape used by the tear sheet, so custom reporting code can share the formatting layer.

fundcloud.metrics

Portfolio analytics.

Free functions on returns. Every function accepts a pd.Series (single strategy) or a pd.DataFrame (panel of strategies as columns) and returns the same shape: a scalar stays a scalar for Series input, a Series indexed by column name for DataFrame input.

Organised into six concerns:

  • :mod:fundcloud.metrics.core — scalar metrics independent of a benchmark (return, risk, risk-adjusted, higher moments).
  • :mod:fundcloud.metrics.benchmark — benchmark-relative metrics (alpha, beta, capture ratios, Treynor, information ratio).
  • :mod:fundcloud.metrics.periods — calendar-period aggregates (monthly / yearly tables, best/worst, positive/negative counts).
  • :mod:fundcloud.metrics.rolling — rolling-window metric series (Sharpe / Sortino / volatility / beta / drawdown).
  • :mod:fundcloud.metrics.summary — :func:metrics one-shot bundle and :func:drawdown_details episode table.
  • :mod:fundcloud.metrics.batch — GIL-released batch variants over large panels via the Rust kernels.

sharpe

sharpe(
    returns: Series,
    *,
    risk_free: float | None = ...,
    periods_per_year: int | None = ...,
) -> float
sharpe(
    returns: DataFrame,
    *,
    risk_free: float | None = ...,
    periods_per_year: int | None = ...,
) -> pd.Series
sharpe(
    returns: Series | DataFrame,
    *,
    risk_free: float | None = None,
    periods_per_year: int | None = None,
) -> float | pd.Series

Annualised Sharpe ratio.

Uses the sample standard deviation (ddof=1). Returns are assumed to be simple per-period returns; for log returns the formula is the same numerator and denominator.

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series for a single strategy or DataFrame (one column per strategy) for a panel.

required
risk_free float | None

Annualised risk-free rate to subtract from returns. None falls back to :func:fundcloud.get_config().risk_free_rate.

None
periods_per_year int | None

Annualisation factor. None falls back to :func:fundcloud.get_config().periods_per_year (252 by default).

None

Returns:

Type Description
float or Series

Scalar when returns is a Series; Series indexed by column name when returns is a DataFrame.

See Also

smart_sharpe : Sharpe scaled by an autocorrelation penalty. sortino : Downside-only analogue. probabilistic_sharpe_ratio : Probability the true Sharpe exceeds a target.

Examples:

>>> import pandas as pd, numpy as np
>>> rng = np.random.default_rng(0)
>>> r = pd.Series(rng.normal(0.0005, 0.01, 252))
>>> round(sharpe(r, periods_per_year=252), 2)
0.7
>>> # Works on a DataFrame too — returns a Series indexed by column:
>>> panel = pd.DataFrame({"a": r, "b": -r})
>>> isinstance(sharpe(panel), pd.Series)
True
Source code in python/fundcloud/metrics/core.py
def sharpe(
    returns: pd.Series | pd.DataFrame,
    *,
    risk_free: float | None = None,
    periods_per_year: int | None = None,
) -> float | pd.Series:
    """Annualised Sharpe ratio.

    Uses the **sample** standard deviation (``ddof=1``). Returns are
    assumed to be simple per-period returns; for log returns the
    formula is the same numerator and denominator.

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` for a single strategy or
        ``DataFrame`` (one column per strategy) for a panel.
    risk_free
        Annualised risk-free rate to subtract from returns. ``None``
        falls back to :func:`fundcloud.get_config().risk_free_rate`.
    periods_per_year
        Annualisation factor. ``None`` falls back to
        :func:`fundcloud.get_config().periods_per_year` (252 by default).

    Returns
    -------
    float or pd.Series
        Scalar when ``returns`` is a Series; Series indexed by column
        name when ``returns`` is a DataFrame.

    See Also
    --------
    smart_sharpe : Sharpe scaled by an autocorrelation penalty.
    sortino : Downside-only analogue.
    probabilistic_sharpe_ratio : Probability the true Sharpe exceeds a target.

    Examples
    --------
    >>> import pandas as pd, numpy as np
    >>> rng = np.random.default_rng(0)
    >>> r = pd.Series(rng.normal(0.0005, 0.01, 252))
    >>> round(sharpe(r, periods_per_year=252), 2)  # doctest: +SKIP
    0.7
    >>> # Works on a DataFrame too — returns a Series indexed by column:
    >>> panel = pd.DataFrame({"a": r, "b": -r})
    >>> isinstance(sharpe(panel), pd.Series)
    True
    """
    df = _to_df(returns)
    ppy = _periods(periods_per_year)
    rf_pp = _rf_per_period(risk_free, ppy)
    excess = df - rf_pp
    mu = excess.mean()
    sigma = excess.std(ddof=1)
    out = (mu / sigma) * np.sqrt(ppy)
    out = out.replace([np.inf, -np.inf], np.nan)
    return _collapse(out, returns)

sortino

sortino(
    returns: Series,
    *,
    target: float = ...,
    periods_per_year: int | None = ...,
) -> float
sortino(
    returns: DataFrame,
    *,
    target: float = ...,
    periods_per_year: int | None = ...,
) -> pd.Series
sortino(
    returns: Series | DataFrame,
    *,
    target: float = 0.0,
    periods_per_year: int | None = None,
) -> float | pd.Series

Annualised Sortino ratio.

Like Sharpe but penalises only the downside: the denominator is the root-mean-square of negative deviations from target, divided by the full sample count (ddof=0).

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series (single strategy) or DataFrame (panel of strategies as columns).

required
target float

Minimum acceptable return per period. Returns at or above target contribute zero to the downside deviation. Default 0.0.

0.0
periods_per_year int | None

Annualisation factor. None falls back to the global config.

None

Returns:

Type Description
float or Series

Scalar for a Series input, Series for a DataFrame input.

See Also

sharpe : Symmetric counterpart. adjusted_sortino : Sortino / sqrt(2) for scale-comparison with Sharpe. smart_sortino : Sortino scaled by Lo's autocorrelation penalty.

Source code in python/fundcloud/metrics/core.py
def sortino(
    returns: pd.Series | pd.DataFrame,
    *,
    target: float = 0.0,
    periods_per_year: int | None = None,
) -> float | pd.Series:
    """Annualised Sortino ratio.

    Like Sharpe but penalises only the downside: the denominator is
    the root-mean-square of *negative* deviations from ``target``,
    divided by the full sample count (``ddof=0``).

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` (single strategy) or
        ``DataFrame`` (panel of strategies as columns).
    target
        Minimum acceptable return per period. Returns at or above
        ``target`` contribute zero to the downside deviation. Default
        ``0.0``.
    periods_per_year
        Annualisation factor. ``None`` falls back to the global config.

    Returns
    -------
    float or pd.Series
        Scalar for a Series input, Series for a DataFrame input.

    See Also
    --------
    sharpe : Symmetric counterpart.
    adjusted_sortino : ``Sortino / sqrt(2)`` for scale-comparison with Sharpe.
    smart_sortino : Sortino scaled by Lo's autocorrelation penalty.
    """
    df = _to_df(returns)
    ppy = _periods(periods_per_year)
    diff = df - target
    downside = diff.clip(upper=0.0)
    # pop std with mean=0 => sqrt(mean(x^2))
    dd = np.sqrt((downside**2).mean())
    mu = diff.mean()
    out = (mu / dd) * np.sqrt(ppy)
    out = out.replace([np.inf, -np.inf], np.nan)
    return _collapse(out, returns)

calmar

calmar(
    returns: Series, *, periods_per_year: int | None = ...
) -> float
calmar(
    returns: DataFrame,
    *,
    periods_per_year: int | None = ...,
) -> pd.Series
calmar(
    returns: Series | DataFrame,
    *,
    periods_per_year: int | None = None,
) -> float | pd.Series

Calmar ratio: annualised return divided by |max_drawdown|.

A drawdown-aware return-on-risk score. Often more informative than Sharpe in volatile markets because it cares about the worst realised loss rather than full-distribution variance.

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series or DataFrame.

required
periods_per_year int | None

Annualisation factor for the CAGR numerator. None falls back to the global config.

None

Returns:

Type Description
float or Series

Scalar for a Series input, Series for a DataFrame input. NaN when the sample never drew down.

See Also

sharpe : Volatility-based risk-adjusted return. pain_ratio : Like Calmar but uses pain_index instead of max drawdown.

Source code in python/fundcloud/metrics/core.py
def calmar(
    returns: pd.Series | pd.DataFrame,
    *,
    periods_per_year: int | None = None,
) -> float | pd.Series:
    """Calmar ratio: annualised return divided by ``|max_drawdown|``.

    A drawdown-aware return-on-risk score. Often more informative than
    Sharpe in volatile markets because it cares about the worst
    realised loss rather than full-distribution variance.

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` or ``DataFrame``.
    periods_per_year
        Annualisation factor for the CAGR numerator. ``None`` falls
        back to the global config.

    Returns
    -------
    float or pd.Series
        Scalar for a Series input, Series for a DataFrame input.
        ``NaN`` when the sample never drew down.

    See Also
    --------
    sharpe : Volatility-based risk-adjusted return.
    pain_ratio : Like Calmar but uses ``pain_index`` instead of max drawdown.
    """
    df = _to_df(returns)
    ppy = _periods(periods_per_year)
    ann_ret = (1.0 + df).prod() ** (ppy / max(len(df), 1)) - 1.0
    mdd = max_drawdown(df).abs()
    out = ann_ret / mdd
    out = out.replace([np.inf, -np.inf], np.nan)
    return _collapse(out, returns)

omega

omega(returns: Series, *, target: float = ...) -> float
omega(
    returns: DataFrame, *, target: float = ...
) -> pd.Series
omega(
    returns: Series | DataFrame, *, target: float = 0.0
) -> float | pd.Series

Omega ratio at target threshold.

Ratio of cumulative gains above target to cumulative losses below it: sum(max(r - target, 0)) / sum(max(target - r, 0)). Captures the full distribution rather than just its first two moments — values above 1 mean the upside outweighs the downside.

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series or DataFrame.

required
target float

Threshold separating gains from losses. Default 0.0.

0.0

Returns:

Type Description
float or Series

Non-negative. Scalar for a Series input, Series for a DataFrame input. NaN if the sample has no losses below target.

See Also

profit_factor : Same shape with target=0, dollar-weighted. sortino : Mean / downside-vol form of the same intuition.

Source code in python/fundcloud/metrics/core.py
def omega(returns: pd.Series | pd.DataFrame, *, target: float = 0.0) -> float | pd.Series:
    """Omega ratio at ``target`` threshold.

    Ratio of cumulative gains above ``target`` to cumulative losses
    below it: ``sum(max(r - target, 0)) / sum(max(target - r, 0))``.
    Captures the full distribution rather than just its first two
    moments — values above 1 mean the upside outweighs the downside.

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` or ``DataFrame``.
    target
        Threshold separating gains from losses. Default ``0.0``.

    Returns
    -------
    float or pd.Series
        Non-negative. Scalar for a Series input, Series for a
        DataFrame input. ``NaN`` if the sample has no losses below
        ``target``.

    See Also
    --------
    profit_factor : Same shape with ``target=0``, dollar-weighted.
    sortino : Mean / downside-vol form of the same intuition.
    """
    df = _to_df(returns)
    diff = df - target
    gains = diff.clip(lower=0.0).sum()
    losses = -diff.clip(upper=0.0).sum()
    out = gains / losses
    out = out.replace([np.inf, -np.inf], np.nan)
    return _collapse(out, returns)

drawdown_series

drawdown_series(returns: Series) -> pd.Series
drawdown_series(returns: DataFrame) -> pd.DataFrame
drawdown_series(
    returns: Series | DataFrame,
) -> pd.Series | pd.DataFrame

Drawdown at each timestamp: wealth / running_max - 1.

Always ≤ 0. The running maximum is taken on cumulative wealth ((1 + r).cumprod()), so the series tracks how far below the most recent peak the strategy is on each bar.

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series (single strategy) or DataFrame (panel of strategies as columns).

required

Returns:

Type Description
Series or DataFrame

Same shape as returns. Values are in [-1, 0]; 0 means the strategy is at a new high.

See Also

max_drawdown : Scalar minimum of this series. ulcer_index : RMS of drawdown magnitudes. pain_index : Mean of drawdown magnitudes.

Source code in python/fundcloud/metrics/core.py
def drawdown_series(returns: pd.Series | pd.DataFrame) -> pd.Series | pd.DataFrame:
    """Drawdown at each timestamp: ``wealth / running_max - 1``.

    Always ≤ 0. The running maximum is taken on cumulative wealth
    (``(1 + r).cumprod()``), so the series tracks how far below the
    most recent peak the strategy is on each bar.

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` (single strategy) or
        ``DataFrame`` (panel of strategies as columns).

    Returns
    -------
    pd.Series or pd.DataFrame
        Same shape as ``returns``. Values are in ``[-1, 0]``; ``0``
        means the strategy is at a new high.

    See Also
    --------
    max_drawdown : Scalar minimum of this series.
    ulcer_index : RMS of drawdown magnitudes.
    pain_index : Mean of drawdown magnitudes.
    """
    wealth = (1.0 + returns).cumprod()
    peak = wealth.cummax()
    return wealth / peak - 1.0

max_drawdown

max_drawdown(returns: Series) -> float
max_drawdown(returns: DataFrame) -> pd.Series
max_drawdown(
    returns: Series | DataFrame,
) -> float | pd.Series

Largest peak-to-trough loss over the sample (negative number).

Equivalent to drawdown_series(returns).min().

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series (single strategy) or DataFrame (panel of strategies as columns).

required

Returns:

Type Description
float or Series

Negative number in [-1, 0]. Scalar for a Series input, Series indexed by column for a DataFrame input.

See Also

drawdown_series : Per-bar drawdown timeseries. calmar : CAGR / |max_drawdown|.

Source code in python/fundcloud/metrics/core.py
def max_drawdown(returns: pd.Series | pd.DataFrame) -> float | pd.Series:
    """Largest peak-to-trough loss over the sample (negative number).

    Equivalent to ``drawdown_series(returns).min()``.

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` (single strategy) or
        ``DataFrame`` (panel of strategies as columns).

    Returns
    -------
    float or pd.Series
        Negative number in ``[-1, 0]``. Scalar for a Series input,
        Series indexed by column for a DataFrame input.

    See Also
    --------
    drawdown_series : Per-bar drawdown timeseries.
    calmar : ``CAGR / |max_drawdown|``.
    """
    dd = drawdown_series(_to_df(returns))
    out = dd.min()
    return _collapse(out, returns)

ulcer_index

ulcer_index(returns: Series) -> float
ulcer_index(returns: DataFrame) -> pd.Series
ulcer_index(
    returns: Series | DataFrame,
) -> float | pd.Series

Ulcer Index: RMS of drawdowns, in percent.

Captures both the depth and duration of drawdowns — a fast brief drawdown scores lower than a shallow but persistent one. Scale is percent (multiplied by 100) to match Peter Martin's original convention.

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series or DataFrame.

required

Returns:

Type Description
float or Series

Non-negative. Scalar for a Series input, Series for a DataFrame input.

See Also

pain_index : Mean of drawdown magnitudes (linear, not RMS). ulcer_performance_index : Martin ratio — return per unit of ulcer.

Source code in python/fundcloud/metrics/core.py
def ulcer_index(returns: pd.Series | pd.DataFrame) -> float | pd.Series:
    """Ulcer Index: RMS of drawdowns, in percent.

    Captures both the depth and duration of drawdowns — a fast brief
    drawdown scores lower than a shallow but persistent one. Scale is
    percent (multiplied by 100) to match Peter Martin's original
    convention.

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` or ``DataFrame``.

    Returns
    -------
    float or pd.Series
        Non-negative. Scalar for a Series input, Series for a
        DataFrame input.

    See Also
    --------
    pain_index : Mean of drawdown magnitudes (linear, not RMS).
    ulcer_performance_index : Martin ratio — return per unit of ulcer.
    """
    dd_pct = drawdown_series(_to_df(returns)) * 100.0
    out = np.sqrt((dd_pct**2).mean())
    return _collapse(out, returns)

cvar

cvar(returns: Series, *, alpha: float = ...) -> float
cvar(
    returns: DataFrame, *, alpha: float = ...
) -> pd.Series
cvar(
    returns: Series | DataFrame, *, alpha: float = 0.95
) -> float | pd.Series

Conditional Value-at-Risk (Expected Shortfall) at confidence alpha.

Average return in the left tail — i.e. the mean of all returns at or below the (1 - alpha) quantile. More conservative than :func:value_at_risk: where VaR asks "how bad is the tail threshold?", CVaR asks "how bad is the average outcome inside the tail?".

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series or DataFrame.

required
alpha float

Confidence level in (0, 1). Default 0.95. Raises ValueError outside this range.

0.95

Returns:

Type Description
float or Series

Negative number. Scalar for a Series input, Series for a DataFrame input. NaN when no observation lies in the tail.

Raises:

Type Description
ValueError

If alpha is not strictly in (0, 1).

See Also

value_at_risk : Tail-quantile threshold (less conservative).

Source code in python/fundcloud/metrics/core.py
def cvar(returns: pd.Series | pd.DataFrame, *, alpha: float = 0.95) -> float | pd.Series:
    """Conditional Value-at-Risk (Expected Shortfall) at confidence ``alpha``.

    Average return in the left tail — i.e. the mean of all returns at
    or below the ``(1 - alpha)`` quantile. More conservative than
    :func:`value_at_risk`: where VaR asks "how bad is the tail
    *threshold*?", CVaR asks "how bad is the *average* outcome inside
    the tail?".

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` or ``DataFrame``.
    alpha
        Confidence level in ``(0, 1)``. Default ``0.95``. Raises
        ``ValueError`` outside this range.

    Returns
    -------
    float or pd.Series
        Negative number. Scalar for a Series input, Series for a
        DataFrame input. ``NaN`` when no observation lies in the tail.

    Raises
    ------
    ValueError
        If ``alpha`` is not strictly in ``(0, 1)``.

    See Also
    --------
    value_at_risk : Tail-quantile threshold (less conservative).
    """
    if not 0.0 < alpha < 1.0:
        raise ValueError("alpha must be in (0, 1)")
    df = _to_df(returns)
    q = df.quantile(1.0 - alpha)
    out = pd.Series(index=df.columns, dtype=float)
    for c in df.columns:
        mask = df[c] <= q[c]
        out[c] = df.loc[mask, c].mean() if mask.any() else np.nan
    return _collapse(out, returns)

value_at_risk

value_at_risk(
    returns: Series, *, alpha: float = ...
) -> float
value_at_risk(
    returns: DataFrame, *, alpha: float = ...
) -> pd.Series
value_at_risk(
    returns: Series | DataFrame, *, alpha: float = 0.95
) -> float | pd.Series

Historical Value-at-Risk at confidence alpha.

The empirical (1 - alpha) quantile of the returns distribution. Sign convention: a loss is a negative number, so VaR is typically negative. Higher alpha means deeper into the tail.

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series or DataFrame.

required
alpha float

Confidence level in (0, 1). Default 0.95 (the 5 % worst-case return). Raises ValueError outside this range.

0.95

Returns:

Type Description
float or Series

Negative number (or zero for non-negative-return samples). Scalar for a Series input, Series for a DataFrame input.

Raises:

Type Description
ValueError

If alpha is not strictly in (0, 1).

See Also

cvar : Mean of returns below this quantile (Expected Shortfall).

Source code in python/fundcloud/metrics/core.py
def value_at_risk(returns: pd.Series | pd.DataFrame, *, alpha: float = 0.95) -> float | pd.Series:
    """Historical Value-at-Risk at confidence ``alpha``.

    The empirical ``(1 - alpha)`` quantile of the returns
    distribution. Sign convention: a **loss** is a negative number,
    so VaR is typically negative. Higher ``alpha`` means deeper into
    the tail.

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` or ``DataFrame``.
    alpha
        Confidence level in ``(0, 1)``. Default ``0.95`` (the 5 %
        worst-case return). Raises ``ValueError`` outside this range.

    Returns
    -------
    float or pd.Series
        Negative number (or zero for non-negative-return samples).
        Scalar for a Series input, Series for a DataFrame input.

    Raises
    ------
    ValueError
        If ``alpha`` is not strictly in ``(0, 1)``.

    See Also
    --------
    cvar : Mean of returns below this quantile (Expected Shortfall).
    """
    if not 0.0 < alpha < 1.0:
        raise ValueError("alpha must be in (0, 1)")
    df = _to_df(returns)
    out = df.quantile(1.0 - alpha)
    return _collapse(out, returns)

returns_stats

returns_stats(
    returns: Series | DataFrame,
    *,
    risk_free: float | None = None,
    periods_per_year: int | None = None,
    cvar_alpha: float = 0.95,
) -> pd.DataFrame

Bundle of the common metrics into a single, scannable summary table.

Rows are metrics (periods, total_return, cagr, ann_volatility, sharpe, sortino, calmar, max_drawdown, ulcer_index, cvar, omega); columns are strategies. Backs the :meth:fundcloud.accessors.DataFrameAccessor.summary accessor and :meth:Portfolio.summary.

Parameters:

Name Type Description Default
returns Series | DataFrame

Per-period returns. Series (single strategy) or DataFrame (panel of strategies as columns).

required
risk_free float | None

Annualised risk-free rate; forwarded to :func:sharpe. None falls back to the global config.

None
periods_per_year int | None

Annualisation factor; forwarded to the annualised metrics. None falls back to the global config.

None
cvar_alpha float

Confidence level for the CVaR row. Default 0.95.

0.95

Returns:

Type Description
DataFrame

Metric-by-strategy table; always a DataFrame, even for a single-strategy Series input (the column is named after the Series).

See Also

fundcloud.metrics.metrics : Full ~55-row analogue with optional benchmark columns.

Source code in python/fundcloud/metrics/core.py
def returns_stats(
    returns: pd.Series | pd.DataFrame,
    *,
    risk_free: float | None = None,
    periods_per_year: int | None = None,
    cvar_alpha: float = 0.95,
) -> pd.DataFrame:
    """Bundle of the common metrics into a single, scannable summary table.

    Rows are metrics (``periods``, ``total_return``, ``cagr``,
    ``ann_volatility``, ``sharpe``, ``sortino``, ``calmar``,
    ``max_drawdown``, ``ulcer_index``, ``cvar``, ``omega``); columns
    are strategies. Backs the :meth:`fundcloud.accessors.DataFrameAccessor.summary`
    accessor and :meth:`Portfolio.summary`.

    Parameters
    ----------
    returns
        Per-period returns. ``Series`` (single strategy) or
        ``DataFrame`` (panel of strategies as columns).
    risk_free
        Annualised risk-free rate; forwarded to :func:`sharpe`.
        ``None`` falls back to the global config.
    periods_per_year
        Annualisation factor; forwarded to the annualised metrics.
        ``None`` falls back to the global config.
    cvar_alpha
        Confidence level for the CVaR row. Default ``0.95``.

    Returns
    -------
    pd.DataFrame
        Metric-by-strategy table; always a DataFrame, even for a
        single-strategy ``Series`` input (the column is named after the
        Series).

    See Also
    --------
    fundcloud.metrics.metrics : Full ~55-row analogue with optional benchmark columns.
    """
    df = _to_df(returns)
    ppy = _periods(periods_per_year)
    n = len(df)
    total_return = (1.0 + df).prod() - 1.0
    cagr = (1.0 + df).prod() ** (ppy / max(n, 1)) - 1.0
    ann_vol = df.std(ddof=1) * np.sqrt(ppy)
    rows = {
        "periods": pd.Series(n, index=df.columns),
        "total_return": total_return,
        "cagr": cagr,
        "ann_volatility": ann_vol,
        "sharpe": sharpe(df, risk_free=risk_free, periods_per_year=ppy),
        "sortino": sortino(df, periods_per_year=ppy),
        "calmar": calmar(df, periods_per_year=ppy),
        "max_drawdown": max_drawdown(df),
        "ulcer_index": ulcer_index(df),
        "cvar": cvar(df, alpha=cvar_alpha),
        "omega": omega(df),
    }
    return pd.DataFrame(rows).T

batch_sharpe

batch_sharpe(
    strategies: Mapping[str, Series | DataFrame],
    *,
    risk_free: float | None = None,
    periods_per_year: int | None = None,
) -> pd.Series
Source code in python/fundcloud/metrics/batch.py
def batch_sharpe(
    strategies: Mapping[str, pd.Series | pd.DataFrame],
    *,
    risk_free: float | None = None,
    periods_per_year: int | None = None,
) -> pd.Series:
    rows = {
        name: _core.sharpe(
            _reduce_returns(r),
            risk_free=risk_free,
            periods_per_year=periods_per_year,
        )
        for name, r in strategies.items()
    }
    return pd.Series(rows, name="sharpe", dtype=float)

batch_sortino

batch_sortino(
    strategies: Mapping[str, Series | DataFrame],
    *,
    target: float = 0.0,
    periods_per_year: int | None = None,
) -> pd.Series
Source code in python/fundcloud/metrics/batch.py
def batch_sortino(
    strategies: Mapping[str, pd.Series | pd.DataFrame],
    *,
    target: float = 0.0,
    periods_per_year: int | None = None,
) -> pd.Series:
    rows = {
        name: _core.sortino(_reduce_returns(r), target=target, periods_per_year=periods_per_year)
        for name, r in strategies.items()
    }
    return pd.Series(rows, name="sortino", dtype=float)

batch_max_drawdown

batch_max_drawdown(
    strategies: Mapping[str, Series | DataFrame],
) -> pd.Series
Source code in python/fundcloud/metrics/batch.py
def batch_max_drawdown(
    strategies: Mapping[str, pd.Series | pd.DataFrame],
) -> pd.Series:
    rows = {name: _core.max_drawdown(_reduce_returns(r)) for name, r in strategies.items()}
    return pd.Series(rows, name="max_drawdown", dtype=float)

batch_cvar

batch_cvar(
    strategies: Mapping[str, Series | DataFrame],
    *,
    alpha: float = 0.95,
) -> pd.Series
Source code in python/fundcloud/metrics/batch.py
def batch_cvar(
    strategies: Mapping[str, pd.Series | pd.DataFrame],
    *,
    alpha: float = 0.95,
) -> pd.Series:
    rows = {name: _core.cvar(_reduce_returns(r), alpha=alpha) for name, r in strategies.items()}
    return pd.Series(rows, name="cvar", dtype=float)

batch_summary

batch_summary(
    strategies: Mapping[str, Series | DataFrame],
    *,
    risk_free: float | None = None,
    periods_per_year: int | None = None,
    cvar_alpha: float = 0.95,
) -> pd.DataFrame

One row per strategy, standard metrics as columns.

Source code in python/fundcloud/metrics/batch.py
def batch_summary(
    strategies: Mapping[str, pd.Series | pd.DataFrame],
    *,
    risk_free: float | None = None,
    periods_per_year: int | None = None,
    cvar_alpha: float = 0.95,
) -> pd.DataFrame:
    """One row per strategy, standard metrics as columns."""
    if not strategies:
        return pd.DataFrame()
    rows = {}
    for name, r in strategies.items():
        s = _reduce_returns(r)
        rows[name] = _core.returns_stats(
            s,
            risk_free=risk_free,
            periods_per_year=periods_per_year,
            cvar_alpha=cvar_alpha,
        ).iloc[:, 0]
    out = pd.DataFrame(rows).T
    # Enforce float dtype; sklearn/skfolio sometimes hands us object columns.
    return out.apply(pd.to_numeric, errors="coerce").replace([np.inf, -np.inf], np.nan)