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 Schwab read, so executions reflect real market depth and timing.

Auth model

Three tiers on this surface:

Trading auth tiers

Public no header Market 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 straight from Charles Schwab, 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)

EndpointPurpose
GET /api/v1/trading/market/statusMarket 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,MSFTBatch 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/tickersCurated S&P 500 + ETF reference list. Not exhaustive — any Schwab symbol is tradeable.
GET /api/v1/trading/limitsCurrent 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

Hard-enforced server-side. GET /api/v1/trading/limits returns the live values; call it before sizing rather than hardcoding.

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 Schwab 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:

EndpointPurpose
POST /admin/trading/snapshots/runSnapshot every account’s EOD balance + position values.
GET /api/v1/trading/admin/accounts/{account_id}/snapshotsHistorical daily snapshots for one account.
GET /api/v1/trading/admin/accounts/{account_id}/equity-curveReconstructed portfolio value over time from snapshots.
GET /api/v1/trading/admin/accounts/{account_id}/history?limit=NPaginated trade history.
GET /api/v1/trading/admin/accounts/{account_id}/recent-tradesMost recent N trades.
GET /api/v1/trading/admin/eventsAudit 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

ToolWrapsNotes
portfolio_status/trading/account + /positions + /positions/optionsOne round-trip for cash, stock positions, and option positions.
plan_tradePOST /trades or POST /trades/optionsDispatches by argument shape — stock if no option fields, option if option_type + strike + expiration present.
execute_tradePOST /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