[object Object]

← back to Ken

Ken: add real-money-only dashboard toggle (Paper Arena ⟷ Real Money)

8ba89727c19dfe7aa0447ed1d8d4a60e67989d80 · 2026-08-04 10:13:03 -0700 · Steve Abrams

- New GET /api/dashboard/real endpoint: same shape as /api/dashboard but
  computed from ken_trades (actual Kalshi orders) + live account balance,
  not the 99-bot paper arena. Paper path untouched.
- Dashboard.jsx: persistent realMode toggle button in header, dynamic
  labels/subtitle, refetches on toggle. Rebuilt bundle.

Files touched

Diff

commit 8ba89727c19dfe7aa0447ed1d8d4a60e67989d80
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 4 10:13:03 2026 -0700

    Ken: add real-money-only dashboard toggle (Paper Arena ⟷ Real Money)
    
    - New GET /api/dashboard/real endpoint: same shape as /api/dashboard but
      computed from ken_trades (actual Kalshi orders) + live account balance,
      not the 99-bot paper arena. Paper path untouched.
    - Dashboard.jsx: persistent realMode toggle button in header, dynamic
      labels/subtitle, refetches on toggle. Rebuilt bundle.
---
 kalshi-dash/server.js               | 126 ++++++++++++++++++++++++++++++++++++
 kalshi-dash/src/pages/Dashboard.jsx |  39 +++++++++--
 2 files changed, 161 insertions(+), 4 deletions(-)

diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index c6f833b..ff48f9d 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -2182,6 +2182,132 @@ const routes = {
     }
   },
 
