Raxx · internal docs

internal · gated ↑ index

MBT — Raxx-native paper-trading engine

Status: Draft Owner: software-architect Last updated: 2026-04-22 Parent epic: #183 — Multi-tenant architecture (reframed) Supersedes: PR #184 (multi-tenant Alpaca OAuth-for-everyone architecture) Related ADRs: 0002, 0003, 0013, 0008 (Superseded), 0009 (Superseded), 0010 (Superseded), 0011 (Superseded)


1. Context + goal

Raxx (backend Raptor, frontend Antlers, algo layer MQ-A) is moving from single-operator tool to multi-tenant SaaS. The prior design (PR #184) routed every user through Alpaca OAuth for paper + live. That over-depends on Alpaca: their paper trading is a simulation against their market data, and we can run the simulation ourselves. Per-user broker OAuth as a paper-trading pre-requisite adds credential-storage tension and onboarding friction for the majority of users who never need a live broker.

This doc specifies MBT, the Raxx-native paper-trading engine — an internal codename alongside Raptor / Antlers / MQ-A; do not expand the acronym. MBT owns accounts, positions, orders, fills, and activity history for every free and Pro user. Alpaca narrows to: (a) shared server-side market-data provider, and (b) optional per-user live-broker handoff for Pro+ users graduating from paper. Most users never touch Alpaca credentials.

The shift restores the "no stored credentials" invariant for the majority, makes tier-gated paper-history retention trivial (our DB), gives us data sovereignty, and keeps Raxx permanently out of BD / Broker-API territory.

2. Invariants that apply

From auth.md §2 and ADR 0002/0003:

  1. No stored credentials. Restored fully for MBT users — no broker token to store. The ADR 0009 exception narrows to the Pro+ live-handoff subset in ADR 0014.
  2. Passkeys / WebAuthn only. Step-up required for live-handoff enrollment and every live order submit.
  3. Paper-first gating. MBT is paper-only. Graduation to live Alpaca requires N-cycle paper-profitability or explicit audited override.
  4. GDPR by default. MBT account, position, order, and fill rows are user PII — exportable, erasable (30-day cooling), retention-bounded per tier.
  5. Audit trail for every state change — every submit, modify, cancel, fill, corporate action writes an audit_log row.
  6. Credentials into infra. Market Data key + live-handoff OAuth client secret live in the secret store.

If any piece of this design reads as violating an invariant, treat it as a bug in the doc.

3. v1 scope + v2+ roadmap

Target: parity-with-Alpaca-paper for the 90% options-forward retail use case (Persona: Weekly-Income Pat). Honest split:

Ships in v1:

Deferred to v2+: trailing stops, PDT detection, stock splits / mergers / symbol changes, DRIP, short-borrow fees, calendar + diagonal spreads, portfolio margin, extended-hours, partial fills, futures, crypto.

Users are told upfront what's simulated. "Simulated paper trade with realistic but not perfect fills" is the honest frame.

4. Order-matching model

Single-book-per-user simulator — orders do not match against other users. Orders match against real Alpaca market-data quotes in simulated time.

Fill simulation is a pure function of (order, nbbo_tick_history, account_state) — deterministic replay for backtests and audit ("why did this fill at $X").

5. Data model

New tables in backend_v2/db/; schemas are sketches, final migration in the implementation sub-card.

mbt_accounts
  id PK, user_id FK -> users.id ON DELETE CASCADE, account_type ('cash'|'margin'),
  cash_balance, buying_power (derived), equity (derived),
  status ('active'|'restricted'|'closed'), created_at
  UNIQUE (user_id)   -- one MBT account per user in v1

mbt_positions
  id PK, account_id FK, symbol, asset_class ('equity'|'option'),
  qty (signed; negative = short), avg_entry_price, opened_at, closed_at NULL

mbt_orders
  id PK, account_id FK, client_order_id (idempotency key), parent_order_id (bracket/OCO),
  symbol, asset_class, side ('buy'|'sell'), qty,
  order_type ('market'|'limit'|'stop'|'stop_limit'), limit_price NULL, stop_price NULL,
  time_in_force ('day'|'gtc'|'ioc'|'fok'),
  status ('new'|'accepted'|'partially_filled'|'filled'|'canceled'|'rejected'|'expired'),
  submitted_at, accepted_at NULL, filled_at NULL, canceled_at NULL, reject_reason NULL,
  legs JSONB NULL   -- multi-leg: [{symbol, side, qty, ratio}]

mbt_fills
  id PK, order_id FK, symbol, qty, price, filled_at,
  nbbo_bid, nbbo_ask   -- snapshot at fill time for replay / audit

mbt_activities
  id PK, account_id FK,
  activity_type ('fill'|'dividend'|'expiration'|'adjustment'|'interest'|'system_nudge'),
  symbol NULL, amount, ref_id NULL, occurred_at

mbt_eod_snapshots
  id PK, account_id FK, as_of_date DATE, equity, cash, positions_json JSONB
  UNIQUE (account_id, as_of_date)

Retention per docs/marketing/pricing.md: Free 90d / Pro 3yr / Pro+ unlimited. Nightly Celery purge. Orders, fills, positions follow the same tier cap (they're the source of activities).

GDPR erasure: users.id delete cascades through MBT tables. After 30-day cooling (ADR 0003), hard-deleted. audit_log rows persist with hashed actor id for 2 years.

Absent by design: no broker-credential column, no api_key, no access_token. ADR 0002 CI grep covers backend_v2/ wholesale.

6. APIs

REST endpoints under /api/mbt/*, session-cookie authenticated. Contracts mirror common trading-API shapes (Alpaca, IBKR) so future iOS + console surfaces reuse without re-designing.

Method Path Purpose
GET /api/mbt/account Cash, buying_power, equity, status.
GET /api/mbt/positions List open positions.
GET /api/mbt/positions/{symbol} Single position.
GET /api/mbt/pnl?from=...&to=... Realized + unrealized P&L for window.
POST /api/mbt/orders Submit. Body: {symbol, side, qty, order_type, limit_price?, stop_price?, time_in_force, legs?, client_order_id?}.
GET /api/mbt/orders?status=... List (paginated, filterable).
GET /api/mbt/orders/{id} Single order.
DELETE /api/mbt/orders/{id} Cancel (idempotent).
PATCH /api/mbt/orders/{id} Replace (change limit/stop/qty).
GET /api/mbt/activities?type=...&from=...&to=... Paginated activity stream.
GET /api/mbt/activities/{id} Single activity.

Envelope: {"data": ..., "meta": {...}}. Errors: {"error": {"code": "...", "message": "...", "request_id": "..."}}. Rate limits per tier (§9). client_order_id unique per account — duplicate submit returns original order.

7. Market data integration

One server-side Alpaca Market Data API key in the secret store (ALPACA_MARKET_DATA_KEY / ..._SECRET). Raxx's account, not per-user. Consumed by MBT fills, charts, MQ-A bar-close callbacks, and backtests.

Tier gating (enforced at the market-data service boundary, not Antlers):

Scaling note: Alpaca caps one WebSocket per API key; at scale, add a second key or entitlement upgrade. MBT itself does not need per-user streams — it shares the top-of-book charts consume.

8. Async jobs — Celery + Redis

MBT background work: EOD mark-to-market, retention purges, dividend crediting, option expirations, fill notifications, backtests, GDPR exports.

v1 choice: Celery 5 + Redis 7 (broker + result backend). Temporal / Cadence deferred to v2 for stateful long-running workflows.

Why Celery + Redis over Temporal for v1:

Task inventory (indicative): mbt.eod_mark_all (nightly), mbt.purge_expired (hourly), mbt.credit_dividends (daily ex-date), mbt.expire_options (market close), mbt.send_fill_notification (per fill), mbt.run_backtest (user-triggered), mbt.export_user_data (GDPR), mbt.accrue_margin_interest (nightly), mbt.evaluate_goal_streak (per cadence close).

9. Session engine

Full design in session-engine.md. Summary: REST endpoints POST /api/sessions, GET /api/sessions/current, DELETE /api/sessions/current, POST /api/sessions/refresh, POST /api/sessions/step-up. JWT + HttpOnly; Secure; SameSite=Strict cookie; server-side session row in Redis for instant revocation; per-tier rate limits (Free 60 / Pro 600 / Pro+ 3000 req/min per docs/marketing/pricing.md). MBT consumes sessions via standard middleware.

10. Migration path (current state → MBT)

Current state: one shared Alpaca paper account keyed to operator's env credentials; positions + fills live in Alpaca, not our DB.

  1. Phase 0 — prep. MBT schema migration lands (tables created, no writes). Service layer, REST endpoints, Celery tasks land behind FLAG_MBT_PAPER_TRADING=off. One-shot importer built: pulls each current user's Alpaca paper state into MBT.
  2. Phase 1 — shadow double-write. FLAG_MBT_PAPER_TRADING=shadow globally. Every Alpaca paper write also writes MBT. Reads still Alpaca. Nightly mbt.verify_parity job diffs computed position/equity vs Alpaca's; discrepancies page ops.
  3. Phase 2 — flagged cutover. Per-user flag flip to on for allowlisted users. Reads + writes go to MBT; Alpaca paper stops being written for that user. Prior Alpaca history imported once into mbt_eod_snapshots.
  4. Phase 3 — GA. Global flip. Alpaca paper writes disabled; paper credentials removed from env. Alpaca Trading API integration narrows to Market Data (server-side) + Pro+ live-handoff (ADR 0014).

Rollback: per-user flag flips back to Alpaca-paper reads through Phase 2 (MBT continues shadow-writing so no state is lost). After Phase 3, rollback is a partial-restore from MBT backups.

11. Rollout plan

Standard dark → shadow → beta → GA gated by FLAG_MBT_PAPER_TRADING:

  1. Dark — migrations + code landed; flag off; CI covers code paths; no user traffic.
  2. Shadow — flag=shadow globally; ≥ 2 weeks; parity diff < 0.1% across 10k orders.
  3. Beta — flag=on for allowlisted users (Kris + self-selected early adopters); ≥ 2 weeks; active feedback on UI, fill realism, P&L.
  4. GA — flag=on globally; Alpaca paper writes disabled.

Flag is flippable per-user or global. Kill switches: MBT_TRADING_DISABLED=1 (503 on all order submits, read-only mode), MBT_NEW_ORDERS_DISABLED=1 (position views + cancels still work, new submits blocked — useful if market data goes stale).

12. Security considerations

13. Regulatory posture (flags, not solutions)

Longer-term vision includes a "platform where users funnel money and follow Kris's trading methodology." That raises Investment Advisers Act §202(a)(11) + copy-trading questions. Flagging now; do not design product around this without securities counsel.

Flag to user: engage securities counsel before any commitment past "user-directed simulation + educational content." v1 MBT stays in the safest lane deliberately.

14. Open questions

Qs 1–3 RESOLVED via the investor-profile model. See mbt-investor-profiles.md and ADR 0015.

Q1 — Starting cash balance default: RESOLVED. Not a single fixed value. Starting cash is profile-driven: Trial = $10k, Income Builder = $50k, Diversifier = $100k. User-overridable within tier cap. Existing users without a profile default to Diversifier ($100k) on FLAG_MBT_INVESTOR_PROFILES activation.

Q2 — Multi-leg fill aggressiveness: RESOLVED. Defaults are profile-driven: Trial = conservative (worse-of-mid/offer), Income Builder = mid, Diversifier = aggressive (mid + attempt on touch). Surfaced as a per-order "Fill preference" toggle in Antlers; visibility scaled by profile.

Q3 — Interest rate on cash / margin-interest model: RESOLVED. Margin interest is a teaching moment, not a config knob. When MBT accrues simulated margin interest, Antlers surfaces a narrative card: the interest amount, a plain-language "why this exists" explanation, and a tax-strategy note (deductibility; consult CPA). Narrative verbosity is profile-tuned. See mbt-investor-profiles.md §7.

Qs 4–7 remain open:

  1. Backtest parity. Unify existing backtest engine with MBT (backtest = MBT against historical feed) in v1 or v2? Scope-defining.
  2. Live-handoff scope. Pro+ only, or Pro too? Pricing doc implies Pro gets "Alpaca live + 1 other broker."
  3. Market-data capacity ceiling. At what user count does one Alpaca data account become insufficient? Alpaca-partnerships inquiry.
  4. Options chain archive source for Pro+ (2+ yr). Alpaca's options history is thin; ORATS / Polygon / CBOE candidates. Blocks Pro+ GA, not MBT v1.

product-manager may now file implementation sub-cards for account creation, order flow, and profile onboarding. Qs 4–7 do not block v1 core sub-cards.


End of doc. See ADR 0013 for the MBT decision record, ADR 0014 for the Alpaca scope reframe, ADR 0015 for the investor-profile decision, and docs/architecture/session-engine.md for the session + rate-limit spec.