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>
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 atable(headers, rows)helper; panels are<details class="panel">blocks. -
api/main.py— stdlib HTTP server.DashboardState.snapshots()(line ~247) assembles the payload;orderssnapshots come fromstorage/projections.pyand 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_priceare STRINGS from the broker — coerce with+x/parseFloatin JS before formatting. -
Positions table today (
dashboard.html:1268-1296, insiderender(d)): columnsSymbol, Chart, Side, Qty, Avg, Last, R / risk, P/L[, Exit]. AstopBymap (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): columnsOrder, Symbol, Side, Qty, Price, Status, UpdatedwherePricerenderso.price— a single number that hides whether it is a limit, a stop, or a fill. Its data comes fromquery_order_lifecycle_snapshotinstorage/projections.py— check what fields that projection emits; if it does not pass throughlimit_price/stop_price/filled_avg_price/type, extend it (read-only projection, safe). -
Approval queue table (
dashboard.html:1229-1246): has aModecolumn renderingq.approval_mode || "manual"— the autonomous stack auto-approves, so this column always readsauto; dead weight. -
Repo conventions: no framework, no build step — plain ES5-ish inline JS,
esc()for HTML escaping,fmt()/money()for numbers,pillspans 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.htmlstorage/projections.py(ONLYquery_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 underarchive/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 (seegit log --oneline -10for 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_pricewhereo.side === "sell"ando.type === "limit"(the take-profit leg).entryLimitBy[symbol]=+o.limit_pricewhereo.side === "buy"ando.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_snapshotinstorage/projections.pydoes not already emitlimit_price,stop_price,filled_avg_price, andtype, 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 theOrder(truncated order-id) column — move the fullorder_idinto atitleattribute on the Symbol cell so it is hover-recoverable, not gone. - Coerce string prices (
+o.limit_priceetc.) beforefmt.
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-shellvia Playwright withargs=["--no-sandbox"], navigate withwait_until="load"(NOT "networkidle" — the SSE/api/streamnever idles), capturepage.on("pageerror"). A passing API check is NOT a passing page.
Test plan
- If
query_order_lifecycle_snapshotchanges: extend its existing unit test (find it withgrep -rn "order_lifecycle" tests/unit/) with one case assertinglimit_price/stop_price/filled_avg_price/typepass through from a syntheticaccount_orders_updated-style payload. Model after the neighboring projection tests in the same file. - UI has no JS test harness — the
node --checkgate plus (optional) headless render IS the UI verification. Do not introduce a JS test framework for this plan.
Done criteria
- Positions table shows
StopandTargetcolumns populated from live bracket legs (em-dash when absent) - Order Lifecycle shows
Type, Limit, Stop, Filland no order-id column; order-id recoverable via Symbol-celltitle - Approval queue has no
Modecolumn; risk chip shows absolute entry and stop prices node --checkon the extracted script exits 0python3 -m pytest tests/ -q --ignore=tests/unit/apiall pass;ruff check --select F api/ storage/exits 0git statusshows no modifications outside the in-scope listplans/README.mdstatus 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_snapshotrequires touching how events are WRITTEN (it must stay a read-side change). node --checkfails 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/entryLimitByas chart level-lines — keep them top-level inrender(d), not inlined. - Reviewer should scrutinize: string→number coercion on broker prices (the
"2.05"strings), and that the leg maps filter by workingstatus(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.