Files
momentum-system/plans/012-dashboard-order-limits-declutter.md
T
momentum devandClaude Fable 5 33708ffe6a plans(012-015): dashboard visibility milestone — order limits, symbol charts, decision trace, /backtest replay
Owner-requested UI work, advisor-planned for executor handoff:
- 012: show working limit/stop/target prices on positions + order lifecycle,
  cut dead columns (order-id, approval Mode)
- 013: symbol drill-in — 24h candlestick + live buy/sell fill markers
  (TradingView Lightweight Charts v5.2, vendored, Apache-2.0)
- 014: live decision trace — surface entry_gate_evaluated receipts as a feed
  (value→threshold per gate) + chart reject markers
- 015: /backtest replay page — theoretical entries/exits over past sessions
  under CURRENT settings, assembled strictly from Experiment-OS primitives

All four are read-path only; zero live-loop surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 06:00:45 -05:00

11 KiB

Plan 012: Show the actual buy/sell limit prices everywhere they act, and cut the dashboard fields that carry no decision weight

Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving to the next step. If anything in the "STOP conditions" section occurs, stop and report — do not improvise. When done, update the status row for this plan in plans/README.md — unless a reviewer dispatched you and told you they maintain the index.

Drift check (run first): git diff --stat 43a3ace..HEAD -- api/static/dashboard.html storage/projections.py api/main.py tests/unit/api/ If any in-scope file changed since this plan was written, compare the "Current state" excerpts against the live code before proceeding; on a mismatch, treat it as a STOP condition.

Status

  • Priority: P2
  • Effort: S
  • Risk: LOW (read-only UI + one projection field; no trading-path code)
  • Depends on: none
  • Category: dx / operator-visibility
  • Planned at: commit 43a3ace, 2026-07-16

Why this matters

The owner watches this dashboard to understand what the bot is DOING with money, and today the most decision-relevant numbers — the actual limit price a buy was submitted at, the resting protective stop, and the take-profit limit — are either hidden (bracket legs appear only as anonymous rows in Order Lifecycle) or collapsed into a single generic "Price" column. Meanwhile some columns carry no decision weight (truncated broker order-id, an approval "Mode" column that always reads auto). This plan makes the working orders legible (limit/stop/fill split out, bracket legs attached to their position) and removes the dead columns.

Current state

  • api/static/dashboard.html (1492 lines) — the ONE dashboard UI (owner preference: extend this page at :8765; do NOT touch the retired NiceGUI control room). Single inline <script>; tables built with a table(headers, rows) helper; panels are <details class="panel"> blocks.

  • api/main.py — stdlib HTTP server. DashboardState.snapshots() (line ~247) assembles the payload; orders snapshots come from storage/projections.py and each order dict already carries everything needed — verified against production:

    {"symbol": "EHGO", "side": "buy", "quantity": 5102.0, "type": "limit",
     "status": "filled", "limit_price": "2.05", "stop_price": null,
     "filled_avg_price": "2.05", "submitted_at": "2026-07-16T14:19:16Z"}
    

    NOTE: limit_price/stop_price/filled_avg_price are STRINGS from the broker — coerce with +x / parseFloat in JS before formatting.

  • Positions table today (dashboard.html:1268-1296, inside render(d)): columns Symbol, Chart, Side, Qty, Avg, Last, R / risk, P/L[, Exit]. A stopBy map (line ~1263) already scans order snapshots for the resting stop leg:

    (d.orders || []).forEach(snap => (snap.orders || []).forEach(o => {
      if (o.side === "sell" && (o.type === "stop" || o.type === "stop_limit") && o.stop_price != null && !(o.symbol in stopBy))
        stopBy[o.symbol] = +o.stop_price;
    }));
    
  • Order Lifecycle table (dashboard.html:1312-1326): columns Order, Symbol, Side, Qty, Price, Status, Updated where Price renders o.price — a single number that hides whether it is a limit, a stop, or a fill. Its data comes from query_order_lifecycle_snapshot in storage/projections.py — check what fields that projection emits; if it does not pass through limit_price / stop_price / filled_avg_price / type, extend it (read-only projection, safe).

  • Approval queue table (dashboard.html:1229-1246): has a Mode column rendering q.approval_mode || "manual" — the autonomous stack auto-approves, so this column always reads auto; dead weight.

  • Repo conventions: no framework, no build step — plain ES5-ish inline JS, esc() for HTML escaping, fmt()/money() for numbers, pill spans for side badges. Match them. Comments in this file explain WHY a column exists (see the "show only the ACTIONABLE orders" comment at line ~1312) — keep that style.

Commands you will need

Purpose Command Expected on success
JS syntax gate extract the inline <script> and run node --check on it (see Step 5) exit 0
API tests python3 -m pytest tests/unit/api/ -q pass (NOTE: on some local sandboxes these fail with urllib errors on clean HEAD — if they fail IDENTICALLY before your change, that is pre-existing; compare against a stash run)
Full suite python3 -m pytest tests/ -q --ignore=tests/unit/api all pass
Lint ruff check --select F api/ storage/ exit 0

Scope

In scope (the only files you should modify):

  • api/static/dashboard.html
  • storage/projections.py (ONLY query_order_lifecycle_snapshot, only to pass through fields that already exist in the event payloads)
  • tests/unit/api/ (extend an existing test file if a projection changes)

