Order flow and market profile

A candle tells you where price went. Order flow tells you who took it there. A footprint chart opens each bar and shows how much traded at every price, split by the side that started each trade: a buyer lifting the offer (the ask) or a seller hitting the bid. Read that way, a bar shows where buyers or sellers pressed, where their orders were soaked up, and where price moved through a level with almost nothing traded.

Market profile steps back to the whole session. A TPO profile prints one letter at each price for every half hour that traded there, so the shape of the day (where it spent most of its time, its value area and its point of control) stands out at a glance. A volume profile does the same with traded volume.

Every chart below runs the real openalgo-charts/profile tier in your browser on a seeded, simulated tape, so each visit shows the same bars. Scroll over a chart to zoom and drag to pan. The footprints share one tape of fourteen one-minute bars from 09:15, so a bar named in one example can be found in all of them.

Reading a footprint: bid by ask

Each column is one bar. Every row is a price, with the volume sold into the bid on the left and the volume bought from the ask on the right, and a slim candle beside it. The outlined row is the point of control (POC), the price with the most volume in that bar. Wider rows group neighbouring prices together: try 2 or 4 points to see a busy bar condense into a few readable rows.

Row size
Bid on the left, ask on the right · outlined row is the POC
Loading live chart
const aggregator = new FootprintAggregator({ mode: 'interval', seconds: 60 }, tickSize, rowTicks);
const bars = [];
for (const print of tape) {                 // { time, price, qty, side: 'bid' | 'ask' }
  const { bar, isNew } = aggregator.onTick(print);
  if (isNew) bars.push(bar); else bars[bars.length - 1] = bar;
}
const footprint = new Footprint({ tickSize: tickSize * rowTicks, displayMode: 'bidask', showPoc: true, pocStyle: 'outline' });
chart.addPrimitive(footprint);
footprint.setBars(bars);

Imbalances and stacked imbalances

Imbalances compare diagonally, because at any moment the best bid sits one row below the best ask: the ask volume at a price is weighed against the bid volume that stood one row under it. An ask cell is a buying imbalance when it is at least the ratio times the bid one row below it; a bid cell is a selling imbalance against the ask one row above. Three or more in a row on one side form a stacked imbalance, drawn as a bracket: a strong sign one side was in control.

A row inside a bar that never traded counts as zero volume, so the cell beside it is compared with zero, and any positive volume against zero is an imbalance (new in 2.6.4). The 09:16 bar shows it: price jumped two points at a time on its way up, leaving 24004, 24006 and 24008 untraded, and every traded row beside a gap lights up.

Imbalance ratio
Rows
Loading live chart
const footprint = new Footprint({
  displayMode: 'imbalance',   // plain cells, imbalanced ones filled
  imbalanceRatio: 3,          // ask at a row against the bid one row lower, and the reverse
  stackedImbalances: 3,       // three in a row on one side draws a bracket
  showEmptyRows: true,        // draw a row the price jumped over as 0 x 0
});

One value per row: the display modes

When bid and ask side by side is more than you need, print one number per row. Delta is ask minus bid, Delta % is that as a share of the row’s volume, and Volume is the total. Delta-shaded volume (new in 2.6.4) prints the row’s volume on a cell coloured by the sign of its delta and shaded by its size, so you read size and direction together: a deep green cell was bought hard, a deep red one sold hard, and a pale one was close to balanced. Volume and delta prints both numbers.

Values per row
Loading live chart
footprint.setOptions({ displayMode: 'deltaShadedVolume' });

Cell styles: heatmap, profile and ladder

The style changes only the drawing, never the numbers. Heatmap shades each cell by its volume, Profile draws bars that grow out from the middle (bid to the left, ask to the right) so a bar reads as a small histogram, and Ladder draws square, high-contrast cells. Shading against the busiest row on screen, instead of within each bar, makes the heaviest clusters on the chart stand out.

Cell style
Shade against
Loading live chart
footprint.setOptions({ cellStyle: 'profile', intensityScope: 'bar' });

Order flow signals

Each layer marks one pattern that traders look for, and each is off until you ask for it. A large-volume cell (bigOrderMultiple) is a single cell several times the average near the bar’s high or low, often read as a large resting order, though the chart only sees executed volume. Absorption bands rows near the high or low where both bid and ask volume were heavy. Multiple nodes highlights a POC that two bars share. A delta flip marks a bar whose delta turned after a run of the other sign at a swing. Divergence boxes the delta label when it disagrees with the candle, such as the up bar at 09:18 that closed with net selling. Unfinished auctions draw a dash-dot line at a high or low that traded on both sides, a level price often revisits.

Layers
Loading live chart
footprint.setOptions({
  bigOrderMultiple: 3,        // a print of 3x the average cell near the bar's high or low
  absorptionMultiple: 2,      // bid and ask both at 2x their average cell
  multipleNodeBars: 2,        // two bars in a row share a point of control
  flipBars: 2,                // delta turns after two bars of the other sign
  showDeltaLabels: true, markDivergence: true,
  showUnfinishedAuctions: true,
});
const { bars, auctions } = footprint.signals();   // the same findings, for alerts or a scanner

Zones and zone alerts

With zones on, stacked imbalances, shared POCs and absorption extend to the right as price levels until price deals with them. zoneKinds picks which of the three to draw and alert on, and zoneLookback lets zones start only in the newest bars, so an old, crowded chart stays readable. onZoneAlert calls you once per zone when the newest bar’s range comes back to it after it became active (a jump across the zone counts, as it would trigger a stop there), the moment to raise an alert or play a sound. Zones touched in the bars a chart first loads are history, not news, so they are not reported. Press Play to run the tape forward and watch the log.

