---
title: "Volatility, Sharpe ratio and maximum drawdown from a NAV series"
description: "Compute annualised volatility, Sharpe ratio and maximum drawdown from a fund's indexed NAV series in Python, and see why your figures differ from the factsheet."
canonical: https://fundfactsapi.com/blog/volatility-sharpe-max-drawdown-from-nav-series
published: 2026-09-05
updated: 2026-09-05
publisher: FundFacts API
---

# Volatility, Sharpe ratio and maximum drawdown from a NAV series

> Compute annualised volatility, Sharpe ratio and maximum drawdown from a fund's indexed NAV series in Python, and see why your figures differ from the factsheet.

Fund factsheets print a 3-year volatility, a Sharpe ratio and sometimes a maximum drawdown. When you compute the same numbers from the fund's own NAV series, you rarely land on exactly the same value. This post walks through the three calculations from first principles, shows the sampling and scaling choices that move the result, and explains why the fund house's figure and yours can legitimately disagree.

## The input: an indexed NAV series

FundFacts API returns the fund's performance chart as `data.indexedPerformance.points`, an array of `{ date, value }` where the value is the NAV rebased to a starting level (typically 100). Example values:

```json
{
  "indexedPerformance": {
    "points": [
      { "date": "2021-09-30", "value": 100.0 },
      { "date": "2021-10-29", "value": 103.2 },
      { "date": "2021-11-30", "value": 101.7 }
    ]
  }
}
```

Two things matter before you compute anything. First, the spacing: the points may be daily, weekly or monthly depending on what the fund house publishes, so detect the frequency rather than assume it. Second, the series is a total-return index in the share-class currency, with distributions reinvested, which is the right basis for risk metrics.

## Step 1: returns

Everything starts with periodic returns. For consecutive values V(t-1) and V(t):

```
simple return   r_t = V_t / V_(t-1) - 1
log return      l_t = ln(V_t / V_(t-1))
```

Simple returns are what factsheets use and what the formulas below assume. Log returns are convenient because they add across periods; the difference is negligible for daily data and small for monthly data, but be consistent.

## Step 2: annualised volatility

