DocumentationProfiles & Order Flow

Profiles & order flow

The profile tier (openalgo-charts/profile) adds market-structure studies: Volume Profile, Market Profile (TPO), Footprint, and cumulative delta.

Volume Profile

computeVolumeProfile(bars, tickSize, valueAreaPct) returns buckets plus the point of control (POC) and value-area high/low. Render it with the HorizontalProfile primitive:

const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 220, 3600);
chart.addSeries('candlestick').setData(bars);

const vp = lib.computeVolumeProfile(bars, 0.5, 0.7);
chart.addPrimitive(new lib.HorizontalProfile({
// HorizontalProfile draws { price, value } buckets; map volume -> value.
buckets: vp.buckets.map(function (b) { return { price: b.price, value: b.volume }; }),
poc: vp.poc, vah: vp.vah, val: vp.val,
width: 150, side: 'right', barColor: '#3b5168', vaColor: '#4a6fa5',
}));

chart.timeScale.fitContent(bars.length);
return chart;
live
Rendering live chart…
Volume Profile with POC (gold) and value area on the right.

Market Profile (TPO)

A real market profile draws letter blocks — one letter per time period, stacked at every price that period traded. computeMarketProfile builds them and the MarketProfile primitive renders them:

const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 160, 1800); // ~30-min bars
chart.addSeries('candlestick').setData(bars);

const mp = lib.computeMarketProfile(bars, {
tickSize: 0.1,
rowTicks: lib.rowTicksFor(0.5, 0.1),  // 0.5-point rows
session: 'composite',
blockMinutes: 120,
});
chart.addPrimitive(new lib.MarketProfile(mp, { colorMode: 'period', letterWidth: 9 }));

chart.timeScale.fitContent(bars.length);
return chart;
live
Rendering live chart…
TPO letter blocks, one hue per period, with POC / value area and the initial-balance bracket.
💡

There is also a simpler computeTpo(bars, periodBars, tickSize, valueAreaPct) that returns { poc, vah, val } counts in volume-profile shape, for when you want a plain histogram from the HorizontalProfile primitive rather than letters. See Market Profile (TPO) for the full renderer.

Footprint & cumulative delta

Footprint shows per-price bid vs ask volume with diagonal-imbalance highlighting. Build each bar with computeFootprint(time, classifiedTrades, tickSize) and render with the Footprint primitive.

Cells are filled proportionally to volume rather than outlined, so a column reads as a heat ladder at a glance; imbalanced cells fill saturated instead of gaining a border, and runs of consecutive same-side imbalances get a bracket down the edge. Column width is derived from the chart’s bar spacing unless you pin cellWidth, and when rows get shorter than minTextHeight the numbers drop out and the column degrades to a pure heatmap — so zooming out never turns into unreadable overlap.

The demo below replays a classified tape into a FootprintAggregator: ticks arrive a few at a time, the forming bar updates in place, and the tape loops when it runs out. That is the same path a live WebSocket trade feed takes.

const chart = lib.createChart(el, { priceAxisWidth: 68 });
const tick = 0.5;
const price = chart.addSeries('candlestick');

// Deterministic tape, so the demo replays the same session every time.
let seed = 20240719;
const rnd = function () { seed = (seed * 1103515245 + 12345) & 2147483647; return seed / 2147483647; };

let mid = 100, drift = 0, t = 1700000000;
const tape = [];
for (let i = 0; i < 1400; i++) {
drift += (rnd() - 0.5) * 0.05;
drift = Math.max(-0.3, Math.min(0.3, drift));
drift -= (mid - 100) * 0.02;                       // mean-revert: keep the ladder shallow
const buy = rnd() < 0.5 + drift;
if (rnd() < 0.28) mid += (buy ? 1 : -1) * tick;
tape.push({
  time: t + i,
  price: buy ? mid + tick / 2 : mid - tick / 2,
  qty: Math.round(Math.pow(rnd(), 3) * 400 + 5),   // heavy-tailed: a few prints matter
  side: buy ? 'ask' : 'bid',
});
}

const fp = new lib.Footprint({
tickSize: tick,
imbalanceRatio: 3,
stackedImbalances: 3,
statsRows: ['volume', 'delta', 'deltaPct', 'cvd'],
});
chart.addPrimitive(fp);

let agg, bars, candles, cursor;
const reset = function () {
agg = new lib.FootprintAggregator({ mode: 'interval', seconds: 30 }, tick);
bars = []; candles = []; cursor = 0;
};
reset();

const step = function () {
for (let n = 0; n < 6 && cursor < tape.length; n++) {
  const tk = tape[cursor++];
  const u = agg.onTick(tk);
  if (u.isNew) {
    bars.push(u.bar);
    candles.push({ time: u.bar.time, open: tk.price, high: tk.price, low: tk.price, close: tk.price, volume: tk.qty });
    if (bars.length > 12) { bars.shift(); candles.shift(); }
  } else {
    bars[bars.length - 1] = u.bar;
    const c = candles[candles.length - 1];
    c.high = Math.max(c.high, tk.price); c.low = Math.min(c.low, tk.price);
    c.close = tk.price; c.volume += tk.qty;
  }
}
if (cursor >= tape.length) reset();
price.setData(candles.slice());
fp.setBars(bars.slice());
chart.timeScale.fitContent(candles.length);
};

