Transform, resample, and stream market data at any scale
ohlc-resample converts market data into the candlestick charts you want:
- Combine raw ticks or trades into OHLCV candles (open, high, low, close, volume), by time period or by a fixed number of ticks.
- Rebuild candles from one time frame to a coarser one (1-minute to 5-minute, for example).
- Work with the common formats (CCXT-style arrays, JSON, CSV, JSONL, Parquet) and stream very large datasets without running out of memory.
- Fill in missing candles so your chart has no gaps.
curl -fsSL https://github.com/adiled/ohlc-resample/raw/main/install.sh | shInstalls the ohlc command to ~/.local/bin (and a Node runtime if needed).
Pin a version with --version 2.0.0. Uninstall with --uninstall.
npm install ohlc-resample # or pnpm add / yarn add / bun addRequires Node.js 26 or newer.
# Install
curl -fsSL https://github.com/adiled/ohlc-resample/raw/main/install.sh | sh
# Resample 1-minute candles in data.csv into 5-minute candles
ohlc -i data.csv -b 60 -n 300
# Resample a Parquet file and write JSON candles to a file
ohlc -i data.parquet -f json -o candles.jsonOR use directly with npx, no install needed:
npx ohlc-resample -i data.csv -b 60 -n 300See the CLI section below for all options.
- OHLCV arrays (CCXT-style)
[[time, open, high, low, close, volume], ...] - OHLCV JSON objects
[{ time, open, high, low, close, volume }, ...] - Trade / tick JSON objects
[{ time, price, quantity }, ...] - CSV, JSON, and JSONL files
- Parquet files
- Arbitrary schemas, when you supply a
map(see Feeding data as-is)
Input times are epoch milliseconds. See Types.
import {
resampleOhlcv,
resampleTicksByTime,
resampleTicksByCount,
} from "ohlc-resample";
// OHLCV candles from 1 minute to 5 minutes
resampleOhlcv(objectOhlcv, { baseTimeframe: 60, newTimeframe: 5 * 60 }); // IOHLCV[]
resampleOhlcv(arrayOhlcv, { baseTimeframe: 60, newTimeframe: 5 * 60 }); // OHLCV[]
// Ticks grouped into 1-minute OHLCV candles
resampleTicksByTime(tickData, { timeframe: 60, fillGaps: true }); // IOHLCV[]
// Ticks grouped into candles of 5 ticks each
resampleTicksByCount(tickData, { tickCount: 5 }); // IOHLCV[]Each function accepts an array or any sync iterable / generator. The result
uses the same shape as your input (tuples in, tuples out; objects in, objects
out). resampleOhlcv also accepts a binary Float64Array of interleaved
[time, open, high, low, close, volume] values, the fastest way to feed large
binary data.
The async variants read from any async source (a ReadableStream, an async
generator, anything you can for await over) and emit each candle as soon as
it is ready, so memory stays bounded by the active time window rather than the
input size.
import {
resampleOhlcvAsync,
resampleTicksByTimeAsync,
resampleTicksByCountAsync,
} from "ohlc-resample";
// Stream OHLCV candles, one bucket at a time
for await (const candle of resampleOhlcvAsync(readableStream, {
baseTimeframe: 60,
newTimeframe: 300,
})) { }
// Stream ticks into time buckets
for await (const candle of resampleTicksByTimeAsync(tickSource, {
timeframe: 60,
fillGaps: true,
})) { }
// Stream ticks into count buckets (memory scales with tickCount)
for await (const candle of resampleTicksByCountAsync(tickSource, { tickCount: 5 })) { }Sorted input. The array functions sort a copy for you. A stream cannot
buffer the whole input, so pass data ascending by time unless you use the
healing window. Set outOfOrderMs to a number of milliseconds and the stream
keeps each bucket open that long, folding delayed or out-of-order records into
the correct bucket as they arrive. outOfOrderMs = 0 (default) is exact for
already-sorted input.
The async variants also accept a file path to a Parquet file, read one row group at a time so memory stays bounded by the largest row group.
for await (const candle of resampleOhlcvAsync("data.parquet", {
baseTimeframe: 60,
newTimeframe: 300,
})) { } // OHLCV tuple
for await (const candle of resampleTicksByTimeAsync("ticks.parquet", {
timeframe: 60,
})) { } // IOHLCV objectParquet columns are read by exact canonical name (time, open, high,
low, close, volume for OHLCV; time / price / quantity for ticks).
Timestamps in any unit (ms, microseconds, nanoseconds, days) become
milliseconds automatically. Missing required columns throw; OHLCV volume is
optional and defaults to 0. Parquet works only with the async variants.
map removes the need to pre-transform data before passing it in. Feed
records in whatever schema you already have (CCXT's timestamp / amount,
foreign Parquet columns, and so on) and map tells the resampler which keys to
read. It applies uniformly to Parquet rows, CSV headers, and JSON objects. Two
shapes are accepted:
-
Record form, mapping canonical OHLCV fields to the keys in your records. Fields you leave out use the canonical key directly:
// read `time` from `timestamp` and `volume` from `amount`; the rest stay canonical for await (const candle of resampleOhlcvAsync(readableStream, { baseTimeframe: 60, newTimeframe: 300, map: { time: 'timestamp', volume: 'amount' }, })) { } // arbitrary Parquet columns for await (const candle of resampleOhlcvAsync('data.parquet', { baseTimeframe: 60, newTimeframe: 300, map: { time: 'mytime', open: 'myopen', high: 'myhigh', low: 'mylow', close: 'myclose', volume: 'myvol' }, })) { }
-
Function form, a full transform
(record) => IOHLCVfor complete control:for await (const candle of resampleOhlcvAsync(readableStream, { baseTimeframe: 60, newTimeframe: 300, map: (r) => ({ time: r.timestamp, open: r.open, high: r.high, low: r.low, close: r.close, volume: r.amount, }), })) { }
Ticks accept the same shapes over time / price / quantity (for example
map: { time: 'timestamp', quantity: 'amount' }). Positional inputs (OHLCV
tuples and Float64Array) have no keys to read and are unaffected. The sync
functions take canonical arrays, so map applies only to the async variants.
The package is ESM-first with a CommonJS wrapper ("type": "module"). Use
import for the full API; require('ohlc-resample') also works and resolves
to the same module instance.
export type IOHLCV = {
time: number; // epoch milliseconds
open: number;
high: number;
low: number;
close: number;
volume: number;
};
export type OHLCV = [number, number, number, number, number, number]; // [time, open, high, low, close, volume]
export type TradeTick = {
time: number; // epoch milliseconds
price: number;
quantity: number;
};Note: input times for all of the above must be in milliseconds.
Resample CCXT (object) OHLCV to a coarser time frame
import { resampleOhlcv } from "ohlc-resample";
const link_btc_1m = [
{ time: 1563625680000, open: 0.00024824, high: 0.00024851, low: 0.00024798, close: 0.00024831, volume: 2264 },
{ time: 1563625740000, open: 0.00024817, high: 0.00024832, low: 0.00024795, close: 0.00024828, volume: 3145 },
];
// Candles built from the ticks within each 2-minute window
const link_btc_2m = resampleOhlcv(link_btc_1m, {
baseTimeframe: 60,
newTimeframe: 120,
});Resample ticks to OHLCV candles by tick count
import { resampleTicksByCount, TradeTick } from "ohlc-resample";
const adabnb_trades = [
{ time: "1564502620356", side: "sell", quantity: "4458", price: "0.00224", tradeId: "1221272" },
{ time: "1564503133949", side: "sell", quantity: "3480", price: "0.002242", tradeId: "1221273" },
{ time: "1564503134553", side: "buy", quantity: "51", price: "0.002248", tradeId: "1221274" },
];
const airbnb_ticks: TradeTick[] = adabnb_trades.map((trade: any) => ({
time: Number(trade.time),
quantity: Number(trade.quantity),
price: Number(trade.price),
}));
// Candles built from two ticks each
const tickChart = resampleTicksByCount(airbnb_ticks, { tickCount: 2 });The installed command is ohlc (the npm package binary is ohlc-resample).
Options:
-V, --version Show version number
-i, --input <path> Input file path (csv, json, jsonl, parquet) or use pipe
-o, --output <path> Output file path (csv, json) or use stdout
-f, --format <fmt> Output format (csv, json, jsonl) (default: "json")
--input-format <fmt> Input format when piping (csv, json, jsonl, auto) (default: "auto")
-s, --shape <shape> Output shape for JSON: object, array, auto (default: "auto")
-b, --base-timeframe <number> Base timeframe in seconds (default: "60")
-n, --new-timeframe <number> New timeframe in seconds (default: "300")
--map <mapping> Feed data as-is; map fields to canonical keys (e.g. time=timestamp,volume=amount)
--audit Audit the input instead of resampling (print a JSON trust report)
-h, --help Display help for commandThe CLI accepts and emits two equivalent JSON shapes:
// object shape (IOHLCV[])
[{ "time": 1609459200000, "open": 100, "high": 105, "low": 95, "close": 102, "volume": 1000 }]
// array shape (OHLCV[], CCXT-style tuple)
[[1609459200000, 100, 105, 95, 102, 1000]]Input shape is auto-detected. Output shape mirrors the input by default;
override with -s array or -s object. CSV input is always parsed as
object-shape, and CSV output is always rows.
The CLI --map flag is the record-form map from the library, as
field=sourceKey entries separated by commas. It applies to CSV headers, JSON
object keys, and Parquet columns; tuple (array) records have no keys and are
unaffected. With --map, a CSV's first line is always treated as the header.
# CCXT-style JSON objects: timestamp + amount
ohlc -i data.json --map time=timestamp,volume=amount
# Arbitrary Parquet columns
ohlc -i data.parquet --map time=mytime,open=myopen,high=myhigh,low=mylow,close=myclose,volume=myvol.csv, .jsonl, and .ndjson files stream line by line through the
resampler, so memory never scales with file size. (A JSON array file is the
exception: the whole document must be parsed to know where the array ends.)
.parquet files stream row group by row group. Output is written
incrementally in CSV, JSON (a valid, parseable array), or JSONL.
# Stream a 100MB CSV to JSONL candles
ohlc -i huge.csv -f jsonl -o candles.jsonl
# Pipe JSONL line by line (no buffering)
cat data.jsonl | ohlc --input-format jsonl -f jsonlPipe data from other commands; the format is detected automatically, or force
it with --input-format. Supported pipe input is JSON or CSV (Parquet is
file-only via -i).
cat data.json | ohlc
cat data.csv | ohlc --input-format csvTell your AI assistant to reshape your market data and it just does it. Point it at your feed, name the time frame you want, and it hands back clean output at any scale. It already speaks the street: CCXT-style feeds, tick data, Parquet exports, gaps in the series, files too big for memory. You ask, it delivers.
Set it up in one shot, then it works with whichever assistant you use:
npm i -g ohlc-resampleThat installs both the ohlc command and the MCP server. Most assistants
auto-detect it; for the ones that need a nudge, add a server entry:
{
"mcpServers": {
"ohlc-resample": {
"command": "ohlc-resample-mcp",
"args": []
}
}
}Your assistant gets two tools.
resample_ohlcv_file reshapes your data. Give it the file to read and
the time frame you want, and it does the rest:
- 1-minute bars into 5-minute (or any coarser frame)
- raw trades or ticks into clean OHLCV
- CCXT
timestamp/amountdata without renaming a thing - CSV, JSON, JSONL, or Parquet in; JSON, CSV, or JSONL out
Pass an output file and it writes there, or let it return the data directly. No need to paste anything into the chat.
Example: resample 1-minute bars to 5-minute and save them.
resample_ohlcv_file(input_path: "data.csv", base_timeframe: 60, new_timeframe: 300, output_path: "out.json")
audit_ohlcv_file tells you whether you can trust the source before you
resample it, and exactly why. Point it at the same file and it reports the
record count, the time span, the source time frame, any out-of-order or
duplicate bars, bars whose high/low/open/close don't add up, NaN or negative
values, and bars that are simply missing.
Example: check a feed before committing to a resample.
audit_ohlcv_file(input_path: "data.csv")
π€ Adil Shaikh hello@adils.me (https://adils.me)
- Website: https://adils.me
- Github: @adiled
π€ Past authors of candlestick-convert
Contributions, issues and feature requests are welcome!
Feel free to check the issues page.
npm testGive a βοΈ if this project helped you!
Copyright Β© 2022 Adil Shaikh hello@adils.me (https://adils.me).
This project is LGPL--3.0 licensed.