DocumentationMarket Profile (TPO)

Market Profile (TPO)

Market Profile organizes a session’s trade into Time Price Opportunities (TPOs): the session is split into fixed-length time periods (one letter each), and every price a period traded at earns that letter. Stacking the letters sideways draws a distribution that shows where the market spent its time - the fat middle is acceptance, the thin edges are rejection.

It is two pieces: computeMarketProfile (pure analytics, in the profile tier) and the MarketProfile primitive that renders the result on the chart.

import { computeMarketProfile, MarketProfile } from 'openalgo-charts/profile';
 
const result = computeMarketProfile(bars, { tickSize: 0.05, session: 'day', blockMinutes: 30 });
chart.addPrimitive(new MarketProfile(result));

Concepts

  • Session - how bars are grouped into profiles: day, week, month, or composite (one profile over everything). Grouping is by the IST calendar.
  • Period / letter - each blockMinutes slice of a session is one TPO letter (A, B, … Z, then a..z).
  • POC - Point of Control, the price with the most TPOs (the longest row).
  • Value Area (VAH / VAL) - the band around the POC holding valueAreaPercent of all TPOs.
  • Initial Balance - the price range of the opening initialBalancePeriods periods.
  • Single prints - lone TPOs away from the extremes (quick moves that left a gap in time).
  • Poor high / low - a session extreme printed by more than one TPO (a weak, often revisited, edge).

A profile in a few lines

The renderer overlays the price series, so add a price series first (it provides the time axis). Here is a composite profile - one distribution over all the bars, letters packed from the left:

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

const mp = lib.computeMarketProfile(bars, { tickSize: 1, session: 'composite', blockMinutes: 30 });
chart.addPrimitive(new lib.MarketProfile(mp));

chart.timeScale.fitContent(bars.length);
return chart;
live
Rendering live chart…
Composite TPO: letters packed left, POC (gold) and value area with a faint fill.

Sessions

Switch session to split the same bars into one profile per day / week / month. Each session’s profile is drawn under its own bars, so you can read structure across time:

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

const mp = lib.computeMarketProfile(bars, { tickSize: 1, session: 'day', blockMinutes: 60, valueAreaPercent: 0.7 });
chart.addPrimitive(new lib.MarketProfile(mp)); // IB bracket on the left of each session

chart.timeScale.fitContent(bars.length);
return chart;
live
Rendering live chart…
Day sessions: a separate profile per IST day, each with its own POC, value area, and initial-balance bracket.

Color modes & volume sub-profile

colorMode is valueArea (default), uniform, or heat (opacity scales with TPO count). Turn letters off for a solid histogram, and overlay a volume-at-price sub-profile:

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

const mp = lib.computeMarketProfile(bars, { tickSize: 1, session: 'composite', blockMinutes: 30 });
chart.addPrimitive(new lib.MarketProfile(mp, {
colorMode: 'heat',
showLetters: false,        // solid blocks instead of letters
showVolumeProfile: true,   // volume-at-price on the right edge
volumeProfileWidth: 70,
}));

chart.timeScale.fitContent(bars.length);
return chart;
live
Rendering live chart…
Heat coloring by TPO count, solid blocks, with a volume sub-profile.

computeMarketProfile options

computeMarketProfile(bars, options): MarketProfileResult
OptionDefaultDescription
tickSize0.05the instrument’s real tick
rowTicks1ticks per TPO row — see Row size
session'day''day' / 'week' / 'month' / 'composite'
blockMinutes30period length (one letter per period)
valueAreaPercent0.7fraction of TPOs inside the value area (0..1)
initialBalancePeriods2opening periods that form the Initial Balance
windownonerestrict each session to a SessionWindow; see Sessions
compositeSessions1merge N consecutive sessions into one profile
tailEdges0min run of single prints that promotes a buying / selling tail

Row size

TPO row height is tickSize * rowTicks. Keeping the two separate means widening rows never means lying about the instrument’s tick — which matters, because the tick is what imbalance and single-print logic are counted on.

rowTicks is the multiplier a trader already thinks in. Nifty trades in 0.1 and you want 2-point rows, so the multiplier is 2 / 0.1 = 20:

computeMarketProfile(bars, { tickSize: 0.1, rowTicks: 20 });      // 2-point rows
computeMarketProfile(bars, { tickSize: 0.1, rowTicks: rowTicksFor(2, 0.1) });  // same thing

rowTicksFor(rowSize, tickSize) does the division for you and floors at 1.

💡

The order flow / footprint side takes the same multiplier, so a chart’s bricks and its profile rows can share one grid: computeFootprint(time, trades, 0.1, 20) and new FootprintAggregator(tf, 0.1, 20). See Profiles & Order Flow.

Session windows

A window drops bars outside a trading session and anchors period A to the window’s open — not to whatever bar happened to arrive first, which otherwise shifts every letter. Windows that cross midnight are handled as one session, not two halves.

import { computeMarketProfile, TRADING_HOURS } from 'openalgo-charts/profile';
 