step();
const timer = setInterval(step, 420);               // slow enough to watch a bar build
const teardown = chart.destroy.bind(chart);
chart.destroy = function () { clearInterval(timer); teardown(); };
return chart;
live
Rendering live chart…
A replayed tape: the rightmost column is the forming bar, updating in place as ticks arrive.

For live data, aggregate the stream instead of building bars by hand:

import { FootprintAggregator } from 'openalgo-charts/profile';
 
const fa = new FootprintAggregator({ mode: 'interval', seconds: 60 }, 0.05);
ws.onTrade((t) => {
  const u = fa.onTick({ time: t.timeSec, price: t.price, qty: t.qty, side: t.atBid ? 'bid' : 'ask' });
  if (u) footprint.setBars(/* accumulated bars */);
});
⚠️

Footprint and order flow require a feed that classifies each trade as hitting the bid or the ask. A plain OHLCV or LTP stream cannot produce them - the demo above fakes the classification for illustration.

Display modes

displayMode switches what a column shows without touching the underlying bars:

ModeColumnsReads as
bidask (default)two — bid on the left, ask on the rightclassic footprint; diagonal imbalances are visible
deltaone, signednet aggression per price; green above zero, red below
volumeone, unsignedwhere trade actually happened, side ignored

Stats table

statsRows renders a per-bar table under the columns, one row per metric, in the order given. Each cell is tinted by its own strength, so a run of heavy positive delta reads as a green block. Pass [] to hide the table.

RowValue
volumetotal bar volume
deltaaskVol - bidVol summed over the bar
deltaPctthat delta as a percentage of bar volume
cvdcumulative delta — a running total across all loaded bars
tradesnumber of prints in the bar

Runtime restyling and hit-testing

setOptions(partial) merges and repaints, so a toolbar can drive the primitive live:

fp.setOptions({ displayMode: 'delta', imbalanceRatio: 5, statsRows: ['volume', 'cvd'] });

hitTest(x, y) and hoverAt(x, y) map a pointer position back to the bar and price row under the cursor — enough to build a hover inspector without the library owning any DOM. hoverAt returns { time, price, cell, stats }, where cell is null when the pointer is over the stats table rather than a price row:

chart.on('crosshair:move', (e) => {
  const hit = e.point ? fp.hoverAt(e.point.x, e.point.y) : null;
  tooltip.hidden = hit === null;
  if (hit === null) return;
  const cell = hit.cell ? `bid ${hit.cell.bidVol} x ask ${hit.cell.askVol}` : '';
  tooltip.textContent = `${cell}  vol ${hit.stats.volume}  Δ ${hit.stats.delta}`;
});
💡

examples/orderflow/index.html in the repo wires all of this to a live synthetic tape — the forming bar updates in place as ticks arrive, which is the same path a real WebSocket trade feed takes.

Footprint analytics

Three pure functions operate on footprint data without any rendering side-effects. They are useful for custom overlays, alerts, or server-side analysis.

import {
  computeFootprint,
  diagonalImbalances,
  cumulativeDelta,
  stackedImbalances,
} from 'openalgo-charts/profile';

diagonalImbalances

diagonalImbalances(cells, ratio = 3) scans the FootprintCell[] of one bar (sorted high to low, as returned by computeFootprint) and returns Imbalance[]. A buy imbalance fires when ask volume at price P is at least ratio times the bid volume at the cell one tick below; a sell imbalance fires when bid volume at P is at least ratio times the ask volume at the cell one tick above.

const bar = computeFootprint(time, trades, tickSize);
const imb = diagonalImbalances(bar.cells);      // ratio defaults to 3
const imb4 = diagonalImbalances(bar.cells, 4);  // stricter threshold
// imb: Array<{ price: number; side: 'buy' | 'sell' }>

cumulativeDelta

cumulativeDelta(bars) takes a FootprintBar[] and returns a number[] of the same length. Each element is the running total of bar.delta through that index. Bar delta is computed by computeFootprint as sum(askVol - bidVol) across all cells in the bar.

const cd = cumulativeDelta(footprintBars);
// cd[i] is the cumulative net delta through bar i

stackedImbalances

stackedImbalances(cells, ratio = 3, minStack = 3) builds on diagonalImbalances and finds runs of minStack or more consecutive same-side imbalances within one bar’s cell stack. Returns StackedImbalance[], each with { startPrice, endPrice, side, count }.

const stacked = stackedImbalances(bar.cells, 3, 3);
// Array<{ startPrice: number; endPrice: number; side: 'buy' | 'sell'; count: number }>

Combined example

// sessionTrades: Array<{ time: number; trades: ClassifiedTrade[] }>
const bars = sessionTrades.map((g) => computeFootprint(g.time, g.trades, 0.5));
 