+  // ════════════════════════════════════════
+  // REAL-MONEY DASHBOARD — same shape as /api/dashboard but computed from
+  // ken_trades (actual Kalshi orders) + live account balance, NOT the paper
+  // arena. Feeds the "Real money only" toggle on the Dashboard page.
+  // ════════════════════════════════════════
+  'GET /api/dashboard/real': async (req, res) => {
+    try {
+      // Live account cash (cents). Best-effort — fall back to bankroll if the
+      // Kalshi balance call rate-limits.
+      let cashCents = 0;
+      try {
+        const bal = await kalshiFetch('GET', '/portfolio/balance');
+        cashCents = Number(bal?.balance || 0);
+      } catch {
+        const { rows: [rs] } = await q('SELECT bankroll FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+        cashCents = Math.round(Number(rs?.bankroll || 0) * 100);
+      }
+      // real safe_mode
+      const { rows: [rsCfg] } = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const safeMode = rsCfg?.config?.safe_mode ?? true;
+
+      // Aggregates over REAL trades
+      const { rows: [agg] } = await kenQ(`
+        SELECT
+          COALESCE(SUM(cost_cents) FILTER (WHERE status='open'), 0)              AS capital_at_risk,
+          COUNT(*) FILTER (WHERE status='open')                                  AS open_count,
+          COUNT(DISTINCT ticker) FILTER (WHERE status='open')                    AS open_markets,
+          COALESCE(SUM(cost_cents), 0)                                           AS total_invested,
+          COALESCE(SUM(pnl_cents) FILTER (WHERE status!='open'), 0)              AS realized_pnl,
+          COALESCE(SUM(cost_cents + COALESCE(pnl_cents,0)) FILTER (WHERE status!='open'), 0) AS total_returned,
+          COALESCE(SUM(pnl_cents) FILTER (WHERE status!='open' AND closed_at::date = CURRENT_DATE), 0) AS daily_pnl
+        FROM ken_trades
+      `);
+      const capitalAtRisk = Number(agg?.capital_at_risk || 0);
+      const realizedPnl   = Number(agg?.realized_pnl || 0);
+      const balance   = cashCents + capitalAtRisk;               // equity = cash + open cost basis
+      const seedMoney = cashCents + capitalAtRisk - realizedPnl;  // seed ≡ balance − realized
+      const roi = seedMoney > 0 ? (realizedPnl / seedMoney * 100) : 0;
+
+      // Daily economics (resolved real trades, last 7 days)
+      const { rows: spendRows } = await kenQ(`
+        SELECT closed_at::date AS day, COUNT(*) AS trades,
+               SUM(CASE WHEN pnl_cents>0 THEN 1 ELSE 0 END) AS wins,
+               SUM(CASE WHEN pnl_cents<0 THEN 1 ELSE 0 END) AS losses,
+               SUM(pnl_cents) AS net_pnl,
+               SUM(CASE WHEN pnl_cents>0 THEN pnl_cents ELSE 0 END) AS gross_wins,
+               SUM(CASE WHEN pnl_cents<0 THEN ABS(pnl_cents) ELSE 0 END) AS gross_losses,
+               SUM(cost_cents) AS capital_deployed
+        FROM ken_trades WHERE status!='open' AND closed_at IS NOT NULL
+        GROUP BY closed_at::date ORDER BY day DESC LIMIT 7
+      `);
+      // Today
+      const { rows: [today] } = await kenQ(`
+        SELECT COUNT(*) AS trades,
+               SUM(CASE WHEN pnl_cents>0 THEN 1 ELSE 0 END) AS wins,
+               SUM(CASE WHEN pnl_cents<0 THEN 1 ELSE 0 END) AS losses
+        FROM ken_trades WHERE created_at::date = CURRENT_DATE
+      `);
+      // Recent fills (open + resolved), newest first
+      const { rows: fills } = await kenQ(`
+        SELECT ticker, title, side, count, price_cents, pnl_cents, status
+        FROM ken_trades ORDER BY id DESC LIMIT 10
+      `);
+      // P&L history (resolved, last 14d)
+      const { rows: pnlHistory } = await kenQ(`
+        SELECT closed_at::date AS day, SUM(pnl_cents) AS daily_pnl
+        FROM ken_trades WHERE status!='open' AND closed_at > NOW() - INTERVAL '14 days'
+        GROUP BY closed_at::date ORDER BY day ASC
+      `);
+      // Per-ticker performance
+      const { rows: byTicker } = await kenQ(`
+        SELECT ticker, MAX(title) AS title, COUNT(*) AS trades,
+               SUM(CASE WHEN pnl_cents>0 THEN 1 ELSE 0 END) AS wins,
+               SUM(CASE WHEN pnl_cents<0 THEN 1 ELSE 0 END) AS losses,
+               SUM(COALESCE(pnl_cents,0)) AS pnl
+        FROM ken_trades GROUP BY ticker ORDER BY pnl DESC
+      `);
+      const mkMarket = (m) => {
+        const t = m.ticker || ''; const et = t.replace(/-[^-]+$/, '') || t; const st = et.replace(/-[^-]+$/, '') || et;
+        return { ticker: m.ticker, title: m.title, trades: Number(m.trades), wins: Number(m.wins),
+                 losses: Number(m.losses), pnl: Number(m.pnl), edge: '', url: `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}` };
+      };
+
+      json(res, {
+        env: KALSHI_ENV,
+        real_mode: true,
+        safe_mode: safeMode,
+        balance,
+        seed_money: seedMoney,
+        net_pnl_from_seed: realizedPnl,
+        roi_pct: parseFloat(roi.toFixed(2)),
+        total_invested: Number(agg?.total_invested || 0),
+        total_returned: Number(agg?.total_returned || 0),
+        daily_budget: 1000,
+        num_portfolios: 1,
+        capital_at_risk: capitalAtRisk,
+        open_positions_cost: Number(agg?.open_count || 0),
+        daily_economics: spendRows.map(r => ({
+          day: r.day, trades: Number(r.trades), wins: Number(r.wins), losses: Number(r.losses),
+          net_pnl: Number(r.net_pnl), gross_wins: Number(r.gross_wins), gross_losses: Number(r.gross_losses),
+          capital_deployed: Number(r.capital_deployed),
+        })),
+        position_count: Number(agg?.open_count || 0),
+        open_orders: Number(agg?.open_markets || 0),
+        daily_pnl: Number(agg?.daily_pnl || 0),
+        total_pnl: realizedPnl,
+        markets_tracked: cachedActiveMarkets.length,
+        today_trades: Number(today?.trades || 0),
+        today_wins: Number(today?.wins || 0),
+        today_losses: Number(today?.losses || 0),
+        recent_fills: fills.map(f => ({
+          ticker: f.ticker, title: f.title, side: f.side,
+          count: Number(f.count), yes_price: Number(f.price_cents),
+          pnl: f.pnl_cents == null ? null : Number(f.pnl_cents), status: f.status,
+        })),
+        pnl_history: pnlHistory.map(p => Number(p.daily_pnl) / 100),
+        top_markets: byTicker.filter(m => Number(m.pnl) >= 0).slice(0, 10).map(mkMarket),
+        worst_markets: byTicker.filter(m => Number(m.pnl) < 0).slice(0, 5).map(mkMarket),
+        strategies: [],
+        top_opportunities: [],
+      });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
   // ════════════════════════════════════════
   // SCAN CONFIG (User-adjustable intervals + risk params)
   // ════════════════════════════════════════
diff --git a/kalshi-dash/src/pages/Dashboard.jsx b/kalshi-dash/src/pages/Dashboard.jsx
index 9d66324..56b046f 100644
--- a/kalshi-dash/src/pages/Dashboard.jsx
+++ b/kalshi-dash/src/pages/Dashboard.jsx
@@ -1266,16 +1266,27 @@ function CollapsibleSection({ title, defaultOpen = true, children }) {
 export default function Dashboard() {
   const [data, setData] = useState(null);
   const [loading, setLoading] = useState(true);
+  // Real-money-only toggle — persists across reloads.
+  const [realMode, setRealMode] = useState(() => localStorage.getItem('ken_real_mode') === '1');
 
   useEffect(() => {
     loadDashboard();
     const i = setInterval(loadDashboard, 30000);
     return () => clearInterval(i);
-  }, []);
+  }, [realMode]);
+
+  function toggleReal() {
+    setRealMode(prev => {
+      const next = !prev;
+      localStorage.setItem('ken_real_mode', next ? '1' : '0');
+      return next;
+    });
+    setLoading(true);
+  }
 
   async function loadDashboard() {
     try {
-      const r = await api('/dashboard');
+      const r = await api(realMode ? '/dashboard/real' : '/dashboard');
       setData(r);
     } catch {}
     setLoading(false);
@@ -1300,7 +1311,7 @@ export default function Dashboard() {
 
   const statCards = [
     {
-      label: 'Paper Balance',
+      label: realMode ? 'Real Balance' : 'Paper Balance',
       rawValue: d.balance || 0,
       displayValue: d.balance ? `$${(d.balance / 100).toLocaleString(undefined, { minimumFractionDigits: 2 })}` : '—',
       color: 'var(--green)',
@@ -1361,9 +1372,29 @@ export default function Dashboard() {
               LIVE
             </span>
           </div>
-          <p className="section-subtitle">Ken event contracts overview</p>
+          <p className="section-subtitle">
+            {realMode ? 'Live real-money account — actual Kalshi orders only' : 'Ken event contracts overview (paper arena)'}
+          </p>
         </div>
         <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+          {/* ── Paper ⟷ Real-money toggle ── */}
+          <button
+            onClick={toggleReal}
+            title="Toggle real-money-only view (actual Kalshi orders, not the 99-bot paper arena)"
+            className="badge"
+            style={{
+              cursor: 'pointer',
+              display: 'flex', alignItems: 'center', gap: 6, fontWeight: 700,
+              border: `1px solid ${realMode ? 'rgba(255,92,92,0.35)' : 'var(--border)'}`,
+              background: realMode ? 'var(--red-dim)' : 'rgba(107,114,128,0.12)',
+              color: realMode ? 'var(--red)' : 'var(--text-muted)',
+            }}
+          >
+            <span style={{ width: 8, height: 8, borderRadius: '50%', display: 'inline-block',
+              background: realMode ? 'var(--red)' : 'var(--text-muted)',
+              boxShadow: realMode ? '0 0 6px var(--red)' : 'none' }} />
+            {realMode ? 'REAL MONEY' : 'PAPER ARENA'}
+          </button>
           <span
             className="badge"
             style={d.env === 'prod'

← 2bec9fc auto-save: 2026-08-04T09:32:01 (1 files) — kalshi-dash/place  ·  back to Ken  ·  Ken: make real-money dashboard mode deep-linkable via ?real= aa47a3b →