Skip to content

Charts

Order flow

Footprint bars, volume profile, market profile, delta, cumulative delta and relative volume.

The Position This Library Takes

Most charting libraries estimate directional volume. They do not know which side was the aggressor, so they infer it from where the price moved inside the bar, which is the tick rule from the 1990s. The delta, cumulative delta and footprint numbers that come out of it disagree with the platforms that read exchange aggressor flags, and traders notice.

ORTEX Charts takes the other route. The host supplies the aggressor data and the library renders it exactly. Where the data is missing it estimates, marks the result as estimated in the tooltip, and offers a strict mode that shows a gap rather than a guess.

That is a real requirement, not a preference: footprint bars, exact volume profiles and true delta need per-price bid and ask volume from your feed. Without it you get the same estimates everyone else produces, honestly labeled.

The Data Model

Three optional shapes carry order-flow information into the library.

// On a bar: volume that lifted the offer and volume that hit the bid.
interface Bar {
  time: number; open: number; high: number; low: number; close: number; volume: number;
  buyVolume?: number;
  sellVolume?: number;
}

// Per bar, per price level: a footprint.
interface FootprintBar {
  time: number;
  levels: Array<{ price: number; bid: number; ask: number }>;   // ascending by price
}

// On a tick: the aggressor side when the feed reports it.
interface Tick { time: number; price: number; size: number; side?: "buy" | "sell" }

bid is volume that traded at the bid, meaning aggressive sellers. ask is volume that lifted the offer, meaning aggressive buyers.

buyVolume and sellVolume travel through every path in the engine: row conversion, bar aggregation, the realtime append buffers, update, and the aligned columns indicators read. Two consequences follow from how the columns are allocated:

  • A series created without aggressor volumes ignores them on later bars. History cannot be retro-fitted, so set the first setData with the columns you intend to use.
  • A series created with them writes NaN for a bar that lacks them, which is what method: "aggressor" shows as a gap.

Footprint Bars

const fp = chart.setFootprint(flow, {
  tickSize: 0.01,
  layout: "bidAsk",
  imbalanceRatio: 3,
});

fp.updateBar(liveFootprintBar);   // realtime

For every bar wider than minBarWidth the primitive draws one cell per price level: the bid half shaded in the down color and the ask half in the up color by share of the busiest level in that bar, the numbers when the cell is at least nine pixels high, diagonal imbalances outlined, the bar point of control boxed, and delta plus total volume under the bar.

When bars are narrower than minBarWidth the cells are unreadable, so the primitive draws a thin per-bar delta strip along the bottom of the pane instead. The information stays visible at every zoom level rather than disappearing.

Hovering a cell shows its bid, ask, delta and total.

Option Default What it does
minBarWidth 28 Below this bar width in pixels, the delta strip replaces the cells.
cluster 1 Merge this many consecutive price levels into one cell.
tickSize 0 Level height in price units; zero derives it from the smallest gap between levels.
layout "bidAsk" bidAsk prints bid and ask per cell, delta prints ask − bid, volume prints the total.
imbalanceRatio 3 Outline a level whose ask is at least this many times the bid one level below, and the mirror. Zero disables it.
heat true Shade cells by volume relative to the busiest level of the bar.
showTotals true Print delta and total under each bar.
showPoc true Outline the bar point of control.
buyColor null Overrides the theme up color.
sellColor null Overrides the theme down color.
stripHeight 4 Height of the fallback delta strip in pixels.

setFootprint does two extra things worth knowing. It gives the main bars their aggressor volumes if they arrived without them, so Delta and CVD become exact; and it feeds every volume profile added with addVolumeProfile, so those become exact too.

Footprint bars@ortex-charts/financial
Loading footprint bars
Bid and ask volume at every price inside the bar, with imbalances marked and a delta strip underneath.

Open this example with its source

Building Footprints From Ticks

If your feed gives sided ticks rather than pre-aggregated levels:

import { footprintFromTicks, mergeFootprint } from "@ortex-charts/financial";
import { parseResolution } from "@ortex-charts/math";

const flow = footprintFromTicks(ticks, parseResolution("1"), 0.01, "America/New_York");
const barsWithFlow = mergeFootprint(bars, flow);

chart.setData(barsWithFlow);
chart.setFootprint(flow, { tickSize: 0.01 });

Ticks without a side fall back to the tick rule, which is the estimate every platform without aggressor data uses. Pass sided ticks for exact results.

footprintTotals(bar) gives { buy, sell, total, delta, poc } for one footprint bar, which is what a host-drawn summary row wants.

Volume Profile and Market Profile

const vp = chart.addVolumeProfile({ mode: "visible", tickSize: 0.01, splitDelta: true });

vp.setRange(fromTime, toTime);                                 // switch to a fixed range
vp.setOptions({ mode: "session" });                            // one profile per trading day
vp.setOptions({ kind: "tpo", tpoPeriodMs: 30 * 60_000 });      // market profile
vp.profiles();                                                 // the computed rows, POC and value area