Tape
Zone kinds
Zones start in
Loading live chart

Zone alerts

  1. Press Play to run the tape. Alerts appear here.
const footprint = new Footprint({
  showZones: true,
  zoneKinds: ['stacked', 'node', 'absorption'],
  zoneLookback: 0,   // zones may start in any loaded bar
  stackedImbalances: 3, multipleNodeBars: 2, absorptionMultiple: 2,
});
const unsubscribe = footprint.onZoneAlert(({ zone, time }) => {
  log(time, zone.kind, zone.low, zone.high);   // once per zone, when price comes back to it
});
const { bar, isNew } = aggregator.onTick(print);   // then footprint.setBars(bars) as each print lands

The statistics table

A table under the footprints gives each bar its numbers: the session letter (here one per five minutes, so the fourteen bars run from A to C), ask and bid volume, delta (ask minus bid), delta % (delta over volume), the highest and lowest the running delta reached inside the bar, cumulative delta and cumulative delta per volume (running totals across bars, the second a ratio between -1 and 1), volume and cumulative volume. Signed rows are shaded green when positive and red when negative, more strongly the larger the value.

Session letter, volume by side, delta and the running totals for each bar
Loading live chart
const footprint = new Footprint({
  tableRows: ['session', 'askVolume', 'bidVolume', 'delta', 'deltaPct', 'maxDelta', 'minDelta',
              'cvd', 'cvdRatio', 'volume', 'cumVolume'],
  sessionWindow: TRADING_HOURS.india,   // letters and resets count from this session's open
  cvdReset: 'session',                  // cumulative delta and volume restart at each open
  tpoMinutes: 5,                        // one letter per five minutes, so 14 bars span A to C
});
// Leave room under the columns for the table.
chart.setPriceScaleOptions({ marginBottom: (rows * rowHeight + 24) / plotHeight });

Volume profile of the visible bars

A profile against the left edge adds up every visible bar’s volume at each price, split into ask and bid, so you can see where the whole visible range traded most without reading forty columns. It is rebuilt as you pan and zoom.

Drag the chart to the left for newer bars: the profile is rebuilt from the bars on screen
Loading live chart
const footprint = new Footprint({
  showVisibleProfile: true,     // volume at price of the bars on screen, ask and bid split
  profileWidth: 110,
  profileAskColor: '#159d87',
  profileBidColor: '#cc374b',
});

Footprint colour themes

FOOTPRINT_THEMES (new in 2.6.4) holds six ready colour sets. Each is a plain options patch you spread under your own choices, and covers cells, imbalances, candles, the POC, the signal layers, the visible profile, session letters and the table. A theme never sets the chart background: Daylight is made for a light chart and the other five for dark ones, so this example sets a matching background as you switch.

Made for a dark chart
Loading live chart
import { Footprint, FOOTPRINT_THEMES } from 'openalgo-charts/profile';

const footprint = new Footprint({ ...FOOTPRINT_THEMES.ocean, displayMode: 'bidask' });
footprint.setOptions(FOOTPRINT_THEMES.daylight);   // switch later without rebuilding

Trade size filter and lot display

A feed reports raw quantity, while traders often think in lots. volumeDivisor divides every printed quantity, so the same bars read in lots or units without rebuilding. minTradeSize on the aggregator counts only prints of at least that size, to see what the larger participants did. Smaller prints are not thrown away: they still open their row (it shows zero) and still set the bar’s open, high, low and close, so each column keeps its full range.

Show quantities as
Loading live chart
// The feed reports quantity; one lot is 25.
const aggregator = new FootprintAggregator({ mode: 'interval', seconds: 60 }, tickSize, 1, { minTradeSize: 0 });
const footprint = new Footprint({ volumeDivisor: 25 });   // print lots

Market profile (TPO)

Each session is a column of letters: A for the first half hour, B for the second and so on, one letter at every price that period traded. The widest row is the POC, the shaded band is the value area (where 70% of the letters sit), and the line at the left of each profile marks the initial balance, the range of the first hour. A lone letter at an extreme, a single print, shows price moved through quickly. Colour by value area or by volume to read the same day another way, or split the periods to see how the day built over time.

Row
Colour by
Show
Loading live chart
const result = computeMarketProfile(bars, {
  tickSize: 0.05, rowTicks: 80,        // 4-point rows
  session: 'day', blockMinutes: 30,      // one letter per 30 minutes: A, B, C ...
  valueAreaPercent: 0.7, initialBalancePeriods: 2,
});
chart.addPrimitive(new MarketProfile(result, {
  colorMode: 'period', showInitialBalance: true, showVolumeProfile: true, split: false,
}));

Volume profile by session

A volume profile is the market profile’s volume twin: a horizontal histogram of how much traded at each price, with the POC and value area marked. Build one per day, or one over the whole range for a longer view. This chart has only bars, not classified trades, so its buy and sell split takes each bar’s direction as its side; a footprint is the place for real bid and ask volume.

Sessions
Bars show
Loading live chart
const result = computeVolumeProfileSessions(bars, { tickSize: 2, session: 'day', valueAreaPercent: 0.7 });
chart.addPrimitive(new VolumeProfile(result, {
  displayMode: 'buySell', side: 'right', showPocLabel: false, showValueAreaLabels: false,
}));

The examples use the public API exactly as a host would. For every option, see the profiles and order flow guide, the market profile guide and the volume profile guide.