Skip to content

Market Data API Reference

Public methods for fetching pairs, currencies, tickers, order books, candles, and trades.

MarketMixin

Public market data endpoints — no authentication required.

This mixin is not meant to be used directly. It is mixed into :class:~revolut_x.client.RevolutXClient.

get_candles

get_candles(symbol: str, interval: Interval | int = Interval.HOUR_1, *, since: TimeLike | None = None, until: TimeLike | None = None) -> list[Candle]

Fetch up to 1000 OHLCV candlesticks for a trading pair.

Parameters:

Name Type Description Default
symbol str

Trading pair (e.g. 'BTC-EUR').

required
interval Interval | int

Candle interval — use the :class:~revolut_x.types.Interval enum (e.g. Interval.MIN_1, Interval.HOUR_1) or an integer number of minutes.

HOUR_1
since TimeLike | None

Start time, inclusive — a Unix timestamp in milliseconds, a :class:~datetime.datetime / :class:~datetime.date, or an ISO-8601 string (see :func:~revolut_x.helpers.to_timestamp_ms).

None
until TimeLike | None

End time, inclusive — same accepted formats as since.

None

Returns:

Type Description
list[Candle]

A list of :class:~revolut_x.types.Candle dictionaries with keys:

list[Candle]

start, open, high, low, close, volume.

Note

A single request covers at most 1000 candles. The exchange does not silently truncate a wider window — it rejects it with HTTP 400 ('Lookup window interval exceeds the limit of 1000 candles'). So until - since must stay within 999 * interval; to download a longer range use :meth:iter_candles, which paginates automatically.

Raises:

Type Description
ApiError

If the API returns a non-2xx response — including when since/until span more than 1000 candles.

Example::

>>> from datetime import datetime, timedelta, timezone
>>> from revolut_x import Interval
>>> # last 500 hours — comfortably inside the 1000-candle limit
>>> start = datetime.now(timezone.utc) - timedelta(hours=500)
>>> candles = client.get_candles("BTC-EUR", Interval.HOUR_1,
...                              since=start)
>>> for c in candles[:3]:
...     print(f"{c['start']}: O={c['open']} H={c['high']}")
Source code in src/revolut_x/market.py
def get_candles(
    self,
    symbol: str,
    interval: Interval | int = Interval.HOUR_1,
    *,
    since: TimeLike | None = None,
    until: TimeLike | None = None,
) -> list[Candle]:
    """Fetch up to 1000 OHLCV candlesticks for a trading pair.

    Args:
        symbol: Trading pair (e.g. ``'BTC-EUR'``).
        interval: Candle interval — use the :class:`~revolut_x.types.Interval`
            enum (e.g. ``Interval.MIN_1``, ``Interval.HOUR_1``) or an integer
            number of minutes.
        since: Start time, inclusive — a Unix timestamp in
            **milliseconds**, a :class:`~datetime.datetime` /
            :class:`~datetime.date`, or an ISO-8601 string (see
            :func:`~revolut_x.helpers.to_timestamp_ms`).
        until: End time, inclusive — same accepted formats as ``since``.

    Returns:
        A list of :class:`~revolut_x.types.Candle` dictionaries with keys:
        ``start``, ``open``, ``high``, ``low``, ``close``, ``volume``.

    Note:
        A single request covers at most **1000 candles**.  The exchange
        does *not* silently truncate a wider window — it rejects it with
        HTTP 400 (``'Lookup window interval exceeds the limit of 1000
        candles'``).  So ``until - since`` must stay within
        ``999 * interval``; to download a longer range use
        :meth:`iter_candles`, which paginates automatically.

    Raises:
        ApiError: If the API returns a non-2xx response — including when
            ``since``/``until`` span more than 1000 candles.

    Example::

        >>> from datetime import datetime, timedelta, timezone
        >>> from revolut_x import Interval
        >>> # last 500 hours — comfortably inside the 1000-candle limit
        >>> start = datetime.now(timezone.utc) - timedelta(hours=500)
        >>> candles = client.get_candles("BTC-EUR", Interval.HOUR_1,
        ...                              since=start)
        >>> for c in candles[:3]:
        ...     print(f"{c['start']}: O={c['open']} H={c['high']}")
    """
    api_symbol = _normalize_symbol(symbol)
    params: dict[str, Any] = {"interval": int(interval)}
    if since is not None:
        params["since"] = _to_ms(since)
    if until is not None:
        params["until"] = _to_ms(until)

    _status, data = self._http.request(
        "GET",
        f"/public/candles/{api_symbol}",
        params=params,
    )

    if isinstance(data, dict):
        return data.get("data", [])
    return data

