Files
momentum-system/plans/014-live-decision-trace.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

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_receipt emits entry_gate_evaluated (env TRADING_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 gates map is a soft contract (additive only). Gate keys observed: session, regime, late_confirmation, population, rank (+ others the slow path adds); entries may carry value and threshold.

  • Volume: ~1,300 receipts/session. Retention: 14 days — the nightly housekeeping in research/premarket_eval.py purges entry_gate_evaluated older 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 in do_GET (line ~522) as a flat if-chain; _send_json helper. DashboardState wraps the event store (self.open_store()), store.query_events( event_type=..., since=..., until=..., limit=...) is the query interface (see storage/projections.py:403 query_fills_feed for the exact idiom, including json.loads(event.get("payload_json"))).

  • api/static/dashboard.html — panel pattern: <details class="panel"> blocks (see p-feed Activity Feed, line ~528, and its render at line ~1298 for the row style to imitate). Plan 013 adds a chart modal with createSeriesMarkers; its refresh path re-fetches on the SSE snapshot event 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 → threshold per 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 (new query_gate_receipts(store, symbol=None, limit=100, for_date=None) — same shape as query_fills_feed)
  • api/main.py (new /api/decisions route; add decisions to the snapshots() 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.html
  • tests/unit/api/ (new test file)

Out of scope:

  • trading_execution.py and anything that WRITES events — the receipt schema is consumed as-is; if a gate lacks value/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 the reject_reason (red)
  • the gates map inline, compact: each gate as a chip — rank 5→0 ✓, population ✗, regime ✗ (unfavorable) — green ✓ / red ✗, with value→threshold only when both present. Full receipt JSON in a title tooltip.
  • 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 circle ABOVE bar, text = short reason code (first 12 chars of reject_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:

  1. Seed 3 entry_gate_evaluated events (2 symbols; one passed, one rejected with gates.rank.value/threshold) → query_gate_receipts returns all 3 newest-first; symbol= filter returns only that symbol's; gates map survives round-trip.
  2. /api/decisions?symbol=x&limit=1 over the seeded store → HTTP 200, one row, lower-case symbol matched case-insensitively.
  3. limit=9999 clamps 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→threshold where present; symbol filter works; 14-day hint shown
  • Chart modal shows collapsed reject markers for the open symbol
  • /api/decisions clamped, case-insensitive, date-scoped
  • New endpoint tests pass; full suite passes; ruff check --select F api/ storage/ exits 0; node --check clean
  • git status clean outside scope; plans/README.md updated

STOP conditions

  • Receipt payloads lack the gates map (schema drift since 43a3ace).
  • The snapshot SSE 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.