computeMarketProfile(bars, { window: TRADING_HOURS['india'] });          // 09:15–15:30 IST
computeMarketProfile(bars, { window: { startMinute: 9 * 60 + 15, endMinute: 15 * 60 + 30, name: 'RTH' } });

Built-ins: all-hours, india, asia, london, new-york, us-regular — all in IST minutes-from-midnight, since the library’s calendar is IST (§5.3).

MarketProfile primitive options

Pass in the constructor (new MarketProfile(result, options)) or update live with profile.setOptions({ ... }).

OptionDefaultDescription
blockDisplay'auto'auto / blocks+letters / letters / blocks — see Letters and bricks
minLetterHeight / letterFade7 / 4row height (px) where letters give way to bricks, and the fade band
letterWidth8width of one TPO column, px
font10letter font size, px (auto-shrinks to fit short rows)
colorMode'period'period / valueArea / count / volume / uniform
periodColors12 huespalette for colorMode: 'period'
opacity / outsideVaOpacity0.92 / 0.45block opacity, and dimming outside the value area
splitfalsegive each period its own column slot instead of packing rows left
profileSpacing4gap between session profiles, px
showPoc / pocColor / pocThickness / showPocLabeltrue / gold / 2 / truePoint of Control
showValueArea / vahColor / valColor / showValueAreaLabelstrueVAH & VAL lines + labels
fillValueArea / valueAreaFillColor / valueAreaFillOpacitytrue / teal / 0.07shade the value-area band
showInitialBalance / ibColortrueopening-range bracket
showSinglePrints / singlePrintColortruemark lone TPOs
showTails / buyTailColor / sellTailColortruebuying / selling tails (needs tailEdges)
showPoorHighLow / poorColorfalseflag a session extreme that printed more than one TPO
showNakedLevels / nakedColorfalseextend untested prior POC / VAH / VAL to the right edge
showDevelopingPoc / showDevelopingVafalsetrace POC / value area as they developed
showTpoCounts / countColorfalseper-level TPO count column
showSessionLabel / showDayType / showOpenTypetrue / false / falseheader labels
showVolumeProfile / volumeProfileWidth / volumeProfileSide / showVolumeValuesfalse / 60 / right / falsevolume-at-price sub-profile
zOrdertopdraw over (top) or behind (bottom) the series

Letters and bricks

A TPO row is only as tall as the price scale makes it, so at some zoom a letter stops fitting. Rather than clip glyphs or make you toggle a setting, blockDisplay: 'auto' (the default) crossfades: the block is always drawn, and the letter fades in over letterFade px above minLetterHeight. Zooming through the threshold reads as one continuous change rather than a jump, and the profile stays readable at every scale.

Pin the behaviour when you want it fixed — 'letters' for glyphs only, 'blocks' for a pure brick profile, 'blocks+letters' to force both regardless of room.

💡

The footprint renderer fades its cell numbers the same way, via minTextHeight / textFade.

Hit-testing

hitTest(x, y) and hoverAt(x, y) map a pointer back to the session and price row under it, so a host can build a tooltip without the library owning any DOM. hoverAt returns { sessionIndex, price, level, session, isPoc, inValueArea, isSinglePrint }.

chart.on('crosshair:move', (e) => {
  const hit = e.point ? mp.hoverAt(e.point.x, e.point.y) : null;
  if (hit) tooltip.textContent = `${hit.level.letters}  ${hit.level.count} TPO`;
});

Data model

interface MarketProfileResult {
  sessions: MarketProfileSessionResult[];
  options: MarketProfileOptions;
}
 
interface MarketProfileSessionResult {
  startTime: number;      // UTC seconds (first bar)
  endTime: number;        // UTC seconds (last bar)
  levels: { price: number; count: number; letters: string; volume: number }[]; // high -> low
  poc: number;
  vah: number; val: number;
  periods: number;
  initialBalance: { high: number; low: number };
  singlePrints: number[];
  poorHigh: boolean; poorLow: boolean;
  totalVolume: number;
}

Everything you need for your own annotations (naked POCs, day-type labels, alerts) is on the result - the primitive is just one way to draw it.

Live updates

Recompute on new bars and hand the fresh result to the same primitive; setData repaints:

const profile = new MarketProfile(computeMarketProfile(bars, opts));
chart.addPrimitive(profile);
 
// later, as bars grow:
profile.setData(computeMarketProfile(bars, opts));
profile.setOptions({ colorMode: 'heat' }); // restyle without recomputing

Volume-at-price here comes from each bar’s volume spread across its range - a fast approximation. For true bid/ask volume distribution use the Footprint.

Troubleshooting

SymptomCauseFix
Nothing rendersNo price series, so the time axis has no barsAdd a series (addSeries) before the profile
Only one profile for many dayssession: 'composite'Use session: 'day' (or week / month)
Letters overlap verticallytickSize too small for the price rangeIncrease tickSize (fewer, taller rows)
Letters run past the sessionMany periods and a wide letterWidthLower letterWidth, or raise blockMinutes
Value area looks wrongvalueAreaPercent not in 0..1Pass a fraction (0.7), not a percent (70)