RayMelius Claude Opus 4.6 commited on
Commit
64a57bc
Β·
1 Parent(s): 8311583

Show NN/LLM badges on CH leaderboard, right-click to switch strategy

Browse files

- Type column shows NN (green) or LLM (purple) per member instead of generic AI
- AI decision history badges: RL source shown as NN
- Right-click leaderboard table opens context menu to switch strategy
(hybrid/rl/llm) at runtime via POST /ch/api/strategy
- Current strategy highlighted with checkmark in context menu
- Leaderboard auto-refreshes after strategy switch

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

clearing_house/app.py CHANGED
@@ -111,6 +111,17 @@ def _build_leaderboard(bbos: dict) -> list[dict]:
111
  row["total_value"] = round(row["capital"] + holdings_value, 2)
112
  row["pnl"] = round(row["total_value"] - db.CH_STARTING_CAPITAL, 2)
113
  row["is_human"] = ai_trader.is_human_active(row["member_id"])
 
 
 
 
 
 
 
 
 
 
 
114
  # Sort by total_value descending
115
  rows.sort(key=lambda r: r["total_value"], reverse=True)
116
  for i, row in enumerate(rows):
@@ -392,10 +403,23 @@ def api_member_detail(member_id):
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
  })
397
 
398
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  @app.route("/ch/api/market")
400
  def api_market():
401
  return jsonify(_get_bbos())
 
111
  row["total_value"] = round(row["capital"] + holdings_value, 2)
112
  row["pnl"] = round(row["total_value"] - db.CH_STARTING_CAPITAL, 2)
113
  row["is_human"] = ai_trader.is_human_active(row["member_id"])
114
+ # Determine AI type for this member
115
+ strategy = ai_trader.get_strategy()
116
+ if row["is_human"]:
117
+ row["ai_type"] = "Human"
118
+ elif strategy == "rl":
119
+ row["ai_type"] = "NN"
120
+ elif strategy == "llm":
121
+ row["ai_type"] = "LLM"
122
+ else: # hybrid
123
+ member_num = int(row["member_id"][-2:])
124
+ row["ai_type"] = "NN" if member_num <= 5 else "LLM"
125
  # Sort by total_value descending
126
  rows.sort(key=lambda r: r["total_value"], reverse=True)
127
  for i, row in enumerate(rows):
 
403
  "trades": trades,
404
  "ai_decisions": ai_decisions,
405
  "is_human": ai_trader.is_human_active(member_id),
406
+ "ai_type": _member_ai_type(member_id),
407
  "obligation": db.CH_DAILY_OBLIGATION,
408
  })
409
 
410
 
411
+ def _member_ai_type(member_id: str) -> str:
412
+ if ai_trader.is_human_active(member_id):
413
+ return "Human"
414
+ strategy = ai_trader.get_strategy()
415
+ if strategy == "rl":
416
+ return "NN"
417
+ if strategy == "llm":
418
+ return "LLM"
419
+ member_num = int(member_id[-2:])
420
+ return "NN" if member_num <= 5 else "LLM"
421
+
422
+
423
  @app.route("/ch/api/market")
424
  def api_market():
425
  return jsonify(_get_bbos())
clearing_house/templates/dashboard.html CHANGED
@@ -51,8 +51,10 @@
51
  <td>
52
  {% if row.is_human %}
53
  <span class="badge-human">Human</span>
 
 
54
  {% else %}
55
- <span class="badge-ai">AI</span>
56
  {% endif %}
57
  </td>
58
  <td style="text-align:right">€{{ "{:,.2f}".format(row.capital) }}</td>
@@ -80,7 +82,7 @@
80
  <p style="color:var(--muted); font-size:11px; margin-top:8px;">
81
  Daily obligation: each member must trade at least {{ obligation }} securities.
82
  Holdings value is calculated at current market mid-price.
83
- Click a row for full member detail. Refreshes every 10 seconds.
84
  </p>
85
 
86
  <!-- Member detail slide-out panel -->