get_currencies

get_currencies() -> dict[str, CurrencyConfig]

Retrieve the configuration for all supported currencies.

Returns:

Type Description
dict[str, CurrencyConfig]

A dictionary mapping currency symbols (e.g. 'BTC', 'EUR')

dict[str, CurrencyConfig]

to their :class:~revolut_x.types.CurrencyConfig.

Raises:

Type Description
ApiError

If the API returns a non-2xx response.

Example::

>>> currencies = client.get_currencies()
>>> print(currencies["BTC"]["name"])
Bitcoin
Source code in src/revolut_x/market.py
def get_currencies(self) -> dict[str, CurrencyConfig]:
    """Retrieve the configuration for all supported currencies.

    Returns:
        A dictionary mapping currency symbols (e.g. ``'BTC'``, ``'EUR'``)
        to their :class:`~revolut_x.types.CurrencyConfig`.

    Raises:
        ApiError: If the API returns a non-2xx response.

    Example::

        >>> currencies = client.get_currencies()
        >>> print(currencies["BTC"]["name"])
        Bitcoin
    """
    _status, data = self._http.request("GET", "/public/configuration/currencies")
    return data

get_order_book

get_order_book(symbol: str, depth: int = 10) -> OrderBook

Retrieve the current order book (bids and asks) for a trading pair.

Parameters:

Name Type Description Default
symbol str

Trading pair (e.g. 'BTC-EUR').

required
depth int

Requested number of price levels. Currently ignored by the exchange — the venue always returns 5 levels per side, regardless of this value. Kept so that the SDK picks up deeper books automatically if Revolut X starts honouring it.

10

Returns:

Name Type Description
An OrderBook

class:~revolut_x.types.OrderBook with bids and asks

OrderBook

lists. Each entry is an

OrderBook

class:~revolut_x.types.OrderBookLevel dictionary — read the

OrderBook

price from ['p'] and the quantity from ['q']. Levels are

OrderBook

sorted best-first.

Raises:

Type Description
ApiError

If the API returns a non-2xx response.

Example::

>>> book = client.get_order_book("BTC-EUR")
>>> best_bid = book["bids"][0]
>>> print(f"Best bid: {best_bid['p']} EUR x {best_bid['q']} BTC")
Source code in src/revolut_x/market.py
def get_order_book(self, symbol: str, depth: int = 10) -> OrderBook:
    """Retrieve the current order book (bids and asks) for a trading pair.

    Args:
        symbol: Trading pair (e.g. ``'BTC-EUR'``).
        depth: Requested number of price levels.  **Currently ignored by
            the exchange** — the venue always returns 5 levels per side,
            regardless of this value.  Kept so that the SDK picks up
            deeper books automatically if Revolut X starts honouring it.

    Returns:
        An :class:`~revolut_x.types.OrderBook` with ``bids`` and ``asks``
        lists.  Each entry is an
        :class:`~revolut_x.types.OrderBookLevel` **dictionary** — read the
        price from ``['p']`` and the quantity from ``['q']``.  Levels are
        sorted best-first.

    Raises:
        ApiError: If the API returns a non-2xx response.

    Example::

        >>> book = client.get_order_book("BTC-EUR")
        >>> best_bid = book["bids"][0]
        >>> print(f"Best bid: {best_bid['p']} EUR x {best_bid['q']} BTC")
    """
    api_symbol = _normalize_symbol(symbol)
    _status, data = self._http.request(
        "GET",
        f"/public/order-book/{api_symbol}",
        params={"limit": depth},
    )
    if isinstance(data, dict):
        return data.get("data", data)
    return data

get_pair

get_pair(symbol: str) -> PairConfig

Retrieve the configuration for a specific trading pair.

Parameters:

Name Type Description Default
symbol str

Trading pair in any common format (e.g. 'BTC-EUR', 'BTC/EUR', 'btceur').

required

Returns:

Name Type Description
The PairConfig

class:~revolut_x.types.PairConfig for the requested pair.

Raises:

Type Description
ValueError

If the pair is not found or not active on the exchange.

ApiError

If the API returns a non-2xx response.

Example::

