Skip to main content
What you’ll build: a realized-PnL chart with a marker pinned on every closed position, labelled by how the trade ended (won or lost, held to resolution or sold early). Useful for a trader performance chart or a trade journal of wins and losses.
polymarket_trader_pnl_exits emits one marker per position close, designed to overlay exits on a PnL chart. Each marker says how a position ended (pnl_usd, ts, and a reason), which is exactly what an annotation layer needs. This recipe builds a realized-PnL line and pins a marker at every exit, styled by whether the trader won or lost and whether they held to resolution or sold early.

When to use this

  • A trader performance chart where each closed bet is a labelled point.
  • A trade journal that lists wins and losses with the dollar result of each exit.
  • An overlay on top of a running PnL line that shows when realized PnL stepped.

The exit reasons

Every marker carries one of four reasons. They split on two axes: held to resolution versus sold early, and won versus lost.
ReasonMeaningSuggested style
resolved_winHeld to market resolution and the verdict was a win.Green, filled triangle.
resolved_lossHeld to market resolution and the verdict was a loss.Red, filled triangle.
sold_winClosed before resolution with positive realized PnL.Green, hollow circle.
sold_lossClosed before resolution with negative realized PnL.Red, hollow circle.
Using shape for the close type (triangle for resolution, circle for a sale) and color for the outcome lets a reader decode an exit at a glance.

Step 1: subscribe with a reasons filter

traders is required. The optional reasons filter trims the stream to the exits you chart; passing it cuts message volume since the room bills per message. To plot only the losses, for example, pass the two loss reasons.
import { StructWebSocket } from "@structbuild/sdk";

const ws = new StructWebSocket({ apiKey: "sk_live_xxx" });
await ws.connect();

await ws.subscribe("polymarket_trader_pnl_exits", {
  traders: ["0xd8da6bf26964af9d7eed9e03e53415d37aa96045"],
  reasons: ["resolved_win", "resolved_loss", "sold_win", "sold_loss"],
});
Omit reasons (or pass ["all"]) to receive every exit.

Step 2: map markers to annotations

Each trader_exit_marker_batch envelope carries an array of marker rows. A marker has pnl_usd (realized at exit), ts (Unix seconds), reason, and the descriptive fields (title, outcome, market_slug) for a tooltip. Map each one to your chart’s annotation shape.
type Annotation = {
  x: number;
  y: number;
  color: "green" | "red";
  shape: "triangle" | "circle";
  label: string;
};

const STYLE: Record<string, Pick<Annotation, "color" | "shape">> = {
  resolved_win: { color: "green", shape: "triangle" },
  resolved_loss: { color: "red", shape: "triangle" },
  sold_win: { color: "green", shape: "circle" },
  sold_loss: { color: "red", shape: "circle" },
};

const annotations: Annotation[] = [];

ws.on("trader_exit_marker_batch", (event) => {
  for (const marker of event.data) {
    const style = STYLE[marker.reason];
    annotations.push({
      x: marker.ts * 1000,
      y: marker.pnl_usd,
      ...style,
      label: `${marker.title}: ${marker.pnl_usd >= 0 ? "+" : ""}${marker.pnl_usd.toFixed(2)}`,
    });
  }
  redrawAnnotations(annotations);
});
The marker ts is in seconds; most charting libraries expect milliseconds, so multiply by 1000 when placing the point on a time axis.

Step 3: draw the underlying PnL line

Markers are the overlay; the line beneath them is the trader’s realized PnL over time. Drive it from the trader PnL tick stream so the curve moves continuously and each marker lands on the line at the moment of the exit. Subscribe to polymarket_trader_pnl for the same wallet and append the global realized figure as it ticks.
const series: { x: number; y: number }[] = [];

await ws.subscribe("polymarket_trader_pnl", {
  traders: ["0xd8da6bf26964af9d7eed9e03e53415d37aa96045"],
  update_types: ["global"],
});

ws.on("trader_global_tick_batch", (event) => {
  for (const row of event.data) {
    series.push({ x: Date.now(), y: row.realized_pnl_usd });
  }
  redrawLine(series);
});
Realized PnL steps at each exit, so a marker drawn at (ts, pnl_usd) sits on the riser of the step it caused. See Building a live trader PnL dashboard for the full PnL stream model, including the difference between full-row, tick, and resolution families.

Backfilling the chart

The room streams exits going forward. To paint historical markers when the chart first loads, seed from your own records or a REST PnL pull, then attach the live stream for new exits on top, the same seed-then-stream pattern used across these guides.

Follow-on

Last modified on June 13, 2026