Broker terminal for options portfolio management.
Xenon places orders, tracks fills, surfaces P&L and Greeks attribution, and enforces the naked-short guard against Interactive Brokers. Bring your own thesis — no scanners, no signal generation, no strategy recommendations.
- Interactive Brokers (primary) — quotes, chains, execution, portfolio. Never bypassed.
- Futu OpenD (read-only) — positions snapshot from a local Futu OpenD instance. Surfaces as a separate account tab in the terminal. No order execution and no quote feed; it reads positions and historical deals (
xenon-futu-history-sync). Silent degrade when OpenD is unreachable.
Every execution and portfolio row carries broker, account_env, and broker_account columns so paper and live data never blend in a shared Postgres. Scope resolves via AccountScope (src/xenon/execution/account_scope.py); FastAPI depends on get_account_scope; sync subprocesses read XENON_TRADING_MODE + XENON_BROKER_ACCOUNT.
The only hard-enforced trading rule. Every short call must be covered by long shares (1 contract = 100 shares) or by long calls at the same expiry. Cash-secured puts are allowed. Combos that net to uncovered short calls are blocked at three layers:
- UI pre-submission —
web/lib/nakedShortGuard.ts - API gate —
POST /api/orders/placereturns 403 on violation - Post-sync audit — runs after every
ib_sync(naked_short_audit.py). Shadow mode by default: it logs and recordsservice_health, nothing is cancelled. WithXENON_AUDIT_CANCEL=1on the API, it cancels confirmed violators that the order owner placed. Thexenon-naked-short-auditCLI is an offline check and never cancels.
Full enforcement matrix: src/xenon/CLAUDE.md § Naked Short Protection.
Prerequisites
- Python
3.13viauv(3.14 has anib_async/eventkitincompatibility) - Node.js
22(what CI and the Docker images use;web/package.jsonhas noenginespin) - Interactive Brokers Gateway running for the target mode (paper on
127.0.0.1:4002, live typically on a remote:4001) - Postgres reachable on the configured
DATABASE_URL - (Optional) Futu OpenD — read-only positions for a Futu account tab
git clone https://github.com/moremeds/xenon.git
cd xenon
uv sync --extra test
cd web && npm install && cd ..
# Paper (local IB Gateway on :4002)
scripts/infra/dev.sh paper
# Live (remote IB Gateway, host from .env)
scripts/infra/dev.sh liveDev stack ports: terminal http://localhost:3200, FastAPI 8421 (curl http://localhost:8421/health), realtime WS 8866. Production runs as a Docker stack on the Mac mini and publishes 3000 / 8321 / 8765 on 127.0.0.1 (see Release and deploy below).
dev.sh does not edit .env and does not start IB Gateway — that step stays manual. It refuses to start without XENON_INTERNAL_API_TOKEN (env or .env) and takes only the mode (paper or live) as an argument.
.env (root) — Python services, database, auth:
XENON_TRADING_MODE=paper # paper (port 4002) or live (port 4001)
XENON_PAPER_ACCOUNT=DU0000000 # DU* prefix required for paper
# XENON_LIVE_ACCOUNT=U1234567 # U* prefix required for live
# dev.sh refuses to start when DATABASE_URL points at core_dev (prod).
# Role: xenon_app today; xenon_dev (read-only on core_dev, owner of core_test) after
# step 3 of docs/runbooks/dev-prod-db-cutover.md. See .env.example.
DATABASE_URL=postgresql+asyncpg://xenon_app:<app-pw>@<db-host>:5432/core_test
# pytest only: a separate, existing LOCAL database (your OS user has CREATEDB for xdist clones) whose name contains "test" (not core_dev,
# not the same database as DATABASE_URL), e.g. xenon_test. pytest refuses to start otherwise
# (src/xenon/_test_db.py); per-worker clones are made from it.
DATABASE_URL_TEST=postgresql+asyncpg://<local-user>:<local-pw>@127.0.0.1:5432/xenon_test
# Next -> FastAPI shared secret (required; FastAPI fails closed without a credential)
XENON_INTERNAL_API_TOKEN=...
# Optional read-only machine keys (X-API-Key)
# XENON_QUERY_API_KEY=...
# MDW_API_KEY=...
# Optional — unlocks the Historical Trades panel (IB Flex Query audit overlay)
IB_FLEX_TOKEN=...
IB_FLEX_QUERY_ID=....env.example lists every variable with comments.
web/.env — Next.js terminal (api/server.py loads it too in dev):
ANTHROPIC_API_KEY=... # portfolio Q&A chat
UW_TOKEN=... # Unusual Whales: ratings, ticker info/news/seasonality, portfolio report
EXA_API_KEY=... # optional ticker-page enrichmentWhen IB_FLEX_TOKEN / IB_FLEX_QUERY_ID are unset, the Historical Trades panel renders an empty state with a setup hint and xenon-blotter-history exits gracefully with configured=false.
Real-time trading terminal built with Next.js 16. Streams IB prices, computes live greeks, visualizes portfolio exposures, and drives order management.
Core capabilities
- Real-time price streaming with live greeks (shared BID / MID / ASK / SPREAD layout across ticker, chain, and modify views)
- Multi-leg position monitoring with per-leg P&L and structure classification (verticals, straddles, synthetics, covered calls, risk reversals, butterflies / condors)
- Multi-broker account tab bar: switch between IB (live trading) and Futu (read-only positions snapshot)
- YTD portfolio performance with ET-session refresh guarding against stale snapshots
- Combo spread order workflows with natural-market pricing
- Historical Trades panel (IB Flex Query audit overlay — optional)
- AI chat for portfolio Q&A and command execution
Access control: production is tailnet-only and there is no user login. web/middleware.ts guards /api/** against cross-site browser requests (Host allowlist, fetch-metadata and Origin checks). FastAPI is fail-closed and credential-only: Next sends X-Internal-Token (XENON_INTERNAL_API_TOKEN); machine clients use path-scoped X-API-Key keys; /health is the only exempt path. The realtime WebSocket uses 30s single-use tickets. Details: src/xenon/api/CLAUDE.md § Auth.
Component-level reference: docs/reference/web-ui-reference.md.
All entry points are installed by uv sync and invoked as uv run <name> [args].
| Group | Commands |
|---|---|
| Server | xenon-api (FastAPI dev launcher) |
| IB reads | xenon-ib-orders · xenon-ib-option-chain · xenon-ib-market-depth · xenon-ib-option-greeks |
| Sync / reconcile | xenon-ib-sync · xenon-ib-reconcile · xenon-futu-sync · xenon-futu-history-sync · xenon-futu-statement-sync · xenon-nav-flex-refresh · xenon-nav-reconcile |
| Risk / audit | xenon-naked-short-audit (offline, never cancels) |
| Reports | xenon-portfolio-report · xenon-portfolio-attrib |
| Trade log | xenon-blotter · xenon-blotter-history |
| Utilities | xenon-market-hours |
Orders are not placed from a CLI: place, cancel and modify run inside the API on one order owner connection.
Full descriptions: src/xenon/CLAUDE.md.
IB Gateway ─┐
├─► FastAPI bridge ─► Postgres ─► Next.js terminal ─► User orders
Futu OpenD ─┘ (xenon.api.server) (snapshots, (web/) │
(read-only) orders, │
fills, ▼
journal) IB Gateway
src/xenon/— Python services (FastAPI, IB / Futu clients, execution, reports). Background work (IB activity mirror, Wizard Risk Alert, boot reconcile) runs inside the FastAPI lifespan.web/— Next.js terminal (portfolio, orders, AI chat)brand/— design system and tokensdata/— runtime artifacts (gitignored)docs/— architecture, runbooks, references (index)
Postgres is the runtime source of truth for portfolio, orders, fills, and journal surfaces. The terminal does not silently fall back to JSON files on runtime read paths — that boundary is enforced by CI (scripts/checks/no_json_fallback_on_order_path.py) plus a caller allowlist for xenon.execution.ib_place_order / ib_order_manage (scripts/checks/order_path_caller_allowlist.py).
Deeper reading:
- High-throughput design and WS relay:
docs/architecture/architecture.md - FastAPI, auth, IB Gateway modes:
docs/architecture/api-infrastructure.md - Postgres as source of truth:
docs/architecture/production-database-strategy.md - Full order lifecycle (HTML + Markdown):
docs/architecture/order-stack-end-to-end.md - Order-path incident history:
docs/reference/order-path-incident-history.md
| Source | Role |
|---|---|
| Interactive Brokers | Real-time quotes, chains, portfolio, execution, fills (primary — never bypassed) |
| Futu OpenD | Read-only positions snapshot for the Futu account tab |
| Unusual Whales | Dark-pool flow for xenon-portfolio-report; analyst ratings (GET /ticker/ratings); ticker info, news, seasonality and previous-close routes |
| IB Flex Query | Optional historical fills audit overlay (xenon-blotter-history) |
Never Yahoo Finance.
- Python —
pytest(execution, reports, adapters, FastAPI routes) - Frontend — Vitest (web logic, order-path harness)
- E2E — Playwright (browser workflows)
uv sync --extra test # one-time / after dep changes
uv run python scripts/infra/dev/run_pytest_affected.py # scoped — only affected tests (preferred)
uv run pytest # full suite
cd web && npm test # Vitest
cd web && npx playwright test # E2EOrder-route integration tests use web/tests/fastapiHarness.ts with XENON_API_TEST_MODE to stub broker calls — no live IB required.
| Service | Purpose |
|---|---|
| IB Gateway (cloud / Docker / launchd) | Broker session for quotes, execution, reports |
| FastAPI lifespan tasks | IB activity mirror, Wizard Risk Alert; Flex token expiry on GET /health |
Ops runbooks: ib-connection-troubleshooting · ib-gateway-docker · remote-deploy · ops · release.
VERSION (root) is the source of truth and is kept in parity with package.json by CI (scripts/release/version_sync_check.py). Releases go through a PR; never push master. The tag is pushed alone and release.yml builds the images:
./scripts/release/cut.sh # interactive: patch / minor / major / custom
# then: move the commit to release/vX.Y.Z, open and merge the PR,
# pause Watchtower on the mini and confirm it is stopped,
# tag the merge commit, git push origin refs/tags/vX.Y.Z (steps: docs/runbooks/release.md)Production is a Docker stack on the Mac mini (deploy/compose.prod.yml, images from GHCR). A merge alone never deploys; the tag push does. Cutover steps: docs/runbooks/remote-deploy.md § Standard release flow. Deploy truth and scheduled jobs: deploy/README.md.
Changelog: CHANGELOG.md.