portfolio-analytics
Portfolio-level performance measurement including return metrics, risk metrics, risk-adjusted ratios, rolling analysis, and HTML reports
Best use case
portfolio-analytics is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Portfolio-level performance measurement including return metrics, risk metrics, risk-adjusted ratios, rolling analysis, and HTML reports
Teams using portfolio-analytics should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/portfolio-analytics/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How portfolio-analytics Compares
| Feature / Agent | portfolio-analytics | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Portfolio-level performance measurement including return metrics, risk metrics, risk-adjusted ratios, rolling analysis, and HTML reports
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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
```bash
uv pip install pandas numpy quantstats
```
## Input Format
All analytics start from an **equity curve** — a time-indexed Series of portfolio values:
```python
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
```python
total_return = (equity.iloc[-1] / equity.iloc[0]) - 1
```
### CAGR (Compound Annual Growth Rate)
```python
days = (equity.index[-1] - equity.index[0]).days
cagr = (equity.iloc[-1] / equity.iloc[0]) ** (365.25 / days) - 1
```
### Daily Mean Return
```python
daily_mean = returns.mean()
annualized_mean = daily_mean * 252 # trading days
```
### Cumulative Returns
```python
cumulative = (1 + returns).cumprod() - 1
```
## Risk Metrics
### Annualized Volatility
```python
daily_vol = returns.std()
annual_vol = daily_vol * np.sqrt(252)
```
### Value at Risk (VaR)
Historical VaR at a given confidence level:
```python
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)
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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:
```python
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
```python
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
```python
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:
```python
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
```python
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 drawdownsRelated Skills
yield-analysis
DeFi yield evaluation including fee APR, real vs nominal yield, net APY after costs, and yield sustainability analysis
yellowstone-grpc
Real-time Solana transaction and account streaming via Yellowstone gRPC (Geyser plugin)
whale-tracking
Large wallet monitoring, accumulation and distribution detection, and smart money signal generation for Solana tokens
wash-sale-detection
Wash sale detection under 2025 US crypto rules with 61-day window monitoring, disallowed loss tracking, and safe re-entry countdown
wallet-profiling
Behavioral classification, performance analysis, and trading style detection for Solana wallets
walk-forward-validation
Walk-forward validation framework for trading strategies and ML models with time-series-aware splits, overfit detection, and regime-aware validation
volatility-modeling
Volatility estimation, forecasting, and regime classification using GARCH, EWMA, realized volatility, and volatility cones
vectorbt
High-performance vectorized backtesting with parameter optimization, portfolio simulation, and rich performance metrics
trading-visualization
Professional trading charts including candlesticks, equity curves, drawdowns, correlation heatmaps, and return distributions
trade-journal
Structured trade logging, performance review, behavioral pattern detection, and strategy attribution for systematic improvement
trade-accounting
Double-entry bookkeeping for trading operations with ledger management, P&L statements, balance sheets, and cash flow reporting
token-holder-analysis
Token holder distribution, concentration metrics, insider detection, and supply analysis for Solana tokens