| name | quaq-viz |
| description | Use this skill when the user wants to work on the quaq visualization dashboard, the interactive web UI for inspecting strategy node graphs, OHLCV charts, and backtest metrics. Triggers include: launching the viz server, modifying the dashboard frontend (React/TypeScript), changing chart rendering (uPlot plugins, candlestick/volume/markers), editing the node graph view (React Flow, ELK layout), updating the metrics bar, adjusting the pure black theme, modifying the Zig HTTP backend (viz_server.zig), changing API endpoint payloads (/api/graph, /api/chart, /api/metrics), rebuilding frontend assets, embedding assets into the Zig binary, adding new chart panels or series types, customizing node styling or colors, debugging viz server issues, or configuring strategy outputs (series/markers) for chart display.
|
Quaq Viz Skill
Launch
cd core && zig build run -- viz path/to/strategy.toml
Serves on http://127.0.0.1:8420. Opens browser automatically on macOS.
The server runs the full engine pipeline (parse, validate, compile, run) on startup, precomputes all JSON payloads, then serves them statically. There is no live reload — restart the server after changing the strategy TOML.
Architecture
Two-layer system: Zig HTTP backend + React SPA frontend, compiled into a single binary.
Backend: core/src/viz_server.zig
- Runs full engine pipeline on the given strategy TOML at startup
- Precomputes three JSON payloads into an arena allocator (
VizPayload struct)
- Serves a minimal HTTP/1.1 server on port 8420 (single-threaded)
- Frontend assets embedded via
@embedFile("viz_assets/index.html") etc.
- No external dependencies, no separate web server process
Routes:
| Path | Content-Type | Source |
|---|
/ | text/html | viz_assets/index.html |
/assets/*.js | application/javascript | viz_assets/index.js |
/assets/*.css | text/css | viz_assets/index.css |
/api/graph | application/json | buildGraphJson() |
/api/chart | application/json | buildChartJson() |
/api/metrics | application/json | buildMetricsJson() |
Node Editor Endpoints
The viz server includes a full node editor backend for visual strategy editing:
| Method | Path | Purpose |
|---|
| GET | /api/editor/schema/catalog | Returns catalog of all block kinds with their specs (inputs, outputs, params) |
| POST | /api/editor/load | Parse & validate TOML, returns strategy doc + canonical TOML |
| POST | /api/editor/sync/from-toml | Sync editor state from TOML text changes |
| POST | /api/editor/sync/from-doc | Sync TOML from editor document changes, returns canonical TOML |
| POST | /api/editor/validate | Validate current strategy, returns issues |
| POST | /api/editor/run | Execute backtest with cached bars, returns {ok, run_id, fresh, graph, chart, metrics, issues} |
All POST endpoints accept JSON body. The editor maintains bidirectional sync between TOML text and the visual graph.
Key functions:
buildVizPayload(allocator, toml_bytes) -- full pipeline, returns VizPayload
buildGraphJson(arena, strategy, plan) -- nodes with category/params/ports/series_id, edges with from/to node+port, topo_order, run settings
buildChartJson(arena, chart) -- timestamps, OHLCV arrays, series (id/label/panel/dtype/data), markers (ts/kind/label/price/side/pnl)
buildMetricsJson(arena, metrics) -- total_return, max_drawdown, sharpe, winrate, profit_factor, total_trades, avg_trade_pnl, max_consecutive_losses, total_fees
deriveCategory(kind) -- maps node kind string to category for color coding
executeRun(payload, toml_bytes, allocator) -- hash dedup, parse, validate, compile, run with cached bars, update payload
Node Editor Workflow
The node editor provides bidirectional TOML ↔ graph synchronization:
- Load: POST TOML to
/api/editor/load → returns parsed strategy doc + canonical TOML
- Edit visually: Modify nodes/edges in the graph → POST doc to
/api/editor/sync/from-doc → get updated TOML
- Edit TOML: Modify TOML text → POST to
/api/editor/sync/from-toml → get updated doc
- Validate: POST to
/api/editor/validate at any time → get validation issues
- Run: POST to
/api/editor/run → executes backtest, returns full results
Auto-Run Mode
The editor supports auto-run mode where changes automatically trigger a backtest:
- TOML changes are debounced (500ms) before triggering a run
- The server deduplicates runs using a TOML content hash (
last_toml_hash)
- If the TOML hasn't changed, the server returns
fresh: false with cached results
- First run caches the loaded bars (
cached_bars) to avoid re-fetching on subsequent runs
- Toggle between auto and manual mode in the frontend
Schema Catalog
GET /api/editor/schema/catalog returns all registered block kinds with their full specifications:
- Input ports (name, dtype, required)
- Output ports (name, dtype)
- Parameters (name, kind, required, constraints)
- Mode compatibility (bar_only, bar_tick, all_modes, requires_l2)
Used by the frontend's "Add Node" panel to show available node types with their port/param details.
Frontend: viz/
React 18 + TypeScript, built with Vite. No state management library (useState + custom hooks only).
Component tree:
App
Header (strategy name, symbol, timeframe, mode tags)
MainPanels (60/40 grid split)
GraphPanel (left) -- React Flow + ELK
CustomNode -- per-node card with ports, params, sparkline
NodeDetail -- click-to-inspect overlay panel
MiniChart -- inline uPlot sparkline in node cards
ChartPanel (right) -- uPlot
Price section (candlestick + volume + overlay series + markers)
Equity section (equity + drawdown lines, conditional)
MetricsBar (bottom) -- horizontal metric tiles
Key files:
| File | Purpose |
|---|
viz/src/App.tsx | Root layout, data fetching orchestration |
viz/src/types.ts | TypeScript interfaces for all API responses, CATEGORY_COLORS map |
viz/src/api.ts | Three fetch functions: fetchGraph(), fetchChart(), fetchMetrics() |
viz/src/hooks/useVizData.ts | Parallel fetch of all three endpoints on mount |
viz/src/hooks/useELKLayout.ts | Converts GraphData to React Flow nodes/edges via ELK |
viz/src/lib/elk-layout.ts | ELK layered layout (direction=RIGHT, node width=200) |
viz/src/lib/uplot-plugins.ts | candlestickPlugin(), volumePlugin(), markerPlugin() |
viz/src/components/GraphPanel.tsx | React Flow canvas with custom node types |
viz/src/components/CustomNode.tsx | Node card: kind badge (colored), id, params, port labels, sparkline |
viz/src/components/MiniChart.tsx | Inline 140x36 uPlot sparkline for nodes with series_id |
viz/src/components/ChartPanel.tsx | Price + equity uPlot charts with synchronized zoom/pan |
viz/src/components/MetricsBar.tsx | Bottom bar: Return, Max DD, Sharpe, Win Rate, PF, Trades, Avg PnL, Fees |
viz/src/components/NodeDetail.tsx | Click-to-inspect panel showing node kind, category, params, ports, series |
viz/src/styles/index.css | All CSS: pure black theme, layout grid, node styles, React Flow overrides |
Frontend Build & Deploy
After modifying any file under viz/:
cd viz && npm install && npm run build
cp dist/index.html dist/assets/*.js dist/assets/*.css ../core/src/viz_assets/
cd ../core && zig build
The three files in core/src/viz_assets/ are what get @embedFiled into the binary. Vite config forces stable filenames (assets/index.js, assets/index.css) via rollupOptions.output.
For frontend dev with hot reload:
cd viz && npm run dev
Vite dev server proxies /api requests to http://localhost:8420 (configured in vite.config.ts). Run the Zig viz server separately in another terminal.
Theme
Pure black theme. CSS variables in viz/src/styles/index.css:
| Variable | Value | Usage |
|---|
--bg-primary | #000000 | Main background |
--bg-secondary | #0a0a0a | Header, metrics bar, node cards |
--bg-tertiary | #161616 | Tags, hover states |
--text-primary | #e2e8f0 | Main text |
--text-secondary | #8892a4 | Secondary text, params |
--text-muted | #4a5568 | Labels, ports, grid lines |
--border | #1a1a1a | All borders and dividers |
--accent | #3b82f6 | Logo, selection, focus |
--positive | #22c55e | Green candles, positive return, long entry markers |
--negative | #ef4444 | Red candles, drawdown, short/losing markers |
Node Category Colors
Defined in viz/src/types.ts as CATEGORY_COLORS:
| Category | Color | Hex |
|---|
| data | Blue | #3b82f6 |
| indicator | Green | #22c55e |
| operator | Amber | #f59e0b |
| trade | Red | #ef4444 |
| signal | Purple | #a855f7 |
| risk | Yellow | #eab308 |
| quant_p0/p1/p2 | Cyan | #06b6d4 |
Categories are derived server-side by deriveCategory() in viz_server.zig from the node kind prefix.
Chart Plugins
All in viz/src/lib/uplot-plugins.ts:
- candlestickPlugin -- draws OHLC candles in
drawClear hook (renders below series lines). Green (#22c55e) for up, red (#ef4444) for down. Bar width scales with visible bar count.
- volumePlugin -- draws volume bars in bottom 12% of chart area in
draw hook. 30% opacity. Color matches candle direction.
- markerPlugin -- draws trade entry/exit markers. Entries: triangles (up for long, down for short). Exits: X marks, colored by PnL sign.
- wheelZoomPlugin -- scroll=zoom centered on cursor, trackpad horizontal swipe=pan. Syncs across price and equity charts.
- dragPanPlugin -- click+drag=pan (cursor changes to grabbing). Syncs across price and equity charts.
Price and equity charts are synchronized: zoom/pan on one updates the other via syncPeers.
Graph Layout
ELK layered algorithm with these settings (in viz/src/lib/elk-layout.ts):
- Direction: LEFT to RIGHT
- Node spacing: 40px within layers, 80px between layers
- Node placement: NETWORK_SIMPLEX
- Node width: 200px fixed
- Node height: 80px base, 130px if node has a series (sparkline)
Nodes show: colored kind badge, node id, parameter values, input/output port labels, and an inline sparkline if the node has a bound series.
API Response Schemas
/api/graph
{
"strategy": { "name", "version", "description", "symbol", "timeframe", "mode" },
"nodes": [{ "id", "kind", "category", "params": {}, "inputs": [{"name","dtype"}], "outputs": [{"name","dtype"}], "series_id" }],
"edges": [{ "from_node", "from_port", "to_node", "to_port" }],
"topo_order": ["node_id", ...],
"run": { "initial_cash", "fee_bps", "slippage_bps", "warmup_bars", "fill_mode" }
}
/api/chart
{
"schema_version": "1.1",
"timestamps": [ms_epoch, ...],
"open": [...], "high": [...], "low": [...], "close": [...], "volume": [...],
"series": [{ "id", "label", "panel", "dtype", "data": [...] }],
"markers": [{ "ts", "kind", "label", "price?", "side?", "pnl?" }]
}
/api/metrics
{
"total_return", "max_drawdown", "sharpe", "winrate", "profit_factor",
"total_trades", "avg_trade_pnl", "max_consecutive_losses", "total_fees"
}
Strategy Outputs Configuration
To control what appears on the chart, configure [[outputs.series]] and [[outputs.markers]] in the strategy TOML:
[[outputs.series]]
id = "sma20"
label = "SMA 20"
from = "sma_fast.y"
panel = "price"
dtype = "f64"
style = "line"
[[outputs.series]]
id = "equity"
label = "Equity"
from = "__portfolio.equity"
panel = "equity"
dtype = "f64"
style = "line"
[[outputs.series]]
id = "drawdown"
label = "Drawdown"
from = "__portfolio.drawdown"
panel = "equity"
dtype = "f64"
style = "line"
[[outputs.markers]]
kind = "trade_entry"
[[outputs.markers]]
kind = "trade_exit"
Built-in portfolio series: __portfolio.equity and __portfolio.drawdown (always use panel = "equity").
Series with panel = "price" render as colored lines over the candlestick chart. Series with panel = "equity" render in the separate equity/drawdown panel below.
The equity panel only appears if at least one series has panel = "equity". Overlay series cycle through color palette: #3b82f6, #f59e0b, #a855f7, #06b6d4, #ec4899, #84cc16.
Common Modifications
Add a new API endpoint
- Add payload field to
VizPayload struct in viz_server.zig
- Build the JSON in a new
buildXxxJson() function
- Add route in
handleConnection() matching the path
- Add TypeScript interface in
viz/src/types.ts
- Add fetch function in
viz/src/api.ts
- Add to
useVizData.ts parallel fetch
- Rebuild frontend and copy assets
Add a new chart panel
- Add new panel name (e.g.
"rsi") to series config in strategy TOML
- Filter series by panel name in
ChartPanel.tsx (follow equity panel pattern)
- Create new
useRef, ResizeObserver, and uPlot instance
- Wire zoom/pan sync via
safeSyncPeers
Add a new node visual element
- Modify
CustomNode.tsx to render the new element
- Adjust
NODE_HEIGHT_BASE / NODE_HEIGHT_WITH_CHART in elk-layout.ts if height changes
- Add CSS classes in
viz/src/styles/index.css
Change the metrics bar
- Extend
MetricsData interface in types.ts
- Add field to
buildMetricsJson() in viz_server.zig
- Add metric tile in
MetricsBar.tsx