const cd = cumulativeDelta(bars);
 
bars.forEach((bar, i) => {
  const stacked = stackedImbalances(bar.cells, 3, 3);
  if (stacked.length > 0) {
    console.log(`Bar ${i} cd=${cd[i].toFixed(0)}: ${stacked.length} stacked zone(s)`);
  }
});

Price bucketing utilities

bucketPrice and priceBuckets underpin every profile compute function. They are useful when building a custom renderer or aligning prices from two data sources to the same tick grid.

bucketPrice

bucketPrice(price, step) snaps any raw price to the nearest multiple of step and rounds to 8 decimal places to avoid floating-point drift across repeated additions.

import { bucketPrice } from 'openalgo-charts/profile';
 
bucketPrice(100.003, 0.05); // -> 100.0
bucketPrice(99.978,  0.05); // -> 100.0
bucketPrice(22349.7, 0.5);  // -> 22349.5

priceBuckets

priceBuckets(low, high, step) returns the inclusive list of all bucketed prices spanning [low, high]. The first and last elements are bucketPrice(low, step) and bucketPrice(high, step).

import { priceBuckets } from 'openalgo-charts/profile';
 
priceBuckets(99.9, 100.2, 0.1);
// -> [99.9, 100.0, 100.1, 100.2]

This is handy when pre-allocating one row per price level in a custom canvas renderer, or when merging footprint data from two sources onto a shared price axis.

TPO: simple vs rich API

The profile tier exports two functions for TPO computation. The runnable example above uses computeTpo, the simpler one. computeMarketProfile is the richer alternative for multi-session data.

computeTpo (simple, single session)

computeTpo(bars, periodBars, tickSize, valueAreaPercent = 0.7, ibPeriods = 2) treats the entire input array as one profile. It groups consecutive bars into fixed-size buckets of periodBars bars each, counts how many buckets touched each price, then computes POC, value area, and initial balance. Return shape:

interface TpoResult {
  buckets: { price: number; count: number }[]; // sorted high -> low
  poc:     number;
  vah:     number;
  val:     number;
  ib:      { high: number; low: number };      // initial balance price range
}

Use computeTpo when you have already sliced your bars to a single session or composite window, or when you only need POC, value area, and initial balance with no per-period letters.

computeMarketProfile (rich, multi-session)

computeMarketProfile(bars, options?) accepts a Partial<MarketProfileOptions> and splits the bar array into calendar sessions before building one profile per session. Options and their defaults:

import { computeMarketProfile } from 'openalgo-charts/profile';
 
const result = computeMarketProfile(intradayBars, {
  tickSize:              0.5,
  session:               'day',  // 'day' | 'week' | 'month' | 'composite'
  blockMinutes:          30,     // one TPO letter per block
  valueAreaPercent:      0.7,
  initialBalancePeriods: 2,
});
 
result.sessions.forEach((s) => {
  console.log(`poc=${s.poc}  vah=${s.vah}  val=${s.val}`);
  console.log(`IB ${s.initialBalance.low} - ${s.initialBalance.high}`);
  console.log(`single prints: ${s.singlePrints.join(', ')}`);
  console.log(`poor high: ${s.poorHigh}  poor low: ${s.poorLow}`);
  s.levels.slice(0, 3).forEach((l) =>
    console.log(`  ${l.price}  [${l.letters}]  vol=${l.volume.toFixed(0)}`),
  );
});

Each MarketProfileLevel in levels (sorted high to low) carries:

FieldTypeDescription
pricenumberBucketed price.
countnumberDistinct TPO periods at this price.
lettersstringPeriod letters that touched this price, e.g. "ABF".
volumenumberBar volume distributed evenly across the price range.

Each MarketProfileSessionResult also exposes poorHigh / poorLow booleans (the session extreme printed in more than one period, indicating a weak rejection) and singlePrints (prices that printed in exactly one period away from the session extremes). Session boundaries are computed in IST (UTC+5:30), matching NSE/BSE intraday calendars.

The MarketProfile canvas primitive exported from the profile tier consumes MarketProfileResult directly and handles the letter-based display.

tpoLetter

tpoLetter(period) maps a zero-based period index to the letter used in the classic TPO display. Periods 0-25 map to A-Z and 26-51 map to a-z, then the cycle repeats.

import { tpoLetter } from 'openalgo-charts/profile';
 
tpoLetter(0);  // -> 'A'
tpoLetter(25); // -> 'Z'
tpoLetter(26); // -> 'a'
tpoLetter(51); // -> 'z'
tpoLetter(52); // -> 'A'  (wraps back)

computeMarketProfile calls this internally. Use it directly only when building a custom renderer that needs to label individual cells by period.

When to use each

ScenarioFunction
Single pre-sliced session or fixed composite windowcomputeTpo
Multi-day intraday data, automatic session splittingcomputeMarketProfile
Need TPO letters, single prints, or poor high/low flagscomputeMarketProfile
Feeding the MarketProfile canvas primitivecomputeMarketProfile