RayMelius Claude Opus 4.6 commited on
Commit
57e97ca
Β·
1 Parent(s): 3f2f8ab

Add AI reasoning history to CH member detail panel

Browse files

- New ch_ai_decisions table stores LLM responses and parsed orders
- AI trader logs every decision: LLM response, fallback, invalid parse
- Member detail API returns last 10 AI decisions
- Detail panel shows AI Reasoning section with:
- LLM/Fallback/Invalid badges
- Parsed order (side, qty, symbol, price)
- Raw LLM response in scrollable monospace box

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

clearing_house/app.py CHANGED
@@ -371,6 +371,16 @@ def api_member_detail(member_id):
371
  total_value = round(member["capital"] + total_holdings_value, 2)
372
  total_pnl = round(total_value - db.CH_STARTING_CAPITAL, 2)
373
 
 
 
 
 
 
 
 
 
 
 
374
  return jsonify({
375
  "member_id": member_id,
376
  "capital": round(member["capital"], 2),
@@ -380,6 +390,7 @@ def api_member_detail(member_id):
380
  "pnl": total_pnl,
381
  "daily": daily,
382
  "trades": trades,
 
383
  "is_human": ai_trader.is_human_active(member_id),
384
  "obligation": db.CH_DAILY_OBLIGATION,
385
  })
 
371
  total_value = round(member["capital"] + total_holdings_value, 2)
372
  total_pnl = round(total_value - db.CH_STARTING_CAPITAL, 2)
373
 
374
+ ai_decisions = db.get_ai_decisions(member_id, limit=10)
375
+ # Parse the stored JSON strings for the frontend
376
+ import json as _json
377
+ for d in ai_decisions:
378
+ if d.get("parsed_order") and isinstance(d["parsed_order"], str):
379
+ try:
380
+ d["parsed_order"] = _json.loads(d["parsed_order"])
381
+ except Exception:
382
+ pass
383
+
384
  return jsonify({
385
  "member_id": member_id,
386
  "capital": round(member["capital"], 2),
 
390
  "pnl": total_pnl,
391
  "daily": daily,
392
  "trades": trades,
393
+ "ai_decisions": ai_decisions,
394
  "is_human": ai_trader.is_human_active(member_id),
395
  "obligation": db.CH_DAILY_OBLIGATION,
396
  })
clearing_house/ch_ai_trader.py CHANGED
@@ -268,9 +268,24 @@ def _decide_order_llm(
268
  if text:
269
  order = _parse_llm_order(text, bbos)
270
  if order and _validate_order(order, capital, holdings, bbos):
 
 
 
 
271
  return order
 
 
 
 
 
272
  # Fallback: rule-based
273
- return _fallback_order(capital, holdings, bbos)
 
 
 
 
 
 
274
 
275
 
276
  def _build_prompt(member_id, capital, holdings, daily_trades, bbos, obligation_remaining):
 
268
  if text:
269
  order = _parse_llm_order(text, bbos)
270
  if order and _validate_order(order, capital, holdings, bbos):
271
+ try:
272
+ db.record_ai_decision(member_id, text, order, source="llm")
273
+ except Exception as e:
274
+ print(f"[CH-AI] Failed to log decision: {e}")
275
  return order
276
+ # Log failed parse/validation too
277
+ try:
278
+ db.record_ai_decision(member_id, text, order, source="llm-invalid")
279
+ except Exception:
280
+ pass
281
  # Fallback: rule-based
282
+ fallback = _fallback_order(capital, holdings, bbos)
283
+ if fallback:
284
+ try:
285
+ db.record_ai_decision(member_id, "LLM unavailable, using rule-based fallback", fallback, source="fallback")
286
+ except Exception:
287
+ pass
288
+ return fallback
289
 
290
 
291
  def _build_prompt(member_id, capital, holdings, daily_trades, bbos, obligation_remaining):
clearing_house/ch_database.py CHANGED
@@ -65,10 +65,22 @@ CREATE TABLE IF NOT EXISTS ch_settlements (
65
  settled_at REAL NOT NULL
66
  );
67
 
 
 
 
 
 
 
 
 
 
 
68
  CREATE INDEX IF NOT EXISTS idx_ch_trade_log_member
69
  ON ch_trade_log(member_id, trading_date);
70
  CREATE INDEX IF NOT EXISTS idx_ch_settlements_member
71
  ON ch_settlements(member_id, trading_date);
 
 
72
  """
73
 
74
 
@@ -264,6 +276,41 @@ def get_settlements(member_id: str, limit: int = 30) -> list[dict]:
264
  return [dict(r) for r in rows]
265
 
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  # ── Leaderboard ────────────────────────────────────────────────────────────────
268
 
269
  def get_leaderboard(date: str | None = None) -> list[dict]:
 
65
  settled_at REAL NOT NULL
66
  );
67
 
68
+ CREATE TABLE IF NOT EXISTS ch_ai_decisions (
69
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
70
+ member_id TEXT NOT NULL,
71
+ llm_response TEXT NOT NULL,
72
+ parsed_order TEXT,
73
+ source TEXT NOT NULL DEFAULT 'llm',
74
+ trading_date TEXT NOT NULL,
75
+ timestamp REAL NOT NULL
76
+ );
77
+
78
  CREATE INDEX IF NOT EXISTS idx_ch_trade_log_member
79
  ON ch_trade_log(member_id, trading_date);
80
  CREATE INDEX IF NOT EXISTS idx_ch_settlements_member
81
  ON ch_settlements(member_id, trading_date);
82
+ CREATE INDEX IF NOT EXISTS idx_ch_ai_decisions_member
83
+ ON ch_ai_decisions(member_id, trading_date);
84
  """
85
 
86
 
 
276
  return [dict(r) for r in rows]
277
 
278
 
279
+ # ── AI Decisions ──────────────────────────────────────────────────────────────
280
+
281
+ def record_ai_decision(
282
+ member_id: str,
283
+ llm_response: str,
284
+ parsed_order: dict | None,
285
+ source: str = "llm",
286
+ ) -> None:
287
+ import json as _json
288
+ _conn().execute(
289
+ """INSERT INTO ch_ai_decisions
290
+ (member_id, llm_response, parsed_order, source, trading_date, timestamp)
291
+ VALUES (?,?,?,?,?,?)""",
292
+ (
293
+ member_id,
294
+ llm_response,
295
+ _json.dumps(parsed_order) if parsed_order else None,
296
+ source,
297
+ today_str(),
298
+ time.time(),
299
+ ),
300
+ )
301
+ _conn().commit()
302
+
303
+
304
+ def get_ai_decisions(member_id: str, limit: int = 10) -> list[dict]:
305
+ rows = _conn().execute(
306
+ """SELECT llm_response, parsed_order, source, timestamp
307
+ FROM ch_ai_decisions WHERE member_id=?
308
+ ORDER BY timestamp DESC LIMIT ?""",
309
+ (member_id, limit),
310
+ ).fetchall()
311
+ return [dict(r) for r in rows]
312
+
313
+
314
  # ── Leaderboard ────────────────────────────────────────────────────────────────
315
 
316
  def get_leaderboard(date: str | None = None) -> list[dict]:
clearing_house/templates/dashboard.html CHANGED
@@ -262,6 +262,34 @@ async function openDetail(memberId) {
262
  html += '<p style="color:var(--muted)">No trades today</p>';
263
  }
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  detailContent.innerHTML = html;
266
  } catch(e) {
267
  detailContent.innerHTML = '<p class="negative">Error loading member data</p>';
 
262
  html += '<p style="color:var(--muted)">No trades today</p>';
263
  }
