# Candles and timeframes from an MT5 API

> OHLCV candles from MT4 and MT5 explained: timeframes from M1 to MN1, the forming candle, broker time in timestamps, gaps, paging long ranges and tick volume.

Source: https://tickwire.io/blog/candles-and-timeframes-api/

Tickwire blog, published 2026-10-20. General information, not investment advice.

Almost every trading strategy starts from candles. A moving average, a breakout level, a volatility filter: all of them read a table of bars. Fetching that table looks like the easiest call in any trading API. It is also where many quiet bugs come from: an unfinished bar treated as final, a timestamp read as UTC when it isn't, or a weekend gap taken for missing data.

This post explains what a candle from MetaTrader 4 (MT4) or MetaTrader 5 (MT5) really contains, which timeframes exist, and how to load a long history into pandas without fooling yourself about time zones. The examples use Tickwire's REST API, but the traps are the same with any source of MetaTrader data.

## What is in a candle

A candle (or bar) sums up all the price changes in one time slot. The usual name for its contents is **OHLCV**:

- **Open:** the first price in the slot.
- **High** and **low:** the highest and lowest prices in the slot.
- **Close:** the last price. For the bar that is still forming, this is simply the latest price so far.
- **Volume:** how much trading happened. MetaTrader has two kinds, explained at the end of this section.

MetaTrader builds bars from the **bid** price (the price you can sell at) for most forex and CFD symbols. The ask is higher by the spread, so a buy stop placed exactly at a candle's high can still fill, or fail to fill, in ways the chart doesn't show. Some MT5 symbols traded on an exchange build bars from the last trade price instead. MT5 bars can also carry a `spread` value in points. Treat it as a rough guide, not as the spread you would have paid.

Each candle from the API looks like this:

```json
{ "symbol": "EURUSD", "timeframe": "1h",
  "time": "2026-10-20T06:00:00.000Z", "brokerTime": "2026-10-20 09:00:00.000",
  "open": 1.08412, "high": 1.08490, "low": 1.08377, "close": 1.08455,
  "tickVolume": 3120, "spread": 6 }
```

The `time` is the moment the bar **opened**, not when it closed.

MetaTrader reports two kinds of volume:

- **Tick volume** (`tickVolume`) counts how many price changes happened in the bar. For forex and most CFDs this is the only volume there is, since no central exchange reports traded amounts. It is still a useful activity measure: busy bars have more ticks.
- **Real volume** (`volume`) is the traded amount, and appears only on MT5 symbols where the broker passes on exchange data. For most forex symbols the field is simply absent.

Tick volume depends on the broker's price feed. Two brokers can show very different tick counts for the same hour, so don't compare raw tick volume across accounts.

## Timeframes from M1 to MN1, and MT4 limits

A timeframe is the length of each bar. MT5 offers 21 of them: M1, M2, M3, M4, M5, M6, M10, M12, M15, M20, M30, H1, H2, H3, H4, H6, H8, H12, D1, W1 and MN1 (one month). In Tickwire's API they are written `1m`, `2m` up to `30m`, then `1h` to `12h`, `1d`, `1w` and `1mn`.

MT4 has only nine: M1, M5, M15, M30, H1, H4, D1, W1 and MN1. Ask an MT4 account for `2h` and you get the error `not_supported_on_mt4`. If you need a two-hour bar on MT4, load H1 and merge pairs of bars yourself, starting each pair at a broker-time hour your strategy chooses.

Three timeframes behave differently from the rest:

- **D1** starts at the broker's midnight, not UTC midnight. A daily bar on one broker covers different hours from a daily bar on another.
- **W1** is dated at the start of the broker's week, usually a Sunday, even when trading only begins later, so the bar's time is not its first trade.
- **MN1** is a calendar month, so its length changes from bar to bar.

MT4 has a few more limits. It has no real volume. It can't return tick history, so the ticks endpoint answers `not_supported_on_mt4` rather than empty data. Candles, current candles and paging work the same as on MT5.

## The forming candle and closed candles

The newest bar is normally still **forming**: it has an open, but its high, low and close keep changing until its time slot ends. A strategy that treats it as closed sees a different chart every time it looks.

When you ask for the latest candles without a start time, the last item is usually the forming one. You can fetch the forming bar on its own as well:

```
GET /users/current/accounts/<ACCOUNT_ID>/symbols/EURUSD/current-candles/1h
```

The safe pattern is to compare the two. If the last candle of your history has the same `brokerTime` as the current candle, drop it before calculating signals. Decide on closed bars, then act once per new bar. If you do want intrabar logic, label it clearly in your code so nobody mistakes it for bar-close logic later.

## Broker time in candle timestamps

MetaTrader servers keep their own clock. Many run at UTC+2 in winter and UTC+3 in summer, but not all, and the switch dates follow one country's rules or another's. Bars are cut on that broker clock. That is why each candle carries two times:

- `time` is UTC, written with a `Z`.
- `brokerTime` is the broker's wall clock, written without any zone, because it isn't UTC.

