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:

live
Rendering live chart…
Volume Profile with POC (gold) and value area on the right.
View example code
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;

Market Profile (TPO)

The 2.1.0 adds compact pixel letters, per-session split/unsplit and open/latest-price markers. Try the full demo and browse five theme screenshots.

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:

live
Rendering live chart…
TPO letter blocks, one hue per period, with POC / value area and the initial-balance bracket.
View example code
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;
💡

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

A footprint shows executed bid volume on the left and ask volume on the right at each price. Delta is ask volume minus bid volume. The slim candle beside a column shows its actual open, high, low and close; a pale outline identifies the highest-volume row (the point of control, or POC). Optional lines bound that bar’s 70% value area.

Profile and cluster ladder styles, per-candle stats, and the text-color methods below are available in 2.1.1. See the release notes.

Try the order-flow demo

Seven readable, one-minute bars replay a deterministic NIFTY simulation around 23,800. Executions use a one-point simulation grid and are grouped into 2-point price rows by default. Each simulated execution represents 1–16 lots of 65 quantity units. These are demo settings, rather than a statement of current exchange contract specifications. This is an illustration, not a live market feed. The demo opens paused so you can inspect the numbers before resuming.

Open full-size order-flow demo ↗
ControlWhat changes
StyleVariable-width Profile, rectangular Cluster ladder, or intensity-based Heatmap
ValuesBid × Ask, signed Delta, or total Volume at each price
Quantity / Lot sizeDisplays raw quantity or divides the displayed quantities by an editable lot size, initially 65
TextAuto contrast, bid/ask side, row delta, dominant side, diagonal imbalance, or volume strength
RowReaggregates the retained tape into 2, 4 or 8-point price rows
POC / Value area / CardsToggles the row outline, value-area lines, and per-candle metric cards
TableShows the optional bottom table, with metric names fixed at the left and values aligned to each candle; off by default
Table rowsAdds or removes Delta, Min delta, Max delta, Cumulative delta, Ask volume, Bid volume, and Volume independently
ThemeMidnight, Graphite, Classic neon, Ocean, or Ivory; also updates controls and tooltip
Pause / ResumeStops or resumes the synthetic replay
ResetRestarts the same deterministic session with the selected style, theme and grouping

These are footprint price rows; time-at-price TPO profiles are covered in Market Profile.

The table starts hidden. Its seven metrics are initially selected; changes to that selection are remembered while it is hidden. Selecting no metrics leaves the table empty. Cards and the table can be shown independently. On narrow screens, the demo shows fewer recent candles so every card remains readable; drag to inspect earlier columns. Enabling the table also leaves room for its fixed metric names.

Style and theme are independent and preserve the current data and viewport. Try the Classic neon ladder for bright red and green blocks on black, or the Ivory profile for a light presentation. Hover a price row or a stats card to inspect exact values.

Build a footprint

computeFootprint(time, classifiedTrades, tickSize, rowTicks?) builds one bar. FootprintAggregator incrementally builds bars from a stream. Both populate open, high, low, close, tradeCount, and rowSize alongside the price cells and delta. OHLC metadata uses actual execution prices, even when several ticks share a grouped row.

import { createChart, darkTheme } from 'openalgo-charts';
import { computeFootprint, Footprint } from 'openalgo-charts/profile';
 
const chart = createChart(container, {
  theme: darkTheme,
  timeScale: { maxBarSpacing: 260 }, // allow readable, wide footprint columns
  priceScale: { marginTop: 0.1, marginBottom: 0.25 },
});
const footprint = new Footprint({
  cellStyle: 'profile',
  displayMode: 'bidask',
  statsPosition: 'bar',
  statsRows: ['volume', 'delta', 'deltaPct'],
  showCandle: true,
  pocStyle: 'outline',
  showValueArea: true,
  valueAreaPercent: 0.7,
  buyColor: '#159d87',
  sellColor: '#cc374b',
  pocColor: '#ccd5e1',
});
chart.addPrimitive(footprint);
 
const bar = computeFootprint(1788960600, [
  { price: 6000.00, qty: 12, side: 'bid' },
  { price: 6000.25, qty: 19, side: 'ask' },
  { price: 6000.00, qty: 7, side: 'ask' },
], 0.25);
footprint.setBars([bar]);
 
// A transparent supporting series supplies the time axis and OHLC autoscale.
const clear = 'rgba(0,0,0,0)';
const price = chart.addSeries('candlestick', { style: {
  upColor: clear, downColor: clear, wickUpColor: clear, wickDownColor: clear,
  borderUpColor: clear, borderDownColor: clear,
} });
price.setData([{
  time: bar.time, open: bar.open!, high: bar.high!, low: bar.low!, close: bar.close!,
}]);
chart.timeScale.fitContent(1);
⚠️

