# Candles

Read OHLCV candles for 16 timeframes, tell open from closed candles, use reference candles from before listing, and stream the current candle.

Candles summarize a Pair's trades per time bucket: open, high, low, close, base volume, and quote volume. Read history with [`GET /v1/spot/markets/{symbol}/candles`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/markets/symbol/candles) or [`MarketDataService.GetCandles`](https://testnet.polyester.com/docs/api-docs/connect/marketdata.v1.MarketDataService/GetCandles), and follow the current candle on the public candles channel. Both are public and need no authentication.

## Timeframes

All buckets are in UTC; daylight saving time never shifts a boundary.

| REST              | ConnectRPC                    | Bucket                                           |
| ----------------- | ----------------------------- | ------------------------------------------------ |
| `1s`              | `SEC_1`                       | 1 second                                         |
| `1m`, `3m`, `5m`  | `MIN_1`, `MIN_3`, `MIN_5`     | 1, 3, 5 minutes                                  |
| `15m`, `30m`      | `MIN_15`, `MIN_30`            | 15, 30 minutes                                   |
| `1h`, `2h`, `4h`  | `HOUR_1`, `HOUR_2`, `HOUR_4`  | 1, 2, 4 hours                                    |
| `6h`, `8h`, `12h` | `HOUR_6`, `HOUR_8`, `HOUR_12` | 6, 8, 12 hours                                   |
| `1d`              | `DAY_1`                       | 1 day, starting at 00:00 UTC                     |
| `3d`              | `DAY_3`                       | 3 days, counted from 1970-01-01                  |
| `1w`              | `WEEK_1`                      | 1 week, starting Monday 00:00 UTC                |
| `1mo`             | `MONTH_1`                     | Calendar month, starting on the 1st at 00:00 UTC |

Fixed-length buckets up to `1d` start at whole multiples of their length since 1970-01-01T00:00:00Z, so they always align to UTC midnight. The REST `timeframe` parameter is required and takes only the strings above. A missing value is rejected with HTTP 400 (`INVALID_ARGUMENT`), and any other value with HTTP 400 (`BAD_REQUEST`).

## Read candles

## REST JSON

```http
GET https://api.testnet.polyester.com/v1/spot/markets/BTC-USDT/candles?timeframe=1m&limit=3&includeIncomplete=true
```

```json
{
	"nextPageToken": "cGcBEm1hcmtldGRhdGEuY2FuZGxlcw...",
	"symbol": "BTC-USDT",
	"timeframe": "1m",
	"candles": [
		[
			"1793914620",
			"76135.97",
			"76135.97",
			"76044.66",
			"76059.87",
			"0.78155926",
			"59468.9262044147"
		],
		[
			"1793914560",
			"76105.51",
			"76166.43",
			"76090.29",
			"76135.97",
			"0.7577467",
			"57690.8632925045"
		],
		[
			"1793914500",
			"76075.08",
			"76151.2",
			"76075.08",
			"76105.51",
			"0.46245503",
			"35196.8818432678"
		],
		[
			"1793914440",
			"76090.29",
			"76120.74",
			"76029.45",
			"76075.08",
			"0.31737989",
			"24144.8216305437"
		]
	],
	"referenceCandles": []
}
```

Each candle is a tuple of strings: `[tsSec, open, high, low, close, volume, quoteVolume]`. `tsSec` is the bucket start in Unix seconds. Prices and volumes are decimals.

## ConnectRPC ProtoJSON

```http
POST https://api.testnet.polyester.com/marketdata.v1.MarketDataService/GetCandles
Content-Type: application/json

{"symbolId": 1, "timeframe": "MIN_1", "limit": 2, "includeIncomplete": true}
```

```json
{
	"symbolId": 1,
	"timeframe": "MIN_1",
	"candles": [
		{
			"tsSec": "1793914620",
			"open": "76135970000000",
			"high": "76135970000000",
			"low": "76044660000000",
			"close": "76059870000000",
			"volume": "78155926",
			"quoteVolume": "59468.9262044147"
		},
		{
			"tsSec": "1793914560",
			"open": "76105510000000",
			"high": "76166430000000",
			"low": "76090290000000",
			"close": "76135970000000",
			"volume": "75774670",
			"isClosed": true,
			"quoteVolume": "57690.8632925045"
		}
	],
	"nextPageToken": "cGcBEm1hcmtldGRhdGEuY2FuZGxlcw..."
}
```

Prices use a fixed scale of 9 and `volume` the base Asset's `marketDataVolumeScale`; `quoteVolume` is a decimal string. The response above is shortened to two candles.

These responses were captured on 5 November 2026; page tokens are shortened.

Every parameter is listed in the [API reference](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/markets/symbol/candles). The rules that matter when you read history:

- `limit` counts closed candles only: default `500`, maximum `10000`. With `includeIncomplete`, the open candle is added on top, which is why the REST request above for `limit=3` returned four candles.
- `startTime` and `endTime` are inclusive bounds on the bucket start time. Neither may be more than 5 minutes in the future, and `endTime` may not be before `startTime`.
- `includeIncomplete` adds the current open candle only when it is inside the requested range.
- `includeReference` adds the [reference candles](#reference-candles) for the same timeframe and range.
- Send `pageToken` with the same Pair, timeframe, and bounds as the request that returned it.

Candles are ordered newest first. An empty `nextPageToken` means there are no more pages. A limit above the maximum is rejected with HTTP 400 (`INVALID_ARGUMENT`). An unknown symbol answers HTTP 404 on REST; an unknown `symbolId` answers HTTP 400 (`invalid_argument`) on ConnectRPC.

### Empty series

A series without closed candles answers with empty arrays, not missing fields. Right after a Pair opens, for example, every daily and weekly series is empty until its first bucket closes:

```json
{
	"nextPageToken": "",
	"symbol": "BTC-USDT",
	"timeframe": "1d",
	"candles": [],
	"referenceCandles": []
}
```

REST always sends `candles`, `referenceCandles`, and `nextPageToken`. Test whether they are empty, not whether they are present. REST `symbol` is the Pair symbol from the request path. ConnectRPC JSON omits empty fields, as usual.

## Open and closed candles

History returns closed candles only. With `includeIncomplete`, the current open candle is added in front of them, so it is always the first candle on the first page.

- ConnectRPC sets `isClosed: true` on every closed candle. The open candle has no `isClosed`, which means `false`.
- REST tuples carry no closed flag. With `includeIncomplete=true`, the first tuple is the open candle when its `tsSec` is the start of the current bucket; every other tuple is closed.
- An open candle changes until its bucket ends. Never store it as final.

### How a candle is built

- A candle opens at the previous candle's close, so consecutive candles join at every timeframe. A Pair's first candle opens at its first trade.
- `high` and `low` include the open.
- A bucket without trades is flat at the previous close, with zero volume.
- `volume` is the traded base quantity. `quoteVolume` is the exact traded quote amount, the sum of price times quantity of each trade, not `close` times `volume`.

## Reference candles

With `includeReference`, the response adds `referenceCandles`: a composite candle series of the same market on other venues. Use it to draw price history from before the Pair was listed on Polyester.

- The reference series covers the time before the Pair's `listingAt`. In the example below, the last `BTC-USDT` reference minute is `1793880720` (2026-11-05T12:12:00Z), right before that Pair's `listingAt` of 12:13:33Z.
- REST reference tuples have six slots, `[tsSec, open, high, low, close, volume]`, without `quoteVolume`.
- Reference prices use the Pair's `referencePriceScale` on ConnectRPC, not the fixed scale of 9. Volume uses `marketDataVolumeScale`.
- Reference `volume` was `0` in every reference candle captured for this page. ConnectRPC JSON omits it.
- The `1s` timeframe has no reference series; `referenceCandles` is empty.

```json
{
	"referenceCandles": [
		["1793880720", "80219.15", "80219.16", "80199.99", "80218", "0"],
		["1793880660", "80153.6", "80219.16", "80153.59", "80219.16", "0"]
	]
}
```

When a reference series is requested but temporarily unavailable, the request fails as unavailable instead of returning an empty list. Retry with backoff.

## Backfill

1. Request the newest page

   Call `GetCandles` with the timeframe, a `limit` of up to `10000`, and optionally `startTime`.

2. Follow the page token

   Request older pages with `pageToken` until `nextPageToken` is empty. Keep the same timeframe and bounds; changing them invalidates the token.

3. Key candles by bucket

   Store candles by `tsSec`. A later read of the same bucket replaces the earlier one.

## Live candles

Subscribe to one timeframe of one Pair, using the REST timeframe string:

```text
public:spot:market:candles:<timeframe>:<symbolId>
public:spot:market:candles:<timeframe>:<symbolId>:proto
```

The JSON channel publishes one candle object with decimal strings; the Protobuf channel publishes `marketdata.v1.CandlePoint`. These updates were sent on `public:spot:market:candles:1m:1`, in this order:

```json
{"tsSec":"1793914200","open":"75983.86","high":"75983.86","low":"75983.86","close":"75983.86","volume":"0","quoteVolume":"0","isClosed":false}
{"tsSec":"1793914140","open":"75983.86","high":"76014.26","low":"75923.1","close":"75983.86","volume":"0.16636898","quoteVolume":"12640.141624887","isClosed":true}
{"tsSec":"1793914200","open":"75983.86","high":"76029.46","low":"75983.86","close":"76029.46","volume":"0.00249794","quoteVolume":"189.9170000951","isClosed":false}
```

- Each update for the open bucket has `isClosed: false` and replaces the previous update of that bucket.
- After a bucket ends, one terminal update with `isClosed: true` carries its final values.
- The next bucket's first update can arrive before the previous bucket's terminal update, as above. Key every update by `tsSec`; never assume the latest update is the newest bucket.

> **Fill gaps from history**
>
> After a reconnect that could not recover missed publications, read the affected range with `GetCandles` and replace your candles by `tsSec`. The live channel only carries the current bucket and the terminal update of the one before.

See [Realtime channels](https://testnet.polyester.com/docs/developer-docs/market-data/realtime-channels) for connecting, [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits) for quotas, and the [API reference](https://testnet.polyester.com/docs/api-docs/connect/marketdata.v1.MarketDataService/GetCandles) for the full schema.
