Charts
Examples

Whale prints

Big trades as circles sized by volume, with a draggable alert level.

Whale prints

Shows: ChartOverlay. The trades stream with minSize and a day of history, circle hits with a hover tooltip, a draggable line hit with onDrag (including Cancel), and ctx.alert only in the Live phase.

See also: Interaction.

whale-prints.ts
import { defineScript, group, input, tab, type Trade } from "@tradingthings/tt-create";

const LEVEL_HIT = "alert-level";

/**
 * Big trades as circles sized by volume, with a tooltip on hover. A
 * draggable alert level alerts when a live print trades through it.
 */
export default defineScript({
  name: "Whale prints",
  surface: { kind: "ChartOverlay" },
  inputs: [
    tab("Inputs", [group("Prints", { minSize: input.int({ default: 50, min: 1, label: "Min size" }) })]),
    tab("Style", [
      group("Colors", {
        buy: input.color({ default: "rgba(38,166,154,0.6)", label: "Buy" }),
        sell: input.color({ default: "rgba(239,83,80,0.6)", label: "Sell" }),
        level: input.lineStyle({ default: { color: "#f59e0b", width: 1, dash: [6, 4] }, label: "Alert level" }),
      }),
    ]),
  ],
  subscribe: (ctx) => ({
    trades: ctx.market.trades({ minSize: ctx.inputs.minSize, history: "1d" }),
  }),
  state: () => ({
    prints: [] as Trade[],
    /** Price of the alert level. `null` until the first print places it. */
    alertPrice: null as number | null,
    /** Alert price before the current drag, restored on cancel. */
    dragFrom: null as number | null,
  }),
  on: {
    trades: (s, trade, ctx) => {
      const previous = s.prints.at(-1);

      s.prints.push(trade);
      s.alertPrice ??= trade.price;

      if (ctx.phase === "Live" && previous !== undefined && crosses(previous.price, trade.price, s.alertPrice)) {
        ctx.alert(`Whale ${trade.aggressor} ${trade.size} through ${s.alertPrice}`);
      }
    },
  },
  draw: (s, frame, g, ctx) => {
    for (const [i, p] of s.prints.entries()) {
      if (p.time < frame.visibleTime.from || p.time > frame.visibleTime.to) continue;

      const x = frame.timeToX(p.time);

      if (x === null) continue;

      const y = frame.priceToY(p.price);
      const radius = Math.min(40, Math.sqrt(p.size));

      g.circle(x, y, radius, p.aggressor === "Sell" ? ctx.inputs.sell : ctx.inputs.buy);
      g.hit(`print-${i}`, { type: "Circle", x, y, radius });
    }

    if (s.alertPrice !== null) {
      const y = frame.priceToY(s.alertPrice);

      g.line(0, y, frame.width, y, ctx.inputs.level);
      g.priceLabel(s.alertPrice, frame.formatPrice(s.alertPrice), ctx.inputs.level.color, "#000000");
      g.hit(LEVEL_HIT, { type: "Line", x0: 0, y0: y, x1: frame.width, y1: y }, {
        cursor: "ns-resize",
        draggable: true,
      });
    }
  },
  onHover: (s, event) => {
    const index = event?.hitId?.startsWith("print-") ? Number(event.hitId.slice(6)) : null;
    const print = index === null ? undefined : s.prints[index];

    if (print === undefined) return null;

    return {
      title: `${print.aggressor} ${print.size}`,
      rows: [
        { label: "Price", value: String(print.price) },
        { label: "Time", value: new Date(print.time).toLocaleTimeString() },
      ],
    };
  },
  onDrag: (s, event) => {
    if (event.hitId !== LEVEL_HIT) return;

    switch (event.type) {
      case "Start":
        s.dragFrom = s.alertPrice;
        s.alertPrice = event.price;

        return;
      case "Move":
      case "End":
        s.alertPrice = event.price;

        return;
      case "Cancel":
        s.alertPrice = s.dragFrom;

        return;
    }
  },
  legend: (s) => [{ label: "Prints", value: String(s.prints.length) }],
});

/** Whether a move from `from` to `to` reached or crossed `level`. */
function crosses(from: number, to: number, level: number): boolean {
  return (from < level && to >= level) || (from > level && to <= level);
}