← Back to list
💰

portfolio-analytics Checked

Portfolio-level performance measurement including return metrics, risk metrics, risk-adjusted rat…
💰 Finance & Trading skills By agiprolabs Version v1.0.0 Added 2026-09-20 Updated 2026-09-20 Source ↗
8.1Overall rating
2Views
4Downloads
366Stars

Installation

🤖 Install via AI

Copy the prompt below and send it to your AI assistant (e.g. Claude Code) — it will follow the instructions and install automatically.

Install the "portfolio-analytics" skill by following the instructions at https://skill123.me/install/portfolio-analytics.
⌨️ Command line install

Run in your terminal — downloads and installs to ~/.claude/skills/.

curl -fsSL https://skill123.me/install/portfolio-analytics.sh | bash
📦 Download ZIP

Download the zip and extract it into your skills directory (e.g. ~/.claude/skills/), then restart your session.

⬇ Download v1.0.0 · 21 KB
📊 Static rating
8.1/10
Trigger
6.5
Structure
8.0
Workflow
7.6
Content
9.0
Engineering
10.0
Security
9.0

Six-dimension rubric with reviewer rationale.

View scorecard ↓
🛡 Dynamic test
—

Not live-tested yet — this skill is rated on static analysis only.

About this skill

Portfolio-level performance measurement including return metrics, risk metrics, risk-adjusted ratios, rolling analysis, and HTML reports

Portfolio Analytics

Compute portfolio-level performance metrics from equity curves and trade logs. Covers return metrics, risk metrics, risk-adjusted ratios, drawdown analysis, rolling windows, benchmark comparison, trade-level statistics, and automated HTML report generation via quantstats.

Documentation

Portfolio-level performance measurement including return metrics, risk metrics, risk-adjusted ratios, rolling analysis, and HTML reports

Portfolio Analytics

Compute portfolio-level performance metrics from equity curves and trade logs. Covers return metrics, risk metrics, risk-adjusted ratios, drawdown analysis, rolling windows, benchmark comparison, trade-level statistics, and automated HTML report generation via quantstats.

When to Use This Skill

  • After backtesting a strategy (e.g., from vectorbt or strategy-framework)
  • Comparing multiple strategies or parameter sets side-by-side
  • Generating investor-ready performance reports
  • Evaluating live trading performance against benchmarks
  • Assessing risk-adjusted returns for portfolio allocation decisions

Prerequisites


uv pip install pandas numpy quantstats

Input Format

All analytics start from an **equity curve** — a time-indexed Series of portfolio values:


import pandas as pd
import numpy as np

# From a backtest
equity = pd.Series(
    [10000, 10150, 10080, 10320, 10510, 10440, 10680],
    index=pd.date_range("2025-01-01", periods=7, freq="D"),
    name="strategy_equity"
)

# Convert to returns
returns = equity.pct_change().dropna()

Return Metrics

### Total Return


total_return = (equity.iloc[-1] / equity.iloc[0]) - 1

### CAGR (Compound Annual Growth Rate)


days = (equity.index[-1] - equity.index[0]).days
cagr = (equity.iloc[-1] / equity.iloc[0]) ** (365.25 / days) - 1

### Daily Mean Return


daily_mean = returns.mean()
annualized_mean = daily_mean * 252  # trading days

### Cumulative Returns


cumulative = (1 + returns).cumprod() - 1

Risk Metrics

### Annualized Volatility


daily_vol = returns.std()
annual_vol = daily_vol * np.sqrt(252)

### Value at Risk (VaR)

Historical VaR at a given confidence level:


def historical_var(returns: pd.Series, confidence: float = 0.95) -> float:
    """Compute historical VaR.

    Args:
        returns: Daily return series.
        confidence: Confidence level (e.g., 0.95 for 95%).

    Returns:
        VaR as a positive number representing potential loss.
    """
    return -np.percentile(returns, (1 - confidence) * 100)

