How to Build Candlestick Data from Tick-by-Tick Market Feed

Nitish Kumar Singh

May 30, 2025

Ever wondered how those candlestick charts are made from raw tick data? Here, I’ll show you how to build them step by step — including volume.

Photo by Sajad Nori on Unsplash

We have an array ticks of market feed for any stock, and we will build an array candles that can be used to show candlestick charts and volume bars.

Each tick object has properties as shown below:

{
    last_traded_price: number, // in paise
    last_traded_timestamp: number, // in UNIX seconds
    vol_traded: number // cumulative volume till that tick from market open
    // more properties
}

We will build a candle object that has properties like this:

{
    time: number, // in UNIX time
    open: number, // in rupees
    high: number, // in rupees
    low: number, // in rupees
    close: number, // in rupees
    volume: number
}

We will create a function that takes an array ticks of market feed, frameSec for candle size, and a zone offset to convert the time to that time zone, and returns an array of candles.

const giveCandles = (ticks, frameSec, zone = 19800)=> {
    if (!Array.isArray(ticks) || ticks.length === 0 || frameSec <= 0) return [];

    const sorted = ticks
        .filter(t => t.last_traded_timestamp > 0 && t.last_traded_price > 0 && t.vol_traded >= 0)
        .sort((a, b) => a.last_traded_timestamp - b.last_traded_timestamp);

    const candles = [];
    let candle = null;
    let lastClose = null;
    let prevVolTraded = null;

    for (const { last_traded_timestamp: tsRaw, last_traded_price: prRaw, vol_traded } of sorted) {
        const ts = tsRaw + zone;
        const price = prRaw / 100;
        const slot = Math.floor(ts / frameSec) * frameSec;

        // Calculate volume delta
        const volume = prevVolTraded !== null && vol_traded >= prevVolTraded ? vol_traded - prevVolTraded : 0;
        prevVolTraded = vol_traded;

        // Fill missing candles
        if (candle && slot > candle.time + frameSec) {
            let gapTime = candle.time + frameSec;
            while (gapTime < slot) {
                candles.push({
                    time: gapTime,
                    open: lastClose,
                    high: lastClose,
                    low: lastClose,
                    close: lastClose,
                    volume: 0,
                });
                gapTime += frameSec;
            }
        }

        // Start new candle or update existing
        if (!candle || candle.time !== slot) {
            candle = {
                time: slot,
                open: price,
                high: price,
                low: price,
                close: price,
                volume: volume,
            };
            candles.push(candle);
        } else {
            candle.high = Math.max(candle.high, price);
            candle.low = Math.min(candle.low, price);
            candle.close = price;
            candle.volume += volume;
        }

        lastClose = candle.close;
    }
    return candles;
}

We can remove the filtering and sorting code if we're sure that ticks have no bad data and are in sorted order.

I’ve added time shifting because Lightweight Charts don’t support time zones.

I’ve shifted time to match the Kolkata time zone (UTC+5:30, which is 19800 seconds). You can pass any zone value you want.

Just note that shifting time like this can change the date. In my case, it works because the market closes at 15:30 + 5:30 = 21:00, which doesn’t change the date.

Volume calculation can be simplified like this, but I’ve added extra checks for safety (since volume can drop due to resets, errors, or session changes):

const volume = prevVolTraded !== null ? vol_traded - prevVolTraded : 0;

How we build the candle data mainly depends on calculating the slot — the start time of the candle for a given time frame:

const slot = Math.floor(ts / frameSec) * frameSec;

Let’s say we have a tick at May 30, 2025 14:45:53 (which is 9:15:53 AM in IST) = 1748596553 and frameSec = 60 (for 1-minute candles), then: All ticks that fall in the 9:15 AM candle will have slot = 1748596500. We just build candle data by checking this slot.

I’ve added the code below to fill any missing candle data, so that charts don’t have gaps:

// Fill missing candles
if (candle && slot > candle.time + frameSec) {
    let gapTime = candle.time + frameSec;
    while (gapTime < slot) {
        candles.push({
            time: gapTime,
            open: lastClose,
            high: lastClose,
            low: lastClose,
            close: lastClose,
            volume: 0,
        });
        gapTime += frameSec;
    }
}

Now you know how to build proper candle data from raw tick data. I hope this helped you understand the process better.

If you have any questions or suggestions, feel free to drop a comment. Happy coding!

Published on May 30, 2025
Comments (undefined)

Read More