# SonarQuant — paper-trading sandbox for AI agents A live, isolated sandbox where an agent trades PAPER money against real market mechanics: orders rest one bar (no same-bar fills), gaps fill at the open, costs are always adverse, and one bar covering both a stop and a target resolves to the stop — flagged. Every event is journalled; every number is recomputable from the journal. ## Auth Every /v1 endpoint takes `Authorization: Bearer `. Keys have `read` or `trade` scope; placing orders, feeding bars and creating runs need `trade`. ## The authoring loop (Mode A — you write rules) 1. GET /v1/triggers what you can express 2. GET /v1/schema the strategy document shape 3. POST /v1/validate findings with JSON pointers + the English rendering — show that rendering to your human 4. POST /v1/backtest same engine as live, fed history 5. POST /v1/runs {mode:"rules", strategy} ## The direct loop (Mode B — you decide at runtime) 1. POST /v1/runs {mode:"direct", symbol, capital, guards?} 2. GET /v1/runs/{id}/positions state 3. POST /v1/runs/{id}/orders {side, qty, type, rationale} — `rationale` is journalled; write down WHY 4. GET /v1/runs/{id}/stream SSE; resume with ?after_seq= Your orders rest until the next bar, exactly like a rule's. You do not get a faster market than a strategy document. ## All endpoints GET /v1/triggers · GET /v1/schema · POST /v1/validate · POST /v1/backtest POST /v1/runs · GET /v1/runs · GET /v1/runs/{id} · POST /v1/runs/{id}/bars · POST /v1/runs/{id}/orders · DELETE /v1/runs/{id}/orders/{order_id} · GET /v1/runs/{id}/positions · GET /v1/runs/{id}/fills · GET /v1/runs/{id}/events?after_seq= · GET /v1/runs/{id}/metrics · GET /v1/runs/{id}/stream · POST /v1/runs/{id}/stop ## Retries and limits Send `Idempotency-Key: ` on any POST/DELETE you might retry (place_order above all): the same key + same body replays the original response instead of re-executing, marked `idempotency-replayed: true`. The same key with a DIFFERENT body is refused — that is a bug in your caller, not a retry. Requests are rate-limited per key; a 429 carries `Retry-After` in seconds, and MCP traffic gets a larger budget than REST. ## Read the metrics honestly Every metrics response carries `flags` (intrabar_ambiguous, data_gaps, low_liquidity) and `confidence`. A flagged run is not comparable to a clean one; `insufficient_history` means the sample is too small to be evidence, however good the Sharpe looks. ## Triggers - rsi.level (RSI against a threshold) requires: period, value - stoch.level (Stochastic %K against a threshold) requires: period, value - adx.level (ADX against a threshold — trend strength) requires: period, value - price.level (Price against an absolute level) requires: value - ma.price_vs_ma (Price against a moving average) requires: period, ma_type - ma.cross (One moving average against another) requires: period, slow, ma_type - macd.cross_signal (MACD line against its signal line) - bb.band (Price against a Bollinger band) requires: period, multiple - price.n_bar_high (New high over the trailing N bars, excluding this one) requires: period - price.n_bar_low (New low over the trailing N bars, excluding this one) requires: period - price.consecutive_up (N consecutive higher closes) requires: period - volume.spike (Volume at N times its trailing average, excluding this bar) requires: period, multiple - pos.is_open (A position is currently open) - pos.is_flat (No position is open) - pos.unrealized_pct (Open position's unrealised P&L against a threshold) requires: value - pos.age_bars (Bars since the position was opened, against a threshold) requires: value Edges: crosses_above / crosses_below fire ONCE on the transition; is_above / is_below stay true while the condition holds. There is no default edge — the wrong one is silent, so you must choose.