>>> pair = client.get_pair("ETH-EUR")
>>> print(f"Step: {pair['base_step']}")
Source code in src/revolut_x/market.py
def get_pair(self, symbol: str) -> PairConfig:
    """Retrieve the configuration for a specific trading pair.

    Args:
        symbol: Trading pair in any common format
            (e.g. ``'BTC-EUR'``, ``'BTC/EUR'``, ``'btceur'``).

    Returns:
        The :class:`~revolut_x.types.PairConfig` for the requested pair.

    Raises:
        ValueError: If the pair is not found or not active on the exchange.
        ApiError: If the API returns a non-2xx response.

    Example::

        >>> pair = client.get_pair("ETH-EUR")
        >>> print(f"Step: {pair['base_step']}")
    """
    pairs = self.get_pairs()

    # Try both slash and dash formats (API returns slash-keyed dict)
    clean_slash = symbol.replace("-", "/").upper()
    clean_dash = symbol.replace("/", "-").upper()

    info = pairs.get(clean_slash) or pairs.get(clean_dash)
    if info is not None:
        return info

    # Case-insensitive fallback
    for key, val in pairs.items():
        if key.upper() in (clean_slash, clean_dash):
            return val

    available = ", ".join(sorted(pairs.keys())[:10])
    raise ValueError(
        f"Trading pair '{symbol}' not found on Revolut X. "
        f"Available pairs include: {available}… "
        f"Use get_pairs() to see the full list."
    )

get_pairs

get_pairs() -> dict[str, PairConfig]

Retrieve the configuration and trading limits for all active pairs.

Returns:

Type Description
dict[str, PairConfig]

A dictionary mapping pair symbols (e.g. 'BTC/EUR') to their

dict[str, PairConfig]

class:~revolut_x.types.PairConfig configuration, including

dict[str, PairConfig]

minimum/maximum order sizes, price/quantity step sizes, and status.

Raises:

Type Description
ApiError

If the API returns a non-2xx response.

NetworkError

On connection failure.

Example::

>>> client = RevolutXClient()
>>> pairs = client.get_pairs()
>>> btc = pairs["BTC/EUR"]
>>> print(f"Min order: {btc['min_order_size']} BTC")
Min order: 0.00000001 BTC
Source code in src/revolut_x/market.py
def get_pairs(self) -> dict[str, PairConfig]:
    """Retrieve the configuration and trading limits for all active pairs.

    Returns:
        A dictionary mapping pair symbols (e.g. ``'BTC/EUR'``) to their
        :class:`~revolut_x.types.PairConfig` configuration, including
        minimum/maximum order sizes, price/quantity step sizes, and status.

    Raises:
        ApiError: If the API returns a non-2xx response.
        NetworkError: On connection failure.

    Example::

        >>> client = RevolutXClient()
        >>> pairs = client.get_pairs()
        >>> btc = pairs["BTC/EUR"]
        >>> print(f"Min order: {btc['min_order_size']} BTC")
        Min order: 0.00000001 BTC
    """
    _status, data = self._http.request("GET", "/public/configuration/pairs")
    return data

get_ticker

get_ticker(symbol: str) -> Ticker

Retrieve the current ticker for a specific trading pair.

Parameters:

Name Type Description Default
symbol str

Trading pair (e.g. 'BTC-EUR').

required

Returns:

Name Type Description
A Ticker

class:~revolut_x.types.Ticker with best bid, best ask,

Ticker

and last traded price.

Note

Most symbols are quoted in more than one region ('EEA' and 'UK') at different prices and volumes. Because /public/tickers returns them in an order that changes between calls, picking the first match would hand back a different region — and so a different price — on each call. This method therefore returns the most liquid quote: the match with the highest volume_24h, with the region name breaking an exact tie. To work with a specific region, filter :meth:get_tickers on ['region'] yourself.

Raises:

Type Description
ValueError

If ticker data is not available for the given symbol.

ApiError

If the API returns a non-2xx response.

Example::

