VaR icon indicating copy to clipboard operation
VaR copied to clipboard

Passed in daily PNL while cumulative PNL is expected.

Open julius-datajunkie opened this issue 4 months ago • 3 comments

def compute_cdar(pnl: array_like, var: array_like) -> np.ndarray:
    """Compute the Drawdown of a portfolio

    Parameters
    ----------
    pnl : array_like
        Profit and Loss values.

    Returns
    -------
    np.ndarray
        Drawdowns
    """
    # Compute the drawdowns
    running_max = np.maximum.accumulate(pnl)
    drawdowns = running_max - pnl

    drawdown_values = np.zeros_like(var)

    for i, item in enumerate(var):
        drawdown_values[i] = np.nanmean(drawdowns[drawdowns > item])

    drawdown_values = np.nan_to_num(drawdown_values)

    return drawdown_values

The above implementation of compute_cdar expects pnl argument to the cumulative pnl, while the the pnl passed in is daily pnl.

Reference: https://scikit-portfolio.github.io/scikit-portfolio/efficient_cdar/

julius-datajunkie avatar Sep 02 '25 03:09 julius-datajunkie

Sorry for the late reply and thank you very much for the great catch! I will fix this on Monday. You can fix this too if you want and then generate a PR.

ibaris avatar Sep 06 '25 05:09 ibaris

How about this:

import numpy as np
from typing import Union, Sequence

def compute_drawdown(
    pnl: array_like,
    var: array_like,
    axis: int = 1,
    is_cumulative: bool = False
) -> np.ndarray:
    """
    Compute the Conditional Drawdown at Risk (CDaR) of a portfolio.

    Parameters
    ----------
    pnl : array_like
        Daily or cumulative Profit and Loss values (NAV time series).
        Shape: (n_portfolios, n_timesteps)

    var : array_like
        VaR threshold values (e.g., percentiles of drawdowns).
        Shape: (n_portfolios, n_quantiles)

    axis : int, default = 0
        Axis along which time progresses.

    is_cumulative : bool, default = False
        If False, the function will accumulate the PnL using np.cumsum along the given axis.

    Returns
    -------
    np.ndarray
        CDaR values, shape (n_portfolios, n_quantiles)
    """
    pnl = np.asarray(pnl)
    var = np.asarray(var)

    if not is_cumulative:
        pnl = np.cumsum(pnl, axis=axis)

    running_max = np.maximum.accumulate(pnl, axis=axis)
    drawdowns = running_max - pnl  # drawdowns are positive

    # Compute tail drawdowns that exceed the threshold in `var`
    masks = [drawdowns > var[:, [i]] for i in range(var.shape[-1])]
    tails = np.where(masks, drawdowns, np.nan)
    dd_values = np.nanmean(tails, axis=-1)

    return -dd_values.T  # Flip sign to make CDaR a risk measure (negative expected drawdown)

ibaris avatar Sep 06 '25 06:09 ibaris

I started a new branch where I optimise most of the code, which will lead to a major release. I will resolve this bug in this branch, since the function has more issues then this.

ibaris avatar Sep 08 '25 06:09 ibaris