0.A note to the team #
The current signal tab does the hardest part of this problem: it makes invisible bot behaviour visible. Plotting bot signals on the same canvas as price action is the right instinct. The strategy filtering, date filtering, and status workflow are also a strong base — the underlying data model is clearly there, which is the part that actually takes time.
What follows is not a redesign. It is a sketch of where the existing foundation should evolve next, with a single goal: move from viewer of bot signals to trading-ops console that supervises bots. Almost every suggestion below uses data we already have. The work is mostly assembly and UX, not new data infrastructure.
1.Why this model representation #
Six reasons the proposed layout matches what Polytraders actually does.
It matches the job, not the metaphor
TradingView is built for humans clicking buy and sell. Our users are supervising autonomous agents. Different job, different screen.
It serves three audiences at once
Quant wants why did it fire. Ops wants are we within limits. Engineer wants is latency healthy. Each gets a home zone.
It encodes a visual hierarchy of importance
Top strip = survival. Left rail = portfolio. Centre = supervision. Bottom = plumbing. Matches how a desk actually scans during a live event.
It makes bot decisions auditable
The current view shows that something happened. The decision drawer shows why — model probability, top features, risk checks, counterfactual P&L.
It uses our one differentiating asset
Real-world event timelines. Pistol won, bomb planted, MVP clutch, state called. This is the moat — no generic trading dashboard has it.
It is honest about uncertainty
Binary resolution markets are not continuous price series. A probability line with a confidence band speaks the right language. Candles stay as a secondary view.
2.Jobs to be done #
Two columns. One is what the current screen optimises for. The other is what we need.
| What a manual trader needs | What a bot supervisor needs |
|---|---|
| Price action and indicators | Is my bot alive? What did it just do? Why? |
| An order ticket | Risk limits, kill switch, exposure caps |
| Drawing tools | Strategy attribution and audit trail per fill |
| One market at a time | Portfolio-wide health at a glance |
| Charting customisation | Replay mode, decision context, slippage truth |
3.The proposed layout, annotated #
Six zones, in order of importance. Each one uses data we already have.
Persistent header strip — market, status, position, P&L, edge, resolves-in. Always visible.
Kill switch top-right — two-click confirm, audit-logged. Never navigate to halt.
Strategy leaderboard — live P&L, Sharpe, win %. Click to isolate on chart.
Probability line + band — default view for binary markets. Candles secondary.
Fills as glyphs — shape by order type, size by stake, colour by realised outcome.
Match-clock event ribbon — pistols, bomb plants, timeouts mapped to price. The moat.
Decision drawer — model prob, top features, risk checks, counterfactual P&L per fill.
4.Nine changes, in order of impact #
In order. Earlier items unlock later ones.
Persistent header strip. Market, status, position, P&L, edge, resolves-in, always visible.
Match-clock event ribbon. Real-world events mapped to price. Single biggest moat.
Fills as first-class glyphs. Shape by order type, size by stake, colour by realised outcome, clickable.
Strategy leaderboard. Left rail — live P&L, Sharpe, win %. Click to isolate.
"Why did it trade" drawer. Model probability, top features, risk checks, counterfactual P&L per fill.
Latency + microstructure pane. Signal→fill ms, p50/p95, spread, slippage vs intended price.
Risk + kill switch always-on. Global P&L, drawdown vs limit, exposure vs cap, halt button.
Replay mode + ⌘K palette. Scrub the match at 4× / 16×, fuzzy-search across markets and fills.
Two layouts, one toggle. Monitoring mode (default) vs Chart-first mode for deep investigation.
5.How to build it #
Three phases. Almost everything reuses data we already have.
Architectural assumption. Postgres or Timescale for signals/fills/positions, a Python or Node backend,
a React frontend, a websocket per page multiplexed by market_id and
strategy_id. The chart must render on a <canvas>,
not through React reconciliation — React on every tick will not survive contact with production.
5.1 Phase 1 — Two weeks, zero new data #
The biggest perceived UX jump for the smallest engineering cost. Every input already exists.
- Top header strip. Source: existing
positionstable + market metadata + open P&L calc. Lift the websocket stream that already feeds the chart. Payload{position, avg_price, mid, mark_pnl, edge_vs_mid, resolves_at}. - Strategy leaderboard. Aggregate the existing
fillstable grouped bystrategy_idinto a materialised view that refreshes every minute. Click row → frontend state filters the chart. No backend change for the click behaviour. - Fills as glyphs. Already plotted as signal markers. Change three things: shape by
order_type, radius ∝sqrt(notional)capped, colour by realised outcome viafills LEFT JOIN positions. - Kill switch. Extend the existing per-strategy pause toggle to a global pause-all mutation. Two-click confirm. Audit-logged in
control_actions.
5.2 Phase 2 — Three to four weeks, the differentiating features #
The things competitors will not have. Step 2 (decision persistence) is the single highest-leverage data change in this whole spec.
- Match-clock event ribbon. Subscribe to the same odds feed the bots already consume (CS2, football, tennis all expose discrete in-game events). New table
match_events, one row per event. Render as vertical lines on the chart plus a ribbon strip below. Hover tooltip: price before / price after / bot action within 30s. - Decision persistence. Every time a bot fires a signal, persist the feature vector and decision context alongside the order. Without this, the decision drawer is fake. Schema in section 6.
- Latency + microstructure strip. Rolling 5-minute p50/p95 of
signal_fired_at → fill_confirmed_atserver-side. Spread from Polymarket orderbook websocket. Slippage as(filled_price − intended_price) × sideper fill. One small endpoint, frontend polls every 2s. - Probability view as default. Use existing mid line with a confidence band from the model's predictive variance — or historical residual std as fallback. Candles remain as a toggle for users who prefer them.
5.3 Phase 3 — Polish, the things that compound #
Each item below is small in isolation and large in cumulative effect.
- Replay mode. Already have all timestamped data — signals, fills, match events, prices. Build a scrubber with 1× / 4× / 16× speeds, re-emit through the same render path. Best onboarding tool we can build: new hire watches yesterday's NBA Finals market at 16× and understands the system in five minutes.
- Command palette (⌘K). Use
cmdkby Paco Coursey. Index: markets, strategies, last 7 days of fills, team members, settings. Fuzzy search client-side. - Two layouts, one toggle. Monitoring mode = this spec. Chart-first mode = current Polytraders layout for deep investigation. Persist preference, one keyboard shortcut, one URL param to deep-link.
6.Data model additions #
Two new tables. The first is required for Phase 1's ribbon, the second is the unlock for Phase 2's decision drawer.
create table match_events ( event_id bigserial primary key, market_id text not null, ts timestamptz not null, event_type text not null, -- pistol_won, bomb_plant, timeout, mvp, goal, state_called ... label text not null, -- human label for the ribbon tooltip impact_side text, -- YES | NO | null raw jsonb -- full provider payload, kept for replay ); create index on match_events (market_id, ts);
create table signal_decisions (
signal_id uuid primary key,
strategy_id text not null,
market_id text not null,
fired_at timestamptz not null,
model_prob numeric(6,4) not null,
market_implied_prob numeric(6,4) not null,
edge_cents numeric(6,2) not null,
kelly_fraction numeric(6,4),
top_features jsonb not null, -- [{name, value, contribution}, ...]
risk_checks jsonb not null, -- [{name, passed, threshold, actual}, ...]
suggested_size integer,
actual_size integer,
counterfactual_pnl numeric(12,4), -- backfilled at resolution
order_id uuid references orders(order_id)
);
create index on signal_decisions (market_id, fired_at desc);
create index on signal_decisions (strategy_id, fired_at desc);
On feature attribution. For a linear or logistic model, write
feature_name × coefficient × value for the top contributors. For a gradient-boosted
model use SHAP. For a neural net, integrated gradients or input-perturbation
attribution. The drawer never displays more than the top five.
Backfill is your friend. When signal_decisions lands, ship a backfill job
that reconstructs the last 30 days from logs. The drawer has something to show on day one instead of
waiting for new fills to accumulate.
7.Engineering practicalities #
7.1 State management #
The dashboard trap is re-rendering the entire React tree on every tick. Use a fine-grained reactive store
(Zustand, Valtio, or signals) keyed by market_id, so the header strip re-renders
independently of the leaderboard, which re-renders independently of the chart.
7.2 The chart is not React #
Use a single <canvas> with imperative draws. React's reconciliation cost
is too high for sub-second tick updates with hundreds of marks. The mockup deliberately uses canvas — keep
that pattern.
7.3 Websocket discipline #
One socket per page, multiplexed by market_id and strategy_id
on the server side, dispatched to subscribers on the client. Never one socket per widget.
7.4 Materialised views for leaderboard stats #
Do not compute Sharpe in the request path. Pre-aggregate every minute, refresh on demand if you want immediacy.
7.5 Honest readiness states #
The five-state taxonomy still applies: docs-complete → demo-wired → shadow-ready → runtime-live → production-live.
A dashboard surface that displays decision counterfactuals is shadow-ready, not production-live,
until backfill, replay, and the kill switch have all run hot for two weeks without a regression. See
Start here.
8.If you only have one sprint #
Build the header strip + leaderboard + fill glyphs + kill switch — items 1–4 of Phase 1. That alone takes the screen from viewer to console and reuses 100% of the data we already have. Everything else is upside.