# Your own data

> Static arrays, a datafeed, a WebSocket, or an adapter that maps your existing JSON.

Section: Data. Source: https://ortexcharts.com/docs/data

## 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:

1. **A static array.** You already have the bars. Call `setData`.
2. **A datafeed.** An object with `getBars` and optionally `subscribeBars`; the chart
   handles the initial load, lazy history when the user scrolls left, symbol switching and
   the realtime subscription.
3. **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.
4. **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.

### If You Do Not Have the Data

ORTEX is a market-data company before it is a charting one. The short interest, options flow,
fundamentals, analyst estimates and corporate events behind app.ortex.com are published
through the ORTEX API, documented at [docs.ortex.com](https://docs.ortex.com), and they arrive
in shapes this library draws directly. An OEM agreement can carry a data licence alongside the
library, so one contract covers the chart and the numbers on it:
[talk to us](https://ortexcharts.com/contact?kind=oem).


## Static Bars

```ts
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:

```ts
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](https://ortexcharts.com/docs/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.

```ts
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

```ts
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`, `session` and `priceFormat` to the chart and the
  series, and reports the info through `onSymbolInfo`.
- 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 returns `noMoreData` or 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.

```ts
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();
```

## Loading History in Windows

A datafeed's `getBars` is asked for more as the reader pans, because the binding knows where
the view is. A series you fill yourself has no such help: the obvious code fetches the whole
history up front, which for twenty years of daily rows is a slow first paint and a lot of
bytes nobody looked at.

`attachWindowedData` gives that series the same behavior as the main one. It fetches the
visible span plus a margin, and goes back for more only when the view moves past what it
already holds.

```ts
import { attachWindowedData } from "@ortex-charts/financial";

const series = chart.addSeries("line", { title: "Short interest", priceScaleId: "left" });

const loader = attachWindowedData(
  chart,
  series,
  (from, to) => api.shortInterest(symbol, from, to),   // epoch ms, [from, to)
  {
    margin: 0.5,                        // the screen plus half a screen either side
    earliest: Date.UTC(2005, 0, 1),     // never ask for anything older
    maxSpan: 5 * 365 * 86_400_000,      // split a wider gap into steps, nearest the view first
    initialSpan: 90 * 86_400_000,       // the first request when there is no view yet
    debounce: 150,                      // quiet time after the view settles
    onError: (e) => console.warn(e),
  },
);
```

Your function is handed a half-open range in epoch milliseconds and returns rows in any
order — points or bars. The loader merges them, keeps one copy of each timestamp, and
redraws.

| Option            | Default | What it does                                                    |
| ----------------- | ------- | --------------------------------------------------------------- |
| `margin`          | `0.5`   | Fraction of the visible span fetched beyond each edge            |
| `maxSpan`         | none    | Longest span one request may cover; wider gaps go in steps       |
| `earliest`        | none    | Nothing older is ever requested                                  |
| `latest`          | now     | Nothing newer is requested; a function is read on every check    |
| `debounce`        | `150`   | Milliseconds of quiet after the view settles                     |
| `initialSpan`     | 90 days | The first request when the chart has no visible range yet        |
| `minGap`          | 60 s    | A gap narrower than this is not worth a request                  |
| `retryEmptyAfter` | 5 min   | How long an edge that answered with nothing is left alone        |

The last two exist because of what a real feed does at the right-hand edge. Today's row does
not exist until it exists, so a naive loader asks for the same missing minutes on every
frame. An empty answer is remembered: the left edge is treated as the start of the history
and never asked again, and the right edge is left alone for `retryEmptyAfter` before another
try.

The handle lets you stop or start over:

```ts
loader.loading();       // true while a request is out
loader.loadedRange();   // { from, to } fetched so far, or null
await loader.refresh(); // forget everything and fetch the current window again
loader.dispose();       // stop following the view; the series keeps what it has
```

In the [shell](https://ortexcharts.com/docs/shell#overlays-that-load-as-the-reader-scrolls), an overlay gets the
same thing as `ctx.windowed(series, fetch)`, with the defaults set once in `overlayLoading`.

## 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.

```ts
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

```ts
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

```ts
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.

_Live example: [live-stream](https://ortexcharts.com/examples/live-stream)_

### How 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](https://ortexcharts.com/docs/time).
- 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.

```ts
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

```ts
// 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

```ts
{ 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

```ts
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

```ts
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.

```ts
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.

```ts
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.

```ts
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);
```

_Live example: [short-interest](https://ortexcharts.com/examples/short-interest)_

Three 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`, `nearest` and `bucket` are the other modes;
  see [Concepts](https://ortexcharts.com/docs/concepts).
- **`priceFormat`** is per series, so a percentage pane and a price pane format
  independently.
- **`priceScaleId`** puts 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:

```ts
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](https://ortexcharts.com/docs/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:

```ts
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. |
