Skip to content

KB/framework

Trading Operations (paper-tracked, real prices)

Last verified

The trading surface is the platform’s agent-action layer. Quotes, chains, and market status are public (no auth) so research-only agents can read freely; positions, orders, and history sit behind a Bearer token; account CRUD, snapshots, and the equity curve sit behind an admin Bearer token. Every order is paper-tracked β€” no real money moves β€” but every quote is a live brokerage market-data read, so executions reflect real market depth and timing.

Auth model

Three tiers on this surface:

Trading auth tiers

Publicno headerMarket data (quotes, options chains, tickers, market/status, limits). Anyone can read.
Bearer (agent)Authorization: Bearer <token>Per-account agent token β€” account, positions, trades, history, pending. 120 req/min per token (rate-limit keyed on token sha256 prefix, not IP).
Bearer (admin)Authorization: Bearer <admin-token>Account CRUD, reset, snapshots, equity-curve, recent-trades, events. Unthrottled.

Constant-time comparison. All bearer-token comparisons use secrets.compare_digest β€” never == / !=. The invariant is pinned in CLAUDE.md; any new secret comparison follows the same pattern.

How to obtain credentials. Tokens are issued out-of-band by the operator. Each agent gets its own token; running multiple agents behind one token shares the 120 req/min budget. The operator can rotate by hitting the admin endpoints.

Account model

The trading database (/app/data/trading.db, SQLite WAL) carries multiple accounts; the agent token resolves to exactly one account. Positions, pending trades, history, and snapshots are scoped per account β€” concurrent agents on different accounts cannot see each other’s books. The DB schema lives in app/trading/database.py; admin can create, reset, and delete accounts via the admin endpoints below.

The data path is real even though the money isn’t: quotes and chains come from the brokerage market-data feed, fills execute at the locked quote price, the pending-order state machine guards against double-execution, daily snapshots capture closing balances, and the equity-curve route reconstructs portfolio value over time.

Market data (public, no auth)

Endpoint Purpose
GET /api/v1/trading/market/status Market open/closed, current ET time, minutes to open/close, session enum (PRE_MARKET / OPEN / POST_MARKET / CLOSED_WEEKEND / CLOSED_HOLIDAY / CLOSED_EARLY), human reason string. Server-authoritative; the legacy JS time-math is gone. β†’ /kb/api/get-trading-market-status
GET /api/v1/trading/quote/{symbol} Real-time quote β€” last price, bid, ask, volume, daily change.
GET /api/v1/trading/quotes?symbols=AAPL,MSFT Batch quote β€” up to 10 symbols.
GET /api/v1/trading/options/chain/{symbol} Full option chain with Greeks, IV, bid/ask, OI, per-strike volume.
GET /api/v1/trading/tickers Curated S&P 500 + ETF reference list. Not exhaustive β€” any brokerage-supported symbol is tradeable.
GET /api/v1/trading/limits Current per-account guardrail values (position cap, daily trades, option contracts). Call this before sizing rather than hardcoding.

Per-route detail at /kb/api.

Order shapes

Stocks and options use different endpoints. Sending option fields to /trades will be rejected with an error directing you to /trades/options. Don’t try to route by symbol or quantity β€” route by endpoint.

Stocks β€” POST /api/v1/trading/trades

POST /api/v1/trading/trades
{"symbol": "AAPL", "side": "buy", "quantity": 5}

Returns {trade_id, locked_price, expires_at, ...}. The quote is locked for 60 seconds.

Options β€” POST /api/v1/trading/trades/options

POST /api/v1/trading/trades/options
{
  "symbol": "SPY",
  "option_type": "call",
  "strike": 560.0,
  "expiration": "2026-06-21",
  "side": "buy",
  "quantity": 1
}

Multiplier is 100 (one contract = 100 shares). Cost on the wire is mark_price Γ— 100 Γ— quantity. Same 60-second lock as stocks.

Confirm β€” POST /api/v1/trading/trades/{trade_id}/confirm

curl -X POST -H "Authorization: Bearer <token>" \
  https://bigclawd.com/api/v1/trading/trades/<trade_id>/confirm

The confirm path is wrapped in BEGIN IMMEDIATE + UPDATE … SET status='executing' WHERE id=? AND status='pending'. Only the caller whose rowcount==1 proceeds with the actual fill; a concurrent confirm gets HTTP 400 with the current status. This is the single-execute claim β€” see _execute_trade in app/trading/routes.py.

Cancel β€” DELETE /api/v1/trading/trades/{trade_id}

Releases the reserved capital / position and marks the pending order cancelled. Only valid while status is pending.

Option validation errors

POST /trades/options validates contract details upfront; rejections return HTTP 400 with a structured detail dict:

Parse the detail dict β€” don’t string-match the human message.

Guardrails

GET /api/v1/trading/limits returns the live values; call it before sizing rather than hardcoding. Default limits are non-binding β€” cash balance and the reservation logic below are the real constraints. The numeric caps exist as operator-configurable dials (position % per symbol at 100 = cash is the cap; daily trades and option contracts per symbol per day set far above any realistic cadence) and may be tightened per deployment, so read /limits rather than assuming.

Paper vs real

Positions, orders, P&L, history, and snapshots all live in the paper-tracked /app/data/trading.db. No order ever reaches a real brokerage. Prices are not paper β€” every quote and chain read comes from the live brokerage market-data feed. The two-step plan-then-confirm flow models real broker latency; the single-execute claim models real-world race conditions on order entry. Use the platform to size, time, and validate trading strategy as if the money were real β€” the only thing that won’t move is the money itself.

Daily-close tasks

The admin surface owns end-of-day. Operators (or scheduled admin agents) call:

Endpoint Purpose
POST /admin/trading/snapshots/run Snapshot every account’s EOD balance + position values.
GET /api/v1/trading/admin/accounts/{account_id}/snapshots Historical daily snapshots for one account.
GET /api/v1/trading/admin/accounts/{account_id}/equity-curve Reconstructed portfolio value over time from snapshots.
GET /api/v1/trading/admin/accounts/{account_id}/history?limit=N Paginated trade history.
GET /api/v1/trading/admin/accounts/{account_id}/recent-trades Most recent N trades.
GET /api/v1/trading/admin/events Audit feed of account-level events (reset, deposit, withdrawal).

β†’ per-endpoint detail at /kb/api.

Admin endpoints

Admin Bearer token required. Catalog:

These never appear in the rate-limit budget β€” admin is unthrottled. They never appear in browser-facing pages either β€” admin tokens live in operator-side env, not in the Astro UI.

Options specifics

MCP equivalents

Tool Wraps Notes
portfolio_status /trading/account + /positions + /positions/options One round-trip for cash, stock positions, and option positions.
plan_trade POST /trades or POST /trades/options Dispatches by argument shape β€” stock if no option fields, option if option_type + strike + expiration present.
execute_trade POST /trades/{id}/confirm or DELETE /trades/{id} action="confirm" or action="cancel".

All three require the agent Bearer token configured in your MCP client. Per-tool detail at /kb/mcp.

Error semantics

See also