264
 
265
+ // AI Decision History
266
+ if (d.ai_decisions && d.ai_decisions.length > 0) {
267
+ html += `<h3 style="margin:20px 0 8px;">AI Reasoning (last ${d.ai_decisions.length})</h3>`;
268
+ d.ai_decisions.forEach(dec => {
269
+ const ts = new Date(dec.timestamp * 1000).toLocaleTimeString();
270
+ const srcBadge = dec.source === 'llm'
271
+ ? '<span style="background:#e8eaf6; color:#5c6bc0; padding:1px 6px; border-radius:8px; font-size:10px;">LLM</span>'
272
+ : dec.source === 'fallback'
273
+ ? '<span style="background:#fff3e0; color:#e65100; padding:1px 6px; border-radius:8px; font-size:10px;">Fallback</span>'
274
+ : `<span style="background:#ffebee; color:#c62828; padding:1px 6px; border-radius:8px; font-size:10px;">${dec.source}</span>`;
275
+ const order = dec.parsed_order;
276
+ const orderLine = order
277
+ ? `<span class="${order.side === 'BUY' ? 'positive' : 'negative'}" style="font-weight:bold;">${order.side}</span> ${order.quantity} <strong>${order.symbol}</strong> @ €${fmt(order.price)}`
278
+ : '<span style="color:var(--muted)">No valid order</span>';
279
+
280
+ html += `
281
+ <div style="border:1px solid var(--border, #333); border-radius:6px; padding:10px; margin-bottom:8px; font-size:12px;">
282
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
283
+ <div>${srcBadge} &nbsp; ${orderLine}</div>
284
+ <span style="color:var(--muted); font-size:11px;">${ts}</span>
285
+ </div>
286
+ <div style="color:var(--muted); white-space:pre-wrap; font-family:monospace; font-size:11px; max-height:80px; overflow-y:auto; background:rgba(0,0,0,0.15); padding:6px; border-radius:4px;">${dec.llm_response}</div>
287
+ </div>`;
288
+ });
289
+ } else if (!d.is_human) {
290
+ html += '<h3 style="margin:20px 0 8px;">AI Reasoning</h3><p style="color:var(--muted)">No AI decisions recorded yet</p>';
291
+ }
292
+
293
  detailContent.innerHTML = html;
294
  } catch(e) {
295
  detailContent.innerHTML = '<p class="negative">Error loading member data</p>';