Charts
Examples

Custom API levels

Price levels from your own API, polled every 30 seconds.

Custom API levels

Shows: ChartOverlay. ctx.data.poll through our proxy, a hand-written DataSchema that checks the JSON, DataEvent handled with an exhaustive switch, and ctx.streams.levels.latest in draw.

See also: Streams and history.

api-levels.ts
import { defineScript, input, type DataSchema } from "@tradingthings/tt-create";

/** What the API returns: `[{ "price": 5012.25, "label": "Call wall" }, …]`. */
interface Level {
  readonly price: number;
  readonly label: string;
}

/** Checks the JSON. A zod schema (`z.array(...)`) fits here as well. */
const LEVELS: DataSchema<readonly Level[]> = {
  safeParse: (json) => {
    if (!Array.isArray(json)) return { success: false, error: "expected an array" };

    const levels: Level[] = [];

    for (const item of json) {
      if (typeof item?.price !== "number") return { success: false, error: `no price in ${JSON.stringify(item)}` };

      levels.push({ price: item.price, label: typeof item.label === "string" ? item.label : "" });
    }

    return { success: true, data: levels };
  },
};

/**
 * Price levels from your own API, polled every 30 s, as lines with labels.
 * A failed request keeps the last levels and shows the error in the legend.
 */
export default defineScript({
  name: "API levels",
  surface: { kind: "ChartOverlay" },
  inputs: {
    url: input.string({ default: "https://example.com/levels.json", label: "URL" }),
    line: input.lineStyle({ default: { color: "#42a5f5", width: 1, dash: [6, 4] }, label: "Line" }),
  },
  subscribe: (ctx) => ({
    levels: ctx.data.poll({ url: ctx.inputs.url, every: "30s", schema: LEVELS }),
  }),
  state: () => ({ error: null as string | null }),
  on: {
    levels: (s, event, ctx) => {
      switch (event.type) {
        case "Value":
          s.error = null;

          return;
        case "Error":
          s.error = event.error.type;
          ctx.log("levels failed", event.error);

          return;
        default:
          event satisfies never;
      }
    },
  },
  draw: (_s, frame, g, ctx) => {
    // The last valid value stays in the view, also after an error.
    const levels = ctx.streams.levels.latest ?? [];
    const { line } = ctx.inputs;

    for (const level of levels) {
      const y = frame.priceToY(level.price);

      if (y < 0 || y > frame.height) continue;

      g.line(0, y, frame.width, y, line);
      g.text(level.label, 6, y - 2, { color: line.color, baseline: "bottom" });
      g.priceLabel(level.price, frame.formatPrice(level.price), line.color, "#ffffff");
    }
  },
  legend: (s, _crosshairTime, ctx) => [
    { label: "Levels", value: String(ctx.streams.levels.latest?.length ?? 0) },
    ...(s.error ? [{ label: "Error", value: s.error, color: "#ef5350" }] : []),
  ],
});