> ## Documentation index
> Fetch the complete documentation index at: https://docs.predictefy.com/llms.txt
> Use it to discover every available page before exploring further.

# Backtest a strategy on historical candles

> Pull OHLCV for an outcome, read the quality vocabulary, and refuse to backtest on data that cannot support it.

Historical candles are the input to any backtest. Predictefy labels every candle with where it
came from and how good it is, and a backtest that ignores those labels will produce confident
numbers from data that cannot support them.

## Request

Candles key on an **outcome**, not a market — a binary market has one series per side.
[Identifiers](/guides/market-ids/) covers why.

```sh
curl -s "$PREDICTEFY_API_URL/api/polymarket/fetchOHLCV?outcomeId=OUTCOME_ID&resolution=1h&start=2026-01-01T00:00:00Z&end=2026-06-30T00:00:00Z&limit=5000" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"
```

| Parameter                   | Detail                                                          |
| --------------------------- | --------------------------------------------------------------- |
| `outcomeId`                 | The series. `id` is a compatibility alias; `marketId` also accepted |
| `resolution`                | `1s` `5s` `10s` `30s` `1m` `5m` `15m` `30m` `1h` `4h` `6h` `1d`  |
| `start` / `end`             | ISO timestamp **or** epoch milliseconds                          |
| `limit`                     | 1–5000                                                           |

`5m`, `15m`, `30m`, `4h` and `6h` are **aggregated at query time** from stored history rather
than being stored natively. That is not a defect, but it does mean those buckets inherit the
quality of whatever they were built from — which the response tells you.

## The candle record

```json
{
  "timestamp": 1767225600000,
  "open": 0.41,
  "high": 0.44,
  "low": 0.4,
  "close": 0.43,
  "volume": 128400,
  "source": "official",
  "sourceType": "true-candle",
  "quality": "ok",
  "isTrueCandle": true
}
```

`timestamp`, `open`, `high`, `low` and `close` are always present. Everything else is optional,
and the optional fields are the ones that decide whether a backtest is meaningful.

**`source`** — where the series came from: `official`, `onchain`, `write-forward`, `derived`.

**`sourceType`** — how the bucket was built:

| Value           | Meaning                                                  |
| --------------- | -------------------------------------------------------- |
| `true-candle`   | The venue published this candle                           |
| `point-derived` | Built from point-in-time prices                           |
| `trade-derived` | Built from the trades tape                                |
| `rest-derived`  | A coarse REST trade-tape candle                           |
| `book-derived`  | Built from order-book state                               |
| `rollup`        | Aggregated from finer buckets                             |

**`quality`** — `ok`, `partial`, `suspect`, or `mixed`. `mixed` means the bucket aggregates
inputs of differing quality, which is the expected value for a query-time aggregation.

**`isTrueCandle`** — the single boolean that separates published candles from reconstructed
ones.

:::caution[Candles use a different vocabulary from other reads]
Most records carry `asOf` and `provenance`. Candles do not — they carry `source`, `sourceType`,
`quality` and `isTrueCandle` instead, and the response's `meta.provenance` describes the series
as a whole: `venue-native`, `predictefy-store`, or `merged`. A checker that demands `asOf` on a
candle is asserting the wrong contract.
:::

## Filter before you backtest

```js
function usable(candle) {
  // A backtest that mixes published candles with book-derived reconstructions is
  // measuring two different things and reporting one number.
  if (candle.quality === 'suspect') return false;
  if (candle.sourceType === 'book-derived') return false;
  return true;
}

const candles = body.data.filter(usable);
const coverage = candles.length / body.data.length;
if (coverage < 0.95) {
  throw new Error(`only ${(coverage * 100).toFixed(1)}% of buckets are usable — widen the window or drop the venue`);
}
```

Decide the rule up front and record it with the result. "Backtested on 1h candles, excluding
`suspect` and `book-derived` buckets, 98.2% coverage" is a claim someone can check. A bare Sharpe
ratio is not.

`volume` is nullable. A null is "not reported", not zero — a volume filter that treats null as
zero silently discards every venue that does not publish it.

## What history you can actually read

Two limits apply, and they are different:

- **Venue coverage.** Not every venue has history for every resolution.
  [Historical data](/guides/history/) records what exists, including which venues have
  sub-minute data and in what id format.
- **Plan window.** Your plan caps how far back you may read. Requesting beyond it returns
  `PLAN_REQUIRED` rather than a silently truncated series — see
  [Credits & billing](/guides/credits/).

Check `meta.provenance` on the response: `venue-native` means every returned bucket came from
the venue, `predictefy-store` means our own store served it, and `merged` means both. A backtest
that spans a provenance change is comparing two datasets.

## Cost

History reads are priced above catalog reads, and a backtest is many of them — one per outcome
per window. Pull once and cache locally; re-running a strategy should not re-read the API.
Current weights are in [Credits & billing](/guides/credits/).

## Related

- [Historical data](/guides/history/) — coverage per venue and resolution
- [Identifiers](/guides/market-ids/) — why candles key on `outcomeId`
- [Capability-honest data](/guides/honest-data/) — reading the honesty fields generally
