Raxx · internal docs

internal · gated

Paper Trading v1 — Decomposition and Engine-Gap Map

Status: Design complete; implementation complete; 3 follow-on gaps filed as sub-cards. Date: 2026-07-12 Owner: software-architect Parent epic: #3483 — Paper Trading v1 (close MBT engine gaps + ship unified Simulate surface) Refs: ADR-0108 (MBT fill engine), ADR-0110 (intraday bar feed), ADR-0138 (idempotency key), docs/architecture/mbt-paper-trading-engine.md, docs/architecture/2026-06-11-in-house-paper-trading.md


1. Context

Epic #3483 decomposed the paper-trading build into 15 architect sub-cards (SC-1 through SC-15) and 5 data-scientist gap cards (GAP-1 through GAP-5). All 20 cards are closed and merged. This document is the synthesis artifact: it maps what shipped, reconciles the design against the live code, records the 3 remaining gaps not covered by the original card set, and serves as the Wave 4 exit-criteria reference document (see docs/plan-to-production.md §Wave 4).


2. Invariants That Apply

All invariants from ADR-0108 apply without exception:


3. Data Model

Schema shipped across migrations 0022 (initial MBT tables), 0027 (position group ID), and 0035–0039 (SC-1 through SC-6 columns):

Table Migration Purpose
paper_accounts 0022 + 0038 Per-user cash balance, equity, total P&L, status. last_reset_at added in 0038 (SC-13 cooldown).
paper_positions 0022 + 0027 + 0035 Open and closed positions; current_price, market_value, unrealized_pl, position_group_id for MTM and IC grouping.
paper_orders 0036 Order lifecycle (new/accepted/filled/canceled/rejected/expired). Old table renamed to broker_paper_orders. Columns: parent_order_id, related_leg_orders JSONB, slippage, time_in_force.
paper_fills 0037 Fill records with nbbo_bid, nbbo_ask, slippage_basis for deterministic replay.
market_data_access_log 0037 Per-fetch log for Alpaca redistribution compliance; 13-month retention.
mbt_engine_state 0039 Key-value store for bar watermark (last_processed_bar_ts) used by the catch-up loop.

Reused tables (tax engine, no changes): tax_lots, wash_sale_lots, holding_period_classifications, lcc_strategy_config.

Migration invariants: All PL/pgSQL blocks carry the -- POSTGRES-ONLY sentinel. All migrations define op.drop_table rollback. Real-Postgres smoke tests ran for each migration per project policy.


4. Engine-Gap Map (All Closed)

Card Gap / Capability Status Key File
GAP-1 1-min bar provider wired into resting-order cron Closed api/commands/process_resting_orders.py
GAP-2 Bid/ask spread fill model (buy at ask, sell at bid) Closed api/services/mbt_fill_engine.py _fill_market()
GAP-3 Assignment-at-expiry + worthless-expiry sweep Closed mbt_fill_engine.close_expired_options()
GAP-4 Tax engine post-fill hook (Founders-tier gate) Closed mbt_fill_engine.tax_post_fill_hook() + _tier_resolver
GAP-5 Equity MTM — current_price, unrealized_pl, account equity Closed mbt_fill_engine._apply_mtm_sweep()

Note on GAP-2 and the synchronous market order path: The resting-order cron (process_resting_orders.py) injects Alpaca 1-min bars carrying live bid/ask snapshots. The synchronous market order path in trading.py injects EOD bars via fetch_or_load_bars (Phase 1, "Phase 2 will use 1-min bars"). When EOD bars lack real-time bid/ask, _fill_market() falls back to the midpoint of (high+low). This is a fill-quality gap addressed by PAPER-23.


5. APIs and Contracts

All endpoints live under /api/trading/ (Blueprint trading, backend_v2/api/routes/trading.py), gated on FLAG_PAPER_TRADING_V1:

Method Path Card Notes
POST /api/trading/orders SC-5, SC-6 Idempotency-Key required (ADJ-2); caller_conn wired
DELETE /api/trading/orders/{id} SC-6 Idempotent cancel
PATCH /api/trading/orders/{id} SC-6 Replace-semantics (cancel old, insert new)
GET /api/trading/orders SC-12 Paginated; status_filter param
GET /api/trading/account SC-11 Cash, equity, buying_power, total_pl
GET /api/trading/positions SC-11 Open positions; position_group_id for IC grouping
GET /api/trading/portfolio SC-11 Grouped portfolio (ICs collapsed to one row)
POST /api/trading/paper/reset SC-13 30-day cooldown via paper_accounts.last_reset_at
POST /api/trading/paper/multi-leg SC-7 IC builder direct multi-leg entry

Response envelope: {"data": ..., "meta": {...}}. Errors: {"error_code": "...", "message": "...", "request_id": "..."}.


6. Money-Path Idempotency Inheritance (ADR-0138)

Per ADJ-2 in docs/plan-to-production.md: Idempotency-Key is required on Tier-1 (paper order submit, order replace). The caller_conn parameter is wired on all three MbtFillEngine submission methods, making the paper order INSERT and the idempotency completion UPDATE atomic on a single connection.

Sub-card Endpoint Required AC for implementors
SC-5 (market orders) POST /api/trading/orders (order_type=market) @idempotency_key(fail_closed=True) on route; caller_conn=g.idem_conn passed to submit_market_order()
SC-6 (limit orders) POST /api/trading/orders (order_type=limit), PATCH .../{id} Same decorator; caller_conn on both submit and replace
SC-7 (IC builder) POST /api/trading/paper/multi-leg Parent + all child INSERTs share caller_conn — atomic with idempotency completion UPDATE
SC-8 (LCC roll) via lcc_roll_paper_submit.py Service must forward caller_conn through the roll close + open sequence
SC-13 (reset) POST /api/trading/paper/reset Cooldown is the replay guard; no idempotency key required

Replay returns HTTP 201 with the original response body. Replay events logged to idempotency_log separately from audit_log.


7. Simulate Surface Component Tree (UI-bearing cards)

Gated at the page level: app/(authed)/simulate/page.tsx redirects to /dashboard when FLAG_PAPER_TRADING_V1 is off (hide, never gray — per project feedback).

app/(authed)/simulate/page.tsx          — Server Component; flag gate + auth guard (SC-11)
  └── components/simulate/SimulateView.tsx   — 'use client'; URL toggle + 5s polling (SC-11)
        ├── components/backtest/BacktestView.tsx — backward mode; no regression (SC-11)
        ├── components/simulate/TradeHistoryTab.tsx — orders + tax lot summary (SC-12)
        ├── components/simulate/ResetButton.tsx    — 30-day cooldown + two-step confirm (SC-13)
        └── components/simulate/SwitchToLiveCta.tsx — graduation gate CTA (SC-14)

Toggle state (?mode=forward|backward) and active tab (?tab=portfolio|history) are URL-persisted for deep-linking. Polling pauses via Page Visibility API when the tab is hidden. Missed-bar banner appears when mbt_catchup.missed_bars > 5 (logged by evaluate_resting_orders).

UI-bearing cards: SC-11, SC-12, SC-13, SC-14. Engine/API-only (no frontend work): SC-1 through SC-10, SC-15, GAP-1 through GAP-5.


8. Batch Job Schedule

All jobs ship as Python command modules invoked by Woodpecker cron pipelines:

Task Command WP pipeline Schedule (UTC)
Resting order evaluation (GAP-1 / SC-6) api/commands/process_resting_orders.py mbt-resting-orders-cron.yaml Every 5 min, 14:30–21:00 Mon-Fri
Assignment-at-expiry (SC-9 / GAP-3) api/commands/paper_assignment_at_expiry.py mbt-assignment-at-expiry-cron.yaml 21:00 Mon-Fri
Dividend + split (SC-10) api/commands/paper_dividend_adjustment.py + paper_stock_split_adjustment.py mbt-dividend-split-cron.yaml 09:00 daily
Drift monitoring (soak) mbt-drift-daily-cron.yaml Daily (soak period)
Per-symbol drift audit mbt-drift-per-symbol-weekly-cron.yaml Weekly
EOD P&L snapshot PAPER-21 — not yet built Not yet created Target: 21:00 Mon-Fri
Tier retention purge PAPER-22 — not yet built Not yet created Target: 02:00 daily

