[object Object]

← back to Bertha

chore(alshi-dash): update 1 file (.js) [+1097/-2]

198a6d07544ce2b745d30b9432e73c8db30d0cfc · 2026-02-16 06:47:30 +0000 · DW Commit Agent

Files touched

Diff

commit 198a6d07544ce2b745d30b9432e73c8db30d0cfc
Author: DW Commit Agent <commit-agent@dw-agents.com>
Date:   Mon Feb 16 06:47:30 2026 +0000

    chore(alshi-dash): update 1 file (.js) [+1097/-2]
---
 kalshi-dash/server.js | 1099 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 1097 insertions(+), 2 deletions(-)

diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index f21d1ec..7458969 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -1626,8 +1626,658 @@ const routes = {
       },
     });
   },
+
+  // ── Portfolios API ──
+  'GET /api/portfolios': async (req, res) => {
+    try {
+      const { rows: portfolios } = await kenQ(`SELECT * FROM ken_portfolios ORDER BY id`);
+      // Get today's trade counts per portfolio
+      for (const pf of portfolios) {
+        const { rows: today } = await kenQ(`
+          SELECT COUNT(*) as trades_today, COALESCE(SUM(cost_cents),0) as spent_today
+          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at::date = CURRENT_DATE
+        `, [pf.id]);
+        pf.trades_today = parseInt(today[0]?.trades_today) || 0;
+        pf.spent_today = parseInt(today[0]?.spent_today) || 0;
+        pf.budget_remaining = pf.daily_budget_cents - pf.spent_today;
+
+        // Win rate
+        const totalResolved = pf.trades_won + pf.trades_lost + pf.trades_expired;
+        pf.win_rate = totalResolved > 0 ? Math.round(pf.trades_won / totalResolved * 100) : 0;
+        pf.total_resolved = totalResolved;
+      }
+      json(res, portfolios);
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  'GET /api/portfolio-trades': async (req, res) => {
+    try {
+      const url = new URL(req.url, 'http://x');
+      const pfId = url.searchParams.get('portfolio_id');
+      const status = url.searchParams.get('status');
+      let q = `SELECT * FROM ken_portfolio_trades`;
+      const params = [];
+      const conds = [];
+      if (pfId) { conds.push(`portfolio_id = $${params.length+1}`); params.push(pfId); }
+      if (status) { conds.push(`status = $${params.length+1}`); params.push(status); }
+      if (conds.length) q += ' WHERE ' + conds.join(' AND ');
+      q += ' ORDER BY created_at DESC LIMIT 200';
+      const { rows } = await kenQ(q, params);
+      json(res, rows);
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  'GET /api/predictions-history': async (req, res) => {
+    try {
+      const { rows } = await kenQ(`
+        SELECT id, ticker, title, signal_type, side, confidence, entry_price, exit_price,
+               edge_bps, outcome, pnl_cents, reasoning, created_at, resolved_at
+        FROM ken_predictions
+        WHERE outcome != 'pending'
+        ORDER BY resolved_at DESC NULLS LAST
+        LIMIT 100
+      `);
+      // Also get stats
+      const { rows: stats } = await kenQ(`
+        SELECT outcome, COUNT(*) as cnt, ROUND(AVG(pnl_cents),1) as avg_pnl,
+               ROUND(AVG(confidence),1) as avg_conf
+        FROM ken_predictions WHERE outcome != 'pending'
+        GROUP BY outcome
+      `);
+      json(res, { predictions: rows, stats });
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  'GET /api/daily-pnl': async (req, res) => {
+    try {
+      const { rows } = await kenQ(`
+        SELECT dp.*, p.name as portfolio_name
+        FROM ken_daily_pnl dp
+        JOIN ken_portfolios p ON p.id = dp.portfolio_id
+        ORDER BY dp.date DESC, dp.portfolio_id
+        LIMIT 200
+      `);
+      json(res, rows);
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  // ── Heating & Weather Intelligence API ──
+  'GET /api/heating-intel': async (req, res) => {
+    try {
+      const { rows: posts } = await kenQ(`
+        SELECT source, subreddit as area, keyword, post_title as title, post_url as url,
+               score, sentiment_score, raw_text, created_at
+        FROM ken_sentiment
+        WHERE source IN ('heating_intel', 'noaa_alert', 'noaa_forecast')
+        ORDER BY created_at DESC
+        LIMIT 50
+      `);
+      const { rows: stats } = await kenQ(`
+        SELECT source, COUNT(*) as cnt,
+               ROUND(AVG(sentiment_score)::numeric, 2) as avg_sentiment,
+               ROUND(AVG(score)::numeric, 0) as avg_score
+        FROM ken_sentiment
+        WHERE source IN ('heating_intel', 'noaa_alert', 'noaa_forecast')
+          AND created_at > NOW() - INTERVAL '7 days'
+        GROUP BY source
+      `);
+      json(res, { posts, stats, keywords: HEATING_KEYWORDS });
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  // ── Ken Inc Command Center API ──
+  'GET /api/inc': async (req, res) => {
+    const mem = process.memoryUsage();
+    // Get recent signals
+    let recentSignals = [];
+    try {
+      const { rows } = await kenQ(`SELECT market_id, direction, expected_edge, confidence, reasoning, risk_approved, created_at
+        FROM ken_strategy_signals ORDER BY created_at DESC LIMIT 20`);
+      recentSignals = rows;
+    } catch(e) {}
+
+    // Get pipeline stats
+    let pipelineStats = {};
+    try {
+      const { rows } = await kenQ(`SELECT COUNT(*) as total_signals,
+        COUNT(*) FILTER (WHERE risk_approved = true) as approved,
+        COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '1 hour') as last_hour,
+        AVG(ABS(expected_edge::numeric)) as avg_edge,
+        MAX(confidence::numeric) as max_confidence
+        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'`);
+      pipelineStats = rows[0] || {};
+    } catch(e) {}
+
+    // Sentiment stats
+    let sentimentStats = {};
+    try {
+      const { rows } = await kenQ(`SELECT COUNT(*) as total_articles,
+        COUNT(DISTINCT source) as unique_sources,
+        AVG(sentiment_score) as avg_sentiment
+        FROM ken_sentiment WHERE captured_at > NOW() - INTERVAL '24 hours'`);
+      sentimentStats = rows[0] || {};
+    } catch(e) {}
+
+    // Market snapshot stats
+    let marketStats = {};
+    try {
+      const { rows } = await kenQ(`SELECT COUNT(DISTINCT ticker) as tracked_markets
+        FROM ken_market_snapshots WHERE captured_at > NOW() - INTERVAL '24 hours'`);
+      marketStats = rows[0] || {};
+    } catch(e) {}
+
+    // Build sub-agent status
+    const subAgents = [
+      {
+        id: 'scout', name: 'Scout', role: 'Intelligence Gatherer', emoji: '🔍',
+        color: '#3B82F6', status: 'active',
+        desc: 'Sweeps 77 subreddits, 71 RSS feeds, HN, YouTube, Bluesky, Mastodon, Discord',
+        stats: { articles24h: parseInt(sentimentStats.total_articles) || 0, sources: parseInt(sentimentStats.unique_sources) || 0 },
+        skills: ['Reddit Scraping', 'RSS Feeds', 'Social Media', 'GDELT TV', 'YouTube', 'Lemmy', 'Lobsters'],
+      },
+      {
+        id: 'oracle', name: 'Oracle', role: 'Monte Carlo Engine', emoji: '🎲',
+        color: '#8B5CF6', status: 'active',
+        desc: 'Runs 15,000 iteration Monte Carlo simulations on every signal for probability validation',
+        stats: { iterations: 15000, cores: os.cpus().length, momentumTracked: Object.keys(cachedMomentumMap).length },
+        skills: ['Monte Carlo Simulation', 'Bayesian Inference', 'Ensemble Probability', 'Confidence Intervals'],
+      },
+      {
+        id: 'gemini', name: 'Nova', role: 'AI Analyst', emoji: '🧠',
+        color: '#EC4899', status: geminiCallCount < GEMINI_DAILY_LIMIT ? 'active' : 'rate_limited',
+        desc: 'Gemini 2.0 Flash analyzes high-edge signals with natural language reasoning',
+        stats: { callsToday: geminiCallCount, dailyBudget: GEMINI_DAILY_LIMIT, model: GEMINI_MODEL },
+        skills: ['Signal Analysis', 'News Interpretation', 'Risk Assessment', 'Market Reasoning'],
+      },
+      {
+        id: 'pulse', name: 'Pulse', role: 'Sentiment Analyst', emoji: '💓',
+        color: '#EF4444', status: 'active',
+        desc: '75 event-specific rules across 7 categories with certainty modifiers and source diversity scoring',
+        stats: { rules: 75, categories: 7, avgSentiment: parseFloat(sentimentStats.avg_sentiment || 0).toFixed(3) },
+        skills: ['Event Sentiment', 'Certainty Detection', 'Multi-source Agreement', 'Category Weighting'],
+      },
+      {
+        id: 'hawk', name: 'Hawk', role: 'Market Watcher', emoji: '🦅',
+        color: '#F59E0B', status: 'active',
+        desc: 'Tracks momentum, volume spikes, spreads, and price action across all active markets',
+        stats: { activeMarkets: cachedActiveMarkets.length, trackedMarkets: parseInt(marketStats.tracked_markets) || 0 },
+        skills: ['Momentum Tracking', 'Volume Analysis', 'Spread Detection', 'Price Action'],
+      },
+      {
+        id: 'arbiter', name: 'Arbiter', role: 'Cross-Market Arbitrage', emoji: '⚖️',
+        color: '#14B8A6', status: 'active',
+        desc: 'Compares Kalshi prices vs Polymarket, PredictIt, Manifold, Metaculus for arbitrage',
+        stats: { platforms: 5 },
+        skills: ['Polymarket', 'PredictIt', 'Manifold', 'Metaculus', 'Fuzzy Matching'],
+      },
+      {
+        id: 'shield', name: 'Shield', role: 'Risk Manager', emoji: '🛡️',
+        color: '#22C55E', status: 'active',
+        desc: 'Enforces exposure limits, edge thresholds, time decay, and liquidity requirements',
+        stats: {
+          signalsApproved: parseInt(pipelineStats.approved) || 0,
+          totalSignals: parseInt(pipelineStats.total_signals) || 0,
+          maxConf: parseFloat(pipelineStats.max_confidence || 0).toFixed(2),
+        },
+        skills: ['Position Sizing', 'Exposure Limits', 'Edge Threshold', 'Time Decay', 'Liquidity Check'],
+      },
+      {
+        id: 'storm', name: 'Storm', role: 'Weather Predictor', emoji: '⛈️',
+        color: '#06B6D4', status: 'active',
+        desc: 'NWS forecast data + Bayesian model for 11 US cities, climatological priors by season',
+        stats: { cities: 11 },
+        skills: ['NWS Forecasts', 'Bayesian Weather', 'Climatological Priors', 'Temperature/Precip/Wind'],
+      },
+    ];
+
+    json(res, {
+      ceo: { name: 'Ken', title: 'CEO, Ken Inc.', tagline: 'Trading Intelligence Corporation', color: '#FF69B4' },
+      agents: subAgents,
+      pipeline: {
+        signalsLast24h: parseInt(pipelineStats.total_signals) || 0,
+        approvedLast24h: parseInt(pipelineStats.approved) || 0,
+        lastHour: parseInt(pipelineStats.last_hour) || 0,
+        avgEdge: parseFloat(pipelineStats.avg_edge || 0).toFixed(3),
+      },
+      system: {
+        memory_mb: Math.round(mem.rss / 1024 / 1024),
+        uptime_hours: Math.round(process.uptime() / 3600 * 10) / 10,
+        cpuCores: os.cpus().length,
+        pid: process.pid,
+      },
+      recentSignals: recentSignals.map(s => ({
+        market: s.market_id,
+        direction: s.direction,
+        edge: parseFloat(s.expected_edge || 0),
+        confidence: parseFloat(s.confidence || 0),
+        approved: s.risk_approved,
+        reasoning: s.reasoning?.substring(0, 200),
+        time: s.created_at,
+      })),
+    });
+  },
 };
 
+// ════════════════════════════════════════
+// KEN INC COMMAND CENTER PAGE
+// ════════════════════════════════════════
+const KEN_INC_HTML = `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Ken Inc. | Trading Intelligence Command Center</title>
+<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
+<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
+<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+:root{--bg:#050510;--card:rgba(15,15,35,0.85);--border:rgba(255,255,255,0.06);--text:#e2e8f0;--muted:#64748b;--pink:#ff69b4;--blue:#3b82f6;--purple:#8b5cf6;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--cyan:#06b6d4;--teal:#14b8a6}
+body{font-family:'SF Pro Display',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
+
+/* Animated gradient background */
+body::before{content:'';position:fixed;top:0;left:0;right:0;bottom:0;background:radial-gradient(ellipse at 20% 50%,rgba(139,92,246,0.08) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(255,105,180,0.06) 0%,transparent 50%),radial-gradient(ellipse at 50% 80%,rgba(59,130,246,0.06) 0%,transparent 50%);z-index:-1;animation:bgPulse 8s ease-in-out infinite alternate}
+@keyframes bgPulse{0%{opacity:0.6}100%{opacity:1}}
+
+/* Grid lines overlay */
+body::after{content:'';position:fixed;top:0;left:0;right:0;bottom:0;background-image:linear-gradient(rgba(255,255,255,0.015) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.015) 1px,transparent 1px);background-size:60px 60px;z-index:-1;animation:gridScroll 20s linear infinite}
+@keyframes gridScroll{to{background-position:60px 60px}}
+
+.container{max-width:1400px;margin:0 auto;padding:20px}
+
+/* Header */
+.header{text-align:center;padding:30px 20px;position:relative}
+.header-glow{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:400px;height:400px;background:radial-gradient(circle,rgba(255,105,180,0.15) 0%,transparent 70%);border-radius:50%;filter:blur(60px);animation:glowPulse 4s ease-in-out infinite alternate;pointer-events:none}
+@keyframes glowPulse{0%{opacity:0.5;transform:translate(-50%,-50%) scale(0.9)}100%{opacity:1;transform:translate(-50%,-50%) scale(1.1)}}
+.ceo-avatar{width:80px;height:80px;border-radius:50%;background:linear-gradient(135deg,#ff69b4,#8b5cf6);display:flex;align-items:center;justify-content:center;font-size:36px;margin:0 auto 12px;box-shadow:0 0 30px rgba(255,105,180,0.4),0 0 60px rgba(139,92,246,0.2);animation:avatarFloat 3s ease-in-out infinite;position:relative;z-index:2}
+@keyframes avatarFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
+.ceo-name{font-size:32px;font-weight:800;background:linear-gradient(135deg,#ff69b4,#c084fc,#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;position:relative;z-index:2}
+.ceo-title{color:var(--muted);font-size:14px;letter-spacing:2px;text-transform:uppercase;margin-top:4px;position:relative;z-index:2}
+.ceo-tagline{color:rgba(255,105,180,0.6);font-size:11px;letter-spacing:4px;text-transform:uppercase;margin-top:8px;position:relative;z-index:2}
+
+/* Stats bar */
+.stats-bar{display:flex;justify-content:center;gap:30px;margin:24px 0;flex-wrap:wrap}
+.stat-item{text-align:center;padding:12px 24px;background:var(--card);border:1px solid var(--border);border-radius:12px;backdrop-filter:blur(10px);min-width:120px}
+.stat-val{font-size:24px;font-weight:700;font-variant-numeric:tabular-nums}
+.stat-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
+
+/* Agent Network */
+.network{position:relative;margin:30px auto;max-width:1200px}
+.agents-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
+@media(max-width:1024px){.agents-grid{grid-template-columns:repeat(2,1fr)}}
+@media(max-width:600px){.agents-grid{grid-template-columns:1fr}}
+
+/* Agent Card */
+.agent-card{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:20px;backdrop-filter:blur(10px);transition:all 0.3s cubic-bezier(0.4,0,0.2,1);position:relative;overflow:hidden;cursor:pointer}
+.agent-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;border-radius:16px 16px 0 0;opacity:0;transition:opacity 0.3s}
+.agent-card:hover{transform:translateY(-4px);border-color:rgba(255,255,255,0.12);box-shadow:0 20px 40px rgba(0,0,0,0.3)}
+.agent-card:hover::before{opacity:1}
+.agent-card.expanded{grid-column:span 2}
+.agent-header{display:flex;align-items:center;gap:12px}
+.agent-emoji{font-size:28px;width:48px;height:48px;display:flex;align-items:center;justify-content:center;border-radius:12px;flex-shrink:0}
+.agent-name{font-size:16px;font-weight:700}
+.agent-role{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px}
+.status-indicator{width:8px;height:8px;border-radius:50%;position:absolute;top:16px;right:16px;animation:statusPulse 2s ease-in-out infinite}
+@keyframes statusPulse{0%,100%{opacity:1;box-shadow:0 0 4px currentColor}50%{opacity:0.6;box-shadow:0 0 12px currentColor}}
+.agent-desc{font-size:12px;color:var(--muted);line-height:1.5;margin-top:10px}
+.agent-stats{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}
+.agent-stat{padding:3px 10px;background:rgba(255,255,255,0.04);border:1px solid var(--border);border-radius:6px;font-size:11px;font-variant-numeric:tabular-nums}
+.agent-stat strong{color:var(--text);margin-right:3px}
+.skills-wrap{margin-top:12px;display:flex;flex-wrap:wrap;gap:4px}
+.skill-tag{padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;letter-spacing:0.3px}
+
+/* Signal Feed */
+.signal-feed{margin-top:30px}
+.signal-feed h2{font-size:16px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:8px}
+.signal-feed h2 .live-dot{width:8px;height:8px;border-radius:50%;background:var(--red);animation:livePulse 1s ease-in-out infinite}
+@keyframes livePulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:0.5;transform:scale(0.8)}}
+.signal-list{display:flex;flex-direction:column;gap:6px}
+.signal-item{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--card);border:1px solid var(--border);border-radius:10px;font-size:12px;backdrop-filter:blur(10px);transition:border-color 0.2s}
+.signal-item:hover{border-color:rgba(255,255,255,0.12)}
+.signal-dir{padding:3px 8px;border-radius:4px;font-weight:700;font-size:11px;min-width:60px;text-align:center}
+.signal-dir.buy-yes{background:rgba(34,197,94,0.15);color:var(--green)}
+.signal-dir.buy-no{background:rgba(239,68,68,0.15);color:var(--red)}
+.signal-market{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.signal-edge{font-weight:700;font-variant-numeric:tabular-nums;min-width:50px;text-align:right}
+.signal-conf{color:var(--muted);font-variant-numeric:tabular-nums;min-width:40px;text-align:right}
+.signal-time{color:var(--muted);font-size:11px;min-width:65px;text-align:right}
+.signal-approved{width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;flex-shrink:0}
+
+/* Connection Lines Animation */
+.connections-svg{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}
+.data-flow{stroke-dasharray:8 4;animation:flowDash 1s linear infinite}
+@keyframes flowDash{to{stroke-dashoffset:-12}}
+
+/* Particle effect */
+.particles{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}
+.particle{position:absolute;width:2px;height:2px;border-radius:50%;opacity:0;animation:particleFloat linear infinite}
+@keyframes particleFloat{0%{opacity:0;transform:translateY(0)}10%{opacity:0.6}90%{opacity:0.6}100%{opacity:0;transform:translateY(-100vh)}}
+
+/* Clock */
+.clock{position:fixed;top:16px;right:20px;text-align:right;z-index:10}
+.clock-time{font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--text)}
+.clock-date{font-size:11px;color:var(--muted)}
+
+/* Footer */
+.footer{text-align:center;padding:30px;color:var(--muted);font-size:11px;letter-spacing:1px}
+.footer a{color:var(--pink);text-decoration:none}
+</style>
+</head>
+<body>
+<div id="root"></div>
+<script type="text/babel">
+const { useState, useEffect, useRef } = React;
+
+const AUTH = btoa('admin:DWSecure2024!');
+const api = (url) => fetch(url, { headers: { 'Authorization': 'Basic ' + AUTH } }).then(r => r.json());
+
+function Particles() {
+  const particles = Array.from({ length: 30 }, (_, i) => ({
+    id: i,
+    left: Math.random() * 100 + '%',
+    delay: Math.random() * 10 + 's',
+    duration: 8 + Math.random() * 12 + 's',
+    color: ['#ff69b4','#8b5cf6','#3b82f6','#22c55e','#f59e0b','#06b6d4'][Math.floor(Math.random()*6)],
+    size: 1 + Math.random() * 2 + 'px',
+  }));
+  return (
+    <div className="particles">
+      {particles.map(p => (
+        <div key={p.id} className="particle" style={{
+          left: p.left, bottom: 0, animationDelay: p.delay, animationDuration: p.duration,
+          background: p.color, width: p.size, height: p.size, boxShadow: '0 0 6px ' + p.color,
+        }} />
+      ))}
+    </div>
+  );
+}
+
+function Clock() {
+  const [now, setNow] = useState(new Date());
+  useEffect(() => { const iv = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(iv); }, []);
+  const pt = now.toLocaleString('en-US', { timeZone: 'America/Los_Angeles', hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true });
+  const dateStr = now.toLocaleDateString('en-US', { timeZone: 'America/Los_Angeles', weekday: 'short', month: 'short', day: 'numeric' });
+  return <div className="clock"><div className="clock-time">{pt} PT</div><div className="clock-date">{dateStr}</div></div>;
+}
+
+function AgentCard({ agent, expanded, onToggle }) {
+  const bgColor = agent.color + '15';
+  return (
+    <div className={'agent-card' + (expanded ? ' expanded' : '')} onClick={onToggle}
+      style={{ '--agent-color': agent.color }}>
+      <div className="status-indicator" style={{ background: agent.status === 'active' ? 'var(--green)' : 'var(--yellow)', color: agent.status === 'active' ? 'var(--green)' : 'var(--yellow)' }} />
+      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 3, borderRadius: '16px 16px 0 0', background: agent.color, opacity: 0.7 }} />
+      <div className="agent-header">
+        <div className="agent-emoji" style={{ background: bgColor }}>{agent.emoji}</div>
+        <div>
+          <div className="agent-name" style={{ color: agent.color }}>{agent.name}</div>
+          <div className="agent-role">{agent.role}</div>
+        </div>
+      </div>
+      <div className="agent-desc">{agent.desc}</div>
+      <div className="agent-stats">
+        {Object.entries(agent.stats || {}).map(([k, v]) => (
+          <div key={k} className="agent-stat"><strong>{typeof v === 'number' ? v.toLocaleString() : v}</strong> {k.replace(/([A-Z])/g, ' $1').toLowerCase()}</div>
+        ))}
+      </div>
+      {expanded && (
+        <div className="skills-wrap">
+          {(agent.skills || []).map(s => (
+            <span key={s} className="skill-tag" style={{ background: agent.color + '20', color: agent.color, border: '1px solid ' + agent.color + '30' }}>{s}</span>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
+
+function SignalFeed({ signals }) {
+  if (!signals?.length) return <div style={{ textAlign: 'center', color: 'var(--muted)', padding: 30, fontSize: 13 }}>Waiting for signals...</div>;
+
+  function timeAgo(ts) {
+    const ms = Date.now() - new Date(ts).getTime();
+    if (ms < 60000) return Math.floor(ms/1000) + 's ago';
+    if (ms < 3600000) return Math.floor(ms/60000) + 'm ago';
+    return Math.floor(ms/3600000) + 'h ago';
+  }
+
+  return (
+    <div className="signal-list">
+      {signals.slice(0, 12).map((s, i) => (
+        <div key={i} className="signal-item">
+          <div className={'signal-dir ' + (s.direction === 'BUY_YES' ? 'buy-yes' : 'buy-no')}>
+            {s.direction === 'BUY_YES' ? 'BUY YES' : 'BUY NO'}
+          </div>
+          <div className="signal-market" title={s.market}>{s.market}</div>
+          <div className="signal-edge" style={{ color: Math.abs(s.edge) > 0.1 ? 'var(--green)' : 'var(--muted)' }}>
+            {(s.edge * 100).toFixed(1)}%
+          </div>
+          <div className="signal-conf">{(s.confidence * 100).toFixed(0)}%</div>
+          <div className="signal-approved" style={{ background: s.approved ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.1)', color: s.approved ? 'var(--green)' : 'var(--red)' }}>
+            {s.approved ? '\\u2713' : '\\u2717'}
+          </div>
+          <div className="signal-time">{timeAgo(s.time)}</div>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+// ── PORTFOLIOS TAB ──
+function PortfoliosTab() {
+  const [portfolios, setPortfolios] = useState([]);
+  const [trades, setTrades] = useState([]);
+  const [selectedPf, setSelectedPf] = useState(null);
+  useEffect(() => {
+    api('/api/portfolios').then(setPortfolios).catch(()=>{});
+    api('/api/portfolio-trades').then(setTrades).catch(()=>{});
+    const iv = setInterval(() => {
+      api('/api/portfolios').then(setPortfolios).catch(()=>{});
+      if (selectedPf) api('/api/portfolio-trades?portfolio_id='+selectedPf).then(setTrades).catch(()=>{});
+    }, 20000);
+    return () => clearInterval(iv);
+  }, [selectedPf]);
+
+  const personalities = { aggressive:'Swings big on high-edge', conservative:'Quality over quantity', weather:'Weather markets only', momentum:'Follows the trend', contrarian:'Bets against the crowd', ai_enhanced:'Gemini AI picks only', mc_validated:'Monte Carlo confirmed', scalper:'Tiny edges, high volume', patient:'Waits for perfect setup', random:'Control group (luck test)' };
+  const emojis = { aggressive:'🔥', conservative:'🏛️', weather:'⛈️', momentum:'🚀', contrarian:'🔄', ai_enhanced:'🧠', mc_validated:'🎲', scalper:'⚡', patient:'🐢', random:'🎰' };
+  const colors = { aggressive:'#ef4444', conservative:'#3b82f6', weather:'#06b6d4', momentum:'#f59e0b', contrarian:'#8b5cf6', ai_enhanced:'#ec4899', mc_validated:'#a855f7', scalper:'#eab308', patient:'#22c55e', random:'#64748b' };
+
+  return (<div>
+    <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(280px,1fr))',gap:12,marginBottom:20}}>
+      {portfolios.map(pf => {
+        const c = colors[pf.strategy]||'var(--muted)';
+        return (<div key={pf.id} onClick={()=>{setSelectedPf(pf.id);api('/api/portfolio-trades?portfolio_id='+pf.id).then(setTrades);}}
+          style={{background:'var(--card)',border:selectedPf===pf.id?'1px solid '+c:'1px solid var(--border)',borderRadius:14,padding:16,cursor:'pointer',transition:'all .2s',position:'relative',overflow:'hidden'}}>
+          <div style={{position:'absolute',top:0,left:0,right:0,height:3,background:c,opacity:.7}}/>
+          <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:8}}>
+            <span style={{fontSize:24}}>{emojis[pf.strategy]||'📊'}</span>
+            <div>
+              <div style={{fontSize:15,fontWeight:700,color:c}}>{pf.name}</div>
+              <div style={{fontSize:10,color:'var(--muted)',textTransform:'uppercase',letterSpacing:.5}}>{pf.strategy}</div>
+            </div>
+            <div style={{marginLeft:'auto',textAlign:'right'}}>
+              <div style={{fontSize:18,fontWeight:700,color:pf.balance_cents>=0?'var(--green)':'var(--red)',fontVariantNumeric:'tabular-nums'}}>
+                {pf.balance_cents>=0?'+':''}\${(pf.balance_cents/100).toFixed(2)}
+              </div>
+              <div style={{fontSize:10,color:'var(--muted)'}}>P&L</div>
+            </div>
+          </div>
+          <div style={{fontSize:11,color:'var(--muted)',marginBottom:8,lineHeight:1.4}}>{personalities[pf.strategy]}</div>
+          <div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
+            <span style={{padding:'2px 8px',borderRadius:4,fontSize:10,background:'rgba(34,197,94,.12)',color:'var(--green)'}}>W:{pf.trades_won}</span>
+            <span style={{padding:'2px 8px',borderRadius:4,fontSize:10,background:'rgba(239,68,68,.12)',color:'var(--red)'}}>L:{pf.trades_lost}</span>
+            <span style={{padding:'2px 8px',borderRadius:4,fontSize:10,background:'rgba(255,255,255,.05)',color:'var(--muted)'}}>WR:{pf.win_rate}%</span>
+            <span style={{padding:'2px 8px',borderRadius:4,fontSize:10,background:'rgba(255,255,255,.05)',color:'var(--muted)'}}>Today:{pf.trades_today}</span>
+            <span style={{padding:'2px 8px',borderRadius:4,fontSize:10,background:pf.budget_remaining>0?'rgba(34,197,94,.08)':'rgba(239,68,68,.08)',color:pf.budget_remaining>0?'var(--green)':'var(--red)'}}>{'$'}{(pf.budget_remaining/100).toFixed(2)} left</span>
+            {pf.streak!==0&&<span style={{padding:'2px 8px',borderRadius:4,fontSize:10,background:pf.streak>0?'rgba(34,197,94,.15)':'rgba(239,68,68,.15)',color:pf.streak>0?'var(--green)':'var(--red)'}}>{pf.streak>0?'🔥':'❄️'}{Math.abs(pf.streak)} streak</span>}
+          </div>
+        </div>);
+      })}
+    </div>
+    {selectedPf && trades.length > 0 && (<div>
+      <h3 style={{fontSize:14,fontWeight:600,marginBottom:10,color:'var(--muted)'}}>Recent Trades — {portfolios.find(p=>p.id===selectedPf)?.name}</h3>
+      <div style={{display:'flex',flexDirection:'column',gap:4}}>
+        {trades.slice(0,20).map((t,i) => (<div key={i} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 14px',background:'var(--card)',border:'1px solid var(--border)',borderRadius:8,fontSize:12}}>
+          <span style={{padding:'2px 8px',borderRadius:4,fontWeight:700,fontSize:11,background:t.direction==='BUY_YES'?'rgba(34,197,94,.15)':'rgba(239,68,68,.15)',color:t.direction==='BUY_YES'?'var(--green)':'var(--red)',minWidth:55,textAlign:'center'}}>{t.direction==='BUY_YES'?'YES':'NO'}</span>
+          <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{t.market_title||t.market_id}</span>
+          <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>{t.entry_price_cents}¢ x{t.contracts}</span>
+          <span style={{fontWeight:700,fontVariantNumeric:'tabular-nums',color:t.status==='won'?'var(--green)':t.status==='lost'?'var(--red)':'var(--muted)',minWidth:50,textAlign:'right'}}>
+            {t.status==='open'?'OPEN':t.pnl_cents>0?'+'+t.pnl_cents+'¢':t.pnl_cents+'¢'}
+          </span>
+          <span style={{fontSize:10,color:'var(--muted)',minWidth:60,textAlign:'right'}}>{new Date(t.created_at).toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit',hour12:true})}</span>
+        </div>))}
+      </div>
+    </div>)}
+    {(!selectedPf || trades.length === 0) && <div style={{textAlign:'center',padding:30,color:'var(--muted)',fontSize:13}}>Click a portfolio to see its trades. Portfolios auto-trade every 2 minutes from approved signals.</div>}
+  </div>);
+}
+
+// ── PREDICTIONS TAB ──
+function PredictionsTab() {
+  const [data, setData] = useState(null);
+  useEffect(() => {
+    api('/api/predictions-history').then(setData).catch(()=>{});
+    const iv = setInterval(() => api('/api/predictions-history').then(setData).catch(()=>{}), 30000);
+    return () => clearInterval(iv);
+  }, []);
+  if (!data) return <div style={{textAlign:'center',padding:40,color:'var(--muted)'}}>Loading predictions...</div>;
+
+  const statsMap = {};
+  (data.stats||[]).forEach(s => { statsMap[s.outcome] = s; });
+
+  return (<div>
+    <div style={{display:'flex',gap:12,marginBottom:20,flexWrap:'wrap'}}>
+      {['win','loss','expired'].map(o => {
+        const s = statsMap[o]||{cnt:0,avg_pnl:0,avg_conf:0};
+        const c = o==='win'?'var(--green)':o==='loss'?'var(--red)':'var(--muted)';
+        return (<div key={o} style={{background:'var(--card)',border:'1px solid var(--border)',borderRadius:12,padding:16,flex:1,minWidth:140,textAlign:'center'}}>
+          <div style={{fontSize:28,fontWeight:700,color:c}}>{s.cnt}</div>
+          <div style={{fontSize:10,color:'var(--muted)',textTransform:'uppercase',letterSpacing:1,marginTop:2}}>{o==='win'?'Wins':o==='loss'?'Losses':'Expired'}</div>
+          <div style={{fontSize:11,color:'var(--muted)',marginTop:4}}>Avg P&L: <strong style={{color:c}}>{parseFloat(s.avg_pnl||0)>0?'+':''}{s.avg_pnl||0}¢</strong></div>
+          <div style={{fontSize:11,color:'var(--muted)'}}>Avg Conf: {s.avg_conf||0}%</div>
+        </div>);
+      })}
+      <div style={{background:'var(--card)',border:'1px solid var(--border)',borderRadius:12,padding:16,flex:1,minWidth:140,textAlign:'center'}}>
+        <div style={{fontSize:28,fontWeight:700,color:'var(--blue)'}}>{statsMap.win?Math.round(parseInt(statsMap.win.cnt)/(parseInt(statsMap.win.cnt)+parseInt(statsMap.loss?.cnt||0))*100):0}%</div>
+        <div style={{fontSize:10,color:'var(--muted)',textTransform:'uppercase',letterSpacing:1,marginTop:2}}>Win Rate</div>
+        <div style={{fontSize:11,color:'var(--muted)',marginTop:4}}>W/L only (excl. expired)</div>
+      </div>
+    </div>
+
+    <h3 style={{fontSize:14,fontWeight:600,marginBottom:10,display:'flex',alignItems:'center',gap:8}}>Resolved Predictions <span style={{fontSize:11,color:'var(--muted)',fontWeight:400}}>— what actually happened</span></h3>
+    <div style={{display:'flex',flexDirection:'column',gap:4}}>
+      {(data.predictions||[]).slice(0,30).map((p,i) => {
+        const oc = p.outcome==='win'?'var(--green)':p.outcome==='loss'?'var(--red)':'var(--muted)';
+        return (<div key={i} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 14px',background:'var(--card)',border:'1px solid var(--border)',borderRadius:8,fontSize:12}}>
+          <span style={{padding:'2px 8px',borderRadius:4,fontWeight:700,fontSize:10,background:p.outcome==='win'?'rgba(34,197,94,.15)':p.outcome==='loss'?'rgba(239,68,68,.15)':'rgba(255,255,255,.05)',color:oc,minWidth:48,textAlign:'center',textTransform:'uppercase'}}>{p.outcome}</span>
+          <span style={{padding:'2px 8px',borderRadius:4,fontSize:10,fontWeight:600,background:p.side==='YES'||p.side==='yes'?'rgba(34,197,94,.1)':'rgba(239,68,68,.1)',color:p.side==='YES'||p.side==='yes'?'var(--green)':'var(--red)'}}>{p.side}</span>
+          <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={p.title}>{p.title||p.ticker}</span>
+          <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:11}}>Conf:{p.confidence}%</span>
+          <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:11}}>{p.entry_price}¢→{p.exit_price||'?'}¢</span>
+          <span style={{fontWeight:700,fontVariantNumeric:'tabular-nums',color:oc,minWidth:45,textAlign:'right'}}>{p.pnl_cents>0?'+':''}{p.pnl_cents||0}¢</span>
+          <span style={{fontSize:10,color:'var(--muted)',minWidth:70,textAlign:'right'}}>{p.resolved_at?new Date(p.resolved_at).toLocaleDateString('en-US',{month:'short',day:'numeric'}):''}</span>
+        </div>);
+      })}
+    </div>
+  </div>);
+}
+
+function App() {
+  const [data, setData] = useState(null);
+  const [expandedAgent, setExpandedAgent] = useState(null);
+  const [tab, setTab] = useState('command');
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    const load = async () => {
+      try {
+        const d = await api('/api/inc');
+        setData(d);
+        setLoading(false);
+      } catch(e) { console.error(e); setLoading(false); }
+    };
+    load();
+    const iv = setInterval(load, 15000);
+    return () => clearInterval(iv);
+  }, []);
+
+  if (loading) return (
+    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', flexDirection: 'column', gap: 16 }}>
+      <div style={{ width: 60, height: 60, borderRadius: '50%', background: 'linear-gradient(135deg,#ff69b4,#8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28, animation: 'avatarFloat 2s ease-in-out infinite' }}>K</div>
+      <div style={{ color: 'var(--muted)', fontSize: 14, letterSpacing: 2 }}>INITIALIZING KEN INC...</div>
+    </div>
+  );
+
+  const d = data;
+  const approvalRate = d.pipeline.signalsLast24h > 0
+    ? Math.round(d.pipeline.approvedLast24h / d.pipeline.signalsLast24h * 100) : 0;
+
+  const tabStyle = (t) => ({
+    padding: '10px 24px', fontSize: 13, fontWeight: 600, border: '1px solid ' + (tab===t?'rgba(255,255,255,.2)':'var(--border)'),
+    borderRadius: 10, background: tab===t?'rgba(255,255,255,.08)':'transparent', color: tab===t?'var(--text)':'var(--muted)',
+    cursor: 'pointer', transition: 'all .2s', letterSpacing: .3,
+  });
+
+  return (
+    <div className="container">
+      <Particles />
+      <Clock />
+
+      <div className="header">
+        <div className="header-glow" />
+        <div className="ceo-avatar">K</div>
+        <div className="ceo-name">Ken</div>
+        <div className="ceo-title">Chief Executive Officer</div>
+        <div className="ceo-tagline">Trading Intelligence Corporation</div>
+      </div>
+
+      <div style={{display:'flex',justifyContent:'center',gap:8,marginBottom:24,flexWrap:'wrap'}}>
+        <button style={tabStyle('command')} onClick={()=>setTab('command')}>Command Center</button>
+        <button style={tabStyle('portfolios')} onClick={()=>setTab('portfolios')}>Portfolios (10)</button>
+        <button style={tabStyle('predictions')} onClick={()=>setTab('predictions')}>Predictions</button>
+        <button style={tabStyle('signals')} onClick={()=>setTab('signals')}>Live Signals</button>
+      </div>
+
+      {tab === 'command' && <>
+        <div className="stats-bar">
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--pink)'}}>{d.agents.length}</div><div className="stat-label">Sub-Agents</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--blue)'}}>{d.pipeline.signalsLast24h}</div><div className="stat-label">Signals / 24h</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--green)'}}>{approvalRate}%</div><div className="stat-label">Approval Rate</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--purple)'}}>{d.pipeline.lastHour}</div><div className="stat-label">Last Hour</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--cyan)'}}>{(parseFloat(d.pipeline.avgEdge)*100).toFixed(1)}%</div><div className="stat-label">Avg Edge</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--yellow)'}}>{d.system.memory_mb}MB</div><div className="stat-label">Memory</div></div>
+          <div className="stat-item"><div className="stat-val">{d.system.cpuCores}</div><div className="stat-label">CPU Cores</div></div>
+        </div>
+        <div className="network">
+          <div className="agents-grid">
+            {d.agents.map(a => (
+              <AgentCard key={a.id} agent={a} expanded={expandedAgent===a.id} onToggle={()=>setExpandedAgent(expandedAgent===a.id?null:a.id)} />
+            ))}
+          </div>
+        </div>
+      </>}
+
+      {tab === 'portfolios' && <PortfoliosTab />}
+      {tab === 'predictions' && <PredictionsTab />}
+
+      {tab === 'signals' && <div className="signal-feed">
+        <h2><span className="live-dot" /> Live Signal Feed</h2>
+        <SignalFeed signals={d.recentSignals} />
+      </div>}
+
+      <div className="footer">
+        Ken Inc. Trading Intelligence &bull; {d.system.cpuCores} cores &bull; {d.system.uptime_hours}h uptime &bull; 10 paper portfolios &bull;
+        <a href="/"> Back to Dashboard</a>
+      </div>
+    </div>
+  );
+}
+
+ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+</script>
+</body>
+</html>`;
+
 // ════════════════════════════════════════
 // HTTP SERVER
 // ════════════════════════════════════════
@@ -1680,6 +2330,13 @@ const server = http.createServer(async (req, res) => {
     return json(res, { error: `Unknown API endpoint: ${req.method} ${urlPath}` }, 404);
   }
 
+  // ── Ken Inc. Command Center ──
+  if (urlPath === '/inc' || urlPath === '/inc/') {
+    res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
+    res.end(KEN_INC_HTML);
+    return;
+  }
+
   // ── Serve Static Files / SPA Routing ──
   let filePath = path.join(DIST, urlPath === '/' ? 'index.html' : urlPath);
 
@@ -2660,6 +3317,10 @@ async function redditIntelligenceLoop() {
       'Kalshi', 'Polymarket', 'PredictionMarket', 'sportsbook',
       // ── Weather (hurricane/temp/storm markets) ──
       'TropicalWeather', 'weather', 'tornado', 'hurricane', 'climate',
+      'NaturalGas', 'energy', 'propane', 'HomeImprovement',
+      // ── Heating Bills & Utility Costs ──
+      'personalfinance', 'Frugal', 'povertyfinance', 'homeowners',
+      'HVAC', 'utilities', 'electricvehicles',
       // ── Politics & Elections ──
       'politics', 'Conservative', 'Liberal', 'NeutralPolitics', 'PoliticalDiscussion',
       'fivethirtyeight', 'elections', 'supremecourt', 'moderatepolitics', 'law',
@@ -3659,6 +4320,15 @@ async function signalPipeline(activeMarkets) {
         if (risk.approved && confidence > 0.7) {
           console.log(`[Ken] SIGNAL: ${direction} ${ticker} — edge ${(mcEdge * 100).toFixed(1)}%, conf ${(confidence * 100).toFixed(0)}%, MC:${mcResult.edgeConsistent ? 'consistent' : 'mixed'}${signal.aiEnhanced ? ', AI-enhanced' : ''}, size ${risk.sizeRecommendation}`);
         }
+
+        // Auto-trade across all 10 paper portfolios
+        if (risk.approved) {
+          autoPortfolioTrade({
+            ...signal, marketId: ticker, market_title: market.title,
+            num_articles: numArticles, signal_type: signals[0]?.category,
+            reasoning, expected_edge: mcEdge,
+          }).catch(e => console.error('[Ken/Portfolio] Error:', e.message));
+        }
       }
     }
 
@@ -4633,6 +5303,416 @@ async function autonomousScan() {
   }
 }
 
+// ══════════════════════════════════════════════
+// AUTO-PORTFOLIO TRADING ENGINE — 10 personalities, $20/day each
+// ══════════════════════════════════════════════
+
+// Portfolio strategy filters — each personality decides differently
+const PORTFOLIO_STRATEGIES = {
+  aggressive: (sig) => Math.abs(sig.edge) >= 0.10,
+  conservative: (sig) => sig.confidence >= 0.60 && Math.abs(sig.edge) >= 0.05,
+  weather: (sig) => /weather|temp|rain|snow|wind|hurricane|precip|storm|°f|heat|cold|frost|drought|flood|tornado/i.test(sig.reasoning || ''),
+  momentum: (sig) => /momentum|rising|dropping|delta|volume.spike/i.test(sig.reasoning || sig.signal_type || ''),
+  contrarian: (sig) => sig.direction === 'BUY_NO' || /contrarian|fading/i.test(sig.reasoning || ''),
+  ai_enhanced: (sig) => sig.ai_enhanced === true,
+  mc_validated: (sig) => sig.mc_consistent === true,
+  scalper: (sig) => Math.abs(sig.edge) >= 0.03 && (sig.volume24h || 0) > 500,
+  patient: (sig) => sig.confidence >= 0.55 && sig.mc_consistent === true && (sig.num_articles || 0) >= 5,
+  random: (sig) => Math.random() < 0.33, // 1 in 3 chance
+};
+
+let tradeCounter = 0; // For wildcard portfolio
+
+async function autoPortfolioTrade(signal) {
+  try {
+    const { rows: portfolios } = await kenQ('SELECT * FROM ken_portfolios');
+    const today = new Date().toISOString().split('T')[0];
+
+    for (const pf of portfolios) {
+      const strategy = PORTFOLIO_STRATEGIES[pf.strategy];
+      if (!strategy) continue;
+
+      // Check daily budget: how much did this portfolio spend today?
+      const { rows: todaySpend } = await kenQ(`
+        SELECT COALESCE(SUM(cost_cents), 0) as spent
+        FROM ken_portfolio_trades
+        WHERE portfolio_id = $1 AND created_at::date = CURRENT_DATE AND status = 'open'
+      `, [pf.id]);
+      const spentToday = parseInt(todaySpend[0]?.spent) || 0;
+
+      if (spentToday >= pf.daily_budget_cents) continue; // Budget exhausted
+
+      // Check if strategy accepts this signal
+      const sigData = {
+        edge: signal.expectedEdge || signal.expected_edge || 0,
+        confidence: signal.confidence || 0,
+        direction: signal.direction,
+        reasoning: signal.reasoning || '',
+        signal_type: signal.signal_type || '',
+        ai_enhanced: signal.aiEnhanced || signal.ai_enhanced || false,
+        mc_consistent: signal.mcSimulation?.consistent || signal.mc_consistent || false,
+        volume24h: signal.volume24h || signal.volume_24h || 0,
+        num_articles: signal.num_articles || 0,
+      };
+
+      if (!strategy(sigData)) continue;
+
+      // Skip dupes: don't trade same market twice in same portfolio today
+      const { rows: dupes } = await kenQ(`
+        SELECT id FROM ken_portfolio_trades
+        WHERE portfolio_id = $1 AND market_id = $2 AND created_at::date = CURRENT_DATE
+      `, [pf.id, signal.marketId || signal.market_id]);
+      if (dupes.length > 0) continue;
+
+      // Calculate position size: use edge and confidence to scale (1-5 contracts at ~$0.50 each)
+      const absEdge = Math.abs(parseFloat(sigData.edge));
+      const conf = parseFloat(sigData.confidence);
+      const contracts = Math.max(1, Math.min(5, Math.round(absEdge * conf * 20)));
+      const entryPrice = sigData.direction === 'BUY_YES'
+        ? Math.round((0.5 - absEdge / 2) * 100) // rough market mid
+        : Math.round((0.5 + absEdge / 2) * 100);
+      const clampedPrice = Math.max(5, Math.min(95, entryPrice));
+      const cost = clampedPrice * contracts;
+
+      if (spentToday + cost > pf.daily_budget_cents) continue; // Would exceed budget
+
+      await kenQ(`
+        INSERT INTO ken_portfolio_trades (portfolio_id, signal_id, market_id, market_title, direction, entry_price_cents, contracts, cost_cents, reasoning, confidence, ai_enhanced, mc_consistent)
+        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
+      `, [pf.id, signal.id || null, signal.marketId || signal.market_id, signal.market_title || signal.marketTitle || '',
+          sigData.direction, clampedPrice, contracts, cost, (sigData.reasoning || '').substring(0, 500),
+          conf, sigData.ai_enhanced, sigData.mc_consistent]);
+
+      // Update portfolio stats
+      await kenQ(`UPDATE ken_portfolios SET total_invested_cents = total_invested_cents + $1, last_trade_at = NOW() WHERE id = $2`, [cost, pf.id]);
+
+      console.log(`[Ken/Portfolio] ${pf.name} (${pf.strategy}): ${sigData.direction} ${(signal.marketId || signal.market_id || '').substring(0, 30)} @ ${clampedPrice}¢ x${contracts} = $${(cost/100).toFixed(2)}`);
+    }
+  } catch (e) {
+    console.error('[Ken/Portfolio] Auto-trade error:', e.message);
+  }
+}
+
+// Resolve portfolio trades — check if markets settled
+async function resolvePortfolioTrades() {
+  try {
+    const { rows: openTrades } = await kenQ(`
+      SELECT pt.*, p.name as portfolio_name
+      FROM ken_portfolio_trades pt
+      JOIN ken_portfolios p ON p.id = pt.portfolio_id
+      WHERE pt.status = 'open' AND pt.created_at < NOW() - INTERVAL '4 hours'
+      LIMIT 100
+    `);
+
+    for (const trade of openTrades) {
+      // Get latest price from snapshots
+      const { rows: snaps } = await kenQ(`
+        SELECT last_price FROM ken_market_snapshots
+        WHERE ticker = $1 ORDER BY captured_at DESC LIMIT 1
+      `, [trade.market_id]);
+
+      if (snaps.length === 0) continue;
+      const currentPrice = parseInt(snaps[0].last_price);
+
+      let pnl = 0;
+      let status = 'expired';
+      if (trade.direction === 'BUY_YES') {
+        pnl = (currentPrice - trade.entry_price_cents) * trade.contracts;
+        status = pnl > 0 ? 'won' : pnl < 0 ? 'lost' : 'expired';
+      } else {
+        pnl = (trade.entry_price_cents - currentPrice) * trade.contracts;
+        status = pnl > 0 ? 'won' : pnl < 0 ? 'lost' : 'expired';
+      }
+
+      await kenQ(`
+        UPDATE ken_portfolio_trades SET status = $1, exit_price_cents = $2, pnl_cents = $3, resolved_at = NOW()
+        WHERE id = $4
+      `, [status, currentPrice, pnl, trade.id]);
+
+      // Update portfolio running totals
+      const updateCol = status === 'won' ? 'trades_won' : status === 'lost' ? 'trades_lost' : 'trades_expired';
+      const streakVal = status === 'won' ? 1 : status === 'lost' ? -1 : 0;
+      await kenQ(`
+        UPDATE ken_portfolios SET
+          balance_cents = balance_cents + $1,
+          total_returned_cents = total_returned_cents + GREATEST(0, $1 + $5),
+          ${updateCol} = ${updateCol} + 1,
+          streak = CASE WHEN $2 > 0 AND streak > 0 THEN streak + 1 WHEN $2 < 0 AND streak < 0 THEN streak - 1 ELSE $2 END,
+          best_trade_cents = GREATEST(best_trade_cents, $1),
+          worst_trade_cents = LEAST(worst_trade_cents, $1)
+        WHERE id = $3
+      `, [pnl, streakVal, trade.portfolio_id, updateCol, trade.cost_cents]);
+
+      // Update daily P&L
+      const tradeDate = new Date(trade.created_at).toISOString().split('T')[0];
+      await kenQ(`
+        INSERT INTO ken_daily_pnl (portfolio_id, date, closed_trades, wins, losses, daily_pnl_cents, cumulative_pnl_cents)
+        VALUES ($1, $2, 1, $3, $4, $5, $5)
+        ON CONFLICT (portfolio_id, date) DO UPDATE SET
+          closed_trades = ken_daily_pnl.closed_trades + 1,
+          wins = ken_daily_pnl.wins + $3,
+          losses = ken_daily_pnl.losses + $4,
+          daily_pnl_cents = ken_daily_pnl.daily_pnl_cents + $5
+      `, [trade.portfolio_id, tradeDate, status === 'won' ? 1 : 0, status === 'lost' ? 1 : 0, pnl]);
+
+      // Update learning table
+      const category = extractSignalCategory(trade.reasoning || '');
+      await kenQ(`
+        INSERT INTO ken_learning (category, signal_type, win_rate, avg_pnl_cents, total_trades, last_updated)
+        VALUES ($1, $2, $3, $4, 1, NOW())
+        ON CONFLICT DO NOTHING
+      `, [category, trade.direction, status === 'won' ? 1 : 0, pnl]);
+    }
+
+    if (openTrades.length > 0) console.log(`[Ken/Portfolio] Resolved ${openTrades.length} trades across portfolios`);
+  } catch (e) {
+    console.error('[Ken/Portfolio] Resolution error:', e.message);
+  }
+}
+
+function extractSignalCategory(reasoning) {
+  const r = reasoning.toLowerCase();
+  if (/heating|gas bill|utility bill|electric bill|energy cost|propane|furnace|hvac/.test(r)) return 'heating_energy';
+  if (/weather|temp|rain|snow|wind|hurricane|polar vortex|cold snap|freeze|frost/.test(r)) return 'weather';
+  if (/crypto|bitcoin|btc|eth/.test(r)) return 'crypto';
+  if (/politic|elect|congress|senate|trump|biden/.test(r)) return 'politics';
+  if (/econom|rate|inflation|fed|gdp|job/.test(r)) return 'economics';
+  if (/momentum|volume|spike|spread/.test(r)) return 'momentum';
+  if (/sport|game|nfl|nba|mlb/.test(r)) return 'sports';
+  if (/lng|natural gas|oil|commodity|corn|wheat|soy|cattle|crop/.test(r)) return 'commodities';
+  return 'general';
+}
+
+// ══════════════════════════════════════════════
+// LNG & AGRICULTURAL COMMODITY INTELLIGENCE
+// ══════════════════════════════════════════════
+const COMMODITY_FEEDS = [
+  // ── LNG & Natural Gas (Government) ──
+  { url: 'https://www.eia.gov/rss/naturalgas_rss.xml', source: 'EIA Natural Gas', category: 'lng' },
+  { url: 'https://www.eia.gov/rss/petroleum_rss.xml', source: 'EIA Petroleum', category: 'oil' },
+  { url: 'https://www.eia.gov/rss/todayinenergy_rss.xml', source: 'EIA Today in Energy', category: 'energy' },
+  { url: 'https://www.eia.gov/rss/electricity_rss.xml', source: 'EIA Electricity', category: 'electricity' },
+  // ── Agriculture (Government) ──
+  { url: 'https://www.usda.gov/rss/home.xml', source: 'USDA', category: 'agriculture' },
+  { url: 'https://www.nass.usda.gov/rss/index.php', source: 'USDA NASS', category: 'agriculture' },
+  // ── NOAA Weather (Government) ──
+  { url: 'https://alerts.weather.gov/cap/us.php?x=0', source: 'NWS National Alerts', category: 'weather' },
+  { url: 'https://www.weather.gov/rss_page.php?site_name=nws', source: 'NWS News', category: 'weather' },
+  { url: 'https://www.cpc.ncep.noaa.gov/products/stratosphere/strat-trop/rss.xml', source: 'NOAA CPC', category: 'weather' },
+  // ── FERC & Energy Regulation (Government) ──
+  { url: 'https://www.ferc.gov/rss/news-releases.xml', source: 'FERC News', category: 'energy_regulation' },
+  // ── Weather Risk ──
+  { url: 'https://rfrx.rfrz.io/weatherrisk', source: 'Weather Risk', category: 'weather_commodity' },
+];
+
+// Government Open APIs for Energy & Weather Data
+const GOV_API_ENDPOINTS = {
+  // EIA Open Data API (no key needed for RSS, key for JSON)
+  eia_ng_spot: 'https://api.eia.gov/v2/natural-gas/pri/fut/data/?frequency=daily&data[0]=value&sort[0][column]=period&sort[0][direction]=desc&length=5&api_key=',
+  eia_elec_price: 'https://api.eia.gov/v2/electricity/retail-sales/data/?frequency=monthly&data[0]=price&sort[0][column]=period&sort[0][direction]=desc&length=10&api_key=',
+  // NOAA Weather API (free, no key)
+  noaa_alerts: 'https://api.weather.gov/alerts/active?status=actual&message_type=alert',
+  noaa_forecast_nyc: 'https://api.weather.gov/gridpoints/OKX/33,37/forecast',
+  noaa_forecast_chi: 'https://api.weather.gov/gridpoints/LOT/75,72/forecast',
+  noaa_forecast_dal: 'https://api.weather.gov/gridpoints/FWD/87,108/forecast',
+  noaa_forecast_den: 'https://api.weather.gov/gridpoints/BOU/62,60/forecast',
+  noaa_forecast_msp: 'https://api.weather.gov/gridpoints/MPX/107,71/forecast',
+  // FRED Economic Data (key needed but some RSS free)
+  fred_natgas: 'https://fred.stlouisfed.org/graph/fredgraph.csv?id=DHHNGSP',
+};
+
+// Fetch NOAA weather alerts for extreme cold/heat events
+async function fetchNOAAWeatherIntel() {
+  let found = 0;
+  try {
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10000);
+    const res = await fetch(GOV_API_ENDPOINTS.noaa_alerts, {
+      signal: controller.signal,
+      headers: { 'User-Agent': 'KenInc/1.0 Weather Intelligence', Accept: 'application/geo+json' },
+    });
+    clearTimeout(timeout);
+    if (!res.ok) return found;
+    const data = await res.json();
+
+    const heatingKeywords = /cold|freeze|frost|ice|blizzard|winter storm|wind chill|extreme cold|heating|arctic|polar vortex|below zero|record low|dangerous cold/i;
+    const energyKeywords = /power outage|grid|electricity|utility|energy emergency|rolling blackout|brownout|load shed/i;
+
+    for (const feature of (data.features || []).slice(0, 50)) {
+      const props = feature.properties || {};
+      const headline = props.headline || props.event || '';
+      const desc = props.description || '';
+      const combined = headline + ' ' + desc;
+
+      if (heatingKeywords.test(combined) || energyKeywords.test(combined)) {
+        const areas = (props.areaDesc || '').substring(0, 200);
+        const severity = props.severity || 'Unknown';
+        const certainty = props.certainty || 'Unknown';
+
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('noaa_alert', $1, 'weather_energy', $2, $3, $4, 0, $5, $6)
+          ON CONFLICT DO NOTHING
+        `, [
+          areas,
+          headline.substring(0, 500),
+          props.id || `noaa-${Date.now()}`,
+          severity === 'Extreme' ? 100 : severity === 'Severe' ? 80 : severity === 'Moderate' ? 50 : 20,
+          severity === 'Extreme' ? -0.9 : severity === 'Severe' ? -0.7 : -0.4,
+          `[${severity}/${certainty}] ${areas}: ${desc.substring(0, 400)}`
+        ]);
+        found++;
+      }
+    }
+  } catch (e) {
+    // NOAA API can be slow, don't log every failure
+  }
+
+  // Fetch regional forecasts for major metro areas (heating demand proxy)
+  const metros = [
+    { name: 'NYC', url: GOV_API_ENDPOINTS.noaa_forecast_nyc },
+    { name: 'Chicago', url: GOV_API_ENDPOINTS.noaa_forecast_chi },
+    { name: 'Dallas', url: GOV_API_ENDPOINTS.noaa_forecast_dal },
+    { name: 'Denver', url: GOV_API_ENDPOINTS.noaa_forecast_den },
+    { name: 'Minneapolis', url: GOV_API_ENDPOINTS.noaa_forecast_msp },
+  ];
+
+  for (const metro of metros) {
+    try {
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 8000);
+      const res = await fetch(metro.url, {
+        signal: controller.signal,
+        headers: { 'User-Agent': 'KenInc/1.0', Accept: 'application/geo+json' },
+      });
+      clearTimeout(timeout);
+      if (!res.ok) continue;
+      const data = await res.json();
+
+      for (const period of (data.properties?.periods || []).slice(0, 4)) {
+        const tempF = period.temperature;
+        const wind = period.windSpeed || '';
+        const forecast = period.detailedForecast || period.shortForecast || '';
+
+        // Flag extreme cold (below 20F) or heat waves (above 100F) — heating demand signals
+        if (tempF <= 20 || tempF >= 100) {
+          await kenQ(`
+            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+            VALUES ('noaa_forecast', $1, 'extreme_temp', $2, $3, $4, 0, $5, $6)
+            ON CONFLICT DO NOTHING
+          `, [
+            metro.name,
+            `${metro.name}: ${tempF}°F ${period.name} — ${period.shortForecast}`,
+            `noaa-forecast-${metro.name}-${period.number}-${new Date().toISOString().split('T')[0]}`,
+            Math.abs(tempF <= 20 ? (20 - tempF) * 5 : (tempF - 100) * 5),
+            tempF <= 20 ? -0.8 : -0.6,
+            `${metro.name} ${period.name}: ${tempF}°F, Wind ${wind}. ${forecast.substring(0, 300)}`
+          ]);
+          found++;
+        }
+      }
+      await new Promise(r => setTimeout(r, 500));
+    } catch (e) { /* NOAA timeout */ }
+  }
+
+  if (found > 0) console.log(`[Ken/NOAA] ${found} weather/energy alerts from government APIs`);
+  return found;
+}
+
+// Additional weather-impacted commodity keywords for market scanning
+const COMMODITY_KEYWORDS = [
+  'natural gas', 'lng', 'henry hub', 'gas price', 'heating degree',
+  'corn', 'wheat', 'soybeans', 'soybean', 'cotton', 'cattle', 'hogs',
+  'crop', 'harvest', 'drought', 'frost', 'freeze', 'growing season',
+  'el nino', 'la nina', 'commodity', 'futures', 'agriculture',
+  'ethanol', 'fertilizer', 'grain', 'livestock', 'usda', 'crop report',
+];
+
+// ── HEATING BILL & ENERGY COST INTELLIGENCE ──
+const HEATING_KEYWORDS = [
+  'heating bill', 'gas bill', 'electric bill', 'utility bill', 'energy bill',
+  'heating cost', 'energy cost', 'propane', 'furnace', 'thermostat',
+  'polar vortex', 'cold snap', 'wind chill', 'below zero', 'record cold',
+  'heating oil', 'natural gas price', 'electricity price', 'rate hike',
+  'winter storm', 'power outage', 'grid strain', 'rolling blackout',
+  'hvac', 'insulation', 'heat pump', 'space heater', 'energy assistance',
+];
+
+const HEATING_SUBS = [
+  'personalfinance', 'Frugal', 'povertyfinance', 'homeowners',
+  'HVAC', 'energy', 'NaturalGas', 'HomeImprovement', 'utilities',
+  'weather', 'climate', 'TropicalWeather', 'preppers',
+];
+
+async function fetchHeatingBillIntel() {
+  let found = 0;
+  const heatingRegex = new RegExp(HEATING_KEYWORDS.join('|'), 'i');
+
+  for (let i = 0; i < HEATING_SUBS.length; i += 3) {
+    const batch = HEATING_SUBS.slice(i, i + 3);
+    const results = await Promise.allSettled(batch.map(sub => scrapeSubredditHot(sub, 15)));
+    for (let j = 0; j < results.length; j++) {
+      if (results[j].status !== 'fulfilled') continue;
+      const sub = batch[j];
+      for (const post of results[j].value) {
+        const text = (post.title + ' ' + (post.selftext || '')).toLowerCase();
+        if (heatingRegex.test(text) && post.score > 10) {
+          const sentiment = scoreSentiment(post.title);
+          await kenQ(`
+            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+            VALUES ('heating_intel', $1, 'heating', $2, $3, $4, $5, $6, $7)
+            ON CONFLICT DO NOTHING
+          `, [sub, post.title, post.url, post.score, post.num_comments, sentiment, text.substring(0, 500)]);
+          found++;
+        }
+      }
+    }
+    await new Promise(r => setTimeout(r, 2000));
+  }
+
+  if (found > 0) console.log(`[Ken/Heating] Found ${found} heating bill/energy cost discussions`);
+  return found;
+}
+
+async function fetchCommodityIntel() {
+  let total = 0;
+  for (const feed of COMMODITY_FEEDS) {
+    try {
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 8000);
+      const res = await fetch(feed.url, {
+        signal: controller.signal,
+        headers: { 'User-Agent': 'KenInc/1.0 Commodity Intelligence' },
+      });
+      clearTimeout(timeout);
+      if (!res.ok) continue;
+      const text = await res.text();
+
+      // Extract items from RSS
+      const items = text.match(/<item>[\s\S]*?<\/item>/gi) || [];
+      for (const item of items.slice(0, 15)) {
+        const title = (item.match(/<title><!\[CDATA\[(.*?)\]\]>|<title>(.*?)<\/title>/)?.[1] || item.match(/<title>(.*?)<\/title>/)?.[1] || '').trim();
+        const link = (item.match(/<link>(.*?)<\/link>/)?.[1] || '').trim();
+        if (!title) continue;
+
+        // Score commodity relevance
+        const score = scoreEventSentiment(title);
+
+        try {
+          await kenQ(`
+            INSERT INTO ken_sentiment (source, keyword, post_title, post_url, sentiment_score, captured_at)
+            VALUES ($1, $2, $3, $4, $5, NOW())
+            ON CONFLICT DO NOTHING
+          `, [feed.source, feed.category, title.substring(0, 500), link.substring(0, 500), score.sentiment || 0]);
+          total++;
+        } catch(e) {} // skip dupes
+      }
+    } catch(e) {}
+  }
+  if (total > 0) console.log(`[Ken/Commodities] ${total} articles from ${COMMODITY_FEEDS.length} commodity feeds`);
+  return total;
+}
+
 // ══════════════════════════════════════════════
 // PERFORMANCE TRACKER — resolve old predictions
 // ══════════════════════════════════════════════
@@ -4702,7 +5782,8 @@ server.listen(PORT, '0.0.0.0', () => {
   console.log(`[Ken] CPU cores: ${os.cpus().length} | MC iterations: 15,000 per signal`);
   console.log(`[Ken] Gemini AI: ${GEMINI_MODEL} (daily budget: ${GEMINI_DAILY_LIMIT} calls)`);
   console.log(`[Ken] Slack alerts: every 4 hours (high-conf bypass)`);
-  console.log(`[Ken] Data sources: Kalshi | Reddit (70+) | ${NEWS_FEEDS.length} news feeds | HN | GDELT TV | YouTube | Manifold | r/Kalshi | r/Polymarket | Lemmy | Lobsters | Tildes | Bluesky | Mastodon | Metaculus | Polymarket | PredictIt | FRED Economic | NWS Alerts | GovTrack | Twitter/Nitter | Discord`);
+  console.log(`[Ken] Data sources: Kalshi | Reddit (80+) | ${NEWS_FEEDS.length} news feeds | HN | GDELT TV | YouTube | Manifold | r/Kalshi | r/Polymarket | Lemmy | Lobsters | Tildes | Bluesky | Mastodon | Metaculus | Polymarket | PredictIt | FRED Economic | NWS Alerts | NOAA Forecasts | EIA Energy | FERC | GovTrack | USDA | Twitter/Nitter | Discord`);
+  console.log(`[Ken] Heating intel: ${HEATING_SUBS.length} subreddits | ${HEATING_KEYWORDS.length} keywords | NOAA 5 metro forecasts | EIA electricity/gas prices`);
   console.log(`[Ken] ═══════════════════════════════════════`);
 
   // Mutex: prevent concurrent sweeps from overlapping and doubling memory
@@ -4745,11 +5826,25 @@ server.listen(PORT, '0.0.0.0', () => {
     }
   }, SIGNAL_PIPELINE_MS);
 
-  // Resolve old predictions every hour
+  // Resolve old predictions + portfolio trades every hour
   setInterval(() => {
     resolveOldPredictions().catch(e => console.error('[Ken] Resolution error:', e.message));
+    resolvePortfolioTrades().catch(e => console.error('[Ken] Portfolio resolution error:', e.message));
   }, 60 * 60 * 1000);
 
+  // Commodity + heating bill + NOAA intelligence every 20 minutes
+  setInterval(() => {
+    fetchCommodityIntel().catch(e => console.error('[Ken] Commodity error:', e.message));
+    fetchHeatingBillIntel().catch(e => console.error('[Ken] Heating intel error:', e.message));
+    fetchNOAAWeatherIntel().catch(e => console.error('[Ken] NOAA intel error:', e.message));
+  }, 20 * 60 * 1000);
+  // Initial commodity + heating + NOAA fetch
+  setTimeout(() => {
+    fetchCommodityIntel().catch(() => {});
+    fetchHeatingBillIntel().catch(() => {});
+    fetchNOAAWeatherIntel().catch(() => {});
+  }, 15000);
+
   // Cleanup old data every 24 hours
   setInterval(() => {
     cleanupOldData().catch(e => console.error('[Ken] Cleanup error:', e.message));

← aa3548c chore(alshi-dash): update 1 file (.js) [+249/-22]  ·  back to Bertha  ·  chore(alshi-dash): update 1 file (.js) [+361/-4] 1d9f769 →