The UTC value depends on knowing the broker's offset. Tickwire measures it from live ticks and never guesses, so on a new account the offset can be unknown for a while. Until then `time` may be missing, and a request with a `startTime` (which is UTC) answers `broker_time_unknown` because it can't be translated safely. `brokerTime` is always there.

In practice: use `brokerTime` to decide which trading day a bar belongs to, and use `time` to line bars up with anything else in UTC, such as news, other data feeds or your logs. [Broker server time is not UTC](https://tickwire.io/blog/broker-time-is-not-utc/) explains the offset and its traps in more depth.

## Gaps, and why they are normal

MetaTrader creates a bar only when at least one tick arrived in its slot. A quiet minute at 3 a.m. has no M1 bar at all. The weekend has no bars. A holiday or a broker's daily break leaves a hole too. So a list of candles is not evenly spaced, and code that assumes "the bar before this one is exactly one hour earlier" will be wrong.

Don't fill gaps with made-up bars unless your method truly needs a regular grid, and if you fill them, mark the filled rows. Indicators such as a 20-bar average already count bars, not hours, which is what traders using the chart see too. See [market hours, weekends and gaps for trading bots](https://tickwire.io/blog/market-hours-weekend-gaps/).

The broker also decides how much history exists. Old M1 data is often limited. When you ask for more than the broker holds, you simply get fewer candles back, so always check how many you received.

## Paging long ranges into pandas

One request returns at most 1,000 candles. They come back oldest first, ending with the bar that opened at or before your `startTime`, or the latest bar if you leave it out. To load more, page backwards: ask again with a `startTime` just before the oldest bar you have.

History requests are heavier than normal reads. On Tickwire each costs 5 units of your rate limit, and an account allows 5 history requests at the same time; more get `429` with a `Retry-After` header. Load pages one after another, not all at once.

This loader pages backwards, drops the forming bar, and labels time honestly:

```python
import time
import pandas as pd
import requests

API = "https://api.tickwire.io"
ACC = "<ACCOUNT_ID>"
H = {"auth-token": "tw_live_..."}
BASE = f"{API}/users/current/accounts/{ACC}"

def get(path: str, **params):
    while True:
        r = requests.get(BASE + path, headers=H, params=params, timeout=30)
        if r.status_code == 429:                       # too many history requests
            time.sleep(float(r.headers.get("Retry-After", "1")))
            continue
        r.raise_for_status()
        return r.json()

def load_candles(symbol: str, tf: str, pages: int = 5) -> pd.DataFrame:
    path = f"/historical-market-data/symbols/{symbol}/timeframes/{tf}/candles"
    rows, start = [], None
    for _ in range(pages):
        params = {"limit": 1000} | ({"startTime": start} if start else {})
        batch = get(path, **params)
        rows = batch + rows                            # older pages go in front
        if len(batch) < 1000 or not batch[0].get("time"):
            break                                      # no more history, or UTC unknown
        oldest = pd.Timestamp(batch[0]["time"]) - pd.Timedelta(milliseconds=1)
        start = oldest.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"

    df = pd.DataFrame(rows).drop_duplicates("brokerTime").sort_values("brokerTime")
    df["broker_time"] = pd.to_datetime(df["brokerTime"])   # naive on purpose: not UTC

    forming = get(f"/symbols/{symbol}/current-candles/{tf}")
    df = df[df["brokerTime"] != forming["brokerTime"]]      # keep closed bars only

    if "time" in df and df["time"].notna().all():
        df.index = pd.to_datetime(df["time"], utc=True).rename("time_utc")
    else:
        df.index = df["broker_time"].rename("broker_time")  # say so, don't pretend
    return df[["broker_time", "open", "high", "low", "close", "tickVolume"]]

bars = load_candles("EURUSD", "1h")
print(bars.index.name, len(bars))
gaps = bars["broker_time"].diff() > pd.Timedelta(hours=1)
print(bars.loc[gaps, "broker_time"].tail())            # weekends and quiet spells
```

Some details worth noticing:

- The index name says which clock it uses. A UTC index is time-zone aware; a broker-time index is deliberately naive and named `broker_time`, so it can never be mixed up with UTC by accident.
- Duplicates are dropped on `brokerTime`, because paging boundaries can overlap by one bar.
- The gap check uses broker time, so you can see that the big gaps line up with the broker's weekend.

## Doing this with Tickwire

Tickwire runs one real MT4 or MT5 terminal per account and serves its candles through the same REST routes for both platforms, with UTC `time` and `brokerTime` on every bar. Broker time is labelled as broker time and never assumed to be UTC. To follow the forming bar live, subscribe to candles on the [WebSocket stream](https://tickwire.io/blog/stream-mt5-prices-websocket/) instead of polling.

- [Time zones](https://tickwire.io/docs/time-zones/) shows how the broker offset is measured and read.
- [MT4 and MT5](https://tickwire.io/docs/platforms/) lists the timeframes and features each platform supports.
- [MT5 from Python](https://tickwire.io/mt5-python-api/) covers using these routes from Python on any operating system.

Practise on a demo account before you risk money, and check your loaded bars against the terminal's own chart once before trusting a backtest built on them.
