Examples
Delta histogram
Buy minus sell volume per bar in its own pane.

Shows: ChartPane with its own price scale. Order flow per bar, g.keepVisible(0, 0) to keep the zero line in view, g.priceValue for autoscale, a hover tooltip and right-click menu entries to mark bars.
See also: Surfaces.
import type { OrderFlowBar } from "@tradingthings/tt-create";
import { defineScript, format, input } from "@tradingthings/tt-create";
/** Bar width as a share of the bar spacing. */
const BAR_WIDTH = 0.7;
/**
* Buy minus sell volume of each bar in its own pane: above zero when buyers
* lifted the ask more. Hover shows the split, right click marks bars.
*/
export default defineScript({
name: "Delta histogram",
surface: { kind: "ChartPane", weight: 0.25 },
inputs: {
up: input.color({ default: "rgba(38,166,154,0.8)", label: "Positive" }),
down: input.color({ default: "rgba(239,83,80,0.8)", label: "Negative" }),
marked: input.color({ default: "#f59e0b", label: "Marked" }),
},
subscribe: (ctx) => ({ orderFlow: ctx.market.orderFlow() }),
state: () => ({ marked: new Set<number>() }),
draw: (s, frame, g, ctx) => {
const bars = ctx.streams.orderFlow.bars;
const { from: fromTime, to: toTime } = frame.visibleTime;
const zero = frame.priceToY(0);
const width = Math.max(1, frame.barSpacing * BAR_WIDTH);
g.keepVisible(0, 0);
g.line(0, zero, frame.width, zero, { color: frame.theme.grid, width: 1 });
for (let i = lowerBound(bars, frame.barTimeAt(fromTime) ?? fromTime); i < bars.length; i += 1) {
const bar = bars[i]!;
if (bar.time > toTime) break;
const x = frame.timeToX(bar.time);
if (x === null) continue;
const { delta } = mapBarToDelta(bar);
const y = frame.priceToY(delta);
const rect = { x: x - width / 2, y: Math.min(y, zero), width, height: Math.max(1, Math.abs(zero - y)) };
const color = s.marked.has(bar.time) ? ctx.inputs.marked : delta >= 0 ? ctx.inputs.up : ctx.inputs.down;
g.priceValue(bar.time, delta);
g.rect(rect.x, rect.y, rect.width, rect.height, color);
g.hit(`bar-${bar.time}`, { type: "Rect", ...rect });
}
const last = bars.at(-1);
if (!last) return;
const { delta } = mapBarToDelta(last);
g.priceLabel(delta, format.volume(delta), delta >= 0 ? ctx.inputs.up : ctx.inputs.down, "#ffffff");
},
onHover: (_s, event, ctx) => {
const bar = findHitBar(ctx.streams.orderFlow.bars, event?.hitId ?? null);
if (!bar) return null;
const { delta, buy, sell } = mapBarToDelta(bar);
return {
title: new Date(bar.time).toLocaleTimeString(),
rows: [
{ label: "Delta", value: format.volume(delta), color: delta >= 0 ? ctx.inputs.up : ctx.inputs.down },
{ label: "Buy", value: format.volume(buy) },
{ label: "Sell", value: format.volume(sell) },
],
};
},
onContextMenu: (s, event, ctx) => {
const bar = findHitBar(ctx.streams.orderFlow.bars, event.hitId);
if (bar) {
const marked = s.marked.has(bar.time);
return [
{
label: marked ? "Unmark delta bar" : "Mark delta bar",
onSelect: () => (marked ? s.marked.delete(bar.time) : s.marked.add(bar.time)),
},
];
}
return s.marked.size > 0 ? [{ label: "Clear marked delta bars", onSelect: () => s.marked.clear() }] : [];
},
legend: (_s, crosshairTime, ctx) => {
const bars = ctx.streams.orderFlow.bars;
const bar = crosshairTime === null ? bars.at(-1) : bars[lowerBound(bars, crosshairTime)];
if (!bar || (crosshairTime !== null && bar.time !== crosshairTime)) return [];
const { delta } = mapBarToDelta(bar);
return [{ label: "Delta", value: format.volume(delta), color: delta >= 0 ? ctx.inputs.up : ctx.inputs.down }];
},
});
/** Volume at the ask is buying, at the bid selling. */
function mapBarToDelta(bar: OrderFlowBar): { readonly delta: number; readonly buy: number; readonly sell: number } {
let buy = 0;
let sell = 0;
for (const level of bar.levels) {
buy += level.ask;
sell += level.bid;
}
return { delta: buy - sell, buy, sell };
}
function findHitBar(bars: readonly OrderFlowBar[], hitId: string | null): OrderFlowBar | null {
if (!hitId?.startsWith("bar-")) return null;
const time = Number(hitId.slice(4));
const bar = bars[lowerBound(bars, time)];
return bar?.time === time ? bar : null;
}
/** First index with `time >= target`. Live data is the newest: checked first. */
function lowerBound(list: readonly { readonly time: number }[], target: number): number {
const last = list.length - 1;
if (last < 0 || list[last]!.time < target) return list.length;
let low = 0;
let high = last;
while (low < high) {
const mid = (low + high) >> 1;
if (list[mid]!.time < target) low = mid + 1;
else high = mid;
}
return low;
}