Examples
Trade size distribution
How many live trades of each size, buys stacked on sells.

Shows: Canvas: no time or price scale, the script draws its own axes and bars in pane px. Reacts to the crosshair with onCrosshair.
See also: Surfaces.
import { defineScript, input } from "@tradingthings/tt-create";
/** Lower bound of each size bucket, the last one open-ended. */
const BUCKETS = [1, 2, 3, 5, 10, 20, 50, 100] as const;
/** Space for the axes, in px. */
const AXIS = { left: 44, bottom: 20, top: 12, right: 12 } as const;
const TICKS = 4;
interface Bucket {
buy: number;
sell: number;
}
/**
* How many live trades of each size, buys stacked on sells. Draws its own
* axes on a free canvas. Marks the bucket of the last trade at the
* crosshair's bar.
*/
export default defineScript({
name: "Trade size distribution",
surface: { kind: "Canvas", weight: 0.35 },
inputs: {
buy: input.color({ default: "rgba(38,166,154,0.85)", label: "Buy" }),
sell: input.color({ default: "rgba(239,83,80,0.85)", label: "Sell" }),
},
subscribe: (ctx) => ({ trades: ctx.market.trades() }),
state: () => ({
buckets: BUCKETS.map((): Bucket => ({ buy: 0, sell: 0 })),
/** Bucket of the last trade up to the crosshair's bar, `null` without crosshair. */
crosshairBucket: null as number | null,
}),
on: {
trades: (s, trade) => {
const bucket = s.buckets[mapSizeToBucket(trade.size)]!;
if (trade.aggressor === "Sell") bucket.sell += 1;
else bucket.buy += 1;
},
},
draw: (s, frame, g, ctx) => {
const { theme } = frame;
const plot = {
x: AXIS.left,
y: AXIS.top,
width: frame.width - AXIS.left - AXIS.right,
height: frame.height - AXIS.top - AXIS.bottom,
};
if (plot.width <= 0 || plot.height <= 0) return;
const bottom = plot.y + plot.height;
const max = Math.max(1, ...s.buckets.map((b) => b.buy + b.sell));
const toY = (count: number) => bottom - (count / max) * plot.height;
const text = { color: theme.text, font: theme.font };
// Axes: counts on the left, sizes below.
g.line(plot.x, plot.y, plot.x, bottom, { color: theme.grid, width: 1 });
g.line(plot.x, bottom, plot.x + plot.width, bottom, { color: theme.grid, width: 1 });
for (let i = 0; i <= TICKS; i += 1) {
const count = Math.round((max * i) / TICKS);
const y = toY(count);
g.line(plot.x - 4, y, plot.x, y, { color: theme.grid, width: 1 });
g.text(String(count), plot.x - 6, y, { ...text, align: "right" });
}
const slot = plot.width / BUCKETS.length;
for (const [i, bucket] of s.buckets.entries()) {
const x = plot.x + i * slot + slot * 0.15;
const width = slot * 0.7;
const sellTop = toY(bucket.sell);
const top = toY(bucket.sell + bucket.buy);
g.rect(x, sellTop, width, bottom - sellTop, ctx.inputs.sell);
g.rect(x, top, width, sellTop - top, ctx.inputs.buy);
if (i === s.crosshairBucket) g.rect(x, plot.y, width, 2, theme.text);
g.text(mapBucketToLabel(i), x + width / 2, bottom + AXIS.bottom / 2, { ...text, align: "center" });
g.hit(`bucket-${i}`, { type: "Rect", x, y: plot.y, width, height: plot.height });
}
if (s.buckets.every((b) => b.buy + b.sell === 0)) {
g.text("Waiting for live trades", plot.x + plot.width / 2, plot.y + plot.height / 2, {
...text,
align: "center",
});
}
},
onHover: (s, event) => {
const index = event?.hitId?.startsWith("bucket-") ? Number(event.hitId.slice(7)) : null;
const bucket = index === null ? undefined : s.buckets[index];
if (index === null || !bucket) return null;
return `${mapBucketToLabel(index)} lots: ${bucket.buy} buys, ${bucket.sell} sells`;
},
onCrosshair: (s, event, ctx) => {
if (!event) {
s.crosshairBucket = null;
return;
}
const trades = ctx.streams.trades;
// Last trade at or before the crosshair's bar.
let i = trades.length - 1;
while (i >= 0 && trades.time[i]! > event.time) i -= 1;
s.crosshairBucket = i >= 0 ? mapSizeToBucket(trades.size[i]!) : null;
},
widgets: (s) => [
{
id: "controls",
placement: { type: "Corner", corner: "TopRight" },
widgets: [
{ type: "Badge", text: `${s.buckets.reduce((sum, b) => sum + b.buy + b.sell, 0)} trades` },
{
type: "Button",
label: "Reset",
onClick: () => {
for (const bucket of s.buckets) {
bucket.buy = 0;
bucket.sell = 0;
}
},
},
],
},
],
});
function mapSizeToBucket(size: number): number {
let index = 0;
while (index + 1 < BUCKETS.length && size >= BUCKETS[index + 1]!) index += 1;
return index;
}
function mapBucketToLabel(index: number): string {
const low = BUCKETS[index]!;
const next = BUCKETS[index + 1];
if (next === undefined) return `${low}+`;
return next - 1 === low ? String(low) : `${low}-${next - 1}`;
}