> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polytape.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Plotting

> Charts your user can read — the grammar, the validated palette, the honest labels, and the ready-made templates you pour data into.

PolyTape's data draws two charts well: **a market's price line with a trader's
fills on it**, and **a trader's career equity curve**. This is the spec — the
grammar, the palette, and the honest labels — plus templates you pour data into.

Everything here is deterministic. The price line an agent draws is the same line
the PolyTape web app draws; the fidelity law is shared, byte for byte. You are
not inventing a look, you are reproducing one.

## Which chart, and how to reach it

| You want to show…                                   | Draw                                                  | Template                                  |
| --------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------- |
| A market's price path + where the trader got in/out | the **market chart**                                  | `chart-market-*.html` / `chart-market.py` |
| A trader's cumulative P/L over their career         | the **equity curve**                                  | `chart-equity.html` / `chart-equity.py`   |
| A distribution (sizes, hold times, hourly activity) | a table or a plain bar chart off `get_market_summary` | — the summary already has the numbers     |

**Two market-chart flavors, routed by where the chart will live:**

<Columns cols={2}>
  <Card title="Chat / MCP connector" icon="comment">
    **The SVG template** (`chart-market-svg.html`). Self-contained: no CDN, no
    external fonts, no fetches, \~30KB. Renders inside a chat artifact or a strict
    CSP.

    Reach it with `get_chart_template(kind="market", format="html")`.
  </Card>

  <Card title="File / browser" icon="window">
    **The tape template** (`chart-market-tape.html`). TradingView Lightweight
    Charts for kinetic pan/zoom over the whole tape down to 1-second buckets.

    Fetch it from
    [`/skill/reference/templates/`](https://polytape.io/skill/reference/templates/chart-market-tape.html),
    or `get_chart_template(kind="market-tape")`.
  </Card>
</Columns>

Both draw the identical grammar and palette. **Pick by capability, not
preference.**

***

## The market chart — grammar

One market. Time on x (the viewer's local time), UP-space probability on y (0–1,
shown as ¢). Five marks, in this order:

<Steps>
  <Step title="The price line — Polymarket blue #3d7dff">
    The drawn value is the **mid of the traded band**: `mid = (lo + hi) / 2` for
    each point `[ts, last, lo, hi]`. `last` is the bucket's final real trade;
    `lo` / `hi` are the band edges.

    **Every one of those is a real traded level** — the server never emits a
    computed price, so the line never fabricates a number. Draw it stepped or
    straight; do not interpolate across gaps with a solid line.
  </Step>

  <Step title="The traded band">
    A faint blue fill between `lo` and `hi`. It shows where trading actually
    happened; the line rides its middle.
  </Step>

  <Step title="The fills — ▲ buy / ▼ sell, colored by TOKEN, sized by $">
    Shape encodes side; **color encodes which token they traded**; marker area is
    proportional to `usd`, so a conviction slam reads bigger than a \$2 clip.

    **Fills RIDE THE LINE.** A marker's y is the *line's value at the fill's
    moment*, not the raw fill price. At fine zoom the line ≈ the trade price, so
    riding it is the truth; at coarse zoom the line is the only honest y a bucket
    can show, and a price-true marker would float and read broken. **The exact
    fill price lives in the hover docket, never in the plotted position.** No
    stems.

    **×N pile badges.** When 3 or more fills share an x-slot, label the cluster
    `×N` instead of overprinting; merge colliding badges so counts never overlap.
  </Step>

  <Step title="The resolution pin">
    If the market resolved, walk a dashed **amber `#ffb84d`** segment from the
    last point to the winner level (token-1 win → 1.00, else 0.00) and pin it with
    a label.

    A dashed bridge, never a solid line — the price didn't trade there, it
    settled there.
  </Step>

  <Step title="An honest y-scale">
    Frame the band's actual extent (plus the winner level if resolved), padded
    \~18%, clamped to \[−0.02, 1.02].

    Do **not** force 0–100¢ on a market that lived between 40¢ and 60¢ — you'd
    flatten the whole story.
  </Step>
</Steps>

Header chips carry the context: trader, RESOLVED·winner / OPEN, the P/L (labeled
per the doctrine below), fill count, and a **token legend** — a colored swatch
next to each token's name.

***

## The palette

<Warning>
  The tones are locked and ported verbatim from the web app's `outcomeTone` rule.
  Don't eyeball them and don't recolor.
</Warning>

The CRT tube the chart sits in is **always dark** (`#101410`) — in both day and
night; the theme switch reskins only the room around the glass. So every mark is
validated against one constant surface.

| Role                                                                   | Color                                                                             | Notes                                   |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------- |
| Price line + band                                                      | `#3d7dff` Polymarket blue                                                         | one series; passes lightness + contrast |
| **UP family** (`up`/`yes`/`over`/`true`/`win`/`higher`/`above`/`long`) | `#46ff8e` phosphor green                                                          | token 1 on most binaries                |
| **DOWN family** (`down`/`no`/`under`/`false`/`lower`/`below`/`short`)  | `#ff5a3c` loss red                                                                | token 2 on most binaries                |
| **Named outcomes** (a candidate, a team)                               | djb2 hash → `#ffb000` · `#3fd8d0` · `#b98cff` · `#5aa9ff` · `#ff79c6` · `#8f9bff` | deterministic per name                  |
| Resolution / drawdown pin                                              | `#ffb84d` amber                                                                   | dashed walk + dot                       |

**Colorblind safety, measured.** Against the dark tube, the green↔red pair that
appears on nearly every market separates at **ΔE 29.0** for deuteranopia — far
above the 12 target. The two-sided coloring is colorblind-safe on its own.

The named-outcome palette is a deterministic hue assignment, not a CVD-optimized
ramp: two named tokens can hash to a close pair (worst adjacent blue↔violet is
ΔE 3.1). **That is why the labeled token legend is mandatory and the hover docket
names the token in words** — identity is never carried by color alone.

The phosphor tones sit brighter than a reflective-surface lightness band by
design (they glow on a black tube); they clear the contrast-vs-surface floor,
which is what governs legibility here.

***

## Getting the data

**The default call gives you the whole chart in one shot.** `get_market_events`
(REST `GET /traders/{trader}/markets/{condition_id}`) returns a **sampled**
response — no cursor, no tick flood:

```json theme={null}
{ "market": { "condition_id": "0x…", "title": "…", "outcome": "Up",
              "outcome2": "Down", "resolved": true, "winner": "Up",
              "pnl_usd": 81.08, "open": false },
  "trader": { "wallet": "0x…", "name": "GoingInsolvent" },
  "series": { "from_ts": …, "to_ts": …, "fidelity": 1,
              "points": [[ts, last, lo, hi], …] },
  "fills":  [ { "ts": …, "side": "BUY", "price": 0.48, "token_side": 2,
                "outcome": "Down", "size_tokens": 5.0, "usd": 2.6 }, … ] }
```

* `points` are pre-decimated to your `points=` target — **default 300 = web
  parity, clamp 20–600**, snapped to the fidelity ladder. Ask for `points=120`
  for a chat-sized chart.
* The default window is **title-anchored**: a "2:25–2:30PM ET" market opens at
  the title's start. Pass `from_ts` / `to_ts` to zoom.
* `fills` is the trader's **complete** fill set, always present. Each carries
  `token_side` (1|2) **and** `outcome` (the token's name) — you no longer derive
  the side.
* Draw `(lo + hi) / 2`. This one response fills the SVG template directly.

<AccordionGroup>
  <Accordion title="Full-tape mode — for the interactive template" icon="film">
    When you want continuous zoom from a 53-day arc down to a single second, fetch
    the whole change-compressed tape in one gzip response from
    `GET /traders/{trader}/markets/{condition_id}/tape` —
    `{market, trader, tape: [[ts, last, lo, hi], …]}`, per traded second,
    uncapped.

    The Iran market is 27,066 tape points (\~150KB gzipped): fine for a file, too
    big for a chat artifact. The tape template does all bucketing and the band-mid
    walk client-side.
  </Accordion>

  <Accordion title="raw=1 — the verbatim export path" icon="box-archive">
    The old keyset tick pages (`ticks` + `cursor`) survive behind `?raw=1`. You
    almost never want this for a chart: raw ticks duplicate about **36×** (the
    same second repeats), so a 5-minute market is 8,000 rows across 4 pages. The
    sampled default exists precisely to spare you this.
  </Accordion>

  <Accordion title="Two obsolete idioms" icon="clock-rotate-left">
    **The `limit=1` fills-only trick is dead.** Fills used to arrive in full on
    the first page regardless of `limit`, so agents passed `limit=1` to get fills
    without paying for ticks. The sampled default **always** includes the complete
    fills — just call it plainly.

    **For "what happened at the end?", pass a `from_ts` / `to_ts` window near the
    close.** There is no descending or `until` tick paging; it is a forward keyset
    cursor, so a tight end-window is the endgame view.
  </Accordion>
</AccordionGroup>

***

## Token frame — the one rule that keeps you honest

**The price line is in UP space** (token 1's probability, 0–1). `mid`, `last`,
`lo`, `hi`, and a fill's `price` are all UP-normalized. That is the correct,
single frame for the *line* — one market, one price path.

But a trader buys a **token**, and the price they *paid* is in that token's own
frame: for a token-1 buy it's `price`; for a token-2 buy it's `1 − price`
(= `usd / size_tokens`).

The chart handles this by **coloring the marker by token** (`token_side` →
`outcome` name → tone) while keeping every marker on the one UP line.

<Warning>
  This is why the coloring is load-bearing, not cosmetic. Without it, a two-sided
  scalper who buys UP at 0.48 and DOWN at 0.55 seconds apart looks like one
  person averaging a single position — **the single most common misread on the
  whole surface.** Color it, and the two-sided structure is instantly legible.
</Warning>

Name the tokens in the title and legend so "0.94" reads as "UP at 94¢", not an
ambiguous number.

***

## Labeling a chart's P/L

The [three-voice doctrine](/concepts/pnl) applies to every chart you emit:

* A **resolved** market's `pnl_usd` is a *result* — "won $X" / "lost $X".
* An **open** market's `pnl_usd` is **cash flow**, not a result. Money deployed
  counts as spent. Label it `CASH FLOW`, and quote `pnl_marked_usd` for the
  position's standing ("marked at $X") — never "won $X".
* **Split/merge legs are invisible to fills.** A market's `pnl_usd` includes
  mint, split, and redemption legs that never appear as buy/sell fills — so a
  −$42.3k market can show buy $2k / sell \$41.7k and not reconcile. Don't
  reconstruct P/L from visible fills; quote `pnl_usd`. And **`roi` is meaningless
  for split-sellers** — don't quote it on sell-dominated flow.

***

## The equity curve + drawdown

`get_pnl_series` (REST `GET /traders/{trader}/pnl-series`) returns Polymarket's
own cumulative-P/L series — the same model as the headline:

```json theme={null}
{ "trader": {…}, "window": "all", "model": "polymarket_profile",
  "series": [[ts, pnl_usd], …], "peak": …, "low": …,
  "max_drawdown": …, "as_of": … }
```

`window` ∈ `24h | 7d | 30d | ytd | 1y | all` (default `all`). Pour it into
`chart-equity.html` / `chart-equity.py`.

* **Equity** — the cumulative line with a **zero baseline**; fill green above
  break-even, red below. The final value's sign colors the end dot.
* **Drawdown** — derived in one line: **`dd[i] = p[i] − running_max`** (0 at
  every new high, negative while underwater). `max_drawdown` arrives precomputed;
  the deepest trough gets the amber pin. This is the honest "how bad did it get?"
  view and it needs **no extra call** — the template computes it from `series`.

***

## Whales — don't page, ask the right primitive

A 40,001-market bot has no "biggest win" on page one. **Never page the markets
list 200× to find extremes** — the list is recency-ordered only, by design.

* **`get_top_markets(trader, n)`** returns four ranked slices from one scan:
  `top_wins`, `top_losses` (realized P/L, **resolved markets only** — honestly
  labeled), `most_traded` (by fills), `top_volume`. Keep `n` modest on
  mega-makers; a 180,000-market account is near the heavy-query timeout.
* The card's **career averages** answer "is this winrate any good?".
  `avg_entry_price` is the average cost per token across all buys — the
  **break-even winrate**. `avg_win_usd` / `avg_loss_usd` (served negative) are
  **averages, not medians**, so one blowup skews them: read them alongside
  `top_losses` rather than alone.

***

## Using the templates

Each HTML template has a single placeholder — `const DATA = "__DATA__";` — that
appears exactly once. Replace it with your JSON object and render:

```js theme={null}
const tpl = fs.readFileSync("chart-market-svg.html", "utf8");
const data = /* the sampled response from get_market_events */;
fs.writeFileSync("chart.html", tpl.replace('"__DATA__"', JSON.stringify(data)));
```

The templates **normalize defensively** — they accept the canonical shape
(`series.points`, `size_tokens`, `pnl_usd`, `trader.name`) and tolerate the
fixture-style shape (`points`, `size`, `pnl`, `trader_name`) — so pouring a raw
tool response in usually just works.

| Template                 | Consumes                                                                          | Theme                                     | Notes                           |
| ------------------------ | --------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------- |
| `chart-market-svg.html`  | the sampled response (`series`/`points` + `fills`); or `slabs:[…]` for baked zoom | night default, day switch                 | self-contained; chat-safe       |
| `chart-market-tape.html` | `{market, trader_name, fills, tape:[[ts,last,lo,hi]]}`                            | night default, `window.__setTheme("day")` | TradingView; continuous 1s zoom |
| `chart-equity.html`      | `{series:[[t,p]], peak, low, max_drawdown}`                                       | night default, day switch                 | PNL / DRAWDOWN toggle           |
| `chart-market.py`        | same as SVG (argv JSON path)                                                      | `THEME` constant                          | matplotlib → PNG                |
| `chart-equity.py`        | same as equity (argv JSON path)                                                   | `THEME` constant                          | matplotlib → PNG, 2-panel       |

**Deep-link and test hooks (HTML):** `window.__setRange(fromTs, toTs)` (tape),
`window.__setDom(fromTs, toTs)` (SVG), `window.__setMode("pnl"|"drawdown")`
(equity), `window.__setTheme("day"|"night")`, and `window.__ready === true` once
painted.

<Warning>
  **Zoom on the SVG flavor = baked slabs, not live fetch.** One sampled call =
  one slab. To zoom deep in a chat artifact, bake 2–3 windows (the full arc plus
  the drama stretch) with `from_ts` / `to_ts` and pass them as
  `slabs: [{fidelity, from_ts, to_ts, points}, …]`; the view swaps to the finest
  slab covering the range.

  **Never wire a chat chart to live-fetch — the file would carry your API key.**
</Warning>

**Reaching a template through the connector.** If your client can't fetch web
files, `get_chart_template(kind, format, theme)` returns the file text through
the MCP tool itself. It costs 1 request — a normal tool call.

***

## Minimal end-to-end

```text theme={null}
get_market_summary(trader, market)             # the shape + the window + resolved?
get_market_events(trader, market, points=300)  # one call: series + all fills + header
→ pour the response into chart-market-svg.html
  (or fetch chart-market-tape.html for a file)
→ verdict: quote pnl_usd (resolved) with the token named; if two-sided, say so —
  the coloring already shows it.
```

That is a two-sided scalp and a 53-day blowup, each in **one events call** after
the summary — house-grade, correctly token-colored, honestly labeled.
