# Backtest a gold strategy with historical OHLC

Before you act on any price signal, you want to know whether it would have worked historically. The `/v1/prices/history` endpoint gives you daily OHLC going back years, which is enough to test most strategies without any paid data subscription.

This post walks through fetching the history, computing a 50/200-day moving-average crossover, and evaluating the result: the classic "golden cross / death cross" signal applied to the metal itself. For a primer on what drives gold's price at a macro level, see [What actually drives the gold price](/blog/drives-gold-price).

## Fetching historical data

The history endpoint takes a symbol, a date range, and an interval:

```
GET /v1/prices/history?symbol=XAU-USD-SPOT&from=2023-01-01&to=2026-06-01&interval=1d
```

The response is a `series` array of daily bars:

```json
{
  "series": [
    { "date": "2023-01-02", "open": 1824.30, "high": 1831.50, "low": 1820.10, "close": 1829.40 },
    ...
  ]
}
```

Here is the fetch code:

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

API_KEY = os.environ["GOLDPRICE_API_KEY"]
BASE = "https://api.goldprice.dev"

def fetch_ohlc(symbol: str, from_date: str, to_date: str) -> pd.DataFrame:
    resp = requests.get(
        f"{BASE}/v1/prices/history",
        params={"symbol": symbol, "from": from_date, "to": to_date, "interval": "1d"},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=15,
    )
    resp.raise_for_status()
    data = resp.json()
    df = pd.DataFrame(data["series"])
    df["date"] = pd.to_datetime(df["date"])
    df = df.set_index("date").sort_index()
    return df

df = fetch_ohlc("XAU-USD-SPOT", "2021-01-01", "2026-06-01")
print(df.tail())
```

You need `requests` and `pandas`. `pip install requests pandas` if you do not have them.

## Computing the crossover

A moving-average crossover strategy generates a buy signal when a short-window average crosses above a long-window average, and a sell signal when it crosses back below. We use 50 and 200 days, which are widely watched levels.

```python
df["ma50"] = df["close"].rolling(50).mean()
df["ma200"] = df["close"].rolling(200).mean()

# Signal: 1 when ma50 > ma200, 0 otherwise
df["signal"] = (df["ma50"] > df["ma200"]).astype(int)

# Position: difference of signal tells us when we cross
df["position"] = df["signal"].diff()

# +1.0 = buy (golden cross), -1.0 = sell (death cross)
crosses = df[df["position"] != 0][["close", "position"]].dropna()
print(crosses.head(10))
```

## Evaluating returns

We compare the strategy's return against simply holding gold for the same period.

```python
# Daily log returns
df["log_return"] = (df["close"] / df["close"].shift(1)).apply(lambda x: x if pd.notna(x) else 1)
import numpy as np
df["log_return"] = np.log(df["close"] / df["close"].shift(1))

# Strategy return: only in market when signal == 1
# Shift signal by 1 day to avoid look-ahead bias (we act on the next open)
df["strategy_return"] = df["log_return"] * df["signal"].shift(1)

# Cumulative returns
df["cum_market"] = df["log_return"].cumsum().apply(np.exp)
df["cum_strategy"] = df["strategy_return"].cumsum().apply(np.exp)

final = df[["cum_market", "cum_strategy"]].dropna().iloc[-1]
print(f"Buy-and-hold: {final['cum_market']:.2%}")
print(f"MA crossover: {final['cum_strategy']:.2%}")
```

This gives you cumulative return multipliers. A value of 1.45 means 45% total return over the period.

## Measuring risk

Raw return does not tell you much without knowing how volatile the path was. Annualized Sharpe ratio is a standard check:

```python
trading_days = 252
strat_returns = df["strategy_return"].dropna()

sharpe = (strat_returns.mean() / strat_returns.std()) * np.sqrt(trading_days)
print(f"Annualized Sharpe: {sharpe:.2f}")

# Max drawdown
cum = df["cum_strategy"].dropna()
rolling_max = cum.cummax()
drawdown = (cum - rolling_max) / rolling_max
max_dd = drawdown.min()
print(f"Max drawdown: {max_dd:.2%}")
```

A Sharpe above 1.0 is generally considered acceptable. Max drawdown tells you the worst peak-to-trough loss the strategy experienced. That is the number that tests whether you would have held.

## What to do with the results

A single backtest proves nothing. The 50/200 crossover is studied by enough people that any edge it had historically has likely been priced in. What this code gives you is a framework.

Swap in different window sizes (say, 20/50 or 10/100) and re-run. Add a filter: only take the long signal when the 200-day average is itself rising. Test on different sub-periods: how did the strategy behave during rate-hike cycles versus rate-cut cycles? Each question is a few lines of pandas.

The history endpoint supports any date range your plan covers. To get fresh data for a daily cron job:

```python
from datetime import date, timedelta

today = date.today().isoformat()
two_years_ago = (date.today() - timedelta(days=730)).isoformat()
df = fetch_ohlc("XAU-USD-SPOT", two_years_ago, today)
```

Run this before your strategy logic and you always have an up-to-date dataset.

On data quality: the `/v1/prices/history` endpoint returns the same aggregated price used in live spot queries, computed from multiple sources. Because that aggregation can change over time as sources are added or adjusted, very old historical data and recently computed data may have subtle differences in methodology. For most backtesting purposes this does not matter, but if you are running a high-frequency strategy sensitive to exact price levels, verify that your historical assumptions match how the current aggregation works before drawing hard conclusions.

Get started at [goldprice.dev/docs/quickstart](/docs/quickstart). The free tier includes history access, and you can view [current plan limits](/pricing) before upgrading.
