Which chart, and how to reach it
Two market-chart flavors, routed by where the chart will live:
Chat / MCP connector
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").File / browser
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/,
or get_chart_template(kind="market-tape").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:1
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.2
The traded band
A faint blue fill between
lo and hi. It shows where trading actually
happened; the line rides its middle.3
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.4
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.5
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.
The palette
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.
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:
pointsare pre-decimated to yourpoints=target — default 300 = web parity, clamp 20–600, snapped to the fidelity ladder. Ask forpoints=120for 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_tsto zoom. fillsis the trader’s complete fill set, always present. Each carriestoken_side(1|2) andoutcome(the token’s name) — you no longer derive the side.- Draw
(lo + hi) / 2. This one response fills the SVG template directly.
Full-tape mode — for the interactive template
Full-tape mode — for the interactive template
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.raw=1 — the verbatim export path
raw=1 — the verbatim export path
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.Two obsolete idioms
Two obsolete idioms
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.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.
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 applies to every chart you emit:- A resolved market’s
pnl_usdis a result — “won X”. - An open market’s
pnl_usdis cash flow, not a result. Money deployed counts as spent. Label itCASH FLOW, and quotepnl_marked_usdfor the position’s standing (“marked at X”. - Split/merge legs are invisible to fills. A market’s
pnl_usdincludes mint, split, and redemption legs that never appear as buy/sell fills — so a −2k / sell $41.7k and not reconcile. Don’t reconstruct P/L from visible fills; quotepnl_usd. Androiis 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:
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_drawdownarrives 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 fromseries.
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. Keepnmodest 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_priceis 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 alongsidetop_lossesrather 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:
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.
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.
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.