Order flow requires classified executions: side: 'bid' means an aggressive seller hit the bid; side: 'ask' means an aggressive buyer lifted the ask. OHLCV bars or an LTP stream cannot recover this information. Use a feed’s aggressor classification, or a documented trade classifier; do not label every trade that is not known to be a sell as a buy.

Styles and display modes

cellStyle controls the shape without changing analytics. Profile draws variable-width horizontal bars around the central bid/ask divider. Ladder draws a pair of solid rectangular cells with a narrow central gap. Heatmap, the backwards-compatible default, uses volume intensity. The bid and ask values stay aligned in their respective columns. When rows or columns become too small, text is suppressed to avoid overlap.

OptionValues / default
cellStyle'heatmap' (default), 'profile', 'ladder'
displayMode'bidask' (default), 'delta', 'volume'
tableRowsMetric rows in an independent bottom table; [] (default) disables it
tableLabelWidthWidth of the fixed metric-name column; 150 pixels by default
statsRowsCard or legacy-footer metrics; [] by default
statsPosition'bottom' (default) for the legacy footer; 'bar' for cards below each candle
pocStyle'marker' (default) or 'outline'
showValueAreafalse by default
valueAreaPercent0.7 by default
buyColor, sellColor, textColorOptional palette overrides
pocColor, valueAreaColorReference-line colors
cvdOffsetStarting cumulative delta in raw quantity, default 0
volumeDivisorDivides displayed quantity values only; 1 (raw quantity) by default

Column width follows the chart’s bar spacing, capped by the available slot so adjacent bars remain separate. cellWidth requests a fixed width within that slot. widthFactor controls its relative width. tickSize, when supplied, overrides the effective price step used for row height. Otherwise computed bars supply rowSize, which is especially useful for grouping and sparse rows. imbalanceRatio, imbalanceThreshold, and stackedImbalances control diagonal imbalance highlights and brackets; pass stackedImbalances: 0 to hide the brackets.

Raw quantity and lots

Use Quantity → Lots to divide the displayed numbers by Lot size, initially 65 in this NIFTY simulation. Edit that positive whole-number setting to compare other divisors, or select Raw to see the underlying quantities. Try the NIFTY table in lots. The default is raw quantity; the lot size is a configurable demo setting.

footprint.setOptions({ volumeDivisor: 65 }); // display raw quantities as lots
footprint.setOptions({ volumeDivisor: 1 });  // return to raw quantity

The divisor applies consistently to bid/ask cells, volume, delta, min/max delta, cumulative delta, cards, and the bottom table. Delta percentages, execution counts, prices, bar widths, and color comparisons remain unchanged. Fractional lots retain significant digits. stats(), hover data, and supplied bars remain in raw quantity so changing the display cannot change analytics. Host-owned readouts and tooltips should divide the raw values by the same divisor; the demo does this and labels the selected units.

Text coloring

Text coloring is independent of the cell background. textColorMode supports these methods:

MethodMeaning
'contrast' (default)Readable text against the painted background
'side'Bid text uses the sell shade; ask text uses the buy shade
'delta'Both values follow that price row’s ask-minus-bid delta
'dominant'Highlights the larger side at the same price
'imbalance'Highlights diagonal bid/ask imbalances using the configured ratio and threshold
'volume'In bid/ask view, side volume relative to the peak side; in single-value views, total row volume relative to peak total volume

buyTextColor and sellTextColor let a palette use different foreground shades from its buyColor and sellColor backgrounds. textColor supplies the neutral text shade. Contrast adjustment keeps small numbers readable. The demo exposes each method while preserving its tape, statistics, style and theme. These are the supported methods in this renderer; this does not imply complete parity with other platforms’ Numbers Bars studies.

Per-bar statistics

tableRows chooses the metrics and their order in the bottom table. It is disabled by default ([]). The metric names remain fixed at the left while the value columns follow the chart’s bar centers during pan and zoom. statsRows independently configures the existing per-candle cards when statsPosition: 'bar'; it also defaults to []. The demo explicitly enables three cards while leaving the table off.

footprint.setOptions({
  tableRows: ['delta', 'minDelta', 'maxDelta', 'cvd', 'askVolume', 'bidVolume', 'volume'],
  tableLabelWidth: 150,
  statsPosition: 'bar',
  statsRows: ['volume', 'delta', 'deltaPct'],
});
// Choose fewer table metrics, or pass [] to hide the table.
footprint.setOptions({ tableRows: ['delta', 'cvd', 'volume'] });

