Data
Your own data
Static arrays, a datafeed, a WebSocket, or an adapter that maps your existing JSON.
The Library Never Fetches
ORTEX Charts consumes data; it does not go and get it. There is no built-in HTTP client, no symbol database, no cache service and no ORTEX endpoint your chart depends on. You supply rows, or you supply an object that knows how to produce rows, and the chart draws them.
That leaves four ways in, in increasing order of how much the library does for you:
- A static array. You already have the bars. Call
setData. - A datafeed. An object with
getBarsand optionallysubscribeBars; the chart handles the initial load, lazy history when the user scrolls left, symbol switching and the realtime subscription. - A live source. Anything that pushes items over time — a WebSocket, a poller, a handle you push into — folded into bars at the chart resolution.
- An adapter spec. A small JSON description of where history, live data and symbol metadata live in your messages, from which the library builds the datafeed and the sources for you. No code per feed.
They compose. The common production shape is an adapter spec for the price feed and host-supplied series for everything else.
Static Bars
import { createFinancialChart } from "@ortex-charts/financial";
import type { Bar } from "@ortex-charts/math";
const bars: Bar[] = rows.map((r) => ({
time: r.t * 1000, // milliseconds, UTC
open: r.o,
high: r.h,
low: r.l,
close: r.c,
volume: r.v,
}));
const chart = createFinancialChart(el, { timeZone: "America/New_York", resolution: "1D", data: bars });
chart.setData(nextBars); // replaces everything
Bars must be sorted ascending by time. volume is required by the type; use zero when your
instrument has none.
Every Accepted Input Shape
setData takes whichever of these you already have, so there is normally no conversion step.
| Shape | Example | Use for |
|---|---|---|
Bar[] |
[{ time, open, high, low, close, volume }] |
OHLCV series. |
BarSeries |
{ length, time, open, high, low, close, volume } with Float64Array columns |
Large OHLCV data you already hold columnar. |
[time, value][] |
[[1735689600000, 12.4], …] |
The shape most value endpoints return. |
{ time, value }[] |
[{ time: 1735689600000, value: 12.4 }] |
Readable value series. |
ValueSeries |
{ length, time, value } with Float64Array columns |
Large value data held columnar. |
Columnar input is the cheapest, because nothing is copied. Helpers convert between the forms when you need them:
import { barSeriesFromRows, barSeriesToRows, valueSeriesFromPoints, valueSeriesFromColumns } from "@ortex-charts/math";
const series = barSeriesFromRows(bars);
const rows = barSeriesToRows(series);
const values = valueSeriesFromPoints([[t1, v1], [t2, v2]]);
const columns = valueSeriesFromColumns(timeArray, valueArray);
Optional Aggressor Volumes
A bar may carry buyVolume and sellVolume: the volume that lifted the offer and the
volume that hit the bid. They make delta, cumulative delta and split volume profiles exact
rather than estimated. See Order flow.
The one rule to remember is that a series created without those columns ignores them on
later bars, because history cannot be retro-fitted into an already-allocated column. Decide
at the first setData.
A Datafeed
A datafeed is one object with one required method. Give it to the chart with bind and the
chart handles the initial load, paging older history when the user scrolls near the left
edge, realtime subscription, and the teardown and reload on a symbol or resolution change.
import type { Datafeed } from "@ortex-charts/financial";
const datafeed: Datafeed = {
async resolveSymbol(symbol) {
const info = await fetch(`/api/symbols/${symbol}`).then((r) => r.json());
return {
symbol,
name: info.name,
exchange: info.exchange,
timeZone: info.tz, // applied to the chart
session: info.hours, // applied to the chart, anchors intraday bars
currency: info.ccy,
priceFormat: { precision: info.decimals, minMove: 10 ** -info.decimals },
logo: info.logoUrl, // shown in the toolbar and the legend
};
},
async getBars({ symbol, resolution, from, to, countBack, firstRequest }) {
const url = `/api/bars?s=${symbol}&res=${resolution}&from=${from}&to=${to}`;
const rows: Array<[number, number, number, number, number, number]> = await fetch(url).then((r) => r.json());
return {
bars: rows.map(([t, o, h, l, c, v]) => ({ time: t, open: o, high: h, low: l, close: c, volume: v })),
noMoreData: rows.length === 0,
};
},
subscribeBars(symbol, resolution, onBar) {
const socket = openMyFeed(symbol, resolution, onBar);
return () => socket.close();
},
async searchSymbols(query) {
return fetch(`/api/search?q=${encodeURIComponent(query)}`).then((r) => r.json());
},
};
const binding = chart.bind(datafeed, { symbol: "AAPL", resolution: "1D" });
The Interface
interface Datafeed {
resolveSymbol?(symbol: string): Promise<SymbolInfo>;
getBars(request: BarsRequest): Promise<BarsResponse | Bar[]>;
subscribeBars?(symbol: string, resolution: string, onBar: (bar: Bar) => void): () => void;
searchSymbols?(query: string): Promise<SymbolInfo[]>;
}
interface BarsRequest {
symbol: string;
resolution: string;
from: number; // inclusive start, milliseconds
to: number; // exclusive end, milliseconds
countBack: number; // the caller wants at least this many bars before `to`
firstRequest: boolean;
}
interface BarsResponse {
bars: Bar[];
noMoreData?: boolean; // set when the feed knows there is nothing earlier
}
interface SymbolInfo {
symbol: string;
name?: string;
exchange?: string;
timeZone?: string;
session?: string; // "0930-1600"
priceFormat?: Partial<PriceFormat>;
currency?: string;
logo?: string; // URL or data URI
}
Returning a bare Bar[] from getBars is allowed; it is treated as { bars } with no
noMoreData.
What the Binding Does For You
- Resolves the symbol, applies
timeZone,sessionandpriceFormatto the chart and the series, and reports the info throughonSymbolInfo. - Requests an initial window sized from
initialBars(500 by default) and the resolution, sorts the rows, sets them and scrolls to the right edge. - Watches the visible range and requests older history when fewer than
loadMoreThresholdBars(60 by default) remain to the left of the view, stopping when the feed returnsnoMoreDataor nothing. - Subscribes to realtime bars and applies them through
series.update, so a bar with the same time replaces the current one and a later time appends. - Cancels in-flight work on a symbol or resolution change with a generation counter, so a slow response for the old symbol cannot land on the new chart.
const binding = chart.bind(datafeed, {
symbol: "AAPL",
resolution: "1D",
initialBars: 800,
loadMoreThresholdBars: 100,
onSymbolInfo: (info) => setHeader(info.name, info.currency),
onLoading: (loading) => setSpinner(loading),
onError: (e) => reportError(e),
});
await binding.setSymbol("MSFT");
await binding.setResolution("60");
await binding.reload();
binding.dispose();
Live Sources
A live source is anything with a subscribe(listener) that returns an unsubscribe function.
chart.live(source) folds whatever it emits into the main series.
type LiveItem =
| Tick // { time, price, size, side? } — folded into bars
| Bar // replaces or appends, finer bars fold into coarser
| { time: number; value: number } // for a value series
| readonly [number, number]; // [time, value]
WebSocket
import { websocketSource, type LiveItem } from "@ortex-charts/financial";
const source = websocketSource<LiveItem>({
url: "wss://feed.example.com/stream",
protocols: ["v2"],
onOpen: (send) => send(JSON.stringify({ action: "subscribe", symbols: ["AAPL"] })),
parse: (raw) => {
const m = JSON.parse(String(raw)) as { ev: string; t: number; p: number; s: number };
return m.ev === "T" ? { time: m.t, price: m.p, size: m.s } : null; // null ignores the message
},
onStatus: (s) => setConnectionState(s), // "connecting" | "open" | "closed" | "reconnecting"
onError: (e) => console.warn(e),
reconnect: true,
minReconnectDelayMs: 500,
maxReconnectDelayMs: 15000,
heartbeat: JSON.stringify({ action: "ping" }),
heartbeatMs: 15000,
});
const stop = chart.live(source, { resolution: "1", onUpdate: (item, appended) => countTicks(appended) });
The socket opens on the first subscriber and closes when the last one leaves, so subscribing
the chart is enough to manage the connection. Reconnection is exponential with jitter.
parse may return one item, an array of items, or null to ignore the message.
chart.connect(options) is the same thing in one call when the chart is the only consumer;
it returns a function that both unsubscribes and closes the socket.
Polling and Manual Sources
import { pollingSource, ManualSource, type LiveItem } from "@ortex-charts/financial";
const polled = pollingSource<LiveItem>(async () => {
const q = await fetch("/api/quote/AAPL").then((r) => r.json());
return { time: q.t, price: q.last, size: q.size };
}, 2000);
const manual = new ManualSource<LiveItem>();
chart.live(manual);
manual.push({ time: Date.now(), price: 182.4, size: 100 });
ManualSource is the escape hatch for transports the library does not know about:
server-sent events, a shared worker, a message bus, a test fixture.
@ortex-charts/financialHow Folding Works
streamTo — which is what chart.live calls — decides what to do from the shape of the
item and the kind of the series:
- A tick on a bar series extends the current bar or starts a new one at the chart resolution, in the chart time zone, honoring the session anchor.
- A bar on a bar series merges into the current bar of the chart resolution: the open stays, high and low extend, the close follows, and volume accumulates when the incoming bar is a new sub-bar rather than a restatement of the same one. This is what makes one-minute candles from a feed drive a 15-minute chart correctly.
- A value point on a value series replaces or appends.
- A tick or bar on a value series contributes its price or close.
Adapter Specs
Writing the datafeed above is 40 lines of glue for every feed. An adapter spec replaces
it with data: a description of where history, live updates and symbol metadata live in your
messages. createAdapter turns it into a Datafeed and live sources.
Because a spec is plain data, it can live in a JSON file, be validated, be versioned, and be tested against a captured sample without running a chart.
import { createAdapter, createFinancialChart } from "@ortex-charts/financial";
const spec = {
name: "my-feed",
variables: { token: process.env.FEED_TOKEN! },
symbol: { static: { timeZone: "America/New_York", session: "0930-1600", precision: 2, minMove: 0.01 } },
history: {
url: "https://api.example.com/v1/{symbol}/bars?res={resolution}&from={from}&to={to}&key={token}",
dates: { unit: "date" },
resolutions: { "1D": "daily", "1": "1min", "60": "hourly" },
records: {
path: "bars",
kind: "bar",
time: { path: "d", unit: "date" },
fields: { open: "o", high: "h", low: "l", close: "c", volume: "vol" },
},
},
live: {
url: "wss://stream.example.com?key={token}",
subscribe: { action: "subscribe", symbols: ["{symbol}"] },
messages: [
{
match: [{ path: "ev", equals: "T" }],
records: {
kind: "tick",
time: { path: "t", unit: "ms" },
fields: { price: "p", size: "s" },
filter: [{ path: "sym", equals: "{symbol}" }],
},
},
],
},
} as const;
const adapter = createAdapter(spec);
const chart = createFinancialChart(el, { timeZone: "America/New_York" });
chart.bind(adapter.datafeed, { symbol: "ACME", resolution: "1D" });
chart.live(adapter.live("ACME"));
The Vocabulary
| Concept | What it is |
|---|---|
| Field path | a.b[0].c for objects, a column index such as 3 for array rows, $ for the record itself. |
| Field reference | A path plus scale, offset, default and map, for pence-to-pounds conversions and code lookups. |
| Time unit | ms, s, us, ns, iso, date, datetime, epochDays, with a timeZone for the date units and an optional floor to a bar boundary. |
| Records | An array at path, a single object, or parallel column arrays with layout: "columns". |
| Filter | Conditions applied to each record; {symbol} placeholders split a multiplexed feed. |
| Message spec | Conditions on a whole message plus a records spec; a feed lists one per message type. |
| Templates | {symbol}, {exchange}, {ticker}, {resolution}, {from}, {to}, {countBack}, your variables, and {info.<field>} from symbol resolution. |
Records come in three kinds: bar (needs close), tick (needs price) and value
(needs value). A value record is how a non-price series arrives through the same
machinery.
Field References
// A plain path
{ close: "c" }
// A column index, for array rows like [t, o, h, l, c, v]
{ time: { path: 0, unit: "ms" }, open: 1, high: 2, low: 3, close: 4, volume: 5 }
// Scaling: a feed that quotes UK stocks in pence
{ close: { path: "px", scale: 0.01 } }
// A default when the field is missing
{ volume: { path: "v", default: 0 } }
// A lookup table
{ value: { path: "rating", map: { BUY: 1, HOLD: 0, SELL: -1 } } }
Time Fields
{ time: { path: "t", unit: "ms" } }
{ time: { path: "d", unit: "date", timeZone: "America/New_York" } }
{ time: { path: "ts", unit: "iso" } }
{ time: { path: "t", unit: "s", floor: "1" } } // candles stamped with the trade time
floor is the fix for feeds that stamp a one-minute candle with the time of the last trade
in it rather than with the start of the bar.
Columnar Responses
records: {
layout: "columns",
kind: "bar",
time: { path: "t", unit: "s" },
fields: { open: "o", high: "h", low: "l", close: "c", volume: "v" },
}
A response of { t: [...], o: [...], h: [...], … } needs nothing more than layout.
Testing a Spec Without a Chart
import { validateAdapterSpec, createAdapter, explainMapping } from "@ortex-charts/financial";
const problems = validateAdapterSpec(spec); // string[] with actionable messages
if (problems.length) throw new Error(problems.join("; "));
const adapter = createAdapter(spec);
console.log(adapter.explainHistory(capturedResponse)); // the records the chart would see
console.log(adapter.explainLive(capturedMessage, "ACME")); // the same for a live message
createAdapter validates and throws an AdapterError listing every problem, so a broken
spec fails at construction rather than as an empty chart. When a sample produces nothing,
explainMapping says why.
What the Adapter Handles
- A reconnecting WebSocket with backoff and heartbeat, one socket per symbol, or one shared socket for a multiplexed feed, opened on the first subscriber and closed on the last.
- History paging: more is requested when the user scrolls near the left edge, and the requests stop when the feed says there is nothing earlier.
- Folding: ticks become bars at the chart resolution, finer bars fold into coarser ones, and candles stamped with trade times are floored.
- Sorting and de-duplication of history rows.
The Escape Hatches
Some feeds need logic no declaration can express. When the spec is authored in TypeScript
rather than JSON, url, subscribe, unsubscribe and body may be functions of the
request context, symbol.transform may post-process the resolved info, and any field
reference may carry a transform. A JSON spec cannot express these; that is the trade-off
for the spec being data.
const spec: AdapterSpec = {
name: "venue-aware",
live: {
url: "wss://stream.example.com",
subscribe: (ctx) => (ctx.exchange === "LSE"
? [{ sub: `${ctx.isin}GBP` }, { sub: `${ctx.isin}GBX` }]
: { sub: ctx.ticker }),
messages: [/* … */],
},
};
The ORTEX Preset
ortexAdapterSpec({ apiBase, wsBase }) returns a spec for ORTEX's own API, so the library
is a drop-in on app.ortex.com. It is also the largest worked example of a spec, covering
symbol resolution, daily history over HTTP, a live WebSocket with a per-venue subscribe
handshake, and symbol search.
import { createAdapter, ortexAdapterSpec } from "@ortex-charts/financial";
const adapter = createAdapter(ortexAdapterSpec({ apiBase: "https://app.ortex.com", wsBase: "wss://ws.ortex.com" }));
Your Own Series on Their Own Panes
This is the case that sells the library, and it needs no adapter at all. A host series is just points, a pane and a scale.
const chart = createFinancialChart(el, { timeZone: "America/New_York", resolution: "1D", volume: false, data: bars });
// Estimated short interest, on a pane of its own, as a percentage.
const siPane = chart.chart.addPane({ heightRatio: 0.22 });
chart.chart
.addSeries("area", {
pane: siPane.id,
title: "Estimated Short Interest, % of free float",
color: "#7C5CE6",
priceFormat: { type: "percent", precision: 2, minMove: 0.01 },
align: "forwardFill",
})
.setData(siPoints);
// Days to cover, another pane.
const dtcPane = chart.chart.addPane({ heightRatio: 0.18 });
chart.chart
.addSeries("line", { pane: dtcPane.id, title: "Days to Cover", color: "#F5A524", lineWidth: 1.5 })
.setData(dtcPoints);@ortex-charts/financialThree details make this work with data that does not share the price timeline:
align: "forwardFill"carries the last known value forward, which is what a daily statistic on an intraday chart needs.exact,nearestandbucketare the other modes; see Concepts.priceFormatis per series, so a percentage pane and a price pane format independently.priceScaleIdputs a series on the left scale, or on an id of its own that is never drawn, which gives it independent autoscaling without spending axis width.
To overlay rather than separate, drop the pane and choose a scale:
chart.chart.addSeries("line", {
title: "Utilization",
priceScaleId: "left",
color: "#34D399",
}).setData(utilizationPoints);
chart.chart.priceScale("left").applyOptions({ visible: true, mode: "normal" });
Registering Them in the Toolbar
When you are using the shell, wrap each of these in an OverlayDef and the
user gets them in the Data menu, with the checked state and the layout handled for you:
const overlays: OverlayDef[] = [
{
id: "si",
label: "Short interest",
group: "Securities lending",
defaultOn: true,
add: async ({ chart, symbol }) => {
const rows = await api.shortInterest(symbol);
const s = chart.addSeries("line", { title: "Short interest", priceScaleId: "left", align: "forwardFill" });
s.setData(rows);
return () => chart.removeSeries(s.id);
},
},
];
add may be async, receives the symbol to fetch for, and returns the function that removes
the overlay again. It is called again after every symbol or resolution change, with the
previous disposer run first, so the host never has to watch the chart itself.
Choosing an Approach
| Your situation | Use |
|---|---|
| The data is already in the page. | setData. |
| One symbol, no history paging, a push feed. | setData plus chart.live(source). |
| Symbol switching, resolution switching, lazy history. | A Datafeed and chart.bind. |
| The same as above, but you would rather write JSON than glue. | An adapter spec. |
| Non-price series alongside the price. | Host series on their own panes, or OverlayDef in the shell. |
| A transport the library does not know. | ManualSource and push into it. |