9. Remaining Gaps — New Sub-Cards Filed

Three items were not covered by the original SC-1..SC-15 / GAP-1..GAP-5 card set:

PAPER-21: EOD snapshot command + Woodpecker pipeline

mbt-paper-trading-engine.md §8 specifies paper.eod_snapshot at 21:00 UTC Mon-Fri, writing one mbt_eod_snapshots row per user (equity, cash, positions_json JSONB). No command module or WP pipeline exists. Required for: historical equity tracking, drift comparison baseline, and GDPR data-export completeness. Blocks the Wave 4 7-day soak baseline calculation.

PAPER-22: Tier retention purge command + Woodpecker pipeline

Specified as paper.retention_purge at 02:00 UTC daily, soft-deleting paper_orders, paper_fills, and closed paper_positions older than tier cap (Free 90d / Pro 3yr / Pro+ skip). The market_data_access_log 13-month purge is already present in SC-3; this card covers the paper trading tables only.

PAPER-23: Market order real-time quote (Phase 2 bars provider)

The _bars_provider in trading.py:_get_mbt_engine() uses fetch_or_load_bars (EOD bars). When EOD bars have no bid/ask, _fill_market() uses the midpoint of (high+low) from the most recent bar rather than a live quote. AlpacaMarketDataService.get_latest_quote() exists and returns real-time bid/ask. Phase 2 wires the snapshot quote into the synchronous market order path so fills are at live bid/ask. Simulation quality gap, not a correctness bug. Must land before beta phase; acceptable at dogfood if operator is aware.


10. Dependency Chain

SC-15 (flag + B1 migration) — ships first
  SC-1, SC-2, SC-3 (schema) — parallel after SC-15
    SC-4 (Alpaca client)
      GAP-1 (1-min bars into resting-order cron)
        GAP-2 (bid/ask fill — active in _fill_market)
          SC-5 (market orders)
            SC-6 (limit orders + resting eval)
            SC-7 (IC builder)
            SC-8 (LCC roll)
            SC-9 + GAP-3 (expiry sweep)
            SC-10 (dividend + split)
            GAP-5 (equity MTM)
      GAP-4 (tax hook, Founders gate)
        SC-12 (trade history + tax section)
  SC-11 (Simulate UI forward mode)
    SC-12 (Trade History tab)
  SC-13 (reset endpoint + UI)
  SC-14 (CTA placeholder)

PAPER-21 (EOD snapshot) — no predecessor required
PAPER-22 (tier retention purge) — no predecessor required
PAPER-23 (market order real-time quote) — after SC-4; before beta flip

11. Rollout Plan

Phase Gate Status
Dark FLAG_PAPER_TRADING_V1=off on staging + prod All 20 SC/GAP cards merged; CI green
Dogfood Operator enables flag for self on staging Next step — PAPER-21/22 must land first; PAPER-23 optional (operator call)
Staging soak Flag on staging; 7-day drift < 2%; drift crons running Drift monitoring WP pipelines in place
Beta Flag on for allowlisted beta testers BLR redistribution decision required (ADJ-14 / #3483 operator comment)
GA Flag on globally; prod flip via #4167 Wave 9 go/no-go gate (#4166)

12. Security and GDPR Checklist


13. Open Questions

  1. Alpaca redistribution posture — Operator comment on #3483 required before beta or prod flip (ADJ-14). Not a code card.
  2. PAPER-23 dogfood timing — Can dogfood proceed with EOD-bar fills and defer real-time quote wiring to before beta? Operator decision, not a design decision.
  3. Securities attorney review (#197) — Must resolve before beta. Not a code card; gates SC-11 and SC-12 from reaching real users.

See ADR-0108 for the fill engine spec, ADR-0110 for the intraday bar feed design, ADR-0138 for the idempotency key mechanism, and 2026-06-11-in-house-paper-trading.md for the complete per-card acceptance criteria.