Modes

  • visible recomputes for the bars currently on screen, cached per range and data version.
  • fixed uses range, which is what you set from a date-range drawing or a preset button.
  • session draws one profile per zoned trading day, anchored at the first bar of the day.

Kinds

  • volume counts volume at price: a volume profile.
  • tpo counts one letter per tpoPeriodMs period per row: a market profile. Letters are drawn when rows are at least nine pixels high and a histogram replaces them below that.

Accuracy

With footprint levels attached, volume lands in the row that contains its price and the buy-and-sell split is real. The computed profile reports exact: true.

Without them, each bar volume is spread evenly over the rows its range covers and split by where the close sits in that range. The profile reports exact: false and the tooltip says "estimated". This is the same approximation other libraries make; the difference is that it is labeled.

Point of Control and Value Area

The point of control is the row with the most volume, or the most letters for a market profile. The value area grows outward from it, taking the larger neighbor at each step, until it holds valueArea of the total, which defaults to 70 percent.

Options

Option Default What it does
kind "volume" volume or tpo.
mode "visible" visible, fixed or session.
range null [fromTime, toTime] for fixed.
rows 24 Number of price rows when tickSize is zero.
tickSize 0 Row height in price units; capped at 400 rows.
width 0.3 Widest bar as a fraction of the plot width, or of the session width.
position "right" Which edge the profile grows from.
valueArea 0.7 Share of volume inside the value area.
showPoc true Draw the point-of-control line.
showValueArea true Draw the value area more opaquely.
splitDelta false Draw buy and sell volume as two stacked segments.
showLabels true Print the row total when there is room.
color, buyColor, sellColor null Override the theme.
opacity Fill opacity of the profile.
tpoPeriodMs Market profile period; one letter per period.

Profiles draw below the series, so they never cover the candles. Hovering a row shows its price range, volume, buy and sell split, and delta.

Volume profile and market profile@ortex-charts/financial
Loading volume profile and market profile
Visible-range, fixed-range and per-session profiles with point of control and value area, plus a TPO letter profile.

Open this example with its source

Computing a Profile Without Drawing One

computeProfile is the pure function behind the primitive and is what the tests cover. Use it when you want the numbers for a table, an export or a rule. It takes bar columns, an inclusive bar-index range, options, and an optional map of footprint bars keyed by bar time.

import { computeProfile, type FootprintBar } from "@ortex-charts/financial";

const raw = chart.main.raw;
if (!raw || !("high" in raw)) throw new Error("no bars yet");

const byTime = new Map<number, FootprintBar>(flow.map((f) => [f.time, f]));

const profile = computeProfile(
  { time: raw.time, high: raw.high, low: raw.low, close: raw.close, volume: raw.volume },
  0,
  raw.length - 1,
  { rows: 40, valueArea: 0.7 },
  byTime,
);

console.log(profile.rows[profile.poc], profile.vaLow, profile.vaHigh, profile.exact);

The primitive already keeps its own computed results, so vp.profiles() is the cheaper route when a profile is on screen anyway.

The Three Indicators

Id Pane What it shows
delta Separate, histogram colored by sign, level line at zero Per-bar buy minus sell volume.
cvd Separate, line Cumulative delta.
rvol Separate, histogram, level line at one Volume over the mean of the previous length bars, the current bar excluded.

delta and cvd share a method input:

  • auto uses the real aggressor columns when they exist and estimates otherwise.
  • aggressor uses only real data and writes NaN where it is missing, so a gap in the histogram is a gap in the feed rather than a zero.
  • estimate always uses the tick-rule estimate, which is useful when you want the number other platforms would show.
chart.addIndicator("delta", { method: "aggressor" });
chart.addIndicator("cvd", { method: "auto" });
chart.addIndicator("rvol", { length: 50 });

indicators.barDelta(columns, method) is exported for hosts that want the numbers without a pane.

Delta, cumulative delta and relative volume@ortex-charts/financial
Loading delta, cumulative delta and relative volume
Order-flow indicators computed from aggressor volumes rather than guessed from the close.

Open this example with its source

Putting It Together

import { createFinancialChart, mergeFootprint } from "@ortex-charts/financial";

const chart = createFinancialChart(el, {
  timeZone: "America/New_York",
  resolution: "1",
  session: "0930-1600",
  data: mergeFootprint(bars, flow),
  seriesOptions: { priceFormat: { type: "price", precision: 2, minMove: 0.01 } },
});

chart.setFootprint(flow, { tickSize: 0.01, layout: "bidAsk", imbalanceRatio: 3 });
chart.addVolumeProfile({ mode: "session", tickSize: 0.01, splitDelta: true });
chart.addIndicator("delta", { method: "aggressor" });
chart.addIndicator("cvd");

What Is Not Here

  • Footprint is a primitive drawn over whichever main series is shown, not a series type of its own, so there is no delta-colored candle style yet.
  • Composite profiles across several symbols are not supported.
  • Profiles do not animate as developing profiles during bar replay.
  • The toolbar has no built-in profile toggle. Adding one is a ToolbarAction that calls addVolumeProfile.