### Conditional VaR (CVaR / Expected Shortfall)


def historical_cvar(returns: pd.Series, confidence: float = 0.95) -> float:
    """Mean of returns below the VaR threshold."""
    var = historical_var(returns, confidence)
    return -returns[returns <= -var].mean()

### Maximum Drawdown


def max_drawdown(equity: pd.Series) -> float:
    """Maximum peak-to-trough decline."""
    peak = equity.cummax()
    drawdown = (equity - peak) / peak
    return drawdown.min()  # negative number

def drawdown_series(equity: pd.Series) -> pd.Series:
    """Full drawdown time series."""
    peak = equity.cummax()
    return (equity - peak) / peak

### Time Underwater

View full documentation

def time_underwater(equity: pd.Series) -> int:
    """Longest consecutive period below previous peak (in days)."""
    dd = drawdown_series(equity)
    is_underwater = dd < 0
    groups = (~is_underwater).cumsum()
    underwater_periods = is_underwater.groupby(groups).sum()
    return int(underwater_periods.max()) if len(underwater_periods) > 0 else 0

Risk-Adjusted Ratios

### Sharpe Ratio


def sharpe_ratio(
    returns: pd.Series,
    rf: float = 0.0,
    periods_per_year: int = 252
) -> float:
    """Annualized Sharpe ratio.

    Args:
        returns: Period returns.
        rf: Risk-free rate per period.
        periods_per_year: Annualization factor.

    Returns:
        Annualized Sharpe ratio.
    """
    excess = returns - rf
    if excess.std() == 0:
        return 0.0
    return (excess.mean() / excess.std()) * np.sqrt(periods_per_year)

### Sortino Ratio


def sortino_ratio(
    returns: pd.Series,
    rf: float = 0.0,
    periods_per_year: int = 252
) -> float:
    """Annualized Sortino ratio (penalizes only downside vol)."""
    excess = returns - rf
    downside = excess[excess < 0]
    if len(downside) == 0 or downside.std() == 0:
        return float("inf") if excess.mean() > 0 else 0.0
    return (excess.mean() / downside.std()) * np.sqrt(periods_per_year)

### Calmar Ratio


def calmar_ratio(equity: pd.Series, periods_per_year: int = 252) -> float:
    """CAGR divided by max drawdown (absolute value)."""
    returns = equity.pct_change().dropna()
    days = (equity.index[-1] - equity.index[0]).days
    cagr = (equity.iloc[-1] / equity.iloc[0]) ** (365.25 / days) - 1
    mdd = abs(max_drawdown(equity))
    if mdd == 0:
        return float("inf") if cagr > 0 else 0.0
    return cagr / mdd

### Omega Ratio


def omega_ratio(
    returns: pd.Series,
    threshold: float = 0.0
) -> float:
    """Ratio of probability-weighted gains to losses."""
    excess = returns - threshold
    gains = excess[excess > 0].sum()
    losses = abs(excess[excess <= 0].sum())
    if losses == 0:
        return float("inf") if gains > 0 else 1.0
    return gains / losses

### Information Ratio


def information_ratio(
    returns: pd.Series,
    benchmark_returns: pd.Series,
    periods_per_year: int = 252
) -> float:
    """Excess return per unit of tracking error."""
    active = returns - benchmark_returns
    if active.std() == 0:
        return 0.0
    return (active.mean() / active.std()) * np.sqrt(periods_per_year)

Rolling Analysis

### Rolling Sharpe


def rolling_sharpe(
    returns: pd.Series,
    window: int = 63,
    rf: float = 0.0,
    periods_per_year: int = 252
) -> pd.Series:
    """Rolling annualized Sharpe ratio."""
    excess = returns - rf
    roll_mean = excess.rolling(window).mean()
    roll_std = excess.rolling(window).std()
    return (roll_mean / roll_std) * np.sqrt(periods_per_year)