@@ -119,7 +121,9 @@ async function refreshLeaderboard() {
119
  : `<span class="badge-bad">${row.total_securities}/{{ obligation }}</span>`;
120
  const typeBadge = row.is_human
121
  ? `<span class="badge-human">Human</span>`
122
- : `<span class="badge-ai">AI</span>`;
 
 
123
  tbody.innerHTML += `
124
  <tr data-member="${row.member_id}" style="cursor:pointer">
125
  <td style="color:var(--muted)">${row.rank}</td>
@@ -179,7 +183,9 @@ async function openDetail(memberId) {
179
  const pnlSign = d.pnl >= 0 ? '+' : '';
180
  const typeBadge = d.is_human
181
  ? '<span class="badge-human">Human</span>'
182
- : '<span class="badge-ai">AI</span>';
 
 
183
  const oblStatus = d.daily.total_securities >= d.obligation
184
  ? `<span class="badge-ok">Met (${d.daily.total_securities}/${d.obligation})</span>`
185
  : `<span class="badge-bad">${d.daily.total_securities}/${d.obligation}</span>`;
@@ -269,9 +275,11 @@ async function openDetail(memberId) {
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)}`
@@ -295,4 +303,73 @@ async function openDetail(memberId) {
295
  detailContent.innerHTML = '<p class="negative">Error loading member data</p>';
296
  }
297
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  {% endblock %}
 
51
  <td>
52
  {% if row.is_human %}
53
  <span class="badge-human">Human</span>
54
+ {% elif row.ai_type == 'NN' %}
55
+ <span class="badge-ai" style="background:#00695c;">NN</span>
56
  {% else %}
57
+ <span class="badge-ai" style="background:#5c6bc0;">LLM</span>
58
  {% endif %}
59
  </td>
60
  <td style="text-align:right">€{{ "{:,.2f}".format(row.capital) }}</td>
 
82
  <p style="color:var(--muted); font-size:11px; margin-top:8px;">
83
  Daily obligation: each member must trade at least {{ obligation }} securities.
84
  Holdings value is calculated at current market mid-price.
85
+ Click a row for full member detail. Right-click table to switch AI strategy. Refreshes every 10 seconds.
86
  </p>
87
 
88
  <!-- Member detail slide-out panel -->
 
121
  : `<span class="badge-bad">${row.total_securities}/{{ obligation }}</span>`;
122
  const typeBadge = row.is_human
123
  ? `<span class="badge-human">Human</span>`
124
+ : row.ai_type === 'NN'
125
+ ? `<span class="badge-ai" style="background:#00695c;">NN</span>`
126
+ : `<span class="badge-ai" style="background:#5c6bc0;">LLM</span>`;
127
  tbody.innerHTML += `
128
  <tr data-member="${row.member_id}" style="cursor:pointer">
129
  <td style="color:var(--muted)">${row.rank}</td>
 
183
  const pnlSign = d.pnl >= 0 ? '+' : '';
184
  const typeBadge = d.is_human
185
  ? '<span class="badge-human">Human</span>'
186
+ : d.ai_type === 'NN'
187
+ ? '<span class="badge-ai" style="background:#00695c;">NN</span>'
188
+ : '<span class="badge-ai" style="background:#5c6bc0;">LLM</span>';
189
  const oblStatus = d.daily.total_securities >= d.obligation
190
  ? `<span class="badge-ok">Met (${d.daily.total_securities}/${d.obligation})</span>`
191
  : `<span class="badge-bad">${d.daily.total_securities}/${d.obligation}</span>`;
 
275
  const ts = new Date(dec.timestamp * 1000).toLocaleTimeString();
276
  const srcBadge = dec.source === 'llm'
277
  ? '<span style="background:#e8eaf6; color:#5c6bc0; padding:1px 6px; border-radius:8px; font-size:10px;">LLM</span>'
278
+ : dec.source === 'rl'
279
+ ? '<span style="background:#e0f2f1; color:#00695c; padding:1px 6px; border-radius:8px; font-size:10px;">NN</span>'
280
+ : dec.source === 'fallback'
281
+ ? '<span style="background:#fff3e0; color:#e65100; padding:1px 6px; border-radius:8px; font-size:10px;">Fallback</span>'
282
+ : `<span style="background:#ffebee; color:#c62828; padding:1px 6px; border-radius:8px; font-size:10px;">${dec.source}</span>`;
283
  const order = dec.parsed_order;
284
  const orderLine = order
285
  ? `<span class="${order.side === 'BUY' ? 'positive' : 'negative'}" style="font-weight:bold;">${order.side}</span> ${order.quantity} <strong>${order.symbol}</strong> @ €${fmt(order.price)}`
 
303
  detailContent.innerHTML = '<p class="negative">Error loading member data</p>';
304
  }
305
  }
306
+
307
+ // ── Right-click context menu: switch AI strategy ───────────────────
308
+ const ctxMenu = document.createElement('div');
309
+ ctxMenu.id = 'strategy-menu';
310
+ ctxMenu.style.cssText = `
311
+ display:none; position:fixed; z-index:2000;
312
+ background:var(--card-bg, #1e1e2e); border:1px solid var(--border, #444);
313
+ border-radius:8px; padding:6px 0; box-shadow:0 4px 16px rgba(0,0,0,0.4);
314
+ min-width:180px; font-size:13px;
315
+ `;
316
+ ctxMenu.innerHTML = `
317
+ <div style="padding:6px 14px; color:var(--muted); font-size:11px; font-weight:bold;">AI Strategy</div>
318
+ <div class="ctx-item" data-strategy="hybrid" style="padding:8px 14px; cursor:pointer;">Hybrid (NN + LLM)</div>
319
+ <div class="ctx-item" data-strategy="rl" style="padding:8px 14px; cursor:pointer;">All NN (Neural Network)</div>
320
+ <div class="ctx-item" data-strategy="llm" style="padding:8px 14px; cursor:pointer;">All LLM</div>
321
+ `;
322
+ document.body.appendChild(ctxMenu);
323
+
324
+ // Style hover
325
+ ctxMenu.querySelectorAll('.ctx-item').forEach(item => {
326
+ item.addEventListener('mouseenter', () => item.style.background = 'rgba(255,255,255,0.08)');
327
+ item.addEventListener('mouseleave', () => item.style.background = 'none');
328
+ });
329
+
330
+ // Show on right-click anywhere on the leaderboard table
331
+ document.getElementById('lb-table').addEventListener('contextmenu', (e) => {
332
+ e.preventDefault();
333
+ ctxMenu.style.display = 'block';
334
+ const x = Math.min(e.clientX, window.innerWidth - 200);
335
+ const y = Math.min(e.clientY, window.innerHeight - 140);
336
+ ctxMenu.style.left = x + 'px';
337
+ ctxMenu.style.top = y + 'px';
338
+ highlightCurrentStrategy();
339
+ });
340
+
341
+ // Hide on click elsewhere
342
+ document.addEventListener('click', () => ctxMenu.style.display = 'none');
343
+
344
+ async function highlightCurrentStrategy() {
345
+ try {
346
+ const resp = await fetch('/ch/api/config');
347
+ const cfg = await resp.json();
348
+ ctxMenu.querySelectorAll('.ctx-item').forEach(item => {
349
+ const isCurrent = item.dataset.strategy === cfg.strategy;
350
+ item.style.fontWeight = isCurrent ? 'bold' : 'normal';
351
+ item.style.color = isCurrent ? 'var(--accent, #42a5f5)' : 'var(--text, #ccc)';
352
+ // Reset text to base, then append checkmark
353
+ const base = item.dataset.strategy === 'hybrid' ? 'Hybrid (NN + LLM)'
354
+ : item.dataset.strategy === 'rl' ? 'All NN (Neural Network)' : 'All LLM';
355
+ item.textContent = isCurrent ? base + ' \u2713' : base;
356
+ });
357
+ } catch(e) {}
358
+ }
359
+
360
+ ctxMenu.querySelectorAll('.ctx-item').forEach(item => {
361
+ item.addEventListener('click', async () => {
362
+ ctxMenu.style.display = 'none';
363
+ try {
364
+ const resp = await fetch('/ch/api/strategy', {
365
+ method: 'POST',
366
+ headers: {'Content-Type': 'application/json'},
367
+ body: JSON.stringify({strategy: item.dataset.strategy}),
368
+ });
369
+ if (resp.ok) {
370
+ refreshLeaderboard();
371
+ }
372
+ } catch(e) { console.error('Strategy switch failed:', e); }
373
+ });
374
+ });
375
  {% endblock %}