[Volatility](https://fundfactsapi.com/glossary/volatility) is the standard deviation of periodic returns, scaled to a yearly horizon:

```
sigma_annual = stdev(r) * sqrt(periods_per_year)
```

with `periods_per_year` = 252 for daily data (trading days), 52 for weekly and 12 for monthly. The square-root scaling assumes returns are independent from one period to the next. Real returns are not perfectly independent, which is one reason daily-based and monthly-based volatility for the same window do not agree.

Use the sample standard deviation (divide by n - 1). Most factsheets do, and with 36 monthly points the difference between n and n - 1 is about 1.4% of the figure: small, but visible at two decimals.

## Step 3: Sharpe ratio

The [Sharpe ratio](https://fundfactsapi.com/glossary/sharpe-ratio) is excess return per unit of volatility:

```
sharpe = (R_annual - Rf_annual) / sigma_annual
```

where `R_annual` is the annualised return over the window and `Rf_annual` the risk-free rate over the same window. A common alternative works at the period level:

```
sharpe = mean(r - rf_period) / stdev(r - rf_period) * sqrt(periods_per_year)
```

The two are not identical. The first uses the geometric (compound) annualised return, the second the arithmetic mean scaled up. For a fund with 15% volatility the geometric return is roughly 1.1 percentage points lower than the arithmetic one (about sigma squared over two), so the first formula gives a lower Sharpe. Neither is wrong; document which one you use.

### Choosing the risk-free rate

- Match the share-class currency: a EUR share class should use a euro short rate, a USD share class a Treasury bill or SOFR-based rate.
- Match the window: use the average rate over the three years, not today's rate.
- Some fund houses use zero. If a factsheet Sharpe equals annualised return divided by volatility exactly, that is what happened.

## Step 4: maximum drawdown

[Maximum drawdown](https://fundfactsapi.com/glossary/maximum-drawdown) is the largest peak-to-trough fall in the series:

```
peak_t     = max(V_0, ..., V_t)
drawdown_t = V_t / peak_t - 1
max_dd     = min(drawdown_t)      (a negative number, e.g. -0.23)
```

Unlike volatility and Sharpe, drawdown is not scaled by frequency, but it is still sensitive to it: a monthly series only sees month-end values and will miss an intra-month trough, so monthly maximum drawdown is never deeper than the daily one for the same window.

## Putting it together in Python

```python
import os
import numpy as np
import pandas as pd
import requests

res = requests.get(
    "https://fundfactsapi.com/api/v1/funds/IE00B4L5Y983",
    headers={"Authorization": f"Bearer {os.environ['FUNDFACTS_API_KEY']}"},
)
res.raise_for_status()
points = res.json()["data"]["indexedPerformance"]["points"]

nav = (
    pd.DataFrame(points)
    .assign(date=lambda d: pd.to_datetime(d["date"]))
    .set_index("date")["value"]
    .sort_index()
)

# detect sampling frequency from the median gap between points
gap_days = nav.index.to_series().diff().dt.days.median()
periods_per_year = 252 if gap_days <= 2 else 52 if gap_days <= 8 else 12

# trailing 3-year window
nav3 = nav[nav.index >= nav.index[-1] - pd.DateOffset(years=3)]
rets = nav3.pct_change().dropna()

years = (nav3.index[-1] - nav3.index[0]).days / 365.25
ann_return = (nav3.iloc[-1] / nav3.iloc[0]) ** (1 / years) - 1
ann_vol = rets.std(ddof=1) * np.sqrt(periods_per_year)

rf_annual = 0.02  # illustrative; use the short rate of the share-class currency
sharpe = (ann_return - rf_annual) / ann_vol

drawdown = nav3 / nav3.cummax() - 1
max_dd = drawdown.min()

print(f"freq={periods_per_year} vol={ann_vol:.2%} sharpe={sharpe:.2f} maxDD={max_dd:.2%}")
```

The frequency detection is the part people skip. Feeding monthly points into a `sqrt(252)` scaling inflates volatility by a factor of about 4.6, which is the single most common cause of a wildly wrong number.

## Why your numbers differ from `headlineMetrics.volatility3y`

The API also returns the fund house's own figures, `headlineMetrics.volatility3y`, `headlineMetrics.sharpe3y` and `metrics.maxDrawdown`, read from the factsheet. When yours disagree, one of these is usually the cause:

| Source of difference | Typical effect | How to check |
| --- | --- | --- |
| Window end date | Your series ends at the latest point; the factsheet figure ends at `dataAsOf`, often an earlier month-end | Compare `dataAsOf` with the last `date` in `points` |
| Sampling frequency | Daily and monthly returns over the same window give different estimates; monthly also misses intra-month drawdowns | Resample to month-ends: `nav.resample("ME").last()` (`"M"` on older pandas) |
| Currency | The factsheet may quote risk in the fund's base currency while you hold, or converted to, another currency | Check `keyFacts.currency` and the share-class name |
| Return type | Simple vs log returns, sample vs population standard deviation | Second-decimal differences |
| Risk-free rate | Zero vs cash rate, and which currency's rate | Sharpe only |
| Price basis | ETF factsheets sometimes use NAV, sometimes market price | ETFs only |

Window and frequency together explain most gaps. If a factsheet quotes 3-year volatility and you resample the indexed series to month-ends, take the 36 returns ending at `dataAsOf` and scale by `sqrt(12)`, you will usually land within a few tenths of a percentage point of the published number. Anything beyond that, look at currency.

> **Practical rule** Treat the fund house's figure as the one that is consistent within that house, and your own as the one that is consistent across houses and auditable. Store both, along with the window and frequency you used.

## When to compute and when to use the published figure

For a screener that ranks thousands of share classes, the published `volatility3y` is a reasonable first filter, with the caveat that methodologies differ between houses. For [quant research](https://fundfactsapi.com/use-cases/quant-research), client reporting or anything where two funds sit side by side, compute the metrics yourself from `indexedPerformance.points` so that every fund is treated identically. The [performance data guide](https://fundfactsapi.com/blog/fund-performance-data-explained) covers the return fields in the same response; the regulatory risk indicator (SRRI/SRI) is a separate, coarser measure and is covered in its own guide.

## About FundFacts API

FundFacts turns any fund or ETF ISIN into one dated factsheet (fees and KID costs, the 1–7 risk indicator, holdings, sector and country exposure, returns) read from the fund house's own documents: as a client-ready PDF under your firm's name for advisers and wealth managers, as JSON through its API for developers, or inside ChatGPT and Claude through its MCP server.

FundFacts works inside ChatGPT and Claude as a connector (a remote MCP server): ask about any fund or ETF by ISIN, ticker or name and the assistant answers from FundFacts' dated figures, fees, risk, holdings, exposure and returns, with fund cards, side-by-side comparisons, holdings overlap and portfolio look-through.

- Free API key, no card: https://fundfactsapi.com/signup
- Documentation: https://fundfactsapi.com/docs (OpenAPI: https://fundfactsapi.com/openapi.json)
- MCP server: https://fundfactsapi.com/api/mcp (set-up for every client: https://fundfactsapi.com/mcp-server)
- Pricing: https://fundfactsapi.com/pricing
- Site index for AI assistants: https://fundfactsapi.com/llms.txt
- Informational data, not investment advice.