>>> ticker = client.get_ticker("BTC-EUR")
>>> print(f"BTC price: {ticker['last_price']} EUR")
Source code in src/revolut_x/market.py
def get_ticker(self, symbol: str) -> Ticker:
    """Retrieve the current ticker for a specific trading pair.

    Args:
        symbol: Trading pair (e.g. ``'BTC-EUR'``).

    Returns:
        A :class:`~revolut_x.types.Ticker` with best bid, best ask,
        and last traded price.

    Note:
        Most symbols are quoted in more than one region (``'EEA'`` and
        ``'UK'``) at different prices and volumes.  Because
        ``/public/tickers`` returns them in an order that changes between
        calls, picking the first match would hand back a different region —
        and so a different price — on each call.  This method therefore
        returns the **most liquid** quote: the match with the highest
        ``volume_24h``, with the region name breaking an exact tie.  To
        work with a specific region, filter :meth:`get_tickers` on
        ``['region']`` yourself.

    Raises:
        ValueError: If ticker data is not available for the given symbol.
        ApiError: If the API returns a non-2xx response.

    Example::

        >>> ticker = client.get_ticker("BTC-EUR")
        >>> print(f"BTC price: {ticker['last_price']} EUR")
    """
    tickers = self.get_tickers()

    target_slash = symbol.replace("-", "/").upper()
    target_dash = symbol.replace("/", "-").upper()

    if isinstance(tickers, list):
        matches = [
            t for t in tickers
            if t.get("symbol", "").upper() in (target_slash, target_dash)
        ]
        if matches:
            return max(matches, key=_liquidity_rank)
    elif isinstance(tickers, dict):
        result = tickers.get(target_slash) or tickers.get(target_dash)
        if result is not None:
            return result

    raise ValueError(
        f"Ticker data not available for '{symbol}'. "
        f"Check that the pair exists with get_pairs()."
    )

get_tickers

get_tickers() -> list[Ticker]

Retrieve current tickers (best bid, best ask, last price) for all pairs.

Returns:

Type Description
list[Ticker]

A list of :class:~revolut_x.types.Ticker dictionaries — **one per

list[Ticker]

pair and region**, not one per pair. Most symbols trade in both

list[Ticker]

'EEA' and 'UK' and therefore appear twice, with different

list[Ticker]

prices and volumes in each; read ['region'] to tell them apart.

list[Ticker]

The order of the list is not stable between calls.

Raises:

Type Description
ApiError

If the API returns a non-2xx response.

Source code in src/revolut_x/market.py
def get_tickers(self) -> list[Ticker]:
    """Retrieve current tickers (best bid, best ask, last price) for all pairs.

    Returns:
        A list of :class:`~revolut_x.types.Ticker` dictionaries — **one per
        pair *and* region**, not one per pair.  Most symbols trade in both
        ``'EEA'`` and ``'UK'`` and therefore appear twice, with different
        prices and volumes in each; read ``['region']`` to tell them apart.
        The order of the list is not stable between calls.

    Raises:
        ApiError: If the API returns a non-2xx response.
    """
    _status, data = self._http.request("GET", "/public/tickers")
    if isinstance(data, dict):
        return data.get("data", [])
    return data

get_trades

get_trades(symbol: str, *, start_date: TimeLike | None = None, end_date: TimeLike | None = None, cursor: str | None = None, limit: int = MAX_TRADES_PER_PAGE) -> tuple[list[Trade], str | None]

Fetch a single page of public trades (up to 100 per request).

Parameters:

Name Type Description Default
symbol str

Trading pair (e.g. 'BTC-EUR').

required
start_date TimeLike | None

Start time — Unix timestamp in milliseconds, :class:~datetime.datetime / :class:~datetime.date, or an ISO-8601 string (see :func:~revolut_x.helpers.to_timestamp_ms).

None
end_date TimeLike | None

End time — same accepted formats as start_date.

None
cursor str | None

Pagination cursor from a previous response's metadata.next_cursor field.

None
limit int

Maximum number of trades to return. The exchange accepts 1–100 and rejects anything larger with HTTP 400, so values outside that range are clamped into it. To read more than one page, follow next_cursor or use :meth:iter_trades.

MAX_TRADES_PER_PAGE

Returns:

Type Description
list[Trade]

A tuple of (trades, next_cursor) where trades is a list of

str | None

class:~revolut_x.types.Trade dicts and next_cursor is a

tuple[list[Trade], str | None]

string for fetching the next page (or None if no more data).

Raises:

Type Description
ApiError

If the API returns a non-2xx response.

Example::

