agiprolabs/claude-trading-skills

portfolio-analytics

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

First seen Mar 21, 2026

Installation

$ npx skills add agiprolabs/claude-trading-skills --skill portfolio-analytics

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from agiprolabs/claude-trading-skills · top by installs.

npx skills add agiprolabs/claude-trading-skills

Browse all from agiprolabs/claude-trading-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 350
License LICENSE.md
Default branch main
Open issues 3
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 11,502 B
  • docs SUMMARY.md 3,731 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 372 installs

SKILL.md

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

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)
    table.columns = [
        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    ]
    # Annual column
    annual = returns.resample("YE").apply(lambda x: (1 + x).prod() - 1)
    table["Annual"] = annual.values[:len(table)]
    return table

Benchmark Comparison

def benchmark_comparison(
    strategy_returns: pd.Series,
    benchmark_returns: pd.Series,
    rf: float = 0.0
) -> dict:
    """Compare strategy to benchmark across key metrics."""
    strat_eq = (1 + strategy_returns).cumprod()
    bench_eq = (1 + benchmark_returns).cumprod()

    return {
        "strategy_total_return": strat_eq.iloc[-1] - 1,
        "benchmark_total_return": bench_eq.iloc[-1] - 1,
        "strategy_sharpe": sharpe_ratio(strategy_returns, rf),
        "benchmark_sharpe": sharpe_ratio(benchmark_returns, rf),
        "strategy_max_dd": max_drawdown(strat_eq),
        "benchmark_max_dd": max_drawdown(bench_eq),
        "information_ratio": information_ratio(strategy_returns, benchmark_returns),
        "correlation": strategy_returns.corr(benchmark_returns),
        "beta": (
            strategy_returns.cov(benchmark_returns)
            / benchmark_returns.var()
        ),
        "alpha": (
            strategy_returns.mean()
            - (strategy_returns.cov(benchmark_returns) / benchmark_returns.var())
            * benchmark_returns.mean()
        ) * 252,
    }

Quantstats HTML Reports

Generate investor-ready HTML reports with one function call:

import quantstats as qs

# From returns Series
qs.reports.html(
    returns,
    benchmark=benchmark_returns,  # optional
    output="report.html",
    title="My Strategy",
    rf=0.0,
    periods_per_year=252
)

# Individual metrics
print(f"Sharpe: {qs.stats.sharpe(returns):.2f}")
print(f"Sortino: {qs.stats.sortino(returns):.2f}")
print(f"Max DD: {qs.stats.max_drawdown(returns):.2%}")
print(f"Calmar: {qs.stats.calmar(returns):.2f}")

# Console tearsheet
qs.reports.full(returns)

See references/quantstats_guide.md for full API reference and customization.

Integration with Vectorbt

import vectorbt as vbt

# After running a vectorbt backtest
portfolio = vbt.Portfolio.from_signals(close, entries, exits, init_cash=10000)

# Extract equity curve
equity = portfolio.value()
returns = portfolio.returns()

# Use quantstats
qs.reports.html(returns, output="backtest_report.html")

Files

File Description
references/metrics_guide.md Formulas, derivations, annualization factors, interpretation benchmarks
references/quantstats_guide.md Quantstats library API, customization, integration patterns
scripts/analyze_portfolio.py Single portfolio analysis with all metrics, rolling stats, monthly table
scripts/compare_strategies.py Multi-strategy comparison with ranking by risk-adjusted metrics

Related Skills

  • vectorbt — Backtesting engine that produces equity curves for analysis
  • risk-management — Portfolio-level risk guardrails and allocation
  • position-sizing — Optimal position sizing using portfolio metrics
  • kelly-criterion — Optimal growth rate sizing from win rate and payoff
  • trading-visualization — Chart generation for equity curves and drawdowns