Out of scope (do NOT touch):

  • api/static/admin.html, anything under archive/
  • trading_execution.py, run_live_paper.py, alpaca_paper/ — no live-path changes for a UI plan, ever
  • The snapshot payload keys consumed by other panels (rename nothing)
  • Plan 013's symbol-chart modal (separate plan; don't pre-build hooks for it)

Git workflow

  • Branch: work directly or advisor/012-dashboard-order-limits — repo commits to master with descriptive messages (see git log --oneline -10 for style, e.g. operator-plane: truthful LIVE badge + kill-switch UI + action gating).
  • Do NOT push unless the operator instructed it.

Steps

Step 1: Extend the bracket-leg map to capture the take-profit limit and pending entry

In render(d) next to the existing stopBy map, build two more maps from the same d.orders scan, considering only orders whose status is one of new/accepted/pending_new/partially_filled (a filled or cancelled leg is not a working level):

  • targetBy[symbol] = +o.limit_price where o.side === "sell" and o.type === "limit" (the take-profit leg).
  • entryLimitBy[symbol] = +o.limit_price where o.side === "buy" and o.type === "limit" (a resting entry not yet filled).

Verify: node --check on the extracted script → exit 0.

Step 2: Positions table — show the working levels

Add two columns after Avg: Stop and Target, rendering fmt(stopBy[pos.symbol]) / fmt(targetBy[pos.symbol]) (em-dash when absent — fmt already handles null). Keep the existing ⚠ no-protective-stop marker on the Symbol cell. Update the table([...]) header array to match.

Verify: node --check → exit 0.

Step 3: Order Lifecycle — split "Price" into Limit / Stop / Fill and drop the order-id column

  • If query_order_lifecycle_snapshot in storage/projections.py does not already emit limit_price, stop_price, filled_avg_price, and type, add them as pass-throughs from the payload (same pattern as the existing fields in that function).
  • Change the table columns to: Symbol, Side, Type, Qty, Limit, Stop, Fill, Status, Updated. Drop the Order (truncated order-id) column — move the full order_id into a title attribute on the Symbol cell so it is hover-recoverable, not gone.
  • Coerce string prices (+o.limit_price etc.) before fmt.

Verify: python3 -m pytest tests/unit/api/ -q behaves identically to a pre-change stash run; node --check → exit 0.

Step 4: Approval queue — drop the dead "Mode" column, show explicit numbers

Remove the Mode column. In the riskChip(r) helper (line ~770), the risk summary already shows qty/notional/stop%; add the two absolute prices so the operator sees the exact limits being sent: entry <b>$X.XX</b> and stop <b>$Y.YY</b> (from r.entry_price / r.stop_loss_price, already read there).

Verify: node --check → exit 0.

Step 5: Syntax + render gate

Extract and check the page script:

python3 - <<'EOF'
import re, pathlib
html = pathlib.Path("api/static/dashboard.html").read_text()
scripts = re.findall(r"<script>(.*?)</script>", html, re.S)
pathlib.Path("/tmp/dash_check.js").write_text("\n;\n".join(scripts))
EOF
node --check /tmp/dash_check.js

→ exit 0. If a Playwright headless chromium is available in the environment, also render http://127.0.0.1:8765 after python3 dashboard_api.py and assert zero pageerror events (this repo has caught blank-page JS bugs this way; see Suggested executor toolkit).

Suggested executor toolkit

  • Headless render check: launch chrome-headless-shell via Playwright with args=["--no-sandbox"], navigate with wait_until="load" (NOT "networkidle" — the SSE /api/stream never idles), capture page.on("pageerror"). A passing API check is NOT a passing page.

Test plan

  • If query_order_lifecycle_snapshot changes: extend its existing unit test (find it with grep -rn "order_lifecycle" tests/unit/) with one case asserting limit_price/stop_price/filled_avg_price/type pass through from a synthetic account_orders_updated-style payload. Model after the neighboring projection tests in the same file.
  • UI has no JS test harness — the node --check gate plus (optional) headless render IS the UI verification. Do not introduce a JS test framework for this plan.

Done criteria

  • Positions table shows Stop and Target columns populated from live bracket legs (em-dash when absent)
  • Order Lifecycle shows Type, Limit, Stop, Fill and no order-id column; order-id recoverable via Symbol-cell title
  • Approval queue has no Mode column; risk chip shows absolute entry and stop prices
  • node --check on the extracted script exits 0
  • python3 -m pytest tests/ -q --ignore=tests/unit/api all pass; ruff check --select F api/ storage/ exits 0
  • git status shows no modifications outside the in-scope list
  • plans/README.md status row updated

STOP conditions

Stop and report back (do not improvise) if:

  • The order snapshot dicts do NOT contain limit_price/stop_price/type (the excerpt above was verified against production on 2026-07-16 — if absent, the projection chain changed and this plan is stale).
  • Adding pass-through fields to query_order_lifecycle_snapshot requires touching how events are WRITTEN (it must stay a read-side change).
  • node --check fails on the UNMODIFIED extracted script (pre-existing breakage — report, don't fix someone else's bug in this plan).

Maintenance notes

  • Plan 013 (symbol drill-in chart) reuses stopBy/targetBy/entryLimitBy as chart level-lines — keep them top-level in render(d), not inlined.
  • Reviewer should scrutinize: string→number coercion on broker prices (the "2.05" strings), and that the leg maps filter by working status (a cancelled stop leg must not render as protection).
  • Deferred deliberately: cutting whole panels (Watch States, Account). The owner said "fields that don't really matter" — start with dead columns; panel-level cuts need the owner pointing at specific panels.