>>> trades, cursor = client.get_trades("BTC-EUR", limit=100)
>>> print(f"Got {len(trades)} trades, next cursor: {cursor}")
Source code in src/revolut_x/market.py
def get_trades(
    self,
    symbol: str,
    *,
    start_date: TimeLike | None = None,
    end_date: TimeLike | None = None,
    cursor: str | None = None,
    limit: int = MAX_TRADES_PER_PAGE,
) -> tuple[list[Trade], str | None]:
    """Fetch a single page of public trades (up to 100 per request).

    Args:
        symbol: Trading pair (e.g. ``'BTC-EUR'``).
        start_date: Start time — Unix timestamp in **milliseconds**,
            :class:`~datetime.datetime` / :class:`~datetime.date`, or an
            ISO-8601 string (see
            :func:`~revolut_x.helpers.to_timestamp_ms`).
        end_date: End time — same accepted formats as ``start_date``.
        cursor: Pagination cursor from a previous response's
            ``metadata.next_cursor`` field.
        limit: Maximum number of trades to return.  The exchange accepts
            **1–100** and rejects anything larger with HTTP 400, so values
            outside that range are clamped into it.  To read more than one
            page, follow ``next_cursor`` or use :meth:`iter_trades`.

    Returns:
        A tuple of ``(trades, next_cursor)`` where ``trades`` is a list of
        :class:`~revolut_x.types.Trade` dicts and ``next_cursor`` is a
        string for fetching the next page (or ``None`` if no more data).

    Raises:
        ApiError: If the API returns a non-2xx response.

    Example::

        >>> trades, cursor = client.get_trades("BTC-EUR", limit=100)
        >>> print(f"Got {len(trades)} trades, next cursor: {cursor}")
    """
    api_symbol = _normalize_symbol(symbol)
    params: dict[str, Any] = {
        "symbol": api_symbol,
        "limit": max(1, min(limit, MAX_TRADES_PER_PAGE)),
    }
    if start_date is not None:
        params["start_date"] = _to_ms(start_date)
    if end_date is not None:
        params["end_date"] = _to_ms(end_date)
    if cursor:
        params["cursor"] = cursor

    _status, data = self._http.request(
        "GET", "/public/trades/all", params=params
    )

    if isinstance(data, dict):
        trades = data.get("data", [])
        next_cursor = data.get("metadata", {}).get("next_cursor")
    else:
        trades = data if isinstance(data, list) else []
        next_cursor = None

    return trades, next_cursor

iter_candles

iter_candles(symbol: str, interval: Interval | int = Interval.HOUR_1, *, since: TimeLike | None = None, until: TimeLike | None = None) -> Iterator[Candle]

Iterate over all candles in a time range, automatically paginating.

This is a generator that yields :class:~revolut_x.types.Candle dictionaries one by one, fetching additional pages from the API as needed. It handles the 1000-candle-per-request limit transparently.

Parameters:

Name Type Description Default
symbol str

Trading pair (e.g. 'BTC-EUR').

required
interval Interval | int

Candle interval (see :meth:get_candles).

HOUR_1
since TimeLike | None

Start time — Unix timestamp in milliseconds, :class:~datetime.datetime / :class:~datetime.date, or an ISO-8601 string.

None
until TimeLike | None

End time — same accepted formats as since.

None

Yields:

Type Description
Candle

class:~revolut_x.types.Candle dictionaries in chronological

Candle

order (oldest first). No candle is yielded twice, and none is

Candle

yielded with a start later than until.

Example::

