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>
9.9 KiB
Plan 014: Live decision trace — show WHY the bot did or didn't enter, per gate, with the config values in play
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/main.py api/static/dashboard.html storage/projections.py tests/unit/api/On mismatch with "Current state" excerpts, STOP.
Status
- Priority: P2
- Effort: M
- Risk: LOW (read-only; new endpoint over existing events + UI panel/overlay)
- Depends on: plans/013-symbol-chart-trade-markers.md (the chart modal this overlays; the Decision Feed panel half works standalone)
- Category: direction / operator-visibility
- Planned at: commit
43a3ace, 2026-07-16 (plan written 2026-07-17)
Why this matters
Owner's ask: "see the logic running live... visually clear... make additional
determinations as to some of the smaller env variables and see their impact on
the trade as to when and where the bot thought it would be." The bot ALREADY
emits exactly this as entry_gate_evaluated decision receipts (plan 005) —
one event per entry evaluation with a per-gate breakdown including the value
seen and the threshold applied. Today those receipts are write-only: nothing
on the dashboard reads them. This plan surfaces them two ways: a live
Decision Feed panel (latest gate evaluations, reject reasons, value-vs-
threshold), and reject/entry markers on the plan-013 symbol chart so the
owner sees WHERE on the tape the bot considered and declined.
Current state
-
Receipts:
trading_execution.py:1138_emit_gate_receiptemitsentry_gate_evaluated(envTRADING_GATE_RECEIPTS, default on). Payload verified in production 2026-07-16:{"timestamp": "2026-07-16T10:41:40.255354", "symbol": "ATAI", "source": "fast", "passed": false, "reject_reason": "already_active", "message": "ATAI entry gates (fast) — rejected: already_active", "gates": {"session": {"outcome": "pass"}, "regime": {"outcome": "pass"}, "late_confirmation": {"outcome": "pass"}, "population": {"outcome": "pass"}, "rank": {"value": 5, "threshold": 0, "outcome": "pass"}}}The
gatesmap is a soft contract (additive only). Gate keys observed:session, regime, late_confirmation, population, rank(+ others the slow path adds); entries may carryvalueandthreshold. -
Volume: ~1,300 receipts/session. Retention: 14 days — the nightly housekeeping in
research/premarket_eval.pypurgesentry_gate_evaluatedolder than 14 days (it is in the spam list). The feature is a LIVE lens, not a historical archive; say so in the UI hint. -
api/main.py— stdlib ThreadingHTTPServer; GET routes indo_GET(line ~522) as a flat if-chain;_send_jsonhelper.DashboardStatewraps the event store (self.open_store()),store.query_events( event_type=..., since=..., until=..., limit=...)is the query interface (seestorage/projections.py:403query_fills_feedfor the exact idiom, includingjson.loads(event.get("payload_json"))). -
api/static/dashboard.html— panel pattern:<details class="panel">blocks (seep-feedActivity Feed, line ~528, and itsrenderat line ~1298 for the row style to imitate). Plan 013 adds a chart modal withcreateSeriesMarkers; its refresh path re-fetches on the SSEsnapshotevent with a ≥5 s throttle. -
Effective config: the admin page already exposes env; the receipts carry the numeric threshold per gate — the feed shows
value → thresholdper reject, which is the "which env var did this" answer without new plumbing.
Commands you will need
| Purpose | Command | Expected on success |
|---|---|---|
| API tests | python3 -m pytest tests/unit/api/ -q |
pass (pre-existing local urllib failures possible — compare to a clean-HEAD stash run) |
| Full suite | python3 -m pytest tests/ -q --ignore=tests/unit/api |
all pass |
| Lint (CI gate) | ruff check --select F api/ storage/ |
exit 0 |
| JS syntax gate | extract inline <script>, node --check (plan 012 Step 5 snippet) |
exit 0 |
Scope
In scope:
storage/projections.py(newquery_gate_receipts(store, symbol=None, limit=100, for_date=None)— same shape asquery_fills_feed)api/main.py(new/api/decisionsroute; adddecisionsto thesnapshots()payload ONLY if the SSE-driven panel needs it — prefer the standalone route + lazy fetch like the Component Attribution panel, line ~546, to keep the snapshot payload from growing)api/static/dashboard.htmltests/unit/api/(new test file)
Out of scope:
trading_execution.pyand anything that WRITES events — the receipt schema is consumed as-is; if a gate lacksvalue/threshold, render an em-dash, do not add fields to the emitter.- The 14-day retention policy in
research/premarket_eval.py. api/static/admin.html,archive/.
Git workflow
- Match repo style; suggested message:
operator-plane: live decision trace — gate receipts feed + chart reject markers. - Do NOT push unless instructed.
Steps
Step 1: query_gate_receipts projection
In storage/projections.py, next to query_fills_feed (line 403), add
query_gate_receipts(store, symbol=None, limit=100, for_date=None):
query entry_gate_evaluated via the same _hist_window(for_date) +
store.query_events idiom; parse payload; keep {timestamp, symbol, source, passed, reject_reason, gates}; filter by symbol (case-insensitive upper)
when given; newest first; cap at limit.
Verify: unit test from Test plan case 1 passes.
Step 2: /api/decisions route
In do_GET, add /api/decisions accepting symbol (optional), limit
(default 100, max 500), date (optional, same semantics as /api/snapshots?date=).
Return {"decisions": [...]}.
Verify:
curl -s "http://127.0.0.1:8765/api/decisions?limit=5" | python3 -m json.tool
→ 5 receipt dicts with gates maps (live DB) or empty list (fresh DB).
Step 3: Decision Feed panel
New <details class="panel full" id="p-decisions"> titled
Decision Trace <span class="hint">why each entry was taken or refused · value → threshold per gate · receipts kept 14 days</span>. Lazy-load on
first open + refresh on SSE snapshot while open (imitate the Component
Attribution lazy-open pattern at the bottom of the file, and the ≥5 s throttle
idiom from plan 013). Each row:
- time · symbol · source pill (
fast/auto) · PASS (green) or thereject_reason(red) - the gates map inline, compact: each gate as a chip —
rank 5→0 ✓,population ✗,regime ✗ (unfavorable)— green ✓ / red ✗, withvalue→thresholdonly when both present. Full receipt JSON in atitletooltip. - A symbol filter input at the panel top (client-side param to
/api/decisions?symbol=).
Verify: node --check clean; manual: open panel on the live dashboard →
rows appear; typing a symbol narrows.
Step 4: Chart overlay — where the bot said no
In the plan-013 chart modal fetch path, also fetch
/api/decisions?symbol=S&limit=200. Add markers via the SAME
createSeriesMarkers call (merge arrays, sort by time — Lightweight Charts
requires ascending marker time):
- rejected receipts → small gray-red
circleABOVE bar, text = short reason code (first 12 chars ofreject_reason); collapse consecutive receipts with the same reason within 5 minutes into one marker (1,300/day would smear the chart — the collapse rule is load-bearing). - passed receipts → no marker (the fill arrow from plan 013 already marks the outcome).
Verify: manual — open a symbol the bot rejected today (any
signal_blocked-heavy name): gray-red circles at the rejection times;
tooltip/legend shows the reason.
Test plan
New tests/unit/api/test_decisions_endpoint.py, modeled on
tests/unit/api/test_dashboard_api.py fixtures:
- Seed 3
entry_gate_evaluatedevents (2 symbols; one passed, one rejected withgates.rank.value/threshold) →query_gate_receiptsreturns all 3 newest-first;symbol=filter returns only that symbol's;gatesmap survives round-trip. /api/decisions?symbol=x&limit=1over the seeded store → HTTP 200, one row, lower-case symbol matched case-insensitively.limit=9999clamps to 500 (no unbounded scan).
Verify: python3 -m pytest tests/unit/api/test_decisions_endpoint.py -q
→ all pass.
Done criteria
- Decision Trace panel shows live receipts with per-gate ✓/✗ chips and
value→thresholdwhere present; symbol filter works; 14-day hint shown - Chart modal shows collapsed reject markers for the open symbol
/api/decisionsclamped, case-insensitive, date-scoped- New endpoint tests pass; full suite passes;
ruff check --select F api/ storage/exits 0;node --checkclean git statusclean outside scope;plans/README.mdupdated
STOP conditions
- Receipt payloads lack the
gatesmap (schema drift since43a3ace). - The
snapshotSSE payload would need to grow to make the panel work (the lazy-fetch design above should avoid it; if not, stop and report). - Marker merge with plan 013's fills breaks chart rendering (ascending-time requirement) after one fix attempt.
Maintenance notes
- The gates map is additive-only by contract — the chip renderer must ignore unknown keys gracefully (render name + outcome, skip value).
- If receipt retention ever changes from 14 days, update the panel hint.
- Deferred: an env-var diff view ("this reject would pass if X were Y") — that is a simulation, not a display; belongs with plan 015's machinery if ever built.