### Rolling Max Drawdown


def rolling_max_drawdown(equity: pd.Series, window: int = 252) -> pd.Series:
    """Rolling max drawdown over a fixed window."""
    result = pd.Series(index=equity.index, dtype=float)
    for i in range(window, len(equity)):
        window_eq = equity.iloc[i - window:i + 1]
        peak = window_eq.cummax()
        dd = (window_eq - peak) / peak
        result.iloc[i] = dd.min()
    return result

Trade-Level Analysis

When you have individual trade records:


def trade_statistics(pnl: pd.Series) -> dict:
    """Compute trade-level statistics from a series of trade PnL values.

    Args:
        pnl: Series where each value is the PnL of one trade.

    Returns:
        Dictionary of trade statistics.
    """
    wins = pnl[pnl > 0]
    losses = pnl[pnl < 0]
    total = len(pnl)

    win_rate = len(wins) / total if total > 0 else 0.0
    avg_win = wins.mean() if len(wins) > 0 else 0.0
    avg_loss = losses.mean() if len(losses) > 0 else 0.0
    largest_win = wins.max() if len(wins) > 0 else 0.0
    largest_loss = losses.min() if len(losses) > 0 else 0.0

    gross_profit = wins.sum() if len(wins) > 0 else 0.0
    gross_loss = abs(losses.sum()) if len(losses) > 0 else 0.0
    profit_factor = gross_profit / gross_loss if gross_loss > 0 else float("inf")

    expectancy = pnl.mean() if total > 0 else 0.0

    return {
        "total_trades": total,
        "win_rate": win_rate,
        "avg_win": avg_win,
        "avg_loss": avg_loss,
        "largest_win": largest_win,
        "largest_loss": largest_loss,
        "profit_factor": profit_factor,
        "expectancy": expectancy,
        "gross_profit": gross_profit,
        "gross_loss": gross_loss,
    }

Monthly / Yearly Return Tables


def monthly_returns_table(returns: pd.Series) -> pd.DataFrame:
    """Pivot returns into a month-by-year table.

    Returns:
        DataFrame with years as rows, months (1-12) as columns,
        and an Annual column.
    """
    monthly = returns.resample("ME").apply(lambda x: (1 + x).prod() - 1)
    table = monthly.groupby([monthly.index.year, monthly.index.month]).first()
    table = table.unstack(level=1)

Score breakdown

Trigger
6.5
Description enumerates metric families well (good keyword coverage); a When-to-Use section exists in the body but not in the description; no When-Not boundary, no trigger-phrase variants.
Structure
8.0
References and scripts correctly separated; SKILL.md at 413 lines is acceptable but duplicates many full function definitions also present in scripts; Files table annotates contents but not consultation timing.
Workflow
7.6
Library/tool-wrapper hybrid: concrete executable paths (pandas code, quantstats one-liners, vectorbt integration) rather than pure description; scripts have CLI modes, demo data, and zero-division guards; ranking output in compare_strategies provides a soft verification loop.
Content
9.0
Clean imperative style; every snippet is copy-runnable; consistent formatting.
Engineering
10.0
Frontmatter valid; shebangs present; dependencies explicitly declared (pandas, numpy, quantstats); all referenced paths exist.
Security
9.0
6.0 manifest: scripts read local CSV and write only stdout/report files locally; no network egress, no credential access, no persistence. Capabilities match declared scope (HTML report generation is declared). No injection text or keys.
Comprehensive portfolio performance-measurement skill: full metric library (Sharpe/Sortino/Calmar/Omega/IR, VaR/CVaR, drawdown, rolling) with two runnable scripts (single-portfolio analysis with --demo/--csv, multi-strategy comparison) and a quantstats guide for HTML tearsheets. Deductions: description lacks When-Not; 413-line SKILL.md carries many inline function bodies that belong in references; no explicit load guidance beyond the Files table.