>>> for candle in client.iter_candles("BTC-EUR", Interval.MIN_1,
...                                   since="2024-01-01"):
...     process(candle)  # each candle is a dict
Source code in src/revolut_x/market.py
def iter_candles(
    self,
    symbol: str,
    interval: Interval | int = Interval.HOUR_1,
    *,
    since: TimeLike | None = None,
    until: TimeLike | None = None,
) -> Iterator[Candle]:
    """Iterate over all candles in a time range, automatically paginating.

    This is a generator that yields :class:`~revolut_x.types.Candle`
    dictionaries one by one, fetching additional pages from the API as
    needed.  It handles the 1000-candle-per-request limit transparently.

    Args:
        symbol: Trading pair (e.g. ``'BTC-EUR'``).
        interval: Candle interval (see :meth:`get_candles`).
        since: Start time — Unix timestamp in **milliseconds**,
            :class:`~datetime.datetime` / :class:`~datetime.date`, or an
            ISO-8601 string.
        until: End time — same accepted formats as ``since``.

    Yields:
        :class:`~revolut_x.types.Candle` dictionaries in chronological
        order (oldest first).  No candle is yielded twice, and none is
        yielded with a ``start`` later than ``until``.

    Example::

        >>> for candle in client.iter_candles("BTC-EUR", Interval.MIN_1,
        ...                                   since="2024-01-01"):
        ...     process(candle)  # each candle is a dict
    """
    interval_ms = int(interval) * 60 * 1000  # interval in milliseconds
    # The API rejects a window spanning more than 1000 candles, and the
    # window is inclusive on both ends — so 999 intervals == 1000 candles.
    max_batch_ms = 999 * interval_ms

    since_ms = _to_ms(since) if since is not None else None
    until_ms = _to_ms(until) if until is not None else None

    if since_ms is None:
        # Without a start there is nothing to paginate: a single request
        # already returns the most recent batch.
        yield from self.get_candles(symbol, interval, until=until_ms)
        return

    current_since = since_ms

    while True:
        # Walk fixed-size windows forward rather than chaining off the last
        # returned candle, so that gaps in the series (illiquid pairs with
        # no trades in an interval) cannot cut the iteration short.
        hard_end = until_ms if until_ms is not None else int(time.time() * 1000)
        batch_until = min(current_since + max_batch_ms, hard_end)
        # The API rejects a window where 'until' equals 'since', which is
        # what a final single-candle window would produce. Always ask for
        # at least two candles and drop the overshoot below.
        batch_until = max(batch_until, current_since + interval_ms)

        batch = self.get_candles(
            symbol, interval, since=current_since, until=batch_until
        )
        if until_ms is not None:
            batch = [c for c in batch if int(c.get("start", 0)) <= until_ms]

        yield from batch

        if batch_until >= hard_end:
            break

        current_since = batch_until + interval_ms

iter_trades

iter_trades(symbol: str, *, start_date: TimeLike | None = None, end_date: TimeLike | None = None, limit: int = MAX_TRADES_PER_PAGE) -> Iterator[Trade]

Iterate over all public trades in a time range, automatically paginating.

This generator fetches pages of trades using cursor-based pagination and yields :class:~revolut_x.types.Trade dicts one by one.

Parameters:

Name Type Description Default
symbol str

Trading pair (e.g. 'BTC-EUR').

required
start_date TimeLike | None

Start time — Unix timestamp in milliseconds, :class:~datetime.datetime / :class:~datetime.date, or an ISO-8601 string.

None
end_date TimeLike | None

End time — same accepted formats as start_date.

None
limit int

Page size per request (1–100, clamped into that range).

MAX_TRADES_PER_PAGE

Yields:

Type Description
Trade

class:~revolut_x.types.Trade dictionaries.

Example::

>>> from datetime import datetime, timezone
>>> start = datetime(2024, 1, 1, tzinfo=timezone.utc)
>>> for trade in client.iter_trades("BTC-EUR", start_date=start):
...     print(f"{trade['price']} EUR x {trade['quantity']} BTC")
Source code in src/revolut_x/market.py
def iter_trades(
    self,
    symbol: str,
    *,
    start_date: TimeLike | None = None,
    end_date: TimeLike | None = None,
    limit: int = MAX_TRADES_PER_PAGE,
) -> Iterator[Trade]:
    """Iterate over all public trades in a time range, automatically paginating.

    This generator fetches pages of trades using cursor-based pagination
    and yields :class:`~revolut_x.types.Trade` dicts one by one.

    Args:
        symbol: Trading pair (e.g. ``'BTC-EUR'``).
        start_date: Start time — Unix timestamp in **milliseconds**,
            :class:`~datetime.datetime` / :class:`~datetime.date`, or an
            ISO-8601 string.
        end_date: End time — same accepted formats as ``start_date``.
        limit: Page size per request (1–100, clamped into that range).

    Yields:
        :class:`~revolut_x.types.Trade` dictionaries.

    Example::

        >>> from datetime import datetime, timezone
        >>> start = datetime(2024, 1, 1, tzinfo=timezone.utc)
        >>> for trade in client.iter_trades("BTC-EUR", start_date=start):
        ...     print(f"{trade['price']} EUR x {trade['quantity']} BTC")
    """
    # Convert once so every page sends the same window and a bad value
    # fails on the first iteration rather than halfway through.
    start_ms = _to_ms(start_date) if start_date is not None else None
    end_ms = _to_ms(end_date) if end_date is not None else None

    cursor: str | None = None

    while True:
        trades, next_cursor = self.get_trades(
            symbol,
            start_date=start_ms,
            end_date=end_ms,
            cursor=cursor,
            limit=limit,
        )
        if not trades:
            break

        yield from trades

        if not next_cursor:
            break

        cursor = next_cursor