Reserve a bottom price-scale margin for the table’s pixel height (tableRows.length * statsRowHeight) plus any cards. When fitting a viewport, leave room at the left for tableLabelWidth so its fixed labels do not cover the first data column. The demo adjusts this space when the table is enabled or its rows are changed.

Existing statsPosition: 'bottom' with statsRows continues to work as the legacy footer. Use the independent tableRows option when you want cards and a table together.

RowValue
volumeSum of bid and ask volume
deltaAsk volume minus bid volume
minDeltaLowest running intrabar delta, starting from zero at the bar open
maxDeltaHighest running intrabar delta, starting from zero at the bar open
askVolumeExecuted volume at the ask
bidVolumeExecuted volume at the bid
deltaPctDelta as a percentage of bar volume
cvdcvdOffset plus cumulative delta across the loaded bars
tradesActual execution count from tradeCount; unavailable legacy counts display

footprint.stats() exposes the same calculations. Its trades value is null when an older manually assembled bar has no count metadata; occupied price levels are not a trade count. minDelta and maxDelta also return null and display when legacy bars lack intrabar extrema. They describe the running execution-by-execution delta path, not the largest or smallest price-row delta; aggregated cells alone cannot recover them. Replaying the same retained executions with a different price grouping preserves both extrema. Legacy bars without OHLC remain renderable, but an actual candle requires OHLC metadata.

Streaming and grouping

Retain classified ticks if you want to change price grouping later: changing a renderer’s options cannot recreate the original executions from aggregated cells. Rebuild the aggregator with the new rowTicks and replay those retained ticks. The demo follows this path.

import { FootprintAggregator } from 'openalgo-charts/profile';
 
const aggregator = new FootprintAggregator(
  { mode: 'interval', seconds: 60 },
  0.25, // instrument tick size
  2,    // two ticks per row: 0.50 price units
);
const bars = [];
let cvdOffset = 0;
 
function onClassifiedTrade(tick) {
  const update = aggregator.onTick(tick);
  if (update.isNew) {
    bars.push(update.bar);
    if (bars.length > 100) cvdOffset += bars.shift().delta;
  } else {
    bars[bars.length - 1] = update.bar;
  }
  footprint.setOptions({ cvdOffset });
  footprint.setBars(bars.slice());
  // Also update the supporting price series from each bar's OHLC metadata.
}

Preserve dropped bars’ delta in cvdOffset to keep session CVD continuous when trimming a rolling window. Reset both the window and offset to start a new session. Feed executions in nondecreasing timestamp order: late ticks are rejected before mutating the current bar. Timestamp ties remain together when a tick-count or volume threshold would otherwise create a second bar with the same timestamp.

Runtime restyling and hit-testing

setOptions(partial) merges and repaints without replacing the bars:

footprint.setOptions({ cellStyle: 'ladder', displayMode: 'delta' });
footprint.setOptions({ buyColor: '#32e600', sellColor: '#ff2424', textColor: '#ffffff' });

hitTest(x, y) and hoverAt(x, y) use pane-local chart coordinates. hoverAt returns { time, price, cell, stats }; price and cell are null over a stats card or table. Use it to build a host-owned tooltip:

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

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, rowSize?, threshold = 0) 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 row below; a sell imbalance fires when bid volume at P is at least ratio times the ask volume at the cell one row above. rowSize is the effective grouped price step. Missing rows are not bridged. Without an explicit step, the minimum observed spacing is inferred; this cannot distinguish uniformly missing rows from a coarser grid. threshold is the minimum qualifying side volume. Positive volume against zero can qualify, while zero against zero does not. Both sides can be imbalanced at the same price.

const bar = computeFootprint(time, trades, tickSize);
const imb = diagonalImbalances(bar.cells, 3, bar.rowSize);
const imb4 = diagonalImbalances(bar.cells, 4, bar.rowSize, 10); // ratio 4, minimum volume 10
// 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, rowSize?, threshold = 0) builds on diagonalImbalances and finds runs of minStack or more consecutive same-side imbalances within one bar’s cell stack. Buy and sell runs are tracked independently, and neither crosses a missing row. Returns StackedImbalance[], each with { startPrice, endPrice, side, count }.

const stacked = stackedImbalances(bar.cells, 3, 3, bar.rowSize);
// 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 on the timezone option’s calendar, which defaults to Asia/Kolkata and so matches NSE/BSE intraday calendars out of the box. Set it to America/New_York for a US instrument, or pass a window that names its own zone. See Timezones.

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