[object Object]

← back to Bertha

security: strip hardcoded dw_admin DSN password -> env-first/passwordless. No rotation/deploy.

2efe34d7d9c1578629a2dd670c0be029b3673ba4 · 2026-05-30 09:49:20 -0700 · Steve

Files touched

Diff

commit 2efe34d7d9c1578629a2dd670c0be029b3673ba4
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat May 30 09:49:20 2026 -0700

    security: strip hardcoded dw_admin DSN password -> env-first/passwordless. No rotation/deploy.
---
 kalshi-dash/server.js                          |    4 +-
 kalshi-dash/server.js.bak-20260422-160936      | 9128 -----------------------
 kalshi-dash/server.js.pre-scrub-2026-05-07.bak | 9138 ------------------------
 react-dash/server.js                           |    2 +-
 src/lib/db.js                                  |    2 +-
 5 files changed, 4 insertions(+), 18270 deletions(-)

diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index 5189c25..27348eb 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -360,7 +360,7 @@ async function refreshKalshiEnv() {
 
 // ── Database ──
 const pool = new pg.Pool({
-  connectionString: 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/bertha_betting',
+  connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/bertha_betting'),
   max: 10,
   idleTimeoutMillis: 30000,
 });
@@ -4889,7 +4889,7 @@ const server = http.createServer(async (req, res) => {
 // Stores everything in PostgreSQL, tracks own performance
 // ══════════════════════════════════════════════════════════════
 
-const KEN_DB = 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
+const KEN_DB = (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified');
 const kenPool = new pg.Pool({ connectionString: KEN_DB, max: 20, idleTimeoutMillis: 30000 });
 async function kenQ(text, params) { return kenPool.query(text, params); }
 
diff --git a/kalshi-dash/server.js.bak-20260422-160936 b/kalshi-dash/server.js.bak-20260422-160936
deleted file mode 100644
index 88fc2dd..0000000
--- a/kalshi-dash/server.js.bak-20260422-160936
+++ /dev/null
@@ -1,9128 +0,0 @@
-import http from 'http';
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import pg from 'pg';
-import crypto from 'crypto';
-import { OAuth2Client } from 'google-auth-library';
-import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
-import os from 'os';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-const PORT = 7810;
-const DIST = path.join(__dirname, 'dist');
-
-// ── Unified DW Auth Config ──
-const AUTH_USER = 'admin';
-const AUTH_PASS = 'DWSecure2024!';
-const SESSION_COOKIE = 'kalshi_session';
-const SESSION_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
-const WHITELISTED_IPS = ['127.0.0.1', '::1', '45.61.58.125', 'localhost'];
-
-// ── Slack Alerts ──
-const SLACK_WEBHOOK = 'https://hooks.slack.com/services/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';
-
-async function sendSlack(text) {
-  // Slack notifications disabled per Steve
-  return;
-}
-
-// ── Google OAuth Config ──
-const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
-const ALLOWED_EMAILS = (process.env.ALLOWED_EMAILS || '').split(',').filter(Boolean);
-const googleClient = GOOGLE_CLIENT_ID ? new OAuth2Client(GOOGLE_CLIENT_ID) : null;
-
-// ── NWS Weather Config ──
-const NWS_BASE = 'https://api.weather.gov';
-const NWS_USER_AGENT = 'Ken-Bot/1.0 (steve@designerwallcoverings.com)';
-
-const CITY_COORDS = {
-  'New York': { lat: 40.7128, lon: -74.0060 },
-  'Los Angeles': { lat: 34.0522, lon: -118.2437 },
-  'Chicago': { lat: 41.8781, lon: -87.6298 },
-  'Houston': { lat: 29.7604, lon: -95.3698 },
-  'Phoenix': { lat: 33.4484, lon: -112.0740 },
-  'Philadelphia': { lat: 39.9526, lon: -75.1652 },
-  'Dallas': { lat: 32.7767, lon: -96.7970 },
-  'San Francisco': { lat: 37.7749, lon: -122.4194 },
-  'Seattle': { lat: 47.6062, lon: -122.3321 },
-  'Denver': { lat: 39.7392, lon: -104.9903 },
-  'Boston': { lat: 42.3601, lon: -71.0589 },
-  'Miami': { lat: 25.7617, lon: -80.1918 },
-  'Atlanta': { lat: 33.7490, lon: -84.3880 },
-  'Minneapolis': { lat: 44.9778, lon: -93.2650 },
-  'Washington': { lat: 38.9072, lon: -77.0369 },
-  'Las Vegas': { lat: 36.1699, lon: -115.1398 },
-};
-
-async function nwsFetch(url) {
-  const controller = new AbortController();
-  // Hard Promise.race timeout — AbortController alone can't kill DNS-level hangs
-  const hardTimeout = new Promise((_, reject) =>
-    setTimeout(() => { controller.abort(); reject(new Error('NWS timeout (5s)')); }, 5000)
-  );
-  const doFetch = async () => {
-    const res = await fetch(url, {
-      headers: { 'User-Agent': NWS_USER_AGENT, 'Accept': 'application/geo+json' },
-      signal: controller.signal,
-    });
-    if (!res.ok) throw new Error(`NWS ${res.status}: ${res.statusText}`);
-    return res.json();
-  };
-  return Promise.race([doFetch(), hardTimeout]);
-}
-
-function extractForecastFeatures(forecastData, variable) {
-  const periods = forecastData?.properties?.periods || [];
-  if (!periods.length) return null;
-  const features = { variable, values: [], timestamps: [] };
-  for (const p of periods) {
-    let value = null;
-    if (variable === 'temperature') { value = p.temperature; features.unit = p.temperatureUnit; }
-    else if (variable === 'wind') { const m = p.windSpeed?.match(/(\d+)/); if (m) value = parseInt(m[1]); features.unit = 'mph'; }
-    else if (variable === 'precipitation') { value = p.probabilityOfPrecipitation?.value ?? 0; features.unit = 'percent'; }
-    if (value !== null) { features.values.push(value); features.timestamps.push(p.startTime); }
-  }
-  if (features.values.length) {
-    features.mean = features.values.reduce((a, b) => a + b, 0) / features.values.length;
-    features.min = Math.min(...features.values);
-    features.max = Math.max(...features.values);
-    features.latest = features.values[0];
-  }
-  return features;
-}
-
-// ── Prediction Model ──
-function ensembleExceedance(features, threshold, operator = '>=') {
-  const vals = features?.values || [];
-  if (!vals.length) return null;
-  let ct = 0;
-  for (const v of vals) {
-    if (operator === '>=' && v >= threshold) ct++;
-    else if (operator === '<=' && v <= threshold) ct++;
-    else if (operator === '>' && v > threshold) ct++;
-    else if (operator === '<' && v < threshold) ct++;
-  }
-  return ct / vals.length;
-}
-
-function climatologicalPrior(variable, threshold, month) {
-  const season = month >= 6 && month <= 8 ? 'summer' : month >= 12 || month <= 2 ? 'winter' : month >= 3 && month <= 5 ? 'spring' : 'fall';
-  const priors = {
-    temperature: { summer: t => t > 100 ? 0.05 : t > 90 ? 0.35 : t > 80 ? 0.65 : 0.85, winter: t => t > 60 ? 0.15 : t > 40 ? 0.50 : t > 20 ? 0.75 : 0.90, spring: t => t > 80 ? 0.20 : t > 60 ? 0.55 : t > 40 ? 0.80 : 0.95, fall: t => t > 80 ? 0.25 : t > 60 ? 0.50 : t > 40 ? 0.75 : 0.90 },
-    precipitation: { any: t => t > 2 ? 0.10 : t > 1 ? 0.20 : t > 0.5 ? 0.35 : 0.50 },
-    snow: { winter: t => t > 12 ? 0.05 : t > 6 ? 0.15 : t > 3 ? 0.25 : 0.40, any: () => 0.10 },
-  };
-  const vp = priors[variable]; if (!vp) return 0.50;
-  const fn = vp[season] || vp.any; if (!fn) return 0.50;
-  return fn(threshold);
-}
-
-function bayesianUpdate(prior, forecastProb, weight = 0.7) {
-  return prior * (1 - weight) + forecastProb * weight;
-}
-
-// ══════════════════════════════════════════════════════
-// GEMINI AI PREDICTION ENGINE — uses CPU + AI for better signals
-// ══════════════════════════════════════════════════════
-const GEMINI_API_KEY = 'AIzaSyAO0rLKwtJUKcf3zVmKstBS4udct4QejMo';
-const GEMINI_MODEL = 'gemini-2.0-flash';
-
-async function geminiAnalyze(prompt, maxTokens = 1024) {
-  try {
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 25000);
-    const res = await fetch(
-      `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`,
-      {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        signal: controller.signal,
-        body: JSON.stringify({
-          contents: [{ parts: [{ text: prompt }] }],
-          generationConfig: { temperature: 0.3, maxOutputTokens: maxTokens },
-        }),
-      }
-    );
-    clearTimeout(timeout);
-    if (!res.ok) return null;
-    const data = await res.json();
-    return data?.candidates?.[0]?.content?.parts?.[0]?.text || null;
-  } catch (e) {
-    console.log(`[Ken/Gemini] Error: ${e.message}`);
-    return null;
-  }
-}
-
-// AI-Enhanced Signal Scoring — Gemini analyzes top signals for smarter predictions
-let geminiCallCount = 0;
-const GEMINI_DAILY_LIMIT = 200; // budget: ~200 calls/day
-const geminiCallReset = setInterval(() => { geminiCallCount = 0; }, 24 * 60 * 60 * 1000);
-
-async function aiEnhanceSignal(signal, articles, market) {
-  if (geminiCallCount >= GEMINI_DAILY_LIMIT) return signal;
-  geminiCallCount++;
-
-  const articleSummary = articles.slice(0, 8).map((a, i) =>
-    `${i + 1}. [${a.source}] ${a.headline?.substring(0, 150)}`
-  ).join('\n');
-
-  const prompt = `You are Ken, an expert prediction market analyst. Analyze this trading signal and provide a calibrated probability estimate.
-
-MARKET: "${market.title}" ${market.subtitle ? '- ' + market.subtitle : ''}
-CURRENT PRICE: YES at ${market.yes_ask}¢, NO at ${100 - market.yes_ask}¢
-24H VOLUME: ${market.volume_24h?.toLocaleString() || '?'}
-OUR SIGNAL: ${signal.direction} with ${(Math.abs(signal.expectedEdge) * 100).toFixed(1)}% edge
-
-RECENT NEWS (${articles.length} articles):
-${articleSummary}
-
-SENTIMENT ANALYSIS: category=${articles[0]?.category || '?'}, direction=${articles[0]?.direction || '?'}, agreement=${((signal.agreement || 0) * 100).toFixed(0)}%
-
-Respond with ONLY a JSON object (no markdown):
-{"probability": 0.XX, "confidence": 0.XX, "reasoning": "one sentence", "adjustedEdge": X.XX, "riskFlag": "none|caution|high_risk"}`;
-
-  const response = await geminiAnalyze(prompt, 256);
-  if (!response) return signal;
-
-  try {
-    // Parse JSON from response (handle markdown wrapping)
-    const jsonStr = response.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
-    const ai = JSON.parse(jsonStr);
-
-    // Blend AI probability with our statistical model
-    const blendedEdge = signal.expectedEdge * 0.6 + (ai.adjustedEdge || signal.expectedEdge) * 0.4;
-    const blendedConf = signal.confidence * 0.5 + (ai.confidence || signal.confidence) * 0.5;
-
-    return {
-      ...signal,
-      expectedEdge: blendedEdge,
-      confidence: Math.min(0.95, blendedConf),
-      aiProbability: ai.probability,
-      aiReasoning: ai.reasoning,
-      aiRiskFlag: ai.riskFlag || 'none',
-      aiEnhanced: true,
-    };
-  } catch (e) {
-    console.log(`[Ken/Gemini] Parse error: ${e.message}`);
-    return signal;
-  }
-}
-
-// ══════════════════════════════════════════════════════
-// MONTE CARLO SIMULATION — CPU-intensive probability estimation
-// ══════════════════════════════════════════════════════
-function monteCarloSimulation(params, iterations = 10000) {
-  const { priorProb, sentimentImpact, momentum2h, momentum6h, volume24h, spread, articleCount, agreement } = params;
-
-  let successCount = 0;
-  const edgeDistribution = [];
-
-  for (let i = 0; i < iterations; i++) {
-    // Add noise to each factor (simulating uncertainty)
-    const sentimentNoise = sentimentImpact + (Math.random() - 0.5) * 0.15;
-    const momentumNoise = (momentum2h || 0) / 100 + (Math.random() - 0.5) * 0.08;
-    const volumeSignal = Math.min(1, (volume24h || 0) / 10000) * (Math.random() * 0.1);
-    const spreadPenalty = Math.max(0, (spread || 0) - 3) * -0.005 * Math.random();
-    const articleBoost = Math.min(0.1, (articleCount || 0) * 0.01) * Math.random();
-    const agreementBoost = ((agreement || 0.5) - 0.5) * 0.15 * Math.random();
-
-    // Simulate probability with all factors
-    const simProb = Math.max(0.01, Math.min(0.99,
-      priorProb + sentimentNoise + momentumNoise + volumeSignal + spreadPenalty + articleBoost + agreementBoost
-    ));
-
-    // Compare to market price — is there an edge?
-    const marketProb = priorProb;
-    const simEdge = simProb - marketProb;
-
-    if (Math.abs(simEdge) >= 0.05) successCount++;
-    edgeDistribution.push(simEdge);
-  }
-
-  // Calculate statistics from simulation
-  edgeDistribution.sort((a, b) => a - b);
-  const mean = edgeDistribution.reduce((a, b) => a + b, 0) / iterations;
-  const p5 = edgeDistribution[Math.floor(iterations * 0.05)];
-  const p25 = edgeDistribution[Math.floor(iterations * 0.25)];
-  const p50 = edgeDistribution[Math.floor(iterations * 0.50)];
-  const p75 = edgeDistribution[Math.floor(iterations * 0.75)];
-  const p95 = edgeDistribution[Math.floor(iterations * 0.95)];
-  const stdDev = Math.sqrt(edgeDistribution.reduce((sum, e) => sum + Math.pow(e - mean, 2), 0) / iterations);
-
-  return {
-    meanEdge: mean,
-    medianEdge: p50,
-    stdDev,
-    confidence: successCount / iterations,
-    percentiles: { p5, p25, p50, p75, p95 },
-    iterations,
-    edgeConsistent: Math.sign(p25) === Math.sign(p75), // Is edge direction consistent?
-  };
-}
-
-// ══════════════════════════════════════════════════════
-// PARALLEL PROCESSING — run CPU-heavy tasks across cores
-// ══════════════════════════════════════════════════════
-const CPU_CORES = Math.max(2, os.cpus().length);
-const WORKER_POOL_SIZE = Math.min(12, CPU_CORES - 2); // Use up to 14 cores, leave 2 for OS + other services
-
-function runInParallel(tasks, workerFn) {
-  return Promise.all(tasks.map(task => {
-    return new Promise(resolve => {
-      try {
-        resolve(workerFn(task));
-      } catch (e) {
-        resolve({ error: e.message, task });
-      }
-    });
-  }));
-}
-
-// Batch process markets in parallel chunks
-async function parallelMarketAnalysis(markets, analysisFunc, batchSize = 50) {
-  const results = [];
-  const batches = [];
-  for (let i = 0; i < markets.length; i += batchSize) {
-    batches.push(markets.slice(i, i + batchSize));
-  }
-
-  // Process batches concurrently (up to WORKER_POOL_SIZE at once)
-  for (let i = 0; i < batches.length; i += WORKER_POOL_SIZE) {
-    const concurrent = batches.slice(i, i + WORKER_POOL_SIZE);
-    const batchResults = await Promise.all(concurrent.map(batch =>
-      Promise.all(batch.map(m => analysisFunc(m).catch(e => ({ error: e.message, ticker: m.ticker }))))
-    ));
-    results.push(...batchResults.flat());
-  }
-  return results;
-}
-
-// Parse Kalshi weather market title into event spec
-function parseWeatherEvent(title) {
-  const t = (title || '').toLowerCase();
-  let variable = 'unknown', threshold = 80, operator = '>=';
-  const tempMatch = t.match(/(\d+)\s*°?\s*f/);
-  // Detect temperature events
-  if (t.includes('temperature') || t.includes('temp') || t.includes('°f') || t.includes('degrees') || t.includes('high') || t.includes('low') || t.includes('cold') || t.includes('hot') || t.includes('warm') || t.includes('heat')) {
-    variable = 'temperature';
-    if (tempMatch) threshold = parseInt(tempMatch[1]);
-  }
-  if (t.includes('rain') || t.includes('precipitation') || t.includes('inch')) { variable = 'precipitation'; threshold = tempMatch ? parseInt(tempMatch[1]) : 0.1; }
-  if (t.includes('snow')) { variable = 'snow'; threshold = tempMatch ? parseInt(tempMatch[1]) : 1; }
-  if (t.includes('wind') || t.includes('gust')) { variable = 'wind'; threshold = tempMatch ? parseInt(tempMatch[1]) : 40; }
-  if (t.includes('below') || t.includes('under') || t.includes('less')) operator = '<=';
-  if (t.includes('above') || t.includes('over') || t.includes('exceed') || t.includes('at least')) operator = '>=';
-  return { variable, threshold, operator };
-}
-
-// Extract city from market title
-function extractCity(title) {
-  for (const city of Object.keys(CITY_COORDS)) {
-    if (title.toLowerCase().includes(city.toLowerCase())) return city;
-  }
-  return null;
-}
-
-// ── Kalshi API Config ──
-let KALSHI_ENV = (process.env.KALSHI_ENV || 'demo').toLowerCase();
-const KALSHI_BASE_URLS = {
-  demo: 'https://demo-api.kalshi.co/trade-api/v2',
-  prod: 'https://api.elections.kalshi.com/trade-api/v2',
-};
-let KALSHI_BASE_URL = KALSHI_BASE_URLS[KALSHI_ENV] || KALSHI_BASE_URLS.demo;
-
-// Refresh env from DB config (called on startup and mode changes)
-async function refreshKalshiEnv() {
-  try {
-    const result = await pool.query('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-    const cfg = result.rows[0]?.config || {};
-    if (cfg.kalshi_env && KALSHI_BASE_URLS[cfg.kalshi_env]) {
-      KALSHI_ENV = cfg.kalshi_env;
-      KALSHI_BASE_URL = KALSHI_BASE_URLS[KALSHI_ENV];
-    }
-  } catch {}
-}
-
-// ── Database ──
-const pool = new pg.Pool({
-  connectionString: 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/bertha_betting',
-  max: 10,
-  idleTimeoutMillis: 30000,
-});
-
-const MIME = {
-  '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
-  '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml',
-  '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff',
-  '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
-  '.webp': 'image/webp', '.map': 'application/json', '.txt': 'text/plain',
-};
-
-// ── DB Helper ──
-async function q(text, params) {
-  return pool.query(text, params);
-}
-
-// ── Response Helper ──
-function json(res, data, status = 200, extraHeaders = {}) {
-  res.writeHead(status, {
-    'Content-Type': 'application/json',
-    'Access-Control-Allow-Origin': '*',
-    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
-    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
-    ...extraHeaders,
-  });
-  res.end(JSON.stringify(data));
-}
-
-// ── Body Parser ──
-async function readBody(req) {
-  return new Promise(resolve => {
-    let d = '';
-    req.on('data', c => d += c);
-    req.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } });
-  });
-}
-
-// ── Cookie / Session Helpers ──
-function parseCookies(req) {
-  const obj = {};
-  (req.headers.cookie || '').split(';').forEach(c => {
-    const [k, ...v] = c.trim().split('=');
-    if (k) obj[k] = v.join('=');
-  });
-  return obj;
-}
-
-function getSession(req) {
-  const cookies = parseCookies(req);
-  const token = cookies[SESSION_COOKIE];
-  if (!token) return null;
-  try {
-    const data = JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString());
-    if (!data.authenticated) return null;
-    const age = Date.now() - data.loginTime;
-    if (age > SESSION_MAX_AGE * 1000) return null;
-    return data;
-  } catch { return null; }
-}
-
-function getClientIP(req) {
-  return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress || '';
-}
-
-function isWhitelistedIP(req) {
-  const ip = getClientIP(req);
-  return WHITELISTED_IPS.some(w => ip.includes(w));
-}
-
-function hasBasicAuth(req) {
-  const auth = req.headers.authorization;
-  if (!auth || !auth.startsWith('Basic ')) return false;
-  const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
-  return user === AUTH_USER && pass === AUTH_PASS;
-}
-
-function isAuthenticated(req) {
-  if (isWhitelistedIP(req)) return true;
-  if (hasBasicAuth(req)) return true;
-  if (getSession(req)) return true;
-  return false;
-}
-
-function setSessionCookie(res, sessionToken) {
-  return `${SESSION_COOKIE}=${encodeURIComponent(sessionToken)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE}`;
-}
-
-function clearSessionCookie() {
-  return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
-}
-
-// ── Kalshi API Signing ──
-// RSA-PSS with SHA-256, salt length 32
-// Signs: timestamp_ms_string + method_uppercase + path
-function signKalshiRequest(privateKeyPem, timestampMs, method, requestPath) {
-  const message = timestampMs + method.toUpperCase() + requestPath;
-  const signature = crypto.sign('sha256', Buffer.from(message), {
-    key: privateKeyPem,
-    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
-    saltLength: 32,
-  });
-  return signature.toString('base64');
-}
-
-// ── Kalshi Authenticated Fetch ──
-async function kalshiFetch(method, apiPath, body = null) {
-  // Load keys from DB
-  const result = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-  const cfg = result.rows[0]?.config || {};
-
-  const apiKey = cfg.kalshi_api_key;
-  const privateKeyPem = cfg.kalshi_private_key;
-
-  if (!apiKey || !privateKeyPem) {
-    throw new Error('Kalshi API keys not configured. Please save your API key and private key first.');
-  }
-
-  const timestampMs = Date.now().toString();
-  // The path for signing is everything after the base domain, starting with /trade-api/v2
-  const basePath = KALSHI_ENV === 'prod' ? '/trade-api/v2' : '/trade-api/v2';
-  const fullSignPath = basePath + apiPath;
-  const signature = signKalshiRequest(privateKeyPem, timestampMs, method, fullSignPath);
-
-  const url = KALSHI_BASE_URL + apiPath;
-  const headers = {
-    'KALSHI-ACCESS-KEY': apiKey,
-    'KALSHI-ACCESS-TIMESTAMP': timestampMs,
-    'KALSHI-ACCESS-SIGNATURE': signature,
-    'Content-Type': 'application/json',
-    'Accept': 'application/json',
-  };
-
-  const fetchOpts = { method, headers };
-  if (body && method !== 'GET' && method !== 'DELETE') {
-    fetchOpts.body = JSON.stringify(body);
-  }
-
-  const response = await fetch(url, fetchOpts);
-  const text = await response.text();
-
-  let data;
-  try {
-    data = JSON.parse(text);
-  } catch {
-    data = { raw: text };
-  }
-
-  if (!response.ok) {
-    const errMsg = data?.message || data?.error || data?.raw || `HTTP ${response.status}`;
-    throw new Error(`Kalshi API error (${response.status}): ${errMsg}`);
-  }
-
-  return data;
-}
-
-// ── Kalshi Unauthenticated Fetch (for public endpoints) ──
-async function kalshiPublicFetch(method, apiPath) {
-  const url = KALSHI_BASE_URL + apiPath;
-  const response = await fetch(url, {
-    method,
-    headers: { 'Accept': 'application/json' },
-  });
-  const text = await response.text();
-
-  let data;
-  try {
-    data = JSON.parse(text);
-  } catch {
-    data = { raw: text };
-  }
-
-  if (!response.ok) {
-    const errMsg = data?.message || data?.error || data?.raw || `HTTP ${response.status}`;
-    throw new Error(`Kalshi API error (${response.status}): ${errMsg}`);
-  }
-
-  return data;
-}
-
-// ── Build query string from params object ──
-function buildQueryString(params) {
-  if (!params || Object.keys(params).length === 0) return '';
-  const qs = Object.entries(params)
-    .filter(([, v]) => v !== undefined && v !== null && v !== '')
-    .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
-    .join('&');
-  return qs ? `?${qs}` : '';
-}
-
-// ── API Route Handlers ──
-const routes = {
-
-  // ════════════════════════════════════════
-  // AUTH ENDPOINTS (same DW pattern)
-  // ════════════════════════════════════════
-
-  'POST /api/auth/login': async (req, res) => {
-    const body = await readBody(req);
-    if (body.username === AUTH_USER && body.password === AUTH_PASS) {
-      const sessionData = { authenticated: true, username: AUTH_USER, loginTime: Date.now() };
-      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
-      json(res, { success: true, message: 'Login successful', username: AUTH_USER }, 200, {
-        'Set-Cookie': setSessionCookie(res, token),
-      });
-    } else {
-      json(res, { error: 'Invalid credentials' }, 401);
-    }
-  },
-
-  'POST /api/auth/logout': async (req, res) => {
-    json(res, { success: true, message: 'Logged out successfully' }, 200, {
-      'Set-Cookie': clearSessionCookie(),
-    });
-  },
-
-  'GET /api/auth/session': async (req, res) => {
-    if (isWhitelistedIP(req)) {
-      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'whitelisted_ip' });
-    }
-    if (hasBasicAuth(req)) {
-      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'basic_auth' });
-    }
-    const session = getSession(req);
-    if (session) {
-      return json(res, {
-        authenticated: true,
-        username: session.username,
-        email: session.email,
-        picture: session.picture,
-        provider: session.provider,
-        loginTime: session.loginTime,
-      });
-    }
-    json(res, { authenticated: false }, 401);
-  },
-
-  'POST /api/auth/google': async (req, res) => {
-    if (!googleClient) return json(res, { error: 'Google OAuth not configured' }, 400);
-    const body = await readBody(req);
-    if (!body.credential) return json(res, { error: 'Missing credential' }, 400);
-    try {
-      const ticket = await googleClient.verifyIdToken({ idToken: body.credential, audience: GOOGLE_CLIENT_ID });
-      const payload = ticket.getPayload();
-      const email = payload.email;
-      if (!payload.email_verified) return json(res, { error: 'Email not verified' }, 401);
-      if (ALLOWED_EMAILS.length > 0 && !ALLOWED_EMAILS.includes(email)) {
-        return json(res, { error: 'Email not authorized' }, 403);
-      }
-      const sessionData = {
-        authenticated: true,
-        username: payload.name || email,
-        email,
-        picture: payload.picture,
-        provider: 'google',
-        loginTime: Date.now(),
-      };
-      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
-      json(res, { success: true, username: payload.name || email, email, picture: payload.picture }, 200, {
-        'Set-Cookie': setSessionCookie(res, token),
-      });
-    } catch (err) {
-      json(res, { error: 'Invalid Google token', details: err.message }, 401);
-    }
-  },
-
-  'GET /api/auth/google-config': async (req, res) => {
-    json(res, { client_id: GOOGLE_CLIENT_ID || null, configured: !!GOOGLE_CLIENT_ID });
-  },
-
-  // ════════════════════════════════════════
-  // SETUP WIZARD (save/load progress)
-  // ════════════════════════════════════════
-
-  'GET /api/setup': async (req, res) => {
-    try {
-      const result = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const cfg = result.rows[0]?.config || {};
-      json(res, {
-        setup_complete: !!cfg.setup_complete,
-        setup_progress: cfg.setup_progress || null,
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  'POST /api/setup': async (req, res) => {
-    const body = await readBody(req);
-    try {
-      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const row = current.rows[0] || {};
-      const cfg = row.config || {};
-
-      if (body.action === 'save_progress') {
-        const newConfig = { ...cfg, setup_progress: body.progress };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
-        json(res, { success: true });
-      } else if (body.action === 'complete') {
-        const newConfig = { ...cfg, setup_complete: true, setup_progress: body.progress };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
-        json(res, { success: true, message: 'Setup complete!' });
-      } else {
-        json(res, { error: `Unknown setup action: ${body.action}` }, 400);
-      }
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // HEALTH
-  // ════════════════════════════════════════
-
-  'GET /api/health': async (req, res) => {
-    try {
-      const dbCheck = await q('SELECT 1');
-      const risk = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const cfg = risk.rows[0]?.config || {};
-
-      // Test Kalshi connection
-      let kalshiConnected = false;
-      let balance = null;
-      if (cfg.kalshi_api_key && cfg.kalshi_private_key) {
-        try {
-          const balData = await kalshiFetch('GET', '/portfolio/balance');
-          kalshiConnected = true;
-          balance = balData.balance ?? balData;
-        } catch {
-          // Keys exist but connection failed
-          kalshiConnected = false;
-        }
-      }
-
-      json(res, {
-        status: 'ok',
-        db_connected: !!dbCheck,
-        kalshi_connected: kalshiConnected,
-        env: KALSHI_ENV,
-        safe_mode: cfg.safe_mode ?? true,
-        balance,
-      });
-    } catch (err) {
-      json(res, { status: 'error', error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // KALSHI PROXY (all actions via POST)
-  // ════════════════════════════════════════
-
-  'POST /api/kalshi': async (req, res) => {
-    const body = await readBody(req);
-    const { action } = body;
-
-    if (!action) {
-      return json(res, { error: 'Missing action field' }, 400);
-    }
-
-    try {
-      switch (action) {
-
-        // ── Save API Keys ──
-        case 'save_keys': {
-          if (!body.api_key || !body.private_key) {
-            return json(res, { error: 'api_key and private_key (PEM) required' }, 400);
-          }
-          const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-          const row = current.rows[0] || {};
-          const newConfig = {
-            ...row.config,
-            kalshi_api_key: body.api_key,
-            kalshi_private_key: body.private_key,
-            kalshi_connected: true,
-            kalshi_connected_at: new Date().toISOString(),
-            kalshi_env: KALSHI_ENV,
-          };
-          await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-            JSON.stringify(newConfig), row.id,
-          ]);
-          json(res, {
-            success: true,
-            api_key: body.api_key.substring(0, 12) + '...',
-            env: KALSHI_ENV,
-            message: 'Kalshi API keys saved',
-          });
-          break;
-        }
-
-        // ── Test Connection ──
-        case 'test_connection': {
-          try {
-            const data = await kalshiPublicFetch('GET', '/exchange/status');
-            json(res, { success: true, exchange_status: data, env: KALSHI_ENV });
-          } catch (err) {
-            json(res, { success: false, error: err.message }, 500);
-          }
-          break;
-        }
-
-        // ── Get Balance ──
-        case 'get_balance': {
-          const data = await kalshiFetch('GET', '/portfolio/balance');
-          json(res, { success: true, balance: data });
-          break;
-        }
-
-        // ── Get Markets (public) ──
-        case 'get_markets': {
-          const params = {};
-          if (body.category) params.category = body.category;
-          if (body.status) params.status = body.status;
-          if (body.limit) params.limit = body.limit;
-          if (body.cursor) params.cursor = body.cursor;
-          if (body.series_ticker) params.series_ticker = body.series_ticker;
-          if (body.event_ticker) params.event_ticker = body.event_ticker;
-          const qs = buildQueryString(params);
-          const data = await kalshiPublicFetch('GET', `/markets${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Get Single Market ──
-        case 'get_market': {
-          if (!body.ticker) return json(res, { error: 'ticker required' }, 400);
-          const data = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(body.ticker)}`);
-          json(res, { success: true, market: data });
-          break;
-        }
-
-        // ── Get Orderbook ──
-        case 'get_orderbook': {
-          if (!body.ticker) return json(res, { error: 'ticker required' }, 400);
-          const qs = body.depth ? `?depth=${body.depth}` : '';
-          const data = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(body.ticker)}/orderbook${qs}`);
-          json(res, { success: true, orderbook: data });
-          break;
-        }
-
-        // ── Get Trades (public market trades) ──
-        case 'get_trades': {
-          if (!body.ticker) return json(res, { error: 'ticker required' }, 400);
-          const params = {};
-          if (body.limit) params.limit = body.limit;
-          if (body.cursor) params.cursor = body.cursor;
-          const qs = buildQueryString(params);
-          const data = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(body.ticker)}/trades${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Get Events ──
-        case 'get_events': {
-          // Accept flat fields or nested in body.params (BrowseMarkets sends params wrapper)
-          const src = body.params || body;
-          const params = {};
-          if (src.status) params.status = src.status;
-          if (src.limit) params.limit = src.limit;
-          if (src.cursor) params.cursor = src.cursor;
-          if (src.series_ticker) params.series_ticker = src.series_ticker;
-          if (src.with_nested_markets !== undefined) params.with_nested_markets = src.with_nested_markets;
-          const qs = buildQueryString(params);
-          const data = await kalshiPublicFetch('GET', `/events${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Get Single Event ──
-        case 'get_event': {
-          if (!body.event_ticker) return json(res, { error: 'event_ticker required' }, 400);
-          const qs = body.with_nested_markets ? '?with_nested_markets=true' : '';
-          const data = await kalshiPublicFetch('GET', `/events/${encodeURIComponent(body.event_ticker)}${qs}`);
-          json(res, { success: true, event: data });
-          break;
-        }
-
-        // ── Get Series ──
-        case 'get_series': {
-          if (!body.series_ticker) return json(res, { error: 'series_ticker required' }, 400);
-          const data = await kalshiPublicFetch('GET', `/series/${encodeURIComponent(body.series_ticker)}`);
-          json(res, { success: true, series: data });
-          break;
-        }
-
-        // ── Get Positions (from portfolio trades DB, or real Kalshi API if keys configured) ──
-        case 'get_positions': {
-          {
-            // No API keys — return portfolio trades from DB
-            const { rows: posRows } = await kenQ(`
-              SELECT pt.market_id as ticker, pt.market_title, pt.direction,
-                COUNT(*) FILTER (WHERE pt.status = 'open') as open_count,
-                SUM(pt.contracts) FILTER (WHERE pt.status = 'open') as open_contracts,
-                SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') as open_cost,
-                ROUND(AVG(pt.entry_price_cents) FILTER (WHERE pt.status = 'open')) as avg_price,
-                COUNT(*) as total_trades,
-                SUM(COALESCE(pt.pnl_cents, 0)) FILTER (WHERE pt.status != 'open') as realized_pnl
-              FROM ken_portfolio_trades pt
-              WHERE pt.created_at > NOW() - INTERVAL '24 hours'
-              GROUP BY pt.market_id, pt.market_title, pt.direction
-              HAVING COUNT(*) FILTER (WHERE pt.status = 'open') > 0
-              ORDER BY SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') DESC NULLS LAST
-            `);
-            const positions = posRows.map(r => ({
-              ticker: r.ticker,
-              market_ticker: r.ticker,
-              title: r.market_title || r.ticker,
-              market_exposure: r.direction === 'BUY_YES' ? parseInt(r.open_contracts || 0) : -parseInt(r.open_contracts || 0),
-              position: r.direction === 'BUY_YES' ? parseInt(r.open_contracts || 0) : -parseInt(r.open_contracts || 0),
-              position_cost: parseInt(r.open_cost || 0) * 100,
-              realized_pnl: parseInt(r.realized_pnl || 0) * 100,
-              fees_paid: 0,
-              avg_price_cents: parseInt(r.avg_price || 0),
-              open_trades: parseInt(r.open_count || 0),
-              total_trades: parseInt(r.total_trades || 0),
-            }));
-            json(res, { success: true, positions, market_positions: positions, is_paper: true });
-          }
-          break;
-        }
-
-        // ── Get Fills (auth required) ──
-        case 'get_fills': {
-          const params = {};
-          if (body.limit) params.limit = body.limit;
-          if (body.cursor) params.cursor = body.cursor;
-          if (body.ticker) params.ticker = body.ticker;
-          if (body.order_id) params.order_id = body.order_id;
-          const qs = buildQueryString(params);
-          const data = await kalshiFetch('GET', `/portfolio/fills${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Get Orders (auth required) ──
-        case 'get_orders': {
-          const params = {};
-          if (body.limit) params.limit = body.limit;
-          if (body.cursor) params.cursor = body.cursor;
-          if (body.ticker) params.ticker = body.ticker;
-          if (body.status) params.status = body.status;
-          const qs = buildQueryString(params);
-          const data = await kalshiFetch('GET', `/portfolio/orders${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Create Order (auth required) ──
-        case 'create_order': {
-          if (!body.ticker || !body.side || !body.action || !body.type || !body.count) {
-            return json(res, { error: 'ticker, side, action, type, and count are required' }, 400);
-          }
-          const orderPayload = {
-            ticker: body.ticker,
-            side: body.side,       // 'yes' or 'no'
-            action: body.action,   // 'buy' or 'sell'
-            type: body.type,       // 'limit' or 'market'
-            count: body.count,
-          };
-          if (body.yes_price !== undefined) orderPayload.yes_price = body.yes_price;
-          if (body.no_price !== undefined) orderPayload.no_price = body.no_price;
-          if (body.client_order_id) orderPayload.client_order_id = body.client_order_id;
-          if (body.time_in_force) orderPayload.time_in_force = body.time_in_force;
-          if (body.expiration_ts) orderPayload.expiration_ts = body.expiration_ts;
-
-          const data = await kalshiFetch('POST', '/portfolio/orders', orderPayload);
-          json(res, { success: true, order: data });
-          break;
-        }
-
-        // ── Cancel Order (auth required) ──
-        case 'cancel_order': {
-          if (!body.order_id) return json(res, { error: 'order_id required' }, 400);
-          const data = await kalshiFetch('DELETE', `/portfolio/orders/${encodeURIComponent(body.order_id)}`);
-          json(res, { success: true, result: data });
-          break;
-        }
-
-        // ── Get Single Order (auth required) ──
-        case 'get_order': {
-          if (!body.order_id) return json(res, { error: 'order_id required' }, 400);
-          const data = await kalshiFetch('GET', `/portfolio/orders/${encodeURIComponent(body.order_id)}`);
-          json(res, { success: true, order: data });
-          break;
-        }
-
-        // ── Auto-Trader Status ──
-        case 'trader_status': {
-          const state = await getTraderState();
-          const allTrades = await kenQ('SELECT * FROM ken_trades ORDER BY created_at DESC LIMIT 50');
-          const closedTrades = (allTrades.rows || []).filter(t => t.status !== 'open');
-          const totalPnL = closedTrades.reduce((sum, t) => sum + (t.pnl_cents || 0), 0);
-          const wins = closedTrades.filter(t => (t.pnl_cents || 0) > 0).length;
-          const losses = closedTrades.filter(t => (t.pnl_cents || 0) < 0).length;
-          json(res, {
-            success: true,
-            config: TRADE_CONFIG,
-            state: {
-              open_positions: state.openCount,
-              total_invested: state.totalInvested,
-              available_budget: state.available,
-              daily_loss: state.dailyLoss,
-            },
-            positions: state.openPositions,
-            stats: {
-              total_trades: closedTrades.length,
-              wins, losses,
-              win_rate: closedTrades.length > 0 ? Math.round(wins / closedTrades.length * 100) : 0,
-              total_pnl_cents: totalPnL,
-              total_pnl_dollars: (totalPnL / 100).toFixed(2),
-            },
-            recent_trades: (allTrades.rows || []).slice(0, 20),
-          });
-          break;
-        }
-
-        // ── Toggle Auto-Trader ──
-        case 'trader_toggle': {
-          TRADE_CONFIG.enabled = !TRADE_CONFIG.enabled;
-          saveTradeConfig();
-          json(res, { success: true, enabled: TRADE_CONFIG.enabled, message: `Auto-trader ${TRADE_CONFIG.enabled ? 'ENABLED' : 'DISABLED'}` });
-          console.log(`[Ken] Auto-trader ${TRADE_CONFIG.enabled ? 'ENABLED' : 'DISABLED'} via API`);
-          break;
-        }
-
-        // ── Run Predictions (weather model pipeline) ──
-        case 'run_predictions': {
-          // Step 1: Get weather events from Kalshi
-          let events = [];
-          try {
-            const evData = await kalshiPublicFetch('GET', '/events?status=open&limit=100');
-            events = (evData.events || []).filter(e => {
-              const t = ((e.category || '') + ' ' + (e.title || '')).toLowerCase();
-              return t.includes('weather') || t.includes('temperature') || t.includes('temp ') ||
-                     t.includes('rain') || t.includes('snow') || t.includes('precipitation') ||
-                     t.includes('hurricane') || t.includes('wind') || t.includes('°f');
-            });
-          } catch { /* no events available - use weather cache */ }
-
-          // Step 2: Load cached weather data
-          const riskRow = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-          const rCfg = riskRow.rows[0]?.config || {};
-          const weatherCache = rCfg.weather_cache?.results || [];
-          const month = new Date().getMonth() + 1;
-
-          // Step 3: Generate predictions (only for real weather events with a city match)
-          const predictions = [];
-          for (const evt of events) {
-            const city = extractCity(evt.title);
-            if (!city) continue; // Skip non-geographic events (climate policy, earthquakes, etc.)
-            const spec = parseWeatherEvent(evt.title);
-            if (!spec || spec.variable === 'unknown') continue; // Skip if we can't parse a weather variable
-            const cityWeather = weatherCache.find(w => w.city === city);
-            if (!cityWeather) continue; // Skip if no weather data for this city
-            const features = cityWeather[spec.variable === 'snow' ? 'precipitation' : spec.variable] || cityWeather.temperature;
-
-            if (features && features.values?.length) {
-              const rawProb = ensembleExceedance(features, spec.threshold, spec.operator);
-              const prior = climatologicalPrior(spec.variable, spec.threshold, month);
-              const combined = bayesianUpdate(prior, rawProb ?? 0.5);
-              const pYes = Math.max(0.01, Math.min(0.99, combined));
-
-              // Get market price for edge calc
-              let marketPrice = 50;
-              try {
-                const mkts = await kalshiPublicFetch('GET', `/events/${encodeURIComponent(evt.event_ticker)}?with_nested_markets=true`);
-                const mkt = mkts.event?.markets?.[0] || mkts.markets?.[0];
-                if (mkt) {
-                  const p = mkt.yes_ask || mkt.last_price || 50;
-                  if (p > 0 && p < 100) marketPrice = p; // Skip fully priced markets
-                }
-              } catch {}
-
-              const edgeBps = Math.round((pYes * 100 - marketPrice) * 100);
-              predictions.push({
-                event_ticker: evt.event_ticker,
-                ticker: evt.title?.substring(0, 60),
-                market: evt.title,
-                city,
-                side: edgeBps > 0 ? 'yes' : 'no',
-                p_yes: Math.round(pYes * 1000) / 10,
-                price_cents: marketPrice,
-                edge_bps: edgeBps,
-                method: 'ensemble_bayesian_v1',
-                variable: spec.variable,
-                threshold: spec.threshold,
-                data_points: features.values.length,
-              });
-            }
-          }
-
-          // If no Kalshi events but we have weather data, generate synthetic predictions
-          if (predictions.length === 0 && weatherCache.length > 0) {
-            for (const wd of weatherCache.filter(w => !w.error)) {
-              if (wd.temperature?.values?.length) {
-                const maxT = wd.temperature.max;
-                const threshold = Math.round(maxT / 5) * 5;
-                const rawProb = ensembleExceedance(wd.temperature, threshold, '>=');
-                const prior = climatologicalPrior('temperature', threshold, month);
-                const combined = bayesianUpdate(prior, rawProb ?? 0.5);
-                const pYes = Math.max(0.01, Math.min(0.99, combined));
-                predictions.push({
-                  ticker: `${wd.city} temp >= ${threshold}°F`,
-                  market: `Will the high temperature in ${wd.city} reach ${threshold}°F?`,
-                  city: wd.city,
-                  side: pYes > 0.5 ? 'yes' : 'no',
-                  p_yes: Math.round(pYes * 1000) / 10,
-                  price_cents: 50,
-                  edge_bps: Math.round((pYes * 100 - 50) * 100),
-                  method: 'synthetic_forecast',
-                  variable: 'temperature',
-                  threshold,
-                  data_points: wd.temperature.values.length,
-                });
-              }
-            }
-          }
-
-          predictions.sort((a, b) => Math.abs(b.edge_bps) - Math.abs(a.edge_bps));
-          const buySignals = predictions.filter(p => p.edge_bps > 0);
-          const avgEdge = predictions.length ? Math.round(predictions.reduce((s, p) => s + p.edge_bps, 0) / predictions.length) : 0;
-
-          json(res, {
-            success: true,
-            predictions,
-            buy_signals: buySignals.length,
-            avg_edge_bps: avgEdge,
-            total_events_scanned: events.length,
-            weather_cities: weatherCache.filter(w => !w.error).length,
-          });
-          break;
-        }
-
-        // ── Disconnect: remove keys from config ──
-        case 'disconnect': {
-          const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-          const row = current.rows[0] || {};
-          const newConfig = {
-            ...row.config,
-            kalshi_api_key: null,
-            kalshi_private_key: null,
-            kalshi_connected: false,
-            kalshi_connected_at: null,
-          };
-          await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-            JSON.stringify(newConfig), row.id,
-          ]);
-          json(res, { success: true, message: 'Kalshi keys removed, disconnected' });
-          break;
-        }
-
-        // ── Status: show current connection config (masked) ──
-        case 'status': {
-          const current = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-          const cfg = current.rows[0]?.config || {};
-          json(res, {
-            connected: !!cfg.kalshi_connected,
-            api_key: cfg.kalshi_api_key ? cfg.kalshi_api_key.substring(0, 12) + '...' : null,
-            has_private_key: !!cfg.kalshi_private_key,
-            connected_at: cfg.kalshi_connected_at || null,
-            env: KALSHI_ENV,
-          });
-          break;
-        }
-
-        default:
-          json(res, {
-            error: `Unknown action: ${action}`,
-            available_actions: [
-              'save_keys', 'test_connection', 'get_balance',
-              'get_markets', 'get_market', 'get_orderbook', 'get_trades',
-              'get_events', 'get_event', 'get_series',
-              'get_positions', 'get_fills', 'get_orders',
-              'create_order', 'cancel_order', 'get_order',
-              'run_predictions', 'disconnect', 'status',
-            ],
-          }, 400);
-      }
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // WEATHER ENDPOINTS
-  // ════════════════════════════════════════
-
-  'POST /api/weather': async (req, res) => {
-    const body = await readBody(req);
-
-    if (body.action === 'fetch_all') {
-      try {
-        const cities = body.cities || ['New York', 'Chicago', 'Miami', 'Denver', 'Los Angeles', 'Seattle'];
-        const useNws = body.force_nws === true; // opt-in to real NWS (flaky from this server)
-
-        // ── Generate synthetic seasonal forecast data (fast, reliable) ──
-        function generateSyntheticCities(cityList) {
-          const month = new Date().getMonth();
-          const results = [];
-          let totalDP = 0;
-          for (const city of cityList) {
-            const coords = CITY_COORDS[city]; if (!coords) continue;
-            const baseTemp = [35,38,48,58,68,78,85,83,75,62,48,38][month] + (coords.lat < 35 ? 15 : coords.lat > 42 ? -8 : 0);
-            const temps = Array.from({length: 14}, (_, i) => baseTemp + Math.round((Math.random() - 0.5) * 12) + (i < 7 ? 3 : -2));
-            const precips = Array.from({length: 14}, () => Math.round(Math.random() * 60));
-            const winds = Array.from({length: 14}, () => 5 + Math.round(Math.random() * 15));
-            const mkStat = (arr) => ({ mean: arr.reduce((a,b)=>a+b)/arr.length, min: Math.min(...arr), max: Math.max(...arr), latest: arr[0] });
-            results.push({
-              city, synthetic: true,
-              temperature: { variable: 'temperature', unit: 'F', values: temps, ...mkStat(temps) },
-              precipitation: { variable: 'precipitation', unit: 'percent', values: precips, ...mkStat(precips) },
-              wind: { variable: 'wind', unit: 'mph', values: winds, ...mkStat(winds) },
-              data_points: 42,
-            });
-            totalDP += 42;
-          }
-          return { results, totalDP };
-        }
-
-        let responseData;
-
-        if (useNws) {
-          // Real NWS path — wrapped in 15s global timeout, falls back to synthetic
-          const nwsResult = await Promise.race([
-            (async () => {
-              const results = [];
-              let dp = 0;
-              const mets = new Set();
-              for (const city of cities) {
-                const coords = CITY_COORDS[city]; if (!coords) continue;
-                try {
-                  const grid = await nwsFetch(`${NWS_BASE}/points/${coords.lat},${coords.lon}`);
-                  const forecast = await nwsFetch(`${NWS_BASE}/gridpoints/${grid.properties.gridId}/${grid.properties.gridX},${grid.properties.gridY}/forecast`);
-                  const tempF = extractForecastFeatures(forecast, 'temperature');
-                  const precip = extractForecastFeatures(forecast, 'precipitation');
-                  const wind = extractForecastFeatures(forecast, 'wind');
-                  const cdp = (tempF?.values?.length||0) + (precip?.values?.length||0) + (wind?.values?.length||0);
-                  dp += cdp;
-                  if (tempF) mets.add('temperature'); if (precip) mets.add('precipitation'); if (wind) mets.add('wind');
-                  results.push({ city, temperature: tempF, precipitation: precip, wind, data_points: cdp });
-                } catch (e) { results.push({ city, error: e.message }); }
-              }
-              return { results, totalDP: dp, metrics: [...mets], synthetic: false };
-            })(),
-            new Promise(resolve => setTimeout(() => resolve(null), 15000)),
-          ]);
-
-          if (nwsResult && nwsResult.results.some(r => !r.error)) {
-            responseData = nwsResult;
-          } else {
-            const syn = generateSyntheticCities(cities);
-            responseData = { ...syn, metrics: ['temperature','precipitation','wind'], synthetic: true, note: 'NWS timed out — using synthetic' };
-          }
-        } else {
-          // Default: synthetic data (instant, always works)
-          const syn = generateSyntheticCities(cities);
-          responseData = { ...syn, metrics: ['temperature','precipitation','wind'], synthetic: true, note: 'Using seasonal synthetic forecasts (pass force_nws:true for real NWS data)' };
-        }
-
-        // Cache in DB
-        const row = (await q('SELECT id, config FROM risk_state ORDER BY updated_at DESC LIMIT 1')).rows[0];
-        if (row) {
-          const newCfg = { ...(row.config || {}), weather_cache: { results: responseData.results, fetched_at: new Date().toISOString(), synthetic: responseData.synthetic } };
-          await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newCfg), row.id]);
-        }
-
-        json(res, {
-          success: true,
-          synthetic: responseData.synthetic,
-          stations: responseData.results.length,
-          data_points: responseData.totalDP,
-          metrics: responseData.metrics,
-          cities: responseData.results,
-          fetched_at: new Date().toISOString(),
-          note: responseData.note || undefined,
-        });
-      } catch (err) {
-        json(res, { success: false, error: err.message }, 500);
-      }
-    } else {
-      json(res, { error: `Unknown weather action: ${body.action}` }, 400);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // RISK ENDPOINTS
-  // ════════════════════════════════════════
-
-  'POST /api/risk': async (req, res) => {
-    const body = await readBody(req);
-
-    try {
-      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const row = current.rows[0] || {};
-      const cfg = row.config || {};
-
-      if (body.action === 'get_config') {
-        json(res, {
-          success: true,
-          config: {
-            safe_mode: cfg.safe_mode ?? true,
-            risk_profile: cfg.risk_profile ?? 'conservative',
-            max_position_size: cfg.max_position_size ?? 100,
-            max_contracts_per_market: cfg.max_contracts_per_market ?? 25,
-            max_dollars_at_risk: cfg.max_dollars_at_risk ?? 250,
-            max_daily_loss: cfg.max_daily_loss ?? cfg.daily_loss_cap ?? 50,
-            daily_loss_cap: cfg.daily_loss_cap ?? 50,
-            max_open_orders: cfg.max_open_orders ?? 10,
-            max_exposure: cfg.max_exposure ?? 500,
-            stop_loss_pct: cfg.stop_loss_pct ?? 20,
-            auto_hedge: cfg.auto_hedge ?? false,
-            allowed_categories: cfg.allowed_categories ?? [],
-            kill_switch_active: row.kill_switch_active || false,
-            kill_reason: row.kill_reason || null,
-            bankroll: parseFloat(row.bankroll || 0),
-            daily_pnl: parseFloat(row.daily_pnl || 0),
-            open_exposure: parseFloat(row.open_exposure || 0),
-          },
-        });
-      } else if (body.action === 'update_config') {
-        // Accept flat fields (from RiskProfile step) or wrapped in config object
-        const updates = body.config || {};
-        if (body.max_contracts_per_market !== undefined) updates.max_contracts_per_market = body.max_contracts_per_market;
-        if (body.max_dollars_at_risk !== undefined) updates.max_dollars_at_risk = body.max_dollars_at_risk;
-        if (body.daily_loss_cap !== undefined) updates.daily_loss_cap = body.daily_loss_cap;
-        if (body.risk_profile !== undefined) updates.risk_profile = body.risk_profile;
-        if (body.safe_mode !== undefined) updates.safe_mode = body.safe_mode;
-        const newConfig = { ...cfg, ...updates };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-          JSON.stringify(newConfig), row.id,
-        ]);
-
-        // Also update top-level risk fields if provided
-        if (body.config?.kill_switch_active !== undefined) {
-          await q('UPDATE risk_state SET kill_switch_active = $1, kill_reason = $2, updated_at = NOW() WHERE id = $3', [
-            body.config.kill_switch_active,
-            body.config.kill_reason || null,
-            row.id,
-          ]);
-        }
-
-        const updated = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-        const u = updated.rows[0] || {};
-        json(res, {
-          success: true,
-          message: 'Risk config updated',
-          config: u.config || {},
-        });
-      } else {
-        json(res, { error: `Unknown risk action: ${body.action}` }, 400);
-      }
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // TRADING MODE CONTROL
-  // ════════════════════════════════════════
-
-  'POST /api/trading/mode': async (req, res) => {
-    const body = await readBody(req);
-    try {
-      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const row = current.rows[0] || {};
-      const cfg = row.config || {};
-
-      if (body.action === 'get') {
-        // Get current trading mode
-        const traderState = await getTraderState().catch(() => ({ openPositions: [], openCount: 0, totalInvested: 0, dailyLoss: 0, available: 0 }));
-        return json(res, {
-          env: KALSHI_ENV,
-          safe_mode: cfg.safe_mode ?? true,
-          kalshi_connected: !!(cfg.kalshi_api_key && cfg.kalshi_private_key),
-          trade_config: TRADE_CONFIG,
-          trader_state: traderState,
-          auto_trader_enabled: TRADE_CONFIG.enabled,
-        });
-      }
-
-      if (body.action === 'switch_env') {
-        const newEnv = body.env === 'prod' ? 'prod' : 'demo';
-        const newConfig = { ...cfg, kalshi_env: newEnv };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
-        KALSHI_ENV = newEnv;
-        KALSHI_BASE_URL = KALSHI_BASE_URLS[newEnv];
-        console.log(`[Ken] Environment switched to ${newEnv.toUpperCase()} (${KALSHI_BASE_URL})`);
-        return json(res, { success: true, env: newEnv, base_url: KALSHI_BASE_URL });
-      }
-
-      if (body.action === 'toggle_safe_mode') {
-        const newSafe = !(cfg.safe_mode ?? true);
-        const newConfig = { ...cfg, safe_mode: newSafe };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
-        console.log(`[Ken] Safe mode ${newSafe ? 'ENABLED' : 'DISABLED'}`);
-        return json(res, { success: true, safe_mode: newSafe });
-      }
-
-      if (body.action === 'toggle_auto_trader') {
-        TRADE_CONFIG.enabled = !TRADE_CONFIG.enabled;
-        saveTradeConfig();
-        console.log(`[Ken] Auto-trader ${TRADE_CONFIG.enabled ? 'ENABLED' : 'DISABLED'}`);
-        return json(res, { success: true, auto_trader_enabled: TRADE_CONFIG.enabled });
-      }
-
-      if (body.action === 'update_trade_config') {
-        if (body.budget_cents !== undefined) TRADE_CONFIG.budget_cents = Math.max(100, Math.min(100000, body.budget_cents));
-        if (body.max_position_cents !== undefined) TRADE_CONFIG.max_position_cents = Math.max(50, Math.min(10000, body.max_position_cents));
-        if (body.max_open_positions !== undefined) TRADE_CONFIG.max_open_positions = Math.max(1, Math.min(20, body.max_open_positions));
-        if (body.min_confidence !== undefined) TRADE_CONFIG.min_confidence = Math.max(50, Math.min(99, body.min_confidence));
-        if (body.daily_loss_limit_cents !== undefined) TRADE_CONFIG.daily_loss_limit_cents = Math.max(100, Math.min(50000, body.daily_loss_limit_cents));
-        saveTradeConfig();
-        return json(res, { success: true, trade_config: TRADE_CONFIG });
-      }
-
-      json(res, { error: `Unknown trading mode action: ${body.action}` }, 400);
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // TRADING HISTORY & P&L
-  // ════════════════════════════════════════
-
-  'GET /api/trading/history': async (req, res) => {
-    try {
-      const { rows: trades } = await kenQ('SELECT * FROM ken_trades ORDER BY created_at DESC LIMIT 100');
-      const { rows: [summary] } = await kenQ(`
-        SELECT
-          COUNT(*) as total_trades,
-          COUNT(*) FILTER (WHERE status = 'open') as open_trades,
-          COUNT(*) FILTER (WHERE pnl_cents > 0) as winning_trades,
-          COUNT(*) FILTER (WHERE pnl_cents < 0) as losing_trades,
-          COALESCE(SUM(pnl_cents), 0) as total_pnl,
-          COALESCE(SUM(cost_cents) FILTER (WHERE status = 'open'), 0) as open_exposure
-        FROM ken_trades
-      `);
-      json(res, { trades, summary });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // PAPER TRADING
-  // ════════════════════════════════════════
-
-  'POST /api/paper': async (req, res) => {
-    const body = await readBody(req);
-
-    try {
-      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const row = current.rows[0] || {};
-      const cfg = row.config || {};
-      const paperTrades = cfg.paper_trades || [];
-
-      if (body.action === 'submit_order') {
-        if (!body.ticker || !body.side || !body.count) {
-          return json(res, { error: 'ticker, side, and count required' }, 400);
-        }
-
-        const orderId = `paper_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
-        const fillPrice = body.yes_price || body.no_price || body.price || 50; // cents
-
-        const paperOrder = {
-          order_id: orderId,
-          ticker: body.ticker,
-          side: body.side,           // 'yes' or 'no'
-          action: body.action_type || 'buy',
-          type: body.type || 'market',
-          count: body.count,
-          price: fillPrice,
-          cost: fillPrice * body.count, // in cents
-          status: 'filled',
-          created_at: new Date().toISOString(),
-          filled_at: new Date().toISOString(),
-          is_paper: true,
-        };
-
-        paperTrades.push(paperOrder);
-
-        const newConfig = { ...cfg, paper_trades: paperTrades };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-          JSON.stringify(newConfig), row.id,
-        ]);
-
-        json(res, { success: true, order: paperOrder, message: 'Paper order filled' });
-
-      } else if (body.action === 'get_positions') {
-        // Aggregate from ken_portfolio_trades (live hyper-trading data)
-        try {
-          const { rows } = await kenQ(`
-            SELECT pt.market_id as ticker, pt.market_title, pt.direction,
-              SUM(pt.contracts) as total_contracts,
-              SUM(pt.cost_cents) as position_cost,
-              ROUND(AVG(pt.entry_price_cents)) as avg_price,
-              COUNT(*) as trades,
-              SUM(CASE WHEN pt.status != 'open' THEN pt.pnl_cents ELSE 0 END) as realized_pnl
-            FROM ken_portfolio_trades pt
-            WHERE pt.created_at > NOW() - INTERVAL '24 hours'
-            GROUP BY pt.market_id, pt.market_title, pt.direction
-            ORDER BY SUM(pt.cost_cents) DESC
-          `);
-          const positions = rows.map(r => ({
-            ticker: r.ticker,
-            market_ticker: r.ticker,
-            title: r.market_title,
-            side: r.direction === 'BUY_YES' ? 'yes' : 'no',
-            market_exposure: r.direction === 'BUY_YES' ? parseInt(r.total_contracts) : -parseInt(r.total_contracts),
-            position: r.direction === 'BUY_YES' ? parseInt(r.total_contracts) : -parseInt(r.total_contracts),
-            position_cost: parseInt(r.position_cost) * 100,
-            avg_price_cents: parseInt(r.avg_price),
-            realized_pnl: parseInt(r.realized_pnl || 0) * 100,
-            fees_paid: 0,
-            trades: parseInt(r.trades),
-          }));
-          json(res, { success: true, positions, is_paper: true });
-        } catch (e) {
-          // Fallback to old paper trades
-          const positionMap = {};
-          for (const trade of paperTrades) {
-            const key = `${trade.ticker}_${trade.side}`;
-            if (!positionMap[key]) positionMap[key] = { ticker: trade.ticker, side: trade.side, total_contracts: 0, avg_price: 0, total_cost: 0, trades: 0 };
-            const pos = positionMap[key];
-            if (trade.action === 'buy') { pos.total_cost += trade.price * trade.count; pos.total_contracts += trade.count; }
-            else { pos.total_cost -= trade.price * trade.count; pos.total_contracts -= trade.count; }
-            pos.trades++;
-            pos.avg_price = pos.total_contracts > 0 ? Math.round(pos.total_cost / pos.total_contracts) : 0;
-          }
-          const positions = Object.values(positionMap).filter(p => p.total_contracts !== 0);
-          json(res, { success: true, positions, is_paper: true });
-        }
-
-      } else if (body.action === 'get_fills') {
-        // Return paper trade history
-        const limit = body.limit || 50;
-        const fills = paperTrades.slice(-limit).reverse();
-        json(res, { success: true, fills, total: paperTrades.length, is_paper: true });
-
-      } else if (body.action === 'clear') {
-        // Clear all paper trades
-        const newConfig = { ...cfg, paper_trades: [] };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-          JSON.stringify(newConfig), row.id,
-        ]);
-        json(res, { success: true, message: 'Paper trades cleared' });
-
-      } else {
-        json(res, { error: `Unknown paper action: ${body.action}` }, 400);
-      }
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // ── Positions Detail API (collapsible tables) ──
-  'GET /api/positions': async (req, res) => {
-    try {
-      // All positions with full stats
-      const { rows: positions } = await kenQ(`
-        SELECT
-          pt.market_id AS ticker,
-          pt.direction,
-          pt.market_title AS title,
-          COUNT(*) FILTER (WHERE pt.status = 'open') AS open_trades,
-          SUM(pt.contracts) FILTER (WHERE pt.status = 'open') AS open_contracts,
-          SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') AS open_cost_cents,
-          ROUND(AVG(pt.entry_price_cents) FILTER (WHERE pt.status = 'open')) AS avg_entry_price,
-          SUM(COALESCE(pt.pnl_cents, 0)) FILTER (WHERE pt.status != 'open') AS realized_pnl_cents,
-          COUNT(*) AS total_trades,
-          COUNT(*) FILTER (WHERE pt.status != 'open') AS closed_trades,
-          MIN(pt.created_at) AS first_trade_at,
-          MAX(pt.created_at) AS last_trade_at,
-          MIN(pt.entry_price_cents) FILTER (WHERE pt.status = 'open') AS min_entry,
-          MAX(pt.entry_price_cents) FILTER (WHERE pt.status = 'open') AS max_entry
-        FROM ken_portfolio_trades pt
-        GROUP BY pt.market_id, pt.direction, pt.market_title
-        HAVING COUNT(*) FILTER (WHERE pt.status = 'open') > 0
-        ORDER BY SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') DESC NULLS LAST
-      `);
-
-      // Category grouping by market event prefix
-      const grouped = {};
-      for (const p of positions) {
-        const prefix = (p.ticker || '').split('-')[0];
-        if (!grouped[prefix]) grouped[prefix] = { event: prefix, title: p.title, positions: [], totalCost: 0, totalPnl: 0, totalContracts: 0 };
-        grouped[prefix].positions.push(p);
-        grouped[prefix].totalCost += parseInt(p.open_cost_cents || 0);
-        grouped[prefix].totalPnl += parseInt(p.realized_pnl_cents || 0);
-        grouped[prefix].totalContracts += parseInt(p.open_contracts || 0);
-      }
-
-      // Summary stats
-      const totalCost = positions.reduce((s, p) => s + parseInt(p.open_cost_cents || 0), 0);
-      const totalPnl = positions.reduce((s, p) => s + parseInt(p.realized_pnl_cents || 0), 0);
-      const totalContracts = positions.reduce((s, p) => s + parseInt(p.open_contracts || 0), 0);
-      const totalTrades = positions.reduce((s, p) => s + parseInt(p.total_trades || 0), 0);
-
-      json(res, {
-        success: true,
-        positions: positions.map(p => ({
-          ...p,
-          open_trades: parseInt(p.open_trades || 0),
-          open_contracts: parseInt(p.open_contracts || 0),
-          open_cost_cents: parseInt(p.open_cost_cents || 0),
-          avg_entry_price: parseInt(p.avg_entry_price || 0),
-          realized_pnl_cents: parseInt(p.realized_pnl_cents || 0),
-          total_trades: parseInt(p.total_trades || 0),
-          closed_trades: parseInt(p.closed_trades || 0),
-          min_entry: parseInt(p.min_entry || 0),
-          max_entry: parseInt(p.max_entry || 0),
-        })),
-        grouped: Object.values(grouped).sort((a, b) => b.totalCost - a.totalCost),
-        summary: { totalCost, totalPnl, totalContracts, totalTrades, positionCount: positions.length },
-      });
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // Get trade history for a specific position
-  'POST /api/positions/trades': async (req, res) => {
-    try {
-      const body = await readBody(req);
-      const { ticker, direction, limit: lim } = body;
-      if (!ticker) return json(res, { error: 'ticker required' }, 400);
-
-      let sql = `SELECT * FROM ken_portfolio_trades WHERE market_id = $1`;
-      const params = [ticker];
-      if (direction) { sql += ` AND direction = $2`; params.push(direction); }
-      sql += ` ORDER BY created_at DESC LIMIT ${Math.min(parseInt(lim) || 100, 500)}`;
-
-      const { rows } = await kenQ(sql, params);
-      json(res, { success: true, trades: rows });
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // ── Signal Pipeline API ──
-  'GET /api/signals': async (req, res) => {
-    try {
-      const { rows: signals } = await kenQ(`
-        SELECT id, market_id, direction, expected_edge, size_recommendation, confidence, reasoning, sources, risk_approved, executed, created_at
-        FROM ken_strategy_signals
-        ORDER BY created_at DESC
-        LIMIT 50
-      `);
-      const { rows: shifts } = await kenQ(`
-        SELECT market_id, prior_probability, sentiment_impact, adjusted_probability, market_probability, edge, num_articles, created_at
-        FROM ken_probability_shifts
-        ORDER BY created_at DESC
-        LIMIT 50
-      `);
-      const { rows: recentSentiment } = await kenQ(`
-        SELECT event_tag, market_ticker, AVG(sentiment) as avg_sentiment, AVG(confidence) as avg_confidence, COUNT(*) as count
-        FROM ken_event_sentiment
-        WHERE created_at > NOW() - INTERVAL '1 hour'
-        GROUP BY event_tag, market_ticker
-        ORDER BY count DESC
-        LIMIT 30
-      `);
-      // Enrich with Kalshi URLs
-      const enrichWithUrl = (rows) => rows.map(r => {
-        const ticker = r.market_id || '';
-        const eventTicker = ticker.replace(/-[^-]+$/, '') || ticker;
-        const seriesTicker = eventTicker.replace(/-[^-]+$/, '') || eventTicker;
-        return { ...r, event_ticker: eventTicker, kalshi_url: `https://kalshi.com/markets/${seriesTicker.toLowerCase()}/${eventTicker.toLowerCase()}` };
-      });
-      json(res, {
-        signals: enrichWithUrl(signals),
-        probabilityShifts: enrichWithUrl(shifts),
-        eventSentiment: recentSentiment,
-        pipeline: {
-          scanInterval: '15 min',
-          signalInterval: '5 min',
-          intelligenceInterval: '30 min',
-          cachedMarkets: cachedActiveMarkets.length,
-          riskLimits: RISK_LIMITS,
-        },
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  'GET /api/signals/dashboard': async (req, res) => {
-    try {
-      // Summary stats for the signal pipeline
-      const { rows: [stats] } = await kenQ(`
-        SELECT
-          COUNT(*) as total_signals,
-          COUNT(CASE WHEN risk_approved THEN 1 END) as approved_signals,
-          COUNT(CASE WHEN direction = 'BUY_YES' THEN 1 END) as buy_yes,
-          COUNT(CASE WHEN direction = 'BUY_NO' THEN 1 END) as buy_no,
-          AVG(expected_edge) as avg_edge,
-          AVG(confidence) as avg_confidence,
-          MAX(created_at) as latest_signal
-        FROM ken_strategy_signals
-        WHERE created_at > NOW() - INTERVAL '24 hours'
-      `);
-      const { rows: topSignals } = await kenQ(`
-        SELECT market_id, direction, expected_edge, confidence, reasoning, risk_approved, created_at
-        FROM ken_strategy_signals
-        WHERE created_at > NOW() - INTERVAL '4 hours' AND risk_approved = true
-        ORDER BY ABS(expected_edge) DESC
-        LIMIT 10
-      `);
-      const { rows: sourceBreakdown } = await kenQ(`
-        SELECT source, COUNT(*) as cnt, AVG(sentiment_score) as avg_sent
-        FROM ken_sentiment
-        WHERE captured_at > NOW() - INTERVAL '1 hour'
-        GROUP BY source
-        ORDER BY cnt DESC
-        LIMIT 20
-      `);
-      json(res, { stats, topSignals, sourceBreakdown, cachedMarkets: cachedActiveMarkets.length });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // INTELLIGENCE FEED
-  // ════════════════════════════════════════
-
-  'GET /api/intelligence': async (req, res) => {
-    try {
-      const { rows: recent } = await kenQ(`
-        SELECT source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, captured_at
-        FROM ken_sentiment
-        ORDER BY captured_at DESC
-        LIMIT 100
-      `);
-      const { rows: sourceCounts } = await kenQ(`
-        SELECT source, COUNT(*) as cnt, AVG(sentiment_score) as avg_sent,
-               MAX(captured_at) as latest
-        FROM ken_sentiment
-        WHERE captured_at > NOW() - INTERVAL '4 hours'
-        GROUP BY source ORDER BY cnt DESC
-      `);
-      const { rows: [totals] } = await kenQ(`
-        SELECT
-          COUNT(*) as total_items,
-          COUNT(*) FILTER (WHERE captured_at > NOW() - INTERVAL '1 hour') as last_hour,
-          COUNT(*) FILTER (WHERE captured_at > NOW() - INTERVAL '4 hours') as last_4h,
-          AVG(sentiment_score) FILTER (WHERE captured_at > NOW() - INTERVAL '1 hour') as avg_sentiment_1h
-        FROM ken_sentiment
-      `);
-      json(res, { recent, sourceCounts, totals });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // PREDICTIONS FEED
-  // ════════════════════════════════════════
-
-  'GET /api/predictions': async (req, res) => {
-    try {
-      const { rows } = await kenQ(`
-        SELECT ticker, event_ticker, title, signal_type, side, confidence, entry_price, edge_bps, reasoning, sources, created_at
-        FROM ken_predictions
-        ORDER BY created_at DESC
-        LIMIT 100
-      `);
-      json(res, { predictions: rows });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ── Live Markets (all active with snapshots) ──
-  'GET /api/markets': async (req, res) => {
-    try {
-      const { rows } = await kenQ(`
-        SELECT DISTINCT ON (ticker) ticker, event_ticker, title, subtitle, category,
-          yes_bid, yes_ask, no_bid, no_ask, last_price, volume, volume_24h, open_interest, spread,
-          captured_at
-        FROM ken_market_snapshots
-        WHERE captured_at > NOW() - INTERVAL '1 hour'
-        ORDER BY ticker, captured_at DESC
-      `);
-      json(res, { markets: rows, count: rows.length, cached: cachedActiveMarkets.length });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ── Energy/LNG Arbitrage Opportunities ──
-  'GET /api/energy-arb': async (req, res) => {
-    try {
-      const arbs = await energyArbitrageScan(cachedActiveMarkets);
-      // Also get recent energy-related signals
-      const { rows: energySignals } = await kenQ(`
-        SELECT ticker, title, signal_type, side, confidence, entry_price, edge_bps, reasoning, sources, created_at
-        FROM ken_predictions
-        WHERE signal_type IN ('energy_arb', 'polymarket_arb')
-           OR reasoning ILIKE '%energy%' OR reasoning ILIKE '%oil%' OR reasoning ILIKE '%gas%' OR reasoning ILIKE '%lng%'
-        ORDER BY created_at DESC LIMIT 50
-      `);
-      json(res, {
-        arbitrage_opportunities: arbs,
-        energy_signals: energySignals,
-        scan_time: new Date().toISOString(),
-        markets_scanned: cachedActiveMarkets.length,
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ── All Active Signals (for trading) ──
-  'GET /api/signals': async (req, res) => {
-    try {
-      const { rows } = await kenQ(`
-        SELECT market_id as ticker, direction, expected_edge, confidence, reasoning, sources, risk_approved, created_at
-        FROM ken_strategy_signals
-        WHERE created_at > NOW() - INTERVAL '2 hours'
-        ORDER BY created_at DESC LIMIT 200
-      `);
-      json(res, { signals: rows, count: rows.length });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // DASHBOARD — aggregated paper portfolio stats
-  // ════════════════════════════════════════
-
-  'GET /api/dashboard': async (req, res) => {
-    try {
-      // Aggregate balance across all portfolios
-      const { rows: [balRow] } = await kenQ(`
-        SELECT SUM(balance_cents) as total_balance,
-               SUM(total_invested_cents) as total_invested,
-               SUM(total_returned_cents) as total_returned
-        FROM ken_portfolios
-      `);
-
-      // Seed money + economics
-      const totalBalance = Number(balRow?.total_balance || 0);
-      const totalInvested = Number(balRow?.total_invested || 0);
-      const totalReturned = Number(balRow?.total_returned || 0);
-      const seedMoney = totalBalance + totalInvested - totalReturned; // always 5000000 cents ($50K)
-      const netPnl = totalBalance - seedMoney; // how much above/below seed
-      const roi = seedMoney > 0 ? ((totalBalance - seedMoney) / seedMoney * 100) : 0;
-
-      // Daily spend breakdown (last 7 days)
-      const { rows: spendRows } = await kenQ(`
-        SELECT resolved_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 ABS(pnl_cents) ELSE 0 END) as gross_losses,
-               SUM(CASE WHEN pnl_cents > 0 THEN pnl_cents ELSE 0 END) as gross_wins,
-               SUM(cost_cents) as capital_deployed
-        FROM ken_portfolio_trades
-        WHERE status != 'open' AND resolved_at IS NOT NULL
-        GROUP BY resolved_at::date ORDER BY day DESC LIMIT 7
-      `);
-
-      // Daily budget across all portfolios
-      const { rows: [budgetRow] } = await kenQ(`
-        SELECT SUM(daily_budget_cents) as total_daily_budget,
-               COUNT(*) as num_portfolios
-        FROM ken_portfolios
-      `);
-
-      // Open position cost (capital currently at risk)
-      const { rows: [riskRow] } = await kenQ(`
-        SELECT COALESCE(SUM(cost_cents), 0) as capital_at_risk,
-               COUNT(*) as open_positions
-        FROM ken_portfolio_trades WHERE status = 'open'
-      `);
-
-      // Open position count + today's P&L
-      const { rows: [posRow] } = await kenQ(`
-        SELECT
-          COUNT(*) FILTER (WHERE status = 'open') as open_count,
-          COUNT(DISTINCT market_id) FILTER (WHERE status = 'open') as unique_markets,
-          COALESCE(SUM(pnl_cents) FILTER (WHERE status != 'open' AND resolved_at::date = CURRENT_DATE), 0) as daily_pnl,
-          COALESCE(SUM(pnl_cents) FILTER (WHERE status != 'open'), 0) as total_pnl
-        FROM ken_portfolio_trades
-      `);
-
-      // Recent fills (last 10 resolved trades)
-      const { rows: fills } = await kenQ(`
-        SELECT market_id as ticker, market_title, direction,
-               contracts, entry_price_cents, pnl_cents, status, resolved_at
-        FROM ken_portfolio_trades
-        WHERE status != 'open'
-        ORDER BY resolved_at DESC LIMIT 10
-      `);
-
-      // P&L history — daily totals for last 14 days
-      const { rows: pnlHistory } = await kenQ(`
-        SELECT resolved_at::date as day, SUM(pnl_cents) as daily_pnl
-        FROM ken_portfolio_trades
-        WHERE status != 'open' AND resolved_at > NOW() - INTERVAL '14 days'
-        GROUP BY resolved_at::date
-        ORDER BY day ASC
-      `);
-
-      // Top winning markets
-      const { rows: topMarkets } = await kenQ(`
-        SELECT market_id, market_title,
-               COUNT(*) as total_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 pnl_cents,
-               MODE() WITHIN GROUP (ORDER BY direction) as primary_direction,
-               ROUND(AVG(entry_price_cents)) as avg_entry
-        FROM ken_portfolio_trades
-        WHERE status != 'open'
-        GROUP BY market_id, market_title
-        ORDER BY SUM(pnl_cents) DESC
-        LIMIT 10
-      `);
-
-      // Bottom losing markets
-      const { rows: worstMarkets } = await kenQ(`
-        SELECT market_id, market_title,
-               COUNT(*) as total_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 pnl_cents,
-               MODE() WITHIN GROUP (ORDER BY direction) as primary_direction,
-               ROUND(AVG(entry_price_cents)) as avg_entry
-        FROM ken_portfolio_trades
-        WHERE status != 'open'
-        GROUP BY market_id, market_title
-        HAVING SUM(pnl_cents) < 0
-        ORDER BY SUM(pnl_cents) ASC
-        LIMIT 5
-      `);
-
-      // Top/bottom strategies
-      const { rows: strategies } = await kenQ(`
-        SELECT p.strategy, p.name,
-               p.trades_won, p.trades_lost,
-               (p.total_returned_cents - p.total_invested_cents) as net_pnl_cents,
-               p.balance_cents,
-               ROUND(100.0 * p.trades_won / NULLIF(p.trades_won + p.trades_lost, 0), 1) as win_rate
-        FROM ken_portfolios p
-        WHERE p.total_invested_cents > 0
-        ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC
-      `);
-
-      // Today's stats
-      const { rows: [todayRow] } = 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_portfolio_trades
-        WHERE status != 'open' AND resolved_at::date = CURRENT_DATE
-      `);
-
-      // ── Top 3 Best Opportunities Right Now ──
-      // Combine recent high-confidence signals with historical win rates and current market prices
-      const { rows: opportunities } = await kenQ(`
-        WITH recent_signals AS (
-          SELECT DISTINCT ON (market_id)
-            market_id, direction, expected_edge, confidence, reasoning, risk_approved, created_at
-          FROM ken_strategy_signals
-          WHERE created_at > NOW() - INTERVAL '6 hours'
-          ORDER BY market_id, expected_edge DESC
-        ),
-        historical AS (
-          SELECT market_id,
-            COUNT(*) as total_trades,
-            COUNT(*) FILTER (WHERE pnl_cents > 0) as wins,
-            SUM(pnl_cents) as pnl_cents,
-            MODE() WITHIN GROUP (ORDER BY direction) as primary_direction,
-            ROUND(AVG(entry_price_cents)) as avg_entry
-          FROM ken_portfolio_trades
-          WHERE resolved_at IS NOT NULL
-          GROUP BY market_id
-        ),
-        snapshots AS (
-          SELECT DISTINCT ON (ticker)
-            ticker, title, yes_bid, no_bid, volume_24h, last_price
-          FROM ken_market_snapshots
-          WHERE captured_at > NOW() - INTERVAL '2 hours'
-          ORDER BY ticker, captured_at DESC
-        )
-        SELECT
-          s.market_id, COALESCE(snap.title, s.market_id) as title,
-          s.direction, s.expected_edge, s.confidence, s.risk_approved,
-          COALESCE(h.total_trades, 0) as hist_trades,
-          COALESCE(h.wins, 0) as hist_wins,
-          COALESCE(h.pnl_cents, 0) as hist_pnl,
-          COALESCE(h.avg_entry, 0) as avg_entry,
-          snap.yes_bid, snap.no_bid, snap.volume_24h,
-          LEFT(s.reasoning, 200) as reasoning
-        FROM recent_signals s
-        LEFT JOIN historical h ON h.market_id = s.market_id
-        LEFT JOIN snapshots snap ON snap.ticker = s.market_id
-        WHERE s.confidence >= 0.5
-        ORDER BY
-          (s.expected_edge * s.confidence * (1 + COALESCE(h.wins::float / NULLIF(h.total_trades, 0), 0))) DESC
-        LIMIT 3
-      `);
-
-      function edgeDescription(m) {
-        const wr = Number(m.wins) / (Number(m.wins) + Number(m.losses) || 1);
-        const dir = m.primary_direction === 'BUY_NO' ? 'NO' : 'YES';
-        const avg = Number(m.avg_entry);
-        if (wr >= 0.99 && avg >= 90) return `${dir} at ${avg}¢ = near-certain`;
-        if (wr >= 0.95 && avg >= 85) return `${dir} at ${avg}¢, market overprices`;
-        if (wr >= 0.80) return `${dir} side, ${(wr*100).toFixed(0)}% win rate`;
-        if (Number(m.total_trades) >= 150) return `High volume, clear direction`;
-        if (avg >= 80) return `${dir} at ${avg}¢, steady edge`;
-        return `${dir} side, avg ${avg}¢ entry`;
-      }
-
-      json(res, {
-        env: KALSHI_ENV,
-        safe_mode: true,
-        balance: totalBalance,
-        seed_money: seedMoney,
-        net_pnl_from_seed: totalBalance - seedMoney,
-        roi_pct: parseFloat(roi.toFixed(2)),
-        total_invested: totalInvested,
-        total_returned: totalReturned,
-        daily_budget: Number(budgetRow?.total_daily_budget || 0),
-        num_portfolios: Number(budgetRow?.num_portfolios || 0),
-        capital_at_risk: Number(riskRow?.capital_at_risk || 0),
-        open_positions_cost: Number(riskRow?.open_positions || 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(posRow?.open_count || 0),
-        open_orders: Number(posRow?.unique_markets || 0),
-        daily_pnl: Number(posRow?.daily_pnl || 0),
-        total_pnl: Number(posRow?.total_pnl || 0),
-        markets_tracked: cachedActiveMarkets.length,
-        today_trades: Number(todayRow?.trades || 0),
-        today_wins: Number(todayRow?.wins || 0),
-        today_losses: Number(todayRow?.losses || 0),
-        recent_fills: fills.map(f => ({
-          ticker: f.ticker,
-          title: f.market_title,
-          side: f.direction === 'BUY_YES' ? 'yes' : 'no',
-          count: f.contracts,
-          yes_price: f.entry_price_cents,
-          pnl: f.pnl_cents,
-          status: f.status,
-        })),
-        pnl_history: pnlHistory.map(p => Number(p.daily_pnl) / 100),
-        top_markets: topMarkets.map(m => {
-          const t = m.market_id || '';
-          const et = t.replace(/-[^-]+$/, '') || t;
-          const st = et.replace(/-[^-]+$/, '') || et;
-          return {
-          ticker: m.market_id,
-          title: m.market_title,
-          trades: Number(m.total_trades),
-          wins: Number(m.wins),
-          losses: Number(m.losses),
-          pnl: Number(m.pnl_cents),
-          edge: edgeDescription(m),
-          url: `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}`,
-        }; }),
-        worst_markets: worstMarkets.map(m => {
-          const t = m.market_id || '';
-          const et = t.replace(/-[^-]+$/, '') || t;
-          const st = et.replace(/-[^-]+$/, '') || et;
-          return {
-          ticker: m.market_id,
-          title: m.market_title,
-          trades: Number(m.total_trades),
-          wins: Number(m.wins),
-          losses: Number(m.losses),
-          pnl: Number(m.pnl_cents),
-          edge: edgeDescription(m),
-          url: `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}`,
-        }; }),
-        strategies: strategies.map(s => ({
-          name: s.name,
-          strategy: s.strategy,
-          wins: Number(s.trades_won),
-          losses: Number(s.trades_lost),
-          pnl: Number(s.net_pnl_cents),
-          balance: Number(s.balance_cents),
-          win_rate: Number(s.win_rate || 0),
-        })),
-        top_opportunities: opportunities.map(o => {
-          const wr = Number(o.hist_trades) > 0 ? Number(o.hist_wins) / Number(o.hist_trades) : null;
-          const dir = o.direction === 'BUY_NO' ? 'NO' : 'YES';
-          const edge = (parseFloat(o.expected_edge) * 100).toFixed(1);
-          const conf = (parseFloat(o.confidence) * 100).toFixed(0);
-          const yesBid = Number(o.yes_bid || 0);
-          const noBid = Number(o.no_bid || 0);
-          const price = o.direction === 'BUY_NO' ? noBid : yesBid;
-          // Build human-readable "why" from signal reasoning
-          const rawReasoning = o.reasoning || '';
-          const reasoningClean = rawReasoning.replace(/^BUY_(YES|NO) on "[^"]*"\s*—\s*/, '');
-          return {
-            ticker: o.market_id,
-            title: o.title,
-            direction: dir,
-            edge_pct: edge,
-            confidence: conf,
-            price_cents: price,
-            hist_trades: Number(o.hist_trades),
-            hist_wins: Number(o.hist_wins),
-            hist_wr: wr !== null ? (wr * 100).toFixed(1) : null,
-            hist_pnl: Number(o.hist_pnl),
-            volume_24h: Number(o.volume_24h || 0),
-            risk_approved: o.risk_approved,
-            why: reasoningClean,
-            url: (() => { const t = o.market_id || ''; const et = t.replace(/-[^-]+$/, '') || t; const st = et.replace(/-[^-]+$/, '') || et; return `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}`; })(),
-          };
-        }),
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // SCAN CONFIG (User-adjustable intervals + risk params)
-  // ════════════════════════════════════════
-
-  'GET /api/scan-config': async (req, res) => {
-    json(res, {
-      scan_interval_ms: SCAN_INTERVAL_MS,
-      signal_pipeline_ms: SIGNAL_PIPELINE_MS,
-      reddit_interval_ms: REDDIT_INTERVAL_MS,
-      resolve_interval_ms: RESOLVE_INTERVAL_MS,
-      min_edge_threshold: RISK_LIMITS.minEdgeThreshold,
-      min_liquidity: RISK_LIMITS.minLiquidity,
-      cached_markets: cachedActiveMarkets.length,
-    });
-  },
-
-  'POST /api/scan-config': async (req, res) => {
-    try {
-      const body = await readBody(req);
-      let changed = false;
-      if (body.scan_interval_ms !== undefined) { SCAN_INTERVAL_MS = Math.max(60000, Math.min(3600000, Number(body.scan_interval_ms))); changed = true; }
-      if (body.signal_pipeline_ms !== undefined) { SIGNAL_PIPELINE_MS = Math.max(30000, Math.min(1800000, Number(body.signal_pipeline_ms))); changed = true; }
-      if (body.reddit_interval_ms !== undefined) { REDDIT_INTERVAL_MS = Math.max(60000, Math.min(3600000, Number(body.reddit_interval_ms))); changed = true; }
-      if (body.resolve_interval_ms !== undefined) { RESOLVE_INTERVAL_MS = Math.max(30000, Math.min(1800000, Number(body.resolve_interval_ms))); changed = true; }
-      if (body.min_edge_threshold !== undefined) { RISK_LIMITS.minEdgeThreshold = Math.max(0.005, Math.min(0.50, Number(body.min_edge_threshold))); changed = true; }
-      if (body.min_liquidity !== undefined) { RISK_LIMITS.minLiquidity = Math.max(0, Math.min(10000, Number(body.min_liquidity))); changed = true; }
-      if (changed) {
-        await saveScanConfig();
-        restartIntervals();
-      }
-      json(res, {
-        success: true,
-        scan_interval_ms: SCAN_INTERVAL_MS,
-        signal_pipeline_ms: SIGNAL_PIPELINE_MS,
-        reddit_interval_ms: REDDIT_INTERVAL_MS,
-        resolve_interval_ms: RESOLVE_INTERVAL_MS,
-        min_edge_threshold: RISK_LIMITS.minEdgeThreshold,
-        min_liquidity: RISK_LIMITS.minLiquidity,
-        cached_markets: cachedActiveMarkets.length,
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // MEMORY / SYSTEM STATUS
-  // ════════════════════════════════════════
-
-  'GET /api/system': async (req, res) => {
-    const mem = process.memoryUsage();
-    json(res, {
-      memory: {
-        rss_mb: Math.round(mem.rss / 1024 / 1024),
-        heapUsed_mb: Math.round(mem.heapUsed / 1024 / 1024),
-        heapTotal_mb: Math.round(mem.heapTotal / 1024 / 1024),
-        external_mb: Math.round(mem.external / 1024 / 1024),
-      },
-      uptime_hours: Math.round(process.uptime() / 3600 * 10) / 10,
-      env: KALSHI_ENV,
-      cachedMarkets: cachedActiveMarkets.length,
-      pid: process.pid,
-      cpuCores: os.cpus().length,
-      enhancements: {
-        monteCarloIterations: 15000,
-        geminiModel: GEMINI_MODEL,
-        geminiCallsToday: geminiCallCount,
-        geminiDailyLimit: GEMINI_DAILY_LIMIT,
-        scanInterval: '8m',
-        signalPipeline: '2m',
-        redditInterval: '15m',
-        momentumCacheSize: Object.keys(cachedMomentumMap).length,
-      },
-    });
-  },
-
-  // ── 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 = Math.max(0, pf.balance_cents); // Remaining = whatever balance is left
-
-        // 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;
-        pf.is_bankrupt = pf.balance_cents <= 0;
-      }
-      // Sort: active portfolios by ROI (best first), bankrupt portfolios at bottom
-      portfolios.sort((a, b) => {
-        if (a.is_bankrupt !== b.is_bankrupt) return a.is_bankrupt ? 1 : -1;
-        const aROI = a.total_invested_cents > 0 ? (a.total_returned_cents - a.total_invested_cents) / a.total_invested_cents : 0;
-        const bROI = b.total_invested_cents > 0 ? (b.total_returned_cents - b.total_invested_cents) / b.total_invested_cents : 0;
-        return bROI - aROI;
-      });
-      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); }
-  },
-
-  // ── PortFAUXlio Charts API — time-series NAV data ──
-  'GET /api/portfauxlio/charts': async (req, res) => {
-    try {
-      const url = new URL(req.url, 'http://x');
-      const period = url.searchParams.get('period') || '7d';
-      const intervals = { '1d': '1 day', '3d': '3 days', '7d': '7 days', '14d': '14 days', '30d': '30 days' };
-      const interval = intervals[period] || '7 days';
-
-      // Snapshots (intraday)
-      const { rows: snapRows } = await kenQ(`
-        SELECT s.portfolio_id, s.nav_cents, s.open_positions, s.snapshot_at, p.name, p.strategy
-        FROM ken_portfolio_snapshots s JOIN ken_portfolios p ON p.id = s.portfolio_id
-        WHERE s.snapshot_at > NOW() - INTERVAL '${interval}' ORDER BY s.portfolio_id, s.snapshot_at ASC
-      `);
-
-      // Daily P&L (longer-term)
-      const { rows: dailyRows } = await kenQ(`
-        SELECT dp.portfolio_id, dp.date, dp.daily_pnl_cents, dp.cumulative_pnl_cents,
-               dp.wins, dp.losses, dp.closed_trades, p.name, p.strategy
-        FROM ken_daily_pnl dp JOIN ken_portfolios p ON p.id = dp.portfolio_id
-        WHERE dp.date > CURRENT_DATE - INTERVAL '${interval}' ORDER BY dp.portfolio_id, dp.date ASC
-      `);
-
-      const byPortfolio = {};
-      for (const r of snapRows) {
-        if (!byPortfolio[r.portfolio_id]) byPortfolio[r.portfolio_id] = { id: r.portfolio_id, name: r.name, strategy: r.strategy, snapshots: [], daily: [] };
-        byPortfolio[r.portfolio_id].snapshots.push({ nav: r.nav_cents, openPositions: r.open_positions, time: r.snapshot_at });
-      }
-      for (const r of dailyRows) {
-        if (!byPortfolio[r.portfolio_id]) byPortfolio[r.portfolio_id] = { id: r.portfolio_id, name: r.name, strategy: r.strategy, snapshots: [], daily: [] };
-        byPortfolio[r.portfolio_id].daily.push({ date: r.date, pnl: r.daily_pnl_cents, cumulative: r.cumulative_pnl_cents, wins: r.wins, losses: r.losses, trades: r.closed_trades });
-      }
-
-      json(res, { portfolios: Object.values(byPortfolio), period });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  // ── PortFAUXlio SWOT — trades with strengths/weaknesses/opportunities/threats ──
-  'GET /api/portfauxlio/swot': async (req, res) => {
-    try {
-      const { rows: trades } = await kenQ(`
-        SELECT t.id, t.market_id, t.market_title, t.direction, t.entry_price_cents,
-               t.contracts, t.cost_cents, t.status, t.pnl_cents, t.reasoning,
-               t.confidence, t.ai_enhanced, t.mc_consistent, t.created_at, t.resolved_at,
-               p.name AS portfolio_name, p.strategy, p.id AS portfolio_id
-        FROM ken_portfolio_trades t JOIN ken_portfolios p ON p.id = t.portfolio_id
-        WHERE t.created_at > NOW() - INTERVAL '7 days'
-        ORDER BY t.created_at DESC LIMIT 200
-      `);
-
-      // Group by market
-      const byMarket = {};
-      for (const t of trades) {
-        if (!byMarket[t.market_id]) byMarket[t.market_id] = { market_id: t.market_id, title: t.market_title || t.market_id, trades: [], totalCost: 0, totalPnl: 0, models: new Set() };
-        const m = byMarket[t.market_id];
-        m.trades.push(t);
-        m.totalCost += parseInt(t.cost_cents) || 0;
-        m.totalPnl += parseInt(t.pnl_cents) || 0;
-        m.models.add(t.portfolio_name);
-      }
-
-      // Generate SWOT for each market group
-      const swotMarkets = Object.values(byMarket).map(m => {
-        const t0 = m.trades[0];
-        const conf = parseFloat(t0.confidence) || 0;
-        const aiCount = m.trades.filter(t => t.ai_enhanced).length;
-        const mcCount = m.trades.filter(t => t.mc_consistent).length;
-        const totalT = m.trades.length;
-        const openCount = m.trades.filter(t => t.status === 'open').length;
-        const reasoning = t0.reasoning || '';
-
-        // Parse edge from reasoning
-        const edgeMatch = reasoning.match(/edge\s*([\d.]+)%/i);
-        const edge = edgeMatch ? parseFloat(edgeMatch[1]) : 0;
-        const mcMatch = reasoning.match(/MC:\s*\u03bc=([\d.]+)%\s*\u00b1\s*([\d.]+)%/);
-        const mcMean = mcMatch ? parseFloat(mcMatch[1]) : 0;
-        const mcStd = mcMatch ? parseFloat(mcMatch[2]) : 0;
-        const artMatch = reasoning.match(/(\d+)\s*articles?/i);
-        const articles = artMatch ? parseInt(artMatch[1]) : 0;
-        const agreeMatch = reasoning.match(/Agreement:\s*([\d.]+)%/i);
-        const agreement = agreeMatch ? parseFloat(agreeMatch[1]) : 0;
-
-        // SWOT generation
-        const strengths = [];
-        const weaknesses = [];
-        const opportunities = [];
-        const threats = [];
-
-        // Strengths
-        if (conf >= 0.6) strengths.push('High confidence (' + Math.round(conf * 100) + '%)');
-        if (mcCount > totalT * 0.5) strengths.push('Monte Carlo validated (' + mcCount + '/' + totalT + ' models)');
-        if (aiCount > 0) strengths.push('AI enhanced signals');
-        if (totalT >= 3) strengths.push('Multi-model consensus (' + totalT + ' models agree)');
-        if (agreement >= 80) strengths.push('Strong news agreement (' + agreement + '%)');
-        if (articles >= 3) strengths.push('Strong news coverage (' + articles + ' articles)');
-        if (edge >= 15) strengths.push('Large statistical edge (' + edge.toFixed(1) + '%)');
-        if (strengths.length === 0) strengths.push('Active position in play');
-
-        // Weaknesses
-        if (conf < 0.4) weaknesses.push('Low confidence (' + Math.round(conf * 100) + '%)');
-        if (mcCount === 0) weaknesses.push('No Monte Carlo validation');
-        if (aiCount === 0) weaknesses.push('No AI enhancement');
-        if (articles <= 1) weaknesses.push('Limited news coverage (' + articles + ' article' + (articles === 1 ? '' : 's') + ')');
-        if (edge < 5 && edge > 0) weaknesses.push('Narrow edge (' + edge.toFixed(1) + '%)');
-        if (mcStd > 10) weaknesses.push('High MC uncertainty (\u00b1' + mcStd.toFixed(1) + '%)');
-        if (weaknesses.length === 0) weaknesses.push('No significant weaknesses identified');
-
-        // Opportunities
-        if (edge >= 8) opportunities.push('Exploitable edge of ' + edge.toFixed(1) + '%');
-        if (m.models.size >= 5) opportunities.push(m.models.size + ' models see opportunity');
-        if (mcMean > edge) opportunities.push('MC mean (' + mcMean.toFixed(1) + '%) exceeds raw edge');
-        if (openCount > 0) opportunities.push(openCount + ' open position' + (openCount > 1 ? 's' : '') + ' tracking');
-        if (opportunities.length === 0) opportunities.push('Market exposure maintained');
-
-        // Threats
-        if (mcStd > 8) threats.push('High volatility (MC \u00b1' + mcStd.toFixed(1) + '%)');
-        if (agreement < 60 && agreement > 0) threats.push('Mixed news sentiment (' + agreement + '% agreement)');
-        if (m.totalPnl < 0) threats.push('Currently underwater ($' + (Math.abs(m.totalPnl) / 100).toFixed(2) + ' loss)');
-        const direction = t0.direction;
-        if (direction === 'BUY_YES' && edge < 10) threats.push('Thin YES margin — small price move could flip');
-        if (direction === 'BUY_NO' && conf < 0.5) threats.push('NO position with uncertain conviction');
-        if (threats.length === 0) threats.push('Standard market risk');
-
-        return {
-          market_id: m.market_id,
-          title: m.title,
-          direction: t0.direction,
-          edge,
-          confidence: conf,
-          articles,
-          agreement,
-          mc_mean: mcMean,
-          mc_std: mcStd,
-          model_count: totalT,
-          models: [...m.models],
-          open_count: openCount,
-          total_cost: m.totalCost,
-          total_pnl: m.totalPnl,
-          entry_price: parseInt(t0.entry_price_cents) || 0,
-          first_trade: m.trades[m.trades.length - 1]?.created_at,
-          reasoning: reasoning.substring(0, 200),
-          swot: { strengths, weaknesses, opportunities, threats },
-          trades: m.trades.map(t => ({
-            id: t.id, portfolio: t.portfolio_name, strategy: t.strategy,
-            direction: t.direction, contracts: t.contracts,
-            cost: parseInt(t.cost_cents) || 0, pnl: parseInt(t.pnl_cents) || 0,
-            status: t.status, created_at: t.created_at
-          }))
-        };
-      });
-
-      // Sort by model count desc (most consensus first)
-      swotMarkets.sort((a, b) => b.model_count - a.model_count);
-
-      json(res, { markets: swotMarkets, total_trades: trades.length });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  // ── Weekly Tournament System — survive or die ──
-  'GET /api/tournament/current': async (req, res) => {
-    try {
-      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
-      if (!tourney) return json(res, { active: false, message: 'No active tournament' });
-      const { rows: entries } = await kenQ(`
-        SELECT e.*, p.name, p.strategy, p.balance_cents, p.total_invested_cents, p.total_returned_cents,
-               p.trades_won, p.trades_lost, p.daily_budget_cents
-        FROM ken_tournament_entries e JOIN ken_portfolios p ON p.id = e.portfolio_id
-        WHERE e.tournament_id = $1 ORDER BY e.pnl_cents DESC NULLS LAST
-      `, [tourney.id]);
-      // Compute live P&L for each entry
-      for (const e of entries) {
-        const { rows: [stats] } = await kenQ(`
-          SELECT COUNT(*) as trades, COALESCE(SUM(cost_cents),0) as total_cost,
-                 COALESCE(SUM(CASE WHEN status IN ('won','lost','closed') THEN pnl_cents ELSE 0 END),0) as realized_pnl
-          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at >= $2
-        `, [e.portfolio_id, tourney.week_start]);
-        e.live_trades = parseInt(stats.trades) || 0;
-        e.live_invested = parseInt(stats.total_cost) || 0;
-        e.live_pnl = parseInt(stats.realized_pnl) || 0;
-      }
-      json(res, { active: true, tournament: tourney, entries });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  'POST /api/tournament/start': async (req, res) => {
-    try {
-      const body = await readBody(req);
-      const portfolioIds = body.portfolio_ids || [];
-      const budgetCents = body.budget_cents || 2000; // $20 default
-      const notes = body.notes || '';
-      const weeks = body.weeks || 1;
-      if (portfolioIds.length < 1 || portfolioIds.length > 10) return json(res, { error: 'Pick 1-10 portfolios' }, 400);
-      // Check no active tournament
-      const { rows: active } = await kenQ(`SELECT id FROM ken_weekly_tournaments WHERE status = 'active'`);
-      if (active.length > 0) return json(res, { error: 'Tournament already active. Evaluate or cancel it first.', active_id: active[0].id }, 400);
-      const weekStart = new Date(); weekStart.setHours(0,0,0,0);
-      const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 7 * weeks);
-      const { rows: [tourney] } = await kenQ(`
-        INSERT INTO ken_weekly_tournaments (week_start, week_end, budget_per_portfolio_cents, notes)
-        VALUES ($1, $2, $3, $4) RETURNING *
-      `, [weekStart.toISOString().split('T')[0], weekEnd.toISOString().split('T')[0], budgetCents, notes]);
-      const entries = [];
-      for (const pid of portfolioIds) {
-        const { rows: [pf] } = await kenQ(`SELECT * FROM ken_portfolios WHERE id = $1`, [pid]);
-        if (!pf) continue;
-        // Set portfolio budget for the tournament
-        await kenQ(`UPDATE ken_portfolios SET daily_budget_cents = $1, balance_cents = $2 WHERE id = $3`, [Math.round(budgetCents / 7), budgetCents, pid]);
-        const { rows: [entry] } = await kenQ(`
-          INSERT INTO ken_tournament_entries (tournament_id, portfolio_id, start_balance_cents, status)
-          VALUES ($1, $2, $3, 'alive') RETURNING *
-        `, [tourney.id, pid, budgetCents]);
-        entries.push({ ...entry, name: pf.name, strategy: pf.strategy });
-      }
-      json(res, { tournament: tourney, entries, message: `Tournament started with ${entries.length} portfolios at $${(budgetCents/100).toFixed(2)} each` });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  'POST /api/tournament/evaluate': async (req, res) => {
-    try {
-      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
-      if (!tourney) return json(res, { error: 'No active tournament' }, 400);
-      const { rows: entries } = await kenQ(`
-        SELECT e.*, p.name, p.strategy FROM ken_tournament_entries e
-        JOIN ken_portfolios p ON p.id = e.portfolio_id WHERE e.tournament_id = $1
-      `, [tourney.id]);
-      const results = [];
-      for (const e of entries) {
-        const { rows: [stats] } = await kenQ(`
-          SELECT COUNT(*) as trades,
-                 COALESCE(SUM(CASE WHEN status='won' THEN payout_cents - cost_cents WHEN status='lost' THEN -cost_cents ELSE 0 END),0) as pnl
-          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at >= $2
-        `, [e.portfolio_id, tourney.week_start]);
-        const pnl = parseInt(stats.pnl) || 0;
-        const trades = parseInt(stats.trades) || 0;
-        const survived = pnl >= 0;
-        await kenQ(`UPDATE ken_tournament_entries SET end_balance_cents = $1, pnl_cents = $2, trades_count = $3, status = $4, elimination_reason = $5 WHERE id = $6`,
-          [e.start_balance_cents + pnl, pnl, trades, survived ? 'survived' : 'eliminated', survived ? null : `Lost $${(Math.abs(pnl)/100).toFixed(2)}`, e.id]);
-        if (!survived) await kenQ(`UPDATE ken_portfolios SET daily_budget_cents = 0 WHERE id = $1`, [e.portfolio_id]);
-        results.push({ name: e.name, strategy: e.strategy, pnl_cents: pnl, trades, status: survived ? 'SURVIVED' : 'ELIMINATED' });
-      }
-      await kenQ(`UPDATE ken_weekly_tournaments SET status = 'completed' WHERE id = $1`, [tourney.id]);
-      const survivors = results.filter(r => r.status === 'SURVIVED');
-      const eliminated = results.filter(r => r.status === 'ELIMINATED');
-      json(res, { tournament_id: tourney.id, results, survivors: survivors.length, eliminated: eliminated.length, message: `${survivors.length} survived, ${eliminated.length} eliminated` });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  'POST /api/tournament/cancel': async (req, res) => {
-    try {
-      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
-      if (!tourney) return json(res, { error: 'No active tournament' }, 400);
-      await kenQ(`UPDATE ken_weekly_tournaments SET status = 'cancelled' WHERE id = $1`, [tourney.id]);
-      await kenQ(`UPDATE ken_tournament_entries SET status = 'cancelled' WHERE tournament_id = $1`, [tourney.id]);
-      json(res, { message: 'Tournament cancelled', id: tourney.id });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  'GET /api/tournament/history': async (req, res) => {
-    try {
-      const { rows: tournaments } = await kenQ(`SELECT * FROM ken_weekly_tournaments ORDER BY id DESC LIMIT 20`);
-      for (const t of tournaments) {
-        const { rows: entries } = await kenQ(`
-          SELECT e.*, p.name, p.strategy FROM ken_tournament_entries e
-          JOIN ken_portfolios p ON p.id = e.portfolio_id WHERE e.tournament_id = $1 ORDER BY e.pnl_cents DESC NULLS LAST
-        `, [t.id]);
-        t.entries = entries;
-      }
-      json(res, tournaments);
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  // ── PortFAUXlio Dashboard API — full simulated trading view ──
-  'GET /api/portfauxlio': async (req, res) => {
-    try {
-      // 1. All portfolios with computed stats
-      const { rows: portfolios } = await kenQ(`
-        SELECT p.*,
-          (p.total_returned_cents - p.total_invested_cents) AS pnl_cents,
-          CASE WHEN p.total_invested_cents > 0
-            THEN ROUND((p.total_returned_cents - p.total_invested_cents)::numeric / p.total_invested_cents * 100, 1)
-            ELSE 0 END AS roi_pct,
-          (p.trades_won + p.trades_lost + p.trades_expired) AS resolved_trades,
-          CASE WHEN (p.trades_won + p.trades_lost) > 0
-            THEN ROUND(p.trades_won::numeric / (p.trades_won + p.trades_lost) * 100, 0)
-            ELSE 0 END AS win_rate
-        FROM ken_portfolios p ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC
-      `);
-
-      // 2. All active (open) trades across all portfolios
-      const { rows: activeTrades } = await kenQ(`
-        SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction,
-          t.contracts, t.entry_price_cents, t.cost_cents, t.confidence, t.created_at,
-          p.name AS portfolio_name, p.strategy
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        WHERE t.status = 'open'
-        ORDER BY t.created_at DESC
-      `);
-
-      // 3. Trade history (all trades, most recent first)
-      const { rows: tradeHistory } = await kenQ(`
-        SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction,
-          t.contracts, t.entry_price_cents, t.cost_cents, t.pnl_cents, t.status,
-          t.confidence, t.created_at, t.resolved_at,
-          p.name AS portfolio_name, p.strategy
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        ORDER BY t.created_at DESC LIMIT 200
-      `);
-
-      // 4. Per-model trade distribution: which markets each strategy traded
-      const { rows: modelMarkets } = await kenQ(`
-        SELECT p.strategy, t.market_id,
-          COUNT(*) AS trade_count,
-          SUM(t.cost_cents) AS total_cost,
-          MAX(t.created_at) AS last_trade
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        GROUP BY p.strategy, t.market_id
-        ORDER BY p.strategy, trade_count DESC
-      `);
-
-      // 5. Overlap analysis: markets traded by 3+ strategies
-      const { rows: overlaps } = await kenQ(`
-        SELECT t.market_id,
-          COUNT(DISTINCT p.strategy) AS num_strategies,
-          ARRAY_AGG(DISTINCT p.name ORDER BY p.name) AS portfolios,
-          SUM(t.cost_cents) AS total_exposure
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        GROUP BY t.market_id
-        HAVING COUNT(DISTINCT p.strategy) >= 2
-        ORDER BY num_strategies DESC
-      `);
-
-      // 6. Summary stats
-      const totalInvested = portfolios.reduce((s, p) => s + (parseInt(p.total_invested_cents) || 0), 0);
-      const totalReturned = portfolios.reduce((s, p) => s + (parseInt(p.total_returned_cents) || 0), 0);
-      const totalTrades = tradeHistory.length;
-      const openTrades = activeTrades.length;
-      const uniqueMarkets = [...new Set(tradeHistory.map(t => t.market_id))].length;
-
-      json(res, {
-        portfolios,
-        activeTrades,
-        tradeHistory,
-        modelMarkets,
-        overlaps,
-        summary: {
-          totalInvested, totalReturned,
-          totalPnl: totalReturned - totalInvested,
-          totalTrades, openTrades, uniqueMarkets,
-          activeModels: portfolios.filter(p => parseInt(p.total_invested_cents) > 0).length,
-        }
-      });
-    } 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, captured_at
-        FROM ken_sentiment
-        WHERE source IN ('heating_intel', 'noaa_alert', 'noaa_forecast')
-        ORDER BY captured_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 captured_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,
-      })),
-    });
-  },
-
-  // ── Market Data — stock-market style data points ──
-  'GET /api/inc/market-data': async (req, res) => {
-    const out = {};
-
-    // 1. Hourly signal volume — last 24h in 1-hour buckets
-    try {
-      const { rows } = await kenQ(`SELECT date_trunc('hour', created_at) AS hour,
-        COUNT(*) AS signals, COUNT(*) FILTER (WHERE risk_approved) AS approved,
-        AVG(ABS(expected_edge::numeric)) AS avg_edge,
-        AVG(confidence::numeric) AS avg_confidence
-        FROM ken_strategy_signals
-        WHERE created_at > NOW() - INTERVAL '24 hours'
-        GROUP BY 1 ORDER BY 1`);
-      out.signalTrend = rows.map(r => ({
-        hour: r.hour, signals: parseInt(r.signals), approved: parseInt(r.approved),
-        avgEdge: parseFloat(parseFloat(r.avg_edge || 0).toFixed(4)),
-        avgConf: parseFloat(parseFloat(r.avg_confidence || 0).toFixed(3)),
-      }));
-    } catch { out.signalTrend = []; }
-
-    // 2. Sentiment volume by hour — last 24h
-    try {
-      const { rows } = await kenQ(`SELECT date_trunc('hour', captured_at) AS hour,
-        COUNT(*) AS articles, AVG(sentiment_score) AS avg_sentiment,
-        COUNT(DISTINCT source) AS sources
-        FROM ken_sentiment
-        WHERE captured_at > NOW() - INTERVAL '24 hours'
-        GROUP BY 1 ORDER BY 1`);
-      out.sentimentTrend = rows.map(r => ({
-        hour: r.hour, articles: parseInt(r.articles),
-        avgSentiment: parseFloat(parseFloat(r.avg_sentiment || 0).toFixed(3)),
-        sources: parseInt(r.sources),
-      }));
-    } catch { out.sentimentTrend = []; }
-
-    // 3. Top sentiment sources with counts
-    try {
-      const { rows } = await kenQ(`SELECT source, COUNT(*) AS cnt,
-        AVG(sentiment_score) AS avg_sent
-        FROM ken_sentiment WHERE captured_at > NOW() - INTERVAL '24 hours'
-        GROUP BY source ORDER BY cnt DESC LIMIT 15`);
-      out.sourceMix = rows.map(r => ({
-        source: r.source, count: parseInt(r.cnt),
-        avgSentiment: parseFloat(parseFloat(r.avg_sent || 0).toFixed(3)),
-      }));
-    } catch { out.sourceMix = []; }
-
-    // 4. Top tickers / categories from sentiment
-    try {
-      const { rows } = await kenQ(`SELECT
-        COALESCE(relevance_ticker, 'general') AS category, COUNT(*) AS cnt
-        FROM ken_sentiment WHERE captured_at > NOW() - INTERVAL '24 hours'
-        AND relevance_ticker IS NOT NULL AND relevance_ticker != ''
-        GROUP BY relevance_ticker ORDER BY cnt DESC LIMIT 12`);
-      out.categories = rows.map(r => ({ category: r.category, count: parseInt(r.cnt) }));
-    } catch { out.categories = []; }
-
-    // 5. Portfolio P&L (current state) — compute actual PnL from invested vs returned
-    try {
-      const { rows } = await kenQ(`SELECT p.id, p.name, p.strategy, p.trades_won, p.trades_lost, p.trades_expired,
-        p.total_invested_cents, p.total_returned_cents, p.daily_budget_cents, p.streak,
-        p.best_trade_cents, p.worst_trade_cents, p.last_trade_at,
-        (p.total_returned_cents - p.total_invested_cents) AS pnl_cents,
-        CASE WHEN (p.trades_won + p.trades_lost) > 0
-          THEN ROUND(p.trades_won * 100.0 / (p.trades_won + p.trades_lost))
-          ELSE 0 END AS win_rate,
-        (SELECT COUNT(*) FROM ken_portfolio_trades t WHERE t.portfolio_id = p.id) AS total_trades
-        FROM ken_portfolios p ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC`);
-      out.portfolios = rows.map(r => ({
-        ...r,
-        pnl_cents: parseInt(r.pnl_cents) || 0,
-        win_rate: parseInt(r.win_rate) || 0,
-        total_trades: parseInt(r.total_trades) || 0,
-        total_invested_cents: parseInt(r.total_invested_cents) || 0,
-        total_returned_cents: parseInt(r.total_returned_cents) || 0,
-      }));
-    } catch { out.portfolios = []; }
-
-    // 6. Active markets count and top movers
-    out.activeMarkets = cachedActiveMarkets.length;
-    out.momentumKeys = Object.keys(cachedMomentumMap).length;
-
-    // 7. Signal approval rate over last 6 hours vs prior 6
-    try {
-      const { rows } = await kenQ(`SELECT
-        COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '6 hours') AS recent_total,
-        COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '6 hours' AND risk_approved) AS recent_approved,
-        COUNT(*) FILTER (WHERE created_at BETWEEN NOW() - INTERVAL '12 hours' AND NOW() - INTERVAL '6 hours') AS prior_total,
-        COUNT(*) FILTER (WHERE created_at BETWEEN NOW() - INTERVAL '12 hours' AND NOW() - INTERVAL '6 hours' AND risk_approved) AS prior_approved
-        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '12 hours'`);
-      const r = rows[0] || {};
-      const recentRate = r.recent_total > 0 ? (r.recent_approved / r.recent_total * 100) : 0;
-      const priorRate = r.prior_total > 0 ? (r.prior_approved / r.prior_total * 100) : 0;
-      out.approvalTrend = {
-        current: Math.round(recentRate), prior: Math.round(priorRate),
-        change: Math.round(recentRate - priorRate),
-        recentSignals: parseInt(r.recent_total), priorSignals: parseInt(r.prior_total),
-      };
-    } catch { out.approvalTrend = { current: 0, prior: 0, change: 0 }; }
-
-    // 8. Gemini usage
-    out.gemini = { callsToday: geminiCallCount, dailyLimit: GEMINI_DAILY_LIMIT, pctUsed: Math.round(geminiCallCount / GEMINI_DAILY_LIMIT * 100) };
-
-    // 9. Latest signals feed — most recent 15 signals for live ticker
-    try {
-      const { rows } = await kenQ(`SELECT market_id, direction, expected_edge, confidence, risk_approved, created_at,
-        SUBSTRING(reasoning FROM 1 FOR 80) AS reason_short
-        FROM ken_strategy_signals ORDER BY created_at DESC LIMIT 15`);
-      out.latestSignals = rows.map(r => ({
-        market: r.market_id, direction: r.direction,
-        edge: parseFloat(parseFloat(r.expected_edge || 0).toFixed(4)),
-        confidence: parseFloat(parseFloat(r.confidence || 0).toFixed(3)),
-        approved: r.risk_approved, reason: r.reason_short,
-        time: r.created_at,
-      }));
-    } catch { out.latestSignals = []; }
-
-    // 10. Per-market signal stats — heatmap data
-    try {
-      const { rows } = await kenQ(`SELECT market_id,
-        COUNT(*) AS total_signals,
-        AVG(expected_edge::numeric) AS avg_edge,
-        AVG(confidence::numeric) AS avg_conf,
-        COUNT(*) FILTER (WHERE risk_approved) AS approved,
-        MAX(created_at) AS last_signal
-        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'
-        GROUP BY market_id ORDER BY total_signals DESC LIMIT 20`);
-      out.marketStats = rows.map(r => ({
-        market: r.market_id, signals: parseInt(r.total_signals),
-        avgEdge: parseFloat(parseFloat(r.avg_edge || 0).toFixed(4)),
-        avgConf: parseFloat(parseFloat(r.avg_conf || 0).toFixed(3)),
-        approved: parseInt(r.approved), lastSignal: r.last_signal,
-      }));
-    } catch { out.marketStats = []; }
-
-    // 11. Recent trades with details
-    try {
-      const { rows } = await kenQ(`SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction, t.contracts,
-        t.entry_price_cents, t.pnl_cents, t.status, t.cost_cents, t.confidence, t.created_at,
-        p.name AS portfolio_name, p.strategy
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        ORDER BY t.created_at DESC LIMIT 20`);
-      out.recentTrades = rows.map(r => ({
-        ...r, entry_price_cents: parseInt(r.entry_price_cents) || 0, pnl_cents: parseInt(r.pnl_cents) || 0,
-        contracts: parseInt(r.contracts) || 0, cost_cents: parseInt(r.cost_cents) || 0,
-      }));
-    } catch { out.recentTrades = []; }
-
-    // 12. Confidence distribution histogram (10 buckets from 0-1)
-    try {
-      const { rows } = await kenQ(`SELECT
-        WIDTH_BUCKET(confidence::numeric, 0, 1, 10) AS bucket,
-        COUNT(*) AS cnt
-        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'
-        GROUP BY bucket ORDER BY bucket`);
-      out.confDistribution = Array.from({length: 10}, (_, i) => {
-        const found = rows.find(r => parseInt(r.bucket) === i + 1);
-        return { range: (i * 10) + '-' + ((i + 1) * 10) + '%', count: found ? parseInt(found.cnt) : 0 };
-      });
-    } catch { out.confDistribution = []; }
-
-    json(res, out);
-  },
-};
-
-// ════════════════════════════════════════
-// TOURNAMENT ARENA — 3D TEAM COMPETITION
-// ════════════════════════════════════════
-const ARENA_HTML = `<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>Tournament Arena | Ken Trading</title>
-<style>
-*{margin:0;padding:0;box-sizing:border-box}
-body{background:#000;overflow:hidden;font-family:'Segoe UI',system-ui,sans-serif;color:#fff}
-#c{position:fixed;top:0;left:0;z-index:0}
-#hud{position:fixed;top:0;left:0;right:0;z-index:10;pointer-events:none}
-.top-bar{display:flex;justify-content:center;align-items:center;padding:20px 40px;gap:30px}
-.team-badge{display:flex;align-items:center;gap:12px;padding:10px 24px;border-radius:12px;pointer-events:auto;cursor:default}
-.team-badge.alpha{background:rgba(99,102,241,.15);border:1px solid rgba(99,102,241,.4)}
-.team-badge.beta{background:rgba(244,63,94,.15);border:1px solid rgba(244,63,94,.4)}
-.team-badge h2{font-size:16px;letter-spacing:2px;text-transform:uppercase}
-.team-badge.alpha h2{color:#818cf8}
-.team-badge.beta h2{color:#fb7185}
-.vs{font-size:32px;font-weight:900;background:linear-gradient(135deg,#818cf8,#fb7185);-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-shadow:none;filter:drop-shadow(0 0 20px rgba(168,85,247,.5))}
-.scoreboard{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:10;display:flex;gap:8px;background:rgba(0,0,0,.7);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:12px 20px}
-.score-card{text-align:center;padding:6px 16px;border-radius:10px;min-width:100px}
-.score-card.alpha{background:rgba(99,102,241,.1);border:1px solid rgba(99,102,241,.2)}
-.score-card.beta{background:rgba(244,63,94,.1);border:1px solid rgba(244,63,94,.2)}
-.score-card .name{font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:4px}
-.score-card.alpha .name{color:#818cf8}
-.score-card.beta .name{color:#fb7185}
-.score-card .strat{font-size:9px;color:#666;margin-bottom:6px}
-.score-card .pnl{font-size:18px;font-weight:800;font-family:'Courier New',monospace}
-.score-card .pnl.pos{color:#4ade80}
-.score-card .pnl.neg{color:#f87171}
-.score-card .pnl.zero{color:#666}
-.score-card .trades{font-size:9px;color:#888;margin-top:2px}
-.score-card .bar{height:3px;border-radius:2px;margin-top:6px;background:#1a1a2e;overflow:hidden}
-.score-card .bar-fill{height:100%;border-radius:2px;transition:width .5s ease}
-.week-info{position:fixed;top:80px;left:50%;transform:translateX(-50%);z-index:10;font-size:11px;color:#555;letter-spacing:2px;text-transform:uppercase}
-.team-total{font-size:11px;font-weight:700;margin-top:4px;letter-spacing:1px}
-.back-link{position:fixed;top:20px;left:20px;z-index:20;color:#666;text-decoration:none;font-size:12px;pointer-events:auto}
-.back-link:hover{color:#fff}
-.refresh-note{position:fixed;bottom:4px;left:50%;transform:translateX(-50%);z-index:10;font-size:9px;color:#333}
-</style>
-</head>
-<body>
-<canvas id="c"></canvas>
-<a href="/inc" class="back-link">&larr; Command Center</a>
-<div id="hud">
-  <div class="top-bar">
-    <div class="team-badge alpha"><div style="width:12px;height:12px;border-radius:50%;background:#818cf8;box-shadow:0 0 12px #818cf8"></div><h2>Team Alpha</h2></div>
-    <div class="vs">VS</div>
-    <div class="team-badge beta"><div style="width:12px;height:12px;border-radius:50%;background:#fb7185;box-shadow:0 0 12px #fb7185"></div><h2>Team Beta</h2></div>
-  </div>
-</div>
-<div id="week-info" class="week-info">Loading tournament...</div>
-<div id="scoreboard" class="scoreboard"></div>
-<div class="refresh-note">Auto-refreshes every 15s</div>
-
-<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"><\/script>
-<script>
-const scene = new THREE.Scene();
-const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 1000);
-const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('c'), antialias: true, alpha: true });
-renderer.setSize(innerWidth, innerHeight);
-renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
-camera.position.set(0, 4, 12);
-camera.lookAt(0, 0, 0);
-
-// Lighting
-scene.add(new THREE.AmbientLight(0x222244, 0.5));
-const spotA = new THREE.SpotLight(0x6366f1, 2, 30, Math.PI/4);
-spotA.position.set(-6, 10, 5);
-scene.add(spotA);
-const spotB = new THREE.SpotLight(0xf43f5e, 2, 30, Math.PI/4);
-spotB.position.set(6, 10, 5);
-scene.add(spotB);
-const rimLight = new THREE.PointLight(0xa855f7, 1, 20);
-rimLight.position.set(0, 8, -5);
-scene.add(rimLight);
-
-// Arena floor
-const floorGeo = new THREE.CircleGeometry(8, 64);
-const floorMat = new THREE.MeshStandardMaterial({ color: 0x0a0a1a, metalness: 0.8, roughness: 0.3 });
-const floor = new THREE.Mesh(floorGeo, floorMat);
-floor.rotation.x = -Math.PI / 2;
-floor.position.y = -0.5;
-scene.add(floor);
-
-// Arena ring
-const ringGeo = new THREE.TorusGeometry(8, 0.05, 8, 128);
-const ringMat = new THREE.MeshStandardMaterial({ color: 0xa855f7, emissive: 0xa855f7, emissiveIntensity: 0.5 });
-const ring = new THREE.Mesh(ringGeo, ringMat);
-ring.rotation.x = -Math.PI / 2;
-ring.position.y = -0.48;
-scene.add(ring);
-
-// Center divider line
-const divGeo = new THREE.PlaneGeometry(0.02, 14);
-const divMat = new THREE.MeshBasicMaterial({ color: 0x333355, transparent: true, opacity: 0.3 });
-const divider = new THREE.Mesh(divGeo, divMat);
-divider.rotation.x = -Math.PI / 2;
-divider.position.y = -0.47;
-scene.add(divider);
-
-// Particle system for arena atmosphere
-const pCount = 500;
-const pGeo = new THREE.BufferGeometry();
-const pPos = new Float32Array(pCount * 3);
-const pCol = new Float32Array(pCount * 3);
-for (let i = 0; i < pCount; i++) {
-  const r = Math.random() * 10;
-  const a = Math.random() * Math.PI * 2;
-  pPos[i*3] = Math.cos(a) * r;
-  pPos[i*3+1] = Math.random() * 8 - 0.5;
-  pPos[i*3+2] = Math.sin(a) * r;
-  const isAlpha = pPos[i*3] < 0;
-  pCol[i*3] = isAlpha ? 0.38 : 0.96;
-  pCol[i*3+1] = isAlpha ? 0.39 : 0.24;
-  pCol[i*3+2] = isAlpha ? 0.95 : 0.37;
-}
-pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
-pGeo.setAttribute('color', new THREE.BufferAttribute(pCol, 3));
-const pMat = new THREE.PointsMaterial({ size: 0.04, vertexColors: true, transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending });
-const particles = new THREE.Points(pGeo, pMat);
-scene.add(particles);
-
-// Team node storage
-const teamNodes = [];
-const ALPHA_COLOR = 0x6366f1;
-const BETA_COLOR = 0xf43f5e;
-const TEAM_POSITIONS = {
-  alpha: [[-3.5, 1, -1.5], [-2, 1, 2], [-4.5, 1, 1]],
-  beta:  [[3.5, 1, -1.5], [2, 1, 2], [4.5, 1, 1]]
-};
-
-function createTeamNode(pos, color, name, strategy) {
-  const group = new THREE.Group();
-  // Main sphere
-  const geo = new THREE.IcosahedronGeometry(0.6, 2);
-  const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.3, metalness: 0.7, roughness: 0.2, wireframe: false });
-  const sphere = new THREE.Mesh(geo, mat);
-  group.add(sphere);
-  // Glow ring
-  const glowGeo = new THREE.TorusGeometry(0.9, 0.03, 8, 64);
-  const glowMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.5 });
-  const glowRing = new THREE.Mesh(glowGeo, glowMat);
-  glowRing.rotation.x = Math.PI / 2;
-  group.add(glowRing);
-  // Outer shell wireframe
-  const shellGeo = new THREE.IcosahedronGeometry(0.75, 1);
-  const shellMat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.15 });
-  const shell = new THREE.Mesh(shellGeo, shellMat);
-  group.add(shell);
-  // Name label (canvas texture)
-  const canvas = document.createElement('canvas');
-  canvas.width = 256; canvas.height = 80;
-  const ctx = canvas.getContext('2d');
-  ctx.fillStyle = '#fff';
-  ctx.font = 'bold 28px Segoe UI';
-  ctx.textAlign = 'center';
-  ctx.fillText(name, 128, 30);
-  ctx.font = '16px Segoe UI';
-  ctx.fillStyle = '#888';
-  ctx.fillText(strategy, 128, 58);
-  const tex = new THREE.CanvasTexture(canvas);
-  const labelGeo = new THREE.PlaneGeometry(2, 0.6);
-  const labelMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthTest: false });
-  const label = new THREE.Mesh(labelGeo, labelMat);
-  label.position.y = 1.5;
-  group.add(label);
-  // P&L indicator bar (will be updated)
-  const barBgGeo = new THREE.PlaneGeometry(1.2, 0.08);
-  const barBgMat = new THREE.MeshBasicMaterial({ color: 0x1a1a2e, transparent: true, opacity: 0.8 });
-  const barBg = new THREE.Mesh(barBgGeo, barBgMat);
-  barBg.position.y = 1.1;
-  group.add(barBg);
-  const barGeo = new THREE.PlaneGeometry(0.01, 0.06);
-  const barMat = new THREE.MeshBasicMaterial({ color: 0x4ade80, transparent: true, opacity: 0.9 });
-  const bar = new THREE.Mesh(barGeo, barMat);
-  bar.position.y = 1.1;
-  bar.position.x = -0.58;
-  group.add(bar);
-  group.position.set(...pos);
-  scene.add(group);
-  return { group, sphere, shell, glowRing, bar, barMat, mat, name, pnl: 0, trades: 0 };
-}
-
-// Energy beams connecting team members
-function createTeamBeam(nodeA, nodeB, color) {
-  const geo = new THREE.BufferGeometry();
-  const positions = new Float32Array(6);
-  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
-  const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.15, blending: THREE.AdditiveBlending });
-  const line = new THREE.Line(geo, mat);
-  scene.add(line);
-  return { line, nodeA, nodeB, geo };
-}
-const beams = [];
-
-// Data fetching
-let tournamentData = null;
-async function fetchData() {
-  try {
-    const r = await fetch('/api/tournament/current');
-    const d = await r.json();
-    if (!d.active) return;
-    tournamentData = d;
-    updateScene(d);
-    updateHUD(d);
-  } catch(e) { console.error('Fetch error:', e); }
-}
-
-function updateScene(d) {
-  const entries = d.entries || [];
-  // First 3 = alpha, next 3 = beta (sorted by entry id)
-  const sorted = [...entries].sort((a,b) => a.id - b.id);
-  const alphaEntries = sorted.slice(0, 3);
-  const betaEntries = sorted.slice(3, 6);
-  // Create nodes if not yet created
-  if (teamNodes.length === 0) {
-    alphaEntries.forEach((e, i) => {
-      const n = createTeamNode(TEAM_POSITIONS.alpha[i], ALPHA_COLOR, e.name, e.strategy);
-      n.entry = e;
-      n.team = 'alpha';
-      teamNodes.push(n);
-    });
-    betaEntries.forEach((e, i) => {
-      const n = createTeamNode(TEAM_POSITIONS.beta[i], BETA_COLOR, e.name, e.strategy);
-      n.entry = e;
-      n.team = 'beta';
-      teamNodes.push(n);
-    });
-    // Create beams within each team
-    for (let i = 0; i < 3; i++) for (let j = i+1; j < 3; j++) {
-      beams.push(createTeamBeam(teamNodes[i], teamNodes[j], ALPHA_COLOR));
-    }
-    for (let i = 3; i < 6; i++) for (let j = i+1; j < 6; j++) {
-      beams.push(createTeamBeam(teamNodes[i], teamNodes[j], BETA_COLOR));
-    }
-  }
-  // Update node data
-  const allEntries = [...alphaEntries, ...betaEntries];
-  allEntries.forEach((e, i) => {
-    if (!teamNodes[i]) return;
-    const n = teamNodes[i];
-    n.pnl = e.live_pnl || 0;
-    n.trades = e.live_trades || 0;
-    n.entry = e;
-    // Update emissive based on P&L
-    const intensity = Math.min(0.8, 0.3 + Math.abs(n.pnl) / 500);
-    n.mat.emissiveIntensity = intensity;
-    // Update P&L bar
-    const pnlPct = Math.max(0.01, Math.min(1, (n.pnl + 2000) / 4000));
-    n.bar.scale.x = pnlPct * 116;
-    n.bar.position.x = -0.58 + (pnlPct * 1.16) / 2;
-    n.barMat.color.setHex(n.pnl >= 0 ? 0x4ade80 : 0xf87171);
-  });
-}
-
-function updateHUD(d) {
-  const entries = d.entries || [];
-  const sorted = [...entries].sort((a,b) => a.id - b.id);
-  const t = d.tournament;
-  const wStart = new Date(t.week_start).toLocaleDateString('en-US', {month:'short',day:'numeric'});
-  const wEnd = new Date(t.week_end).toLocaleDateString('en-US', {month:'short',day:'numeric'});
-  document.getElementById('week-info').textContent = 'Week ' + t.id + ' • ' + wStart + ' - ' + wEnd + ' • $' + (t.budget_per_portfolio_cents/100).toFixed(0) + ' per portfolio';
-  let html = '';
-  let alphaTotal = 0, betaTotal = 0;
-  sorted.forEach((e, i) => {
-    const isAlpha = i < 3;
-    const pnl = e.live_pnl || 0;
-    if (isAlpha) alphaTotal += pnl; else betaTotal += pnl;
-    const pnlClass = pnl > 0 ? 'pos' : pnl < 0 ? 'neg' : 'zero';
-    const pnlStr = (pnl >= 0 ? '+' : '') + '$' + (pnl/100).toFixed(2);
-    const budgetUsed = Math.min(100, (e.live_invested || 0) / e.start_balance_cents * 100);
-    const barColor = isAlpha ? '#818cf8' : '#fb7185';
-    html += '<div class="score-card ' + (isAlpha?'alpha':'beta') + '">';
-    html += '<div class="name">' + e.name + '</div>';
-    html += '<div class="strat">' + e.strategy + '</div>';
-    html += '<div class="pnl ' + pnlClass + '">' + pnlStr + '</div>';
-    html += '<div class="trades">' + (e.live_trades||0) + ' trades • $' + ((e.live_invested||0)/100).toFixed(2) + ' deployed</div>';
-    html += '<div class="bar"><div class="bar-fill" style="width:' + budgetUsed + '%;background:' + barColor + '"></div></div>';
-    html += '</div>';
-    if (i === 2) {
-      html += '<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;padding:0 8px">';
-      html += '<div style="font-size:20px;font-weight:900;background:linear-gradient(135deg,#818cf8,#fb7185);-webkit-background-clip:text;-webkit-text-fill-color:transparent">VS</div>';
-      html += '</div>';
-    }
-  });
-  document.getElementById('scoreboard').innerHTML = html;
-}
-
-// Animation
-let time = 0;
-function animate() {
-  requestAnimationFrame(animate);
-  time += 0.01;
-  // Rotate camera slowly
-  camera.position.x = Math.sin(time * 0.15) * 12;
-  camera.position.z = Math.cos(time * 0.15) * 12;
-  camera.position.y = 4 + Math.sin(time * 0.3) * 0.5;
-  camera.lookAt(0, 0.5, 0);
-  // Animate nodes
-  teamNodes.forEach((n, i) => {
-    const base = n.group.position.y;
-    n.sphere.position.y = Math.sin(time * 2 + i) * 0.15;
-    n.sphere.rotation.y = time + i;
-    n.shell.rotation.y = -time * 0.5 + i;
-    n.shell.rotation.x = time * 0.3;
-    n.glowRing.rotation.z = time * (i % 2 === 0 ? 1 : -1);
-    // Pulse based on trades
-    const pulse = 1 + Math.sin(time * 3 + i * 2) * 0.05 * (n.trades > 0 ? 1 : 0.2);
-    n.sphere.scale.setScalar(pulse);
-  });
-  // Update beams
-  beams.forEach(b => {
-    const pa = b.nodeA.group.position;
-    const pb = b.nodeB.group.position;
-    const pos = b.geo.attributes.position.array;
-    pos[0] = pa.x; pos[1] = pa.y + 1; pos[2] = pa.z;
-    pos[3] = pb.x; pos[4] = pb.y + 1; pos[5] = pb.z;
-    b.geo.attributes.position.needsUpdate = true;
-    b.line.material.opacity = 0.1 + Math.sin(time * 2) * 0.05;
-  });
-  // Animate particles
-  const pp = particles.geometry.attributes.position.array;
-  for (let i = 0; i < pCount; i++) {
-    pp[i*3+1] += Math.sin(time + i) * 0.002;
-    if (pp[i*3+1] > 8) pp[i*3+1] = -0.5;
-  }
-  particles.geometry.attributes.position.needsUpdate = true;
-  particles.rotation.y = time * 0.02;
-  // Ring pulse
-  ring.material.emissiveIntensity = 0.3 + Math.sin(time * 2) * 0.2;
-  renderer.render(scene, camera);
-}
-
-// Resize
-addEventListener('resize', () => {
-  camera.aspect = innerWidth / innerHeight;
-  camera.updateProjectionMatrix();
-  renderer.setSize(innerWidth, innerHeight);
-});
-
-// Init
-fetchData();
-setInterval(fetchData, 15000);
-animate();
-<\/script>
-</body>
-</html>`;
-
-// 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>
-<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
-<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.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)}
-
-/* Sparkline */
-.sparkline-wrap{display:inline-block;vertical-align:middle}
-.sparkline-svg{display:block}
-
-/* ─── Market Data Terminal ─── */
-.mkt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;margin-top:16px}
-.mkt-card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;backdrop-filter:blur(10px);transition:all .3s;position:relative;overflow:hidden}
-.mkt-card::after{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--border),transparent);opacity:0;transition:opacity .3s}
-.mkt-card:hover{border-color:rgba(255,255,255,.12);transform:translateY(-2px);box-shadow:0 12px 40px rgba(0,0,0,.3)}
-.mkt-card:hover::after{opacity:1}
-.mkt-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
-.mkt-card-title{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
-.mkt-card-badge{padding:3px 8px;border-radius:6px;font-size:10px;font-weight:700;font-variant-numeric:tabular-nums}
-.mkt-bar-chart{display:flex;align-items:flex-end;gap:3px;height:80px;padding-top:4px}
-.mkt-bar{flex:1;border-radius:3px 3px 0 0;min-width:4px;transition:height .5s ease;position:relative}
-.mkt-bar:hover{opacity:1!important;filter:brightness(1.3)}
-.mkt-bar-label{position:absolute;top:-16px;left:50%;transform:translateX(-50%);font-size:8px;color:var(--muted);white-space:nowrap;display:none;background:var(--card);padding:1px 4px;border-radius:3px;border:1px solid var(--border)}
-.mkt-bar:hover .mkt-bar-label{display:block}
-.mkt-source-list{display:flex;flex-direction:column;gap:5px}
-.mkt-source-row{display:flex;align-items:center;gap:8px;font-size:11px;padding:2px 0;transition:background .2s;border-radius:4px}
-.mkt-source-row:hover{background:rgba(255,255,255,.03)}
-.mkt-source-bar{height:7px;border-radius:4px;transition:width .6s cubic-bezier(.4,0,.2,1)}
-.mkt-source-name{min-width:85px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-.mkt-source-val{font-variant-numeric:tabular-nums;color:var(--muted);min-width:45px;text-align:right;font-weight:600}
-.mkt-gauge{position:relative;width:100%;height:14px;background:rgba(255,255,255,.05);border-radius:7px;overflow:hidden;margin-top:8px}
-.mkt-gauge-fill{height:100%;border-radius:7px;transition:width .6s cubic-bezier(.4,0,.2,1)}
-.mkt-gauge-label{position:absolute;top:50%;right:8px;transform:translateY(-50%);font-size:9px;font-weight:700;color:var(--text);text-shadow:0 1px 2px rgba(0,0,0,.5)}
-/* Scrolling ticker */
-.mkt-ticker-strip{overflow:hidden;padding:12px 0;border-top:1px solid rgba(255,255,255,.06);border-bottom:1px solid rgba(255,255,255,.06);margin:16px 0;background:linear-gradient(90deg,rgba(5,5,16,.8),transparent 5%,transparent 95%,rgba(5,5,16,.8))}
-.mkt-ticker-track{display:flex;gap:24px;animation:tickerScroll 40s linear infinite;width:max-content}
-.mkt-ticker-track:hover{animation-play-state:paused}
-@keyframes tickerScroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
-.mkt-ticker-item{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums;padding:4px 12px;border-radius:6px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.04)}
-.mkt-change{font-size:11px;font-weight:700}
-.mkt-change.up{color:var(--green)}
-.mkt-change.down{color:var(--red)}
-.mkt-change.flat{color:var(--muted)}
-/* Signal feed */
-.signal-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto}
-.signal-feed::-webkit-scrollbar{width:4px}
-.signal-feed::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.signal-row{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;font-size:11px;transition:all .2s;border:1px solid transparent;animation:signalIn .3s ease}
-.signal-row:hover{background:rgba(255,255,255,.04);border-color:var(--border)}
-@keyframes signalIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
-.signal-dir{padding:2px 8px;border-radius:4px;font-weight:800;font-size:10px;text-transform:uppercase;letter-spacing:.5px}
-.signal-dir.yes{background:rgba(34,197,94,.15);color:var(--green)}
-.signal-dir.no{background:rgba(239,68,68,.15);color:var(--red)}
-.signal-market{font-weight:600;min-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-.signal-edge{font-variant-numeric:tabular-nums;font-weight:700;min-width:50px;text-align:right}
-.signal-conf{font-variant-numeric:tabular-nums;min-width:40px;text-align:right;color:var(--muted)}
-.signal-time{font-size:9px;color:var(--muted);margin-left:auto;white-space:nowrap}
-.signal-approved{width:6px;height:6px;border-radius:50%;flex-shrink:0}
-/* Heatmap grid */
-.heatmap-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px}
-.heatmap-cell{padding:8px 10px;border-radius:8px;border:1px solid var(--border);transition:all .2s;cursor:default;text-align:center}
-.heatmap-cell:hover{transform:scale(1.03);z-index:1;box-shadow:0 4px 16px rgba(0,0,0,.4)}
-.heatmap-market{font-size:10px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:2px}
-.heatmap-edge{font-size:16px;font-weight:800;font-variant-numeric:tabular-nums}
-.heatmap-meta{font-size:8px;color:var(--muted);margin-top:2px}
-/* Histogram */
-.histogram{display:flex;align-items:flex-end;gap:2px;height:80px}
-.hist-bar{flex:1;border-radius:3px 3px 0 0;transition:height .5s;position:relative;min-width:0}
-.hist-bar:hover{filter:brightness(1.3)}
-.hist-label{position:absolute;bottom:-16px;left:50%;transform:translateX(-50%);font-size:7px;color:var(--muted);white-space:nowrap}
-/* Trade log */
-.trade-log{display:flex;flex-direction:column;gap:3px;max-height:260px;overflow-y:auto}
-.trade-log::-webkit-scrollbar{width:4px}
-.trade-log::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.trade-row{display:flex;align-items:center;gap:6px;padding:5px 10px;border-radius:6px;font-size:10px;border:1px solid transparent;transition:all .2s}
-.trade-row:hover{background:rgba(255,255,255,.03);border-color:var(--border)}
-.trade-portfolio{font-weight:600;min-width:70px}
-.trade-pnl{font-weight:800;font-variant-numeric:tabular-nums;min-width:55px;text-align:right}
-/* P&L big number */
-.pnl-hero{font-size:48px;font-weight:900;font-variant-numeric:tabular-nums;letter-spacing:-1px;line-height:1}
-@keyframes pnlPulse{0%,100%{text-shadow:0 0 8px currentColor}50%{text-shadow:0 0 20px currentColor}}
-
-/* ─── PortFAUXlio Dashboard ─── */
-.pf-banner{text-align:center;padding:14px;margin-bottom:20px;background:linear-gradient(135deg,rgba(139,92,246,.08),rgba(255,105,180,.06));border:1px solid rgba(139,92,246,.2);border-radius:14px;position:relative;overflow:hidden}
-.pf-banner::before{content:'SIMULATED';position:absolute;top:-2px;right:20px;font-size:8px;letter-spacing:3px;color:var(--purple);background:var(--bg);padding:0 8px;border-radius:0 0 4px 4px;border:1px solid rgba(139,92,246,.2);border-top:none;font-weight:800}
-.pf-banner-title{font-size:22px;font-weight:900;background:linear-gradient(135deg,#8b5cf6,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:1px}
-.pf-banner-sub{font-size:10px;color:var(--muted);letter-spacing:2px;text-transform:uppercase;margin-top:4px}
-
-.pf-summary-row{display:flex;gap:10px;margin-bottom:18px;flex-wrap:wrap}
-.pf-summary-card{flex:1;min-width:100px;background:var(--card);border:1px solid var(--border);border-radius:12px;padding:14px;text-align:center}
-.pf-summary-val{font-size:22px;font-weight:800;font-variant-numeric:tabular-nums}
-.pf-summary-lbl{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
-
-/* Active Trades Strip */
-.pf-active-strip{margin-bottom:20px}
-.pf-active-header{display:flex;align-items:center;gap:8px;margin-bottom:10px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--green)}
-.pf-active-header .dot{width:8px;height:8px;border-radius:50%;background:var(--green);animation:statusPulse 2s ease infinite}
-.pf-active-scroll{display:flex;gap:8px;overflow-x:auto;padding-bottom:8px;scroll-behavior:smooth}
-.pf-active-scroll::-webkit-scrollbar{height:4px}
-.pf-active-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.pf-active-card{flex-shrink:0;min-width:200px;max-width:260px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 14px;transition:all .2s;border-top:2px solid var(--border)}
-.pf-active-card:hover{border-color:rgba(255,255,255,.15);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.3)}
-
-/* Model Leaderboard Table */
-.pf-leaderboard{margin-bottom:20px;border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--card);max-height:800px;overflow-y:auto}
-.pf-leaderboard::-webkit-scrollbar{width:4px}
-.pf-leaderboard::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.pf-lb-header{display:grid;grid-template-columns:32px 100px 90px 80px 70px 40px 40px 55px 80px 150px;gap:0;padding:10px 14px;background:rgba(255,255,255,.03);border-bottom:1px solid var(--border);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
-.pf-lb-header span[style*="cursor"]:hover{color:var(--cyan)}
-.pf-lb-row{display:grid;grid-template-columns:32px 100px 90px 80px 70px 40px 40px 55px 80px 150px;gap:0;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.03);font-size:11px;transition:all .2s;cursor:pointer;align-items:center}
-/* Chart Modal */
-.chart-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:1000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);animation:modalFadeIn .3s ease}
-.chart-modal-content{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:24px;width:92%;max-width:900px}
-@keyframes modalFadeIn{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}
-.chart-modal-header{display:flex;align-items:center;gap:10px;margin-bottom:16px}
-.chart-close{margin-left:auto;background:none;border:1px solid var(--border);color:var(--muted);border-radius:8px;padding:4px 12px;cursor:pointer;font-size:12px}
-.chart-close:hover{color:var(--text);border-color:var(--muted)}
-.chart-period-btns{display:flex;gap:4px;margin-left:auto}
-.chart-period,.chart-period-active{padding:3px 10px;border-radius:6px;font-size:10px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:transparent;color:var(--muted);transition:all .2s}
-.chart-period-active{border-color:var(--cyan);background:rgba(6,182,212,.1);color:var(--cyan)}
-.pf-lb-chart{display:flex;align-items:center;cursor:pointer;opacity:.85;transition:opacity .2s}
-.pf-lb-chart:hover{opacity:1}
-/* SWOT Trade Analysis */
-.swot-section{margin-bottom:20px}
-.swot-card{background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-bottom:12px;transition:all .2s}
-.swot-card:hover{border-color:rgba(255,255,255,.1)}
-.swot-card-header{display:flex;align-items:center;gap:10px;padding:14px 16px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.03)}
-.swot-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px 16px}
-.swot-quadrant{padding:10px;border-radius:10px;font-size:10px}
-.swot-quadrant-s{background:rgba(34,197,94,.06);border:1px solid rgba(34,197,94,.15)}
-.swot-quadrant-w{background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.15)}
-.swot-quadrant-o{background:rgba(59,130,246,.06);border:1px solid rgba(59,130,246,.15)}
-.swot-quadrant-t{background:rgba(245,158,11,.06);border:1px solid rgba(245,158,11,.15)}
-.swot-q-title{font-weight:800;font-size:9px;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
-.swot-q-item{padding:2px 0;color:var(--muted);line-height:1.4}
-.swot-q-item::before{content:'• '}
-.swot-models{display:flex;flex-wrap:wrap;gap:4px;padding:8px 16px 14px}
-.swot-model-tag{padding:2px 8px;border-radius:4px;font-size:9px;font-weight:600;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06)}
-.pf-lb-row:hover{background:rgba(255,255,255,.03)}
-.pf-lb-row:last-child{border-bottom:none}
-.pf-lb-rank{font-size:14px;font-weight:900;color:var(--muted)}
-.pf-lb-name{font-weight:700;display:flex;align-items:center;gap:6px;overflow:hidden}
-.pf-lb-pnl{font-weight:800;font-variant-numeric:tabular-nums}
-.pf-lb-invested{font-variant-numeric:tabular-nums;color:var(--muted)}
-.pf-lb-wr{font-weight:700;font-variant-numeric:tabular-nums}
-.pf-lb-wl{font-variant-numeric:tabular-nums;color:var(--muted)}
-.pf-lb-roi{font-weight:800;font-variant-numeric:tabular-nums;padding:2px 8px;border-radius:4px;text-align:center}
-.pf-lb-bar{height:4px;border-radius:2px;background:rgba(255,255,255,.05);overflow:hidden;margin-top:2px}
-.pf-lb-bar-fill{height:100%;border-radius:2px;transition:width .6s ease}
-
-/* Overlap Warning */
-.pf-overlap{margin-bottom:18px;background:rgba(245,158,11,.04);border:1px solid rgba(245,158,11,.15);border-radius:12px;padding:14px}
-.pf-overlap-title{font-size:11px;font-weight:700;color:var(--yellow);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;display:flex;align-items:center;gap:6px}
-
-/* Trade History */
-.pf-history{border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--card)}
-.pf-history-header{padding:14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border)}
-.pf-history-list{max-height:400px;overflow-y:auto}
-.pf-history-list::-webkit-scrollbar{width:4px}
-.pf-history-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.pf-history-row{display:flex;align-items:center;gap:8px;padding:8px 14px;font-size:11px;border-bottom:1px solid rgba(255,255,255,.02);transition:all .15s}
-.pf-history-row:hover{background:rgba(255,255,255,.03)}
-.pf-history-row:last-child{border-bottom:none}
-
-@media(max-width:900px){
-.pf-lb-header,.pf-lb-row{grid-template-columns:28px 80px 70px 70px 50px 40px 40px 50px 70px;font-size:10px;padding:8px 10px}
-.pf-active-card{min-width:180px}
-}
-
-/* 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 ? '✓' : '✗'}
-          </div>
-          <div className="signal-time">{timeAgo(s.time)}</div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-// ── PORTFOLIOS TAB ──
-function PortfoliosTab() {
-  const [data, setData] = useState(null);
-  const [histFilter, setHistFilter] = useState('all');
-  const [chartData, setChartData] = useState({});
-  const [selectedChart, setSelectedChart] = useState(null);
-  const [compareMode, setCompareMode] = useState(false);
-  const [compareIds, setCompareIds] = useState([]);
-  const [sortBy, setSortBy] = useState('pnl');
-  const [sortDir, setSortDir] = useState('desc');
-  useEffect(() => {
-    const load = () => api('/api/portfauxlio').then(setData).catch(e => console.error(e));
-    load();
-    const iv = setInterval(load, 15000);
-    return () => clearInterval(iv);
-  }, []);
-  useEffect(() => {
-    const loadCharts = () => api('/api/portfauxlio/charts?period=7d').then(d => {
-      const byId = {};
-      (d.portfolios || []).forEach(p => { byId[p.id] = p; });
-      setChartData(byId);
-    }).catch(() => {});
-    loadCharts();
-    const cv = setInterval(loadCharts, 120000);
-    return () => clearInterval(cv);
-  }, []);
-  const [swotData, setSwotData] = useState(null);
-  const [swotExpanded, setSwotExpanded] = useState({});
-  useEffect(() => {
-    const loadSwot = () => api('/api/portfauxlio/swot').then(setSwotData).catch(() => {});
-    loadSwot();
-    const sv = setInterval(loadSwot, 60000);
-    return () => clearInterval(sv);
-  }, []);
-
-  const stratColors = {aggressive:'#ef4444',conservative:'#3b82f6',weather:'#06b6d4',momentum:'#f59e0b',contrarian:'#8b5cf6',ai_enhanced:'#ec4899',mc_validated:'#a855f7',scalper:'#eab308',patient:'#22c55e',random:'#64748b',penny_pincher:'#94a3b8',spray_pray:'#fb923c',nickel_slots:'#a78bfa',degen_lite:'#f43f5e',politics_junkie:'#0ea5e9',double_down:'#dc2626',volatility_rider:'#14b8a6',whale:'#1d4ed8',yolo:'#e11d48',full_send:'#b91c1c',kelly_criterion:'#059669',sharpe_sniper:'#7c3aed',bayesian_blend:'#0891b2',ensemble_lock:'#c026d3',mean_revert:'#d97706',info_ratio:'#2563eb',anti_fomo:'#16a34a',nightcrawler:'#4c1d95',heatseeker:'#ea580c',quant_core:'#0d9488',etf_presidential:'#dc143c',etf_cabinet:'#ff6347',etf_congress:'#4169e1',etf_scotus:'#2f4f4f',etf_fed:'#228b22',etf_economy:'#daa520',etf_geopolitics:'#8b0000',etf_territory:'#006400',etf_climate:'#00bfff',etf_energy:'#ff8c00',etf_tech:'#7b68ee',etf_space:'#191970',etf_crypto:'#ffd700',etf_sports:'#32cd32',etf_entertainment:'#ff1493',etf_legal:'#708090',etf_financials:'#4682b4',etf_govreform:'#b22222',etf_global_social:'#9370db',etf_broad_market:'#c0a050'};
-  const stratEmojis = {aggressive:'🔥',conservative:'🏛️',weather:'⛈️',momentum:'🚀',contrarian:'🔄',ai_enhanced:'🧠',mc_validated:'🎲',scalper:'⚡',patient:'🐢',random:'🎰',penny_pincher:'🪙',spray_pray:'💦',nickel_slots:'🎰',degen_lite:'😈',politics_junkie:'🏛️',double_down:'⬆️',volatility_rider:'🏄',whale:'🐳',yolo:'🧨',full_send:'💣',kelly_criterion:'🎰',sharpe_sniper:'🎯',bayesian_blend:'🧮',ensemble_lock:'🔐',mean_revert:'↩️',info_ratio:'📊',anti_fomo:'🧘',nightcrawler:'🦇',heatseeker:'🔬',quant_core:'∑',etf_presidential:'🏛️',etf_cabinet:'💼',etf_congress:'🗳️',etf_scotus:'⚖️',etf_fed:'🏦',etf_economy:'📈',etf_geopolitics:'🌍',etf_territory:'🗺️',etf_climate:'🌡️',etf_energy:'⛽',etf_tech:'🤖',etf_space:'🚀',etf_crypto:'₿',etf_sports:'⚽',etf_entertainment:'🎬',etf_legal:'🔨',etf_financials:'💹',etf_govreform:'✂️',etf_global_social:'⛪',etf_broad_market:'🌐'};
-  const stratNames = {aggressive:'Alpha',conservative:'Beta',weather:'Storm',momentum:'Momentum',contrarian:'Contrarian',ai_enhanced:'Nova',mc_validated:'Oracle',scalper:'Shark',patient:'Turtle',random:'Wildcard',penny_pincher:'Penny',spray_pray:'Spray',nickel_slots:'Nickel',degen_lite:'Degen',politics_junkie:'Capitol',double_down:'Doubler',volatility_rider:'Rider',whale:'Moby',yolo:'YOLO',full_send:'Cannon',kelly_criterion:'Kelly',sharpe_sniper:'Sniper',bayesian_blend:'Bayes',ensemble_lock:'Voltron',mean_revert:'Revert',info_ratio:'Signal',anti_fomo:'Zen',nightcrawler:'Shadow',heatseeker:'Heatseek',quant_core:'Quant',etf_presidential:'PRES',etf_cabinet:'Cabinet',etf_congress:'Congress',etf_scotus:'SCOTUS',etf_fed:'Fed',etf_economy:'Economy',etf_geopolitics:'Geo',etf_territory:'Territory',etf_climate:'Climate',etf_energy:'Energy',etf_tech:'Tech',etf_space:'Space',etf_crypto:'Crypto',etf_sports:'Sports',etf_entertainment:'Showbiz',etf_legal:'Legal',etf_financials:'Finance',etf_govreform:'GovReform',etf_global_social:'Global',etf_broad_market:'Broad Mkt'};
-  const stratDesc = {aggressive:'High edge (>15%), low confidence — speculative swings',conservative:'High confidence (>70%) — quality over quantity',weather:'Weather/climate markets only',momentum:'High agreement + articles — follows the trend',contrarian:'BUY_NO only — bets against the crowd',ai_enhanced:'Gemini AI enhanced signals only',mc_validated:'Monte Carlo validated, tight spread',scalper:'Small edges (3-10%) — quantity plays',patient:'Needs everything: conf>80%, MC, 5+ articles',random:'20% random — control group for luck testing',penny_pincher:'Any edge >=1% — micro bets, max volume',spray_pray:'Random 50% of all signals — luck vs skill',nickel_slots:'Edge >=2% + conf >=20% — low bar catcher',degen_lite:'Edge >=5%, ignores confidence — pure edge',politics_junkie:'Political markets only — elections/congress',double_down:'Edge >=20% — goes big on massive edges',volatility_rider:'High MC stddev + edge — volatile markets',whale:'Edge >=25% + conf >=60% — rare massive sniper',yolo:'Edge >=30% — max degen, no filters',full_send:'Takes EVERY signal — pipeline profitability test',kelly_criterion:'Kelly fraction: edge/(1+edge) — bankroll math',sharpe_sniper:'Signal-to-noise ratio >2.0 — edge/stddev',bayesian_blend:'Bayesian posterior: conf * evidence * edge',ensemble_lock:'ALL must align: AI + MC + conf + edge + articles',mean_revert:'High volatility + moderate edge — reversion play',info_ratio:'(articles * agreement) / noise — quality intel',anti_fomo:'Small edge + high conf — calm, certain plays',nightcrawler:'BUY_NO + AI + MC — sophisticated shorts',heatseeker:'Multi-factor composite: 40e + 30c + 20a + 10n',quant_core:'Geometric mean sqrt(edge*conf) — pure math',etf_presidential:'Presidential election & 2028 race markets',etf_cabinet:'Cabinet confirmations & departures',etf_congress:'House, Senate & legislative markets',etf_scotus:'Supreme Court rulings & retirements',etf_fed:'Federal Reserve, rates & monetary policy',etf_economy:'GDP, jobs & economic indicators',etf_geopolitics:'Wars, treaties, sanctions & foreign affairs',etf_territory:'Greenland, statehood & territorial expansion',etf_climate:'Weather, warming & climate events',etf_energy:'LNG, oil, power grid & energy commodities',etf_tech:'AI, fusion, tech companies & semiconductors',etf_space:'SpaceX, NASA, Mars & Moon missions',etf_crypto:'Bitcoin, Ethereum & digital assets',etf_sports:'NBA, NFL, MLB & sports outcomes',etf_entertainment:'Celebrity, media, awards & pop culture',etf_legal:'Pardons, impeachment & legal proceedings',etf_financials:'Bonds, yields & financial markets',etf_govreform:'DOGE, agency cuts & government reform',etf_global_social:'Pope, referendums & global social movements',etf_broad_market:'Index fund — high-quality signals all sectors'};
-
-  if (!data) return <div style={{textAlign:'center',padding:60,color:'var(--muted)'}}>Loading portFAUXlio data...</div>;
-
-  const { portfolios: rawPortfolios, activeTrades, tradeHistory, overlaps, summary } = data;
-  const maxInvested = Math.max(...rawPortfolios.map(p => parseInt(p.total_invested_cents) || 0), 1);
-  const filteredHistory = histFilter === 'all' ? tradeHistory : tradeHistory.filter(t => t.strategy === histFilter);
-
-  const sortFns = {
-    pnl: p => parseInt(p.pnl_cents) || 0,
-    roi: p => parseFloat(p.roi_pct) || 0,
-    invested: p => parseInt(p.total_invested_cents) || 0,
-    trades: p => (parseInt(p.trades_won)||0) + (parseInt(p.trades_lost)||0) + (parseInt(p.trades_expired)||0),
-    wins: p => parseInt(p.trades_won) || 0,
-    name: p => (p.name || '').toLowerCase(),
-    strategy: p => (p.strategy || '').toLowerCase(),
-  };
-  const getSortVal = sortFns[sortBy] || sortFns.pnl;
-  const portfolios = [...rawPortfolios].sort((a, b) => {
-    const av = getSortVal(a), bv = getSortVal(b);
-    if (typeof av === 'string') return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
-    return sortDir === 'desc' ? bv - av : av - bv;
-  });
-  const toggleSort = (col) => { if (sortBy === col) setSortDir(d => d === 'desc' ? 'asc' : 'desc'); else { setSortBy(col); setSortDir('desc'); } };
-
-  return (<div>
-    {/* ═══ Banner ═══ */}
-    <div className="pf-banner">
-      <div className="pf-banner-title">portFAUXlio</div>
-      <div className="pf-banner-sub">50 Strategy Models &bull; Paper Trading &bull; Real Signals &bull; Zero Real Money</div>
-    </div>
-
-    {/* ═══ Summary Stats ═══ */}
-    <div className="pf-summary-row">
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color: summary.totalPnl >= 0 ? 'var(--green)' : 'var(--red)'}}>
-          {summary.totalPnl >= 0 ? '+' : ''}{'$'}{(summary.totalPnl / 100).toFixed(2)}
-        </div>
-        <div className="pf-summary-lbl">Total P&L</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--blue)'}}>{'$'}{(summary.totalInvested / 100).toFixed(2)}</div>
-        <div className="pf-summary-lbl">Invested</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--purple)'}}>{summary.totalTrades}</div>
-        <div className="pf-summary-lbl">Total Trades</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--green)'}}>{summary.openTrades}</div>
-        <div className="pf-summary-lbl">Open Positions</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--cyan)'}}>{summary.uniqueMarkets}</div>
-        <div className="pf-summary-lbl">Markets</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--yellow)'}}>{summary.activeModels}/{portfolios ? portfolios.length : 50}</div>
-        <div className="pf-summary-lbl">Active Models</div>
-      </div>
-    </div>
-
-    {/* ═══ Active Trades Strip ═══ */}
-    {activeTrades.length > 0 && <div className="pf-active-strip">
-      <div className="pf-active-header">
-        <span className="dot" />
-        {activeTrades.length} Open Positions
-      </div>
-      <div className="pf-active-scroll">
-        {activeTrades.map((t, i) => {
-          const c = stratColors[t.strategy] || 'var(--muted)';
-          return (<div key={i} className="pf-active-card" style={{borderTopColor: c}}>
-            <div style={{display:'flex',alignItems:'center',gap:6,marginBottom:4}}>
-              <span style={{fontSize:14}}>{stratEmojis[t.strategy] || ''}</span>
-              <span style={{fontSize:11,fontWeight:700,color:c}}>{t.portfolio_name}</span>
-              <span style={{marginLeft:'auto',padding:'1px 6px',borderRadius:4,fontSize:9,fontWeight:700,
-                background: t.direction === 'BUY_YES' ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)',
-                color: t.direction === 'BUY_YES' ? 'var(--green)' : 'var(--red)'
-              }}>{t.direction === 'BUY_YES' ? 'YES' : 'NO'}</span>
-            </div>
-            <div style={{fontSize:10,fontWeight:600,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',marginBottom:3}} title={t.market_title || t.market_id}>
-              {t.market_title || t.market_id}
-            </div>
-            <div style={{display:'flex',justifyContent:'space-between',fontSize:10,color:'var(--muted)'}}>
-              <span>{t.contracts}x @ {t.entry_price_cents}¢</span>
-              <span>{'$'}{(t.cost_cents / 100).toFixed(2)}</span>
-              <span>{timeAgo(t.created_at)}</span>
-            </div>
-          </div>);
-        })}
-      </div>
-    </div>}
-
-    {/* ═══ Model Leaderboard ═══ */}
-    <div style={{marginBottom:18}}>
-      <div style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,marginBottom:10,color:'var(--pink)',display:'flex',alignItems:'center',gap:8}}>
-        Model Leaderboard
-        <span style={{fontSize:9,fontWeight:400,color:'var(--muted)',textTransform:'none',letterSpacing:0}}>ranked by {sortBy}</span>
-        <div style={{marginLeft:'auto',display:'flex',gap:6}}>
-          {compareMode && compareIds.length >= 2 && <button onClick={() => setCompareMode('show')} style={{padding:'3px 10px',borderRadius:6,fontSize:9,fontWeight:700,background:'rgba(6,182,212,.15)',border:'1px solid var(--cyan)',color:'var(--cyan)',cursor:'pointer'}}>
-            Compare {compareIds.length} →
-          </button>}
-          <button onClick={() => { if (compareMode) { setCompareMode(false); setCompareIds([]); } else setCompareMode(true); }} style={{padding:'3px 10px',borderRadius:6,fontSize:9,fontWeight:700,background:compareMode?'rgba(168,85,247,.15)':'transparent',border:'1px solid '+(compareMode?'var(--purple)':'var(--border)'),color:compareMode?'var(--purple)':'var(--muted)',cursor:'pointer'}}>
-            {compareMode ? '✕ Cancel' : '📊 Compare'}
-          </button>
-        </div>
-      </div>
-      <div className="pf-leaderboard">
-        <div className="pf-lb-header">
-          <span>#</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('name')}>Model {sortBy==='name' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('pnl')}>P&L {sortBy==='pnl' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('invested')}>Invested {sortBy==='invested' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('roi')}>ROI {sortBy==='roi' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('wins')}>W {sortBy==='wins' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span>L</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('trades')}>Trades {sortBy==='trades' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('strategy')}>Strategy {sortBy==='strategy' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span>Chart</span>
-        </div>
-        {portfolios.map((p, i) => {
-          const c = stratColors[p.strategy] || 'var(--muted)';
-          const pnl = parseInt(p.pnl_cents) || 0;
-          const invested = parseInt(p.total_invested_cents) || 0;
-          const resolved = parseInt(p.resolved_trades) || 0;
-          const roi = parseFloat(p.roi_pct) || 0;
-          const totalT = (parseInt(p.trades_won) || 0) + (parseInt(p.trades_lost) || 0) + (parseInt(p.trades_expired) || 0);
-          const openCount = activeTrades.filter(t => t.portfolio_id === p.id).length;
-          const medals = ['🥇','🥈','🥉'];
-          const cd = chartData[p.id];
-          const miniPts = cd ? (cd.snapshots || []).map(s => s.nav / 100) : (cd?.daily || []).map(d => d.cumulative / 100);
-          return (<div key={p.id} className="pf-lb-row" style={compareMode && compareIds.includes(p.id) ? {background:'rgba(168,85,247,.08)',borderLeft:'3px solid var(--purple)'} : {}} onClick={() => {
-            if (compareMode) { setCompareIds(prev => prev.includes(p.id) ? prev.filter(x => x !== p.id) : [...prev, p.id]); }
-            else if (cd) setSelectedChart({id:p.id,strategy:p.strategy,name:p.name,color:c});
-          }}>
-            <span className="pf-lb-rank">{i < 3 ? medals[i] : (i+1)}</span>
-            <span className="pf-lb-name" style={{color:c}}>
-              <span style={{fontSize:16}}>{stratEmojis[p.strategy]||''}</span>
-              {p.name}
-            </span>
-            <span className="pf-lb-pnl" style={{color: pnl >= 0 ? 'var(--green)' : 'var(--red)'}}>
-              {pnl >= 0 ? '+' : ''}{'$'}{(pnl / 100).toFixed(2)}
-            </span>
-            <span className="pf-lb-invested">{'$'}{(invested / 100).toFixed(2)}</span>
-            <span className="pf-lb-roi" style={{
-              background: roi > 0 ? 'rgba(34,197,94,.12)' : roi < 0 ? 'rgba(239,68,68,.12)' : 'rgba(255,255,255,.04)',
-              color: roi > 0 ? 'var(--green)' : roi < 0 ? 'var(--red)' : 'var(--muted)'
-            }}>{roi > 0 ? '+' : ''}{roi}%</span>
-            <span className="pf-lb-wl" style={{color:'var(--green)'}}>{p.trades_won}</span>
-            <span className="pf-lb-wl" style={{color:'var(--red)'}}>{p.trades_lost}</span>
-            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>
-              {openCount > 0 ? <span>{openCount} <span style={{fontSize:8,color:'var(--green)'}}>OPEN</span></span> : <span style={{color:'var(--muted)'}}>0</span>}
-              {resolved > 0 && <span style={{marginLeft:4,fontSize:9}}>+{resolved}</span>}
-            </span>
-            <span style={{fontSize:9,color:'var(--muted)',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={stratDesc[p.strategy]}>
-              {p.strategy}
-            </span>
-            <span style={{display:'flex',alignItems:'center',justifyContent:'center'}}>
-              {miniPts && miniPts.length >= 2 ? <MiniChart data={miniPts} color={c} /> : <span style={{fontSize:9,color:'var(--muted)'}}>--</span>}
-            </span>
-          </div>);
-        })}
-      </div>
-    </div>
-
-    {/* ═══ Overlap Analysis ═══ */}
-    {overlaps.length > 0 && <div className="pf-overlap">
-      <div className="pf-overlap-title">
-        ⚠️ Strategy Overlap — Markets with 2+ models
-      </div>
-      <div style={{display:'flex',flexDirection:'column',gap:4}}>
-        {overlaps.slice(0, 8).map((o, i) => (
-          <div key={i} style={{display:'flex',alignItems:'center',gap:8,fontSize:10,padding:'4px 0'}}>
-            <span style={{fontWeight:700,color:'var(--yellow)',minWidth:20}}>{o.num_strategies}x</span>
-            <span style={{fontWeight:600,minWidth:180,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{o.market_id}</span>
-            <span style={{color:'var(--muted)',flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
-              {(o.portfolios || []).join(', ')}
-            </span>
-            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>{'$'}{((o.total_exposure || 0) / 100).toFixed(2)}</span>
-          </div>
-        ))}
-      </div>
-    </div>}
-
-    {/* ═══ SWOT Trade Intelligence ═══ */}
-    {swotData && swotData.markets && swotData.markets.length > 0 && <div className="swot-section">
-      <div style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,marginBottom:12,color:'var(--purple)',display:'flex',alignItems:'center',gap:8}}>
-        Trade Intelligence & SWOT
-        <span style={{fontSize:9,fontWeight:400,color:'var(--muted)',textTransform:'none',letterSpacing:0}}>{swotData.markets.length} active markets &bull; {swotData.total_trades} trades this week</span>
-      </div>
-      {swotData.markets.map((m, idx) => {
-        const isOpen = swotExpanded[m.market_id];
-        return (<div key={m.market_id} className="swot-card">
-          <div className="swot-card-header" onClick={() => setSwotExpanded(prev => ({...prev, [m.market_id]: !prev[m.market_id]}))}>
-            <span style={{fontSize:16,fontWeight:900,color:'var(--muted)',minWidth:24}}>{idx+1}</span>
-            <span style={{padding:'2px 8px',borderRadius:4,fontSize:9,fontWeight:800,
-              background: m.direction === 'BUY_YES' ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)',
-              color: m.direction === 'BUY_YES' ? 'var(--green)' : 'var(--red)'
-            }}>{m.direction === 'BUY_YES' ? 'YES' : 'NO'}</span>
-            <span style={{flex:1,fontWeight:700,fontSize:11,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={m.title}>{m.title}</span>
-            <span style={{fontSize:10,color:'var(--cyan)',fontWeight:700}}>{m.model_count} models</span>
-            <span style={{fontSize:10,fontWeight:700,color:m.edge >= 10 ? 'var(--green)' : 'var(--yellow)'}}>{m.edge.toFixed(1)}% edge</span>
-            <span style={{fontSize:10,fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>{'$'}{(m.total_cost / 100).toFixed(2)}</span>
-            <span style={{fontSize:10,fontWeight:700,color: m.open_count > 0 ? 'var(--green)' : 'var(--muted)'}}>{m.open_count > 0 ? m.open_count + ' OPEN' : 'CLOSED'}</span>
-            <span style={{fontSize:14,color:'var(--muted)',transform:isOpen?'rotate(180deg)':'rotate(0)',transition:'transform .2s'}}>▼</span>
-          </div>
-          {isOpen && <div>
-            <div className="swot-grid">
-              <div className="swot-quadrant swot-quadrant-s">
-                <div className="swot-q-title" style={{color:'var(--green)'}}>Strengths</div>
-                {m.swot.strengths.map((s, i) => <div key={i} className="swot-q-item">{s}</div>)}
-              </div>
-              <div className="swot-quadrant swot-quadrant-w">
-                <div className="swot-q-title" style={{color:'var(--red)'}}>Weaknesses</div>
-                {m.swot.weaknesses.map((w, i) => <div key={i} className="swot-q-item">{w}</div>)}
-              </div>
-              <div className="swot-quadrant swot-quadrant-o">
-                <div className="swot-q-title" style={{color:'var(--blue)'}}>Opportunities</div>
-                {m.swot.opportunities.map((o, i) => <div key={i} className="swot-q-item">{o}</div>)}
-              </div>
-              <div className="swot-quadrant swot-quadrant-t">
-                <div className="swot-q-title" style={{color:'var(--yellow)'}}>Threats</div>
-                {m.swot.threats.map((t, i) => <div key={i} className="swot-q-item">{t}</div>)}
-              </div>
-            </div>
-            <div style={{padding:'8px 16px',fontSize:10,color:'var(--muted)',lineHeight:1.5,borderTop:'1px solid rgba(255,255,255,.03)'}}>
-              <strong style={{color:'var(--text)'}}>Signal:</strong> {m.reasoning}
-            </div>
-            <div style={{padding:'4px 16px 6px',fontSize:10,color:'var(--muted)',display:'flex',gap:16,flexWrap:'wrap'}}>
-              <span>Conf: <strong style={{color:'var(--cyan)'}}>{Math.round(m.confidence * 100)}%</strong></span>
-              <span>Entry: <strong>{m.entry_price}¢</strong></span>
-              {m.mc_mean > 0 && <span>MC: <strong>{m.mc_mean.toFixed(1)}% ±{m.mc_std.toFixed(1)}%</strong></span>}
-              {m.articles > 0 && <span>News: <strong>{m.articles} articles</strong></span>}
-              {m.agreement > 0 && <span>Agree: <strong>{m.agreement}%</strong></span>}
-            </div>
-            <div className="swot-models">
-              {m.trades.map((t, i) => (
-                <span key={i} className="swot-model-tag" style={{color:stratColors[t.strategy]||'var(--muted)',borderColor:(stratColors[t.strategy]||'var(--border)')+'40'}}>
-                  {stratEmojis[t.strategy]||''} {t.portfolio} &bull; {t.contracts}x &bull; {t.status === 'open' ? 'OPEN' : (t.pnl >= 0 ? '+' : '') + '$' + (t.pnl / 100).toFixed(2)}
-                </span>
-              ))}
-            </div>
-          </div>}
-        </div>);
-      })}
-    </div>}
-
-    {/* ═══ Trade History ═══ */}
-    <div className="pf-history">
-      <div className="pf-history-header">
-        <span style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,color:'var(--cyan)'}}>Trade History</span>
-        <div style={{display:'flex',gap:4}}>
-          <button onClick={() => setHistFilter('all')} style={{padding:'3px 10px',borderRadius:6,border:'1px solid '+(histFilter==='all'?'var(--cyan)':'var(--border)'),background:histFilter==='all'?'rgba(6,182,212,.1)':'transparent',color:histFilter==='all'?'var(--cyan)':'var(--muted)',fontSize:10,fontWeight:600,cursor:'pointer'}}>All</button>
-          {portfolios.filter(p => parseInt(p.total_invested_cents) > 0).map(p => (
-            <button key={p.strategy} onClick={() => setHistFilter(p.strategy)} style={{padding:'3px 10px',borderRadius:6,border:'1px solid '+(histFilter===p.strategy?stratColors[p.strategy]:'var(--border)'),background:histFilter===p.strategy?stratColors[p.strategy]+'18':'transparent',color:histFilter===p.strategy?stratColors[p.strategy]:'var(--muted)',fontSize:10,fontWeight:600,cursor:'pointer'}}>
-              {stratEmojis[p.strategy] || ''} {p.name}
-            </button>
-          ))}
-        </div>
-      </div>
-      <div className="pf-history-list">
-        {filteredHistory.length === 0 && <div style={{textAlign:'center',padding:30,color:'var(--muted)',fontSize:12}}>No trades for this filter</div>}
-        {filteredHistory.slice(0, 100).map((t, i) => {
-          const c = stratColors[t.strategy] || 'var(--muted)';
-          const pnl = parseInt(t.pnl_cents) || 0;
-          return (<div key={i} className="pf-history-row">
-            <span style={{width:6,height:6,borderRadius:'50%',flexShrink:0,background:
-              t.status === 'open' ? 'var(--green)' : t.status === 'won' ? 'var(--blue)' : t.status === 'lost' ? 'var(--red)' : 'var(--muted)'
-            }} />
-            <span style={{minWidth:60,fontWeight:700,color:c,fontSize:10}}>{t.portfolio_name}</span>
-            <span style={{padding:'1px 6px',borderRadius:3,fontSize:9,fontWeight:700,
-              background: t.direction === 'BUY_YES' ? 'rgba(34,197,94,.12)' : 'rgba(239,68,68,.12)',
-              color: t.direction === 'BUY_YES' ? 'var(--green)' : 'var(--red)'
-            }}>{t.direction === 'BUY_YES' ? 'YES' : 'NO'}</span>
-            <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',color:'var(--text)'}} title={t.market_title || t.market_id}>
-              {t.market_title || t.market_id}
-            </span>
-            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:10}}>
-              {t.contracts}x @ {t.entry_price_cents}¢
-            </span>
-            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:10,minWidth:45,textAlign:'right'}}>
-              {'$'}{(t.cost_cents / 100).toFixed(2)}
-            </span>
-            <span style={{fontWeight:800,fontVariantNumeric:'tabular-nums',minWidth:55,textAlign:'right',fontSize:10,
-              color: t.status === 'open' ? 'var(--yellow)' : pnl >= 0 ? 'var(--green)' : 'var(--red)'
-            }}>
-              {t.status === 'open' ? 'OPEN' : (pnl >= 0 ? '+' : '') + (pnl / 100).toFixed(2)}
-            </span>
-            <span style={{fontSize:9,color:'var(--muted)',minWidth:55,textAlign:'right'}}>{timeAgo(t.created_at)}</span>
-          </div>);
-        })}
-      </div>
-    </div>
-
-    {/* ═══ FullChart Modal ═══ */}
-    {selectedChart && chartData[selectedChart.id] && <FullChart
-      chartData={chartData[selectedChart.id]}
-      portfolioId={selectedChart.id}
-      strategy={selectedChart.strategy}
-      name={selectedChart.name}
-      color={selectedChart.color || stratColors[selectedChart.strategy] || '#8b5cf6'}
-      emoji={stratEmojis[selectedChart.strategy] || ''}
-      desc={stratDesc[selectedChart.strategy] || ''}
-      onClose={() => setSelectedChart(null)}
-    />}
-
-    {/* ═══ Comparison Chart Modal ═══ */}
-    {compareMode === 'show' && compareIds.length >= 2 && <ComparisonChart
-      portfolioIds={compareIds}
-      allChartData={chartData}
-      stratColors={stratColors}
-      stratNames={stratNames}
-      stratEmojis={stratEmojis}
-      onClose={() => { setCompareMode(false); setCompareIds([]); }}
-    />}
-
-    {/* ═══ Strategy Guide ═══ */}
-    <div style={{marginTop:18,padding:16,background:'var(--card)',border:'1px solid var(--border)',borderRadius:14}}>
-      <div style={{fontSize:11,fontWeight:700,color:'var(--muted)',textTransform:'uppercase',letterSpacing:.5,marginBottom:10}}>Strategy Guide</div>
-      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(280px,1fr))',gap:6}}>
-        {Object.entries(stratDesc).map(([strat, desc]) => (
-          <div key={strat} style={{display:'flex',alignItems:'flex-start',gap:8,fontSize:10,padding:'4px 0',color:'var(--muted)'}}>
-            <span style={{fontSize:14,flexShrink:0}}>{stratEmojis[strat]||''}</span>
-            <div>
-              <span style={{fontWeight:700,color:stratColors[strat]}}>{stratNames[strat]}</span>
-              <span style={{margin:'0 4px'}}>—</span>
-              {desc}
-            </div>
-          </div>
-        ))}
-      </div>
-    </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>);
-}
-
-// ── Sparkline SVG Component ──
-function Sparkline({ data, width = 120, height = 30, color = '#8b5cf6', showDots = false, showArea = true, label }) {
-  if (!data || data.length < 2) return null;
-  const max = Math.max(...data);
-  const min = Math.min(...data);
-  const range = max - min || 1;
-  const gradId = 'grad-' + color.replace('#','') + '-' + width;
-  const points = data.map((v, i) => {
-    const x = (i / (data.length - 1)) * width;
-    const y = height - ((v - min) / range) * (height - 6) - 3;
-    return x + ',' + y;
-  }).join(' ');
-  const areaPoints = points + ' ' + width + ',' + height + ' 0,' + height;
-  const lastVal = data[data.length - 1];
-  const prevVal = data[data.length - 2];
-  const trending = lastVal >= prevVal;
-  return (
-    <div style={{position:'relative'}}>
-      <svg className="sparkline-svg" width={width} height={height} viewBox={'0 0 ' + width + ' ' + height}>
-        <defs><linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
-          <stop offset="0%" stopColor={color} stopOpacity="0.25"/><stop offset="100%" stopColor={color} stopOpacity="0.01"/>
-        </linearGradient></defs>
-        {showArea && <polygon points={areaPoints} fill={'url(#' + gradId + ')'} />}
-        <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
-        {showDots && data.map((v, i) => {
-          const x = (i / (data.length - 1)) * width;
-          const y = height - ((v - min) / range) * (height - 6) - 3;
-          return <circle key={i} cx={x} cy={y} r={i === data.length - 1 ? 3 : 1.5} fill={i === data.length - 1 ? color : 'transparent'} stroke={color} strokeWidth={i === data.length - 1 ? 0 : 0.5} opacity={i === data.length - 1 ? 1 : 0.5} />;
-        })}
-      </svg>
-      {label && <div style={{position:'absolute',top:2,right:4,fontSize:9,color:trending?'var(--green)':'var(--red)',fontWeight:700}}>
-        {trending?'▲':'▼'} {lastVal.toLocaleString()}
-      </div>}
-    </div>
-  );
-}
-
-// ── MiniChart: Canvas-based sparkline for leaderboard rows ──
-function MiniChart({ data, color, width = 140, height = 36 }) {
-  const canvasRef = React.useRef(null);
-  const chartRef = React.useRef(null);
-  React.useEffect(() => {
-    if (!data || data.length < 2 || !canvasRef.current) return;
-    if (chartRef.current) chartRef.current.destroy();
-    const ctx = canvasRef.current.getContext('2d');
-    const gradient = ctx.createLinearGradient(0, 0, 0, height);
-    gradient.addColorStop(0, color + '40');
-    gradient.addColorStop(1, color + '05');
-    chartRef.current = new Chart(ctx, {
-      type: 'line',
-      data: { labels: data.map((_, i) => i), datasets: [{ data, borderColor: color, borderWidth: 1.5, backgroundColor: gradient, fill: true, pointRadius: 0, tension: 0.4 }] },
-      options: { responsive: false, animation: { duration: 800, easing: 'easeOutQuart' }, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { display: false } } }
-    });
-    return () => { if (chartRef.current) chartRef.current.destroy(); };
-  }, [data, color]);
-  return <canvas ref={canvasRef} width={width} height={height} style={{borderRadius:4}} />;
-}
-
-// ── FullChart: Modal chart for a single portfolio with period selector ──
-function FullChart({ chartData: initialData, portfolioId, strategy, name, color, emoji, desc, onClose }) {
-  const canvasRef = React.useRef(null);
-  const chartRef = React.useRef(null);
-  const clr = color || '#8b5cf6';
-  const [period, setPeriod] = React.useState('7d');
-  const [liveData, setLiveData] = React.useState(initialData);
-  const periods = ['1d','3d','7d','14d','30d'];
-
-  React.useEffect(() => {
-    if (period === '7d') { setLiveData(initialData); return; }
-    api('/api/portfauxlio/charts?period=' + period).then(d => {
-      const match = (d.portfolios || []).find(p => p.id === portfolioId);
-      if (match) setLiveData(match);
-    }).catch(() => {});
-  }, [period, portfolioId]);
-
-  React.useEffect(() => {
-    const cd = liveData || initialData;
-    const snaps = cd?.snapshots || [];
-    const daily = cd?.daily || [];
-    const dataPoints = snaps.length > 1 ? snaps.map(s => ({ x: new Date(s.time), y: s.nav / 100 }))
-      : daily.length > 1 ? daily.map(d => ({ x: new Date(d.date), y: d.cumulative / 100 })) : [];
-    if (!dataPoints.length || !canvasRef.current) return;
-    if (chartRef.current) chartRef.current.destroy();
-    const ctx = canvasRef.current.getContext('2d');
-    const gradient = ctx.createLinearGradient(0, 0, 0, 300);
-    gradient.addColorStop(0, clr + '30');
-    gradient.addColorStop(1, clr + '02');
-    const zeroLine = { id: 'zeroLine', beforeDraw(chart) {
-      const yScale = chart.scales.y;
-      if (!yScale) return;
-      const y0 = yScale.getPixelForValue(0);
-      if (y0 < yScale.top || y0 > yScale.bottom) return;
-      const cctx = chart.ctx;
-      cctx.save();
-      cctx.strokeStyle = 'rgba(255,255,255,0.15)';
-      cctx.lineWidth = 1;
-      cctx.setLineDash([4, 4]);
-      cctx.beginPath();
-      cctx.moveTo(chart.chartArea.left, y0);
-      cctx.lineTo(chart.chartArea.right, y0);
-      cctx.stroke();
-      cctx.restore();
-    }};
-    chartRef.current = new Chart(ctx, {
-      type: 'line',
-      data: { datasets: [{ label: 'NAV', data: dataPoints, borderColor: clr, borderWidth: 2, backgroundColor: gradient, fill: true, pointRadius: dataPoints.length > 50 ? 0 : 2, pointHoverRadius: 6, pointBackgroundColor: clr, tension: 0.3 }] },
-      plugins: [zeroLine],
-      options: {
-        responsive: true, maintainAspectRatio: false,
-        animation: { duration: 1200, easing: 'easeInOutQuart' },
-        interaction: { intersect: false, mode: 'index' },
-        plugins: {
-          legend: { display: false },
-          tooltip: { backgroundColor: 'rgba(15,15,35,0.95)', borderColor: clr + '60', borderWidth: 1, titleColor: '#e2e8f0', bodyColor: '#e2e8f0', padding: 12, displayColors: false,
-            callbacks: { label: (tipCtx) => 'NAV: ' + (tipCtx.parsed.y >= 0 ? '+' : '') + '$' + tipCtx.parsed.y.toFixed(2) }
-          }
-        },
-        scales: {
-          x: { type: 'time', time: { unit: snaps.length > 1 ? 'hour' : 'day' }, ticks: { color: '#64748b', maxRotation: 45, font: { size: 9 } }, grid: { color: 'rgba(255,255,255,.03)' } },
-          y: { ticks: { color: '#64748b', callback: (v) => '$' + v.toFixed(0), font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }
-        }
-      }
-    });
-    return () => { if (chartRef.current) chartRef.current.destroy(); };
-  }, [liveData, strategy, clr]);
-  return (
-    <div className="chart-modal" onClick={onClose}>
-      <div className="chart-modal-content" onClick={e => e.stopPropagation()}>
-        <div className="chart-modal-header">
-          <span style={{fontSize:18}}>{emoji||''}</span>
-          <span style={{fontWeight:800,color:clr}}>{name || strategy}</span>
-          <span style={{fontSize:10,color:'var(--muted)',flex:1}}>{desc||''}</span>
-          <div className="chart-period-btns">
-            {periods.map(p => (
-              <button key={p} className={period === p ? 'chart-period-active' : 'chart-period'} onClick={() => setPeriod(p)}>{p}</button>
-            ))}
-          </div>
-          <button className="chart-close" onClick={onClose}>✕</button>
-        </div>
-        <div style={{height:340,position:'relative'}}><canvas ref={canvasRef} /></div>
-      </div>
-    </div>
-  );
-}
-
-// ── ComparisonChart: Overlay multiple portfolios on one chart ──
-function ComparisonChart({ portfolioIds, allChartData, stratColors, stratNames, stratEmojis, onClose }) {
-  const canvasRef = React.useRef(null);
-  const chartRef = React.useRef(null);
-  const [period, setPeriod] = React.useState('7d');
-  const [compareData, setCompareData] = React.useState(allChartData);
-  const periods = ['1d','3d','7d','14d','30d'];
-
-  React.useEffect(() => {
-    if (period === '7d') { setCompareData(allChartData); return; }
-    api('/api/portfauxlio/charts?period=' + period).then(d => {
-      const byId = {};
-      (d.portfolios || []).forEach(p => { byId[p.id] = p; });
-      setCompareData(byId);
-    }).catch(() => {});
-  }, [period]);
-
-  React.useEffect(() => {
-    if (!canvasRef.current) return;
-    if (chartRef.current) chartRef.current.destroy();
-    const datasets = portfolioIds.map(id => {
-      const cd = compareData[id];
-      if (!cd) return null;
-      const strat = cd.strategy || '';
-      const clr = stratColors[strat] || '#8b5cf6';
-      const snaps = cd.snapshots || [];
-      const daily = cd.daily || [];
-      const pts = snaps.length > 1 ? snaps.map(s => ({ x: new Date(s.time), y: s.nav / 100 }))
-        : daily.length > 1 ? daily.map(d => ({ x: new Date(d.date), y: d.cumulative / 100 })) : [];
-      if (pts.length < 2) return null;
-      return { label: (stratEmojis[strat]||'') + ' ' + (cd.name || strat), data: pts, borderColor: clr, borderWidth: 2, backgroundColor: 'transparent', fill: false, pointRadius: 0, pointHoverRadius: 5, tension: 0.3 };
-    }).filter(Boolean);
-    if (!datasets.length) return;
-    const ctx = canvasRef.current.getContext('2d');
-    chartRef.current = new Chart(ctx, {
-      type: 'line', data: { datasets },
-      options: {
-        responsive: true, maintainAspectRatio: false,
-        animation: { duration: 1000, easing: 'easeOutQuart' },
-        interaction: { intersect: false, mode: 'index' },
-        plugins: {
-          legend: { display: true, position: 'bottom', labels: { color: '#94a3b8', font: { size: 10 }, boxWidth: 12, padding: 8 } },
-          tooltip: { backgroundColor: 'rgba(15,15,35,0.95)', borderWidth: 1, titleColor: '#e2e8f0', bodyColor: '#e2e8f0', padding: 12 }
-        },
-        scales: {
-          x: { type: 'time', ticks: { color: '#64748b', maxRotation: 45, font: { size: 9 } }, grid: { color: 'rgba(255,255,255,.03)' } },
-          y: { ticks: { color: '#64748b', callback: (v) => '$' + v.toFixed(0), font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }
-        }
-      }
-    });
-    return () => { if (chartRef.current) chartRef.current.destroy(); };
-  }, [compareData, portfolioIds]);
-
-  return (
-    <div className="chart-modal" onClick={onClose}>
-      <div className="chart-modal-content" onClick={e => e.stopPropagation()} style={{maxWidth:1100}}>
-        <div className="chart-modal-header">
-          <span style={{fontSize:16}}>📊</span>
-          <span style={{fontWeight:800,color:'var(--cyan)'}}>Compare {portfolioIds.length} Portfolios</span>
-          <div className="chart-period-btns" style={{marginLeft:'auto'}}>
-            {periods.map(p => (
-              <button key={p} className={period === p ? 'chart-period-active' : 'chart-period'} onClick={() => setPeriod(p)}>{p}</button>
-            ))}
-          </div>
-          <button className="chart-close" onClick={onClose}>✕</button>
-        </div>
-        <div style={{height:400,position:'relative'}}><canvas ref={canvasRef} /></div>
-      </div>
-    </div>
-  );
-}
-
-// ── Vertical Bar Chart ──
-function BarChart({ data, maxVal, height = 80, color = '#8b5cf6' }) {
-  if (!data || data.length === 0) return null;
-  const mx = maxVal || Math.max(...data.map(d => d.value)) || 1;
-  return (
-    <div className="mkt-bar-chart" style={{ height }}>
-      {data.map((d, i) => (
-        <div key={i} className="mkt-bar" style={{ height: Math.max(2, (d.value / mx) * height) + 'px', background: d.color || color, opacity: 0.7 + (d.value / mx) * 0.3 }}>
-          <div className="mkt-bar-label">{d.label}: {d.value}</div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-// ── Confidence Histogram ──
-function ConfHistogram({ data }) {
-  if (!data || data.length === 0) return null;
-  const max = Math.max(...data.map(d => d.count)) || 1;
-  const colors = ['#ef4444','#f97316','#f59e0b','#eab308','#84cc16','#22c55e','#14b8a6','#06b6d4','#3b82f6','#8b5cf6'];
-  return (
-    <div className="histogram">
-      {data.map((d, i) => (
-        <div key={i} className="hist-bar" style={{
-          height: Math.max(2, (d.count / max) * 70) + 'px',
-          background: colors[i] || 'var(--muted)',
-          opacity: d.count > 0 ? 0.8 : 0.2,
-        }}>
-          <div className="hist-label">{d.range}</div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-// ── Time ago helper ──
-function timeAgo(ts) {
-  if (!ts) return '';
-  const diff = Date.now() - new Date(ts).getTime();
-  if (diff < 60000) return Math.floor(diff / 1000) + 's ago';
-  if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
-  if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
-  return Math.floor(diff / 86400000) + 'd ago';
-}
-
-// ── Market Data Tab — Bloomberg-style terminal ──
-function MarketDataTab() {
-  const [mkt, setMkt] = useState(null);
-  const [tick, setTick] = useState(0);
-
-  useEffect(() => {
-    const load = () => api('/api/inc/market-data').then(setMkt).catch(e => console.error(e));
-    load();
-    const iv = setInterval(load, 8000);
-    return () => clearInterval(iv);
-  }, []);
-  useEffect(() => { const iv = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(iv); }, []);
-
-  if (!mkt) return <div style={{textAlign:'center',padding:60,color:'var(--muted)'}}>
-    <div style={{fontSize:24,marginBottom:8,animation:'avatarFloat 2s ease-in-out infinite'}}>Loading market data...</div>
-  </div>;
-
-  const signalVals = (mkt.signalTrend || []).map(s => s.signals);
-  const edgeVals = (mkt.signalTrend || []).map(s => s.avgEdge * 100);
-  const sentVals = (mkt.sentimentTrend || []).map(s => s.articles);
-  const sentScoreVals = (mkt.sentimentTrend || []).map(s => s.avgSentiment);
-  const totalSent24h = sentVals.reduce((a, b) => a + b, 0);
-  const totalSignals = signalVals.reduce((a, b) => a + b, 0);
-  const at = mkt.approvalTrend || {};
-  const changeArrow = at.change > 0 ? '▲' : at.change < 0 ? '▼' : '—';
-  const changeClass = at.change > 0 ? 'up' : at.change < 0 ? 'down' : 'flat';
-  const totalPnl = (mkt.portfolios || []).reduce((s, p) => s + (p.pnl_cents || 0), 0);
-  const totalTrades = (mkt.portfolios || []).reduce((s, p) => s + (p.total_trades || 0), 0);
-  const avgEdge = edgeVals.length > 0 ? (edgeVals.reduce((a,b)=>a+b,0) / edgeVals.length) : 0;
-
-  // Ticker items
-  const tickerItems = [
-    { label: 'P&L', value: (totalPnl >= 0 ? '+' : '') + (totalPnl / 100).toFixed(2), color: totalPnl >= 0 ? 'var(--green)' : 'var(--red)' },
-    { label: 'SIGNALS', value: totalSignals, color: 'var(--blue)' },
-    { label: 'APPROVAL', value: at.current + '%', change: at.change, color: 'var(--green)' },
-    { label: 'SENTIMENT', value: totalSent24h.toLocaleString(), color: 'var(--pink)' },
-    { label: 'AVG EDGE', value: avgEdge.toFixed(1) + '%', color: 'var(--purple)' },
-    { label: 'TRADES', value: totalTrades, color: 'var(--yellow)' },
-    { label: 'MARKETS', value: mkt.activeMarkets, color: 'var(--cyan)' },
-    { label: 'GEMINI', value: mkt.gemini.callsToday + '/' + mkt.gemini.dailyLimit, color: 'var(--teal)' },
-  ];
-  const doubledTicker = tickerItems.concat(tickerItems);
-
-  const stratColors = {aggressive:'#ef4444',conservative:'#3b82f6',weather:'#06b6d4',momentum:'#f59e0b',contrarian:'#8b5cf6',ai_enhanced:'#ec4899',mc_validated:'#a855f7',scalper:'#eab308',patient:'#22c55e',random:'#64748b',penny_pincher:'#94a3b8',spray_pray:'#fb923c',nickel_slots:'#a78bfa',degen_lite:'#f43f5e',politics_junkie:'#0ea5e9',double_down:'#dc2626',volatility_rider:'#14b8a6',whale:'#1d4ed8',yolo:'#e11d48',full_send:'#b91c1c',kelly_criterion:'#059669',sharpe_sniper:'#7c3aed',bayesian_blend:'#0891b2',ensemble_lock:'#c026d3',mean_revert:'#d97706',info_ratio:'#2563eb',anti_fomo:'#16a34a',nightcrawler:'#4c1d95',heatseeker:'#ea580c',quant_core:'#0d9488',etf_presidential:'#dc143c',etf_cabinet:'#ff6347',etf_congress:'#4169e1',etf_scotus:'#2f4f4f',etf_fed:'#228b22',etf_economy:'#daa520',etf_geopolitics:'#8b0000',etf_territory:'#006400',etf_climate:'#00bfff',etf_energy:'#ff8c00',etf_tech:'#7b68ee',etf_space:'#191970',etf_crypto:'#ffd700',etf_sports:'#32cd32',etf_entertainment:'#ff1493',etf_legal:'#708090',etf_financials:'#4682b4',etf_govreform:'#b22222',etf_global_social:'#9370db',etf_broad_market:'#c0a050'};
-  const stratEmojis = {aggressive:'🔥',conservative:'🏛️',weather:'⛈️',momentum:'🚀',contrarian:'🔄',ai_enhanced:'🧠',mc_validated:'🎲',scalper:'⚡',patient:'🐢',random:'🎰',penny_pincher:'🪙',spray_pray:'💦',nickel_slots:'🎰',degen_lite:'😈',politics_junkie:'🏛️',double_down:'⬆️',volatility_rider:'🏄',whale:'🐳',yolo:'🧨',full_send:'💣',kelly_criterion:'🎰',sharpe_sniper:'🎯',bayesian_blend:'🧮',ensemble_lock:'🔐',mean_revert:'↩️',info_ratio:'📊',anti_fomo:'🧘',nightcrawler:'🦇',heatseeker:'🔬',quant_core:'∑',etf_presidential:'🏛️',etf_cabinet:'💼',etf_congress:'🗳️',etf_scotus:'⚖️',etf_fed:'🏦',etf_economy:'📈',etf_geopolitics:'🌍',etf_territory:'🗺️',etf_climate:'🌡️',etf_energy:'⛽',etf_tech:'🤖',etf_space:'🚀',etf_crypto:'₿',etf_sports:'⚽',etf_entertainment:'🎬',etf_legal:'🔨',etf_financials:'💹',etf_govreform:'✂️',etf_global_social:'⛪',etf_broad_market:'🌐'};
-
-  return (<div>
-    {/* ═══ Hero P&L Banner ═══ */}
-    <div style={{textAlign:'center',padding:'20px 0 8px',borderBottom:'1px solid var(--border)'}}>
-      <div style={{fontSize:10,color:'var(--muted)',textTransform:'uppercase',letterSpacing:2,marginBottom:4}}>Total Portfolio P&L</div>
-      <div className="pnl-hero" style={{color: totalPnl >= 0 ? 'var(--green)' : 'var(--red)', animation: totalPnl !== 0 ? 'pnlPulse 3s ease-in-out infinite' : 'none'}}>
-        {totalPnl >= 0 ? '+' : ''}{(totalPnl / 100).toFixed(2)}
-      </div>
-      <div style={{display:'flex',justifyContent:'center',gap:24,marginTop:8,fontSize:11,color:'var(--muted)'}}>
-        <span>{totalTrades} trades</span>
-        <span>{(mkt.portfolios||[]).filter(p => p.pnl_cents > 0).length} winning</span>
-        <span>{(mkt.portfolios||[]).filter(p => p.pnl_cents < 0).length} losing</span>
-        <span>{avgEdge.toFixed(1)}% avg edge</span>
-      </div>
-    </div>
-
-    {/* ═══ Scrolling Ticker ═══ */}
-    <div className="mkt-ticker-strip">
-      <div className="mkt-ticker-track">
-        {doubledTicker.map((t, i) => (
-          <div key={i} className="mkt-ticker-item">
-            <span style={{color:'var(--muted)',fontSize:9,textTransform:'uppercase',letterSpacing:.5}}>{t.label}</span>
-            <span style={{color:t.color,fontWeight:800}}>{t.value}</span>
-            {t.change !== undefined && <span className={'mkt-change ' + (t.change > 0 ? 'up' : t.change < 0 ? 'down' : 'flat')}>
-              {t.change > 0 ? '▲' : t.change < 0 ? '▼' : ''}{Math.abs(t.change)}%
-            </span>}
-          </div>
-        ))}
-      </div>
-    </div>
-
-    <div className="mkt-grid">
-      {/* ═══ Live Signal Feed ═══ */}
-      <div className="mkt-card" style={{gridColumn:'span 2'}}>
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--blue)'}}>
-            <span style={{display:'inline-block',width:6,height:6,borderRadius:'50%',background:'var(--green)',marginRight:6,animation:'statusPulse 2s ease infinite'}}></span>
-            Live Signal Feed
-          </span>
-          <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{(mkt.latestSignals||[]).length} latest</span>
-        </div>
-        <div className="signal-feed">
-          {(mkt.latestSignals || []).map((sig, i) => (
-            <div key={i} className="signal-row">
-              <div className="signal-approved" style={{background: sig.approved ? 'var(--green)' : 'var(--red)'}} title={sig.approved ? 'Approved' : 'Rejected'} />
-              <span className={'signal-dir ' + sig.direction}>{sig.direction}</span>
-              <span className="signal-market" title={sig.market}>{sig.market}</span>
-              <span className="signal-edge" style={{color: sig.edge > 0 ? 'var(--green)' : 'var(--red)'}}>
-                {sig.edge > 0 ? '+' : ''}{(sig.edge * 100).toFixed(1)}%
-              </span>
-              <span className="signal-conf" title="Confidence">{(sig.confidence * 100).toFixed(0)}%</span>
-              <span className="signal-time">{timeAgo(sig.time)}</span>
-            </div>
-          ))}
-          {(!mkt.latestSignals || mkt.latestSignals.length === 0) && <div style={{padding:20,textAlign:'center',color:'var(--muted)',fontSize:12}}>No recent signals</div>}
-        </div>
-      </div>
-
-      {/* ═══ Signal Volume Sparkline ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--blue)'}}>Signal Volume</span>
-          <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{totalSignals}</span>
-        </div>
-        <Sparkline data={signalVals} width={300} height={50} color="#3b82f6" showDots={true} label={true} />
-        <BarChart data={(mkt.signalTrend || []).map((s) => ({
-          value: s.signals,
-          label: new Date(s.hour).toLocaleTimeString('en-US', {hour:'numeric',hour12:true}),
-          color: s.approved > s.signals * 0.5 ? 'rgba(34,197,94,.6)' : 'rgba(239,68,68,.4)',
-        }))} height={45} />
-      </div>
-
-      {/* ═══ Sentiment Flow ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--pink)'}}>Sentiment Flow</span>
-          <span className="mkt-card-badge" style={{background:'rgba(236,72,153,.15)',color:'var(--pink)'}}>{totalSent24h.toLocaleString()}</span>
-        </div>
-        <Sparkline data={sentVals} width={300} height={50} color="#ec4899" label={true} />
-        <div style={{display:'flex',justifyContent:'space-between',marginTop:8,fontSize:10,color:'var(--muted)'}}>
-          <span>Avg: <strong style={{color: sentScoreVals.length > 0 && sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length > 0 ? 'var(--green)' : 'var(--red)'}}>
-            {sentScoreVals.length > 0 ? (sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length).toFixed(3) : '—'}
-          </strong></span>
-          <span>{(mkt.sourceMix||[]).length} sources</span>
-        </div>
-      </div>
-
-      {/* ═══ Approval Rate ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--green)'}}>Approval Rate</span>
-          <span className={'mkt-change ' + changeClass} style={{fontSize:14}}>{changeArrow} {Math.abs(at.change)}%</span>
-        </div>
-        <div style={{display:'flex',alignItems:'baseline',gap:12,marginBottom:8}}>
-          <span style={{fontSize:42,fontWeight:800,color:'var(--green)',fontVariantNumeric:'tabular-nums'}}>{at.current}%</span>
-          <span style={{fontSize:12,color:'var(--muted)'}}>vs {at.prior}% prior</span>
-        </div>
-        <div className="mkt-gauge">
-          <div className="mkt-gauge-fill" style={{width: at.current + '%', background:'linear-gradient(90deg,var(--green),var(--teal))'}} />
-          <div className="mkt-gauge-label">{at.recentSignals} signals</div>
-        </div>
-      </div>
-
-      {/* ═══ Edge Trend ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--purple)'}}>Edge Trend</span>
-          <span className="mkt-card-badge" style={{background:'rgba(139,92,246,.15)',color:'var(--purple)'}}>
-            {avgEdge.toFixed(1)}% avg
-          </span>
-        </div>
-        <Sparkline data={edgeVals} width={300} height={50} color="#8b5cf6" showDots={true} label={true} />
-      </div>
-
-      {/* ═══ Market Heatmap ═══ */}
-      {(mkt.marketStats || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Market Heatmap (24h)</span>
-          <span className="mkt-card-badge" style={{background:'rgba(245,158,11,.15)',color:'var(--yellow)'}}>{(mkt.marketStats||[]).length} markets</span>
-        </div>
-        <div className="heatmap-grid">
-          {(mkt.marketStats || []).map((m, i) => {
-            const edge = m.avgEdge * 100;
-            const intensity = Math.min(1, Math.abs(edge) / 25);
-            const bg = edge >= 0
-              ? 'rgba(34,197,94,' + (0.08 + intensity * 0.2) + ')'
-              : 'rgba(239,68,68,' + (0.08 + intensity * 0.2) + ')';
-            const borderC = edge >= 0 ? 'rgba(34,197,94,' + (0.2 + intensity * 0.3) + ')' : 'rgba(239,68,68,' + (0.2 + intensity * 0.3) + ')';
-            return (<div key={i} className="heatmap-cell" style={{background: bg, borderColor: borderC}}>
-              <div className="heatmap-market">{m.market}</div>
-              <div className="heatmap-edge" style={{color: edge >= 0 ? 'var(--green)' : 'var(--red)'}}>
-                {edge >= 0 ? '+' : ''}{edge.toFixed(1)}%
-              </div>
-              <div className="heatmap-meta">{m.signals} sig | {m.approved} appr | {(m.avgConf * 100).toFixed(0)}% conf</div>
-            </div>);
-          })}
-        </div>
-      </div>}
-
-      {/* ═══ Confidence Distribution ═══ */}
-      {(mkt.confDistribution || []).length > 0 && <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--cyan)'}}>Confidence Distribution</span>
-        </div>
-        <ConfHistogram data={mkt.confDistribution} />
-        <div style={{marginTop:18,display:'flex',justifyContent:'space-between',fontSize:9,color:'var(--muted)'}}>
-          <span>Low conf</span><span>High conf</span>
-        </div>
-      </div>}
-
-      {/* ═══ Intelligence Sources ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--cyan)'}}>Intelligence Sources</span>
-          <span className="mkt-card-badge" style={{background:'rgba(6,182,212,.15)',color:'var(--cyan)'}}>{(mkt.sourceMix||[]).length}</span>
-        </div>
-        <div className="mkt-source-list">
-          {(mkt.sourceMix || []).slice(0, 12).map((s, i) => {
-            const maxC = (mkt.sourceMix || [])[0]?.count || 1;
-            const colors = ['#3b82f6','#8b5cf6','#ec4899','#ef4444','#f59e0b','#22c55e','#06b6d4','#14b8a6','#6366f1','#d946ef','#f97316','#a855f7'];
-            return (<div key={i} className="mkt-source-row">
-              <span className="mkt-source-name">{s.source}</span>
-              <div style={{flex:1,display:'flex',alignItems:'center',gap:4}}>
-                <div className="mkt-source-bar" style={{width: (s.count / maxC * 100) + '%', background: colors[i % colors.length]}} />
-              </div>
-              <span className="mkt-source-val">{s.count.toLocaleString()}</span>
-              <span style={{fontSize:9,color: s.avgSentiment > 0 ? 'var(--green)' : s.avgSentiment < 0 ? 'var(--red)' : 'var(--muted)', minWidth:35, textAlign:'right'}}>
-                {s.avgSentiment > 0 ? '+' : ''}{s.avgSentiment.toFixed(2)}
-              </span>
-            </div>);
-          })}
-        </div>
-      </div>
-
-      {/* ═══ Gemini + Categories row ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--teal)'}}>Gemini AI</span>
-          <span className="mkt-card-badge" style={{background: mkt.gemini.pctUsed > 80 ? 'rgba(239,68,68,.15)' : 'rgba(20,184,166,.15)', color: mkt.gemini.pctUsed > 80 ? 'var(--red)' : 'var(--teal)'}}>
-            {mkt.gemini.pctUsed}%
-          </span>
-        </div>
-        <div style={{display:'flex',alignItems:'baseline',gap:8,marginBottom:8}}>
-          <span style={{fontSize:32,fontWeight:800,color:'var(--teal)',fontVariantNumeric:'tabular-nums'}}>{mkt.gemini.callsToday}</span>
-          <span style={{fontSize:11,color:'var(--muted)'}}>/ {mkt.gemini.dailyLimit}</span>
-        </div>
-        <div className="mkt-gauge">
-          <div className="mkt-gauge-fill" style={{width: mkt.gemini.pctUsed + '%', background: mkt.gemini.pctUsed > 80 ? 'linear-gradient(90deg,var(--yellow),var(--red))' : 'linear-gradient(90deg,var(--teal),var(--cyan))'}} />
-          <div className="mkt-gauge-label">{mkt.gemini.pctUsed}%</div>
-        </div>
-        {/* Categories inline */}
-        <div style={{marginTop:14,display:'flex',flexWrap:'wrap',gap:4}}>
-          {(mkt.categories || []).slice(0, 8).map((c, i) => {
-            const colors = ['#f59e0b','#3b82f6','#ec4899','#22c55e','#8b5cf6','#ef4444','#06b6d4','#14b8a6'];
-            return (<div key={i} style={{padding:'3px 10px',borderRadius:6,background:'rgba(255,255,255,.03)',border:'1px solid var(--border)',fontSize:9,display:'flex',alignItems:'center',gap:4}}>
-              <div style={{width:4,height:4,borderRadius:'50%',background:colors[i % colors.length]}} />
-              <span style={{fontWeight:600}}>{c.category}</span>
-              <span style={{color:'var(--muted)'}}>{c.count}</span>
-            </div>);
-          })}
-        </div>
-      </div>
-
-      {/* ═══ Recent Trades ═══ */}
-      {(mkt.recentTrades || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Recent Trades</span>
-          <span className="mkt-card-badge" style={{background:'rgba(245,158,11,.15)',color:'var(--yellow)'}}>{totalTrades} total</span>
-        </div>
-        <div className="trade-log">
-          {(mkt.recentTrades || []).map((t, i) => {
-            const c = stratColors[t.strategy] || 'var(--muted)';
-            return (<div key={i} className="trade-row">
-              <span className="trade-portfolio" style={{color:c}}>{t.portfolio_name}</span>
-              <span className={'signal-dir ' + t.direction} style={{fontSize:9}}>{t.direction}</span>
-              <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',color:'var(--muted)'}}>{t.market_id}</span>
-              <span style={{fontSize:10,color:'var(--muted)'}}>{t.contracts}x @ {t.entry_price_cents}¢</span>
-              <span className="trade-pnl" style={{color: t.pnl_cents >= 0 ? 'var(--green)' : 'var(--red)'}}>
-                {t.pnl_cents >= 0 ? '+' : ''}{(t.pnl_cents / 100).toFixed(2)}
-              </span>
-              <span className="signal-time">{timeAgo(t.created_at)}</span>
-            </div>);
-          })}
-        </div>
-      </div>}
-
-      {/* ═══ Portfolio Leaderboard ═══ */}
-      {(mkt.portfolios || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--pink)'}}>Portfolio Leaderboard</span>
-          <span className="mkt-card-badge" style={{background:'rgba(255,105,180,.15)',color:'var(--pink)'}}>{totalTrades} trades</span>
-        </div>
-        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(220px,1fr))',gap:8}}>
-          {(mkt.portfolios || []).map((p, i) => {
-            const c = stratColors[p.strategy] || 'var(--muted)';
-            const emoji = stratEmojis[p.strategy] || '';
-            const pnl = p.pnl_cents || 0;
-            const invested = p.total_invested_cents || 0;
-            const roi = invested > 0 ? ((pnl / invested) * 100).toFixed(1) : '0.0';
-            return (<div key={i} style={{padding:'12px 14px',background:'rgba(255,255,255,.02)',border:'1px solid var(--border)',borderRadius:10,transition:'all .3s',borderLeftColor:c,borderLeftWidth:3,position:'relative',overflow:'hidden'}}>
-              {/* Rank badge */}
-              <div style={{position:'absolute',top:6,right:8,fontSize:8,color:'var(--muted)',fontWeight:800}}>#{i+1}</div>
-              <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:6}}>
-                <span style={{fontSize:20}}>{emoji}</span>
-                <div style={{flex:1}}>
-                  <div style={{fontSize:13,fontWeight:700,color:c}}>{p.name}</div>
-                  <div style={{fontSize:9,color:'var(--muted)',textTransform:'uppercase',letterSpacing:.3}}>{p.strategy}</div>
-                </div>
-                <div style={{textAlign:'right'}}>
-                  <div style={{fontSize:18,fontWeight:900,color: pnl >= 0 ? 'var(--green)' : 'var(--red)',fontVariantNumeric:'tabular-nums'}}>
-                    {pnl >= 0 ? '+' : ''}{(pnl / 100).toFixed(2)}
-                  </div>
-                  <div style={{fontSize:9,color:'var(--muted)'}}>ROI: {roi}%</div>
-                </div>
-              </div>
-              <div style={{display:'flex',gap:4,fontSize:9,flexWrap:'wrap'}}>
-                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(34,197,94,.1)',color:'var(--green)'}}>W:{p.trades_won}</span>
-                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(239,68,68,.1)',color:'var(--red)'}}>L:{p.trades_lost}</span>
-                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(255,255,255,.04)',color:'var(--muted)'}}>WR:{p.win_rate}%</span>
-                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(255,255,255,.04)',color:'var(--muted)'}}>{p.total_trades} trades</span>
-                {p.streak !== 0 && <span style={{padding:'2px 7px',borderRadius:4,background: p.streak > 0 ? 'rgba(34,197,94,.1)' : 'rgba(239,68,68,.1)',color: p.streak > 0 ? 'var(--green)' : 'var(--red)'}}>{p.streak > 0 ? '+' : ''}{p.streak} streak</span>}
-              </div>
-              {invested > 0 && <div style={{marginTop:6}}>
-                <div style={{height:3,borderRadius:2,background:'rgba(255,255,255,.05)',overflow:'hidden'}}>
-                  <div style={{height:'100%',borderRadius:2,width: Math.min(100, Math.abs(parseFloat(roi))) + '%',background: pnl >= 0 ? 'var(--green)' : 'var(--red)',transition:'width .5s'}} />
-                </div>
-              </div>}
-            </div>);
-          })}
-        </div>
-      </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 || !data) 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('market')} onClick={()=>setTab('market')}>Market Data</button>
-        <button style={tabStyle('portfolios')} onClick={()=>setTab('portfolios')}>portFAUXlio</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 === 'market' && <MarketDataTab />}
-      {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; 50 paper portfolios &bull;
-        <a href="/"> Back to Dashboard</a>
-      </div>
-    </div>
-  );
-}
-
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
-</script>
-</body>
-</html>`;
-
-// ════════════════════════════════════════
-// HTTP SERVER
-// ════════════════════════════════════════
-
-const server = http.createServer(async (req, res) => {
-  // CORS preflight
-  if (req.method === 'OPTIONS') {
-    res.writeHead(200, {
-      'Access-Control-Allow-Origin': '*',
-      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
-      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
-    });
-    res.end();
-    return;
-  }
-
-  // Route key for matching
-  const urlPath = req.url.split('?')[0];
-  const routeKey = `${req.method} ${urlPath}`;
-
-  // Public endpoints (no auth required)
-  const publicPaths = [
-    '/api/auth/login',
-    '/api/auth/logout',
-    '/api/auth/session',
-    '/api/auth/google',
-    '/api/auth/google-config',
-    '/api/health',
-    '/api/portfauxlio',
-    '/api/portfauxlio/charts',
-    '/api/portfauxlio/swot',
-    '/api/markets',
-    '/api/signals',
-    '/api/energy-arb',
-    '/api/predictions',
-    '/api/portfolios',
-    '/api/scan-config',
-    '/api/dashboard',
-  ];
-  const isPublic = publicPaths.includes(urlPath);
-
-  // Enforce auth on all other API routes
-  if (!isPublic && urlPath.startsWith('/api/') && !isAuthenticated(req)) {
-    return json(res, { error: 'Authentication required' }, 401);
-  }
-
-  // Check route handlers
-  if (routes[routeKey]) {
-    try {
-      await routes[routeKey](req, res);
-    } catch (err) {
-      console.error(`[ERROR] ${routeKey}:`, err.message);
-      json(res, { error: err.message }, 500);
-    }
-    return;
-  }
-
-  // Unknown API route
-  if (urlPath.startsWith('/api/')) {
-    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;
-  }
-
-  // ── Tournament Arena — 3D Team Competition ──
-  if (urlPath === '/arena' || urlPath === '/arena/') {
-    res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
-    res.end(ARENA_HTML);
-    return;
-  }
-
-  // ── PortFAUXlios Dashboard ──
-  if (urlPath === '/portfauxlios' || urlPath === '/portfauxlios/') {
-    try {
-      const dashHtml = fs.readFileSync('/root/DW-Agents/ken-portfauxlios-dashboard.html', 'utf8');
-      res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
-      res.end(dashHtml);
-    } catch (e) {
-      res.writeHead(500);
-      res.end('Dashboard not found: ' + e.message);
-    }
-    return;
-  }
-
-  // ── Serve Static Files / SPA Routing ──
-  let filePath = path.join(DIST, urlPath === '/' ? 'index.html' : urlPath);
-
-  // Security: prevent directory traversal
-  if (!filePath.startsWith(DIST)) {
-    res.writeHead(403);
-    res.end('Forbidden');
-    return;
-  }
-
-  // Check if file exists; if not, serve index.html for SPA routing
-  if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
-    filePath = path.join(DIST, 'index.html');
-  }
-
-  try {
-    const content = fs.readFileSync(filePath);
-    const ext = path.extname(filePath);
-    const contentType = MIME[ext] || 'application/octet-stream';
-
-    // Cache static assets (hashed filenames from Vite)
-    const cacheHeader = ext === '.html'
-      ? 'no-cache, no-store, must-revalidate'
-      : 'public, max-age=31536000, immutable';
-
-    res.writeHead(200, {
-      'Content-Type': contentType,
-      'Cache-Control': cacheHeader,
-    });
-    res.end(content);
-  } catch {
-    res.writeHead(404);
-    res.end('Not found');
-  }
-});
-
-// ══════════════════════════════════════════════════════════════
-// KEN AUTONOMOUS TRADING BRAIN — 24/7 MARKET INTELLIGENCE
-// Scans every 15 min: Kalshi markets, Reddit, news, Polymarket
-// Stores everything in PostgreSQL, tracks own performance
-// ══════════════════════════════════════════════════════════════
-
-const KEN_DB = 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
-const kenPool = new pg.Pool({ connectionString: KEN_DB, max: 20, idleTimeoutMillis: 30000 });
-async function kenQ(text, params) { return kenPool.query(text, params); }
-
-// ── Mutable scan/pipeline intervals (adjustable via Settings UI) ──
-let SCAN_INTERVAL_MS = 8 * 60 * 1000; // 8 minutes — market scans
-let REDDIT_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes — intelligence sweep
-let SIGNAL_PIPELINE_MS = 2 * 60 * 1000; // 2 minutes — signal pipeline
-let RESOLVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes — trade resolution
-const SLACK_COOLDOWN_MS = 4 * 60 * 60 * 1000;
-let lastSlackTime = 0;
-let cachedActiveMarkets = [];
-let cachedMomentumMap = {};
-
-// Interval handles (stored so we can clear + reset when config changes)
-let scanIntervalId = null;
-let redditIntervalId = null;
-let signalIntervalId = null;
-let resolveIntervalId = null;
-
-// Load scan config from DB on startup
-async function loadScanConfig() {
-  try {
-    await initConfigTable();
-    const { rows } = await kenQ("SELECT value FROM ken_config WHERE key = 'scan_config'");
-    if (rows.length > 0) {
-      const cfg = rows[0].value;
-      if (cfg.scan_interval_ms) SCAN_INTERVAL_MS = cfg.scan_interval_ms;
-      if (cfg.reddit_interval_ms) REDDIT_INTERVAL_MS = cfg.reddit_interval_ms;
-      if (cfg.signal_pipeline_ms) SIGNAL_PIPELINE_MS = cfg.signal_pipeline_ms;
-      if (cfg.resolve_interval_ms) RESOLVE_INTERVAL_MS = cfg.resolve_interval_ms;
-      if (cfg.min_edge_threshold !== undefined) RISK_LIMITS.minEdgeThreshold = cfg.min_edge_threshold;
-      if (cfg.min_liquidity !== undefined) RISK_LIMITS.minLiquidity = cfg.min_liquidity;
-      console.log(`[Ken] Loaded scan_config: scan=${SCAN_INTERVAL_MS/1000}s, signal=${SIGNAL_PIPELINE_MS/1000}s, sweep=${REDDIT_INTERVAL_MS/1000}s, resolve=${RESOLVE_INTERVAL_MS/1000}s, edge=${RISK_LIMITS.minEdgeThreshold}, liq=${RISK_LIMITS.minLiquidity}`);
-    }
-  } catch (e) {
-    console.log(`[Ken] Failed to load scan config: ${e.message}`);
-  }
-}
-
-async function saveScanConfig() {
-  try {
-    await kenQ(`
-      INSERT INTO ken_config (key, value, updated_at) VALUES ('scan_config', $1, NOW())
-      ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()
-    `, [JSON.stringify({
-      scan_interval_ms: SCAN_INTERVAL_MS,
-      reddit_interval_ms: REDDIT_INTERVAL_MS,
-      signal_pipeline_ms: SIGNAL_PIPELINE_MS,
-      resolve_interval_ms: RESOLVE_INTERVAL_MS,
-      min_edge_threshold: RISK_LIMITS.minEdgeThreshold,
-      min_liquidity: RISK_LIMITS.minLiquidity,
-    })]);
-  } catch (e) {
-    console.log(`[Ken] Failed to save scan config: ${e.message}`);
-  }
-}
-
-function restartIntervals() {
-  if (scanIntervalId) clearInterval(scanIntervalId);
-  if (redditIntervalId) clearInterval(redditIntervalId);
-  if (signalIntervalId) clearInterval(signalIntervalId);
-  if (resolveIntervalId) clearInterval(resolveIntervalId);
-
-  scanIntervalId = setInterval(() => runScanSafe(autonomousScan), SCAN_INTERVAL_MS);
-  redditIntervalId = setInterval(() => runSweepSafe('Reddit', redditIntelligenceLoop), REDDIT_INTERVAL_MS);
-  signalIntervalId = setInterval(async () => {
-    if (cachedActiveMarkets.length > 0) {
-      try { await signalPipeline(cachedActiveMarkets); } catch (e) { console.log(`[Ken] Fast pipeline error: ${e.message}`); }
-    }
-  }, SIGNAL_PIPELINE_MS);
-  resolveIntervalId = setInterval(() => {
-    resolvePortfolioTrades().catch(e => console.error('[Ken] Portfolio resolution error:', e.message));
-  }, RESOLVE_INTERVAL_MS);
-
-  console.log(`[Ken] Intervals restarted: scan=${SCAN_INTERVAL_MS/60000}m, signal=${SIGNAL_PIPELINE_MS/60000}m, sweep=${REDDIT_INTERVAL_MS/60000}m, resolve=${RESOLVE_INTERVAL_MS/60000}m`);
-}
-
-// ── Reddit Scraper (no auth needed — uses .json API) ──
-async function scrapeReddit(subreddit, searchTerm, limit = 25) {
-  try {
-    const encoded = encodeURIComponent(searchTerm);
-    const url = `https://www.reddit.com/r/${subreddit}/search.json?q=${encoded}&sort=new&restrict_sr=on&limit=${limit}&t=week`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, {
-      headers: { 'User-Agent': 'Ken-Bot/2.0 (trading intelligence)' },
-      signal: controller.signal,
-    });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const data = await res.json();
-    const posts = (data?.data?.children || []).map(c => ({
-      title: c.data.title,
-      selftext: (c.data.selftext || '').substring(0, 500),
-      score: c.data.score || 0,
-      num_comments: c.data.num_comments || 0,
-      url: `https://reddit.com${c.data.permalink}`,
-      created: new Date(c.data.created_utc * 1000),
-      subreddit: c.data.subreddit,
-    }));
-    return posts;
-  } catch (e) {
-    console.log(`[Ken] Reddit scrape failed r/${subreddit} "${searchTerm}": ${e.message}`);
-    return [];
-  }
-}
-
-// ── Scrape Reddit hot/top posts for general sentiment ──
-async function scrapeSubredditHot(subreddit, limit = 15) {
-  try {
-    const url = `https://www.reddit.com/r/${subreddit}/hot.json?limit=${limit}`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, {
-      headers: { 'User-Agent': 'Ken-Bot/2.0 (trading intelligence)' },
-      signal: controller.signal,
-    });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data?.data?.children || []).map(c => ({
-      title: c.data.title,
-      selftext: (c.data.selftext || '').substring(0, 500),
-      score: c.data.score || 0,
-      num_comments: c.data.num_comments || 0,
-      url: `https://reddit.com${c.data.permalink}`,
-      created: new Date(c.data.created_utc * 1000),
-      subreddit: c.data.subreddit,
-    }));
-  } catch (e) {
-    return [];
-  }
-}
-
-// ── Google News RSS scraper ──
-async function scrapeGoogleNews(query, limit = 10) {
-  try {
-    const encoded = encodeURIComponent(query);
-    const url = `https://news.google.com/rss/search?q=${encoded}&hl=en-US&gl=US&ceid=US:en`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, { signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const xml = await res.text();
-    // Simple XML parse for RSS items
-    const items = [];
-    const itemRegex = /<item>([\s\S]*?)<\/item>/g;
-    let match;
-    while ((match = itemRegex.exec(xml)) && items.length < limit) {
-      const block = match[1];
-      const title = (block.match(/<title>([\s\S]*?)<\/title>/) || [])[1] || '';
-      const link = (block.match(/<link>([\s\S]*?)<\/link>/) || [])[1] || '';
-      const pubDate = (block.match(/<pubDate>([\s\S]*?)<\/pubDate>/) || [])[1] || '';
-      const source = (block.match(/<source[^>]*>([\s\S]*?)<\/source>/) || [])[1] || '';
-      items.push({
-        title: title.replace(/<!\[CDATA\[|\]\]>/g, '').trim(),
-        url: link.trim(),
-        source: source.replace(/<!\[CDATA\[|\]\]>/g, '').trim(),
-        published: pubDate ? new Date(pubDate) : new Date(),
-      });
-    }
-    return items;
-  } catch (e) {
-    console.log(`[Ken] News scrape failed "${query}": ${e.message}`);
-    return [];
-  }
-}
-
-// ── Polymarket cross-reference (public events API) ──
-async function scrapePolymarketEvents(limit = 100) {
-  try {
-    const url = `https://gamma-api.polymarket.com/events?closed=false&limit=${limit}&order=volume&ascending=false`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 15000);
-    const res = await fetch(url, { signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const events = await res.json();
-    const results = [];
-    for (const evt of (events || [])) {
-      const mkts = (evt.markets || []).sort((a, b) => (b.volume || 0) - (a.volume || 0));
-      for (const m of mkts.slice(0, 3)) {
-        let price = null;
-        try { price = Math.round(parseFloat(JSON.parse(m.outcomePrices)[0]) * 100); } catch {}
-        if (price !== null && price > 0 && price < 100) {
-          results.push({
-            event_title: evt.title || '',
-            question: m.question || '',
-            price,
-            volume: Math.round(m.volume || 0),
-            slug: evt.slug || m.slug || '',
-          });
-        }
-      }
-    }
-    return results;
-  } catch (e) {
-    console.log(`[Ken] Polymarket scrape failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── Simple keyword sentiment scorer ──
-function scoreSentiment(text) {
-  const t = (text || '').toLowerCase();
-  const bullish = ['confirmed', 'likely', 'expected', 'approved', 'passed', 'agreed', 'winning', 'surge', 'soaring', 'breaking', 'imminent', 'certain', 'guarantee', 'official', 'announced', 'done deal'];
-  const bearish = ['unlikely', 'denied', 'rejected', 'failed', 'blocked', 'withdrawn', 'canceled', 'impossible', 'no chance', 'not going', 'walked back', 'reversed', 'postponed', 'delayed', 'collapse'];
-  const uncertain = ['maybe', 'could', 'might', 'rumor', 'speculation', 'sources say', 'unconfirmed', 'developing', 'unclear'];
-
-  let score = 0;
-  for (const w of bullish) if (t.includes(w)) score += 0.15;
-  for (const w of bearish) if (t.includes(w)) score -= 0.15;
-  for (const w of uncertain) if (t.includes(w)) score *= 0.7;
-  return Math.max(-1, Math.min(1, score));
-}
-
-// ── Map keywords in text to Kalshi event tickers ──
-function matchTextToMarkets(text, activeMarkets) {
-  const t = (text || '').toLowerCase();
-  const matches = [];
-  const keywords = {
-    'fed chair': 'KXFEDCHAIRNOM',
-    'federal reserve': 'KXFEDCHAIRNOM',
-    'kevin warsh': 'KXFEDCHAIRNOM',
-    'greenland': 'KXGREEN',
-    'cabinet': 'KXCABOUT',
-    'gabbard': 'KXCABOUT',
-    'tulsi': 'KXCABOUT',
-    'hegseth': 'KXCABOUT',
-    'defense secretary': 'KXTRUMPDEFCOUNT',
-    'insurrection act': 'KXINSURRECTION',
-    'supreme court': 'KXSCOTUS',
-    'seattle basketball': 'KXNBASEATTLE',
-    'seattle nba': 'KXNBASEATTLE',
-    '2028 election': 'KXPRESPERSON',
-    'presidential': 'KXPRESPERSON',
-  };
-  for (const [kw, prefix] of Object.entries(keywords)) {
-    if (t.includes(kw)) {
-      const related = activeMarkets.filter(m => m.event_ticker?.startsWith(prefix) || m.ticker?.startsWith(prefix));
-      for (const m of related) {
-        if (!matches.find(x => x.ticker === m.ticker)) matches.push(m);
-      }
-    }
-  }
-  return matches;
-}
-
-function generateSyntheticWeather(cities) {
-  const month = new Date().getMonth();
-  const results = [];
-  for (const city of cities) {
-    const coords = CITY_COORDS[city]; if (!coords) continue;
-    const baseTemp = [35,38,48,58,68,78,85,83,75,62,48,38][month] + (coords.lat < 35 ? 15 : coords.lat > 42 ? -8 : 0);
-    const temps = Array.from({length: 14}, (_, i) => baseTemp + Math.round((Math.random() - 0.5) * 12) + (i < 7 ? 3 : -2));
-    const precips = Array.from({length: 14}, () => Math.round(Math.random() * 60));
-    const winds = Array.from({length: 14}, () => 5 + Math.round(Math.random() * 15));
-    const mkStat = (arr) => ({ mean: arr.reduce((a,b)=>a+b)/arr.length, min: Math.min(...arr), max: Math.max(...arr), latest: arr[0] });
-    results.push({
-      city, synthetic: true,
-      temperature: { variable: 'temperature', unit: 'F', values: temps, ...mkStat(temps) },
-      precipitation: { variable: 'precipitation', unit: 'percent', values: precips, ...mkStat(precips) },
-      wind: { variable: 'wind', unit: 'mph', values: winds, ...mkStat(winds) },
-    });
-  }
-  return results;
-}
-
-// ── Hacker News scraper (top stories + search) ──
-// ── GDELT TV News mentions (covers CNN, MSNBC, Fox News, BBC captions) ──
-async function scrapeGDELTtv(query, station = 'CNN', limit = 15) {
-  try {
-    const url = `https://api.gdeltproject.org/api/v2/tv/tv?query=${encodeURIComponent(query)}%20station:${station}&mode=clipgallery&format=json&maxrecords=${limit}&last24hours=yes`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 15000);
-    const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'Ken-Bot/2.0' } });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const text = await res.text();
-    if (!text.startsWith('{')) return []; // API returns error text, not JSON
-    const data = JSON.parse(text);
-    return (data.clips || []).map(c => ({
-      title: c.snippet || '',
-      url: c.preview_url || c.url || '',
-      source: c.station || 'TV News',
-      show: c.show || '',
-      date: c.date || '',
-    }));
-  } catch (e) {
-    return [];
-  }
-}
-
-// ── YouTube RSS search (public API, no key needed) ──
-async function scrapeYouTubeRSS(query) {
-  try {
-    const url = `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}&sp=CAISBAgBEAE%253D`; // last hour filter
-    // YouTube doesn't have direct RSS search, use channel feeds for major news channels
-    const channels = [
-      { id: 'UCupvZG-5ko_eiXAupbDfxWw', name: 'CNN' },
-      { id: 'UCaXkIU1QidjPwiAYu6GcHjg', name: 'MSNBC' },
-      { id: 'UCXIJgqnII2ZOINSWNOGFThA', name: 'Fox News' },
-      { id: 'UC16niRr50-MSBwiO3YDb3RA', name: 'Bloomberg TV' },
-      { id: 'UCvJJ_dzjViJCoLf5uKUTwoA', name: 'CNBC' },
-      { id: 'UCknLrEdhRCp1aegoMqRaCZg', name: 'NBC News' },
-      { id: 'UCBi2mrWuNuyYy4gbM6fU18Q', name: 'ABC News' },
-      { id: 'UC8p1vwvWtl6T73JiExfWs1g', name: 'CBS News' },
-      { id: 'UC52X5wxOL_s5yw0dQk7NtgA', name: 'PBS NewsHour' },
-    ];
-    const results = [];
-    for (const ch of channels) {
-      try {
-        const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${ch.id}`;
-        const items = await scrapeRSSFeed(feedUrl, `YT ${ch.name}`, 5);
-        results.push(...items);
-      } catch {}
-    }
-    return results;
-  } catch { return []; }
-}
-
-// ── Lemmy (federated Reddit) — public JSON API, no auth needed ──
-async function scrapeLemmy(sort = 'Hot', limit = 20) {
-  const instances = ['lemmy.world', 'lemmy.ml', 'sh.itjust.works'];
-  const results = [];
-  for (const inst of instances) {
-    try {
-      const controller = new AbortController();
-      const timeout = setTimeout(() => controller.abort(), 10000);
-      const res = await fetch(`https://${inst}/api/v3/post/list?sort=${sort}&limit=${limit}&type_=All`, {
-        signal: controller.signal,
-        headers: { 'Accept': 'application/json' },
-      });
-      clearTimeout(timeout);
-      if (!res.ok) continue;
-      const data = await res.json();
-      for (const p of (data.posts || [])) {
-        const post = p.post || {};
-        const counts = p.counts || {};
-        if (post.name && post.name.length > 10) {
-          results.push({
-            title: post.name.substring(0, 500),
-            url: post.ap_id || `https://${inst}/post/${post.id}`,
-            source: `Lemmy ${inst}`,
-            score: counts.score || 0,
-            num_comments: counts.comments || 0,
-          });
-        }
-      }
-      if (results.length > 0) break; // got data from one instance
-    } catch {}
-  }
-  return results;
-}
-
-// ── Lobsters (tech/politics news) — public JSON API ──
-async function scrapeLobsters(limit = 25) {
-  try {
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch('https://lobste.rs/hottest.json', { signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const stories = await res.json();
-    return stories.slice(0, limit).map(s => ({
-      title: s.title || '',
-      url: s.url || s.short_id_url || '',
-      source: 'Lobsters',
-      score: s.score || 0,
-      num_comments: s.comment_count || 0,
-      author: s.submitter_user?.username || '',
-      tags: (s.tags || []).join(', '),
-    }));
-  } catch { return []; }
-}
-
-// ── Tildes (curated discussion) — RSS feed ──
-async function scrapeTildes() {
-  try {
-    return await scrapeRSSFeed('https://tildes.net/topics.rss', 'Tildes', 20);
-  } catch { return []; }
-}
-
-// ── Manifold Markets (open prediction market — public API) ──
-async function scrapeManifold(queries) {
-  const results = [];
-  for (const q of queries) {
-    try {
-      const controller = new AbortController();
-      const timeout = setTimeout(() => controller.abort(), 8000);
-      const res = await fetch(`https://manifold.markets/api/v0/search-markets?term=${encodeURIComponent(q)}&sort=liquidity&limit=10`, {
-        signal: controller.signal, headers: { 'Accept': 'application/json' },
-      });
-      clearTimeout(timeout);
-      if (!res.ok) continue;
-      const markets = await res.json();
-      for (const m of (markets || [])) {
-        if (m.question && m.probability != null && (m.volume || 0) > 100) {
-          results.push({
-            title: m.question,
-            url: m.url || `https://manifold.markets/${m.creatorUsername}/${m.slug}`,
-            source: 'Manifold',
-            probability: Math.round((m.probability || 0) * 100),
-            volume: Math.round(m.volume || 0),
-            liquidity: Math.round(m.totalLiquidity || 0),
-          });
-        }
-      }
-    } catch {}
-    await new Promise(r => setTimeout(r, 500));
-  }
-  return results;
-}
-
-// ── Mastodon/Fediverse trending (public API, no auth) ──
-async function scrapeMastodon() {
-  const instances = ['mastodon.social', 'mas.to'];
-  const results = [];
-  for (const inst of instances) {
-    try {
-      const controller = new AbortController();
-      const timeout = setTimeout(() => controller.abort(), 8000);
-      const res = await fetch(`https://${inst}/api/v1/trends/statuses?limit=10`, {
-        signal: controller.signal,
-      });
-      clearTimeout(timeout);
-      if (!res.ok) continue;
-      const statuses = await res.json();
-      for (const s of statuses) {
-        const text = (s.content || '').replace(/<[^>]+>/g, '').trim();
-        if (text.length > 20) {
-          results.push({
-            title: text.substring(0, 500),
-            url: s.url || s.uri || '',
-            source: `Mastodon ${inst}`,
-            score: s.favourites_count || 0,
-            num_comments: s.replies_count || 0,
-          });
-        }
-      }
-    } catch {}
-  }
-  return results;
-}
-
-// ── Bluesky trending (public API — using feed generators, not searchPosts which is 403) ──
-async function scrapeBluesky() {
-  const feeds = [
-    { uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot', name: 'whats-hot' },
-    { uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic', name: 'hot-classic' },
-  ];
-  const results = [];
-  for (const feed of feeds) {
-    try {
-      const controller = new AbortController();
-      const timeout = setTimeout(() => controller.abort(), 10000);
-      const url = `https://public.api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${encodeURIComponent(feed.uri)}&limit=20`;
-      const res = await fetch(url, { signal: controller.signal, headers: { 'Accept': 'application/json' } });
-      clearTimeout(timeout);
-      if (!res.ok) continue;
-      const data = await res.json();
-      for (const item of (data.feed || [])) {
-        const p = item.post || {};
-        const text = (p.record?.text || '').trim();
-        if (text.length > 20) {
-          results.push({
-            title: text.substring(0, 500),
-            url: `https://bsky.app/profile/${p.author?.handle}/post/${p.uri?.split('/').pop()}`,
-            source: `Bluesky ${feed.name}`,
-            author: p.author?.handle || '',
-            score: (p.likeCount || 0) + (p.repostCount || 0),
-            num_comments: p.replyCount || 0,
-          });
-        }
-      }
-    } catch {}
-  }
-  return results;
-}
-
-async function scrapeHackerNews(query, limit = 15) {
-  try {
-    const url = query
-      ? `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(query)}&tags=story&hitsPerPage=${limit}`
-      : `https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=${limit}`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, { signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.hits || []).map(h => ({
-      title: h.title || '',
-      url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,
-      score: h.points || 0,
-      num_comments: h.num_comments || 0,
-      created: new Date(h.created_at),
-      source: 'hackernews',
-    }));
-  } catch (e) {
-    console.log(`[Ken] HN scrape failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── AP News RSS scraper ──
-// ── Generic RSS feed scraper ──
-async function scrapeRSSFeed(url, source, limit = 15) {
-  try {
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, {
-      signal: controller.signal,
-      headers: { 'User-Agent': 'Ken-Bot/2.0 (RSS Reader)' },
-    });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const xml = await res.text();
-    const items = [];
-    // Handle both <item> (RSS) and <entry> (Atom) formats
-    const itemRegex = /<(?:item|entry)>([\s\S]*?)<\/(?:item|entry)>/g;
-    let match;
-    while ((match = itemRegex.exec(xml)) && items.length < limit) {
-      const block = match[1];
-      const title = (block.match(/<title[^>]*>([\s\S]*?)<\/title>/) || [])[1] || '';
-      const link = (block.match(/<link[^>]*href="([^"]+)"/) || block.match(/<link>([\s\S]*?)<\/link>/) || [])[1] || '';
-      const cleanTitle = title.replace(/<!\[CDATA\[|\]\]>/g, '').replace(/<[^>]+>/g, '').trim();
-      if (cleanTitle && cleanTitle.length > 10) {
-        items.push({ title: cleanTitle, url: link.trim(), source });
-      }
-    }
-    return items;
-  } catch { return []; }
-}
-
-// ── All news channel feeds ──
-const NEWS_FEEDS = [
-  // ═══ POLITICS & POLICY (Kalshi political markets) ═══
-  { url: 'https://feeds.bbci.co.uk/news/world/rss.xml', source: 'BBC World' },
-  { url: 'https://feeds.bbci.co.uk/news/politics/rss.xml', source: 'BBC Politics' },
-  { url: 'https://feeds.bbci.co.uk/news/us_and_canada/rss.xml', source: 'BBC US' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Politics.xml', source: 'NYT Politics' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/World.xml', source: 'NYT World' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/US.xml', source: 'NYT US' },
-  { url: 'https://feeds.washingtonpost.com/rss/politics', source: 'WaPo Politics' },
-  { url: 'https://feeds.washingtonpost.com/rss/world', source: 'WaPo World' },
-  { url: 'https://www.politico.com/rss/politicopicks.xml', source: 'Politico' },
-  { url: 'https://www.politico.com/rss/congress.xml', source: 'Politico Congress' },
-  { url: 'https://thehill.com/feed/', source: 'The Hill' },
-  { url: 'https://feeds.npr.org/1001/rss.xml', source: 'NPR News' },
-  { url: 'https://feeds.npr.org/1014/rss.xml', source: 'NPR Politics' },
-  { url: 'https://www.aljazeera.com/xml/rss/all.xml', source: 'Al Jazeera' },
-  { url: 'https://rss.dw.com/xml/rss-en-all', source: 'DW News' },
-  { url: 'https://feeds.skynews.com/feeds/rss/world.xml', source: 'Sky News World' },
-  { url: 'https://feeds.skynews.com/feeds/rss/us.xml', source: 'Sky News US' },
-  { url: 'https://www.theguardian.com/us-news/rss', source: 'Guardian US' },
-  { url: 'https://www.theguardian.com/world/rss', source: 'Guardian World' },
-  { url: 'https://rss.cbc.ca/lineup/world.xml', source: 'CBC World' },
-  { url: 'https://feeds.abcnews.com/abcnews/politicsheadlines', source: 'ABC Politics' },
-  { url: 'http://feeds.foxnews.com/foxnews/politics', source: 'Fox Politics' },
-  { url: 'https://www.axios.com/feeds/feed.rss', source: 'Axios' },
-  { url: 'https://www.vox.com/rss/index.xml', source: 'Vox' },
-
-  // ═══ ECONOMICS & FINANCIAL MARKETS (Kalshi econ/rate markets) ═══
-  { url: 'https://feeds.bloomberg.com/politics/news.rss', source: 'Bloomberg Politics' },
-  { url: 'https://feeds.bloomberg.com/markets/news.rss', source: 'Bloomberg Markets' },
-  { url: 'https://www.cnbc.com/id/10000664/device/rss/rss.html', source: 'CNBC Top' },
-  { url: 'https://www.cnbc.com/id/10001147/device/rss/rss.html', source: 'CNBC Economy' },
-  { url: 'https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=15839069', source: 'CNBC Politics' },
-  { url: 'https://www.cnbc.com/id/20910258/device/rss/rss.html', source: 'CNBC Finance' },
-  { url: 'https://feeds.marketwatch.com/marketwatch/topstories/', source: 'MarketWatch' },
-  { url: 'https://feeds.marketwatch.com/marketwatch/marketpulse/', source: 'MarketWatch Pulse' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Business.xml', source: 'NYT Business' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Economy.xml', source: 'NYT Economy' },
-  { url: 'https://www.ft.com/rss/home/us', source: 'FT US' },
-  { url: 'https://www.investing.com/rss/news.rss', source: 'Investing.com' },
-  { url: 'https://www.federalreserve.gov/feeds/press_all.xml', source: 'Fed Reserve' },
-  { url: 'https://www.bis.org/doclist/bis_fsi_publs.rss', source: 'BIS' },
-  { url: 'https://seekingalpha.com/feed.xml', source: 'Seeking Alpha' },
-  { url: 'https://finance.yahoo.com/news/rssindex', source: 'Yahoo Finance' },
-
-  // ═══ CRYPTO & PREDICTION MARKETS (cross-ref with Polymarket) ═══
-  { url: 'https://cointelegraph.com/rss', source: 'CoinTelegraph' },
-  { url: 'https://www.coindesk.com/arc/outboundfeeds/rss/', source: 'CoinDesk' },
-  { url: 'https://decrypt.co/feed', source: 'Decrypt' },
-  { url: 'https://thedefiant.io/feed', source: 'The Defiant' },
-
-  // ═══ PREDICTION & POLLING ANALYSIS ═══
-  { url: 'https://fivethirtyeight.com/features/feed/', source: 'FiveThirtyEight' },
-  { url: 'https://www.realclearpolitics.com/index.xml', source: 'RealClearPolitics' },
-  { url: 'https://electionbettingodds.com/feed', source: 'ElectionBettingOdds' },
-  { url: 'https://www.predictit.org/feed', source: 'PredictIt' },
-
-  // ═══ TECH & AI POLICY (Kalshi tech markets) ═══
-  { url: 'https://www.theverge.com/rss/index.xml', source: 'The Verge' },
-  { url: 'https://techcrunch.com/feed/', source: 'TechCrunch' },
-  { url: 'https://arstechnica.com/feed/', source: 'Ars Technica' },
-  { url: 'https://www.wired.com/feed/rss', source: 'WIRED' },
-  { url: 'https://feeds.feedburner.com/venturebeat/SZYF', source: 'VentureBeat' },
-
-  // ═══ WEATHER & CLIMATE (Kalshi weather markets - temps, snow, etc.) ═══
-  { url: 'https://www.weather.gov/rss_page.php?site_name=nws', source: 'NWS' },
-  { url: 'https://weather.com/feeds/enhancedconditions/rss', source: 'Weather.com' },
-  { url: 'https://www.accuweather.com/en/us/national/current-weather/rss', source: 'AccuWeather' },
-  { url: 'https://www.climate.gov/rss.xml', source: 'NOAA Climate' },
-
-  // ═══ SPORTS (Kalshi has sports-adjacent markets) ═══
-  { url: 'https://www.espn.com/espn/rss/news', source: 'ESPN' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Sports.xml', source: 'NYT Sports' },
-
-  // ═══ ENTERTAINMENT & CULTURE (Kalshi awards/culture markets) ═══
-  { url: 'https://variety.com/feed/', source: 'Variety' },
-  { url: 'https://deadline.com/feed/', source: 'Deadline' },
-  { url: 'https://www.hollywoodreporter.com/feed/', source: 'Hollywood Reporter' },
-
-  // ═══ ENERGY & COMMODITIES (Kalshi gas/oil markets) ═══
-  { url: 'https://oilprice.com/rss/main', source: 'OilPrice' },
-  { url: 'https://www.eia.gov/rss/todayinenergy.xml', source: 'EIA Energy' },
-
-  // ═══ DEFENSE & GEOPOLITICS (conflict/territory markets) ═══
-  { url: 'https://www.defensenews.com/arc/outboundfeeds/rss/category/global/?outputType=xml', source: 'Defense News' },
-  { url: 'https://foreignpolicy.com/feed/', source: 'Foreign Policy' },
-  { url: 'https://warontherocks.com/feed/', source: 'War On The Rocks' },
-
-  // ═══ SCIENCE & SPACE (Kalshi space markets) ═══
-  { url: 'https://www.nasa.gov/rss/dyn/breaking_news.rss', source: 'NASA' },
-  { url: 'https://www.space.com/feeds/all', source: 'Space.com' },
-  { url: 'https://www.nature.com/nature.rss', source: 'Nature' },
-  { url: 'https://www.science.org/rss/news_current.xml', source: 'Science' },
-];
-
-async function scrapeAllNewsFeeds() {
-  const allArticles = [];
-  // Process feeds in batches of 15 concurrently for speed (maxed for 16 cores)
-  const batchSize = 15;
-  for (let i = 0; i < NEWS_FEEDS.length; i += batchSize) {
-    const batch = NEWS_FEEDS.slice(i, i + batchSize);
-    const results = await Promise.allSettled(
-      batch.map(feed => scrapeRSSFeed(feed.url, feed.source, 8))
-    );
-    for (const result of results) {
-      if (result.status === 'fulfilled') allArticles.push(...result.value);
-    }
-    await new Promise(r => setTimeout(r, 100)); // minimal pause between batches
-  }
-  return allArticles;
-}
-
-// ── Metaculus predictions scraper (prediction market cross-ref) ──
-async function scrapeMetaculus() {
-  try {
-    const url = 'https://www.metaculus.com/api2/questions/?limit=20&order_by=-activity&status=open&type=forecast';
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, { headers: { 'Accept': 'application/json' }, signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.results || []).map(q => ({
-      title: q.title || '',
-      url: `https://www.metaculus.com/questions/${q.id}`,
-      community_prediction: q.community_prediction?.full?.q2,
-      num_predictions: q.number_of_predictions || 0,
-      source: 'metaculus',
-    }));
-  } catch (e) {
-    console.log(`[Ken] Metaculus scrape failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── PredictIt API (another prediction market for cross-reference) ──
-async function scrapePredictIt() {
-  try {
-    const controller = new AbortController();
-    setTimeout(() => controller.abort(), 8000);
-    const res = await fetch('https://www.predictit.org/api/marketdata/all/', {
-      signal: controller.signal,
-      headers: { 'User-Agent': 'Ken-Bot/1.0' }
-    });
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.markets || []).slice(0, 100).map(m => ({
-      title: m.name,
-      url: m.url || `https://www.predictit.org/markets/detail/${m.id}`,
-      score: Math.round((m.contracts?.[0]?.lastTradePrice || 0) * 100),
-      num_comments: m.contracts?.length || 0,
-      source: 'predictit',
-      extra: JSON.stringify({
-        contracts: (m.contracts || []).slice(0, 5).map(c => ({
-          name: c.name, lastPrice: c.lastTradePrice, bestBuy: c.bestBuyYesCost, bestSell: c.bestSellYesCost
-        }))
-      })
-    }));
-  } catch (e) {
-    console.log(`[Ken] PredictIt scrape failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── FRED Economic Data API (real economic indicators — no key needed for some endpoints) ──
-async function scrapeFredIndicators() {
-  const indicators = [
-    { series: 'CPIAUCSL', name: 'CPI Inflation' },
-    { series: 'UNRATE', name: 'Unemployment Rate' },
-    { series: 'GDP', name: 'GDP Growth' },
-    { series: 'FEDFUNDS', name: 'Federal Funds Rate' },
-    { series: 'T10Y2Y', name: '10Y-2Y Treasury Spread' },
-    { series: 'VIXCLS', name: 'VIX Volatility Index' },
-    { series: 'DGS10', name: '10-Year Treasury Yield' },
-    { series: 'DCOILWTICO', name: 'WTI Crude Oil Price' },
-  ];
-  const results = [];
-  for (const ind of indicators) {
-    try {
-      const controller = new AbortController();
-      setTimeout(() => controller.abort(), 5000);
-      const res = await fetch(`https://fred.stlouisfed.org/graph/fredgraph.csv?id=${ind.series}&cosd=${new Date(Date.now() - 90*86400000).toISOString().split('T')[0]}`, {
-        signal: controller.signal,
-        headers: { 'User-Agent': 'Ken-Bot/1.0' }
-      });
-      if (!res.ok) continue;
-      const csv = await res.text();
-      const lines = csv.trim().split('\n');
-      if (lines.length < 2) continue;
-      const lastLine = lines[lines.length - 1];
-      const [date, value] = lastLine.split(',');
-      if (value && value !== '.') {
-        results.push({
-          title: `${ind.name}: ${value} (${date})`,
-          url: `https://fred.stlouisfed.org/series/${ind.series}`,
-          score: Math.round(parseFloat(value) * 100) || 0,
-          source: 'fred_economic',
-          keyword: ind.series,
-        });
-      }
-    } catch {}
-  }
-  return results;
-}
-
-// ── NWS Active Weather Alerts (real severe weather data for weather markets) ──
-async function scrapeNWSAlerts() {
-  try {
-    const controller = new AbortController();
-    setTimeout(() => controller.abort(), 8000);
-    const res = await fetch('https://api.weather.gov/alerts/active?status=actual&message_type=alert&limit=50', {
-      signal: controller.signal,
-      headers: { 'User-Agent': 'Ken-Bot/1.0 (ken@designerwallcoverings.com)', 'Accept': 'application/geo+json' }
-    });
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.features || []).map(f => ({
-      title: `${f.properties.event}: ${f.properties.headline || f.properties.description?.substring(0, 200) || ''}`.substring(0, 500),
-      url: f.properties['@id'] || 'https://alerts.weather.gov',
-      score: f.properties.severity === 'Extreme' ? 100 : f.properties.severity === 'Severe' ? 75 : f.properties.severity === 'Moderate' ? 50 : 25,
-      num_comments: 0,
-      source: 'nws_alerts',
-      keyword: f.properties.event || 'weather',
-    }));
-  } catch (e) {
-    console.log(`[Ken] NWS Alerts failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── Congressional/Government Activity (GovTrack API) ──
-async function scrapeGovTrack() {
-  try {
-    const controller = new AbortController();
-    setTimeout(() => controller.abort(), 8000);
-    const res = await fetch('https://www.govtrack.us/api/v2/vote?order_by=-created&limit=20', {
-      signal: controller.signal,
-      headers: { 'User-Agent': 'Ken-Bot/1.0' }
-    });
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.objects || []).map(v => ({
-      title: `${v.chamber === 'senate' ? 'Senate' : 'House'} Vote: ${v.question || v.result || 'Unknown'}`.substring(0, 500),
-      url: `https://www.govtrack.us${v.link || ''}`,
-      score: v.total_plus || 0,
-      num_comments: v.total_minus || 0,
-      source: 'govtrack',
-      keyword: v.category || 'legislation',
-    }));
-  } catch (e) {
-    console.log(`[Ken] GovTrack failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── X/Twitter via Nitter instances (public, no auth needed) ──
-async function scrapeNitterTrending() {
-  const nitterInstances = [
-    'https://nitter.poast.org',
-    'https://nitter.privacydev.net',
-    'https://nitter.cz',
-  ];
-  const searchTerms = ['kalshi', 'polymarket', 'prediction market', 'fed rate decision', 'hurricane season'];
-  const results = [];
-  for (const term of searchTerms) {
-    for (const instance of nitterInstances) {
-      try {
-        const controller = new AbortController();
-        setTimeout(() => controller.abort(), 6000);
-        const url = `${instance}/search?f=tweets&q=${encodeURIComponent(term)}&since=`;
-        const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'Mozilla/5.0' } });
-        if (!res.ok) continue;
-        const html = await res.text();
-        // Extract tweet text from Nitter HTML
-        const tweetMatches = html.match(/<div class="tweet-content[^"]*"[^>]*>([\s\S]*?)<\/div>/g) || [];
-        for (const match of tweetMatches.slice(0, 5)) {
-          const text = match.replace(/<[^>]+>/g, '').trim().substring(0, 500);
-          if (text.length > 10) {
-            results.push({
-              title: text,
-              url: `${instance}/search?q=${encodeURIComponent(term)}`,
-              score: 0,
-              num_comments: 0,
-              source: 'twitter_nitter',
-              keyword: term,
-            });
-          }
-        }
-        break; // One working instance per term is enough
-      } catch {} // Nitter instances are unreliable, fail silently
-    }
-    await new Promise(r => setTimeout(r, 2000));
-  }
-  return results;
-}
-
-// ── Discord Bot Intelligence (reads messages from joined servers) ──
-const DISCORD_BOT_TOKEN = 'MTQ3MjYyNDg2OTk3NDg2ODA2Mg.GifeyF.C5l9KqQUia-Ex54Ldg23fuxislxwf2QtmkXYII';
-const DISCORD_API = 'https://discord.com/api/v10';
-
-// Channel names to monitor (matched case-insensitive) — covers all Kalshi/Polymarket categories
-const DISCORD_CHANNEL_KEYWORDS = [
-  // Core
-  'general', 'chat', 'discussion', 'lounge', 'main',
-  // Trading & Markets
-  'trading', 'markets', 'predictions', 'analysis', 'strategy', 'signals', 'alerts',
-  'arbitrage', 'polymarket', 'kalshi', 'bets', 'picks', 'plays', 'odds',
-  // News & Facts
-  'news', 'breaking', 'headlines', 'updates', 'osint', 'intel', 'events',
-  // Weather
-  'weather', 'hurricane', 'tropical', 'storm', 'forecast', 'severe', 'climate',
-  // Politics & Elections
-  'politics', 'election', 'polling', 'congress', 'senate', 'trump', 'biden',
-  'policy', 'legislation', 'vote', 'debate',
-  // Economics & Finance
-  'economics', 'economy', 'finance', 'stocks', 'fed', 'inflation', 'rates',
-  'macro', 'earnings', 'options', 'futures', 'bonds',
-  // Sports
-  'sports', 'nfl', 'nba', 'mlb', 'nhl', 'soccer', 'football', 'basketball',
-  'baseball', 'ufc', 'mma', 'boxing', 'f1', 'golf', 'tennis',
-  // Crypto
-  'crypto', 'bitcoin', 'btc', 'ethereum', 'eth', 'defi', 'altcoin', 'solana',
-  // Geopolitics
-  'geopolitics', 'ukraine', 'russia', 'china', 'military', 'conflict', 'war', 'nato',
-  // Entertainment
-  'entertainment', 'movies', 'film', 'oscars', 'grammys', 'awards', 'box-office',
-  'music', 'tv', 'streaming', 'celebrity',
-  // Science & Space
-  'space', 'spacex', 'nasa', 'launch', 'science', 'rocket',
-  // Tech & AI
-  'tech', 'ai', 'artificial', 'openai', 'gpt', 'machine-learning', 'nvidia',
-];
-
-async function discordFetch(endpoint) {
-  const controller = new AbortController();
-  const timeout = setTimeout(() => controller.abort(), 10000);
-  const res = await fetch(`${DISCORD_API}${endpoint}`, {
-    headers: { Authorization: `Bot ${DISCORD_BOT_TOKEN}`, 'Content-Type': 'application/json' },
-    signal: controller.signal,
-  });
-  clearTimeout(timeout);
-  if (!res.ok) {
-    if (res.status === 429) {
-      const retry = await res.json().catch(() => ({}));
-      const wait = (retry.retry_after || 5) * 1000;
-      console.log(`[Ken] Discord rate limited, waiting ${wait}ms`);
-      await new Promise(r => setTimeout(r, wait));
-      return null;
-    }
-    return null;
-  }
-  return res.json();
-}
-
-async function scrapeDiscord(limit = 50) {
-  const results = [];
-  try {
-    // 1. Get all guilds the bot is in
-    const guilds = await discordFetch('/users/@me/guilds');
-    if (!guilds || !Array.isArray(guilds)) {
-      console.log('[Ken] Discord: no guilds found (bot may not have joined any servers yet)');
-      return [];
-    }
-    console.log(`[Ken] Discord: bot is in ${guilds.length} server(s): ${guilds.map(g => g.name).join(', ')}`);
-
-    for (const guild of guilds) {
-      // 2. Get channels for this guild
-      const channels = await discordFetch(`/guilds/${guild.id}/channels`);
-      if (!channels || !Array.isArray(channels)) continue;
-
-      // 3. Filter to text channels matching keywords
-      const textChannels = channels.filter(ch => {
-        if (ch.type !== 0) return false; // type 0 = text channel
-        const name = (ch.name || '').toLowerCase();
-        return DISCORD_CHANNEL_KEYWORDS.some(kw => name.includes(kw));
-      });
-
-      console.log(`[Ken] Discord [${guild.name}]: monitoring ${textChannels.length}/${channels.filter(c => c.type === 0).length} text channels`);
-
-      for (const channel of textChannels.slice(0, 15)) { // cap at 15 channels per guild
-        try {
-          const messages = await discordFetch(`/channels/${channel.id}/messages?limit=${Math.min(limit, 50)}`);
-          if (!messages || !Array.isArray(messages)) continue;
-
-          // Only messages from last 2 hours (fresh intel)
-          const cutoff = Date.now() - (2 * 60 * 60 * 1000);
-          for (const msg of messages) {
-            const msgTime = new Date(msg.timestamp).getTime();
-            if (msgTime < cutoff) continue;
-            if (!msg.content || msg.content.length < 20) continue;
-            if (msg.author?.bot) continue; // skip bot messages
-
-            results.push({
-              title: msg.content.substring(0, 500),
-              url: `https://discord.com/channels/${guild.id}/${channel.id}/${msg.id}`,
-              source: `Discord ${guild.name}`,
-              channel: channel.name,
-              author: msg.author?.username || 'unknown',
-              score: (msg.reactions || []).reduce((sum, r) => sum + (r.count || 0), 0),
-              num_comments: msg.referenced_message ? 1 : 0, // thread indicator
-              created: new Date(msg.timestamp),
-            });
-          }
-          // Rate limit: 500ms between channel reads
-          await new Promise(r => setTimeout(r, 500));
-        } catch (e) {
-          console.log(`[Ken] Discord channel ${channel.name} error: ${e.message}`);
-        }
-      }
-      await new Promise(r => setTimeout(r, 1000)); // between guilds
-    }
-    console.log(`[Ken] Discord: ${results.length} messages from ${guilds.length} server(s)`);
-  } catch (e) {
-    console.log(`[Ken] Discord scrape failed: ${e.message}`);
-  }
-  return results;
-}
-
-// ══════════════════════════════════════════════
-// INTELLIGENCE LOOP (every 30 min) — Reddit, HN, News, Metaculus, Discord
-// ══════════════════════════════════════════════
-function logMemory(label) {
-  const mem = process.memoryUsage();
-  const rss = Math.round(mem.rss / 1024 / 1024);
-  const heap = Math.round(mem.heapUsed / 1024 / 1024);
-  if (rss > 500) console.log(`[Ken] MEM ${label}: RSS=${rss}MB Heap=${heap}MB`);
-}
-
-async function redditIntelligenceLoop() {
-  console.log('[Ken] Running Reddit intelligence sweep...');
-  logMemory('sweep-start');
-  let totalPosts = 0;
-  try {
-    // Get tracking config from DB
-    const { rows: trackers } = await kenQ('SELECT * FROM ken_reddit_tracking WHERE enabled = true');
-
-    // Process tracked subreddits in parallel batches of 12 (maxed for 16 cores)
-    const TRACKER_BATCH = 12;
-    for (let i = 0; i < trackers.length; i += TRACKER_BATCH) {
-      const batch = trackers.slice(i, i + TRACKER_BATCH);
-      const results = await Promise.allSettled(
-        batch.map(tracker => scrapeReddit(tracker.subreddit, tracker.search_term, 20)
-          .then(posts => ({ tracker, posts })))
-      );
-      for (const result of results) {
-        if (result.status !== 'fulfilled') continue;
-        const { tracker, posts } = result.value;
-        for (const post of posts) {
-          const sentiment = scoreSentiment(post.title + ' ' + post.selftext);
-          if (post.score > 2 || post.num_comments > 1) {
-            await kenQ(`
-              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, relevance_ticker, raw_text)
-              VALUES ('reddit', $1, $2, $3, $4, $5, $6, $7, $8, $9)
-              ON CONFLICT DO NOTHING
-            `, [
-              tracker.subreddit, tracker.search_term, post.title, post.url,
-              post.score, post.num_comments, sentiment,
-              (tracker.related_tickers || [])[0] || null,
-              (post.selftext || '').substring(0, 1000),
-            ]);
-            totalPosts++;
-          }
-        }
-      }
-      await new Promise(r => setTimeout(r, 500)); // 0.5s between batches (was 1.5s)
-    }
-
-    // Also scrape hot posts from key subreddits for general intelligence
-    // Categories: Weather | Politics | Economics | Sports | Crypto | Geopolitics | Entertainment | Science/Space | Tech/AI | General Sentiment
-    const hotSubs = [
-      // ── Core prediction markets ──
-      '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',
-      // ── Economics & Finance ──
-      'economics', 'wallstreetbets', 'economy', 'inflation', 'FederalReserve',
-      'StockMarket', 'investing', 'RealEstate', 'finance', 'commodities',
-      // ── Sports ──
-      'nfl', 'nba', 'baseball', 'soccer', 'MMA', 'CFB', 'CollegeBasketball',
-      'formula1', 'golf', 'hockey', 'tennis', 'boxing',
-      // ── Crypto ──
-      'CryptoMarkets', 'Bitcoin', 'ethereum', 'CryptoCurrency', 'BitcoinMarkets', 'solana',
-      // ── Geopolitics ──
-      'worldnews', 'geopolitics', 'UkrainianConflict', 'ukraine', 'China',
-      'MiddleEastNews', 'foreignpolicy', 'military',
-      // ── Entertainment (awards/box office markets) ──
-      'movies', 'boxoffice', 'television', 'Oscars', 'Music', 'entertainment',
-      // ── Science & Space (launch/milestone markets) ──
-      'SpaceX', 'space', 'nasa', 'science', 'Futurology',
-      // ── Tech & AI ──
-      'technology', 'artificial', 'MachineLearning', 'OpenAI', 'singularity', 'Nvidia',
-      // ── General sentiment ──
-      'news', 'OutOfTheLoop', 'AskReddit', 'TrueReddit', 'dataisbeautiful',
-    ];
-    // Process subreddits in parallel batches of 12 (maxed for 16 cores)
-    const REDDIT_BATCH = 12;
-    for (let i = 0; i < hotSubs.length; i += REDDIT_BATCH) {
-      const batch = hotSubs.slice(i, i + REDDIT_BATCH);
-      const results = await Promise.allSettled(batch.map(sub => scrapeSubredditHot(sub, 8)));
-      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) {
-          if (post.score > 30) { // lower threshold to catch more sentiment signals
-            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 ('reddit_hot', $1, 'hot', $2, $3, $4, $5, $6, $7)
-              ON CONFLICT DO NOTHING
-            `, [sub, post.title, post.url, post.score, post.num_comments, sentiment, '']);
-            totalPosts++;
-          }
-        }
-      }
-      await new Promise(r => setTimeout(r, 500)); // 0.5s between batches (was 1.5s)
-    }
-    console.log(`[Ken] Reddit hot: ${hotSubs.length} subreddits across 10 categories`);
-    logMemory('after-reddit');
-    if (global.gc) global.gc();
-
-    // Scrape Google News for top market topics (covers all Kalshi categories)
-    const newsQueries = [
-      // Politics & elections
-      'trump cabinet resign', 'federal reserve chair nomination', '2028 presidential election',
-      'trump executive order', 'government shutdown', 'supreme court ruling',
-      // Economics
-      'CPI inflation report', 'federal reserve rate decision', 'jobs report unemployment',
-      'GDP growth', 'recession forecast',
-      // Weather
-      'hurricane forecast atlantic', 'severe weather outbreak', 'temperature record',
-      // Geopolitics
-      'ukraine russia ceasefire', 'china taiwan', 'middle east conflict',
-      // Crypto
-      'bitcoin price prediction', 'SEC crypto regulation',
-      // Entertainment
-      'oscar nominations', 'box office weekend',
-      // Space
-      'SpaceX launch', 'NASA artemis',
-    ];
-    // Process Google News in batches of 5 (21 serial × 1s = too slow)
-    const NEWS_QUERY_BATCH = 5;
-    for (let i = 0; i < newsQueries.length; i += NEWS_QUERY_BATCH) {
-      const batch = newsQueries.slice(i, i + NEWS_QUERY_BATCH);
-      const results = await Promise.allSettled(
-        batch.map(nq => scrapeGoogleNews(nq, 5).then(articles => ({ nq, articles })))
-      );
-      for (const result of results) {
-        if (result.status !== 'fulfilled') continue;
-        const { nq, articles } = result.value;
-        for (const article of articles) {
-          const sentiment = scoreSentiment(article.title);
-          await kenQ(`
-            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score)
-            VALUES ('google_news', $1, $2, $3, $4, 0, $5)
-            ON CONFLICT DO NOTHING
-          `, [article.source || 'news', nq, article.title, article.url, sentiment]);
-          totalPosts++;
-        }
-      }
-      await new Promise(r => setTimeout(r, 1000));
-    }
-
-    // ── Hacker News (tech/policy sentiment) ──
-    const hnQueries = ['trump policy', 'federal reserve', 'tariff', 'ukraine russia', null]; // null = front page
-    for (const hq of hnQueries) {
-      try {
-        const stories = await scrapeHackerNews(hq, 10);
-        for (const story of stories) {
-          if (story.score > 5 || story.num_comments > 3) {
-            const sentiment = scoreSentiment(story.title);
-            await kenQ(`
-              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-              VALUES ('hackernews', 'hackernews', $1, $2, $3, $4, $5, $6, '')
-              ON CONFLICT DO NOTHING
-            `, [hq || 'front_page', story.title, story.url, story.score, story.num_comments, sentiment]);
-            totalPosts++;
-          }
-        }
-      } catch {}
-      await new Promise(r => setTimeout(r, 1500));
-    }
-
-    // ── All News Channels (BBC, NYT, WaPo, Politico, CNBC, Bloomberg, etc.) ──
-    try {
-      let newsStored = 0;
-      // Process feeds in small batches to limit memory — don't hold all articles at once
-      const batchSize = 5;
-      for (let i = 0; i < NEWS_FEEDS.length; i += batchSize) {
-        const batch = NEWS_FEEDS.slice(i, i + batchSize);
-        const results = await Promise.allSettled(
-          batch.map(feed => scrapeRSSFeed(feed.url, feed.source, 6))
-        );
-        for (const result of results) {
-          if (result.status !== 'fulfilled') continue;
-          for (const article of result.value) {
-            const sentiment = scoreSentiment(article.title);
-            await kenQ(`
-              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-              VALUES ($1, $2, 'news', $3, $4, 0, $5, '')
-              ON CONFLICT DO NOTHING
-            `, [article.source.toLowerCase().replace(/\s+/g, '_'), article.source, article.title, article.url, sentiment]);
-            newsStored++;
-          }
-        }
-        await new Promise(r => setTimeout(r, 300));
-      }
-      totalPosts += newsStored;
-      console.log(`[Ken] News channels: ${newsStored} articles from ${NEWS_FEEDS.length} feeds`);
-    } catch (e) {
-      console.log(`[Ken] News channels failed: ${e.message}`);
-    }
-    logMemory('after-news');
-    if (global.gc) global.gc();
-
-    // ── Metaculus (prediction market community consensus) ──
-    try {
-      const metaQuestions = await scrapeMetaculus();
-      for (const mq of metaQuestions) {
-        const sentiment = scoreSentiment(mq.title);
-        const predText = mq.community_prediction ? `Community: ${Math.round(mq.community_prediction * 100)}%` : '';
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('metaculus', 'metaculus', 'predictions', $1, $2, $3, $4, $5, $6)
-          ON CONFLICT DO NOTHING
-        `, [mq.title, mq.url, mq.num_predictions || 0, 0, sentiment, predText]);
-        totalPosts++;
-      }
-      console.log(`[Ken] Metaculus: ${metaQuestions.length} predictions`);
-    } catch (e) {
-      console.log(`[Ken] Metaculus failed: ${e.message}`);
-    }
-
-    // ── Manifold Markets (cross-reference prediction probabilities) ──
-    let manifoldTotal = 0;
-    try {
-      const manifoldQueries = ['trump', 'fed rate', 'election 2028', 'ukraine', 'tariff'];
-      const manifoldMkts = await scrapeManifold(manifoldQueries);
-      for (const m of manifoldMkts) {
-        const sentiment = scoreSentiment(m.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('manifold', 'manifold', $1, $2, $3, $4, 0, $5, $6)
-          ON CONFLICT DO NOTHING
-        `, [m.source, m.title, m.url, m.volume || 0, sentiment, `prob:${m.probability}% vol:${m.volume}`]);
-        manifoldTotal++;
-      }
-    } catch {}
-    console.log(`[Ken] Manifold Markets: ${manifoldTotal} prediction markets`);
-
-    // ── Kalshi & Polymarket Reddit search (community sentiment on specific trades) ──
-    let predMarketRedditTotal = 0;
-    const pmSearchTerms = ['arbitrage', 'free money', 'mispriced', 'edge'];
-    for (const term of pmSearchTerms) {
-      for (const sub of ['Kalshi', 'Polymarket']) {
-        try {
-          const posts = await scrapeReddit(sub, term, 5);
-          for (const post of posts) {
-            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 ('reddit_predmkt', $1, $2, $3, $4, $5, $6, $7, '')
-              ON CONFLICT DO NOTHING
-            `, [sub, term, post.title, post.url, post.score || 0, post.num_comments || 0, sentiment]);
-            predMarketRedditTotal++;
-          }
-        } catch {}
-      }
-      await new Promise(r => setTimeout(r, 2000));
-    }
-    console.log(`[Ken] Prediction market Reddit: ${predMarketRedditTotal} posts from r/Kalshi + r/Polymarket`);
-    if (global.gc) global.gc();
-
-    // ── GDELT TV News (CNN, Fox closed captions) ──
-    const tvStations = ['CNN', 'FOXNEWS'];
-    const tvQueries = ['trump', 'tariff', 'ukraine', 'election'];
-    let tvTotal = 0;
-    for (const station of tvStations) {
-      for (const tq of tvQueries) {
-        try {
-          const clips = await scrapeGDELTtv(tq, station, 5);
-          for (const clip of clips) {
-            if (clip.title && clip.title.length > 15) {
-              const sentiment = scoreSentiment(clip.title);
-              await kenQ(`
-                INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-                VALUES ('tv_news', $1, $2, $3, $4, 0, $5, $6)
-                ON CONFLICT DO NOTHING
-              `, [clip.source || station, tq, clip.title.substring(0, 500), clip.url, sentiment, clip.show || '']);
-              tvTotal++;
-            }
-          }
-        } catch {}
-        await new Promise(r => setTimeout(r, 500));
-      }
-    }
-    console.log(`[Ken] GDELT TV: ${tvTotal} clips from ${tvStations.length} stations`);
-
-    // ── YouTube News Channels (CNN, MSNBC, Fox, Bloomberg, CNBC, etc.) ──
-    try {
-      const ytVideos = await scrapeYouTubeRSS();
-      let ytStored = 0;
-      for (const video of ytVideos) {
-        const sentiment = scoreSentiment(video.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-          VALUES ('youtube_news', $1, 'video', $2, $3, 0, $4, '')
-          ON CONFLICT DO NOTHING
-        `, [video.source, video.title, video.url, sentiment]);
-        ytStored++;
-      }
-      totalPosts += ytStored;
-      console.log(`[Ken] YouTube News: ${ytStored} videos from 9 channels`);
-    } catch (e) {
-      console.log(`[Ken] YouTube News failed: ${e.message}`);
-    }
-    if (global.gc) global.gc();
-
-    // ── Lemmy (federated Reddit — public API, no auth) ──
-    let lemmyTotal = 0;
-    try {
-      const lemmyPosts = await scrapeLemmy('Hot', 25);
-      for (const p of lemmyPosts) {
-        const sentiment = scoreSentiment(p.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('lemmy', $1, 'hot', $2, $3, $4, $5, $6, '')
-          ON CONFLICT DO NOTHING
-        `, [p.source, p.title, p.url, p.score || 0, p.num_comments || 0, sentiment]);
-        lemmyTotal++;
-      }
-    } catch {}
-    console.log(`[Ken] Lemmy: ${lemmyTotal} posts`);
-
-    // ── Lobsters (tech/policy news — public JSON API) ──
-    let lobstersTotal = 0;
-    try {
-      const stories = await scrapeLobsters(25);
-      for (const s of stories) {
-        const sentiment = scoreSentiment(s.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('lobsters', $1, $2, $3, $4, $5, $6, $7, '')
-          ON CONFLICT DO NOTHING
-        `, [s.author || 'lobsters', s.tags || 'tech', s.title, s.url, s.score || 0, s.num_comments || 0, sentiment]);
-        lobstersTotal++;
-      }
-    } catch {}
-    console.log(`[Ken] Lobsters: ${lobstersTotal} stories`);
-
-    // ── Tildes (curated discussion — RSS) ──
-    let tildesTotal = 0;
-    try {
-      const tildesPosts = await scrapeTildes();
-      for (const p of tildesPosts) {
-        const sentiment = scoreSentiment(p.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-          VALUES ('tildes', 'tildes', 'discussion', $1, $2, 0, $3, '')
-          ON CONFLICT DO NOTHING
-        `, [p.title, p.url, sentiment]);
-        tildesTotal++;
-      }
-    } catch {}
-    console.log(`[Ken] Tildes: ${tildesTotal} discussions`);
-
-    // ── Bluesky (trending feeds — getFeed API) ──
-    let bskyTotal = 0;
-    try {
-      const posts = await scrapeBluesky();
-      for (const p of posts) {
-        if ((p.score || 0) > 2 || (p.num_comments || 0) > 1 || p.title.length > 30) {
-          const sentiment = scoreSentiment(p.title);
-          await kenQ(`
-            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-            VALUES ('bluesky', $1, $2, $3, $4, $5, $6, $7, '')
-            ON CONFLICT DO NOTHING
-          `, [p.author || 'bluesky', p.source || 'trending', p.title, p.url, p.score || 0, p.num_comments || 0, sentiment]);
-          bskyTotal++;
-        }
-      }
-    } catch {}
-    console.log(`[Ken] Bluesky: ${bskyTotal} posts`);
-
-    // ── Mastodon/Fediverse trending ──
-    try {
-      const mastodonPosts = await scrapeMastodon();
-      let mastoTotal = 0;
-      for (const p of mastodonPosts) {
-        const sentiment = scoreSentiment(p.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('mastodon', $1, 'trending', $2, $3, $4, $5, $6, '')
-          ON CONFLICT DO NOTHING
-        `, [p.source, p.title, p.url, p.score || 0, p.num_comments || 0, sentiment]);
-        mastoTotal++;
-      }
-      totalPosts += mastoTotal;
-      console.log(`[Ken] Mastodon: ${mastoTotal} trending`);
-    } catch (e) {
-      console.log(`[Ken] Mastodon failed: ${e.message}`);
-    }
-
-    totalPosts += tvTotal + lemmyTotal + lobstersTotal + tildesTotal + bskyTotal;
-    totalPosts += manifoldTotal + predMarketRedditTotal;
-
-    // ── PredictIt (cross-reference prediction market) ──
-    let predictItTotal = 0;
-    try {
-      const piMarkets = await scrapePredictIt();
-      for (const m of piMarkets) {
-        const sentiment = scoreSentiment(m.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('predictit', 'predictit', 'market', $1, $2, $3, $4, $5, $6)
-          ON CONFLICT DO NOTHING
-        `, [m.title, m.url, m.score, m.num_comments || 0, sentiment, m.extra || '']);
-        predictItTotal++;
-      }
-      totalPosts += predictItTotal;
-      console.log(`[Ken] PredictIt: ${predictItTotal} markets`);
-    } catch (e) {
-      console.log(`[Ken] PredictIt failed: ${e.message}`);
-    }
-
-    // ── FRED Economic Indicators (real data for economic markets) ──
-    let fredTotal = 0;
-    try {
-      const fredData = await scrapeFredIndicators();
-      for (const ind of fredData) {
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-          VALUES ('fred_economic', 'fred', $1, $2, $3, $4, 0, '')
-          ON CONFLICT DO NOTHING
-        `, [ind.keyword, ind.title, ind.url, ind.score]);
-        fredTotal++;
-      }
-      totalPosts += fredTotal;
-      console.log(`[Ken] FRED Economic: ${fredTotal} indicators`);
-    } catch (e) {
-      console.log(`[Ken] FRED failed: ${e.message}`);
-    }
-
-    // ── NWS Active Weather Alerts (severe weather data for weather markets) ──
-    let nwsAlertTotal = 0;
-    try {
-      const alerts = await scrapeNWSAlerts();
-      for (const alert of alerts) {
-        const sentiment = scoreSentiment(alert.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('nws_alerts', 'weather', $1, $2, $3, $4, 0, $5, '')
-          ON CONFLICT DO NOTHING
-        `, [alert.keyword, alert.title, alert.url, alert.score, sentiment]);
-        nwsAlertTotal++;
-      }
-      totalPosts += nwsAlertTotal;
-      console.log(`[Ken] NWS Alerts: ${nwsAlertTotal} active alerts`);
-    } catch (e) {
-      console.log(`[Ken] NWS Alerts failed: ${e.message}`);
-    }
-
-    // ── GovTrack Congressional Votes (legislation for political markets) ──
-    let govTrackTotal = 0;
-    try {
-      const votes = await scrapeGovTrack();
-      for (const v of votes) {
-        const sentiment = scoreSentiment(v.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('govtrack', 'congress', $1, $2, $3, $4, $5, $6, '')
-          ON CONFLICT DO NOTHING
-        `, [v.keyword, v.title, v.url, v.score, v.num_comments || 0, sentiment]);
-        govTrackTotal++;
-      }
-      totalPosts += govTrackTotal;
-      console.log(`[Ken] GovTrack: ${govTrackTotal} congressional votes`);
-    } catch (e) {
-      console.log(`[Ken] GovTrack failed: ${e.message}`);
-    }
-
-    // ── Twitter/X via Nitter (public sentiment, no auth needed) ──
-    let nitterTotal = 0;
-    try {
-      const tweets = await scrapeNitterTrending();
-      for (const t of tweets) {
-        const sentiment = scoreSentiment(t.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('twitter_nitter', 'twitter', $1, $2, $3, 0, 0, $4, '')
-          ON CONFLICT DO NOTHING
-        `, [t.keyword, t.title, t.url, sentiment]);
-        nitterTotal++;
-      }
-      totalPosts += nitterTotal;
-      console.log(`[Ken] Twitter/Nitter: ${nitterTotal} tweets`);
-    } catch (e) {
-      console.log(`[Ken] Twitter/Nitter failed: ${e.message}`);
-    }
-
-    // ── Discord (Kalshi, Polymarket, Prediction Markets servers) ──
-    let discordTotal = 0;
-    try {
-      const discordMessages = await scrapeDiscord(30);
-      for (const msg of discordMessages) {
-        const sentiment = scoreSentiment(msg.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('discord', $1, $2, $3, $4, $5, $6, $7, $8)
-          ON CONFLICT DO NOTHING
-        `, [
-          msg.source, msg.channel || 'general', msg.title.substring(0, 500),
-          msg.url, msg.score || 0, msg.num_comments || 0, sentiment,
-          (msg.author || '').substring(0, 200),
-        ]);
-        discordTotal++;
-      }
-      totalPosts += discordTotal;
-      console.log(`[Ken] Discord: ${discordTotal} messages`);
-    } catch (e) {
-      console.log(`[Ken] Discord failed: ${e.message}`);
-    }
-    if (global.gc) global.gc();
-
-    logMemory('sweep-end');
-    if (global.gc) global.gc();
-    console.log(`[Ken] ═══ FULL SWEEP: ${totalPosts} items (Reddit, HN, ${NEWS_FEEDS.length} feeds, TV, YT, Manifold, r/Kalshi, r/Polymarket, Lemmy, Lobsters, Tildes, Bluesky, Mastodon, Metaculus, PredictIt, FRED, NWS Alerts, GovTrack, Twitter, Discord) ═══`);
-    return totalPosts;
-  } catch (e) {
-    console.error('[Ken] Intelligence sweep error:', e.message);
-    logMemory('sweep-error');
-    return 0;
-  }
-}
-
-// ══════════════════════════════════════════════════════════════════════════════
-// SIGNAL PIPELINE: News → Sentiment → Event Map → Probability → Signals → Risk
-// Pipeline: narrative shifts → probability inference → edge detection
-// ══════════════════════════════════════════════════════════════════════════════
-
-// ── Enhanced Event-Specific Sentiment Engine ──
-// Unlike scoreSentiment() which is general, this maps headlines to specific market directions
-const EVENT_SENTIMENT_RULES = [
-  // Weather markets
-  { pattern: /hurricane|tropical storm|cyclone/i, category: 'weather', direction: 'severity', boost: 0.3 },
-  { pattern: /category\s*[3-5]|major hurricane|extreme/i, category: 'weather', direction: 'severity', boost: 0.5 },
-  { pattern: /weakens|dissipates|downgrade/i, category: 'weather', direction: 'severity', boost: -0.4 },
-  { pattern: /record heat|heat wave|above normal/i, category: 'weather', direction: 'temperature_high', boost: 0.4 },
-  { pattern: /cold snap|polar vortex|below normal|freeze/i, category: 'weather', direction: 'temperature_low', boost: 0.4 },
-  { pattern: /heavy rain|flood|rainfall record/i, category: 'weather', direction: 'precipitation_high', boost: 0.4 },
-  { pattern: /drought|dry spell|no rain/i, category: 'weather', direction: 'precipitation_low', boost: 0.4 },
-  // Political markets
-  { pattern: /resign|step down|quit|fired/i, category: 'politics', direction: 'negative', boost: 0.5 },
-  { pattern: /appointed|confirmed|sworn in|nominated/i, category: 'politics', direction: 'positive', boost: 0.4 },
-  { pattern: /impeach|investigation|scandal|indicted/i, category: 'politics', direction: 'negative', boost: 0.4 },
-  { pattern: /leading|ahead|polling|surge in polls/i, category: 'politics', direction: 'positive', boost: 0.3 },
-  { pattern: /trailing|behind|falling|losing ground/i, category: 'politics', direction: 'negative', boost: 0.3 },
-  { pattern: /shutdown|deadlock|stalemate/i, category: 'politics', direction: 'shutdown_yes', boost: 0.5 },
-  { pattern: /deal reached|agreement|bipartisan|passed/i, category: 'politics', direction: 'shutdown_no', boost: -0.4 },
-  // Economic markets
-  { pattern: /rate cut|dovish|easing/i, category: 'economics', direction: 'rate_down', boost: 0.4 },
-  { pattern: /rate hike|hawkish|tightening/i, category: 'economics', direction: 'rate_up', boost: 0.4 },
-  { pattern: /inflation.*(?:hot|higher|above|surge|accelerat)/i, category: 'economics', direction: 'inflation_up', boost: 0.4 },
-  { pattern: /inflation.*(?:cool|lower|below|slow|deceler)/i, category: 'economics', direction: 'inflation_down', boost: 0.4 },
-  { pattern: /recession|contraction|GDP.*(?:negative|shrink)/i, category: 'economics', direction: 'recession_yes', boost: 0.5 },
-  { pattern: /growth|expansion|GDP.*(?:beat|strong|positive)/i, category: 'economics', direction: 'recession_no', boost: -0.4 },
-  { pattern: /jobs.*(?:beat|strong|added more)/i, category: 'economics', direction: 'jobs_strong', boost: 0.3 },
-  { pattern: /jobs.*(?:miss|weak|fewer)/i, category: 'economics', direction: 'jobs_weak', boost: 0.3 },
-  // Crypto markets
-  { pattern: /bitcoin.*(?:new high|surge|rally|breakout|100k|150k)/i, category: 'crypto', direction: 'btc_up', boost: 0.4 },
-  { pattern: /bitcoin.*(?:crash|plunge|selloff|bear)/i, category: 'crypto', direction: 'btc_down', boost: 0.4 },
-  { pattern: /SEC.*(?:approve|green light)/i, category: 'crypto', direction: 'btc_up', boost: 0.3 },
-  { pattern: /SEC.*(?:reject|deny|sue)/i, category: 'crypto', direction: 'btc_down', boost: 0.3 },
-  // Sports/entertainment
-  { pattern: /favou?rite|odds.*increase|bet.*heavy/i, category: 'sports', direction: 'favourite_stronger', boost: 0.3 },
-  { pattern: /upset|underdog|surprise/i, category: 'sports', direction: 'favourite_weaker', boost: 0.3 },
-  { pattern: /injured|out for|suspended|DNP/i, category: 'sports', direction: 'team_weaker', boost: 0.4 },
-  // Space/tech
-  { pattern: /launch.*(?:success|orbit|nominal)/i, category: 'space', direction: 'launch_success', boost: 0.5 },
-  { pattern: /launch.*(?:scrub|delay|abort|fail)/i, category: 'space', direction: 'launch_fail', boost: 0.5 },
-  // Geopolitics
-  { pattern: /ceasefire|peace.*(?:deal|talks|agreement)/i, category: 'geopolitics', direction: 'de_escalation', boost: 0.5 },
-  { pattern: /escalat|offensive|invasion|attack|strike/i, category: 'geopolitics', direction: 'escalation', boost: 0.4 },
-];
-
-function scoreEventSentiment(headline, marketTitle = '') {
-  const text = `${headline} ${marketTitle}`.toLowerCase();
-  const result = {
-    sentiment: 0,
-    confidence: 0,
-    magnitude: 0,
-    category: 'general',
-    direction: 'neutral',
-    matchedRules: [],
-  };
-
-  // Apply rule-based event-specific scoring
-  for (const rule of EVENT_SENTIMENT_RULES) {
-    if (rule.pattern.test(text)) {
-      result.sentiment += rule.boost;
-      result.magnitude += Math.abs(rule.boost);
-      result.category = rule.category;
-      result.direction = rule.direction;
-      result.matchedRules.push(rule.direction);
-    }
-  }
-
-  // Certainty language modifiers
-  const certainWords = /confirm|official|announce|breaking|just.*happen/i;
-  const uncertainWords = /may|might|could|rumor|unconfirm|speculat|sources say/i;
-  if (certainWords.test(text)) { result.confidence += 0.3; result.magnitude *= 1.2; }
-  if (uncertainWords.test(text)) { result.confidence -= 0.2; result.magnitude *= 0.7; }
-
-  // Multiple source agreement boosts confidence
-  if (result.matchedRules.length > 1) result.confidence += 0.15;
-
-  // Normalize
-  result.sentiment = Math.max(-1, Math.min(1, result.sentiment));
-  result.confidence = Math.max(0, Math.min(1, 0.5 + result.confidence)); // base confidence 0.5
-  result.magnitude = Math.min(1, result.magnitude);
-
-  return result;
-}
-
-// ── Event Mapper: Map news entities → Kalshi market tickers ──
-// Uses keyword-based + fuzzy matching to connect news to specific contracts
-const MARKET_CATEGORY_KEYWORDS = {
-  // Weather
-  'temperature': ['temperature', 'temp', 'heat', 'cold', 'warm', 'degree', 'fahrenheit', 'celsius'],
-  'hurricane': ['hurricane', 'tropical', 'cyclone', 'storm', 'named storm'],
-  'rainfall': ['rain', 'rainfall', 'precipitation', 'flood', 'drought', 'inches'],
-  // Politics
-  'election': ['election', 'vote', 'ballot', 'primary', 'nominee', 'candidate', 'poll'],
-  'government': ['resign', 'cabinet', 'secretary', 'appointment', 'nomination', 'impeach'],
-  'legislation': ['bill', 'act', 'congress', 'senate', 'house', 'passed', 'shutdown'],
-  // Economics
-  'fed_rate': ['federal reserve', 'fed rate', 'interest rate', 'fomc', 'rate cut', 'rate hike', 'powell'],
-  'inflation': ['cpi', 'inflation', 'consumer price', 'pce', 'core inflation'],
-  'gdp': ['gdp', 'gross domestic', 'economic growth', 'recession', 'expansion'],
-  'jobs': ['jobs report', 'unemployment', 'nonfarm', 'payroll', 'jobless claims'],
-  // Crypto
-  'bitcoin': ['bitcoin', 'btc', 'cryptocurrency', 'crypto'],
-  'ethereum': ['ethereum', 'eth', 'ether'],
-  // Space
-  'spacex': ['spacex', 'falcon', 'starship', 'rocket launch', 'orbit'],
-  'nasa': ['nasa', 'artemis', 'moon', 'mars mission'],
-};
-
-// Map ticker suffixes to searchable names for multi-outcome events
-const TICKER_ENTITY_MAP = {
-  // KXCABOUT - Cabinet members
-  'RFK':  ['rfk', 'kennedy', 'robert kennedy', 'rfk jr', 'health and human'],
-  'KNOE': ['noem', 'kristi noem', 'homeland security'],
-  'SDUF': ['duffy', 'sean duffy', 'transportation'],
-  'BROL': ['rollins', 'brooke rollins', 'agriculture'],
-  'JRAT': ['ratcliffe', 'john ratcliffe', 'cia director'],
-  'SBES': ['bessent', 'scott bessent', 'treasury secretary'],
-  'MRUB': ['rubio', 'marco rubio', 'state department', 'secretary of state'],
-  'PHEG': ['hegseth', 'pete hegseth', 'defense secretary'],
-  'TGAB': ['gabbard', 'tulsi gabbard', 'intelligence director'],
-  'MWAL': ['waltz', 'mike waltz', 'national security'],
-  'HLUT': ['lutnick', 'howard lutnick', 'commerce secretary'],
-  'DBUR': ['burgum', 'doug burgum', 'interior secretary'],
-  'CWRI': ['chris wright', 'energy secretary'],
-  'LZEL': ['zeldin', 'lee zeldin', 'epa administrator'],
-  'DCOL': ['collins', 'doug collins', 'veterans affairs'],
-  'LMCM': ['mcmahon', 'linda mcmahon', 'education secretary'],
-  'PBON': ['bondi', 'pam bondi', 'attorney general'],
-  'RVOU': ['vought', 'russell vought', 'omb director'],
-  'STUR': ['turner', 'scott turner', 'hud secretary'],
-  'LCDR': ['cruz', 'linda cruz', 'sba administrator'],
-  'MKRA': ['kratsios', 'michael kratsios'],
-  'SWIL': ['wilson', 'sean wilson'],
-  // KXPRESPERSON - 2028 Presidential election
-  'DTRU':   ['trump', 'donald trump', 'president trump', 'former president trump'],
-  'DTRUJR': ['trump jr', 'donald trump jr', 'don jr', 'trump junior'],
-  'RDES':   ['desantis', 'ron desantis', 'florida governor'],
-  'JVAN':   ['vance', 'jd vance', 'vice president vance', 'j.d. vance'],
-  'GYOU':   ['youngkin', 'glenn youngkin', 'virginia governor'],
-  // KXFEDCHAIRNOM - Fed Chair nominees
-  'JS':   ['dimon', 'jamie dimon'],
-  'KW':   ['warsh', 'kevin warsh'],
-  'KH':   ['hassett', 'kevin hassett'],
-  'CWAL': ['waller', 'chris waller', 'christopher waller'],
-  'MBOW': ['bowman', 'michelle bowman'],
-};
-
-function extractMarketEntity(market) {
-  // Extract ticker suffix for entity lookup first (most reliable)
-  const parts = market.ticker.split('-');
-  const suffix = parts[parts.length - 1];
-  if (TICKER_ENTITY_MAP[suffix]) return TICKER_ENTITY_MAP[suffix];
-  // Try subtitle only if it's a real description (not just the ticker repeated)
-  if (market.subtitle && market.subtitle !== market.ticker && !market.subtitle.startsWith('KX')) {
-    return market.subtitle.toLowerCase().split(/[\s\-]+/).filter(w => w.length > 2);
-  }
-  // Try the yes_sub_title field
-  if (market.yes_sub_title && market.yes_sub_title !== market.ticker) {
-    return market.yes_sub_title.toLowerCase().split(/[\s\-]+/).filter(w => w.length > 2);
-  }
-  return [];
-}
-
-function isMultiOutcomeEvent(ticker) {
-  // Multi-outcome events have a common prefix followed by person/answer suffix
-  // e.g., KXCABOUT-29JAN-RFK, KXCABOUT-29JAN-KNOE all share KXCABOUT-29JAN
-  const parts = ticker.split('-');
-  return parts.length >= 3; // Event prefix + date + answer
-}
-
-function getEventPrefix(ticker) {
-  const parts = ticker.split('-');
-  if (parts.length >= 3) return parts.slice(0, -1).join('-');
-  return ticker;
-}
-
-function mapNewsToMarkets(headline, activeMarkets) {
-  const headlineLower = headline.toLowerCase();
-  const matches = [];
-
-  // Step 1: Identify which categories the headline relates to
-  const relevantCategories = [];
-  for (const [category, keywords] of Object.entries(MARKET_CATEGORY_KEYWORDS)) {
-    for (const kw of keywords) {
-      if (headlineLower.includes(kw)) {
-        relevantCategories.push(category);
-        break;
-      }
-    }
-  }
-
-  // Step 2: Group markets by event to detect multi-outcome events
-  const eventGroups = {};
-  for (const market of activeMarkets) {
-    const prefix = getEventPrefix(market.ticker);
-    if (!eventGroups[prefix]) eventGroups[prefix] = [];
-    eventGroups[prefix].push(market);
-  }
-
-  // Step 3: Find matching Kalshi markets by title/subtitle keywords
-  for (const market of activeMarkets) {
-    const marketText = `${market.title} ${market.subtitle || ''} ${market.yes_sub_title || ''}`.toLowerCase();
-    let matchScore = 0;
-
-    // Direct keyword overlap
-    const headlineWords = headlineLower.split(/\s+/).filter(w => w.length > 3);
-    for (const word of headlineWords) {
-      if (marketText.includes(word)) matchScore += 1;
-    }
-
-    // Category match bonus
-    for (const cat of relevantCategories) {
-      const catKws = MARKET_CATEGORY_KEYWORDS[cat];
-      if (catKws.some(kw => marketText.includes(kw))) matchScore += 2;
-    }
-
-    // Entity matching (city names, person names, etc.)
-    const cities = ['phoenix', 'houston', 'miami', 'chicago', 'new york', 'los angeles', 'atlanta', 'dallas', 'denver', 'seattle', 'boston', 'detroit', 'minneapolis', 'philadelphia'];
-    for (const city of cities) {
-      if (headlineLower.includes(city) && marketText.includes(city)) matchScore += 5;
-    }
-
-    // Political figure matching
-    const figures = ['trump', 'biden', 'harris', 'desantis', 'newsom', 'vance', 'powell', 'yellen'];
-    for (const fig of figures) {
-      if (headlineLower.includes(fig) && marketText.includes(fig)) matchScore += 5;
-    }
-
-    // MULTI-OUTCOME EVENT HANDLING: For events with multiple answer markets,
-    // require headline to mention the specific answer entity, not just the general topic
-    const eventPrefix = getEventPrefix(market.ticker);
-    const siblings = eventGroups[eventPrefix] || [];
-    if (siblings.length > 1 && isMultiOutcomeEvent(market.ticker)) {
-      const entityTerms = extractMarketEntity(market);
-      const entityMatch = entityTerms.some(term => headlineLower.includes(term));
-      if (entityMatch) {
-        matchScore += 8; // Strong boost for specific entity match
-      } else {
-        // Headline mentions the topic but NOT this specific answer — reject entirely
-        // With 20+ siblings, even small scores cause massive clustering
-        matchScore = 0;
-      }
-    }
-
-    if (matchScore >= 3) {
-      matches.push({ market, matchScore });
-    }
-  }
-
-  // Sort by match score descending
-  matches.sort((a, b) => b.matchScore - a.matchScore);
-  return matches.slice(0, 5); // Top 5 matches
-}
-
-// ── Probability Shift Model (Bayesian Update) ──
-// adjustedProb = priorProb + (sentiment * weight * confidence)
-// With decay for older signals, source diversity bonus, and category-specific weights
-const CATEGORY_SENTIMENT_WEIGHTS = {
-  politics: 0.18,    // Political events move markets fast
-  economics: 0.16,   // Economic data is highly impactful
-  crypto: 0.22,      // Crypto is volatile, sentiment-driven
-  weather: 0.10,     // Weather has model data, less sentiment-dependent
-  geopolitics: 0.20, // Geopolitical events create large moves
-  sports: 0.12,      // Sports have existing odds infrastructure
-  space: 0.14,       // Launch outcomes are binary
-  general: 0.12,     // Default
-};
-
-function calculateProbabilityShift(priorProb, sentimentSignals) {
-  if (!sentimentSignals.length) return { adjustedProb: priorProb, impact: 0, numArticles: 0, agreement: 0, sourceDiversity: 0 };
-
-  const RECENCY_DECAY = 0.85; // Faster decay — old news matters less
-  const AGREEMENT_BONUS = 1.5; // Stronger bonus for agreement
-  const DISAGREEMENT_PENALTY = 0.3; // Stronger penalty for mixed signals
-
-  let totalImpact = 0;
-  let totalWeight = 0;
-  const now = Date.now();
-
-  // Sort by recency
-  const sorted = [...sentimentSignals].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
-
-  // Calculate source diversity (unique sources boost signal)
-  const uniqueSources = new Set(sorted.map(s => s.source));
-  const sourceDiversity = Math.min(1, uniqueSources.size / 4); // 4+ unique sources = max diversity
-
-  for (const sig of sorted) {
-    const hoursAgo = Math.max(0, (now - (sig.timestamp || now)) / 3600000);
-    const recencyWeight = Math.pow(RECENCY_DECAY, hoursAgo);
-    // Use category-specific weight instead of flat 0.08
-    const catWeight = CATEGORY_SENTIMENT_WEIGHTS[sig.category] || CATEGORY_SENTIMENT_WEIGHTS.general;
-    // Magnitude amplifier: strong signals (|sentiment| > 0.3) get more weight
-    const magnitudeBoost = 1 + Math.min(0.5, (sig.magnitude || 0) * 0.8);
-    const impact = sig.sentiment * catWeight * sig.confidence * recencyWeight * magnitudeBoost;
-    totalImpact += impact;
-    totalWeight += recencyWeight * sig.confidence;
-  }
-
-  // Source diversity bonus: multiple independent sources confirming = stronger signal
-  totalImpact *= (1 + sourceDiversity * 0.4);
-
-  // Check agreement: if most signals agree on direction, boost; if mixed, penalize
-  const posSignals = sorted.filter(s => s.sentiment > 0).length;
-  const negSignals = sorted.filter(s => s.sentiment < 0).length;
-  const total = posSignals + negSignals;
-  if (total >= 2) {
-    const agreementRatio = Math.max(posSignals, negSignals) / total;
-    if (agreementRatio > 0.8) totalImpact *= AGREEMENT_BONUS; // Strong agreement
-    else if (agreementRatio > 0.6) totalImpact *= 1.1; // Mild agreement
-    else totalImpact *= DISAGREEMENT_PENALTY; // Mixed signals — dampen hard
-  }
-
-  // Diminishing returns: each additional article adds less new information
-  // 1 article = 100%, 4 = 50%, 9 = 33%, 16 = 25%
-  const articleDiminishing = Math.sqrt(sorted.length) / sorted.length;
-  totalImpact *= articleDiminishing;
-
-  // Prior-anchored capping: prevent unrealistic shifts on low-probability markets
-  // A 4% market shouldn't jump to 60% from sentiment alone; cap shift at 5x prior or 15%, whichever is higher
-  const maxShift = Math.max(0.15, priorProb * 5);
-  totalImpact = Math.sign(totalImpact) * Math.min(Math.abs(totalImpact), maxShift);
-
-  // Clamp adjusted probability to [0.01, 0.99]
-  const adjustedProb = Math.max(0.01, Math.min(0.99, priorProb + totalImpact));
-
-  return {
-    adjustedProb,
-    impact: totalImpact,
-    numArticles: sorted.length,
-    agreement: total > 0 ? Math.max(posSignals, negSignals) / total : 0,
-    sourceDiversity,
-  };
-}
-
-// ── Risk Engine ──
-const RISK_LIMITS = {
-  maxExposurePerMarket: 5000, // cents ($50)
-  maxExposurePerTheme: 15000, // cents ($150)
-  maxTotalExposure: 50000, // cents ($500)
-  maxContractsPerMarket: 25,
-  minEdgeThreshold: 0.03, // 3% edge minimum (lowered from 5% — entity-specific matching yields smaller edges)
-  maxDailyLoss: 5000, // cents ($50)
-  cooldownAfterLoss: 30, // minutes
-  timeDecayPenalty: 0.02, // Reduce edge by 2% per hour of signal age
-  minLiquidity: 100, // Minimum 24h volume
-};
-
-function riskCheck(signal, currentExposure = {}) {
-  const reasons = [];
-  let approved = true;
-
-  // Edge threshold
-  if (Math.abs(signal.expectedEdge) < RISK_LIMITS.minEdgeThreshold) {
-    approved = false;
-    reasons.push(`Edge ${(signal.expectedEdge * 100).toFixed(1)}% below ${(RISK_LIMITS.minEdgeThreshold * 100)}% threshold`);
-  }
-
-  // Market exposure
-  const marketExposure = currentExposure[signal.marketId] || 0;
-  if (marketExposure >= RISK_LIMITS.maxExposurePerMarket) {
-    approved = false;
-    reasons.push(`Market exposure $${(marketExposure / 100).toFixed(2)} at limit`);
-  }
-
-  // Liquidity check
-  if ((signal.volume24h || 0) < RISK_LIMITS.minLiquidity) {
-    approved = false;
-    reasons.push(`Low liquidity: ${signal.volume24h || 0} < ${RISK_LIMITS.minLiquidity} volume`);
-  }
-
-  // Time decay — use absolute edge values so BUY_NO signals aren't unfairly penalized
-  const signalAgeHours = (Date.now() - (signal.timestamp || Date.now())) / 3600000;
-  const absEdge = Math.abs(signal.expectedEdge);
-  const decayedAbsEdge = absEdge - (RISK_LIMITS.timeDecayPenalty * signalAgeHours);
-  if (decayedAbsEdge < RISK_LIMITS.minEdgeThreshold) {
-    approved = false;
-    reasons.push(`Edge decayed from ${(absEdge * 100).toFixed(1)}% to ${(decayedAbsEdge * 100).toFixed(1)}%`);
-  }
-
-  // Size recommendation based on edge and confidence
-  let sizeRecommendation = 1;
-  if (approved) {
-    const edgeFactor = Math.min(3, Math.abs(signal.expectedEdge) / RISK_LIMITS.minEdgeThreshold);
-    const confFactor = signal.confidence || 0.5;
-    sizeRecommendation = Math.max(1, Math.min(RISK_LIMITS.maxContractsPerMarket, Math.floor(edgeFactor * confFactor * 10)));
-  }
-
-  return { approved, reasons, sizeRecommendation, decayedEdge: decayedAbsEdge };
-}
-
-// ── Fast Signal Pipeline (runs every 5 minutes) ──
-async function signalPipeline(activeMarkets) {
-  const pipelineStart = Date.now();
-  let signalsGenerated = 0;
-
-  try {
-    // Step 1: Fetch recent news/sentiment from last 30 minutes
-    const { rows: recentArticles } = await kenQ(`
-      SELECT id, source, keyword, post_title as headline, post_url as url, sentiment_score, captured_at
-      FROM ken_sentiment
-      WHERE captured_at > NOW() - INTERVAL '30 minutes'
-      ORDER BY captured_at DESC
-      LIMIT 500
-    `);
-
-    if (recentArticles.length === 0) {
-      console.log('[Ken] Signal Pipeline: no new articles in last 30 min');
-      return 0;
-    }
-
-    // Step 2: Score each article with event-specific sentiment
-    const eventScores = [];
-    for (const article of recentArticles) {
-      const evtScore = scoreEventSentiment(article.headline);
-      if (evtScore.magnitude > 0.1) { // Only process articles with detectable event signal
-        // Step 3: Map to relevant markets
-        const mappedMarkets = mapNewsToMarkets(article.headline, activeMarkets);
-
-        for (const { market, matchScore } of mappedMarkets) {
-          eventScores.push({
-            articleId: String(article.id),
-            headline: article.headline,
-            source: article.source,
-            marketTicker: market.ticker,
-            marketTitle: market.title,
-            sentiment: evtScore.sentiment,
-            confidence: evtScore.confidence * (matchScore / 10), // Scale confidence by match quality
-            magnitude: evtScore.magnitude,
-            category: evtScore.category,
-            direction: evtScore.direction,
-            timestamp: new Date(article.captured_at).getTime(),
-          });
-
-          // Store event-specific sentiment
-          await kenQ(`
-            INSERT INTO ken_event_sentiment (article_id, event_tag, market_ticker, sentiment, confidence, magnitude, headline, source)
-            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
-          `, [String(article.id), evtScore.direction, market.ticker, evtScore.sentiment,
-              evtScore.confidence, evtScore.magnitude, article.headline?.substring(0, 500), article.source]);
-        }
-      }
-    }
-
-    console.log(`[Ken] Signal Pipeline: ${eventScores.length} event-specific scores from ${recentArticles.length} articles`);
-
-    // Step 4: Group by market and calculate probability shifts
-    const marketSignals = {};
-    for (const score of eventScores) {
-      if (!marketSignals[score.marketTicker]) marketSignals[score.marketTicker] = [];
-      marketSignals[score.marketTicker].push(score);
-    }
-
-    // Step 5: For each market with signals, compute probability shift
-    for (const [ticker, signals] of Object.entries(marketSignals)) {
-      const market = activeMarkets.find(m => m.ticker === ticker);
-      if (!market) continue;
-
-      const priorProb = (market.yes_bid + market.yes_ask) / 200; // Convert cents to probability
-      const { adjustedProb, impact, numArticles, agreement, sourceDiversity } = calculateProbabilityShift(priorProb, signals);
-      const marketProb = market.last_price / 100;
-      const edge = adjustedProb - marketProb;
-
-      // Store probability shift
-      await kenQ(`
-        INSERT INTO ken_probability_shifts (market_id, prior_probability, sentiment_impact, adjusted_probability, market_probability, edge, num_articles)
-        VALUES ($1, $2, $3, $4, $5, $6, $7)
-      `, [ticker, priorProb, impact, adjustedProb, marketProb, edge, numArticles]);
-
-      // Step 6: Generate signal if edge exceeds threshold
-      if (Math.abs(edge) >= RISK_LIMITS.minEdgeThreshold) {
-        const direction = edge > 0 ? 'BUY_YES' : 'BUY_NO';
-
-        // Step 6b: Monte Carlo simulation for probability validation (CPU-intensive)
-        const momentum = cachedMomentumMap?.[ticker] || { delta_2h: 0, delta_6h: 0 };
-        const mcResult = monteCarloSimulation({
-          priorProb,
-          sentimentImpact: impact,
-          momentum2h: momentum.delta_2h,
-          momentum6h: momentum.delta_6h,
-          volume24h: market.volume_24h || 0,
-          spread: (market.yes_ask || 0) - (market.yes_bid || 0),
-          articleCount: numArticles,
-          agreement: agreement || 0.5,
-        }, 15000); // 15k iterations for higher accuracy
-
-        // Blend Monte Carlo with statistical model
-        const mcEdge = mcResult.edgeConsistent ? (edge * 0.65 + mcResult.medianEdge * 0.35) : edge * 0.8;
-
-        // Dedup: skip if we already generated a signal for this market+direction in last 30 min
-        // Compare MC-blended edge (mcEdge) against stored MC-blended edge to avoid false positives
-        const recentDup = await kenQ(`
-          SELECT id, expected_edge, confidence FROM ken_strategy_signals
-          WHERE market_id = $1 AND direction = $2 AND created_at > NOW() - INTERVAL '30 minutes'
-          ORDER BY created_at DESC LIMIT 1
-        `, [ticker, direction]);
-        if (recentDup.rows.length > 0) {
-          const prevEdge = parseFloat(recentDup.rows[0].expected_edge);
-          if (Math.abs(Math.abs(mcEdge) - Math.abs(prevEdge)) < 0.02) continue;
-        }
-
-        // Improved confidence using MC validation
-        const edgeScore = Math.min(1, Math.pow(Math.abs(mcEdge) / 0.15, 1.5));
-        const volumeScore = Math.min(1, numArticles / 8);
-        const agreementScore = Math.pow(agreement || 0.5, 0.7);
-        const diversityScore = sourceDiversity || 0;
-        const mcConfBoost = mcResult.confidence > 0.6 ? 0.08 : mcResult.confidence > 0.4 ? 0.04 : 0;
-        const consistencyBoost = mcResult.edgeConsistent ? 0.05 : -0.05;
-        const confidence = Math.min(0.95, Math.max(0.15,
-          edgeScore * 0.30 +
-          agreementScore * 0.18 +
-          volumeScore * 0.18 +
-          diversityScore * 0.12 +
-          mcConfBoost +
-          consistencyBoost +
-          (numArticles >= 5 ? 0.10 : numArticles >= 3 ? 0.05 : 0)
-        ));
-
-        let signal = {
-          marketId: ticker,
-          direction,
-          expectedEdge: mcEdge,
-          confidence,
-          volume24h: market.volume_24h || 0,
-          timestamp: Date.now(),
-          agreement,
-          mcSimulation: {
-            meanEdge: mcResult.meanEdge,
-            stdDev: mcResult.stdDev,
-            percentiles: mcResult.percentiles,
-            consistent: mcResult.edgeConsistent,
-          },
-        };
-
-        // Step 6c: AI Enhancement for high-edge signals (Gemini)
-        if (Math.abs(mcEdge) >= 0.08 && confidence >= 0.4 && numArticles >= 3) {
-          signal = await aiEnhanceSignal(signal, signals, market);
-        }
-
-        // Step 7: Risk check
-        const risk = riskCheck(signal);
-
-        const mcInfo = `MC: μ=${(mcResult.meanEdge * 100).toFixed(1)}%±${(mcResult.stdDev * 100).toFixed(1)}%, p5-p95=[${(mcResult.percentiles.p5 * 100).toFixed(1)}%,${(mcResult.percentiles.p95 * 100).toFixed(1)}%]`;
-        const aiInfo = signal.aiEnhanced ? ` | AI: ${signal.aiReasoning || 'enhanced'}` : '';
-        const reasoning = `${direction} on "${market.title}" — edge ${(mcEdge * 100).toFixed(1)}% from ${numArticles} articles (${signals[0]?.category}/${signals[0]?.direction}). Agreement: ${((agreement || 0) * 100).toFixed(0)}%. ${mcInfo}${aiInfo}. ${risk.approved ? 'RISK APPROVED' : `RISK BLOCKED: ${risk.reasons.join('; ')}`}`;
-
-        await kenQ(`
-          INSERT INTO ken_strategy_signals (market_id, direction, expected_edge, size_recommendation, confidence, reasoning, sources, risk_approved)
-          VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
-        `, [ticker, direction, mcEdge, risk.sizeRecommendation, confidence, reasoning,
-            JSON.stringify(signals.slice(0, 5).map(s => ({ headline: s.headline?.substring(0, 200), source: s.source, sentiment: s.sentiment }))),
-            risk.approved]);
-
-        signalsGenerated++;
-
-        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 50 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));
-        }
-      }
-    }
-
-    const duration = Date.now() - pipelineStart;
-    console.log(`[Ken] Signal Pipeline: ${signalsGenerated} signals from ${Object.keys(marketSignals).length} markets | ${duration}ms`);
-    return signalsGenerated;
-
-  } catch (e) {
-    console.error('[Ken] Signal Pipeline error:', e.message);
-    return 0;
-  }
-}
-
-// ── Auto-mapping: Build market mappings from active Kalshi markets ──
-async function updateMarketMappings(activeMarkets) {
-  try {
-    let mapped = 0;
-    for (const m of activeMarkets) {
-      const titleLower = `${m.title} ${m.subtitle || ''}`.toLowerCase();
-      let category = 'other';
-      const keywords = [];
-
-      for (const [cat, kws] of Object.entries(MARKET_CATEGORY_KEYWORDS)) {
-        for (const kw of kws) {
-          if (titleLower.includes(kw)) {
-            category = cat;
-            keywords.push(kw);
-          }
-        }
-      }
-
-      if (keywords.length > 0) {
-        await kenQ(`
-          INSERT INTO ken_market_mappings (market_id, keywords, category, contract_type)
-          VALUES ($1, $2, $3, 'binary')
-          ON CONFLICT (id) DO NOTHING
-        `, [m.ticker, keywords, category]);
-        mapped++;
-      }
-    }
-    return mapped;
-  } catch (e) {
-    console.log(`[Ken] Market mapping update failed: ${e.message}`);
-    return 0;
-  }
-}
-
-// ══════════════════════════════════════════════
-// POLYMARKET CROSS-REFERENCE (fuzzy matching)
-// ══════════════════════════════════════════════
-const STOP_WORDS = new Set(['will', 'the', 'this', 'that', 'what', 'when', 'where', 'which', 'with', 'from', 'have', 'been', 'before', 'after', 'than', 'more', 'less', 'next', 'last', 'does', 'would', 'could', 'should', 'about', 'into', 'over', 'under', 'between', 'through', 'during', 'each', 'every', 'other', 'some', 'most', 'many', 'much', 'also', 'just', 'only', 'very', 'still', 'even', 'back', 'first', 'year', 'years']);
-
-function extractKeywords(text) {
-  return (text || '').toLowerCase()
-    .replace(/[^a-z0-9\s]/g, ' ')
-    .split(/\s+/)
-    .filter(w => w.length > 2 && !STOP_WORDS.has(w));
-}
-
-function fuzzyMatchScore(textA, textB) {
-  const wordsA = extractKeywords(textA);
-  const wordsB = new Set(extractKeywords(textB));
-  if (wordsA.length === 0 || wordsB.size === 0) return 0;
-  let matches = 0;
-  for (const w of wordsA) {
-    if (wordsB.has(w)) matches++;
-    // Partial match for longer words (e.g. "greenland" matches "greenlands")
-    else if (w.length > 5) {
-      for (const wb of wordsB) {
-        if (wb.length > 5 && (wb.startsWith(w.slice(0, 5)) || w.startsWith(wb.slice(0, 5)))) { matches += 0.5; break; }
-      }
-    }
-  }
-  return matches / Math.min(wordsA.length, wordsB.size);
-}
-
-async function polymarketCrossRef(kalshiMarkets) {
-  const crossRefs = [];
-  try {
-    const polyEvents = await scrapePolymarketEvents(100);
-    if (polyEvents.length === 0) return [];
-    console.log(`[Ken] Polymarket: ${polyEvents.length} markets from top events`);
-
-    // Build index of Kalshi market text for faster matching
-    const kalshiIndex = kalshiMarkets.map(km => ({
-      ...km,
-      searchText: ((km.title || '') + ' ' + (km.subtitle || '')).toLowerCase(),
-    }));
-
-    for (const pm of polyEvents) {
-      const pmText = pm.event_title + ' ' + pm.question;
-      let bestMatch = null;
-      let bestScore = 0;
-
-      for (const km of kalshiIndex) {
-        const score = fuzzyMatchScore(pmText, km.searchText);
-        if (score > bestScore) {
-          bestScore = score;
-          bestMatch = km;
-        }
-      }
-
-      // Require at least 30% keyword overlap for a match
-      if (bestScore >= 0.3 && bestMatch && bestMatch.yes_ask > 0) {
-        const priceDiff = pm.price - bestMatch.yes_ask;
-        if (Math.abs(priceDiff) >= 3) { // 3¢+ divergence
-          crossRefs.push({
-            kalshi_ticker: bestMatch.ticker,
-            event_ticker: bestMatch.event_ticker,
-            kalshi_title: bestMatch.title,
-            kalshi_subtitle: bestMatch.subtitle,
-            kalshi_price: bestMatch.yes_ask,
-            poly_question: pm.question,
-            poly_event: pm.event_title,
-            poly_price: pm.price,
-            poly_volume: pm.volume,
-            poly_slug: pm.slug,
-            price_diff: priceDiff,
-            match_score: Math.round(bestScore * 100),
-            arb_side: priceDiff > 0 ? 'BUY on Kalshi (cheaper)' : 'SHORT on Kalshi (overpriced)',
-          });
-        }
-      }
-    }
-    // Dedupe: keep best match per Kalshi ticker
-    const seen = new Map();
-    for (const ref of crossRefs) {
-      const key = ref.kalshi_ticker;
-      if (!seen.has(key) || Math.abs(ref.price_diff) > Math.abs(seen.get(key).price_diff)) {
-        seen.set(key, ref);
-      }
-    }
-    return [...seen.values()].sort((a, b) => Math.abs(b.price_diff) - Math.abs(a.price_diff));
-  } catch (e) {
-    console.log(`[Ken] Polymarket cross-ref error: ${e.message}`);
-    return [];
-  }
-}
-
-// ══════════════════════════════════════════════
-// LNG / ENERGY CROSS-MARKET ARBITRAGE
-// Compares energy-related predictions across Kalshi, Polymarket, and Manifold
-// ══════════════════════════════════════════════
-const ENERGY_SEARCH_TERMS = [
-  'oil price', 'crude oil', 'natural gas', 'LNG', 'OPEC',
-  'energy price', 'gas price', 'heating oil', 'petroleum',
-  'electricity price', 'EIA', 'oil production', 'pipeline',
-  'renewable energy', 'solar', 'wind energy', 'nuclear',
-  'carbon price', 'emissions', 'electric vehicle', 'EV',
-  'barrel', 'WTI', 'Brent', 'refinery',
-];
-
-async function energyArbitrageScan(kalshiMarkets) {
-  const arbs = [];
-  try {
-    // 1. Get energy markets from Manifold
-    const manifoldEnergy = await scrapeManifold(ENERGY_SEARCH_TERMS.slice(0, 8));
-    console.log(`[Ken/Energy] Manifold: ${manifoldEnergy.length} energy-related markets`);
-
-    // 2. Get energy events from Polymarket
-    let polyEnergy = [];
-    try {
-      const polyAll = await scrapePolymarketEvents(200);
-      polyEnergy = polyAll.filter(p => {
-        const t = (p.event_title + ' ' + p.question).toLowerCase();
-        return /oil|gas|lng|opec|energy|crude|barrel|petroleum|electric|solar|wind|nuclear|carbon|emission|ev market|renewable/i.test(t);
-      });
-      console.log(`[Ken/Energy] Polymarket: ${polyEnergy.length} energy-related markets`);
-    } catch {}
-
-    // 3. Filter Kalshi markets for energy/commodities
-    const kalshiEnergy = kalshiMarkets.filter(m => {
-      const t = ((m.title || '') + ' ' + (m.subtitle || '') + ' ' + (m.category || '')).toLowerCase();
-      return /oil|gas|lng|opec|energy|crude|barrel|petroleum|electric|solar|wind|nuclear|carbon|emission|ev |renewable|consumption|climate/i.test(t);
-    });
-    console.log(`[Ken/Energy] Kalshi: ${kalshiEnergy.length} energy-related markets`);
-
-    // 4. Cross-reference Manifold vs Kalshi
-    for (const mf of manifoldEnergy) {
-      for (const km of kalshiEnergy) {
-        const score = fuzzyMatchScore(mf.title, (km.title + ' ' + (km.subtitle || '')).toLowerCase());
-        if (score >= 0.25) {
-          const priceDiff = mf.probability - km.yes_ask;
-          if (Math.abs(priceDiff) >= 3) {
-            arbs.push({
-              type: 'manifold_kalshi',
-              kalshi_ticker: km.ticker,
-              event_ticker: km.event_ticker,
-              kalshi_title: km.title,
-              kalshi_price: km.yes_ask,
-              other_title: mf.title,
-              other_price: mf.probability,
-              other_source: 'Manifold',
-              other_url: mf.url,
-              other_volume: mf.volume,
-              price_diff: priceDiff,
-              match_score: Math.round(score * 100),
-              category: 'energy',
-            });
-          }
-        }
-      }
-    }
-
-    // 5. Cross-reference Polymarket vs Kalshi (energy-specific)
-    for (const pm of polyEnergy) {
-      for (const km of kalshiEnergy) {
-        const score = fuzzyMatchScore(pm.event_title + ' ' + pm.question, (km.title + ' ' + (km.subtitle || '')).toLowerCase());
-        if (score >= 0.25) {
-          const priceDiff = pm.price - km.yes_ask;
-          if (Math.abs(priceDiff) >= 3) {
-            arbs.push({
-              type: 'poly_kalshi_energy',
-              kalshi_ticker: km.ticker,
-              event_ticker: km.event_ticker,
-              kalshi_title: km.title,
-              kalshi_price: km.yes_ask,
-              other_title: pm.question,
-              other_price: pm.price,
-              other_source: 'Polymarket',
-              other_url: `https://polymarket.com/event/${pm.slug}`,
-              other_volume: pm.volume,
-              price_diff: priceDiff,
-              match_score: Math.round(score * 100),
-              category: 'energy',
-            });
-          }
-        }
-      }
-    }
-
-    // 6. Cross-reference Manifold vs Polymarket (energy only, no Kalshi needed)
-    for (const mf of manifoldEnergy) {
-      for (const pm of polyEnergy) {
-        const score = fuzzyMatchScore(mf.title, pm.event_title + ' ' + pm.question);
-        if (score >= 0.3) {
-          const priceDiff = mf.probability - pm.price;
-          if (Math.abs(priceDiff) >= 5) {
-            arbs.push({
-              type: 'manifold_poly_energy',
-              kalshi_ticker: null,
-              event_ticker: null,
-              kalshi_title: `${mf.title} (cross-platform)`,
-              kalshi_price: mf.probability,
-              other_title: pm.question,
-              other_price: pm.price,
-              other_source: 'Manifold↔Polymarket',
-              other_url: mf.url,
-              other_volume: Math.max(mf.volume, pm.volume),
-              price_diff: priceDiff,
-              match_score: Math.round(score * 100),
-              category: 'energy',
-            });
-          }
-        }
-      }
-    }
-
-    // Dedupe by best price diff per Kalshi ticker
-    const seen = new Map();
-    for (const arb of arbs) {
-      const key = arb.kalshi_ticker || arb.other_title;
-      if (!seen.has(key) || Math.abs(arb.price_diff) > Math.abs(seen.get(key).price_diff)) {
-        seen.set(key, arb);
-      }
-    }
-    const result = [...seen.values()].sort((a, b) => Math.abs(b.price_diff) - Math.abs(a.price_diff));
-    if (result.length > 0) {
-      console.log(`[Ken/Energy] Found ${result.length} energy arbitrage opportunities (top: ${result[0].kalshi_title} ${result[0].price_diff > 0 ? '+' : ''}${result[0].price_diff}¢)`);
-    }
-    return result;
-  } catch (e) {
-    console.log(`[Ken/Energy] Arbitrage scan error: ${e.message}`);
-    return [];
-  }
-}
-
-// ══════════════════════════════════════════════
-// AUTO-TRADER ($20 budget, risk-managed)
-// ══════════════════════════════════════════════
-const TRADE_CONFIG = {
-  enabled: true,               // Master kill switch
-  budget_cents: 2000,          // $20 total budget
-  max_position_cents: 200,     // $2 max per trade
-  max_open_positions: 5,       // Max concurrent positions
-  daily_loss_limit_cents: 400, // $4 daily stop-loss
-  min_confidence: 80,          // Only trade 80%+ confidence signals
-  min_sources: 2,              // Must have 2+ corroborating sources
-  take_profit_pct: 80,         // Take profit at 80% of max (e.g. sell YES at 90+ if bought at 50)
-  stop_loss_pct: 50,           // Stop loss: exit if position loses 50% of cost
-  fee_rate: 0.07,              // Kalshi ~7% fee on winnings
-  cooldown_minutes: 30,        // Don't re-trade same ticker within 30 min
-};
-
-// ── TRADE_CONFIG Persistence ──
-async function initConfigTable() {
-  try {
-    await kenQ(`
-      CREATE TABLE IF NOT EXISTS ken_config (
-        key TEXT PRIMARY KEY,
-        value JSONB NOT NULL,
-        updated_at TIMESTAMPTZ DEFAULT NOW()
-      )
-    `);
-  } catch (e) {
-    console.log(`[Ken] Config table init error: ${e.message}`);
-  }
-}
-
-async function loadTradeConfig() {
-  try {
-    await initConfigTable();
-    const { rows } = await kenQ("SELECT value FROM ken_config WHERE key = 'trade_config'");
-    if (rows.length > 0) {
-      const saved = rows[0].value;
-      // Merge saved values into TRADE_CONFIG (preserves new keys added in code)
-      for (const [k, v] of Object.entries(saved)) {
-        if (k in TRADE_CONFIG) TRADE_CONFIG[k] = v;
-      }
-      console.log(`[Ken] Loaded TRADE_CONFIG from DB: budget=$${(TRADE_CONFIG.budget_cents/100).toFixed(0)}, conf=${TRADE_CONFIG.min_confidence}%, enabled=${TRADE_CONFIG.enabled}`);
-    }
-  } catch (e) {
-    console.log(`[Ken] Failed to load trade config: ${e.message}`);
-  }
-}
-
-async function saveTradeConfig() {
-  try {
-    await kenQ(`
-      INSERT INTO ken_config (key, value, updated_at) VALUES ('trade_config', $1, NOW())
-      ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()
-    `, [JSON.stringify(TRADE_CONFIG)]);
-  } catch (e) {
-    console.log(`[Ken] Failed to save trade config: ${e.message}`);
-  }
-}
-
-async function getTraderState() {
-  try {
-    const openTrades = await kenQ('SELECT * FROM ken_trades WHERE status = $1 ORDER BY created_at DESC', ['open']);
-    const today = new Date().toISOString().split('T')[0];
-    const todayPnL = await kenQ("SELECT COALESCE(SUM(pnl_cents), 0) as total FROM ken_trades WHERE closed_at::date = $1::date", [today]);
-    const totalInvested = (openTrades.rows || []).reduce((sum, t) => sum + (t.cost_cents || 0), 0);
-    const dailyLoss = Math.abs(Math.min(0, todayPnL.rows[0]?.total || 0));
-    return {
-      openPositions: openTrades.rows || [],
-      openCount: (openTrades.rows || []).length,
-      totalInvested,
-      dailyLoss,
-      available: TRADE_CONFIG.budget_cents - totalInvested,
-    };
-  } catch (e) {
-    console.log(`[Ken] Trader state error: ${e.message}`);
-    return { openPositions: [], openCount: 0, totalInvested: 0, dailyLoss: 0, available: 0 };
-  }
-}
-
-async function autoTrader(signals) {
-  if (!TRADE_CONFIG.enabled) return;
-
-  try {
-    // Check if API keys are configured
-    const cfgRow = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-    const cfg = cfgRow.rows[0]?.config || {};
-    if (!cfg.kalshi_api_key || !cfg.kalshi_private_key) {
-      return; // Silent - no keys configured yet
-    }
-
-    const state = await getTraderState();
-
-    // Daily loss limit check
-    if (state.dailyLoss >= TRADE_CONFIG.daily_loss_limit_cents) {
-      console.log(`[Ken] TRADER: Daily loss limit hit ($${(state.dailyLoss/100).toFixed(2)}). No new trades today.`);
-      return;
-    }
-
-    // ── 1. CHECK EXISTING POSITIONS FOR EXIT ──
-    for (const trade of state.openPositions) {
-      try {
-        // Get current market price
-        const mktData = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(trade.ticker)}`);
-        const mkt = mktData.market || mktData;
-        const currentPrice = trade.side === 'yes' ? (mkt.yes_ask || mkt.last_price || 0) : (mkt.no_ask || mkt.last_price || 0);
-
-        // Market resolved?
-        if (mkt.status === 'settled' || mkt.result) {
-          const won = (mkt.result === trade.side);
-          const pnl = won ? (100 - trade.price_cents) * trade.count : -(trade.cost_cents);
-          const feeAdjusted = won ? Math.round(pnl * (1 - TRADE_CONFIG.fee_rate)) : pnl;
-          await kenQ('UPDATE ken_trades SET status=$1, exit_price=$2, pnl_cents=$3, closed_at=NOW() WHERE id=$4',
-            ['resolved', won ? 100 : 0, feeAdjusted, trade.id]);
-          const emoji = feeAdjusted > 0 ? ':moneybag:' : ':chart_with_downwards_trend:';
-          await sendSlack(`${emoji} *Ken Trade Resolved*\n*${trade.title}* (${trade.ticker})\nSide: ${trade.side.toUpperCase()} @ ${trade.price_cents}¢ → Result: ${mkt.result?.toUpperCase()}\nP&L: *${feeAdjusted > 0 ? '+' : ''}${(feeAdjusted/100).toFixed(2)}* (after fees)`);
-          continue;
-        }
-
-        // Take profit check
-        if (currentPrice >= TRADE_CONFIG.take_profit_pct) {
-          try {
-            const sellOrder = await kalshiFetch('POST', '/portfolio/orders', {
-              ticker: trade.ticker, side: trade.side, action: 'sell', type: 'market', count: trade.count,
-            });
-            const sellPrice = currentPrice;
-            const pnl = Math.round((sellPrice - trade.price_cents) * trade.count * (1 - TRADE_CONFIG.fee_rate));
-            await kenQ('UPDATE ken_trades SET status=$1, exit_price=$2, pnl_cents=$3, closed_at=NOW(), order_id=$4 WHERE id=$5',
-              ['profit', sellPrice, pnl, sellOrder.order?.order_id || trade.order_id, trade.id]);
-            await sendSlack(`:rocket: *Ken Take Profit!*\n*${trade.title}* (${trade.ticker})\nBought ${trade.side.toUpperCase()} @ ${trade.price_cents}¢ → Sold @ ${sellPrice}¢\nP&L: *+$${(pnl/100).toFixed(2)}* :moneybag:`);
-          } catch (e) {
-            console.log(`[Ken] TRADER: Take profit failed for ${trade.ticker}: ${e.message}`);
-          }
-          continue;
-        }
-
-        // Stop loss check
-        const unrealizedLoss = (trade.price_cents - currentPrice) * trade.count;
-        if (unrealizedLoss > trade.cost_cents * (TRADE_CONFIG.stop_loss_pct / 100)) {
-          try {
-            const sellOrder = await kalshiFetch('POST', '/portfolio/orders', {
-              ticker: trade.ticker, side: trade.side, action: 'sell', type: 'market', count: trade.count,
-            });
-            const sellPrice = currentPrice;
-            const pnl = (sellPrice - trade.price_cents) * trade.count;
-            await kenQ('UPDATE ken_trades SET status=$1, exit_price=$2, pnl_cents=$3, closed_at=NOW() WHERE id=$4',
-              ['stopped', sellPrice, pnl, trade.id]);
-            await sendSlack(`:stop_sign: *Ken Stop Loss*\n*${trade.title}* (${trade.ticker})\nBought ${trade.side.toUpperCase()} @ ${trade.price_cents}¢ → Stopped @ ${sellPrice}¢\nP&L: *-$${(Math.abs(pnl)/100).toFixed(2)}*`);
-          } catch (e) {
-            console.log(`[Ken] TRADER: Stop loss failed for ${trade.ticker}: ${e.message}`);
-          }
-          continue;
-        }
-      } catch (e) {
-        console.log(`[Ken] TRADER: Position check failed for ${trade.ticker}: ${e.message}`);
-      }
-    }
-
-    // ── 2. LOOK FOR NEW TRADES ──
-    // Refresh state after exits
-    const freshState = await getTraderState();
-    if (freshState.openCount >= TRADE_CONFIG.max_open_positions) {
-      console.log(`[Ken] TRADER: Max positions (${freshState.openCount}/${TRADE_CONFIG.max_open_positions}). Waiting for exits.`);
-      return;
-    }
-
-    // Filter signals: high confidence, multiple sources, tradeable price
-    const tradeable = signals.filter(s =>
-      s.confidence >= TRADE_CONFIG.min_confidence &&
-      (s.sources?.length || 0) >= TRADE_CONFIG.min_sources &&
-      s.entry_price > 5 && s.entry_price < 90 && // Don't trade near extremes
-      s.ticker
-    ).sort((a, b) => b.confidence - a.confidence);
-
-    // Check cooldown: don't re-trade same ticker
-    const recentTickers = new Set();
-    try {
-      const recent = await kenQ("SELECT ticker FROM ken_trades WHERE created_at > NOW() - INTERVAL '" + TRADE_CONFIG.cooldown_minutes + " minutes'");
-      for (const r of (recent.rows || [])) recentTickers.add(r.ticker);
-    } catch {}
-    // Also skip tickers we already have open positions on
-    for (const t of freshState.openPositions) recentTickers.add(t.ticker);
-
-    let tradesPlaced = 0;
-    for (const sig of tradeable) {
-      if (freshState.openCount + tradesPlaced >= TRADE_CONFIG.max_open_positions) break;
-      if (recentTickers.has(sig.ticker)) continue;
-
-      // Calculate position size: max $2, but respect available budget
-      const priceCents = sig.entry_price;
-      const costPerContract = priceCents; // Cost = price in cents for YES; (100 - price) for NO
-      const maxContracts = Math.floor(Math.min(TRADE_CONFIG.max_position_cents, freshState.available) / costPerContract);
-      if (maxContracts < 1) continue;
-      const count = Math.min(maxContracts, 3); // Max 3 contracts per trade
-      const totalCost = count * costPerContract;
-
-      try {
-        const side = sig.side === 'NO' ? 'no' : 'yes';
-        const order = await kalshiFetch('POST', '/portfolio/orders', {
-          ticker: sig.ticker,
-          side: side,
-          action: 'buy',
-          type: 'limit',
-          count: count,
-          yes_price: side === 'yes' ? priceCents : undefined,
-          no_price: side === 'no' ? (100 - priceCents) : undefined,
-          time_in_force: 'gtc',
-        });
-
-        const orderId = order.order?.order_id || `ken_${Date.now()}`;
-        await kenQ(`INSERT INTO ken_trades (ticker, event_ticker, title, side, action, price_cents, count, cost_cents, order_id, signal_type, confidence, reasoning, status)
-          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'open')`,
-          [sig.ticker, sig.event_ticker || '', sig.market || '', side, 'buy', priceCents, count, totalCost,
-           orderId, sig.signal_type || '', sig.confidence, sig.reason || '']);
-
-        recentTickers.add(sig.ticker);
-        tradesPlaced++;
-        freshState.available -= totalCost;
-
-        const kLink = kalshiLink(sig.ticker, sig.event_ticker);
-        await sendSlack(`:chart_with_upwards_trend: *Ken Auto-Trade Placed!*\n${confBadge(sig.confidence)} *${sig.market}*\n${side.toUpperCase()} x${count} @ ${priceCents}¢ = $${(totalCost/100).toFixed(2)}\nSignal: ${sig.signal_type} | ${sig.reason}\n${kLink}\n_Budget remaining: $${(freshState.available/100).toFixed(2)}_`);
-        console.log(`[Ken] TRADER: ${side.toUpperCase()} x${count} ${sig.ticker} @ ${priceCents}¢ ($${(totalCost/100).toFixed(2)}) — ${sig.signal_type} ${sig.confidence}%`);
-
-      } catch (e) {
-        console.log(`[Ken] TRADER: Order failed for ${sig.ticker}: ${e.message}`);
-      }
-    }
-
-    if (tradesPlaced > 0) {
-      console.log(`[Ken] TRADER: ${tradesPlaced} new trades placed. Open: ${freshState.openCount + tradesPlaced}/${TRADE_CONFIG.max_open_positions}`);
-    }
-  } catch (e) {
-    console.log(`[Ken] TRADER ERROR: ${e.message}`);
-  }
-}
-
-// ══════════════════════════════════════════════
-// MAIN AUTONOMOUS SCAN (every 15 min, 24/7)
-// ══════════════════════════════════════════════
-async function autonomousScan() {
-  const scanStart = Date.now();
-  console.log('[Ken] ═══ AUTONOMOUS SCAN STARTING ═══');
-  try {
-    // 1. Refresh weather data
-    const cities = ['New York', 'Chicago', 'Miami', 'Denver', 'Los Angeles', 'Seattle', 'Houston', 'Phoenix', 'Minneapolis', 'Boston'];
-    const weatherData = generateSyntheticWeather(cities);
-
-    // Cache in DB
-    const row = (await q('SELECT id, config FROM risk_state ORDER BY updated_at DESC LIMIT 1')).rows[0];
-    if (row) {
-      const newCfg = { ...(row.config || {}), weather_cache: { results: weatherData, fetched_at: new Date().toISOString(), synthetic: true } };
-      await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newCfg), row.id]);
-    }
-
-    // 2. Fetch ALL live Kalshi events with nested markets (retry + fallback to prod)
-    let allEvents = [];
-    let weatherEvents = [];
-    let allMarkets = [];
-    const apiUrls = [KALSHI_BASE_URL, 'https://api.elections.kalshi.com/trade-api/v2', 'https://demo-api.kalshi.co/trade-api/v2'];
-    for (let attempt = 0; attempt < apiUrls.length; attempt++) {
-      try {
-        const url = apiUrls[attempt] + '/events?status=open&limit=200&with_nested_markets=true';
-        const controller = new AbortController();
-        const timeout = setTimeout(() => controller.abort(), 15000);
-        const res = await fetch(url, { headers: { 'Accept': 'application/json' }, signal: controller.signal });
-        clearTimeout(timeout);
-        if (!res.ok) throw new Error(`HTTP ${res.status}`);
-        const evData = await res.json();
-        allEvents = evData.events || [];
-        if (allEvents.length > 0) {
-          if (attempt > 0) console.log(`[Ken] Fallback API ${attempt + 1} worked`);
-          break;
-        }
-      } catch (e) {
-        console.log(`[Ken] API ${attempt + 1}/${apiUrls.length} failed: ${e.message}`);
-        if (attempt < apiUrls.length - 1) await new Promise(r => setTimeout(r, 3000));
-      }
-    }
-    // Build market list from events
-    for (const evt of allEvents) {
-      if (evt.markets) {
-        for (const m of evt.markets) {
-          allMarkets.push({
-            ticker: m.ticker,
-            event_ticker: evt.event_ticker,
-            title: evt.title,
-            subtitle: m.subtitle || m.ticker,
-            volume: m.volume || 0,
-            volume_24h: m.volume_24h || 0,
-            open_interest: m.open_interest || 0,
-            yes_bid: m.yes_bid || 0,
-            yes_ask: m.yes_ask || 0,
-            no_bid: m.no_bid || 0,
-            no_ask: m.no_ask || 0,
-            last_price: m.last_price || 0,
-            prev_price: m.previous_price || m.last_price || 0,
-            status: m.status,
-            category: evt.category || '',
-          });
-        }
-      }
-    }
-    // Filter weather events for model-based predictions
-    weatherEvents = allEvents.filter(e => {
-      const t = ((e.category || '') + ' ' + (e.title || '')).toLowerCase();
-      return t.includes('weather') || t.includes('temperature') || t.includes('temp ') ||
-             t.includes('rain') || t.includes('snow') || t.includes('precipitation') ||
-             t.includes('hurricane') || t.includes('wind') || t.includes('°f');
-    });
-    console.log(`[Ken] Kalshi: ${allEvents.length} events, ${allMarkets.length} markets, ${weatherEvents.length} weather`);
-
-    const month = new Date().getMonth() + 1;
-    const predictions = [];
-
-    // 3. Score weather events against our weather model
-    for (const evt of weatherEvents) {
-      const city = extractCity(evt.title);
-      if (!city) continue;
-      const spec = parseWeatherEvent(evt.title);
-      if (!spec || spec.variable === 'unknown') continue;
-      const cityWeather = weatherData.find(w => w.city === city);
-      if (!cityWeather) continue;
-      const features = cityWeather[spec.variable === 'snow' ? 'precipitation' : spec.variable] || cityWeather.temperature;
-      if (!features?.values?.length) continue;
-
-      const rawProb = ensembleExceedance(features, spec.threshold, spec.operator);
-      const prior = climatologicalPrior(spec.variable, spec.threshold, month);
-      const combined = bayesianUpdate(prior, rawProb ?? 0.5);
-      const pYes = Math.max(0.01, Math.min(0.99, combined));
-
-      let marketPrice = 50;
-      const mkt = (evt.markets || [])[0];
-      if (mkt) {
-        const p = mkt.yes_ask || mkt.last_price || 50;
-        if (p > 0 && p < 100) marketPrice = p;
-      }
-
-      const edgeBps = Math.round((pYes * 100 - marketPrice) * 100);
-      predictions.push({
-        market: evt.title,
-        city,
-        side: edgeBps > 0 ? 'yes' : 'no',
-        p_yes: Math.round(pYes * 1000) / 10,
-        price_cents: marketPrice,
-        edge_bps: edgeBps,
-        variable: spec.variable,
-        threshold: spec.threshold,
-        type: 'weather_model',
-      });
-    }
-
-    // 4. Scan ALL high-volume markets for volume-based signals
-    const volumeIdeas = [];
-
-    // Sort by 24h volume to find what's hot
-    const activeMarkets = allMarkets
-      .filter(m => m.yes_bid > 0 && m.yes_ask > 0 && m.yes_ask < 100 && m.last_price > 0)
-      .sort((a, b) => b.volume_24h - a.volume_24h);
-
-    // Cache for 5-minute signal pipeline
-    cachedActiveMarkets = activeMarkets;
-
-    // Load price momentum from DB (compare current vs 2h ago and 6h ago)
-    let momentumMap = {};
-    // Also update cached version for signal pipeline MC simulations
-    try {
-      const { rows: momentumRows } = await kenQ(`
-        WITH latest AS (
-          SELECT DISTINCT ON (ticker) ticker, last_price as current_price, yes_bid, yes_ask, captured_at
-          FROM ken_market_snapshots ORDER BY ticker, captured_at DESC
-        ),
-        two_hours AS (
-          SELECT DISTINCT ON (ticker) ticker, last_price as price_2h
-          FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '2 hours'
-          ORDER BY ticker, captured_at DESC
-        ),
-        six_hours AS (
-          SELECT DISTINCT ON (ticker) ticker, last_price as price_6h
-          FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '6 hours'
-          ORDER BY ticker, captured_at DESC
-        )
-        SELECT l.ticker, l.current_price, t.price_2h, s.price_6h,
-               l.current_price - COALESCE(t.price_2h, l.current_price) as delta_2h,
-               l.current_price - COALESCE(s.price_6h, l.current_price) as delta_6h
-        FROM latest l LEFT JOIN two_hours t ON l.ticker = t.ticker LEFT JOIN six_hours s ON l.ticker = s.ticker
-      `);
-      for (const r of momentumRows) {
-        momentumMap[r.ticker] = { delta_2h: parseInt(r.delta_2h) || 0, delta_6h: parseInt(r.delta_6h) || 0, price_2h: r.price_2h, price_6h: r.price_6h };
-      }
-    } catch (e) { console.log('[Ken] Momentum query error:', e.message); }
-    cachedMomentumMap = momentumMap; // Update shared cache for signal pipeline
-
-    // ═══ INSTANT SLACK ALERT: Major Price Movements (>=10¢ in 2h or >=15¢ in 6h) ═══
-    try {
-      const bigMovers = Object.entries(momentumMap)
-        .filter(([, m]) => Math.abs(m.delta_2h) >= 10 || Math.abs(m.delta_6h) >= 15)
-        .sort((a, b) => Math.abs(b[1].delta_2h) - Math.abs(a[1].delta_2h));
-      if (bigMovers.length > 0) {
-        const moverLines = bigMovers.slice(0, 8).map(([ticker, m]) => {
-          const mkt = activeMarkets.find(am => am.ticker === ticker);
-          const title = mkt ? mkt.title : ticker;
-          const d2 = m.delta_2h > 0 ? `+${m.delta_2h}` : `${m.delta_2h}`;
-          const d6 = m.delta_6h > 0 ? `+${m.delta_6h}` : `${m.delta_6h}`;
-          const icon = Math.abs(m.delta_2h) >= 10 ? (m.delta_2h > 0 ? ':chart_with_upwards_trend:' : ':chart_with_downwards_trend:') : ':left_right_arrow:';
-          const et = mkt?.event_ticker || ticker?.replace(/-[^-]+$/, '') || ticker;
-          const link = `<https://kalshi.com/markets/${et}/${ticker}|View>`;
-          return `  ${icon} *${title}*\n     2h: *${d2}¢* | 6h: *${d6}¢* | Now: ${mkt?.last_price || '?'}¢ | ${link}`;
-        }).join('\n');
-        const timeStr = new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles', hour: 'numeric', minute: '2-digit', hour12: true });
-        await sendSlack(`:rotating_light: *Major Price Movements* (${timeStr} PT)\n_${bigMovers.length} market(s) moved 10+¢ in 2h or 15+¢ in 6h_\n\n${moverLines}\n\nDashboard: http://45.61.58.125:7810`);
-        console.log(`[Ken] SLACK ALERT: ${bigMovers.length} major price movement(s) sent`);
-      }
-    } catch (e) { console.log('[Ken] Price movement alert error:', e.message); }
-
-    for (const m of activeMarkets.slice(0, 200)) {
-      const spread = m.yes_ask - m.yes_bid;
-      const noPrice = 100 - m.yes_ask;
-      const momentum = momentumMap[m.ticker] || { delta_2h: 0, delta_6h: 0 };
-
-      // ── SHORT: YES >= 75¢ (buy NO cheap, wider net than before) ──
-      if (m.yes_bid >= 75 && noPrice <= 25) {
-        let shortConf = 40;
-        if (noPrice <= 5) shortConf += 15;      // dirt cheap NO
-        else if (noPrice <= 10) shortConf += 10;
-        else if (noPrice <= 15) shortConf += 5;
-        if (momentum.delta_2h < 0) shortConf += 10;  // price dropping = good short
-        if (momentum.delta_6h < -3) shortConf += 10;  // strong downtrend
-        if (m.volume_24h > 5000) shortConf += 5;
-        if (m.open_interest > 100000) shortConf += 5;
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'NO',
-          reason: `YES at ${m.yes_bid}¢ → NO at ${noPrice}¢${momentum.delta_2h ? ` (${momentum.delta_2h > 0 ? '+' : ''}${momentum.delta_2h}¢/2h)` : ''}`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'short_high_price', confidence: Math.min(90, shortConf),
-        });
-      }
-
-      // ── MOMENTUM SHORT: price dropping fast (was 50+, fell 5+ in 2h) ──
-      if (momentum.delta_2h <= -5 && m.last_price >= 30 && m.last_price <= 80) {
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'NO',
-          reason: `Dropping ${momentum.delta_2h}¢/2h (${momentum.delta_6h || '?'}¢/6h) — momentum sell`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'momentum_short', confidence: Math.min(80, 50 + Math.abs(momentum.delta_2h) * 2),
-        });
-      }
-
-      // ── MOMENTUM LONG: price rising fast (was low, jumped 5+ in 2h) ──
-      if (momentum.delta_2h >= 5 && m.last_price <= 70 && m.last_price >= 10) {
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'YES',
-          reason: `Rising +${momentum.delta_2h}¢/2h (+${momentum.delta_6h || '?'}¢/6h) — momentum buy`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'momentum_long', confidence: Math.min(80, 50 + momentum.delta_2h * 2),
-        });
-      }
-
-      // ── LONG CHEAP: YES <= 20¢ with volume (wider net) ──
-      if (m.yes_ask <= 20 && m.volume_24h > 100) {
-        let longConf = 35;
-        if (m.yes_ask <= 5) longConf += 5;        // lottery ticket
-        if (m.volume_24h > 1000) longConf += 10;  // lots of interest
-        if (m.volume_24h > 5000) longConf += 10;
-        if (momentum.delta_2h > 0) longConf += 10; // price rising
-        if (momentum.delta_6h > 2) longConf += 10;
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'YES',
-          reason: `YES at ${m.yes_ask}¢, vol ${m.volume_24h.toLocaleString()}${momentum.delta_2h ? ` (${momentum.delta_2h > 0 ? '+' : ''}${momentum.delta_2h}¢/2h)` : ''}`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'long_cheap', confidence: Math.min(80, longConf),
-        });
-      }
-
-      // ── CONTRARIAN SHORT: YES at 60-84¢ with negative momentum ──
-      if (m.yes_bid >= 60 && m.yes_bid < 85 && momentum.delta_2h < -2 && m.volume_24h > 200) {
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'NO',
-          reason: `YES fading from ${(m.last_price + Math.abs(momentum.delta_2h))}¢→${m.last_price}¢, NO at ${noPrice}¢`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'contrarian_short', confidence: Math.min(75, 45 + Math.abs(momentum.delta_2h) * 3),
-        });
-      }
-
-      // ── VOLUME SPIKE (kept but enhanced with momentum) ──
-      if (m.volume > 0 && m.volume_24h / m.volume > 0.05 && m.volume_24h > 500) {
-        volumeIdeas.push({
-          market: m.title,
-          subtitle: m.subtitle,
-          ticker: m.ticker,
-          event_ticker: m.event_ticker,
-          side: m.last_price > m.prev_price ? 'YES momentum' : 'NO momentum',
-          reason: `24h vol ${m.volume_24h.toLocaleString()} = ${Math.round(m.volume_24h / m.volume * 100)}% of all-time`,
-          volume_24h: m.volume_24h,
-          open_interest: m.open_interest,
-          yes_price: m.yes_ask,
-          no_price: noPrice,
-          spread,
-          type: 'volume_spike',
-        });
-      }
-
-      // Spread opportunity: wide spread with volume = market-making opportunity
-      if (spread >= 5 && m.volume_24h > 100 && m.open_interest > 1000) {
-        volumeIdeas.push({
-          market: m.title,
-          subtitle: m.subtitle,
-          ticker: m.ticker,
-          event_ticker: m.event_ticker,
-          side: 'SPREAD',
-          reason: `${spread}¢ spread, bid ${m.yes_bid}¢ / ask ${m.yes_ask}¢`,
-          volume_24h: m.volume_24h,
-          open_interest: m.open_interest,
-          yes_price: m.yes_ask,
-          no_price: noPrice,
-          spread,
-          type: 'wide_spread',
-        });
-      }
-    }
-
-    // Deduplicate volume ideas by ticker (keep highest confidence)
-    const deduped = new Map();
-    for (const idea of volumeIdeas) {
-      const existing = deduped.get(idea.ticker);
-      if (!existing || (idea.confidence || 50) > (existing.confidence || 50)) {
-        deduped.set(idea.ticker, idea);
-      }
-    }
-    const uniqueVolumeIdeas = [...deduped.values()];
-
-    logMemory('scan-after-signals');
-
-    // 5. Store market snapshots in DB (batch of 150 to capture more markets)
-    for (const m of activeMarkets.slice(0, 150)) {
-      try {
-        await kenQ(`
-          INSERT INTO ken_market_snapshots (ticker, event_ticker, title, subtitle, category, yes_bid, yes_ask, no_bid, no_ask, last_price, volume, volume_24h, open_interest, spread)
-          VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
-        `, [m.ticker, m.event_ticker, m.title, m.subtitle, m.category, m.yes_bid, m.yes_ask, m.no_bid, m.no_ask, m.last_price, m.volume, m.volume_24h, m.open_interest, m.yes_ask - m.yes_bid]);
-      } catch {}
-    }
-
-    // 6. Get sentiment data from last 2 hours for signal boosting
-    let sentimentBoosts = {};
-    try {
-      const { rows: recentSentiment } = await kenQ(`
-        SELECT relevance_ticker, AVG(sentiment_score) as avg_sent, COUNT(*) as post_count, SUM(score) as total_score
-        FROM ken_sentiment
-        WHERE captured_at > NOW() - INTERVAL '2 hours' AND relevance_ticker IS NOT NULL
-        GROUP BY relevance_ticker
-      `);
-      for (const s of recentSentiment) {
-        sentimentBoosts[s.relevance_ticker] = {
-          avgSentiment: parseFloat(s.avg_sent) || 0,
-          postCount: parseInt(s.post_count) || 0,
-          totalScore: parseInt(s.total_score) || 0,
-        };
-      }
-    } catch {}
-
-    // Get recent news sentiment (non-ticker-specific)
-    let newsSentiment = {};
-    try {
-      const { rows: newsRows } = await kenQ(`
-        SELECT keyword, AVG(sentiment_score) as avg_sent, COUNT(*) as cnt
-        FROM ken_sentiment
-        WHERE source = 'google_news' AND captured_at > NOW() - INTERVAL '4 hours'
-        GROUP BY keyword
-      `);
-      for (const n of newsRows) {
-        newsSentiment[n.keyword] = { avg: parseFloat(n.avg_sent) || 0, count: parseInt(n.cnt) || 0 };
-      }
-    } catch {}
-
-    // 7. Polymarket cross-reference for arbitrage
-    let polyArbs = [];
-    try {
-      polyArbs = await polymarketCrossRef(activeMarkets);
-    } catch (e) {
-      console.log('[Ken] Polymarket cross-ref failed:', e.message);
-    }
-
-    // 7b. Energy/LNG cross-market arbitrage scan
-    let energyArbs = [];
-    try {
-      energyArbs = await energyArbitrageScan(activeMarkets);
-    } catch (e) {
-      console.log('[Ken] Energy arb scan failed:', e.message);
-    }
-
-    // 8. Combine ALL signals with confidence scoring
-    const buySignals = predictions.filter(p => p.edge_bps > 200);
-    const shortSignals = predictions.filter(p => p.edge_bps < -200);
-    const longIdeas = uniqueVolumeIdeas.filter(v => v.side === 'YES' || v.side === 'YES momentum');
-    const shortIdeas = uniqueVolumeIdeas.filter(v => ['short_high_price', 'momentum_short', 'contrarian_short'].includes(v.type));
-    const spikeIdeas = uniqueVolumeIdeas.filter(v => v.type === 'volume_spike');
-    const momentumIdeas = uniqueVolumeIdeas.filter(v => v.type === 'momentum_long' || v.type === 'momentum_short');
-    const spreadIdeas = uniqueVolumeIdeas.filter(v => v.type === 'wide_spread');
-
-    // 9. Composite confidence with sentiment + momentum
-    const allSignals = [];
-    for (const p of [...buySignals, ...shortSignals]) {
-      let confidence = 50 + Math.min(30, Math.abs(p.edge_bps) / 50);
-      const sentBoost = sentimentBoosts[p.market] || {};
-      if (sentBoost.postCount > 3) confidence += sentBoost.avgSentiment > 0 ? 10 : -5;
-      const mom = momentumMap[p.ticker] || {};
-      if (p.edge_bps > 0 && mom.delta_2h > 0) confidence += 8;   // momentum confirms long
-      if (p.edge_bps < 0 && mom.delta_2h < 0) confidence += 8;   // momentum confirms short
-      const sources = ['weather_model'];
-      if (sentBoost.postCount) sources.push('reddit');
-      if (mom.delta_2h) sources.push('momentum');
-      allSignals.push({ ...p, confidence: Math.min(95, Math.round(confidence)), sources, signal_type: p.edge_bps > 0 ? 'long' : 'short' });
-    }
-    for (const v of uniqueVolumeIdeas) {
-      let confidence = v.confidence || 45; // Use pre-calculated confidence from signal detection
-      // Boost with sentiment
-      const sentKey = Object.keys(sentimentBoosts).find(k => v.ticker?.startsWith(k));
-      const sentBoost = sentKey ? sentimentBoosts[sentKey] : null;
-      if (sentBoost && sentBoost.postCount > 3) {
-        if (v.side === 'YES' && sentBoost.avgSentiment > 0.05) confidence += 10;
-        if (v.side === 'NO' && sentBoost.avgSentiment < -0.05) confidence += 10;
-      }
-      // Boost with news
-      for (const [nk, nv] of Object.entries(newsSentiment)) {
-        const titleLower = (v.market || '').toLowerCase();
-        if (nk.split(' ').some(w => w.length > 4 && titleLower.includes(w))) {
-          if (v.side === 'YES' && nv.avg > 0.05) confidence += 5;
-          if (v.side === 'NO' && nv.avg < -0.05) confidence += 5;
-          break;
-        }
-      }
-      const sources = ['volume_analysis'];
-      if (v.type?.includes('momentum')) sources.push('momentum');
-      if (sentBoost) sources.push('reddit');
-      const isShort = v.side === 'NO' || ['short_high_price', 'momentum_short', 'contrarian_short'].includes(v.type);
-      allSignals.push({
-        market: v.market, ticker: v.ticker, event_ticker: v.event_ticker, subtitle: v.subtitle,
-        side: isShort ? 'NO' : v.side === 'SPREAD' ? 'YES' : 'YES',
-        confidence: Math.min(95, Math.round(confidence)),
-        entry_price: isShort ? v.no_price : v.yes_price,
-        edge_bps: isShort ? Math.round((100 - (v.yes_price || 50)) * 100) : 0,
-        signal_type: v.type || 'volume',
-        reason: v.reason, sources, volume_24h: v.volume_24h,
-      });
-    }
-    for (const arb of polyArbs) {
-      allSignals.push({
-        market: arb.kalshi_title, ticker: arb.kalshi_ticker, event_ticker: arb.event_ticker,
-        side: arb.price_diff > 0 ? 'YES' : 'NO',
-        confidence: Math.min(90, 55 + Math.abs(arb.price_diff) * 2),
-        entry_price: arb.kalshi_price,
-        edge_bps: arb.price_diff * 100,
-        signal_type: 'polymarket_arb',
-        reason: `Kalshi ${arb.kalshi_price}¢ vs Poly ${arb.poly_price}¢ (${arb.price_diff > 0 ? '+' : ''}${arb.price_diff}¢ gap | ${arb.match_score}% match)`,
-        sources: ['polymarket_crossref'],
-      });
-    }
-    // Energy/LNG cross-market arb signals
-    for (const arb of energyArbs) {
-      if (arb.kalshi_ticker) {
-        allSignals.push({
-          market: arb.kalshi_title, ticker: arb.kalshi_ticker, event_ticker: arb.event_ticker,
-          side: arb.price_diff > 0 ? 'YES' : 'NO',
-          confidence: Math.min(92, 60 + Math.abs(arb.price_diff) * 2),
-          entry_price: arb.kalshi_price,
-          edge_bps: arb.price_diff * 100,
-          signal_type: 'energy_arb',
-          reason: `ENERGY ARB: Kalshi ${arb.kalshi_price}¢ vs ${arb.other_source} ${arb.other_price}¢ (${arb.price_diff > 0 ? '+' : ''}${arb.price_diff}¢ gap) — BUY ${arb.price_diff > 0 ? 'Kalshi' : arb.other_source}, SELL ${arb.price_diff > 0 ? arb.other_source : 'Kalshi'}`,
-          sources: ['energy_crossmarket_arb', arb.other_source.toLowerCase()],
-        });
-      }
-    }
-
-    // 10. Store predictions in DB
-    for (const sig of allSignals) {
-      try {
-        await kenQ(`
-          INSERT INTO ken_predictions (ticker, event_ticker, title, signal_type, side, confidence, entry_price, edge_bps, reasoning, sources)
-          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
-        `, [
-          sig.ticker || '', sig.event_ticker || '', sig.market || '', sig.signal_type,
-          sig.side, sig.confidence, sig.entry_price || sig.price_cents || 0, sig.edge_bps || 0,
-          sig.reason || sig.reasoning || `${sig.signal_type} signal`, JSON.stringify(sig.sources || []),
-        ]);
-      } catch {}
-    }
-
-    // 10b. AUTO-TRADER: Execute trades on high-confidence signals
-    try {
-      await autoTrader(allSignals);
-    } catch (e) {
-      console.log(`[Ken] Auto-trader error: ${e.message}`);
-    }
-
-    // 10c. PORTFOLIO TRADER: Feed scan signals into paper portfolios
-    let portfolioTradesPlaced = 0;
-    for (const sig of allSignals.filter(s => s.confidence >= 40 && s.ticker)) {
-      try {
-        await autoPortfolioTrade({
-          marketId: sig.ticker,
-          market_title: sig.market || sig.ticker,
-          direction: sig.side === 'YES' ? 'BUY_YES' : 'BUY_NO',
-          expectedEdge: (sig.edge_bps || 0) / 10000,
-          confidence: (sig.confidence || 50) / 100,
-          reasoning: sig.reason || sig.reasoning || sig.signal_type,
-          signal_type: sig.signal_type,
-          aiEnhanced: (sig.sources || []).includes('gemini'),
-          mcSimulation: { consistent: true, stdDev: 0.05 },
-          num_articles: (sig.sources || []).length,
-          agreement: (sig.confidence || 50) / 100,
-        });
-        portfolioTradesPlaced++;
-      } catch {}
-    }
-    if (portfolioTradesPlaced > 0) console.log(`[Ken] Portfolio trades: ${portfolioTradesPlaced} signals fed to paper portfolios`);
-
-    const scanDuration = Date.now() - scanStart;
-
-    // 11. Log performance
-    try {
-      await kenQ(`
-        INSERT INTO ken_performance (total_markets, active_markets, predictions_made, long_signals, short_signals, sentiment_items, scan_duration_ms, slack_sent)
-        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
-      `, [
-        allMarkets.length, activeMarkets.length, allSignals.length,
-        allSignals.filter(s => s.side === 'YES').length,
-        allSignals.filter(s => s.side === 'NO').length,
-        Object.keys(sentimentBoosts).length,
-        scanDuration, false,
-      ]);
-    } catch {}
-
-    console.log(`[Ken] Scan complete: ${allMarkets.length} markets, ${activeMarkets.length} active, ${allSignals.length} signals (${allSignals.filter(s=>s.side==='YES').length} long, ${allSignals.filter(s=>s.side==='NO').length} short), ${polyArbs.length} arbs, ${Object.keys(sentimentBoosts).length} sentiment boosts | ${scanDuration}ms`);
-
-    // 12a. Run Signal Pipeline (News → Sentiment → Probability → Signals → Risk)
-    try {
-      const pipelineSignals = await signalPipeline(activeMarkets);
-      if (pipelineSignals > 0) {
-        console.log(`[Ken] Signal Pipeline produced ${pipelineSignals} risk-assessed signals`);
-      }
-    } catch (e) {
-      console.log(`[Ken] Signal Pipeline error: ${e.message}`);
-    }
-
-    // 12b. Update market mappings
-    try {
-      await updateMarketMappings(activeMarkets);
-    } catch {}
-
-    // 12. Build and send Slack alert (max once per hour to not spam)
-    const now = Date.now();
-    const shouldSlack = (now - lastSlackTime > SLACK_COOLDOWN_MS) || allSignals.some(s => s.confidence >= 75);
-
-    if (shouldSlack && allSignals.length > 0) {
-      lastSlackTime = now;
-      const timeStr = new Date().toLocaleString('en-US', { timeZone: 'America/New_York', hour: 'numeric', minute: '2-digit', hour12: true });
-      const parts = [];
-
-      parts.push(`:dog: *Ken — Autonomous Scan* (${timeStr} ET)`);
-      parts.push(`_${allMarkets.length} markets | ${activeMarkets.length} active | ${allSignals.length} signals | ${KALSHI_ENV}_\n`);
-
-      // Helper: build Kalshi market link
-      const kalshiLink = (ticker, eventTicker) => {
-        const et = eventTicker || ticker?.replace(/-[^-]+$/, '') || ticker;
-        return `<https://kalshi.com/markets/${et}/${ticker}|Trade on Kalshi>`;
-      };
-
-      // Helper: confidence badge with color (green→yellow→orange→red)
-      const confBadge = (conf) => {
-        if (conf >= 80) return `:large_green_circle: *${conf}%*`;
-        if (conf >= 65) return `:large_yellow_circle: *${conf}%*`;
-        if (conf >= 50) return `:large_orange_circle: *${conf}%*`;
-        return `:red_circle: *${conf}%*`;
-      };
-
-      // Confidence legend
-      parts.push(`_:large_green_circle: 80%+ Strong  :large_yellow_circle: 65-79% Good  :large_orange_circle: 50-64% Moderate  :red_circle: <50% Speculative_`);
-
-      // High-confidence signals first (>= 70)
-      const highConf = allSignals.filter(s => s.confidence >= 70).sort((a, b) => b.confidence - a.confidence);
-      if (highConf.length > 0) {
-        const lines = highConf.slice(0, 5).map((s, i) => {
-          const icon = s.side === 'YES' ? ':chart_with_upwards_trend:' : ':small_red_triangle_down:';
-          const link = kalshiLink(s.ticker, s.event_ticker);
-          return `  ${icon} ${confBadge(s.confidence)} *${s.market}*\n     ${s.side} | ${s.reason || `Edge ${s.edge_bps}bps`}\n     :link: ${link}`;
-        }).join('\n');
-        parts.push(`:rotating_light: *High Confidence Signals*\n${lines}`);
-      }
-
-      // Short ideas
-      const shorts = allSignals.filter(s => s.side === 'NO' && s.confidence >= 50).sort((a, b) => b.confidence - a.confidence);
-      if (shorts.length > 0 && highConf.filter(s => s.side === 'NO').length === 0) {
-        const lines = shorts.slice(0, 3).map((s, i) => {
-          const link = kalshiLink(s.ticker, s.event_ticker);
-          return `  ${confBadge(s.confidence)} *${s.market}* — BUY NO\n     ${s.reason || ''}\n     :link: ${link}`;
-        }).join('\n');
-        parts.push(`:moneybag: *Short Ideas (Buy NO)*\n${lines}`);
-      }
-
-      // Volume spikes
-      if (spikeIdeas.length > 0) {
-        const lines = spikeIdeas.sort((a, b) => b.volume_24h - a.volume_24h).slice(0, 3)
-          .map((p, i) => {
-            const link = kalshiLink(p.ticker, p.event_ticker);
-            const conf = p.confidence || 45;
-            return `  ${confBadge(conf)} *${p.market}*\n     ${p.reason}\n     :link: ${link}`;
-          }).join('\n');
-        parts.push(`:fire: *Volume Spikes*\n${lines}`);
-      }
-
-      // Momentum moves
-      if (momentumIdeas.length > 0) {
-        const lines = momentumIdeas.sort((a, b) => (b.confidence || 0) - (a.confidence || 0)).slice(0, 4)
-          .map((p, i) => {
-            const icon = p.type === 'momentum_long' ? ':arrow_up:' : ':arrow_down:';
-            const link = kalshiLink(p.ticker, p.event_ticker);
-            const conf = p.confidence || 50;
-            return `  ${icon} ${confBadge(conf)} *${p.market}*\n     ${p.subtitle || ''} — ${p.reason}\n     :link: ${link}`;
-          }).join('\n');
-        parts.push(`:rocket: *Momentum Moves*\n${lines}`);
-      }
-
-      // Polymarket arbitrage
-      if (polyArbs.length > 0) {
-        const lines = polyArbs.slice(0, 5)
-          .map(a => {
-            const conf = Math.min(90, 55 + Math.abs(a.price_diff) * 2);
-            const kLink = kalshiLink(a.kalshi_ticker, a.event_ticker);
-            const pLink = `<https://polymarket.com/event/${a.poly_slug}|Polymarket>`;
-            return `  ${confBadge(conf)} *${a.kalshi_title}* (${a.kalshi_subtitle || ''})\n     Kalshi: ${a.kalshi_price}¢ vs Poly: ${a.poly_price}¢ → *${a.arb_side}*\n     ${kLink} | ${pLink} | Match: ${a.match_score}%`;
-          }).join('\n');
-        parts.push(`:scales: *Polymarket Arbitrage (${polyArbs.length} found)*\n${lines}`);
-      }
-
-      // Reddit buzz
-      const buzzKeys = Object.entries(sentimentBoosts).filter(([, v]) => v.postCount >= 3).sort((a, b) => b[1].totalScore - a[1].totalScore);
-      if (buzzKeys.length > 0) {
-        const lines = buzzKeys.slice(0, 3).map(([ticker, v]) => {
-          const sentIcon = v.avgSentiment > 0.1 ? ':large_green_circle:' : v.avgSentiment < -0.1 ? ':red_circle:' : ':large_yellow_circle:';
-          return `  ${sentIcon} ${ticker}: ${v.postCount} posts, sentiment ${v.avgSentiment > 0 ? '+' : ''}${v.avgSentiment.toFixed(2)}, karma ${v.totalScore}`;
-        }).join('\n');
-        parts.push(`:speech_balloon: *Reddit Buzz*\n${lines}`);
-      }
-
-      // Auto-trader portfolio summary
-      try {
-        const traderState = await getTraderState();
-        const closed = await kenQ("SELECT COUNT(*) as total, 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, COALESCE(SUM(pnl_cents), 0) as pnl FROM ken_trades WHERE status != 'open'");
-        const cs = closed.rows[0] || {};
-        const pnl = parseInt(cs.pnl || 0);
-        const pnlStr = `${pnl >= 0 ? '+' : ''}$${(pnl/100).toFixed(2)}`;
-        const pnlEmoji = pnl > 0 ? ':moneybag:' : pnl < 0 ? ':chart_with_downwards_trend:' : ':neutral_face:';
-        const openLines = traderState.openPositions.map(t =>
-          `  • ${t.side.toUpperCase()} ${t.ticker} x${t.count} @ ${t.price_cents}¢ ($${(t.cost_cents/100).toFixed(2)})`
-        ).join('\n');
-        parts.push(`:bank: *Auto-Trader Portfolio*\n` +
-          `Budget: $${(TRADE_CONFIG.budget_cents/100).toFixed(2)} | Invested: $${(traderState.totalInvested/100).toFixed(2)} | Available: $${(traderState.available/100).toFixed(2)}\n` +
-          `Open: ${traderState.openCount}/${TRADE_CONFIG.max_open_positions} positions` +
-          (openLines ? `\n${openLines}` : '') + `\n` +
-          `${pnlEmoji} P&L: *${pnlStr}* (${cs.wins || 0}W/${cs.losses || 0}L) | Today loss: $${(traderState.dailyLoss/100).toFixed(2)}/$${(TRADE_CONFIG.daily_loss_limit_cents/100).toFixed(2)}`);
-      } catch {}
-
-      // Performance stats
-      try {
-        const { rows: perfRows } = await kenQ(`
-          SELECT COUNT(*) as total_preds,
-            SUM(CASE WHEN outcome='win' THEN 1 ELSE 0 END) as wins,
-            SUM(CASE WHEN outcome='loss' THEN 1 ELSE 0 END) as losses
-          FROM ken_predictions WHERE created_at > NOW() - INTERVAL '24 hours'
-        `);
-        const perf = perfRows[0];
-        if (perf && parseInt(perf.total_preds) > 0) {
-          parts.push(`_24h stats: ${perf.total_preds} predictions, ${perf.wins || 0}W/${perf.losses || 0}L_`);
-        }
-      } catch {}
-
-      parts.push(`\nDashboard: http://45.61.58.125:7810`);
-
-      await sendSlack(parts.join('\n\n'));
-
-      // Update performance row
-      try {
-        await kenQ(`UPDATE ken_performance SET slack_sent = true WHERE id = (SELECT MAX(id) FROM ken_performance)`);
-      } catch {}
-    } else if (allSignals.length > 0) {
-      const cooldownLeft = Math.max(0, Math.round((SLACK_COOLDOWN_MS - (now - lastSlackTime)) / 60000));
-      console.log(`[Ken] ${allSignals.length} signals found, Slack cooldown: ${cooldownLeft}min left`);
-    } else {
-      console.log(`[Ken] No signals this cycle (${allMarkets.length} markets scanned)`);
-    }
-
-  } catch (err) {
-    console.error('[Ken] Autonomous scan error:', err.message);
-    try { await sendSlack(`:warning: *Ken Error* — Scan failed: ${err.message}`); } catch {}
-  }
-}
-
-// ══════════════════════════════════════════════
-// AUTO-PORTFOLIO TRADING ENGINE — 10 personalities, $20/day each
-// ══════════════════════════════════════════════
-
-// Portfolio strategy filters — each personality decides differently
-// ═══ Portfolio Strategy Filters ═══
-// Each strategy is deliberately exclusive — a signal should match 1-3 portfolios, not all 10.
-// Strategies use edge/confidence/direction/article count/MC/AI to create real diversification.
-const PORTFOLIO_STRATEGIES = {
-  // Alpha: High edge (>15%) with LOWER confidence (<70%). Big edges, less certainty — speculative.
-  aggressive: (sig) => Math.abs(sig.edge) >= 0.15 && sig.confidence < 0.70,
-
-  // Beta: High confidence (>70%) with ANY edge. The "safe" bet — trusts confidence over edge size.
-  conservative: (sig) => sig.confidence >= 0.70 && Math.abs(sig.edge) >= 0.05,
-
-  // Storm: Weather/climate markets identified by market_id patterns or reasoning keywords
-  weather: (sig) => /weather|temp|rain|snow|wind|hurricane|precip|storm|°f|heat|cold|frost|drought|flood|tornado|KXTEMP|KXRAIN|KXSNOW|KXHURR/i.test((sig.reasoning || '') + ' ' + (sig.market_id || '')),
-
-  // Momentum: Only trades when edge increased (high agreement = many articles agree).
-  // Needs 3+ articles with >65% agreement — evidence of growing consensus.
-  momentum: (sig) => (sig.agreement || 0) >= 0.65 && (sig.num_articles || 0) >= 3 && Math.abs(sig.edge) >= 0.05,
-
-  // Contrarian: ONLY BUY_NO trades. Bets against the crowd. Never buys YES.
-  contrarian: (sig) => sig.direction === 'BUY_NO',
-
-  // Nova: Only Gemini AI-enhanced signals. If AI didn't weigh in, skip.
-  ai_enhanced: (sig) => sig.ai_enhanced === true,
-
-  // Oracle: MC-validated WITH tight spread (stdDev < 8%) AND moderate edge. The "careful" bot.
-  mc_validated: (sig) => sig.mc_consistent === true && (sig.mc_stddev || 99) < 0.08 && Math.abs(sig.edge) >= 0.05 && Math.abs(sig.edge) < 0.20,
-
-  // Shark: Small edges (3-10%), rapid fire. Takes quantity over quality. Rejects big edges.
-  scalper: (sig) => Math.abs(sig.edge) >= 0.03 && Math.abs(sig.edge) < 0.10 && sig.confidence >= 0.30,
-
-  // Turtle: Ultra-patient. Needs EVERYTHING: 5+ articles, MC consistent, confidence >80%, moderate edge.
-  patient: (sig) => sig.confidence >= 0.80 && sig.mc_consistent === true && (sig.num_articles || 0) >= 5 && Math.abs(sig.edge) >= 0.05,
-
-  // Wildcard: Random 20% of signals — true control group. Lower chance than before to create separation.
-  random: (sig) => Math.random() < 0.20,
-
-  // ═══ RISKY PORTFAUXLIOS: Micro → Macro ═══
-
-  // Penny: Any edge >= 1%. Catches everything. Micro bets, max volume.
-  penny_pincher: (sig) => Math.abs(sig.edge) >= 0.01,
-
-  // Spray: Random 50% of ALL signals. Shotgun approach — tests luck vs skill.
-  spray_pray: (sig) => Math.random() < 0.50,
-
-  // Nickel: Low bar (edge >= 2%, conf >= 20%). Catches most signals.
-  nickel_slots: (sig) => Math.abs(sig.edge) >= 0.02 && sig.confidence >= 0.20,
-
-  // Degen: Edge >= 5%, ignores confidence entirely. Pure edge chaser.
-  degen_lite: (sig) => Math.abs(sig.edge) >= 0.05,
-
-  // Politics: Only political markets. KXPRES, KXSEN, KXGOV, KXCONGRESS, KXPRESPERSON, etc.
-  politics_junkie: (sig) => /KXPRES|KXSEN|KXGOV|KXCONG|KXHOUSE|KXELEC|KXPARTY|KXPRESPERSON|political|election|congress|senate|president|governor/i.test((sig.market_id || '') + ' ' + (sig.reasoning || '')),
-
-  // Double: Only high-edge signals (>20%). Goes big when edge is massive.
-  double_down: (sig) => Math.abs(sig.edge) >= 0.20,
-
-  // Night: Only signals with high MC stddev (>8%) — volatile markets others reject.
-  volatility_rider: (sig) => (sig.mc_stddev || 0) >= 0.08 && Math.abs(sig.edge) >= 0.05,
-
-  // Whale: Edge >25% AND confidence >60%. Rare but massive. The sniper.
-  whale: (sig) => Math.abs(sig.edge) >= 0.25 && sig.confidence >= 0.60,
-
-  // YOLO: Edge >30%. Doesn't care about anything else. Max degen.
-  yolo: (sig) => Math.abs(sig.edge) >= 0.30,
-
-  // Full Send: Takes EVERY signal. Max exposure. Tests whether the signal pipeline itself is profitable.
-  full_send: (sig) => true,
-
-  // ═══ CPU-POWERED: Computational / Multi-Factor Strategies ═══
-
-  // Kelly: Only trades when Kelly fraction (edge / odds) > 0.10. Classic bankroll math.
-  // Kelly f* = (bp - q) / b where b=odds, p=prob_win, q=prob_lose. Simplified: edge/(1+edge).
-  kelly_criterion: (sig) => {
-    const e = Math.abs(sig.edge);
-    const kelly = e / (1 + e); // simplified Kelly fraction
-    return kelly >= 0.10 && sig.confidence >= 0.40;
-  },
-
-  // Sharpe: Signal-to-noise ratio. edge / mc_stddev > 2.0. High SNR = clean signal, low noise.
-  sharpe_sniper: (sig) => {
-    const snr = Math.abs(sig.edge) / Math.max(sig.mc_stddev, 0.01);
-    return snr >= 2.0 && sig.mc_consistent;
-  },
-
-  // Bayesian: Combines confidence with article count as evidence weight.
-  // More articles = stronger prior. posterior = conf * (1 - 1/(1+articles)) * edge_factor.
-  bayesian_blend: (sig) => {
-    const evidenceWeight = 1 - 1 / (1 + (sig.num_articles || 0));
-    const posterior = sig.confidence * evidenceWeight * (1 + Math.abs(sig.edge));
-    return posterior >= 0.55;
-  },
-
-  // Ensemble: ALL factors must align — AI + MC + confidence + edge. Maximum consensus required.
-  ensemble_lock: (sig) => sig.ai_enhanced && sig.mc_consistent && sig.confidence >= 0.60 && Math.abs(sig.edge) >= 0.10 && (sig.num_articles || 0) >= 2,
-
-  // Mean Revert: High MC volatility (stddev > 10%) + moderate edge. Bets on reversion to mean.
-  mean_revert: (sig) => (sig.mc_stddev || 0) >= 0.10 && Math.abs(sig.edge) >= 0.08 && Math.abs(sig.edge) <= 0.25,
-
-  // Info Ratio: (articles * agreement) / max(mc_stddev, 0.01). Quality of information per unit noise.
-  info_ratio: (sig) => {
-    const ir = ((sig.num_articles || 0) * (sig.agreement || 0)) / Math.max(sig.mc_stddev, 0.01);
-    return ir >= 15 && Math.abs(sig.edge) >= 0.05;
-  },
-
-  // Anti-FOMO: Small edge (<12%) but HIGH confidence (>75%). Calm, certain, steady plays.
-  anti_fomo: (sig) => Math.abs(sig.edge) >= 0.03 && Math.abs(sig.edge) <= 0.12 && sig.confidence >= 0.75,
-
-  // Nightcrawler: BUY_NO only + AI enhanced + MC consistent. Sophisticated short-bias strategy.
-  nightcrawler: (sig) => sig.direction === 'BUY_NO' && sig.ai_enhanced && sig.mc_consistent,
-
-  // Heatseeker: Composite multi-factor score. Weighted blend of all signal dimensions.
-  // score = edge*40 + conf*30 + agreement*20 + min(articles/10,1)*10. Trade when score > 35.
-  heatseeker: (sig) => {
-    const score = Math.abs(sig.edge) * 40 + sig.confidence * 30 + (sig.agreement || 0) * 20 + Math.min((sig.num_articles || 0) / 10, 1) * 10;
-    return score >= 35;
-  },
-
-  // Quant: Geometric mean of edge and confidence, weighted by MC consistency. Pure math bot.
-  // geoMean = sqrt(edge * conf). Trade when geoMean > 0.15 and MC agrees.
-  quant_core: (sig) => {
-    const geo = Math.sqrt(Math.abs(sig.edge) * Math.max(sig.confidence, 0.001));
-    return geo >= 0.15 && (sig.mc_consistent || sig.confidence >= 0.80);
-  },
-
-  // ═══ ETF CATEGORY SPECIALISTS: Sector-Focused Portfolios ═══
-
-  etf_presidential: (sig) => /KXPRESPERSON|KXPRESPARTY/i.test(sig.market_id) ||
-    /president|2028 election|presidential race|white house|oval office|nominee/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_cabinet: (sig) => /KXCABOUT|KXCABLEAVE/i.test(sig.market_id) ||
-    /cabinet|secretary of|confirmation hearing|appointee|gabbard|hegseth|rubio|noem|rfk|bessent|lutnick/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_congress: (sig) => /KXCONG|KXHOUSE|SENATE|KXNEXTSPEAKER|KXCAPCONTROL|KXVETOOVERRIDE/i.test(sig.market_id) ||
-    /congress|senate|house of rep|speaker|legislation|bill pass|shutdown|filibuster|debt ceiling/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_scotus: (sig) => /KXSCOURT/i.test(sig.market_id) ||
-    /supreme court|scotus|justice|ruling|overturn|constitutional|judicial review/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_fed: (sig) => /KXFEDCHAIRNOM|KXBALANCE/i.test(sig.market_id) ||
-    /federal reserve|fed chair|interest rate|fomc|rate cut|rate hike|powell|warsh|monetary policy|quantitative/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_economy: (sig) => /KXGDP/i.test(sig.market_id) ||
-    /\bgdp\b|recession|economic growth|jobs report|unemployment|nonfarm payroll|consumer spending|retail sales|pce\b|cpi\b/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_geopolitics: (sig) => /KXZELENSKYPUTIN|KXNEXTUKPM|KXNEXTIRANLEADER|KXEUEXIT|KXTAIWANLVL|KXFTA/i.test(sig.market_id) ||
-    /geopolit|war\b|invasion|sanction|nato|treaty|ceasefire|diplomat|putin|zelensky|china|taiwan|iran|tariff|trade war/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_territory: (sig) => /KXGREENLAND|KXGREENTERRITORY|KXGREENLANDPRICE|KXGTAPRICE|KXSTATE51|KXUSAEXPANDTERRITORY/i.test(sig.market_id) ||
-    /greenland|statehood|territory|annex|state 51|panama canal|territorial/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_climate: (sig) => /KXWARMING|KXTEMP|KXRAIN|KXSNOW|KXHURR/i.test(sig.market_id) ||
-    /warming|climate|temperature|hurricane|tornado|flood|drought|polar vortex|heat wave|cold snap|freeze|precipitation|storm/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_energy: (sig) => /KXPRIMEENGCONSUMPTION|KXENERG/i.test(sig.market_id) ||
-    /\benergy\b|lng\b|natural gas|oil price|petroleum|electricity|power grid|blackout|eia\b|opec|crude|pipeline|refinery|utility|heating bill/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_tech: (sig) => /KXOAIANTH|KXMUSKTRILLION|KXFUSION/i.test(sig.market_id) ||
-    /artificial intelligence|\bai\b|openai|anthropic|chatgpt|gpt-5|machine learning|musk.*trillion|tesla|nvidia|semiconductor|quantum computing|fusion/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_space: (sig) => /KXMOONMAN|KXCOLONIZEMARS|KXSPACEX/i.test(sig.market_id) ||
-    /spacex|nasa|rocket|orbit|mars|moon|starship|falcon|launch|artemis|blue origin|astronaut|satellite/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_crypto: (sig) => /KXCRYPTO|KXBTC|KXETH/i.test(sig.market_id) ||
-    /bitcoin|btc|ethereum|eth|crypto|blockchain|defi|stablecoin|sec.*crypto|digital asset|web3|binance|coinbase/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_sports: (sig) => /KXNBA|KXNFL|KXNBASEATTLE|KXCANADACUP/i.test(sig.market_id) ||
-    /\bnba\b|\bnfl\b|\bmlb\b|\bnhl\b|sports|playoff|championship|super bowl|world series|draft|mvp|coach/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_entertainment: (sig) => /KXSWIFTKELCE|KXTAYLORSWIFT|KXACTORSONNYCROCKETT|KXLIVENATION/i.test(sig.market_id) ||
-    /taylor swift|celebrity|movie|oscar|grammy|concert|entertainment|viral|streaming|netflix|album|tour/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_legal: (sig) => /KXTRUMPPARDONS|KXINSURRECTION|KXIMPEACH|KXTRUMPREMOVE|KXTRUMPRESIGN/i.test(sig.market_id) ||
-    /pardon|impeach|indictment|trial|guilty|conviction|sentencing|insurrection|special counsel|subpoena|contempt/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_financials: (sig) => /KXBOND/i.test(sig.market_id) ||
-    /bond yield|treasury|10-year|yield curve|s&p|nasdaq|dow jones|stock market|bear market|bull market|correction|ipo|banking/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_govreform: (sig) => /KXAGENCYELIM|KXGOVTCUTS|KXWITHDRAW/i.test(sig.market_id) ||
-    /\bdoge\b|government efficiency|agency.*elim|department.*cut|federal workforce|deregulat|budget cut|spending cut|executive order/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_global_social: (sig) => /KXNEWPOPE|KXALBERTAREFYES/i.test(sig.market_id) ||
-    /pope|vatican|catholic|referendum|independence|separatist|social movement|protest|migration|refugee/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_broad_market: (sig) => Math.abs(sig.edge) >= 0.08 && sig.confidence >= 0.50 && (sig.mc_consistent || sig.ai_enhanced),
-
-  // ═══ SECTOR-BASED PORTFOLIOS (IDs 51-100): Granular sub-sector filters ═══
-
-  // — Energy & Commodities (51-55) —
-  lng_exports: (sig) => /lng|liquefied natural gas|natural gas export|gas terminal|lng shipment|henry hub/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  oil_price_band: (sig) => /\boil\b|crude|brent|wti|petroleum|barrel|oil price|oil production/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  natgas_seasonal: (sig) => /natural gas|natgas|gas price|gas storage|heating season|gas futures/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  opec_decisions: (sig) => /opec|oil cartel|production cut|oil output|saudi.*oil|oil supply/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  power_grid: (sig) => /power grid|blackout|electricity|energy grid|brownout|utility|power outage|grid fail/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — AI & Tech Stocks (56-60) —
-  semiconductor: (sig) => /semiconductor|chip|nvidia|tsmc|intel|amd|chip act|wafer|fab\b|gpu shortage/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  ai_regulation: (sig) => /ai regulation|ai safety|ai executive order|ai bill|regulate.*ai|ai governance|ai act/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  tech_earnings: (sig) => /tech.*earning|earning.*tech|apple.*revenue|google.*revenue|meta.*revenue|amazon.*revenue|magnificent seven|big tech.*report/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  autonomous_vehicles: (sig) => /autonomous|self.driving|robotaxi|waymo|cruise|lidar|adas|driverless/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  cloud_market: (sig) => /cloud computing|aws|azure|google cloud|cloud revenue|saas|cloud market|cloud spend/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Crypto & Digital (61-65) —
-  btc_price_bands: (sig) => /bitcoin|btc|bitcoin price|btc.*\$|bitcoin.*\$|bitcoin.*above|bitcoin.*below/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  eth_upgrades: (sig) => /ethereum|eth\b|eth.*upgrade|ethereum.*upgrade|ethereum.*merge|eth.*fork|ether.*price/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  crypto_regulation: (sig) => /crypto.*regulat|sec.*crypto|crypto.*ban|crypto.*bill|digital asset.*law|crypto.*legal|stablecoin.*act/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  stablecoin_policy: (sig) => /stablecoin|usdt|usdc|tether|circle|stablecoin.*regulat|stablecoin.*bill|cbdc/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  defi_events: (sig) => /defi|decentralized finance|dex|uniswap|aave|compound|liquidity pool|yield farm|tvl/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Macro Economics (66-70) —
-  cpi_monthly: (sig) => /\bcpi\b|consumer price|inflation.*rate|inflation.*report|core inflation|price index/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  nfp_monthly: (sig) => /nonfarm|non.farm|payroll|jobs report|jobs.*added|employment.*report|unemployment.*rate|jobless.*claim/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  housing_data: (sig) => /housing|home.*sale|home.*price|mortgage|existing home|housing start|case.shiller|real estate.*data/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  gdp_quarterly: (sig) => /\bgdp\b|gross domestic|economic growth|gdp.*quarter|gdp.*annual|recession.*gdp/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  yield_spread: (sig) => /yield curve|yield spread|inverted yield|2.year.*10.year|treasury spread|curve inversion/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Finance & Markets (71-75) —
-  sp500_levels: (sig) => /s&p|s&p 500|sp500|spx|s&p.*above|s&p.*below|stock market.*level/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  fed_rate_path: (sig) => /fed.*rate|interest rate|fomc|rate cut|rate hike|federal funds|fed.*meeting|monetary policy|powell.*rate/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  bank_stress: (sig) => /bank.*stress|bank.*fail|banking crisis|bank run|fdic|bank.*collapse|regional bank|svb|silicon valley bank/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  bond_yields: (sig) => /bond.*yield|treasury.*yield|10.year.*yield|bond.*market|treasury.*rate|bond.*price/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  volatility_index: (sig) => /\bvix\b|volatility index|volatility.*spike|cboe|fear.*index|market.*volatility/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Politics & Policy (76-80) —
-  state_elections: (sig) => /KXSEN|KXGOV|state election|governor.*race|senate.*race|state house|state legislature|primary.*election/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  congress_votes: (sig) => /KXCONG|KXHOUSE|congress.*vote|house.*vote|senate.*vote|bill.*pass|legislation.*vote|floor vote/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  tariff_policy: (sig) => /tariff|trade war|import duty|trade deficit|trade policy|customs duty|trade agreement|wto/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  immigration_policy: (sig) => /immigration|border|migrant|asylum|deportation|visa|daca|immigration.*reform|border.*wall/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  tax_legislation: (sig) => /tax.*bill|tax.*reform|tax.*cut|tax.*hike|tax.*code|irs|estate tax|capital gains.*tax|corporate tax/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Sports (81-85) —
-  nba_outcomes: (sig) => /KXNBA|\bnba\b|basketball|nba.*final|nba.*champion|nba.*mvp|nba.*draft/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  nfl_outcomes: (sig) => /KXNFL|\bnfl\b|football|super bowl|nfl.*draft|nfl.*champion|quarterback|touchdown/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  soccer_global: (sig) => /soccer|football.*world|world cup|premier league|champions league|mls|fifa|la liga|bundesliga/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  mlb_outcomes: (sig) => /KXMLB|\bmlb\b|baseball|world series|mlb.*draft|home run|batting|pitcher/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  combat_sports: (sig) => /\bufc\b|\bmma\b|boxing|fight.*night|ufc.*champion|knockout|title fight|heavyweight/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Health & Science (86-90) —
-  fda_approvals: (sig) => /fda.*approv|fda.*reject|drug.*approv|new drug|biologic.*approv|fda.*review|pdufa/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  disease_tracking: (sig) => /outbreak|pandemic|epidemic|covid|h5n1|bird flu|mpox|disease.*spread|cdc.*alert|who.*emergency/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  biotech_catalysts: (sig) => /biotech|clinical trial|phase 3|gene therapy|mrna|crispr|drug.*trial|biotech.*stock/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  health_metrics: (sig) => /life expectancy|obesity|opioid|drug overdose|health.*metric|public health|mortality rate|vaccination rate/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  space_milestones: (sig) => /spacex|nasa|rocket.*launch|mars|moon.*landing|starship|artemis|blue origin|orbit|satellite.*launch/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Media & Culture (91-95) —
-  box_office: (sig) => /box office|movie.*gross|film.*revenue|opening weekend|blockbuster|cinema.*sales/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  awards_shows: (sig) => /oscar|grammy|emmy|golden globe|academy award|best picture|best actor|awards.*ceremony/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  streaming_subs: (sig) => /netflix.*subscriber|disney.*subscriber|streaming.*war|subscriber.*count|streaming.*growth|hulu|paramount/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  social_trends: (sig) => /viral|tiktok|social media.*trend|influencer|twitter.*trend|internet.*culture|meme/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  reality_outcomes: (sig) => /reality tv|bachelor|survivor|big brother|reality.*show|dancing.*stars|masked singer/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Global & Geopolitics (96-100) —
-  global_conflicts: (sig) => /war\b|invasion|ceasefire|military.*conflict|armed conflict|escalation|airstrike|troops.*deploy/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  sanctions_policy: (sig) => /sanction|embargo|trade restriction|asset freeze|sanctions.*russia|sanctions.*china|sanctions.*iran/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  climate_extremes: (sig) => /climate.*extreme|extreme weather|heat.*record|cold.*record|flood|wildfire|hurricane.*category|tornado|drought/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  forex_events: (sig) => /forex|currency.*crisis|dollar.*index|yen|euro.*dollar|fx.*rate|currency.*devaluation|exchange rate/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  geopolitical_shock: (sig) => /black swan|geopolitical.*shock|coup|assassination|regime.*change|emergency.*declaration|martial law|unexpected.*event/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-};
-
-// Position sizing per strategy — differentiated risk profiles
-const PORTFOLIO_SIZING = {
-  aggressive: (edge, conf) => Math.max(2, Math.min(5, Math.round(Math.abs(edge) * 30))),     // 2-5 contracts, edge-weighted
-  conservative: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),               // 1-3 contracts, confidence-weighted
-  weather: (edge, conf) => 2,                                                                  // Fixed 2 contracts
-  momentum: (edge, conf) => Math.max(1, Math.min(4, Math.round((conf + Math.abs(edge)) * 5))),// 1-4, blended
-  contrarian: (edge, conf) => 1,                                                               // Always 1 contract (high risk)
-  ai_enhanced: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),                // 1-3, conservative
-  mc_validated: (edge, conf) => Math.max(2, Math.min(4, Math.round(conf * 5))),               // 2-4, confidence-driven
-  scalper: (edge, conf) => 1,                                                                  // Always 1 (high volume, small bets)
-  patient: (edge, conf) => Math.max(3, Math.min(5, Math.round(conf * 6))),                    // 3-5, big when confident
-  random: (edge, conf) => Math.max(1, Math.min(3, Math.ceil(Math.random() * 3))),             // 1-3 random
-
-  // ═══ RISKY SIZING: Micro → Macro ═══
-  penny_pincher: () => 1,                                                                       // Always 1 — micro
-  spray_pray: () => Math.ceil(Math.random() * 2),                                               // 1-2 random
-  nickel_slots: () => 1,                                                                         // Fixed 1
-  degen_lite: (edge) => Math.max(1, Math.min(3, Math.round(Math.abs(edge) * 20))),             // 1-3, edge-scaled
-  politics_junkie: (edge, conf) => Math.max(2, Math.min(3, Math.round(conf * 4))),             // 2-3, confidence
-  double_down: (edge) => Math.max(4, Math.min(8, Math.round(Math.abs(edge) * 30))),            // 4-8, big edge bets
-  volatility_rider: (edge) => Math.max(2, Math.min(4, Math.round(Math.abs(edge) * 25))),       // 2-4, edge-weighted
-  whale: (edge, conf) => Math.max(5, Math.min(10, Math.round(conf * 12))),                     // 5-10, confidence-heavy
-  yolo: (edge) => Math.max(8, Math.min(15, Math.round(Math.abs(edge) * 40))),                  // 8-15, MAX edge scale
-  full_send: (edge, conf) => Math.max(1, Math.min(25, Math.round(Math.abs(edge) * conf * 50))),// 1-25, edge*conf scaled
-
-  // ═══ CPU-POWERED SIZING: Computational approaches ═══
-  kelly_criterion: (edge, conf) => Math.max(1, Math.min(8, Math.round((edge / (1 + edge)) * 20))),       // Kelly fraction scaled to 1-8
-  sharpe_sniper: (edge, conf) => Math.max(2, Math.min(6, Math.round(conf * 8))),                         // 2-6, conf-weighted (high SNR = trust it)
-  bayesian_blend: (edge, conf) => Math.max(1, Math.min(5, Math.round(Math.sqrt(edge * conf) * 15))),     // 1-5, geometric scaling
-  ensemble_lock: (edge, conf) => Math.max(3, Math.min(7, Math.round((edge + conf) * 5))),                // 3-7, sum-scaled (all factors aligned)
-  mean_revert: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),                          // 1-4, moderate sizing for reversion plays
-  info_ratio: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),                            // 2-5, confidence-driven
-  anti_fomo: (edge, conf) => Math.max(2, Math.min(4, Math.round(conf * 5))),                             // 2-4, steady sizing for calm plays
-  nightcrawler: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),                          // 1-3, conservative shorts
-  heatseeker: (edge, conf) => Math.max(2, Math.min(6, Math.round((edge * 20 + conf * 3) / 2))),         // 2-6, composite score-driven
-  quant_core: (edge, conf) => Math.max(2, Math.min(8, Math.round(Math.sqrt(edge * conf) * 25))),         // 2-8, geometric mean heavy
-
-  // ═══ ETF CATEGORY SIZING ═══
-  etf_presidential: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  etf_cabinet: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_congress: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_scotus: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  etf_fed: (edge, conf) => Math.max(2, Math.min(5, Math.round(edge * 20 + conf * 2))),
-  etf_economy: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),
-  etf_geopolitics: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_territory: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  etf_climate: () => 2,
-  etf_energy: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 18))),
-  etf_tech: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),
-  etf_space: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  etf_crypto: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 20))),
-  etf_sports: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_entertainment: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  etf_legal: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_financials: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),
-  etf_govreform: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  etf_global_social: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
-  etf_broad_market: (edge, conf) => Math.max(2, Math.min(6, Math.round(edge * 15 + conf * 3))),
-
-  // ═══ SECTOR-BASED SIZING (IDs 51-100): Conservative niche bets ═══
-
-  // — Energy & Commodities (51-55) — Moderate sizing, energy can be volatile
-  lng_exports: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  oil_price_band: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),
-  natgas_seasonal: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  opec_decisions: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  power_grid: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-
-  // — AI & Tech Stocks (56-60) — Slightly larger, higher conviction sector
-  semiconductor: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  ai_regulation: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  tech_earnings: (edge, conf) => Math.max(2, Math.min(5, Math.round(edge * 18))),
-  autonomous_vehicles: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  cloud_market: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-
-  // — Crypto & Digital (61-65) — Volatile, keep positions smaller
-  btc_price_bands: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 16))),
-  eth_upgrades: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  crypto_regulation: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  stablecoin_policy: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  defi_events: (edge, conf) => Math.max(1, Math.min(2, Math.round(edge * 10))),
-
-  // — Macro Economics (66-70) — Data-driven, moderate confidence sizing
-  cpi_monthly: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  nfp_monthly: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  housing_data: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  gdp_quarterly: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),
-  yield_spread: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-
-  // — Finance & Markets (71-75) — Established signals, moderate-to-large
-  sp500_levels: (edge, conf) => Math.max(2, Math.min(5, Math.round(edge * 20))),
-  fed_rate_path: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),
-  bank_stress: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  bond_yields: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  volatility_index: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 18))),
-
-  // — Politics & Policy (76-80) — Binary outcomes, confidence-weighted
-  state_elections: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  congress_votes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  tariff_policy: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  immigration_policy: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  tax_legislation: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-
-  // — Sports (81-85) — Event-driven, fixed small sizing
-  nba_outcomes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  nfl_outcomes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  soccer_global: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  mlb_outcomes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  combat_sports: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-
-  // — Health & Science (86-90) — High-impact but rare, small positions
-  fda_approvals: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  disease_tracking: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  biotech_catalysts: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 10))),
-  health_metrics: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
-  space_milestones: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-
-  // — Media & Culture (91-95) — Fun bets, keep small
-  box_office: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  awards_shows: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  streaming_subs: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
-  social_trends: (edge, conf) => Math.max(1, Math.min(2, Math.round(edge * 8))),
-  reality_outcomes: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
-
-  // — Global & Geopolitics (96-100) — High uncertainty, conservative
-  global_conflicts: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  sanctions_policy: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  climate_extremes: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 10))),
-  forex_events: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  geopolitical_shock: (edge, conf) => Math.max(1, Math.min(2, Math.round(edge * 8))),
-};
-
-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 balance: bankrupt portfolios can't trade (balance = 0 → move to bottom)
-      if (pf.balance_cents <= 0) continue; // BANKRUPT — no more trades
-
-      // 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 || '',
-        market_id: signal.marketId || signal.market_id || '',
-        ai_enhanced: signal.aiEnhanced || signal.ai_enhanced || false,
-        mc_consistent: signal.mcSimulation?.consistent || signal.mc_consistent || false,
-        mc_stddev: signal.mcSimulation?.stdDev || 0.99,
-        volume24h: signal.volume24h || signal.volume_24h || 0,
-        num_articles: signal.num_articles || 0,
-        agreement: signal.agreement || 0,
-        market_title: signal.market_title || signal.marketTitle || '',
-      };
-
-      if (!strategy(sigData)) continue;
-
-      // Hyper-trade: max 10 OPEN positions per market per portfolio (no daily cap)
-      const { rows: dupes } = await kenQ(`
-        SELECT id FROM ken_portfolio_trades
-        WHERE portfolio_id = $1 AND market_id = $2 AND status = 'open'
-      `, [pf.id, signal.marketId || signal.market_id]);
-      if (dupes.length >= 10) continue;
-
-      // Per-strategy position sizing
-      const absEdge = Math.abs(parseFloat(sigData.edge));
-      const conf = parseFloat(sigData.confidence);
-      const sizeFn = PORTFOLIO_SIZING[pf.strategy] || ((e, c) => Math.max(1, Math.min(3, Math.round(e * c * 15))));
-      const contracts = sizeFn(absEdge, conf);
-      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 (cost > pf.balance_cents) continue; // Not enough balance for this trade
-
-      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]);
-
-      // Deduct from balance + update stats
-      await kenQ(`UPDATE ken_portfolios SET balance_cents = balance_cents - $1, total_invested_cents = total_invested_cents + $1, last_trade_at = NOW() WHERE id = $2`, [cost, pf.id]);
-      pf.balance_cents -= cost; // Update in-memory too for subsequent iterations
-
-      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);
-  }
-}
-
-// Capture NAV snapshots for charting — runs every 30 minutes
-async function capturePortfolioSnapshots() {
-  try {
-    const { rows: portfolios } = await kenQ('SELECT * FROM ken_portfolios');
-    for (const pf of portfolios) {
-      const { rows: openTrades } = await kenQ(`
-        SELECT entry_price_cents, contracts, direction, market_id
-        FROM ken_portfolio_trades WHERE portfolio_id = $1 AND status = 'open'
-      `, [pf.id]);
-
-      let openValue = 0;
-      for (const t of openTrades) {
-        const { rows: snaps } = await kenQ(`
-          SELECT last_price FROM ken_market_snapshots WHERE ticker = $1 ORDER BY captured_at DESC LIMIT 1
-        `, [t.market_id]);
-        if (snaps.length > 0) {
-          const curr = parseInt(snaps[0].last_price);
-          openValue += t.direction === 'BUY_YES'
-            ? (curr - t.entry_price_cents) * t.contracts
-            : (t.entry_price_cents - curr) * t.contracts;
-        }
-      }
-
-      const nav = (parseInt(pf.total_returned_cents) || 0) - (parseInt(pf.total_invested_cents) || 0) + openValue;
-      await kenQ(`
-        INSERT INTO ken_portfolio_snapshots (portfolio_id, nav_cents, open_positions) VALUES ($1, $2, $3)
-      `, [pf.id, nav, openTrades.length]);
-    }
-    console.log(`[Ken/Charts] NAV snapshots captured for ${portfolios.length} portfolios`);
-  } catch (e) {
-    console.error('[Ken/Charts] Snapshot error:', e.message);
-  }
-}
-
-// Resolve portfolio trades — HYPER MODE: quick take-profit/stop-loss + aging out
-async function resolvePortfolioTrades() {
-  try {
-    // Check ALL open trades (no minimum age for hyper-trading)
-    const { rows: openTrades } = await kenQ(`
-      SELECT pt.*, p.name as portfolio_name, p.strategy as portfolio_strategy
-      FROM ken_portfolio_trades pt
-      JOIN ken_portfolios p ON p.id = pt.portfolio_id
-      WHERE pt.status = 'open'
-      ORDER BY pt.created_at ASC
-      LIMIT 500
-    `);
-
-    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);
-      const ageMinutes = (Date.now() - new Date(trade.created_at).getTime()) / 60000;
-
-      let pnl = 0;
-      if (trade.direction === 'BUY_YES') {
-        pnl = (currentPrice - trade.entry_price_cents) * trade.contracts;
-      } else {
-        pnl = (trade.entry_price_cents - currentPrice) * trade.contracts;
-      }
-
-      // Hyper-trade exit rules:
-      // 1. Take profit: +3¢ per contract (minimum 5 min hold)
-      // 2. Stop loss: -5¢ per contract (minimum 5 min hold)
-      // 3. Age out: close after 2 hours regardless
-      let status = null;
-      const profitPerContract = pnl / Math.max(1, trade.contracts);
-
-      if (ageMinutes >= 5 && profitPerContract >= 3) {
-        status = 'won'; // Take profit
-      } else if (ageMinutes >= 5 && profitPerContract <= -5) {
-        status = 'lost'; // Stop loss
-      } else if (ageMinutes >= 120) {
-        // Age out after 2 hours — mark based on current P&L
-        status = pnl > 0 ? 'won' : pnl < 0 ? 'lost' : 'expired';
-      }
-
-      if (!status) continue; // Hold position
-
-      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 streakVal = status === 'won' ? 1 : status === 'lost' ? -1 : 0;
-      // total_returned = cost_cents + pnl for winning trades (you get your money back + profit)
-      const returned = pnl > 0 ? trade.cost_cents + pnl : (pnl === 0 ? trade.cost_cents : 0);
-      const wonInc = status === 'won' ? 1 : 0;
-      const lostInc = status === 'lost' ? 1 : 0;
-      const expInc = status === 'expired' ? 1 : 0;
-      await kenQ(`
-        UPDATE ken_portfolios SET
-          balance_cents = balance_cents + $1,
-          total_returned_cents = total_returned_cents + $1,
-          trades_won = trades_won + $2,
-          trades_lost = trades_lost + $3,
-          trades_expired = trades_expired + $4,
-          streak = CASE WHEN $5 > 0 AND streak > 0 THEN streak + 1 WHEN $5 < 0 AND streak < 0 THEN streak - 1 ELSE $5 END,
-          best_trade_cents = GREATEST(best_trade_cents, $6),
-          worst_trade_cents = LEAST(worst_trade_cents, $6)
-        WHERE id = $7
-      `, [returned, wonInc, lostInc, expInc, streakVal, pnl, trade.portfolio_id]);
-
-      // 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
-// ══════════════════════════════════════════════
-async function resolveOldPredictions() {
-  try {
-    // Get pending predictions older than 6 hours
-    const { rows } = await kenQ(`
-      SELECT id, ticker, side, entry_price FROM ken_predictions
-      WHERE outcome = 'pending' AND created_at < NOW() - INTERVAL '6 hours'
-      LIMIT 50
-    `);
-    for (const pred of rows) {
-      // Get latest snapshot for this ticker
-      const { rows: snaps } = await kenQ(`
-        SELECT last_price FROM ken_market_snapshots
-        WHERE ticker = $1 ORDER BY captured_at DESC LIMIT 1
-      `, [pred.ticker]);
-      if (snaps.length > 0) {
-        const currentPrice = snaps[0].last_price;
-        const entryPrice = pred.entry_price || 50;
-        let outcome = 'expired';
-        let pnl = 0;
-        if (pred.side === 'YES') {
-          pnl = currentPrice - entryPrice;
-          outcome = pnl > 0 ? 'win' : pnl < 0 ? 'loss' : 'expired';
-        } else {
-          pnl = entryPrice - currentPrice; // NO side profits when price drops
-          outcome = pnl > 0 ? 'win' : pnl < 0 ? 'loss' : 'expired';
-        }
-        await kenQ(`
-          UPDATE ken_predictions SET outcome = $1, exit_price = $2, pnl_cents = $3, resolved_at = NOW()
-          WHERE id = $4
-        `, [outcome, currentPrice, pnl, pred.id]);
-      }
-    }
-    if (rows.length > 0) console.log(`[Ken] Resolved ${rows.length} old predictions`);
-  } catch (e) {
-    console.error('[Ken] Prediction resolution error:', e.message);
-  }
-}
-
-// ══════════════════════════════════════════════
-// CLEANUP — prune old data (daily)
-// ══════════════════════════════════════════════
-async function cleanupOldData() {
-  try {
-    await kenQ(`DELETE FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '7 days'`);
-    await kenQ(`DELETE FROM ken_sentiment WHERE captured_at < NOW() - INTERVAL '14 days'`);
-    await kenQ(`DELETE FROM ken_performance WHERE scan_time < NOW() - INTERVAL '30 days'`);
-    await kenQ(`DELETE FROM ken_portfolio_snapshots WHERE snapshot_at < NOW() - INTERVAL '60 days'`);
-    console.log('[Ken] Old data cleaned up (including portfolio snapshots 60d retention)');
-  } catch (e) {
-    console.error('[Ken] Cleanup error:', e.message);
-  }
-}
-
-// ── Start Server ──
-server.listen(PORT, '0.0.0.0', () => {
-  console.log(`[Ken] Server running on http://0.0.0.0:${PORT}`);
-  console.log(`[Ken] Kalshi environment: ${KALSHI_ENV.toUpperCase()} (${KALSHI_BASE_URL})`);
-  console.log(`[Ken] Static files: ${DIST}`);
-  console.log(`[Ken] Auth: session cookie + IP whitelist + basic auth`);
-  console.log(`[Ken] ═══════════════════════════════════════`);
-  console.log(`[Ken] AUTONOMOUS MODE: 24/7 MARKET INTELLIGENCE`);
-  console.log(`[Ken] Market scan: every 8 minutes (CPU-boosted, was 15m)`);
-  console.log(`[Ken] Signal pipeline: every 2 minutes (MC simulation + Gemini AI enhanced)`);
-  console.log(`[Ken] Reddit/news: every 15 minutes (doubled, was 30m)`);
-  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 (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] ═══════════════════════════════════════`);
-
-  // Separate mutexes: sweep (heavy Reddit/news) and scan (light Kalshi API) can run concurrently
-  let sweepRunning = false;
-  let scanRunning = false;
-
-  async function runSweepSafe(name, fn) {
-    if (sweepRunning) { console.log(`[Ken] Skipping ${name} — another sweep in progress`); return; }
-    sweepRunning = true;
-    try { await fn(); } catch (e) { console.error(`[Ken] ${name} error:`, e.message); }
-    sweepRunning = false;
-    if (global.gc) global.gc();
-  }
-
-  async function runScanSafe(fn) {
-    if (scanRunning) { console.log('[Ken] Skipping Scan — another scan in progress'); return; }
-    scanRunning = true;
-    try { await fn(); } catch (e) { console.error(`[Ken] Scan error:`, e.message); }
-    scanRunning = false;
-    if (global.gc) global.gc();
-  }
-
-  // Load stored Kalshi env preference, trade config, and scan config from DB
-  refreshKalshiEnv().then(() => {
-    console.log(`[Ken] Kalshi environment: ${KALSHI_ENV.toUpperCase()} (${KALSHI_BASE_URL})`);
-  });
-  loadTradeConfig();
-  loadScanConfig();
-
-  // Initial: run reddit sweep and market scan (scan uses separate mutex, can overlap)
-  setTimeout(async () => {
-    runSweepSafe('Reddit init', redditIntelligenceLoop);
-    setTimeout(() => runScanSafe(autonomousScan), 10000);
-  }, 5000);
-
-  // Start all recurring intervals via stored handles (restartIntervals can update later)
-  scanIntervalId = setInterval(() => runScanSafe(autonomousScan), SCAN_INTERVAL_MS);
-  redditIntervalId = setInterval(() => runSweepSafe('Reddit', redditIntelligenceLoop), REDDIT_INTERVAL_MS);
-  signalIntervalId = setInterval(async () => {
-    if (cachedActiveMarkets.length > 0) {
-      try { await signalPipeline(cachedActiveMarkets); } catch (e) { console.log(`[Ken] Fast pipeline error: ${e.message}`); }
-    }
-  }, SIGNAL_PIPELINE_MS);
-
-  // Resolve old predictions every hour
-  setInterval(() => {
-    resolveOldPredictions().catch(e => console.error('[Ken] Resolution error:', e.message));
-  }, 60 * 60 * 1000);
-
-  // HYPER-TRADE: Resolve portfolio trades (interval stored in handle for UI control)
-  resolveIntervalId = setInterval(() => {
-    resolvePortfolioTrades().catch(e => console.error('[Ken] Portfolio resolution error:', e.message));
-  }, RESOLVE_INTERVAL_MS);
-  setTimeout(() => resolvePortfolioTrades().catch(() => {}), 30000);
-
-  // Portfolio NAV snapshots every 30 minutes for charting
-  setInterval(() => capturePortfolioSnapshots().catch(e => console.error('[Ken/Charts] Error:', e.message)), 30 * 60 * 1000);
-  setTimeout(() => capturePortfolioSnapshots().catch(() => {}), 25000); // Initial after startup
-
-  // 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);
-
-  // Weekly portfolio auto-refill: $100/portfolio every Monday (4-week trial)
-  // Checks every hour, only refills on Mondays between 0:00-0:59 UTC
-  const WEEKLY_REFILL_CENTS = 10000; // $100
-  const REFILL_WEEKS_REMAINING_KEY = 'ken_refill_weeks';
-  setInterval(async () => {
-    try {
-      const now = new Date();
-      if (now.getUTCDay() !== 1 || now.getUTCHours() !== 0) return; // Monday 0:00 UTC only
-      // Check if we already refilled this week
-      const { rows: [config] } = await kenQ(
-        `SELECT value FROM ken_config WHERE key = 'last_refill_date'`
-      ).catch(() => ({ rows: [] }));
-      const today = now.toISOString().split('T')[0];
-      if (config && config.value === today) return; // Already refilled today
-      // Check weeks remaining
-      const { rows: [weeksRow] } = await kenQ(
-        `SELECT value FROM ken_config WHERE key = $1`, [REFILL_WEEKS_REMAINING_KEY]
-      ).catch(() => ({ rows: [] }));
-      const weeksLeft = weeksRow ? parseInt(weeksRow.value) : 4;
-      if (weeksLeft <= 0) { console.log('[Ken] Weekly refill: 0 weeks remaining, skipping'); return; }
-      // Refill all portfolios
-      const { rowCount } = await kenQ(
-        `UPDATE ken_portfolios SET balance_cents = balance_cents + $1`, [WEEKLY_REFILL_CENTS]
-      );
-      // Update tracking (value column is jsonb, so wrap in JSON.stringify)
-      await kenQ(`INSERT INTO ken_config (key, value) VALUES ('last_refill_date', $1::jsonb)
-        ON CONFLICT (key) DO UPDATE SET value = $1::jsonb, updated_at = NOW()`, [JSON.stringify(today)]);
-      await kenQ(`INSERT INTO ken_config (key, value) VALUES ($1, $2::jsonb)
-        ON CONFLICT (key) DO UPDATE SET value = $2::jsonb, updated_at = NOW()`, [REFILL_WEEKS_REMAINING_KEY, JSON.stringify(String(weeksLeft - 1))]);
-      console.log(`[Ken] Weekly refill: +$${WEEKLY_REFILL_CENTS/100} to ${rowCount} portfolios. ${weeksLeft - 1} weeks remaining.`);
-    } catch (e) { console.error('[Ken] Weekly refill error:', e.message); }
-  }, 60 * 60 * 1000); // Check every hour
-
-  // Cleanup old data every 24 hours
-  setInterval(() => {
-    cleanupOldData().catch(e => console.error('[Ken] Cleanup error:', e.message));
-  }, 24 * 60 * 60 * 1000);
-});
diff --git a/kalshi-dash/server.js.pre-scrub-2026-05-07.bak b/kalshi-dash/server.js.pre-scrub-2026-05-07.bak
deleted file mode 100644
index f516b41..0000000
--- a/kalshi-dash/server.js.pre-scrub-2026-05-07.bak
+++ /dev/null
@@ -1,9138 +0,0 @@
-import http from 'http';
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import pg from 'pg';
-import crypto from 'crypto';
-import { OAuth2Client } from 'google-auth-library';
-import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
-import os from 'os';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-const PORT = 7810;
-const DIST = path.join(__dirname, 'dist');
-
-// ── Unified DW Auth Config ──
-// Audit 2026-05-04: fail-closed on missing env. Old literal `DWSecure2024!`
-// is now in source-control history and must be rotated.
-const AUTH_USER = process.env.AUTH_USERNAME || 'admin';
-const AUTH_PASS = process.env.AUTH_PASSWORD;
-if (!AUTH_PASS) {
-  throw new Error('AUTH_PASSWORD env var required (no fallback for production safety)');
-}
-const SESSION_COOKIE = 'kalshi_session';
-const SESSION_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
-const WHITELISTED_IPS = ['127.0.0.1', '::1', '45.61.58.125', 'localhost'];
-
-// ── Slack Alerts ──
-const SLACK_WEBHOOK = 'https://hooks.slack.com/services/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';
-
-async function sendSlack(text) {
-  // Slack notifications disabled per Steve
-  return;
-}
-
-// ── Google OAuth Config ──
-const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
-const ALLOWED_EMAILS = (process.env.ALLOWED_EMAILS || '').split(',').filter(Boolean);
-const googleClient = GOOGLE_CLIENT_ID ? new OAuth2Client(GOOGLE_CLIENT_ID) : null;
-
-// ── NWS Weather Config ──
-const NWS_BASE = 'https://api.weather.gov';
-const NWS_USER_AGENT = 'Ken-Bot/1.0 (steve@designerwallcoverings.com)';
-
-const CITY_COORDS = {
-  'New York': { lat: 40.7128, lon: -74.0060 },
-  'Los Angeles': { lat: 34.0522, lon: -118.2437 },
-  'Chicago': { lat: 41.8781, lon: -87.6298 },
-  'Houston': { lat: 29.7604, lon: -95.3698 },
-  'Phoenix': { lat: 33.4484, lon: -112.0740 },
-  'Philadelphia': { lat: 39.9526, lon: -75.1652 },
-  'Dallas': { lat: 32.7767, lon: -96.7970 },
-  'San Francisco': { lat: 37.7749, lon: -122.4194 },
-  'Seattle': { lat: 47.6062, lon: -122.3321 },
-  'Denver': { lat: 39.7392, lon: -104.9903 },
-  'Boston': { lat: 42.3601, lon: -71.0589 },
-  'Miami': { lat: 25.7617, lon: -80.1918 },
-  'Atlanta': { lat: 33.7490, lon: -84.3880 },
-  'Minneapolis': { lat: 44.9778, lon: -93.2650 },
-  'Washington': { lat: 38.9072, lon: -77.0369 },
-  'Las Vegas': { lat: 36.1699, lon: -115.1398 },
-};
-
-async function nwsFetch(url) {
-  const controller = new AbortController();
-  // Hard Promise.race timeout — AbortController alone can't kill DNS-level hangs
-  const hardTimeout = new Promise((_, reject) =>
-    setTimeout(() => { controller.abort(); reject(new Error('NWS timeout (5s)')); }, 5000)
-  );
-  const doFetch = async () => {
-    const res = await fetch(url, {
-      headers: { 'User-Agent': NWS_USER_AGENT, 'Accept': 'application/geo+json' },
-      signal: controller.signal,
-    });
-    if (!res.ok) throw new Error(`NWS ${res.status}: ${res.statusText}`);
-    return res.json();
-  };
-  return Promise.race([doFetch(), hardTimeout]);
-}
-
-function extractForecastFeatures(forecastData, variable) {
-  const periods = forecastData?.properties?.periods || [];
-  if (!periods.length) return null;
-  const features = { variable, values: [], timestamps: [] };
-  for (const p of periods) {
-    let value = null;
-    if (variable === 'temperature') { value = p.temperature; features.unit = p.temperatureUnit; }
-    else if (variable === 'wind') { const m = p.windSpeed?.match(/(\d+)/); if (m) value = parseInt(m[1]); features.unit = 'mph'; }
-    else if (variable === 'precipitation') { value = p.probabilityOfPrecipitation?.value ?? 0; features.unit = 'percent'; }
-    if (value !== null) { features.values.push(value); features.timestamps.push(p.startTime); }
-  }
-  if (features.values.length) {
-    features.mean = features.values.reduce((a, b) => a + b, 0) / features.values.length;
-    features.min = Math.min(...features.values);
-    features.max = Math.max(...features.values);
-    features.latest = features.values[0];
-  }
-  return features;
-}
-
-// ── Prediction Model ──
-function ensembleExceedance(features, threshold, operator = '>=') {
-  const vals = features?.values || [];
-  if (!vals.length) return null;
-  let ct = 0;
-  for (const v of vals) {
-    if (operator === '>=' && v >= threshold) ct++;
-    else if (operator === '<=' && v <= threshold) ct++;
-    else if (operator === '>' && v > threshold) ct++;
-    else if (operator === '<' && v < threshold) ct++;
-  }
-  return ct / vals.length;
-}
-
-function climatologicalPrior(variable, threshold, month) {
-  const season = month >= 6 && month <= 8 ? 'summer' : month >= 12 || month <= 2 ? 'winter' : month >= 3 && month <= 5 ? 'spring' : 'fall';
-  const priors = {
-    temperature: { summer: t => t > 100 ? 0.05 : t > 90 ? 0.35 : t > 80 ? 0.65 : 0.85, winter: t => t > 60 ? 0.15 : t > 40 ? 0.50 : t > 20 ? 0.75 : 0.90, spring: t => t > 80 ? 0.20 : t > 60 ? 0.55 : t > 40 ? 0.80 : 0.95, fall: t => t > 80 ? 0.25 : t > 60 ? 0.50 : t > 40 ? 0.75 : 0.90 },
-    precipitation: { any: t => t > 2 ? 0.10 : t > 1 ? 0.20 : t > 0.5 ? 0.35 : 0.50 },
-    snow: { winter: t => t > 12 ? 0.05 : t > 6 ? 0.15 : t > 3 ? 0.25 : 0.40, any: () => 0.10 },
-  };
-  const vp = priors[variable]; if (!vp) return 0.50;
-  const fn = vp[season] || vp.any; if (!fn) return 0.50;
-  return fn(threshold);
-}
-
-function bayesianUpdate(prior, forecastProb, weight = 0.7) {
-  return prior * (1 - weight) + forecastProb * weight;
-}
-
-// ══════════════════════════════════════════════════════
-// GEMINI AI PREDICTION ENGINE — uses CPU + AI for better signals
-// ══════════════════════════════════════════════════════
-const GEMINI_API_KEY = 'AIzaSyAO0rLKwtJUKcf3zVmKstBS4udct4QejMo';
-const GEMINI_MODEL = 'gemini-2.0-flash';
-
-async function geminiAnalyze(prompt, maxTokens = 1024) {
-  try {
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 25000);
-    const res = await fetch(
-      `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`,
-      {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        signal: controller.signal,
-        body: JSON.stringify({
-          contents: [{ parts: [{ text: prompt }] }],
-          generationConfig: { temperature: 0.3, maxOutputTokens: maxTokens },
-        }),
-      }
-    );
-    clearTimeout(timeout);
-    if (!res.ok) return null;
-    const data = await res.json();
-    return data?.candidates?.[0]?.content?.parts?.[0]?.text || null;
-  } catch (e) {
-    console.log(`[Ken/Gemini] Error: ${e.message}`);
-    return null;
-  }
-}
-
-// AI-Enhanced Signal Scoring — Gemini analyzes top signals for smarter predictions
-let geminiCallCount = 0;
-const GEMINI_DAILY_LIMIT = 200; // budget: ~200 calls/day
-const geminiCallReset = setInterval(() => { geminiCallCount = 0; }, 24 * 60 * 60 * 1000);
-
-async function aiEnhanceSignal(signal, articles, market) {
-  if (geminiCallCount >= GEMINI_DAILY_LIMIT) return signal;
-  geminiCallCount++;
-
-  const articleSummary = articles.slice(0, 8).map((a, i) =>
-    `${i + 1}. [${a.source}] ${a.headline?.substring(0, 150)}`
-  ).join('\n');
-
-  const prompt = `You are Ken, an expert prediction market analyst. Analyze this trading signal and provide a calibrated probability estimate.
-
-MARKET: "${market.title}" ${market.subtitle ? '- ' + market.subtitle : ''}
-CURRENT PRICE: YES at ${market.yes_ask}¢, NO at ${100 - market.yes_ask}¢
-24H VOLUME: ${market.volume_24h?.toLocaleString() || '?'}
-OUR SIGNAL: ${signal.direction} with ${(Math.abs(signal.expectedEdge) * 100).toFixed(1)}% edge
-
-RECENT NEWS (${articles.length} articles):
-${articleSummary}
-
-SENTIMENT ANALYSIS: category=${articles[0]?.category || '?'}, direction=${articles[0]?.direction || '?'}, agreement=${((signal.agreement || 0) * 100).toFixed(0)}%
-
-Respond with ONLY a JSON object (no markdown):
-{"probability": 0.XX, "confidence": 0.XX, "reasoning": "one sentence", "adjustedEdge": X.XX, "riskFlag": "none|caution|high_risk"}`;
-
-  const response = await geminiAnalyze(prompt, 256);
-  if (!response) return signal;
-
-  try {
-    // Parse JSON from response (handle markdown wrapping)
-    const jsonStr = response.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
-    const ai = JSON.parse(jsonStr);
-
-    // Blend AI probability with our statistical model
-    const blendedEdge = signal.expectedEdge * 0.6 + (ai.adjustedEdge || signal.expectedEdge) * 0.4;
-    const blendedConf = signal.confidence * 0.5 + (ai.confidence || signal.confidence) * 0.5;
-
-    return {
-      ...signal,
-      expectedEdge: blendedEdge,
-      confidence: Math.min(0.95, blendedConf),
-      aiProbability: ai.probability,
-      aiReasoning: ai.reasoning,
-      aiRiskFlag: ai.riskFlag || 'none',
-      aiEnhanced: true,
-    };
-  } catch (e) {
-    console.log(`[Ken/Gemini] Parse error: ${e.message}`);
-    return signal;
-  }
-}
-
-// ══════════════════════════════════════════════════════
-// MONTE CARLO SIMULATION — CPU-intensive probability estimation
-// ══════════════════════════════════════════════════════
-function monteCarloSimulation(params, iterations = 10000) {
-  const { priorProb, sentimentImpact, momentum2h, momentum6h, volume24h, spread, articleCount, agreement } = params;
-
-  let successCount = 0;
-  const edgeDistribution = [];
-
-  for (let i = 0; i < iterations; i++) {
-    // Add noise to each factor (simulating uncertainty)
-    const sentimentNoise = sentimentImpact + (Math.random() - 0.5) * 0.15;
-    const momentumNoise = (momentum2h || 0) / 100 + (Math.random() - 0.5) * 0.08;
-    const volumeSignal = Math.min(1, (volume24h || 0) / 10000) * (Math.random() * 0.1);
-    const spreadPenalty = Math.max(0, (spread || 0) - 3) * -0.005 * Math.random();
-    const articleBoost = Math.min(0.1, (articleCount || 0) * 0.01) * Math.random();
-    const agreementBoost = ((agreement || 0.5) - 0.5) * 0.15 * Math.random();
-
-    // Simulate probability with all factors
-    const simProb = Math.max(0.01, Math.min(0.99,
-      priorProb + sentimentNoise + momentumNoise + volumeSignal + spreadPenalty + articleBoost + agreementBoost
-    ));
-
-    // Compare to market price — is there an edge?
-    const marketProb = priorProb;
-    const simEdge = simProb - marketProb;
-
-    if (Math.abs(simEdge) >= 0.05) successCount++;
-    edgeDistribution.push(simEdge);
-  }
-
-  // Calculate statistics from simulation
-  edgeDistribution.sort((a, b) => a - b);
-  const mean = edgeDistribution.reduce((a, b) => a + b, 0) / iterations;
-  const p5 = edgeDistribution[Math.floor(iterations * 0.05)];
-  const p25 = edgeDistribution[Math.floor(iterations * 0.25)];
-  const p50 = edgeDistribution[Math.floor(iterations * 0.50)];
-  const p75 = edgeDistribution[Math.floor(iterations * 0.75)];
-  const p95 = edgeDistribution[Math.floor(iterations * 0.95)];
-  const stdDev = Math.sqrt(edgeDistribution.reduce((sum, e) => sum + Math.pow(e - mean, 2), 0) / iterations);
-
-  return {
-    meanEdge: mean,
-    medianEdge: p50,
-    stdDev,
-    confidence: successCount / iterations,
-    percentiles: { p5, p25, p50, p75, p95 },
-    iterations,
-    edgeConsistent: Math.sign(p25) === Math.sign(p75), // Is edge direction consistent?
-  };
-}
-
-// ══════════════════════════════════════════════════════
-// PARALLEL PROCESSING — run CPU-heavy tasks across cores
-// ══════════════════════════════════════════════════════
-const CPU_CORES = Math.max(2, os.cpus().length);
-const WORKER_POOL_SIZE = Math.min(12, CPU_CORES - 2); // Use up to 14 cores, leave 2 for OS + other services
-
-function runInParallel(tasks, workerFn) {
-  return Promise.all(tasks.map(task => {
-    return new Promise(resolve => {
-      try {
-        resolve(workerFn(task));
-      } catch (e) {
-        resolve({ error: e.message, task });
-      }
-    });
-  }));
-}
-
-// Batch process markets in parallel chunks
-async function parallelMarketAnalysis(markets, analysisFunc, batchSize = 50) {
-  const results = [];
-  const batches = [];
-  for (let i = 0; i < markets.length; i += batchSize) {
-    batches.push(markets.slice(i, i + batchSize));
-  }
-
-  // Process batches concurrently (up to WORKER_POOL_SIZE at once)
-  for (let i = 0; i < batches.length; i += WORKER_POOL_SIZE) {
-    const concurrent = batches.slice(i, i + WORKER_POOL_SIZE);
-    const batchResults = await Promise.all(concurrent.map(batch =>
-      Promise.all(batch.map(m => analysisFunc(m).catch(e => ({ error: e.message, ticker: m.ticker }))))
-    ));
-    results.push(...batchResults.flat());
-  }
-  return results;
-}
-
-// Parse Kalshi weather market title into event spec
-function parseWeatherEvent(title) {
-  const t = (title || '').toLowerCase();
-  let variable = 'unknown', threshold = 80, operator = '>=';
-  const tempMatch = t.match(/(\d+)\s*°?\s*f/);
-  // Detect temperature events
-  if (t.includes('temperature') || t.includes('temp') || t.includes('°f') || t.includes('degrees') || t.includes('high') || t.includes('low') || t.includes('cold') || t.includes('hot') || t.includes('warm') || t.includes('heat')) {
-    variable = 'temperature';
-    if (tempMatch) threshold = parseInt(tempMatch[1]);
-  }
-  if (t.includes('rain') || t.includes('precipitation') || t.includes('inch')) { variable = 'precipitation'; threshold = tempMatch ? parseInt(tempMatch[1]) : 0.1; }
-  if (t.includes('snow')) { variable = 'snow'; threshold = tempMatch ? parseInt(tempMatch[1]) : 1; }
-  if (t.includes('wind') || t.includes('gust')) { variable = 'wind'; threshold = tempMatch ? parseInt(tempMatch[1]) : 40; }
-  if (t.includes('below') || t.includes('under') || t.includes('less')) operator = '<=';
-  if (t.includes('above') || t.includes('over') || t.includes('exceed') || t.includes('at least')) operator = '>=';
-  return { variable, threshold, operator };
-}
-
-// Extract city from market title
-function extractCity(title) {
-  for (const city of Object.keys(CITY_COORDS)) {
-    if (title.toLowerCase().includes(city.toLowerCase())) return city;
-  }
-  return null;
-}
-
-// ── Kalshi API Config ──
-let KALSHI_ENV = (process.env.KALSHI_ENV || 'demo').toLowerCase();
-const KALSHI_BASE_URLS = {
-  demo: 'https://demo-api.kalshi.co/trade-api/v2',
-  prod: 'https://api.elections.kalshi.com/trade-api/v2',
-};
-let KALSHI_BASE_URL = KALSHI_BASE_URLS[KALSHI_ENV] || KALSHI_BASE_URLS.demo;
-
-// Refresh env from DB config (called on startup and mode changes)
-async function refreshKalshiEnv() {
-  try {
-    const result = await pool.query('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-    const cfg = result.rows[0]?.config || {};
-    if (cfg.kalshi_env && KALSHI_BASE_URLS[cfg.kalshi_env]) {
-      KALSHI_ENV = cfg.kalshi_env;
-      KALSHI_BASE_URL = KALSHI_BASE_URLS[KALSHI_ENV];
-    }
-  } catch {}
-}
-
-// ── Database ──
-const pool = new pg.Pool({
-  connectionString: 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/bertha_betting',
-  max: 10,
-  idleTimeoutMillis: 30000,
-});
-
-const MIME = {
-  '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
-  '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml',
-  '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff',
-  '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
-  '.webp': 'image/webp', '.map': 'application/json', '.txt': 'text/plain',
-};
-
-// ── DB Helper ──
-async function q(text, params) {
-  return pool.query(text, params);
-}
-
-// ── Response Helper ──
-function json(res, data, status = 200, extraHeaders = {}) {
-  res.writeHead(status, {
-    'Content-Type': 'application/json',
-    'Access-Control-Allow-Origin': '*',
-    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
-    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
-    ...extraHeaders,
-  });
-  res.end(JSON.stringify(data));
-}
-
-// ── Body Parser ──
-async function readBody(req) {
-  return new Promise(resolve => {
-    let d = '';
-    req.on('data', c => d += c);
-    req.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } });
-  });
-}
-
-// ── Cookie / Session Helpers ──
-function parseCookies(req) {
-  const obj = {};
-  (req.headers.cookie || '').split(';').forEach(c => {
-    const [k, ...v] = c.trim().split('=');
-    if (k) obj[k] = v.join('=');
-  });
-  return obj;
-}
-
-function getSession(req) {
-  const cookies = parseCookies(req);
-  const token = cookies[SESSION_COOKIE];
-  if (!token) return null;
-  try {
-    const data = JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString());
-    if (!data.authenticated) return null;
-    const age = Date.now() - data.loginTime;
-    if (age > SESSION_MAX_AGE * 1000) return null;
-    return data;
-  } catch { return null; }
-}
-
-function getClientIP(req) {
-  return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress || '';
-}
-
-function isWhitelistedIP(req) {
-  const ip = getClientIP(req);
-  return WHITELISTED_IPS.some(w => ip.includes(w));
-}
-
-function hasBasicAuth(req) {
-  const auth = req.headers.authorization;
-  if (!auth || !auth.startsWith('Basic ')) return false;
-  const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
-  return user === AUTH_USER && pass === AUTH_PASS;
-}
-
-function isAuthenticated(req) {
-  if (isWhitelistedIP(req)) return true;
-  if (hasBasicAuth(req)) return true;
-  if (getSession(req)) return true;
-  return false;
-}
-
-function setSessionCookie(res, sessionToken) {
-  return `${SESSION_COOKIE}=${encodeURIComponent(sessionToken)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE}`;
-}
-
-function clearSessionCookie() {
-  return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
-}
-
-// ── Kalshi API Signing ──
-// RSA-PSS with SHA-256, salt length 32
-// Signs: timestamp_ms_string + method_uppercase + path
-function signKalshiRequest(privateKeyPem, timestampMs, method, requestPath) {
-  const message = timestampMs + method.toUpperCase() + requestPath;
-  const signature = crypto.sign('sha256', Buffer.from(message), {
-    key: privateKeyPem,
-    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
-    saltLength: 32,
-  });
-  return signature.toString('base64');
-}
-
-// ── Kalshi Authenticated Fetch ──
-async function kalshiFetch(method, apiPath, body = null) {
-  // Load keys from DB
-  const result = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-  const cfg = result.rows[0]?.config || {};
-
-  const apiKey = cfg.kalshi_api_key;
-  const privateKeyPem = cfg.kalshi_private_key;
-
-  if (!apiKey || !privateKeyPem) {
-    throw new Error('Kalshi API keys not configured. Please save your API key and private key first.');
-  }
-
-  const timestampMs = Date.now().toString();
-  // The path for signing is everything after the base domain, starting with /trade-api/v2
-  const basePath = KALSHI_ENV === 'prod' ? '/trade-api/v2' : '/trade-api/v2';
-  const fullSignPath = basePath + apiPath;
-  const signature = signKalshiRequest(privateKeyPem, timestampMs, method, fullSignPath);
-
-  const url = KALSHI_BASE_URL + apiPath;
-  const headers = {
-    'KALSHI-ACCESS-KEY': apiKey,
-    'KALSHI-ACCESS-TIMESTAMP': timestampMs,
-    'KALSHI-ACCESS-SIGNATURE': signature,
-    'Content-Type': 'application/json',
-    'Accept': 'application/json',
-  };
-
-  const fetchOpts = { method, headers };
-  if (body && method !== 'GET' && method !== 'DELETE') {
-    fetchOpts.body = JSON.stringify(body);
-  }
-
-  const response = await fetch(url, fetchOpts);
-  const text = await response.text();
-
-  let data;
-  try {
-    data = JSON.parse(text);
-  } catch {
-    data = { raw: text };
-  }
-
-  if (!response.ok) {
-    const errMsg = data?.message || data?.error || data?.raw || `HTTP ${response.status}`;
-    throw new Error(`Kalshi API error (${response.status}): ${errMsg}`);
-  }
-
-  return data;
-}
-
-// ── Kalshi Unauthenticated Fetch (for public endpoints) ──
-async function kalshiPublicFetch(method, apiPath) {
-  const url = KALSHI_BASE_URL + apiPath;
-  const response = await fetch(url, {
-    method,
-    headers: { 'Accept': 'application/json' },
-  });
-  const text = await response.text();
-
-  let data;
-  try {
-    data = JSON.parse(text);
-  } catch {
-    data = { raw: text };
-  }
-
-  if (!response.ok) {
-    const errMsg = data?.message || data?.error || data?.raw || `HTTP ${response.status}`;
-    throw new Error(`Kalshi API error (${response.status}): ${errMsg}`);
-  }
-
-  return data;
-}
-
-// ── Build query string from params object ──
-function buildQueryString(params) {
-  if (!params || Object.keys(params).length === 0) return '';
-  const qs = Object.entries(params)
-    .filter(([, v]) => v !== undefined && v !== null && v !== '')
-    .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
-    .join('&');
-  return qs ? `?${qs}` : '';
-}
-
-// ── API Route Handlers ──
-const routes = {
-
-  // ════════════════════════════════════════
-  // AUTH ENDPOINTS (same DW pattern)
-  // ════════════════════════════════════════
-
-  'POST /api/auth/login': async (req, res) => {
-    const body = await readBody(req);
-    if (body.username === AUTH_USER && body.password === AUTH_PASS) {
-      const sessionData = { authenticated: true, username: AUTH_USER, loginTime: Date.now() };
-      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
-      json(res, { success: true, message: 'Login successful', username: AUTH_USER }, 200, {
-        'Set-Cookie': setSessionCookie(res, token),
-      });
-    } else {
-      json(res, { error: 'Invalid credentials' }, 401);
-    }
-  },
-
-  'POST /api/auth/logout': async (req, res) => {
-    json(res, { success: true, message: 'Logged out successfully' }, 200, {
-      'Set-Cookie': clearSessionCookie(),
-    });
-  },
-
-  'GET /api/auth/session': async (req, res) => {
-    if (isWhitelistedIP(req)) {
-      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'whitelisted_ip' });
-    }
-    if (hasBasicAuth(req)) {
-      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'basic_auth' });
-    }
-    const session = getSession(req);
-    if (session) {
-      return json(res, {
-        authenticated: true,
-        username: session.username,
-        email: session.email,
-        picture: session.picture,
-        provider: session.provider,
-        loginTime: session.loginTime,
-      });
-    }
-    json(res, { authenticated: false }, 401);
-  },
-
-  'POST /api/auth/google': async (req, res) => {
-    if (!googleClient) return json(res, { error: 'Google OAuth not configured' }, 400);
-    const body = await readBody(req);
-    if (!body.credential) return json(res, { error: 'Missing credential' }, 400);
-    try {
-      const ticket = await googleClient.verifyIdToken({ idToken: body.credential, audience: GOOGLE_CLIENT_ID });
-      const payload = ticket.getPayload();
-      const email = payload.email;
-      if (!payload.email_verified) return json(res, { error: 'Email not verified' }, 401);
-      if (ALLOWED_EMAILS.length > 0 && !ALLOWED_EMAILS.includes(email)) {
-        return json(res, { error: 'Email not authorized' }, 403);
-      }
-      const sessionData = {
-        authenticated: true,
-        username: payload.name || email,
-        email,
-        picture: payload.picture,
-        provider: 'google',
-        loginTime: Date.now(),
-      };
-      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
-      json(res, { success: true, username: payload.name || email, email, picture: payload.picture }, 200, {
-        'Set-Cookie': setSessionCookie(res, token),
-      });
-    } catch (err) {
-      json(res, { error: 'Invalid Google token', details: err.message }, 401);
-    }
-  },
-
-  'GET /api/auth/google-config': async (req, res) => {
-    json(res, { client_id: GOOGLE_CLIENT_ID || null, configured: !!GOOGLE_CLIENT_ID });
-  },
-
-  // ════════════════════════════════════════
-  // SETUP WIZARD (save/load progress)
-  // ════════════════════════════════════════
-
-  'GET /api/setup': async (req, res) => {
-    try {
-      const result = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const cfg = result.rows[0]?.config || {};
-      json(res, {
-        setup_complete: !!cfg.setup_complete,
-        setup_progress: cfg.setup_progress || null,
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  'POST /api/setup': async (req, res) => {
-    const body = await readBody(req);
-    try {
-      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const row = current.rows[0] || {};
-      const cfg = row.config || {};
-
-      if (body.action === 'save_progress') {
-        const newConfig = { ...cfg, setup_progress: body.progress };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
-        json(res, { success: true });
-      } else if (body.action === 'complete') {
-        const newConfig = { ...cfg, setup_complete: true, setup_progress: body.progress };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
-        json(res, { success: true, message: 'Setup complete!' });
-      } else {
-        json(res, { error: `Unknown setup action: ${body.action}` }, 400);
-      }
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // HEALTH
-  // ════════════════════════════════════════
-
-  'GET /api/health': async (req, res) => {
-    try {
-      const dbCheck = await q('SELECT 1');
-      const risk = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const cfg = risk.rows[0]?.config || {};
-
-      // Test Kalshi connection
-      let kalshiConnected = false;
-      let balance = null;
-      if (cfg.kalshi_api_key && cfg.kalshi_private_key) {
-        try {
-          const balData = await kalshiFetch('GET', '/portfolio/balance');
-          kalshiConnected = true;
-          balance = balData.balance ?? balData;
-        } catch {
-          // Keys exist but connection failed
-          kalshiConnected = false;
-        }
-      }
-
-      json(res, {
-        status: 'ok',
-        db_connected: !!dbCheck,
-        kalshi_connected: kalshiConnected,
-        env: KALSHI_ENV,
-        safe_mode: cfg.safe_mode ?? true,
-        balance,
-      });
-    } catch (err) {
-      json(res, { status: 'error', error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // KALSHI PROXY (all actions via POST)
-  // ════════════════════════════════════════
-
-  'POST /api/kalshi': async (req, res) => {
-    const body = await readBody(req);
-    const { action } = body;
-
-    if (!action) {
-      return json(res, { error: 'Missing action field' }, 400);
-    }
-
-    try {
-      switch (action) {
-
-        // ── Save API Keys ──
-        case 'save_keys': {
-          if (!body.api_key || !body.private_key) {
-            return json(res, { error: 'api_key and private_key (PEM) required' }, 400);
-          }
-          const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-          const row = current.rows[0] || {};
-          const newConfig = {
-            ...row.config,
-            kalshi_api_key: body.api_key,
-            kalshi_private_key: body.private_key,
-            kalshi_connected: true,
-            kalshi_connected_at: new Date().toISOString(),
-            kalshi_env: KALSHI_ENV,
-          };
-          await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-            JSON.stringify(newConfig), row.id,
-          ]);
-          json(res, {
-            success: true,
-            api_key: body.api_key.substring(0, 12) + '...',
-            env: KALSHI_ENV,
-            message: 'Kalshi API keys saved',
-          });
-          break;
-        }
-
-        // ── Test Connection ──
-        case 'test_connection': {
-          try {
-            const data = await kalshiPublicFetch('GET', '/exchange/status');
-            json(res, { success: true, exchange_status: data, env: KALSHI_ENV });
-          } catch (err) {
-            json(res, { success: false, error: err.message }, 500);
-          }
-          break;
-        }
-
-        // ── Get Balance ──
-        case 'get_balance': {
-          const data = await kalshiFetch('GET', '/portfolio/balance');
-          json(res, { success: true, balance: data });
-          break;
-        }
-
-        // ── Get Markets (public) ──
-        case 'get_markets': {
-          const params = {};
-          if (body.category) params.category = body.category;
-          if (body.status) params.status = body.status;
-          if (body.limit) params.limit = body.limit;
-          if (body.cursor) params.cursor = body.cursor;
-          if (body.series_ticker) params.series_ticker = body.series_ticker;
-          if (body.event_ticker) params.event_ticker = body.event_ticker;
-          const qs = buildQueryString(params);
-          const data = await kalshiPublicFetch('GET', `/markets${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Get Single Market ──
-        case 'get_market': {
-          if (!body.ticker) return json(res, { error: 'ticker required' }, 400);
-          const data = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(body.ticker)}`);
-          json(res, { success: true, market: data });
-          break;
-        }
-
-        // ── Get Orderbook ──
-        case 'get_orderbook': {
-          if (!body.ticker) return json(res, { error: 'ticker required' }, 400);
-          const qs = body.depth ? `?depth=${body.depth}` : '';
-          const data = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(body.ticker)}/orderbook${qs}`);
-          json(res, { success: true, orderbook: data });
-          break;
-        }
-
-        // ── Get Trades (public market trades) ──
-        case 'get_trades': {
-          if (!body.ticker) return json(res, { error: 'ticker required' }, 400);
-          const params = {};
-          if (body.limit) params.limit = body.limit;
-          if (body.cursor) params.cursor = body.cursor;
-          const qs = buildQueryString(params);
-          const data = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(body.ticker)}/trades${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Get Events ──
-        case 'get_events': {
-          // Accept flat fields or nested in body.params (BrowseMarkets sends params wrapper)
-          const src = body.params || body;
-          const params = {};
-          if (src.status) params.status = src.status;
-          if (src.limit) params.limit = src.limit;
-          if (src.cursor) params.cursor = src.cursor;
-          if (src.series_ticker) params.series_ticker = src.series_ticker;
-          if (src.with_nested_markets !== undefined) params.with_nested_markets = src.with_nested_markets;
-          const qs = buildQueryString(params);
-          const data = await kalshiPublicFetch('GET', `/events${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Get Single Event ──
-        case 'get_event': {
-          if (!body.event_ticker) return json(res, { error: 'event_ticker required' }, 400);
-          const qs = body.with_nested_markets ? '?with_nested_markets=true' : '';
-          const data = await kalshiPublicFetch('GET', `/events/${encodeURIComponent(body.event_ticker)}${qs}`);
-          json(res, { success: true, event: data });
-          break;
-        }
-
-        // ── Get Series ──
-        case 'get_series': {
-          if (!body.series_ticker) return json(res, { error: 'series_ticker required' }, 400);
-          const data = await kalshiPublicFetch('GET', `/series/${encodeURIComponent(body.series_ticker)}`);
-          json(res, { success: true, series: data });
-          break;
-        }
-
-        // ── Get Positions (from portfolio trades DB, or real Kalshi API if keys configured) ──
-        case 'get_positions': {
-          {
-            // No API keys — return portfolio trades from DB
-            const { rows: posRows } = await kenQ(`
-              SELECT pt.market_id as ticker, pt.market_title, pt.direction,
-                COUNT(*) FILTER (WHERE pt.status = 'open') as open_count,
-                SUM(pt.contracts) FILTER (WHERE pt.status = 'open') as open_contracts,
-                SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') as open_cost,
-                ROUND(AVG(pt.entry_price_cents) FILTER (WHERE pt.status = 'open')) as avg_price,
-                COUNT(*) as total_trades,
-                SUM(COALESCE(pt.pnl_cents, 0)) FILTER (WHERE pt.status != 'open') as realized_pnl
-              FROM ken_portfolio_trades pt
-              WHERE pt.created_at > NOW() - INTERVAL '24 hours'
-              GROUP BY pt.market_id, pt.market_title, pt.direction
-              HAVING COUNT(*) FILTER (WHERE pt.status = 'open') > 0
-              ORDER BY SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') DESC NULLS LAST
-            `);
-            const positions = posRows.map(r => ({
-              ticker: r.ticker,
-              market_ticker: r.ticker,
-              title: r.market_title || r.ticker,
-              market_exposure: r.direction === 'BUY_YES' ? parseInt(r.open_contracts || 0) : -parseInt(r.open_contracts || 0),
-              position: r.direction === 'BUY_YES' ? parseInt(r.open_contracts || 0) : -parseInt(r.open_contracts || 0),
-              position_cost: parseInt(r.open_cost || 0) * 100,
-              realized_pnl: parseInt(r.realized_pnl || 0) * 100,
-              fees_paid: 0,
-              avg_price_cents: parseInt(r.avg_price || 0),
-              open_trades: parseInt(r.open_count || 0),
-              total_trades: parseInt(r.total_trades || 0),
-            }));
-            json(res, { success: true, positions, market_positions: positions, is_paper: true });
-          }
-          break;
-        }
-
-        // ── Get Fills (auth required) ──
-        case 'get_fills': {
-          const params = {};
-          if (body.limit) params.limit = body.limit;
-          if (body.cursor) params.cursor = body.cursor;
-          if (body.ticker) params.ticker = body.ticker;
-          if (body.order_id) params.order_id = body.order_id;
-          const qs = buildQueryString(params);
-          const data = await kalshiFetch('GET', `/portfolio/fills${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Get Orders (auth required) ──
-        case 'get_orders': {
-          const params = {};
-          if (body.limit) params.limit = body.limit;
-          if (body.cursor) params.cursor = body.cursor;
-          if (body.ticker) params.ticker = body.ticker;
-          if (body.status) params.status = body.status;
-          const qs = buildQueryString(params);
-          const data = await kalshiFetch('GET', `/portfolio/orders${qs}`);
-          json(res, { success: true, ...data });
-          break;
-        }
-
-        // ── Create Order (auth required) ──
-        case 'create_order': {
-          if (!body.ticker || !body.side || !body.action || !body.type || !body.count) {
-            return json(res, { error: 'ticker, side, action, type, and count are required' }, 400);
-          }
-          const orderPayload = {
-            ticker: body.ticker,
-            side: body.side,       // 'yes' or 'no'
-            action: body.action,   // 'buy' or 'sell'
-            type: body.type,       // 'limit' or 'market'
-            count: body.count,
-          };
-          if (body.yes_price !== undefined) orderPayload.yes_price = body.yes_price;
-          if (body.no_price !== undefined) orderPayload.no_price = body.no_price;
-          if (body.client_order_id) orderPayload.client_order_id = body.client_order_id;
-          if (body.time_in_force) orderPayload.time_in_force = body.time_in_force;
-          if (body.expiration_ts) orderPayload.expiration_ts = body.expiration_ts;
-
-          const data = await kalshiFetch('POST', '/portfolio/orders', orderPayload);
-          json(res, { success: true, order: data });
-          break;
-        }
-
-        // ── Cancel Order (auth required) ──
-        case 'cancel_order': {
-          if (!body.order_id) return json(res, { error: 'order_id required' }, 400);
-          const data = await kalshiFetch('DELETE', `/portfolio/orders/${encodeURIComponent(body.order_id)}`);
-          json(res, { success: true, result: data });
-          break;
-        }
-
-        // ── Get Single Order (auth required) ──
-        case 'get_order': {
-          if (!body.order_id) return json(res, { error: 'order_id required' }, 400);
-          const data = await kalshiFetch('GET', `/portfolio/orders/${encodeURIComponent(body.order_id)}`);
-          json(res, { success: true, order: data });
-          break;
-        }
-
-        // ── Auto-Trader Status ──
-        case 'trader_status': {
-          const state = await getTraderState();
-          const allTrades = await kenQ('SELECT * FROM ken_trades ORDER BY created_at DESC LIMIT 50');
-          const closedTrades = (allTrades.rows || []).filter(t => t.status !== 'open');
-          const totalPnL = closedTrades.reduce((sum, t) => sum + (t.pnl_cents || 0), 0);
-          const wins = closedTrades.filter(t => (t.pnl_cents || 0) > 0).length;
-          const losses = closedTrades.filter(t => (t.pnl_cents || 0) < 0).length;
-          json(res, {
-            success: true,
-            config: TRADE_CONFIG,
-            state: {
-              open_positions: state.openCount,
-              total_invested: state.totalInvested,
-              available_budget: state.available,
-              daily_loss: state.dailyLoss,
-            },
-            positions: state.openPositions,
-            stats: {
-              total_trades: closedTrades.length,
-              wins, losses,
-              win_rate: closedTrades.length > 0 ? Math.round(wins / closedTrades.length * 100) : 0,
-              total_pnl_cents: totalPnL,
-              total_pnl_dollars: (totalPnL / 100).toFixed(2),
-            },
-            recent_trades: (allTrades.rows || []).slice(0, 20),
-          });
-          break;
-        }
-
-        // ── Toggle Auto-Trader ──
-        case 'trader_toggle': {
-          TRADE_CONFIG.enabled = !TRADE_CONFIG.enabled;
-          saveTradeConfig();
-          json(res, { success: true, enabled: TRADE_CONFIG.enabled, message: `Auto-trader ${TRADE_CONFIG.enabled ? 'ENABLED' : 'DISABLED'}` });
-          console.log(`[Ken] Auto-trader ${TRADE_CONFIG.enabled ? 'ENABLED' : 'DISABLED'} via API`);
-          break;
-        }
-
-        // ── Run Predictions (weather model pipeline) ──
-        case 'run_predictions': {
-          // Step 1: Get weather events from Kalshi
-          let events = [];
-          try {
-            const evData = await kalshiPublicFetch('GET', '/events?status=open&limit=100');
-            events = (evData.events || []).filter(e => {
-              const t = ((e.category || '') + ' ' + (e.title || '')).toLowerCase();
-              return t.includes('weather') || t.includes('temperature') || t.includes('temp ') ||
-                     t.includes('rain') || t.includes('snow') || t.includes('precipitation') ||
-                     t.includes('hurricane') || t.includes('wind') || t.includes('°f');
-            });
-          } catch { /* no events available - use weather cache */ }
-
-          // Step 2: Load cached weather data
-          const riskRow = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-          const rCfg = riskRow.rows[0]?.config || {};
-          const weatherCache = rCfg.weather_cache?.results || [];
-          const month = new Date().getMonth() + 1;
-
-          // Step 3: Generate predictions (only for real weather events with a city match)
-          const predictions = [];
-          for (const evt of events) {
-            const city = extractCity(evt.title);
-            if (!city) continue; // Skip non-geographic events (climate policy, earthquakes, etc.)
-            const spec = parseWeatherEvent(evt.title);
-            if (!spec || spec.variable === 'unknown') continue; // Skip if we can't parse a weather variable
-            const cityWeather = weatherCache.find(w => w.city === city);
-            if (!cityWeather) continue; // Skip if no weather data for this city
-            const features = cityWeather[spec.variable === 'snow' ? 'precipitation' : spec.variable] || cityWeather.temperature;
-
-            if (features && features.values?.length) {
-              const rawProb = ensembleExceedance(features, spec.threshold, spec.operator);
-              const prior = climatologicalPrior(spec.variable, spec.threshold, month);
-              const combined = bayesianUpdate(prior, rawProb ?? 0.5);
-              const pYes = Math.max(0.01, Math.min(0.99, combined));
-
-              // Get market price for edge calc
-              let marketPrice = 50;
-              try {
-                const mkts = await kalshiPublicFetch('GET', `/events/${encodeURIComponent(evt.event_ticker)}?with_nested_markets=true`);
-                const mkt = mkts.event?.markets?.[0] || mkts.markets?.[0];
-                if (mkt) {
-                  const p = mkt.yes_ask || mkt.last_price || 50;
-                  if (p > 0 && p < 100) marketPrice = p; // Skip fully priced markets
-                }
-              } catch {}
-
-              const edgeBps = Math.round((pYes * 100 - marketPrice) * 100);
-              predictions.push({
-                event_ticker: evt.event_ticker,
-                ticker: evt.title?.substring(0, 60),
-                market: evt.title,
-                city,
-                side: edgeBps > 0 ? 'yes' : 'no',
-                p_yes: Math.round(pYes * 1000) / 10,
-                price_cents: marketPrice,
-                edge_bps: edgeBps,
-                method: 'ensemble_bayesian_v1',
-                variable: spec.variable,
-                threshold: spec.threshold,
-                data_points: features.values.length,
-              });
-            }
-          }
-
-          // If no Kalshi events but we have weather data, generate synthetic predictions
-          if (predictions.length === 0 && weatherCache.length > 0) {
-            for (const wd of weatherCache.filter(w => !w.error)) {
-              if (wd.temperature?.values?.length) {
-                const maxT = wd.temperature.max;
-                const threshold = Math.round(maxT / 5) * 5;
-                const rawProb = ensembleExceedance(wd.temperature, threshold, '>=');
-                const prior = climatologicalPrior('temperature', threshold, month);
-                const combined = bayesianUpdate(prior, rawProb ?? 0.5);
-                const pYes = Math.max(0.01, Math.min(0.99, combined));
-                predictions.push({
-                  ticker: `${wd.city} temp >= ${threshold}°F`,
-                  market: `Will the high temperature in ${wd.city} reach ${threshold}°F?`,
-                  city: wd.city,
-                  side: pYes > 0.5 ? 'yes' : 'no',
-                  p_yes: Math.round(pYes * 1000) / 10,
-                  price_cents: 50,
-                  edge_bps: Math.round((pYes * 100 - 50) * 100),
-                  method: 'synthetic_forecast',
-                  variable: 'temperature',
-                  threshold,
-                  data_points: wd.temperature.values.length,
-                });
-              }
-            }
-          }
-
-          predictions.sort((a, b) => Math.abs(b.edge_bps) - Math.abs(a.edge_bps));
-          const buySignals = predictions.filter(p => p.edge_bps > 0);
-          const avgEdge = predictions.length ? Math.round(predictions.reduce((s, p) => s + p.edge_bps, 0) / predictions.length) : 0;
-
-          json(res, {
-            success: true,
-            predictions,
-            buy_signals: buySignals.length,
-            avg_edge_bps: avgEdge,
-            total_events_scanned: events.length,
-            weather_cities: weatherCache.filter(w => !w.error).length,
-          });
-          break;
-        }
-
-        // ── Disconnect: remove keys from config ──
-        case 'disconnect': {
-          const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-          const row = current.rows[0] || {};
-          const newConfig = {
-            ...row.config,
-            kalshi_api_key: null,
-            kalshi_private_key: null,
-            kalshi_connected: false,
-            kalshi_connected_at: null,
-          };
-          await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-            JSON.stringify(newConfig), row.id,
-          ]);
-          json(res, { success: true, message: 'Kalshi keys removed, disconnected' });
-          break;
-        }
-
-        // ── Status: show current connection config (masked) ──
-        case 'status': {
-          const current = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-          const cfg = current.rows[0]?.config || {};
-          json(res, {
-            connected: !!cfg.kalshi_connected,
-            api_key: cfg.kalshi_api_key ? cfg.kalshi_api_key.substring(0, 12) + '...' : null,
-            has_private_key: !!cfg.kalshi_private_key,
-            connected_at: cfg.kalshi_connected_at || null,
-            env: KALSHI_ENV,
-          });
-          break;
-        }
-
-        default:
-          json(res, {
-            error: `Unknown action: ${action}`,
-            available_actions: [
-              'save_keys', 'test_connection', 'get_balance',
-              'get_markets', 'get_market', 'get_orderbook', 'get_trades',
-              'get_events', 'get_event', 'get_series',
-              'get_positions', 'get_fills', 'get_orders',
-              'create_order', 'cancel_order', 'get_order',
-              'run_predictions', 'disconnect', 'status',
-            ],
-          }, 400);
-      }
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // WEATHER ENDPOINTS
-  // ════════════════════════════════════════
-
-  'POST /api/weather': async (req, res) => {
-    const body = await readBody(req);
-
-    if (body.action === 'fetch_all') {
-      try {
-        const cities = body.cities || ['New York', 'Chicago', 'Miami', 'Denver', 'Los Angeles', 'Seattle'];
-        const useNws = body.force_nws === true; // opt-in to real NWS (flaky from this server)
-
-        // ── Generate synthetic seasonal forecast data (fast, reliable) ──
-        function generateSyntheticCities(cityList) {
-          const month = new Date().getMonth();
-          const results = [];
-          let totalDP = 0;
-          for (const city of cityList) {
-            const coords = CITY_COORDS[city]; if (!coords) continue;
-            const baseTemp = [35,38,48,58,68,78,85,83,75,62,48,38][month] + (coords.lat < 35 ? 15 : coords.lat > 42 ? -8 : 0);
-            const temps = Array.from({length: 14}, (_, i) => baseTemp + Math.round((Math.random() - 0.5) * 12) + (i < 7 ? 3 : -2));
-            const precips = Array.from({length: 14}, () => Math.round(Math.random() * 60));
-            const winds = Array.from({length: 14}, () => 5 + Math.round(Math.random() * 15));
-            const mkStat = (arr) => ({ mean: arr.reduce((a,b)=>a+b)/arr.length, min: Math.min(...arr), max: Math.max(...arr), latest: arr[0] });
-            results.push({
-              city, synthetic: true,
-              temperature: { variable: 'temperature', unit: 'F', values: temps, ...mkStat(temps) },
-              precipitation: { variable: 'precipitation', unit: 'percent', values: precips, ...mkStat(precips) },
-              wind: { variable: 'wind', unit: 'mph', values: winds, ...mkStat(winds) },
-              data_points: 42,
-            });
-            totalDP += 42;
-          }
-          return { results, totalDP };
-        }
-
-        let responseData;
-
-        if (useNws) {
-          // Real NWS path — wrapped in 15s global timeout, falls back to synthetic
-          const nwsResult = await Promise.race([
-            (async () => {
-              const results = [];
-              let dp = 0;
-              const mets = new Set();
-              for (const city of cities) {
-                const coords = CITY_COORDS[city]; if (!coords) continue;
-                try {
-                  const grid = await nwsFetch(`${NWS_BASE}/points/${coords.lat},${coords.lon}`);
-                  const forecast = await nwsFetch(`${NWS_BASE}/gridpoints/${grid.properties.gridId}/${grid.properties.gridX},${grid.properties.gridY}/forecast`);
-                  const tempF = extractForecastFeatures(forecast, 'temperature');
-                  const precip = extractForecastFeatures(forecast, 'precipitation');
-                  const wind = extractForecastFeatures(forecast, 'wind');
-                  const cdp = (tempF?.values?.length||0) + (precip?.values?.length||0) + (wind?.values?.length||0);
-                  dp += cdp;
-                  if (tempF) mets.add('temperature'); if (precip) mets.add('precipitation'); if (wind) mets.add('wind');
-                  results.push({ city, temperature: tempF, precipitation: precip, wind, data_points: cdp });
-                } catch (e) { results.push({ city, error: e.message }); }
-              }
-              return { results, totalDP: dp, metrics: [...mets], synthetic: false };
-            })(),
-            new Promise(resolve => setTimeout(() => resolve(null), 15000)),
-          ]);
-
-          if (nwsResult && nwsResult.results.some(r => !r.error)) {
-            responseData = nwsResult;
-          } else {
-            const syn = generateSyntheticCities(cities);
-            responseData = { ...syn, metrics: ['temperature','precipitation','wind'], synthetic: true, note: 'NWS timed out — using synthetic' };
-          }
-        } else {
-          // Default: synthetic data (instant, always works)
-          const syn = generateSyntheticCities(cities);
-          responseData = { ...syn, metrics: ['temperature','precipitation','wind'], synthetic: true, note: 'Using seasonal synthetic forecasts (pass force_nws:true for real NWS data)' };
-        }
-
-        // Cache in DB
-        const row = (await q('SELECT id, config FROM risk_state ORDER BY updated_at DESC LIMIT 1')).rows[0];
-        if (row) {
-          const newCfg = { ...(row.config || {}), weather_cache: { results: responseData.results, fetched_at: new Date().toISOString(), synthetic: responseData.synthetic } };
-          await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newCfg), row.id]);
-        }
-
-        json(res, {
-          success: true,
-          synthetic: responseData.synthetic,
-          stations: responseData.results.length,
-          data_points: responseData.totalDP,
-          metrics: responseData.metrics,
-          cities: responseData.results,
-          fetched_at: new Date().toISOString(),
-          note: responseData.note || undefined,
-        });
-      } catch (err) {
-        json(res, { success: false, error: err.message }, 500);
-      }
-    } else {
-      json(res, { error: `Unknown weather action: ${body.action}` }, 400);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // RISK ENDPOINTS
-  // ════════════════════════════════════════
-
-  'POST /api/risk': async (req, res) => {
-    const body = await readBody(req);
-
-    try {
-      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const row = current.rows[0] || {};
-      const cfg = row.config || {};
-
-      if (body.action === 'get_config') {
-        json(res, {
-          success: true,
-          config: {
-            safe_mode: cfg.safe_mode ?? true,
-            risk_profile: cfg.risk_profile ?? 'conservative',
-            max_position_size: cfg.max_position_size ?? 100,
-            max_contracts_per_market: cfg.max_contracts_per_market ?? 25,
-            max_dollars_at_risk: cfg.max_dollars_at_risk ?? 250,
-            max_daily_loss: cfg.max_daily_loss ?? cfg.daily_loss_cap ?? 50,
-            daily_loss_cap: cfg.daily_loss_cap ?? 50,
-            max_open_orders: cfg.max_open_orders ?? 10,
-            max_exposure: cfg.max_exposure ?? 500,
-            stop_loss_pct: cfg.stop_loss_pct ?? 20,
-            auto_hedge: cfg.auto_hedge ?? false,
-            allowed_categories: cfg.allowed_categories ?? [],
-            kill_switch_active: row.kill_switch_active || false,
-            kill_reason: row.kill_reason || null,
-            bankroll: parseFloat(row.bankroll || 0),
-            daily_pnl: parseFloat(row.daily_pnl || 0),
-            open_exposure: parseFloat(row.open_exposure || 0),
-          },
-        });
-      } else if (body.action === 'update_config') {
-        // Accept flat fields (from RiskProfile step) or wrapped in config object
-        const updates = body.config || {};
-        if (body.max_contracts_per_market !== undefined) updates.max_contracts_per_market = body.max_contracts_per_market;
-        if (body.max_dollars_at_risk !== undefined) updates.max_dollars_at_risk = body.max_dollars_at_risk;
-        if (body.daily_loss_cap !== undefined) updates.daily_loss_cap = body.daily_loss_cap;
-        if (body.risk_profile !== undefined) updates.risk_profile = body.risk_profile;
-        if (body.safe_mode !== undefined) updates.safe_mode = body.safe_mode;
-        const newConfig = { ...cfg, ...updates };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-          JSON.stringify(newConfig), row.id,
-        ]);
-
-        // Also update top-level risk fields if provided
-        if (body.config?.kill_switch_active !== undefined) {
-          await q('UPDATE risk_state SET kill_switch_active = $1, kill_reason = $2, updated_at = NOW() WHERE id = $3', [
-            body.config.kill_switch_active,
-            body.config.kill_reason || null,
-            row.id,
-          ]);
-        }
-
-        const updated = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-        const u = updated.rows[0] || {};
-        json(res, {
-          success: true,
-          message: 'Risk config updated',
-          config: u.config || {},
-        });
-      } else {
-        json(res, { error: `Unknown risk action: ${body.action}` }, 400);
-      }
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // TRADING MODE CONTROL
-  // ════════════════════════════════════════
-
-  'POST /api/trading/mode': async (req, res) => {
-    const body = await readBody(req);
-    try {
-      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const row = current.rows[0] || {};
-      const cfg = row.config || {};
-
-      if (body.action === 'get') {
-        // Get current trading mode
-        const traderState = await getTraderState().catch(() => ({ openPositions: [], openCount: 0, totalInvested: 0, dailyLoss: 0, available: 0 }));
-        return json(res, {
-          env: KALSHI_ENV,
-          safe_mode: cfg.safe_mode ?? true,
-          kalshi_connected: !!(cfg.kalshi_api_key && cfg.kalshi_private_key),
-          trade_config: TRADE_CONFIG,
-          trader_state: traderState,
-          auto_trader_enabled: TRADE_CONFIG.enabled,
-        });
-      }
-
-      if (body.action === 'switch_env') {
-        const newEnv = body.env === 'prod' ? 'prod' : 'demo';
-        const newConfig = { ...cfg, kalshi_env: newEnv };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
-        KALSHI_ENV = newEnv;
-        KALSHI_BASE_URL = KALSHI_BASE_URLS[newEnv];
-        console.log(`[Ken] Environment switched to ${newEnv.toUpperCase()} (${KALSHI_BASE_URL})`);
-        return json(res, { success: true, env: newEnv, base_url: KALSHI_BASE_URL });
-      }
-
-      if (body.action === 'toggle_safe_mode') {
-        const newSafe = !(cfg.safe_mode ?? true);
-        const newConfig = { ...cfg, safe_mode: newSafe };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
-        console.log(`[Ken] Safe mode ${newSafe ? 'ENABLED' : 'DISABLED'}`);
-        return json(res, { success: true, safe_mode: newSafe });
-      }
-
-      if (body.action === 'toggle_auto_trader') {
-        TRADE_CONFIG.enabled = !TRADE_CONFIG.enabled;
-        saveTradeConfig();
-        console.log(`[Ken] Auto-trader ${TRADE_CONFIG.enabled ? 'ENABLED' : 'DISABLED'}`);
-        return json(res, { success: true, auto_trader_enabled: TRADE_CONFIG.enabled });
-      }
-
-      if (body.action === 'update_trade_config') {
-        if (body.budget_cents !== undefined) TRADE_CONFIG.budget_cents = Math.max(100, Math.min(100000, body.budget_cents));
-        if (body.max_position_cents !== undefined) TRADE_CONFIG.max_position_cents = Math.max(50, Math.min(10000, body.max_position_cents));
-        if (body.max_open_positions !== undefined) TRADE_CONFIG.max_open_positions = Math.max(1, Math.min(20, body.max_open_positions));
-        if (body.min_confidence !== undefined) TRADE_CONFIG.min_confidence = Math.max(50, Math.min(99, body.min_confidence));
-        if (body.daily_loss_limit_cents !== undefined) TRADE_CONFIG.daily_loss_limit_cents = Math.max(100, Math.min(50000, body.daily_loss_limit_cents));
-        saveTradeConfig();
-        return json(res, { success: true, trade_config: TRADE_CONFIG });
-      }
-
-      json(res, { error: `Unknown trading mode action: ${body.action}` }, 400);
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // TRADING HISTORY & P&L
-  // ════════════════════════════════════════
-
-  'GET /api/trading/history': async (req, res) => {
-    try {
-      const { rows: trades } = await kenQ('SELECT * FROM ken_trades ORDER BY created_at DESC LIMIT 100');
-      const { rows: [summary] } = await kenQ(`
-        SELECT
-          COUNT(*) as total_trades,
-          COUNT(*) FILTER (WHERE status = 'open') as open_trades,
-          COUNT(*) FILTER (WHERE pnl_cents > 0) as winning_trades,
-          COUNT(*) FILTER (WHERE pnl_cents < 0) as losing_trades,
-          COALESCE(SUM(pnl_cents), 0) as total_pnl,
-          COALESCE(SUM(cost_cents) FILTER (WHERE status = 'open'), 0) as open_exposure
-        FROM ken_trades
-      `);
-      json(res, { trades, summary });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // PAPER TRADING
-  // ════════════════════════════════════════
-
-  'POST /api/paper': async (req, res) => {
-    const body = await readBody(req);
-
-    try {
-      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-      const row = current.rows[0] || {};
-      const cfg = row.config || {};
-      const paperTrades = cfg.paper_trades || [];
-
-      if (body.action === 'submit_order') {
-        if (!body.ticker || !body.side || !body.count) {
-          return json(res, { error: 'ticker, side, and count required' }, 400);
-        }
-
-        const orderId = `paper_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
-        const fillPrice = body.yes_price || body.no_price || body.price || 50; // cents
-
-        const paperOrder = {
-          order_id: orderId,
-          ticker: body.ticker,
-          side: body.side,           // 'yes' or 'no'
-          action: body.action_type || 'buy',
-          type: body.type || 'market',
-          count: body.count,
-          price: fillPrice,
-          cost: fillPrice * body.count, // in cents
-          status: 'filled',
-          created_at: new Date().toISOString(),
-          filled_at: new Date().toISOString(),
-          is_paper: true,
-        };
-
-        paperTrades.push(paperOrder);
-
-        const newConfig = { ...cfg, paper_trades: paperTrades };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-          JSON.stringify(newConfig), row.id,
-        ]);
-
-        json(res, { success: true, order: paperOrder, message: 'Paper order filled' });
-
-      } else if (body.action === 'get_positions') {
-        // Aggregate from ken_portfolio_trades (live hyper-trading data)
-        try {
-          const { rows } = await kenQ(`
-            SELECT pt.market_id as ticker, pt.market_title, pt.direction,
-              SUM(pt.contracts) as total_contracts,
-              SUM(pt.cost_cents) as position_cost,
-              ROUND(AVG(pt.entry_price_cents)) as avg_price,
-              COUNT(*) as trades,
-              SUM(CASE WHEN pt.status != 'open' THEN pt.pnl_cents ELSE 0 END) as realized_pnl
-            FROM ken_portfolio_trades pt
-            WHERE pt.created_at > NOW() - INTERVAL '24 hours'
-            GROUP BY pt.market_id, pt.market_title, pt.direction
-            ORDER BY SUM(pt.cost_cents) DESC
-          `);
-          const positions = rows.map(r => ({
-            ticker: r.ticker,
-            market_ticker: r.ticker,
-            title: r.market_title,
-            side: r.direction === 'BUY_YES' ? 'yes' : 'no',
-            market_exposure: r.direction === 'BUY_YES' ? parseInt(r.total_contracts) : -parseInt(r.total_contracts),
-            position: r.direction === 'BUY_YES' ? parseInt(r.total_contracts) : -parseInt(r.total_contracts),
-            position_cost: parseInt(r.position_cost) * 100,
-            avg_price_cents: parseInt(r.avg_price),
-            realized_pnl: parseInt(r.realized_pnl || 0) * 100,
-            fees_paid: 0,
-            trades: parseInt(r.trades),
-          }));
-          json(res, { success: true, positions, is_paper: true });
-        } catch (e) {
-          // Fallback to old paper trades
-          const positionMap = {};
-          for (const trade of paperTrades) {
-            const key = `${trade.ticker}_${trade.side}`;
-            if (!positionMap[key]) positionMap[key] = { ticker: trade.ticker, side: trade.side, total_contracts: 0, avg_price: 0, total_cost: 0, trades: 0 };
-            const pos = positionMap[key];
-            if (trade.action === 'buy') { pos.total_cost += trade.price * trade.count; pos.total_contracts += trade.count; }
-            else { pos.total_cost -= trade.price * trade.count; pos.total_contracts -= trade.count; }
-            pos.trades++;
-            pos.avg_price = pos.total_contracts > 0 ? Math.round(pos.total_cost / pos.total_contracts) : 0;
-          }
-          const positions = Object.values(positionMap).filter(p => p.total_contracts !== 0);
-          json(res, { success: true, positions, is_paper: true });
-        }
-
-      } else if (body.action === 'get_fills') {
-        // Return paper trade history
-        const limit = body.limit || 50;
-        const fills = paperTrades.slice(-limit).reverse();
-        json(res, { success: true, fills, total: paperTrades.length, is_paper: true });
-
-      } else if (body.action === 'clear') {
-        // Clear all paper trades
-        const newConfig = { ...cfg, paper_trades: [] };
-        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
-          JSON.stringify(newConfig), row.id,
-        ]);
-        json(res, { success: true, message: 'Paper trades cleared' });
-
-      } else {
-        json(res, { error: `Unknown paper action: ${body.action}` }, 400);
-      }
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // ── Positions Detail API (collapsible tables) ──
-  'GET /api/positions': async (req, res) => {
-    try {
-      // All positions with full stats
-      const { rows: positions } = await kenQ(`
-        SELECT
-          pt.market_id AS ticker,
-          pt.direction,
-          pt.market_title AS title,
-          COUNT(*) FILTER (WHERE pt.status = 'open') AS open_trades,
-          SUM(pt.contracts) FILTER (WHERE pt.status = 'open') AS open_contracts,
-          SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') AS open_cost_cents,
-          ROUND(AVG(pt.entry_price_cents) FILTER (WHERE pt.status = 'open')) AS avg_entry_price,
-          SUM(COALESCE(pt.pnl_cents, 0)) FILTER (WHERE pt.status != 'open') AS realized_pnl_cents,
-          COUNT(*) AS total_trades,
-          COUNT(*) FILTER (WHERE pt.status != 'open') AS closed_trades,
-          MIN(pt.created_at) AS first_trade_at,
-          MAX(pt.created_at) AS last_trade_at,
-          MIN(pt.entry_price_cents) FILTER (WHERE pt.status = 'open') AS min_entry,
-          MAX(pt.entry_price_cents) FILTER (WHERE pt.status = 'open') AS max_entry
-        FROM ken_portfolio_trades pt
-        GROUP BY pt.market_id, pt.direction, pt.market_title
-        HAVING COUNT(*) FILTER (WHERE pt.status = 'open') > 0
-        ORDER BY SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') DESC NULLS LAST
-      `);
-
-      // Category grouping by market event prefix
-      const grouped = {};
-      for (const p of positions) {
-        const prefix = (p.ticker || '').split('-')[0];
-        if (!grouped[prefix]) grouped[prefix] = { event: prefix, title: p.title, positions: [], totalCost: 0, totalPnl: 0, totalContracts: 0 };
-        grouped[prefix].positions.push(p);
-        grouped[prefix].totalCost += parseInt(p.open_cost_cents || 0);
-        grouped[prefix].totalPnl += parseInt(p.realized_pnl_cents || 0);
-        grouped[prefix].totalContracts += parseInt(p.open_contracts || 0);
-      }
-
-      // Summary stats
-      const totalCost = positions.reduce((s, p) => s + parseInt(p.open_cost_cents || 0), 0);
-      const totalPnl = positions.reduce((s, p) => s + parseInt(p.realized_pnl_cents || 0), 0);
-      const totalContracts = positions.reduce((s, p) => s + parseInt(p.open_contracts || 0), 0);
-      const totalTrades = positions.reduce((s, p) => s + parseInt(p.total_trades || 0), 0);
-
-      json(res, {
-        success: true,
-        positions: positions.map(p => ({
-          ...p,
-          open_trades: parseInt(p.open_trades || 0),
-          open_contracts: parseInt(p.open_contracts || 0),
-          open_cost_cents: parseInt(p.open_cost_cents || 0),
-          avg_entry_price: parseInt(p.avg_entry_price || 0),
-          realized_pnl_cents: parseInt(p.realized_pnl_cents || 0),
-          total_trades: parseInt(p.total_trades || 0),
-          closed_trades: parseInt(p.closed_trades || 0),
-          min_entry: parseInt(p.min_entry || 0),
-          max_entry: parseInt(p.max_entry || 0),
-        })),
-        grouped: Object.values(grouped).sort((a, b) => b.totalCost - a.totalCost),
-        summary: { totalCost, totalPnl, totalContracts, totalTrades, positionCount: positions.length },
-      });
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // Get trade history for a specific position
-  'POST /api/positions/trades': async (req, res) => {
-    try {
-      const body = await readBody(req);
-      const { ticker, direction, limit: lim } = body;
-      if (!ticker) return json(res, { error: 'ticker required' }, 400);
-
-      let sql = `SELECT * FROM ken_portfolio_trades WHERE market_id = $1`;
-      const params = [ticker];
-      if (direction) { sql += ` AND direction = $2`; params.push(direction); }
-      sql += ` ORDER BY created_at DESC LIMIT ${Math.min(parseInt(lim) || 100, 500)}`;
-
-      const { rows } = await kenQ(sql, params);
-      json(res, { success: true, trades: rows });
-    } catch (err) {
-      json(res, { success: false, error: err.message }, 500);
-    }
-  },
-
-  // ── Signal Pipeline API ──
-  'GET /api/signals': async (req, res) => {
-    try {
-      const { rows: signals } = await kenQ(`
-        SELECT id, market_id, direction, expected_edge, size_recommendation, confidence, reasoning, sources, risk_approved, executed, created_at
-        FROM ken_strategy_signals
-        ORDER BY created_at DESC
-        LIMIT 50
-      `);
-      const { rows: shifts } = await kenQ(`
-        SELECT market_id, prior_probability, sentiment_impact, adjusted_probability, market_probability, edge, num_articles, created_at
-        FROM ken_probability_shifts
-        ORDER BY created_at DESC
-        LIMIT 50
-      `);
-      const { rows: recentSentiment } = await kenQ(`
-        SELECT event_tag, market_ticker, AVG(sentiment) as avg_sentiment, AVG(confidence) as avg_confidence, COUNT(*) as count
-        FROM ken_event_sentiment
-        WHERE created_at > NOW() - INTERVAL '1 hour'
-        GROUP BY event_tag, market_ticker
-        ORDER BY count DESC
-        LIMIT 30
-      `);
-      // Enrich with Kalshi URLs
-      const enrichWithUrl = (rows) => rows.map(r => {
-        const ticker = r.market_id || '';
-        const eventTicker = ticker.replace(/-[^-]+$/, '') || ticker;
-        const seriesTicker = eventTicker.replace(/-[^-]+$/, '') || eventTicker;
-        return { ...r, event_ticker: eventTicker, kalshi_url: `https://kalshi.com/markets/${seriesTicker.toLowerCase()}/${eventTicker.toLowerCase()}` };
-      });
-      json(res, {
-        signals: enrichWithUrl(signals),
-        probabilityShifts: enrichWithUrl(shifts),
-        eventSentiment: recentSentiment,
-        pipeline: {
-          scanInterval: '15 min',
-          signalInterval: '5 min',
-          intelligenceInterval: '30 min',
-          cachedMarkets: cachedActiveMarkets.length,
-          riskLimits: RISK_LIMITS,
-        },
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  'GET /api/signals/dashboard': async (req, res) => {
-    try {
-      // Summary stats for the signal pipeline
-      const { rows: [stats] } = await kenQ(`
-        SELECT
-          COUNT(*) as total_signals,
-          COUNT(CASE WHEN risk_approved THEN 1 END) as approved_signals,
-          COUNT(CASE WHEN direction = 'BUY_YES' THEN 1 END) as buy_yes,
-          COUNT(CASE WHEN direction = 'BUY_NO' THEN 1 END) as buy_no,
-          AVG(expected_edge) as avg_edge,
-          AVG(confidence) as avg_confidence,
-          MAX(created_at) as latest_signal
-        FROM ken_strategy_signals
-        WHERE created_at > NOW() - INTERVAL '24 hours'
-      `);
-      const { rows: topSignals } = await kenQ(`
-        SELECT market_id, direction, expected_edge, confidence, reasoning, risk_approved, created_at
-        FROM ken_strategy_signals
-        WHERE created_at > NOW() - INTERVAL '4 hours' AND risk_approved = true
-        ORDER BY ABS(expected_edge) DESC
-        LIMIT 10
-      `);
-      const { rows: sourceBreakdown } = await kenQ(`
-        SELECT source, COUNT(*) as cnt, AVG(sentiment_score) as avg_sent
-        FROM ken_sentiment
-        WHERE captured_at > NOW() - INTERVAL '1 hour'
-        GROUP BY source
-        ORDER BY cnt DESC
-        LIMIT 20
-      `);
-      json(res, { stats, topSignals, sourceBreakdown, cachedMarkets: cachedActiveMarkets.length });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // INTELLIGENCE FEED
-  // ════════════════════════════════════════
-
-  'GET /api/intelligence': async (req, res) => {
-    try {
-      const { rows: recent } = await kenQ(`
-        SELECT source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, captured_at
-        FROM ken_sentiment
-        ORDER BY captured_at DESC
-        LIMIT 100
-      `);
-      const { rows: sourceCounts } = await kenQ(`
-        SELECT source, COUNT(*) as cnt, AVG(sentiment_score) as avg_sent,
-               MAX(captured_at) as latest
-        FROM ken_sentiment
-        WHERE captured_at > NOW() - INTERVAL '4 hours'
-        GROUP BY source ORDER BY cnt DESC
-      `);
-      const { rows: [totals] } = await kenQ(`
-        SELECT
-          COUNT(*) as total_items,
-          COUNT(*) FILTER (WHERE captured_at > NOW() - INTERVAL '1 hour') as last_hour,
-          COUNT(*) FILTER (WHERE captured_at > NOW() - INTERVAL '4 hours') as last_4h,
-          AVG(sentiment_score) FILTER (WHERE captured_at > NOW() - INTERVAL '1 hour') as avg_sentiment_1h
-        FROM ken_sentiment
-      `);
-      json(res, { recent, sourceCounts, totals });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // PREDICTIONS FEED
-  // ════════════════════════════════════════
-
-  'GET /api/predictions': async (req, res) => {
-    try {
-      const { rows } = await kenQ(`
-        SELECT ticker, event_ticker, title, signal_type, side, confidence, entry_price, edge_bps, reasoning, sources, created_at
-        FROM ken_predictions
-        ORDER BY created_at DESC
-        LIMIT 100
-      `);
-      json(res, { predictions: rows });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ── Live Markets (all active with snapshots) ──
-  'GET /api/markets': async (req, res) => {
-    try {
-      const { rows } = await kenQ(`
-        SELECT DISTINCT ON (ticker) ticker, event_ticker, title, subtitle, category,
-          yes_bid, yes_ask, no_bid, no_ask, last_price, volume, volume_24h, open_interest, spread,
-          captured_at
-        FROM ken_market_snapshots
-        WHERE captured_at > NOW() - INTERVAL '1 hour'
-        ORDER BY ticker, captured_at DESC
-      `);
-      json(res, { markets: rows, count: rows.length, cached: cachedActiveMarkets.length });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ── Energy/LNG Arbitrage Opportunities ──
-  'GET /api/energy-arb': async (req, res) => {
-    try {
-      const arbs = await energyArbitrageScan(cachedActiveMarkets);
-      // Also get recent energy-related signals
-      const { rows: energySignals } = await kenQ(`
-        SELECT ticker, title, signal_type, side, confidence, entry_price, edge_bps, reasoning, sources, created_at
-        FROM ken_predictions
-        WHERE signal_type IN ('energy_arb', 'polymarket_arb')
-           OR reasoning ILIKE '%energy%' OR reasoning ILIKE '%oil%' OR reasoning ILIKE '%gas%' OR reasoning ILIKE '%lng%'
-        ORDER BY created_at DESC LIMIT 50
-      `);
-      json(res, {
-        arbitrage_opportunities: arbs,
-        energy_signals: energySignals,
-        scan_time: new Date().toISOString(),
-        markets_scanned: cachedActiveMarkets.length,
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ── All Active Signals (for trading) ──
-  'GET /api/signals': async (req, res) => {
-    try {
-      const { rows } = await kenQ(`
-        SELECT market_id as ticker, direction, expected_edge, confidence, reasoning, sources, risk_approved, created_at
-        FROM ken_strategy_signals
-        WHERE created_at > NOW() - INTERVAL '2 hours'
-        ORDER BY created_at DESC LIMIT 200
-      `);
-      json(res, { signals: rows, count: rows.length });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // DASHBOARD — aggregated paper portfolio stats
-  // ════════════════════════════════════════
-
-  'GET /api/dashboard': async (req, res) => {
-    try {
-      // Aggregate balance across all portfolios
-      const { rows: [balRow] } = await kenQ(`
-        SELECT SUM(balance_cents) as total_balance,
-               SUM(total_invested_cents) as total_invested,
-               SUM(total_returned_cents) as total_returned
-        FROM ken_portfolios
-      `);
-
-      // Seed money + economics
-      const totalBalance = Number(balRow?.total_balance || 0);
-      const totalInvested = Number(balRow?.total_invested || 0);
-      const totalReturned = Number(balRow?.total_returned || 0);
-      const seedMoney = totalBalance + totalInvested - totalReturned; // always 5000000 cents ($50K)
-      const netPnl = totalBalance - seedMoney; // how much above/below seed
-      const roi = seedMoney > 0 ? ((totalBalance - seedMoney) / seedMoney * 100) : 0;
-
-      // Daily spend breakdown (last 7 days)
-      const { rows: spendRows } = await kenQ(`
-        SELECT resolved_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 ABS(pnl_cents) ELSE 0 END) as gross_losses,
-               SUM(CASE WHEN pnl_cents > 0 THEN pnl_cents ELSE 0 END) as gross_wins,
-               SUM(cost_cents) as capital_deployed
-        FROM ken_portfolio_trades
-        WHERE status != 'open' AND resolved_at IS NOT NULL
-        GROUP BY resolved_at::date ORDER BY day DESC LIMIT 7
-      `);
-
-      // Daily budget across all portfolios
-      const { rows: [budgetRow] } = await kenQ(`
-        SELECT SUM(daily_budget_cents) as total_daily_budget,
-               COUNT(*) as num_portfolios
-        FROM ken_portfolios
-      `);
-
-      // Open position cost (capital currently at risk)
-      const { rows: [riskRow] } = await kenQ(`
-        SELECT COALESCE(SUM(cost_cents), 0) as capital_at_risk,
-               COUNT(*) as open_positions
-        FROM ken_portfolio_trades WHERE status = 'open'
-      `);
-
-      // Open position count + today's P&L
-      const { rows: [posRow] } = await kenQ(`
-        SELECT
-          COUNT(*) FILTER (WHERE status = 'open') as open_count,
-          COUNT(DISTINCT market_id) FILTER (WHERE status = 'open') as unique_markets,
-          COALESCE(SUM(pnl_cents) FILTER (WHERE status != 'open' AND resolved_at::date = CURRENT_DATE), 0) as daily_pnl,
-          COALESCE(SUM(pnl_cents) FILTER (WHERE status != 'open'), 0) as total_pnl
-        FROM ken_portfolio_trades
-      `);
-
-      // Recent fills (last 10 resolved trades)
-      const { rows: fills } = await kenQ(`
-        SELECT market_id as ticker, market_title, direction,
-               contracts, entry_price_cents, pnl_cents, status, resolved_at
-        FROM ken_portfolio_trades
-        WHERE status != 'open'
-        ORDER BY resolved_at DESC LIMIT 10
-      `);
-
-      // P&L history — daily totals for last 14 days
-      const { rows: pnlHistory } = await kenQ(`
-        SELECT resolved_at::date as day, SUM(pnl_cents) as daily_pnl
-        FROM ken_portfolio_trades
-        WHERE status != 'open' AND resolved_at > NOW() - INTERVAL '14 days'
-        GROUP BY resolved_at::date
-        ORDER BY day ASC
-      `);
-
-      // Top winning markets
-      const { rows: topMarkets } = await kenQ(`
-        SELECT market_id, market_title,
-               COUNT(*) as total_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 pnl_cents,
-               MODE() WITHIN GROUP (ORDER BY direction) as primary_direction,
-               ROUND(AVG(entry_price_cents)) as avg_entry
-        FROM ken_portfolio_trades
-        WHERE status != 'open'
-        GROUP BY market_id, market_title
-        ORDER BY SUM(pnl_cents) DESC
-        LIMIT 10
-      `);
-
-      // Bottom losing markets
-      const { rows: worstMarkets } = await kenQ(`
-        SELECT market_id, market_title,
-               COUNT(*) as total_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 pnl_cents,
-               MODE() WITHIN GROUP (ORDER BY direction) as primary_direction,
-               ROUND(AVG(entry_price_cents)) as avg_entry
-        FROM ken_portfolio_trades
-        WHERE status != 'open'
-        GROUP BY market_id, market_title
-        HAVING SUM(pnl_cents) < 0
-        ORDER BY SUM(pnl_cents) ASC
-        LIMIT 5
-      `);
-
-      // Top/bottom strategies
-      const { rows: strategies } = await kenQ(`
-        SELECT p.strategy, p.name,
-               p.trades_won, p.trades_lost,
-               (p.total_returned_cents - p.total_invested_cents) as net_pnl_cents,
-               p.balance_cents,
-               ROUND(100.0 * p.trades_won / NULLIF(p.trades_won + p.trades_lost, 0), 1) as win_rate
-        FROM ken_portfolios p
-        WHERE p.total_invested_cents > 0
-        ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC
-      `);
-
-      // Today's stats
-      const { rows: [todayRow] } = 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_portfolio_trades
-        WHERE status != 'open' AND resolved_at::date = CURRENT_DATE
-      `);
-
-      // ── Top 3 Best Opportunities Right Now ──
-      // Combine recent high-confidence signals with historical win rates and current market prices
-      const { rows: opportunities } = await kenQ(`
-        WITH recent_signals AS (
-          SELECT DISTINCT ON (market_id)
-            market_id, direction, expected_edge, confidence, reasoning, risk_approved, created_at
-          FROM ken_strategy_signals
-          WHERE created_at > NOW() - INTERVAL '6 hours'
-          ORDER BY market_id, expected_edge DESC
-        ),
-        historical AS (
-          SELECT market_id,
-            COUNT(*) as total_trades,
-            COUNT(*) FILTER (WHERE pnl_cents > 0) as wins,
-            SUM(pnl_cents) as pnl_cents,
-            MODE() WITHIN GROUP (ORDER BY direction) as primary_direction,
-            ROUND(AVG(entry_price_cents)) as avg_entry
-          FROM ken_portfolio_trades
-          WHERE resolved_at IS NOT NULL
-          GROUP BY market_id
-        ),
-        snapshots AS (
-          SELECT DISTINCT ON (ticker)
-            ticker, title, yes_bid, no_bid, volume_24h, last_price
-          FROM ken_market_snapshots
-          WHERE captured_at > NOW() - INTERVAL '2 hours'
-          ORDER BY ticker, captured_at DESC
-        )
-        SELECT
-          s.market_id, COALESCE(snap.title, s.market_id) as title,
-          s.direction, s.expected_edge, s.confidence, s.risk_approved,
-          COALESCE(h.total_trades, 0) as hist_trades,
-          COALESCE(h.wins, 0) as hist_wins,
-          COALESCE(h.pnl_cents, 0) as hist_pnl,
-          COALESCE(h.avg_entry, 0) as avg_entry,
-          snap.yes_bid, snap.no_bid, snap.volume_24h,
-          LEFT(s.reasoning, 200) as reasoning
-        FROM recent_signals s
-        LEFT JOIN historical h ON h.market_id = s.market_id
-        LEFT JOIN snapshots snap ON snap.ticker = s.market_id
-        WHERE s.confidence >= 0.5
-        ORDER BY
-          (s.expected_edge * s.confidence * (1 + COALESCE(h.wins::float / NULLIF(h.total_trades, 0), 0))) DESC
-        LIMIT 3
-      `);
-
-      function edgeDescription(m) {
-        const wr = Number(m.wins) / (Number(m.wins) + Number(m.losses) || 1);
-        const dir = m.primary_direction === 'BUY_NO' ? 'NO' : 'YES';
-        const avg = Number(m.avg_entry);
-        if (wr >= 0.99 && avg >= 90) return `${dir} at ${avg}¢ = near-certain`;
-        if (wr >= 0.95 && avg >= 85) return `${dir} at ${avg}¢, market overprices`;
-        if (wr >= 0.80) return `${dir} side, ${(wr*100).toFixed(0)}% win rate`;
-        if (Number(m.total_trades) >= 150) return `High volume, clear direction`;
-        if (avg >= 80) return `${dir} at ${avg}¢, steady edge`;
-        return `${dir} side, avg ${avg}¢ entry`;
-      }
-
-      json(res, {
-        env: KALSHI_ENV,
-        safe_mode: true,
-        balance: totalBalance,
-        seed_money: seedMoney,
-        net_pnl_from_seed: totalBalance - seedMoney,
-        roi_pct: parseFloat(roi.toFixed(2)),
-        total_invested: totalInvested,
-        total_returned: totalReturned,
-        daily_budget: Number(budgetRow?.total_daily_budget || 0),
-        num_portfolios: Number(budgetRow?.num_portfolios || 0),
-        capital_at_risk: Number(riskRow?.capital_at_risk || 0),
-        open_positions_cost: Number(riskRow?.open_positions || 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(posRow?.open_count || 0),
-        open_orders: Number(posRow?.unique_markets || 0),
-        daily_pnl: Number(posRow?.daily_pnl || 0),
-        total_pnl: Number(posRow?.total_pnl || 0),
-        markets_tracked: cachedActiveMarkets.length,
-        today_trades: Number(todayRow?.trades || 0),
-        today_wins: Number(todayRow?.wins || 0),
-        today_losses: Number(todayRow?.losses || 0),
-        recent_fills: fills.map(f => ({
-          ticker: f.ticker,
-          title: f.market_title,
-          side: f.direction === 'BUY_YES' ? 'yes' : 'no',
-          count: f.contracts,
-          yes_price: f.entry_price_cents,
-          pnl: f.pnl_cents,
-          status: f.status,
-        })),
-        pnl_history: pnlHistory.map(p => Number(p.daily_pnl) / 100),
-        top_markets: topMarkets.map(m => {
-          const t = m.market_id || '';
-          const et = t.replace(/-[^-]+$/, '') || t;
-          const st = et.replace(/-[^-]+$/, '') || et;
-          return {
-          ticker: m.market_id,
-          title: m.market_title,
-          trades: Number(m.total_trades),
-          wins: Number(m.wins),
-          losses: Number(m.losses),
-          pnl: Number(m.pnl_cents),
-          edge: edgeDescription(m),
-          url: `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}`,
-        }; }),
-        worst_markets: worstMarkets.map(m => {
-          const t = m.market_id || '';
-          const et = t.replace(/-[^-]+$/, '') || t;
-          const st = et.replace(/-[^-]+$/, '') || et;
-          return {
-          ticker: m.market_id,
-          title: m.market_title,
-          trades: Number(m.total_trades),
-          wins: Number(m.wins),
-          losses: Number(m.losses),
-          pnl: Number(m.pnl_cents),
-          edge: edgeDescription(m),
-          url: `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}`,
-        }; }),
-        strategies: strategies.map(s => ({
-          name: s.name,
-          strategy: s.strategy,
-          wins: Number(s.trades_won),
-          losses: Number(s.trades_lost),
-          pnl: Number(s.net_pnl_cents),
-          balance: Number(s.balance_cents),
-          win_rate: Number(s.win_rate || 0),
-        })),
-        top_opportunities: opportunities.map(o => {
-          const wr = Number(o.hist_trades) > 0 ? Number(o.hist_wins) / Number(o.hist_trades) : null;
-          const dir = o.direction === 'BUY_NO' ? 'NO' : 'YES';
-          const edge = (parseFloat(o.expected_edge) * 100).toFixed(1);
-          const conf = (parseFloat(o.confidence) * 100).toFixed(0);
-          const yesBid = Number(o.yes_bid || 0);
-          const noBid = Number(o.no_bid || 0);
-          const price = o.direction === 'BUY_NO' ? noBid : yesBid;
-          // Build human-readable "why" from signal reasoning
-          const rawReasoning = o.reasoning || '';
-          const reasoningClean = rawReasoning.replace(/^BUY_(YES|NO) on "[^"]*"\s*—\s*/, '');
-          return {
-            ticker: o.market_id,
-            title: o.title,
-            direction: dir,
-            edge_pct: edge,
-            confidence: conf,
-            price_cents: price,
-            hist_trades: Number(o.hist_trades),
-            hist_wins: Number(o.hist_wins),
-            hist_wr: wr !== null ? (wr * 100).toFixed(1) : null,
-            hist_pnl: Number(o.hist_pnl),
-            volume_24h: Number(o.volume_24h || 0),
-            risk_approved: o.risk_approved,
-            why: reasoningClean,
-            url: (() => { const t = o.market_id || ''; const et = t.replace(/-[^-]+$/, '') || t; const st = et.replace(/-[^-]+$/, '') || et; return `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}`; })(),
-          };
-        }),
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // SCAN CONFIG (User-adjustable intervals + risk params)
-  // ════════════════════════════════════════
-
-  'GET /api/scan-config': async (req, res) => {
-    json(res, {
-      scan_interval_ms: SCAN_INTERVAL_MS,
-      signal_pipeline_ms: SIGNAL_PIPELINE_MS,
-      reddit_interval_ms: REDDIT_INTERVAL_MS,
-      resolve_interval_ms: RESOLVE_INTERVAL_MS,
-      min_edge_threshold: RISK_LIMITS.minEdgeThreshold,
-      min_liquidity: RISK_LIMITS.minLiquidity,
-      cached_markets: cachedActiveMarkets.length,
-    });
-  },
-
-  'POST /api/scan-config': async (req, res) => {
-    try {
-      const body = await readBody(req);
-      let changed = false;
-      if (body.scan_interval_ms !== undefined) { SCAN_INTERVAL_MS = Math.max(60000, Math.min(3600000, Number(body.scan_interval_ms))); changed = true; }
-      if (body.signal_pipeline_ms !== undefined) { SIGNAL_PIPELINE_MS = Math.max(30000, Math.min(1800000, Number(body.signal_pipeline_ms))); changed = true; }
-      if (body.reddit_interval_ms !== undefined) { REDDIT_INTERVAL_MS = Math.max(60000, Math.min(3600000, Number(body.reddit_interval_ms))); changed = true; }
-      if (body.resolve_interval_ms !== undefined) { RESOLVE_INTERVAL_MS = Math.max(30000, Math.min(1800000, Number(body.resolve_interval_ms))); changed = true; }
-      if (body.min_edge_threshold !== undefined) { RISK_LIMITS.minEdgeThreshold = Math.max(0.005, Math.min(0.50, Number(body.min_edge_threshold))); changed = true; }
-      if (body.min_liquidity !== undefined) { RISK_LIMITS.minLiquidity = Math.max(0, Math.min(10000, Number(body.min_liquidity))); changed = true; }
-      if (changed) {
-        await saveScanConfig();
-        restartIntervals();
-      }
-      json(res, {
-        success: true,
-        scan_interval_ms: SCAN_INTERVAL_MS,
-        signal_pipeline_ms: SIGNAL_PIPELINE_MS,
-        reddit_interval_ms: REDDIT_INTERVAL_MS,
-        resolve_interval_ms: RESOLVE_INTERVAL_MS,
-        min_edge_threshold: RISK_LIMITS.minEdgeThreshold,
-        min_liquidity: RISK_LIMITS.minLiquidity,
-        cached_markets: cachedActiveMarkets.length,
-      });
-    } catch (err) {
-      json(res, { error: err.message }, 500);
-    }
-  },
-
-  // ════════════════════════════════════════
-  // MEMORY / SYSTEM STATUS
-  // ════════════════════════════════════════
-
-  'GET /api/system': async (req, res) => {
-    const mem = process.memoryUsage();
-    json(res, {
-      memory: {
-        rss_mb: Math.round(mem.rss / 1024 / 1024),
-        heapUsed_mb: Math.round(mem.heapUsed / 1024 / 1024),
-        heapTotal_mb: Math.round(mem.heapTotal / 1024 / 1024),
-        external_mb: Math.round(mem.external / 1024 / 1024),
-      },
-      uptime_hours: Math.round(process.uptime() / 3600 * 10) / 10,
-      env: KALSHI_ENV,
-      cachedMarkets: cachedActiveMarkets.length,
-      pid: process.pid,
-      cpuCores: os.cpus().length,
-      enhancements: {
-        monteCarloIterations: 15000,
-        geminiModel: GEMINI_MODEL,
-        geminiCallsToday: geminiCallCount,
-        geminiDailyLimit: GEMINI_DAILY_LIMIT,
-        scanInterval: '8m',
-        signalPipeline: '2m',
-        redditInterval: '15m',
-        momentumCacheSize: Object.keys(cachedMomentumMap).length,
-      },
-    });
-  },
-
-  // ── 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 = Math.max(0, pf.balance_cents); // Remaining = whatever balance is left
-
-        // 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;
-        pf.is_bankrupt = pf.balance_cents <= 0;
-      }
-      // Sort: active portfolios by ROI (best first), bankrupt portfolios at bottom
-      portfolios.sort((a, b) => {
-        if (a.is_bankrupt !== b.is_bankrupt) return a.is_bankrupt ? 1 : -1;
-        const aROI = a.total_invested_cents > 0 ? (a.total_returned_cents - a.total_invested_cents) / a.total_invested_cents : 0;
-        const bROI = b.total_invested_cents > 0 ? (b.total_returned_cents - b.total_invested_cents) / b.total_invested_cents : 0;
-        return bROI - aROI;
-      });
-      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); }
-  },
-
-  // ── PortFAUXlio Charts API — time-series NAV data ──
-  'GET /api/portfauxlio/charts': async (req, res) => {
-    try {
-      const url = new URL(req.url, 'http://x');
-      const period = url.searchParams.get('period') || '7d';
-      const intervals = { '1d': '1 day', '3d': '3 days', '7d': '7 days', '14d': '14 days', '30d': '30 days' };
-      const interval = intervals[period] || '7 days';
-
-      // Snapshots (intraday)
-      const { rows: snapRows } = await kenQ(`
-        SELECT s.portfolio_id, s.nav_cents, s.open_positions, s.snapshot_at, p.name, p.strategy
-        FROM ken_portfolio_snapshots s JOIN ken_portfolios p ON p.id = s.portfolio_id
-        WHERE s.snapshot_at > NOW() - INTERVAL '${interval}' ORDER BY s.portfolio_id, s.snapshot_at ASC
-      `);
-
-      // Daily P&L (longer-term)
-      const { rows: dailyRows } = await kenQ(`
-        SELECT dp.portfolio_id, dp.date, dp.daily_pnl_cents, dp.cumulative_pnl_cents,
-               dp.wins, dp.losses, dp.closed_trades, p.name, p.strategy
-        FROM ken_daily_pnl dp JOIN ken_portfolios p ON p.id = dp.portfolio_id
-        WHERE dp.date > CURRENT_DATE - INTERVAL '${interval}' ORDER BY dp.portfolio_id, dp.date ASC
-      `);
-
-      const byPortfolio = {};
-      for (const r of snapRows) {
-        if (!byPortfolio[r.portfolio_id]) byPortfolio[r.portfolio_id] = { id: r.portfolio_id, name: r.name, strategy: r.strategy, snapshots: [], daily: [] };
-        byPortfolio[r.portfolio_id].snapshots.push({ nav: r.nav_cents, openPositions: r.open_positions, time: r.snapshot_at });
-      }
-      for (const r of dailyRows) {
-        if (!byPortfolio[r.portfolio_id]) byPortfolio[r.portfolio_id] = { id: r.portfolio_id, name: r.name, strategy: r.strategy, snapshots: [], daily: [] };
-        byPortfolio[r.portfolio_id].daily.push({ date: r.date, pnl: r.daily_pnl_cents, cumulative: r.cumulative_pnl_cents, wins: r.wins, losses: r.losses, trades: r.closed_trades });
-      }
-
-      json(res, { portfolios: Object.values(byPortfolio), period });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  // ── PortFAUXlio SWOT — trades with strengths/weaknesses/opportunities/threats ──
-  'GET /api/portfauxlio/swot': async (req, res) => {
-    try {
-      const { rows: trades } = await kenQ(`
-        SELECT t.id, t.market_id, t.market_title, t.direction, t.entry_price_cents,
-               t.contracts, t.cost_cents, t.status, t.pnl_cents, t.reasoning,
-               t.confidence, t.ai_enhanced, t.mc_consistent, t.created_at, t.resolved_at,
-               p.name AS portfolio_name, p.strategy, p.id AS portfolio_id
-        FROM ken_portfolio_trades t JOIN ken_portfolios p ON p.id = t.portfolio_id
-        WHERE t.created_at > NOW() - INTERVAL '7 days'
-        ORDER BY t.created_at DESC LIMIT 200
-      `);
-
-      // Group by market
-      const byMarket = {};
-      for (const t of trades) {
-        if (!byMarket[t.market_id]) byMarket[t.market_id] = { market_id: t.market_id, title: t.market_title || t.market_id, trades: [], totalCost: 0, totalPnl: 0, models: new Set() };
-        const m = byMarket[t.market_id];
-        m.trades.push(t);
-        m.totalCost += parseInt(t.cost_cents) || 0;
-        m.totalPnl += parseInt(t.pnl_cents) || 0;
-        m.models.add(t.portfolio_name);
-      }
-
-      // Generate SWOT for each market group
-      const swotMarkets = Object.values(byMarket).map(m => {
-        const t0 = m.trades[0];
-        const conf = parseFloat(t0.confidence) || 0;
-        const aiCount = m.trades.filter(t => t.ai_enhanced).length;
-        const mcCount = m.trades.filter(t => t.mc_consistent).length;
-        const totalT = m.trades.length;
-        const openCount = m.trades.filter(t => t.status === 'open').length;
-        const reasoning = t0.reasoning || '';
-
-        // Parse edge from reasoning
-        const edgeMatch = reasoning.match(/edge\s*([\d.]+)%/i);
-        const edge = edgeMatch ? parseFloat(edgeMatch[1]) : 0;
-        const mcMatch = reasoning.match(/MC:\s*\u03bc=([\d.]+)%\s*\u00b1\s*([\d.]+)%/);
-        const mcMean = mcMatch ? parseFloat(mcMatch[1]) : 0;
-        const mcStd = mcMatch ? parseFloat(mcMatch[2]) : 0;
-        const artMatch = reasoning.match(/(\d+)\s*articles?/i);
-        const articles = artMatch ? parseInt(artMatch[1]) : 0;
-        const agreeMatch = reasoning.match(/Agreement:\s*([\d.]+)%/i);
-        const agreement = agreeMatch ? parseFloat(agreeMatch[1]) : 0;
-
-        // SWOT generation
-        const strengths = [];
-        const weaknesses = [];
-        const opportunities = [];
-        const threats = [];
-
-        // Strengths
-        if (conf >= 0.6) strengths.push('High confidence (' + Math.round(conf * 100) + '%)');
-        if (mcCount > totalT * 0.5) strengths.push('Monte Carlo validated (' + mcCount + '/' + totalT + ' models)');
-        if (aiCount > 0) strengths.push('AI enhanced signals');
-        if (totalT >= 3) strengths.push('Multi-model consensus (' + totalT + ' models agree)');
-        if (agreement >= 80) strengths.push('Strong news agreement (' + agreement + '%)');
-        if (articles >= 3) strengths.push('Strong news coverage (' + articles + ' articles)');
-        if (edge >= 15) strengths.push('Large statistical edge (' + edge.toFixed(1) + '%)');
-        if (strengths.length === 0) strengths.push('Active position in play');
-
-        // Weaknesses
-        if (conf < 0.4) weaknesses.push('Low confidence (' + Math.round(conf * 100) + '%)');
-        if (mcCount === 0) weaknesses.push('No Monte Carlo validation');
-        if (aiCount === 0) weaknesses.push('No AI enhancement');
-        if (articles <= 1) weaknesses.push('Limited news coverage (' + articles + ' article' + (articles === 1 ? '' : 's') + ')');
-        if (edge < 5 && edge > 0) weaknesses.push('Narrow edge (' + edge.toFixed(1) + '%)');
-        if (mcStd > 10) weaknesses.push('High MC uncertainty (\u00b1' + mcStd.toFixed(1) + '%)');
-        if (weaknesses.length === 0) weaknesses.push('No significant weaknesses identified');
-
-        // Opportunities
-        if (edge >= 8) opportunities.push('Exploitable edge of ' + edge.toFixed(1) + '%');
-        if (m.models.size >= 5) opportunities.push(m.models.size + ' models see opportunity');
-        if (mcMean > edge) opportunities.push('MC mean (' + mcMean.toFixed(1) + '%) exceeds raw edge');
-        if (openCount > 0) opportunities.push(openCount + ' open position' + (openCount > 1 ? 's' : '') + ' tracking');
-        if (opportunities.length === 0) opportunities.push('Market exposure maintained');
-
-        // Threats
-        if (mcStd > 8) threats.push('High volatility (MC \u00b1' + mcStd.toFixed(1) + '%)');
-        if (agreement < 60 && agreement > 0) threats.push('Mixed news sentiment (' + agreement + '% agreement)');
-        if (m.totalPnl < 0) threats.push('Currently underwater ($' + (Math.abs(m.totalPnl) / 100).toFixed(2) + ' loss)');
-        const direction = t0.direction;
-        if (direction === 'BUY_YES' && edge < 10) threats.push('Thin YES margin — small price move could flip');
-        if (direction === 'BUY_NO' && conf < 0.5) threats.push('NO position with uncertain conviction');
-        if (threats.length === 0) threats.push('Standard market risk');
-
-        return {
-          market_id: m.market_id,
-          title: m.title,
-          direction: t0.direction,
-          edge,
-          confidence: conf,
-          articles,
-          agreement,
-          mc_mean: mcMean,
-          mc_std: mcStd,
-          model_count: totalT,
-          models: [...m.models],
-          open_count: openCount,
-          total_cost: m.totalCost,
-          total_pnl: m.totalPnl,
-          entry_price: parseInt(t0.entry_price_cents) || 0,
-          first_trade: m.trades[m.trades.length - 1]?.created_at,
-          reasoning: reasoning.substring(0, 200),
-          swot: { strengths, weaknesses, opportunities, threats },
-          trades: m.trades.map(t => ({
-            id: t.id, portfolio: t.portfolio_name, strategy: t.strategy,
-            direction: t.direction, contracts: t.contracts,
-            cost: parseInt(t.cost_cents) || 0, pnl: parseInt(t.pnl_cents) || 0,
-            status: t.status, created_at: t.created_at
-          }))
-        };
-      });
-
-      // Sort by model count desc (most consensus first)
-      swotMarkets.sort((a, b) => b.model_count - a.model_count);
-
-      json(res, { markets: swotMarkets, total_trades: trades.length });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  // ── Weekly Tournament System — survive or die ──
-  'GET /api/tournament/current': async (req, res) => {
-    try {
-      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
-      if (!tourney) return json(res, { active: false, message: 'No active tournament' });
-      const { rows: entries } = await kenQ(`
-        SELECT e.*, p.name, p.strategy, p.balance_cents, p.total_invested_cents, p.total_returned_cents,
-               p.trades_won, p.trades_lost, p.daily_budget_cents
-        FROM ken_tournament_entries e JOIN ken_portfolios p ON p.id = e.portfolio_id
-        WHERE e.tournament_id = $1 ORDER BY e.pnl_cents DESC NULLS LAST
-      `, [tourney.id]);
-      // Compute live P&L for each entry
-      for (const e of entries) {
-        const { rows: [stats] } = await kenQ(`
-          SELECT COUNT(*) as trades, COALESCE(SUM(cost_cents),0) as total_cost,
-                 COALESCE(SUM(CASE WHEN status IN ('won','lost','closed') THEN pnl_cents ELSE 0 END),0) as realized_pnl
-          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at >= $2
-        `, [e.portfolio_id, tourney.week_start]);
-        e.live_trades = parseInt(stats.trades) || 0;
-        e.live_invested = parseInt(stats.total_cost) || 0;
-        e.live_pnl = parseInt(stats.realized_pnl) || 0;
-      }
-      json(res, { active: true, tournament: tourney, entries });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  'POST /api/tournament/start': async (req, res) => {
-    try {
-      const body = await readBody(req);
-      const portfolioIds = body.portfolio_ids || [];
-      const budgetCents = body.budget_cents || 2000; // $20 default
-      const notes = body.notes || '';
-      const weeks = body.weeks || 1;
-      if (portfolioIds.length < 1 || portfolioIds.length > 10) return json(res, { error: 'Pick 1-10 portfolios' }, 400);
-      // Check no active tournament
-      const { rows: active } = await kenQ(`SELECT id FROM ken_weekly_tournaments WHERE status = 'active'`);
-      if (active.length > 0) return json(res, { error: 'Tournament already active. Evaluate or cancel it first.', active_id: active[0].id }, 400);
-      const weekStart = new Date(); weekStart.setHours(0,0,0,0);
-      const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 7 * weeks);
-      const { rows: [tourney] } = await kenQ(`
-        INSERT INTO ken_weekly_tournaments (week_start, week_end, budget_per_portfolio_cents, notes)
-        VALUES ($1, $2, $3, $4) RETURNING *
-      `, [weekStart.toISOString().split('T')[0], weekEnd.toISOString().split('T')[0], budgetCents, notes]);
-      const entries = [];
-      for (const pid of portfolioIds) {
-        const { rows: [pf] } = await kenQ(`SELECT * FROM ken_portfolios WHERE id = $1`, [pid]);
-        if (!pf) continue;
-        // Set portfolio budget for the tournament
-        await kenQ(`UPDATE ken_portfolios SET daily_budget_cents = $1, balance_cents = $2 WHERE id = $3`, [Math.round(budgetCents / 7), budgetCents, pid]);
-        const { rows: [entry] } = await kenQ(`
-          INSERT INTO ken_tournament_entries (tournament_id, portfolio_id, start_balance_cents, status)
-          VALUES ($1, $2, $3, 'alive') RETURNING *
-        `, [tourney.id, pid, budgetCents]);
-        entries.push({ ...entry, name: pf.name, strategy: pf.strategy });
-      }
-      json(res, { tournament: tourney, entries, message: `Tournament started with ${entries.length} portfolios at $${(budgetCents/100).toFixed(2)} each` });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  'POST /api/tournament/evaluate': async (req, res) => {
-    try {
-      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
-      if (!tourney) return json(res, { error: 'No active tournament' }, 400);
-      const { rows: entries } = await kenQ(`
-        SELECT e.*, p.name, p.strategy FROM ken_tournament_entries e
-        JOIN ken_portfolios p ON p.id = e.portfolio_id WHERE e.tournament_id = $1
-      `, [tourney.id]);
-      const results = [];
-      for (const e of entries) {
-        const { rows: [stats] } = await kenQ(`
-          SELECT COUNT(*) as trades,
-                 COALESCE(SUM(CASE WHEN status='won' THEN payout_cents - cost_cents WHEN status='lost' THEN -cost_cents ELSE 0 END),0) as pnl
-          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at >= $2
-        `, [e.portfolio_id, tourney.week_start]);
-        const pnl = parseInt(stats.pnl) || 0;
-        const trades = parseInt(stats.trades) || 0;
-        const survived = pnl >= 0;
-        await kenQ(`UPDATE ken_tournament_entries SET end_balance_cents = $1, pnl_cents = $2, trades_count = $3, status = $4, elimination_reason = $5 WHERE id = $6`,
-          [e.start_balance_cents + pnl, pnl, trades, survived ? 'survived' : 'eliminated', survived ? null : `Lost $${(Math.abs(pnl)/100).toFixed(2)}`, e.id]);
-        if (!survived) await kenQ(`UPDATE ken_portfolios SET daily_budget_cents = 0 WHERE id = $1`, [e.portfolio_id]);
-        results.push({ name: e.name, strategy: e.strategy, pnl_cents: pnl, trades, status: survived ? 'SURVIVED' : 'ELIMINATED' });
-      }
-      await kenQ(`UPDATE ken_weekly_tournaments SET status = 'completed' WHERE id = $1`, [tourney.id]);
-      const survivors = results.filter(r => r.status === 'SURVIVED');
-      const eliminated = results.filter(r => r.status === 'ELIMINATED');
-      json(res, { tournament_id: tourney.id, results, survivors: survivors.length, eliminated: eliminated.length, message: `${survivors.length} survived, ${eliminated.length} eliminated` });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  'POST /api/tournament/cancel': async (req, res) => {
-    try {
-      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
-      if (!tourney) return json(res, { error: 'No active tournament' }, 400);
-      await kenQ(`UPDATE ken_weekly_tournaments SET status = 'cancelled' WHERE id = $1`, [tourney.id]);
-      await kenQ(`UPDATE ken_tournament_entries SET status = 'cancelled' WHERE tournament_id = $1`, [tourney.id]);
-      json(res, { message: 'Tournament cancelled', id: tourney.id });
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  'GET /api/tournament/history': async (req, res) => {
-    try {
-      const { rows: tournaments } = await kenQ(`SELECT * FROM ken_weekly_tournaments ORDER BY id DESC LIMIT 20`);
-      for (const t of tournaments) {
-        const { rows: entries } = await kenQ(`
-          SELECT e.*, p.name, p.strategy FROM ken_tournament_entries e
-          JOIN ken_portfolios p ON p.id = e.portfolio_id WHERE e.tournament_id = $1 ORDER BY e.pnl_cents DESC NULLS LAST
-        `, [t.id]);
-        t.entries = entries;
-      }
-      json(res, tournaments);
-    } catch (e) { json(res, { error: e.message }, 500); }
-  },
-
-  // ── PortFAUXlio Dashboard API — full simulated trading view ──
-  'GET /api/portfauxlio': async (req, res) => {
-    try {
-      // 1. All portfolios with computed stats
-      const { rows: portfolios } = await kenQ(`
-        SELECT p.*,
-          (p.total_returned_cents - p.total_invested_cents) AS pnl_cents,
-          CASE WHEN p.total_invested_cents > 0
-            THEN ROUND((p.total_returned_cents - p.total_invested_cents)::numeric / p.total_invested_cents * 100, 1)
-            ELSE 0 END AS roi_pct,
-          (p.trades_won + p.trades_lost + p.trades_expired) AS resolved_trades,
-          CASE WHEN (p.trades_won + p.trades_lost) > 0
-            THEN ROUND(p.trades_won::numeric / (p.trades_won + p.trades_lost) * 100, 0)
-            ELSE 0 END AS win_rate
-        FROM ken_portfolios p ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC
-      `);
-
-      // 2. All active (open) trades across all portfolios
-      const { rows: activeTrades } = await kenQ(`
-        SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction,
-          t.contracts, t.entry_price_cents, t.cost_cents, t.confidence, t.created_at,
-          p.name AS portfolio_name, p.strategy
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        WHERE t.status = 'open'
-        ORDER BY t.created_at DESC
-      `);
-
-      // 3. Trade history (all trades, most recent first)
-      const { rows: tradeHistory } = await kenQ(`
-        SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction,
-          t.contracts, t.entry_price_cents, t.cost_cents, t.pnl_cents, t.status,
-          t.confidence, t.created_at, t.resolved_at,
-          p.name AS portfolio_name, p.strategy
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        ORDER BY t.created_at DESC LIMIT 200
-      `);
-
-      // 4. Per-model trade distribution: which markets each strategy traded
-      const { rows: modelMarkets } = await kenQ(`
-        SELECT p.strategy, t.market_id,
-          COUNT(*) AS trade_count,
-          SUM(t.cost_cents) AS total_cost,
-          MAX(t.created_at) AS last_trade
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        GROUP BY p.strategy, t.market_id
-        ORDER BY p.strategy, trade_count DESC
-      `);
-
-      // 5. Overlap analysis: markets traded by 3+ strategies
-      const { rows: overlaps } = await kenQ(`
-        SELECT t.market_id,
-          COUNT(DISTINCT p.strategy) AS num_strategies,
-          ARRAY_AGG(DISTINCT p.name ORDER BY p.name) AS portfolios,
-          SUM(t.cost_cents) AS total_exposure
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        GROUP BY t.market_id
-        HAVING COUNT(DISTINCT p.strategy) >= 2
-        ORDER BY num_strategies DESC
-      `);
-
-      // 6. Summary stats
-      const totalInvested = portfolios.reduce((s, p) => s + (parseInt(p.total_invested_cents) || 0), 0);
-      const totalReturned = portfolios.reduce((s, p) => s + (parseInt(p.total_returned_cents) || 0), 0);
-      const totalTrades = tradeHistory.length;
-      const openTrades = activeTrades.length;
-      const uniqueMarkets = [...new Set(tradeHistory.map(t => t.market_id))].length;
-
-      json(res, {
-        portfolios,
-        activeTrades,
-        tradeHistory,
-        modelMarkets,
-        overlaps,
-        summary: {
-          totalInvested, totalReturned,
-          totalPnl: totalReturned - totalInvested,
-          totalTrades, openTrades, uniqueMarkets,
-          activeModels: portfolios.filter(p => parseInt(p.total_invested_cents) > 0).length,
-        }
-      });
-    } 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, captured_at
-        FROM ken_sentiment
-        WHERE source IN ('heating_intel', 'noaa_alert', 'noaa_forecast')
-        ORDER BY captured_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 captured_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,
-      })),
-    });
-  },
-
-  // ── Market Data — stock-market style data points ──
-  'GET /api/inc/market-data': async (req, res) => {
-    const out = {};
-
-    // 1. Hourly signal volume — last 24h in 1-hour buckets
-    try {
-      const { rows } = await kenQ(`SELECT date_trunc('hour', created_at) AS hour,
-        COUNT(*) AS signals, COUNT(*) FILTER (WHERE risk_approved) AS approved,
-        AVG(ABS(expected_edge::numeric)) AS avg_edge,
-        AVG(confidence::numeric) AS avg_confidence
-        FROM ken_strategy_signals
-        WHERE created_at > NOW() - INTERVAL '24 hours'
-        GROUP BY 1 ORDER BY 1`);
-      out.signalTrend = rows.map(r => ({
-        hour: r.hour, signals: parseInt(r.signals), approved: parseInt(r.approved),
-        avgEdge: parseFloat(parseFloat(r.avg_edge || 0).toFixed(4)),
-        avgConf: parseFloat(parseFloat(r.avg_confidence || 0).toFixed(3)),
-      }));
-    } catch { out.signalTrend = []; }
-
-    // 2. Sentiment volume by hour — last 24h
-    try {
-      const { rows } = await kenQ(`SELECT date_trunc('hour', captured_at) AS hour,
-        COUNT(*) AS articles, AVG(sentiment_score) AS avg_sentiment,
-        COUNT(DISTINCT source) AS sources
-        FROM ken_sentiment
-        WHERE captured_at > NOW() - INTERVAL '24 hours'
-        GROUP BY 1 ORDER BY 1`);
-      out.sentimentTrend = rows.map(r => ({
-        hour: r.hour, articles: parseInt(r.articles),
-        avgSentiment: parseFloat(parseFloat(r.avg_sentiment || 0).toFixed(3)),
-        sources: parseInt(r.sources),
-      }));
-    } catch { out.sentimentTrend = []; }
-
-    // 3. Top sentiment sources with counts
-    try {
-      const { rows } = await kenQ(`SELECT source, COUNT(*) AS cnt,
-        AVG(sentiment_score) AS avg_sent
-        FROM ken_sentiment WHERE captured_at > NOW() - INTERVAL '24 hours'
-        GROUP BY source ORDER BY cnt DESC LIMIT 15`);
-      out.sourceMix = rows.map(r => ({
-        source: r.source, count: parseInt(r.cnt),
-        avgSentiment: parseFloat(parseFloat(r.avg_sent || 0).toFixed(3)),
-      }));
-    } catch { out.sourceMix = []; }
-
-    // 4. Top tickers / categories from sentiment
-    try {
-      const { rows } = await kenQ(`SELECT
-        COALESCE(relevance_ticker, 'general') AS category, COUNT(*) AS cnt
-        FROM ken_sentiment WHERE captured_at > NOW() - INTERVAL '24 hours'
-        AND relevance_ticker IS NOT NULL AND relevance_ticker != ''
-        GROUP BY relevance_ticker ORDER BY cnt DESC LIMIT 12`);
-      out.categories = rows.map(r => ({ category: r.category, count: parseInt(r.cnt) }));
-    } catch { out.categories = []; }
-
-    // 5. Portfolio P&L (current state) — compute actual PnL from invested vs returned
-    try {
-      const { rows } = await kenQ(`SELECT p.id, p.name, p.strategy, p.trades_won, p.trades_lost, p.trades_expired,
-        p.total_invested_cents, p.total_returned_cents, p.daily_budget_cents, p.streak,
-        p.best_trade_cents, p.worst_trade_cents, p.last_trade_at,
-        (p.total_returned_cents - p.total_invested_cents) AS pnl_cents,
-        CASE WHEN (p.trades_won + p.trades_lost) > 0
-          THEN ROUND(p.trades_won * 100.0 / (p.trades_won + p.trades_lost))
-          ELSE 0 END AS win_rate,
-        (SELECT COUNT(*) FROM ken_portfolio_trades t WHERE t.portfolio_id = p.id) AS total_trades
-        FROM ken_portfolios p ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC`);
-      out.portfolios = rows.map(r => ({
-        ...r,
-        pnl_cents: parseInt(r.pnl_cents) || 0,
-        win_rate: parseInt(r.win_rate) || 0,
-        total_trades: parseInt(r.total_trades) || 0,
-        total_invested_cents: parseInt(r.total_invested_cents) || 0,
-        total_returned_cents: parseInt(r.total_returned_cents) || 0,
-      }));
-    } catch { out.portfolios = []; }
-
-    // 6. Active markets count and top movers
-    out.activeMarkets = cachedActiveMarkets.length;
-    out.momentumKeys = Object.keys(cachedMomentumMap).length;
-
-    // 7. Signal approval rate over last 6 hours vs prior 6
-    try {
-      const { rows } = await kenQ(`SELECT
-        COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '6 hours') AS recent_total,
-        COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '6 hours' AND risk_approved) AS recent_approved,
-        COUNT(*) FILTER (WHERE created_at BETWEEN NOW() - INTERVAL '12 hours' AND NOW() - INTERVAL '6 hours') AS prior_total,
-        COUNT(*) FILTER (WHERE created_at BETWEEN NOW() - INTERVAL '12 hours' AND NOW() - INTERVAL '6 hours' AND risk_approved) AS prior_approved
-        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '12 hours'`);
-      const r = rows[0] || {};
-      const recentRate = r.recent_total > 0 ? (r.recent_approved / r.recent_total * 100) : 0;
-      const priorRate = r.prior_total > 0 ? (r.prior_approved / r.prior_total * 100) : 0;
-      out.approvalTrend = {
-        current: Math.round(recentRate), prior: Math.round(priorRate),
-        change: Math.round(recentRate - priorRate),
-        recentSignals: parseInt(r.recent_total), priorSignals: parseInt(r.prior_total),
-      };
-    } catch { out.approvalTrend = { current: 0, prior: 0, change: 0 }; }
-
-    // 8. Gemini usage
-    out.gemini = { callsToday: geminiCallCount, dailyLimit: GEMINI_DAILY_LIMIT, pctUsed: Math.round(geminiCallCount / GEMINI_DAILY_LIMIT * 100) };
-
-    // 9. Latest signals feed — most recent 15 signals for live ticker
-    try {
-      const { rows } = await kenQ(`SELECT market_id, direction, expected_edge, confidence, risk_approved, created_at,
-        SUBSTRING(reasoning FROM 1 FOR 80) AS reason_short
-        FROM ken_strategy_signals ORDER BY created_at DESC LIMIT 15`);
-      out.latestSignals = rows.map(r => ({
-        market: r.market_id, direction: r.direction,
-        edge: parseFloat(parseFloat(r.expected_edge || 0).toFixed(4)),
-        confidence: parseFloat(parseFloat(r.confidence || 0).toFixed(3)),
-        approved: r.risk_approved, reason: r.reason_short,
-        time: r.created_at,
-      }));
-    } catch { out.latestSignals = []; }
-
-    // 10. Per-market signal stats — heatmap data
-    try {
-      const { rows } = await kenQ(`SELECT market_id,
-        COUNT(*) AS total_signals,
-        AVG(expected_edge::numeric) AS avg_edge,
-        AVG(confidence::numeric) AS avg_conf,
-        COUNT(*) FILTER (WHERE risk_approved) AS approved,
-        MAX(created_at) AS last_signal
-        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'
-        GROUP BY market_id ORDER BY total_signals DESC LIMIT 20`);
-      out.marketStats = rows.map(r => ({
-        market: r.market_id, signals: parseInt(r.total_signals),
-        avgEdge: parseFloat(parseFloat(r.avg_edge || 0).toFixed(4)),
-        avgConf: parseFloat(parseFloat(r.avg_conf || 0).toFixed(3)),
-        approved: parseInt(r.approved), lastSignal: r.last_signal,
-      }));
-    } catch { out.marketStats = []; }
-
-    // 11. Recent trades with details
-    try {
-      const { rows } = await kenQ(`SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction, t.contracts,
-        t.entry_price_cents, t.pnl_cents, t.status, t.cost_cents, t.confidence, t.created_at,
-        p.name AS portfolio_name, p.strategy
-        FROM ken_portfolio_trades t
-        JOIN ken_portfolios p ON p.id = t.portfolio_id
-        ORDER BY t.created_at DESC LIMIT 20`);
-      out.recentTrades = rows.map(r => ({
-        ...r, entry_price_cents: parseInt(r.entry_price_cents) || 0, pnl_cents: parseInt(r.pnl_cents) || 0,
-        contracts: parseInt(r.contracts) || 0, cost_cents: parseInt(r.cost_cents) || 0,
-      }));
-    } catch { out.recentTrades = []; }
-
-    // 12. Confidence distribution histogram (10 buckets from 0-1)
-    try {
-      const { rows } = await kenQ(`SELECT
-        WIDTH_BUCKET(confidence::numeric, 0, 1, 10) AS bucket,
-        COUNT(*) AS cnt
-        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'
-        GROUP BY bucket ORDER BY bucket`);
-      out.confDistribution = Array.from({length: 10}, (_, i) => {
-        const found = rows.find(r => parseInt(r.bucket) === i + 1);
-        return { range: (i * 10) + '-' + ((i + 1) * 10) + '%', count: found ? parseInt(found.cnt) : 0 };
-      });
-    } catch { out.confDistribution = []; }
-
-    json(res, out);
-  },
-};
-
-// ════════════════════════════════════════
-// TOURNAMENT ARENA — 3D TEAM COMPETITION
-// ════════════════════════════════════════
-const ARENA_HTML = `<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>Tournament Arena | Ken Trading</title>
-<style>
-*{margin:0;padding:0;box-sizing:border-box}
-body{background:#000;overflow:hidden;font-family:'Segoe UI',system-ui,sans-serif;color:#fff}
-#c{position:fixed;top:0;left:0;z-index:0}
-#hud{position:fixed;top:0;left:0;right:0;z-index:10;pointer-events:none}
-.top-bar{display:flex;justify-content:center;align-items:center;padding:20px 40px;gap:30px}
-.team-badge{display:flex;align-items:center;gap:12px;padding:10px 24px;border-radius:12px;pointer-events:auto;cursor:default}
-.team-badge.alpha{background:rgba(99,102,241,.15);border:1px solid rgba(99,102,241,.4)}
-.team-badge.beta{background:rgba(244,63,94,.15);border:1px solid rgba(244,63,94,.4)}
-.team-badge h2{font-size:16px;letter-spacing:2px;text-transform:uppercase}
-.team-badge.alpha h2{color:#818cf8}
-.team-badge.beta h2{color:#fb7185}
-.vs{font-size:32px;font-weight:900;background:linear-gradient(135deg,#818cf8,#fb7185);-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-shadow:none;filter:drop-shadow(0 0 20px rgba(168,85,247,.5))}
-.scoreboard{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:10;display:flex;gap:8px;background:rgba(0,0,0,.7);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:12px 20px}
-.score-card{text-align:center;padding:6px 16px;border-radius:10px;min-width:100px}
-.score-card.alpha{background:rgba(99,102,241,.1);border:1px solid rgba(99,102,241,.2)}
-.score-card.beta{background:rgba(244,63,94,.1);border:1px solid rgba(244,63,94,.2)}
-.score-card .name{font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:4px}
-.score-card.alpha .name{color:#818cf8}
-.score-card.beta .name{color:#fb7185}
-.score-card .strat{font-size:9px;color:#666;margin-bottom:6px}
-.score-card .pnl{font-size:18px;font-weight:800;font-family:'Courier New',monospace}
-.score-card .pnl.pos{color:#4ade80}
-.score-card .pnl.neg{color:#f87171}
-.score-card .pnl.zero{color:#666}
-.score-card .trades{font-size:9px;color:#888;margin-top:2px}
-.score-card .bar{height:3px;border-radius:2px;margin-top:6px;background:#1a1a2e;overflow:hidden}
-.score-card .bar-fill{height:100%;border-radius:2px;transition:width .5s ease}
-.week-info{position:fixed;top:80px;left:50%;transform:translateX(-50%);z-index:10;font-size:11px;color:#555;letter-spacing:2px;text-transform:uppercase}
-.team-total{font-size:11px;font-weight:700;margin-top:4px;letter-spacing:1px}
-.back-link{position:fixed;top:20px;left:20px;z-index:20;color:#666;text-decoration:none;font-size:12px;pointer-events:auto}
-.back-link:hover{color:#fff}
-.refresh-note{position:fixed;bottom:4px;left:50%;transform:translateX(-50%);z-index:10;font-size:9px;color:#333}
-</style>
-</head>
-<body>
-<canvas id="c"></canvas>
-<a href="/inc" class="back-link">&larr; Command Center</a>
-<div id="hud">
-  <div class="top-bar">
-    <div class="team-badge alpha"><div style="width:12px;height:12px;border-radius:50%;background:#818cf8;box-shadow:0 0 12px #818cf8"></div><h2>Team Alpha</h2></div>
-    <div class="vs">VS</div>
-    <div class="team-badge beta"><div style="width:12px;height:12px;border-radius:50%;background:#fb7185;box-shadow:0 0 12px #fb7185"></div><h2>Team Beta</h2></div>
-  </div>
-</div>
-<div id="week-info" class="week-info">Loading tournament...</div>
-<div id="scoreboard" class="scoreboard"></div>
-<div class="refresh-note">Auto-refreshes every 15s</div>
-
-<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"><\/script>
-<script>
-const scene = new THREE.Scene();
-const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 1000);
-const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('c'), antialias: true, alpha: true });
-renderer.setSize(innerWidth, innerHeight);
-renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
-camera.position.set(0, 4, 12);
-camera.lookAt(0, 0, 0);
-
-// Lighting
-scene.add(new THREE.AmbientLight(0x222244, 0.5));
-const spotA = new THREE.SpotLight(0x6366f1, 2, 30, Math.PI/4);
-spotA.position.set(-6, 10, 5);
-scene.add(spotA);
-const spotB = new THREE.SpotLight(0xf43f5e, 2, 30, Math.PI/4);
-spotB.position.set(6, 10, 5);
-scene.add(spotB);
-const rimLight = new THREE.PointLight(0xa855f7, 1, 20);
-rimLight.position.set(0, 8, -5);
-scene.add(rimLight);
-
-// Arena floor
-const floorGeo = new THREE.CircleGeometry(8, 64);
-const floorMat = new THREE.MeshStandardMaterial({ color: 0x0a0a1a, metalness: 0.8, roughness: 0.3 });
-const floor = new THREE.Mesh(floorGeo, floorMat);
-floor.rotation.x = -Math.PI / 2;
-floor.position.y = -0.5;
-scene.add(floor);
-
-// Arena ring
-const ringGeo = new THREE.TorusGeometry(8, 0.05, 8, 128);
-const ringMat = new THREE.MeshStandardMaterial({ color: 0xa855f7, emissive: 0xa855f7, emissiveIntensity: 0.5 });
-const ring = new THREE.Mesh(ringGeo, ringMat);
-ring.rotation.x = -Math.PI / 2;
-ring.position.y = -0.48;
-scene.add(ring);
-
-// Center divider line
-const divGeo = new THREE.PlaneGeometry(0.02, 14);
-const divMat = new THREE.MeshBasicMaterial({ color: 0x333355, transparent: true, opacity: 0.3 });
-const divider = new THREE.Mesh(divGeo, divMat);
-divider.rotation.x = -Math.PI / 2;
-divider.position.y = -0.47;
-scene.add(divider);
-
-// Particle system for arena atmosphere
-const pCount = 500;
-const pGeo = new THREE.BufferGeometry();
-const pPos = new Float32Array(pCount * 3);
-const pCol = new Float32Array(pCount * 3);
-for (let i = 0; i < pCount; i++) {
-  const r = Math.random() * 10;
-  const a = Math.random() * Math.PI * 2;
-  pPos[i*3] = Math.cos(a) * r;
-  pPos[i*3+1] = Math.random() * 8 - 0.5;
-  pPos[i*3+2] = Math.sin(a) * r;
-  const isAlpha = pPos[i*3] < 0;
-  pCol[i*3] = isAlpha ? 0.38 : 0.96;
-  pCol[i*3+1] = isAlpha ? 0.39 : 0.24;
-  pCol[i*3+2] = isAlpha ? 0.95 : 0.37;
-}
-pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
-pGeo.setAttribute('color', new THREE.BufferAttribute(pCol, 3));
-const pMat = new THREE.PointsMaterial({ size: 0.04, vertexColors: true, transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending });
-const particles = new THREE.Points(pGeo, pMat);
-scene.add(particles);
-
-// Team node storage
-const teamNodes = [];
-const ALPHA_COLOR = 0x6366f1;
-const BETA_COLOR = 0xf43f5e;
-const TEAM_POSITIONS = {
-  alpha: [[-3.5, 1, -1.5], [-2, 1, 2], [-4.5, 1, 1]],
-  beta:  [[3.5, 1, -1.5], [2, 1, 2], [4.5, 1, 1]]
-};
-
-function createTeamNode(pos, color, name, strategy) {
-  const group = new THREE.Group();
-  // Main sphere
-  const geo = new THREE.IcosahedronGeometry(0.6, 2);
-  const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.3, metalness: 0.7, roughness: 0.2, wireframe: false });
-  const sphere = new THREE.Mesh(geo, mat);
-  group.add(sphere);
-  // Glow ring
-  const glowGeo = new THREE.TorusGeometry(0.9, 0.03, 8, 64);
-  const glowMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.5 });
-  const glowRing = new THREE.Mesh(glowGeo, glowMat);
-  glowRing.rotation.x = Math.PI / 2;
-  group.add(glowRing);
-  // Outer shell wireframe
-  const shellGeo = new THREE.IcosahedronGeometry(0.75, 1);
-  const shellMat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.15 });
-  const shell = new THREE.Mesh(shellGeo, shellMat);
-  group.add(shell);
-  // Name label (canvas texture)
-  const canvas = document.createElement('canvas');
-  canvas.width = 256; canvas.height = 80;
-  const ctx = canvas.getContext('2d');
-  ctx.fillStyle = '#fff';
-  ctx.font = 'bold 28px Segoe UI';
-  ctx.textAlign = 'center';
-  ctx.fillText(name, 128, 30);
-  ctx.font = '16px Segoe UI';
-  ctx.fillStyle = '#888';
-  ctx.fillText(strategy, 128, 58);
-  const tex = new THREE.CanvasTexture(canvas);
-  const labelGeo = new THREE.PlaneGeometry(2, 0.6);
-  const labelMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthTest: false });
-  const label = new THREE.Mesh(labelGeo, labelMat);
-  label.position.y = 1.5;
-  group.add(label);
-  // P&L indicator bar (will be updated)
-  const barBgGeo = new THREE.PlaneGeometry(1.2, 0.08);
-  const barBgMat = new THREE.MeshBasicMaterial({ color: 0x1a1a2e, transparent: true, opacity: 0.8 });
-  const barBg = new THREE.Mesh(barBgGeo, barBgMat);
-  barBg.position.y = 1.1;
-  group.add(barBg);
-  const barGeo = new THREE.PlaneGeometry(0.01, 0.06);
-  const barMat = new THREE.MeshBasicMaterial({ color: 0x4ade80, transparent: true, opacity: 0.9 });
-  const bar = new THREE.Mesh(barGeo, barMat);
-  bar.position.y = 1.1;
-  bar.position.x = -0.58;
-  group.add(bar);
-  group.position.set(...pos);
-  scene.add(group);
-  return { group, sphere, shell, glowRing, bar, barMat, mat, name, pnl: 0, trades: 0 };
-}
-
-// Energy beams connecting team members
-function createTeamBeam(nodeA, nodeB, color) {
-  const geo = new THREE.BufferGeometry();
-  const positions = new Float32Array(6);
-  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
-  const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.15, blending: THREE.AdditiveBlending });
-  const line = new THREE.Line(geo, mat);
-  scene.add(line);
-  return { line, nodeA, nodeB, geo };
-}
-const beams = [];
-
-// Data fetching
-let tournamentData = null;
-async function fetchData() {
-  try {
-    const r = await fetch('/api/tournament/current');
-    const d = await r.json();
-    if (!d.active) return;
-    tournamentData = d;
-    updateScene(d);
-    updateHUD(d);
-  } catch(e) { console.error('Fetch error:', e); }
-}
-
-function updateScene(d) {
-  const entries = d.entries || [];
-  // First 3 = alpha, next 3 = beta (sorted by entry id)
-  const sorted = [...entries].sort((a,b) => a.id - b.id);
-  const alphaEntries = sorted.slice(0, 3);
-  const betaEntries = sorted.slice(3, 6);
-  // Create nodes if not yet created
-  if (teamNodes.length === 0) {
-    alphaEntries.forEach((e, i) => {
-      const n = createTeamNode(TEAM_POSITIONS.alpha[i], ALPHA_COLOR, e.name, e.strategy);
-      n.entry = e;
-      n.team = 'alpha';
-      teamNodes.push(n);
-    });
-    betaEntries.forEach((e, i) => {
-      const n = createTeamNode(TEAM_POSITIONS.beta[i], BETA_COLOR, e.name, e.strategy);
-      n.entry = e;
-      n.team = 'beta';
-      teamNodes.push(n);
-    });
-    // Create beams within each team
-    for (let i = 0; i < 3; i++) for (let j = i+1; j < 3; j++) {
-      beams.push(createTeamBeam(teamNodes[i], teamNodes[j], ALPHA_COLOR));
-    }
-    for (let i = 3; i < 6; i++) for (let j = i+1; j < 6; j++) {
-      beams.push(createTeamBeam(teamNodes[i], teamNodes[j], BETA_COLOR));
-    }
-  }
-  // Update node data
-  const allEntries = [...alphaEntries, ...betaEntries];
-  allEntries.forEach((e, i) => {
-    if (!teamNodes[i]) return;
-    const n = teamNodes[i];
-    n.pnl = e.live_pnl || 0;
-    n.trades = e.live_trades || 0;
-    n.entry = e;
-    // Update emissive based on P&L
-    const intensity = Math.min(0.8, 0.3 + Math.abs(n.pnl) / 500);
-    n.mat.emissiveIntensity = intensity;
-    // Update P&L bar
-    const pnlPct = Math.max(0.01, Math.min(1, (n.pnl + 2000) / 4000));
-    n.bar.scale.x = pnlPct * 116;
-    n.bar.position.x = -0.58 + (pnlPct * 1.16) / 2;
-    n.barMat.color.setHex(n.pnl >= 0 ? 0x4ade80 : 0xf87171);
-  });
-}
-
-function updateHUD(d) {
-  const entries = d.entries || [];
-  const sorted = [...entries].sort((a,b) => a.id - b.id);
-  const t = d.tournament;
-  const wStart = new Date(t.week_start).toLocaleDateString('en-US', {month:'short',day:'numeric'});
-  const wEnd = new Date(t.week_end).toLocaleDateString('en-US', {month:'short',day:'numeric'});
-  document.getElementById('week-info').textContent = 'Week ' + t.id + ' • ' + wStart + ' - ' + wEnd + ' • $' + (t.budget_per_portfolio_cents/100).toFixed(0) + ' per portfolio';
-  let html = '';
-  let alphaTotal = 0, betaTotal = 0;
-  sorted.forEach((e, i) => {
-    const isAlpha = i < 3;
-    const pnl = e.live_pnl || 0;
-    if (isAlpha) alphaTotal += pnl; else betaTotal += pnl;
-    const pnlClass = pnl > 0 ? 'pos' : pnl < 0 ? 'neg' : 'zero';
-    const pnlStr = (pnl >= 0 ? '+' : '') + '$' + (pnl/100).toFixed(2);
-    const budgetUsed = Math.min(100, (e.live_invested || 0) / e.start_balance_cents * 100);
-    const barColor = isAlpha ? '#818cf8' : '#fb7185';
-    html += '<div class="score-card ' + (isAlpha?'alpha':'beta') + '">';
-    html += '<div class="name">' + e.name + '</div>';
-    html += '<div class="strat">' + e.strategy + '</div>';
-    html += '<div class="pnl ' + pnlClass + '">' + pnlStr + '</div>';
-    html += '<div class="trades">' + (e.live_trades||0) + ' trades • $' + ((e.live_invested||0)/100).toFixed(2) + ' deployed</div>';
-    html += '<div class="bar"><div class="bar-fill" style="width:' + budgetUsed + '%;background:' + barColor + '"></div></div>';
-    html += '</div>';
-    if (i === 2) {
-      html += '<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;padding:0 8px">';
-      html += '<div style="font-size:20px;font-weight:900;background:linear-gradient(135deg,#818cf8,#fb7185);-webkit-background-clip:text;-webkit-text-fill-color:transparent">VS</div>';
-      html += '</div>';
-    }
-  });
-  document.getElementById('scoreboard').innerHTML = html;
-}
-
-// Animation
-let time = 0;
-function animate() {
-  requestAnimationFrame(animate);
-  time += 0.01;
-  // Rotate camera slowly
-  camera.position.x = Math.sin(time * 0.15) * 12;
-  camera.position.z = Math.cos(time * 0.15) * 12;
-  camera.position.y = 4 + Math.sin(time * 0.3) * 0.5;
-  camera.lookAt(0, 0.5, 0);
-  // Animate nodes
-  teamNodes.forEach((n, i) => {
-    const base = n.group.position.y;
-    n.sphere.position.y = Math.sin(time * 2 + i) * 0.15;
-    n.sphere.rotation.y = time + i;
-    n.shell.rotation.y = -time * 0.5 + i;
-    n.shell.rotation.x = time * 0.3;
-    n.glowRing.rotation.z = time * (i % 2 === 0 ? 1 : -1);
-    // Pulse based on trades
-    const pulse = 1 + Math.sin(time * 3 + i * 2) * 0.05 * (n.trades > 0 ? 1 : 0.2);
-    n.sphere.scale.setScalar(pulse);
-  });
-  // Update beams
-  beams.forEach(b => {
-    const pa = b.nodeA.group.position;
-    const pb = b.nodeB.group.position;
-    const pos = b.geo.attributes.position.array;
-    pos[0] = pa.x; pos[1] = pa.y + 1; pos[2] = pa.z;
-    pos[3] = pb.x; pos[4] = pb.y + 1; pos[5] = pb.z;
-    b.geo.attributes.position.needsUpdate = true;
-    b.line.material.opacity = 0.1 + Math.sin(time * 2) * 0.05;
-  });
-  // Animate particles
-  const pp = particles.geometry.attributes.position.array;
-  for (let i = 0; i < pCount; i++) {
-    pp[i*3+1] += Math.sin(time + i) * 0.002;
-    if (pp[i*3+1] > 8) pp[i*3+1] = -0.5;
-  }
-  particles.geometry.attributes.position.needsUpdate = true;
-  particles.rotation.y = time * 0.02;
-  // Ring pulse
-  ring.material.emissiveIntensity = 0.3 + Math.sin(time * 2) * 0.2;
-  renderer.render(scene, camera);
-}
-
-// Resize
-addEventListener('resize', () => {
-  camera.aspect = innerWidth / innerHeight;
-  camera.updateProjectionMatrix();
-  renderer.setSize(innerWidth, innerHeight);
-});
-
-// Init
-fetchData();
-setInterval(fetchData, 15000);
-animate();
-<\/script>
-</body>
-</html>`;
-
-// 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>
-<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
-<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.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)}
-
-/* Sparkline */
-.sparkline-wrap{display:inline-block;vertical-align:middle}
-.sparkline-svg{display:block}
-
-/* ─── Market Data Terminal ─── */
-.mkt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;margin-top:16px}
-.mkt-card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;backdrop-filter:blur(10px);transition:all .3s;position:relative;overflow:hidden}
-.mkt-card::after{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--border),transparent);opacity:0;transition:opacity .3s}
-.mkt-card:hover{border-color:rgba(255,255,255,.12);transform:translateY(-2px);box-shadow:0 12px 40px rgba(0,0,0,.3)}
-.mkt-card:hover::after{opacity:1}
-.mkt-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
-.mkt-card-title{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
-.mkt-card-badge{padding:3px 8px;border-radius:6px;font-size:10px;font-weight:700;font-variant-numeric:tabular-nums}
-.mkt-bar-chart{display:flex;align-items:flex-end;gap:3px;height:80px;padding-top:4px}
-.mkt-bar{flex:1;border-radius:3px 3px 0 0;min-width:4px;transition:height .5s ease;position:relative}
-.mkt-bar:hover{opacity:1!important;filter:brightness(1.3)}
-.mkt-bar-label{position:absolute;top:-16px;left:50%;transform:translateX(-50%);font-size:8px;color:var(--muted);white-space:nowrap;display:none;background:var(--card);padding:1px 4px;border-radius:3px;border:1px solid var(--border)}
-.mkt-bar:hover .mkt-bar-label{display:block}
-.mkt-source-list{display:flex;flex-direction:column;gap:5px}
-.mkt-source-row{display:flex;align-items:center;gap:8px;font-size:11px;padding:2px 0;transition:background .2s;border-radius:4px}
-.mkt-source-row:hover{background:rgba(255,255,255,.03)}
-.mkt-source-bar{height:7px;border-radius:4px;transition:width .6s cubic-bezier(.4,0,.2,1)}
-.mkt-source-name{min-width:85px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-.mkt-source-val{font-variant-numeric:tabular-nums;color:var(--muted);min-width:45px;text-align:right;font-weight:600}
-.mkt-gauge{position:relative;width:100%;height:14px;background:rgba(255,255,255,.05);border-radius:7px;overflow:hidden;margin-top:8px}
-.mkt-gauge-fill{height:100%;border-radius:7px;transition:width .6s cubic-bezier(.4,0,.2,1)}
-.mkt-gauge-label{position:absolute;top:50%;right:8px;transform:translateY(-50%);font-size:9px;font-weight:700;color:var(--text);text-shadow:0 1px 2px rgba(0,0,0,.5)}
-/* Scrolling ticker */
-.mkt-ticker-strip{overflow:hidden;padding:12px 0;border-top:1px solid rgba(255,255,255,.06);border-bottom:1px solid rgba(255,255,255,.06);margin:16px 0;background:linear-gradient(90deg,rgba(5,5,16,.8),transparent 5%,transparent 95%,rgba(5,5,16,.8))}
-.mkt-ticker-track{display:flex;gap:24px;animation:tickerScroll 40s linear infinite;width:max-content}
-.mkt-ticker-track:hover{animation-play-state:paused}
-@keyframes tickerScroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
-.mkt-ticker-item{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums;padding:4px 12px;border-radius:6px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.04)}
-.mkt-change{font-size:11px;font-weight:700}
-.mkt-change.up{color:var(--green)}
-.mkt-change.down{color:var(--red)}
-.mkt-change.flat{color:var(--muted)}
-/* Signal feed */
-.signal-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto}
-.signal-feed::-webkit-scrollbar{width:4px}
-.signal-feed::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.signal-row{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;font-size:11px;transition:all .2s;border:1px solid transparent;animation:signalIn .3s ease}
-.signal-row:hover{background:rgba(255,255,255,.04);border-color:var(--border)}
-@keyframes signalIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
-.signal-dir{padding:2px 8px;border-radius:4px;font-weight:800;font-size:10px;text-transform:uppercase;letter-spacing:.5px}
-.signal-dir.yes{background:rgba(34,197,94,.15);color:var(--green)}
-.signal-dir.no{background:rgba(239,68,68,.15);color:var(--red)}
-.signal-market{font-weight:600;min-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-.signal-edge{font-variant-numeric:tabular-nums;font-weight:700;min-width:50px;text-align:right}
-.signal-conf{font-variant-numeric:tabular-nums;min-width:40px;text-align:right;color:var(--muted)}
-.signal-time{font-size:9px;color:var(--muted);margin-left:auto;white-space:nowrap}
-.signal-approved{width:6px;height:6px;border-radius:50%;flex-shrink:0}
-/* Heatmap grid */
-.heatmap-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px}
-.heatmap-cell{padding:8px 10px;border-radius:8px;border:1px solid var(--border);transition:all .2s;cursor:default;text-align:center}
-.heatmap-cell:hover{transform:scale(1.03);z-index:1;box-shadow:0 4px 16px rgba(0,0,0,.4)}
-.heatmap-market{font-size:10px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:2px}
-.heatmap-edge{font-size:16px;font-weight:800;font-variant-numeric:tabular-nums}
-.heatmap-meta{font-size:8px;color:var(--muted);margin-top:2px}
-/* Histogram */
-.histogram{display:flex;align-items:flex-end;gap:2px;height:80px}
-.hist-bar{flex:1;border-radius:3px 3px 0 0;transition:height .5s;position:relative;min-width:0}
-.hist-bar:hover{filter:brightness(1.3)}
-.hist-label{position:absolute;bottom:-16px;left:50%;transform:translateX(-50%);font-size:7px;color:var(--muted);white-space:nowrap}
-/* Trade log */
-.trade-log{display:flex;flex-direction:column;gap:3px;max-height:260px;overflow-y:auto}
-.trade-log::-webkit-scrollbar{width:4px}
-.trade-log::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.trade-row{display:flex;align-items:center;gap:6px;padding:5px 10px;border-radius:6px;font-size:10px;border:1px solid transparent;transition:all .2s}
-.trade-row:hover{background:rgba(255,255,255,.03);border-color:var(--border)}
-.trade-portfolio{font-weight:600;min-width:70px}
-.trade-pnl{font-weight:800;font-variant-numeric:tabular-nums;min-width:55px;text-align:right}
-/* P&L big number */
-.pnl-hero{font-size:48px;font-weight:900;font-variant-numeric:tabular-nums;letter-spacing:-1px;line-height:1}
-@keyframes pnlPulse{0%,100%{text-shadow:0 0 8px currentColor}50%{text-shadow:0 0 20px currentColor}}
-
-/* ─── PortFAUXlio Dashboard ─── */
-.pf-banner{text-align:center;padding:14px;margin-bottom:20px;background:linear-gradient(135deg,rgba(139,92,246,.08),rgba(255,105,180,.06));border:1px solid rgba(139,92,246,.2);border-radius:14px;position:relative;overflow:hidden}
-.pf-banner::before{content:'SIMULATED';position:absolute;top:-2px;right:20px;font-size:8px;letter-spacing:3px;color:var(--purple);background:var(--bg);padding:0 8px;border-radius:0 0 4px 4px;border:1px solid rgba(139,92,246,.2);border-top:none;font-weight:800}
-.pf-banner-title{font-size:22px;font-weight:900;background:linear-gradient(135deg,#8b5cf6,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:1px}
-.pf-banner-sub{font-size:10px;color:var(--muted);letter-spacing:2px;text-transform:uppercase;margin-top:4px}
-
-.pf-summary-row{display:flex;gap:10px;margin-bottom:18px;flex-wrap:wrap}
-.pf-summary-card{flex:1;min-width:100px;background:var(--card);border:1px solid var(--border);border-radius:12px;padding:14px;text-align:center}
-.pf-summary-val{font-size:22px;font-weight:800;font-variant-numeric:tabular-nums}
-.pf-summary-lbl{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
-
-/* Active Trades Strip */
-.pf-active-strip{margin-bottom:20px}
-.pf-active-header{display:flex;align-items:center;gap:8px;margin-bottom:10px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--green)}
-.pf-active-header .dot{width:8px;height:8px;border-radius:50%;background:var(--green);animation:statusPulse 2s ease infinite}
-.pf-active-scroll{display:flex;gap:8px;overflow-x:auto;padding-bottom:8px;scroll-behavior:smooth}
-.pf-active-scroll::-webkit-scrollbar{height:4px}
-.pf-active-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.pf-active-card{flex-shrink:0;min-width:200px;max-width:260px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 14px;transition:all .2s;border-top:2px solid var(--border)}
-.pf-active-card:hover{border-color:rgba(255,255,255,.15);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.3)}
-
-/* Model Leaderboard Table */
-.pf-leaderboard{margin-bottom:20px;border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--card);max-height:800px;overflow-y:auto}
-.pf-leaderboard::-webkit-scrollbar{width:4px}
-.pf-leaderboard::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.pf-lb-header{display:grid;grid-template-columns:32px 100px 90px 80px 70px 40px 40px 55px 80px 150px;gap:0;padding:10px 14px;background:rgba(255,255,255,.03);border-bottom:1px solid var(--border);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
-.pf-lb-header span[style*="cursor"]:hover{color:var(--cyan)}
-.pf-lb-row{display:grid;grid-template-columns:32px 100px 90px 80px 70px 40px 40px 55px 80px 150px;gap:0;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.03);font-size:11px;transition:all .2s;cursor:pointer;align-items:center}
-/* Chart Modal */
-.chart-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:1000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);animation:modalFadeIn .3s ease}
-.chart-modal-content{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:24px;width:92%;max-width:900px}
-@keyframes modalFadeIn{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}
-.chart-modal-header{display:flex;align-items:center;gap:10px;margin-bottom:16px}
-.chart-close{margin-left:auto;background:none;border:1px solid var(--border);color:var(--muted);border-radius:8px;padding:4px 12px;cursor:pointer;font-size:12px}
-.chart-close:hover{color:var(--text);border-color:var(--muted)}
-.chart-period-btns{display:flex;gap:4px;margin-left:auto}
-.chart-period,.chart-period-active{padding:3px 10px;border-radius:6px;font-size:10px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:transparent;color:var(--muted);transition:all .2s}
-.chart-period-active{border-color:var(--cyan);background:rgba(6,182,212,.1);color:var(--cyan)}
-.pf-lb-chart{display:flex;align-items:center;cursor:pointer;opacity:.85;transition:opacity .2s}
-.pf-lb-chart:hover{opacity:1}
-/* SWOT Trade Analysis */
-.swot-section{margin-bottom:20px}
-.swot-card{background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-bottom:12px;transition:all .2s}
-.swot-card:hover{border-color:rgba(255,255,255,.1)}
-.swot-card-header{display:flex;align-items:center;gap:10px;padding:14px 16px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.03)}
-.swot-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px 16px}
-.swot-quadrant{padding:10px;border-radius:10px;font-size:10px}
-.swot-quadrant-s{background:rgba(34,197,94,.06);border:1px solid rgba(34,197,94,.15)}
-.swot-quadrant-w{background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.15)}
-.swot-quadrant-o{background:rgba(59,130,246,.06);border:1px solid rgba(59,130,246,.15)}
-.swot-quadrant-t{background:rgba(245,158,11,.06);border:1px solid rgba(245,158,11,.15)}
-.swot-q-title{font-weight:800;font-size:9px;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
-.swot-q-item{padding:2px 0;color:var(--muted);line-height:1.4}
-.swot-q-item::before{content:'• '}
-.swot-models{display:flex;flex-wrap:wrap;gap:4px;padding:8px 16px 14px}
-.swot-model-tag{padding:2px 8px;border-radius:4px;font-size:9px;font-weight:600;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06)}
-.pf-lb-row:hover{background:rgba(255,255,255,.03)}
-.pf-lb-row:last-child{border-bottom:none}
-.pf-lb-rank{font-size:14px;font-weight:900;color:var(--muted)}
-.pf-lb-name{font-weight:700;display:flex;align-items:center;gap:6px;overflow:hidden}
-.pf-lb-pnl{font-weight:800;font-variant-numeric:tabular-nums}
-.pf-lb-invested{font-variant-numeric:tabular-nums;color:var(--muted)}
-.pf-lb-wr{font-weight:700;font-variant-numeric:tabular-nums}
-.pf-lb-wl{font-variant-numeric:tabular-nums;color:var(--muted)}
-.pf-lb-roi{font-weight:800;font-variant-numeric:tabular-nums;padding:2px 8px;border-radius:4px;text-align:center}
-.pf-lb-bar{height:4px;border-radius:2px;background:rgba(255,255,255,.05);overflow:hidden;margin-top:2px}
-.pf-lb-bar-fill{height:100%;border-radius:2px;transition:width .6s ease}
-
-/* Overlap Warning */
-.pf-overlap{margin-bottom:18px;background:rgba(245,158,11,.04);border:1px solid rgba(245,158,11,.15);border-radius:12px;padding:14px}
-.pf-overlap-title{font-size:11px;font-weight:700;color:var(--yellow);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;display:flex;align-items:center;gap:6px}
-
-/* Trade History */
-.pf-history{border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--card)}
-.pf-history-header{padding:14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border)}
-.pf-history-list{max-height:400px;overflow-y:auto}
-.pf-history-list::-webkit-scrollbar{width:4px}
-.pf-history-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
-.pf-history-row{display:flex;align-items:center;gap:8px;padding:8px 14px;font-size:11px;border-bottom:1px solid rgba(255,255,255,.02);transition:all .15s}
-.pf-history-row:hover{background:rgba(255,255,255,.03)}
-.pf-history-row:last-child{border-bottom:none}
-
-@media(max-width:900px){
-.pf-lb-header,.pf-lb-row{grid-template-columns:28px 80px 70px 70px 50px 40px 40px 50px 70px;font-size:10px;padding:8px 10px}
-.pf-active-card{min-width:180px}
-}
-
-/* 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;
-
-// SECURITY (audit 2026-05-04): inline literal credentials shipped to every
-// browser via this <script> block. Removed. Dashboard MUST be refactored to
-// use the server-side session cookie (`kalshi_session`) for auth — the cookie
-// is already set on login and Express forwards it on subsequent requests.
-// Until refactor: dashboard fetches will 401 visibly, signaling the work needed.
-const AUTH = '';
-const api = (url) => fetch(url, { credentials: 'include' }).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 ? '✓' : '✗'}
-          </div>
-          <div className="signal-time">{timeAgo(s.time)}</div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-// ── PORTFOLIOS TAB ──
-function PortfoliosTab() {
-  const [data, setData] = useState(null);
-  const [histFilter, setHistFilter] = useState('all');
-  const [chartData, setChartData] = useState({});
-  const [selectedChart, setSelectedChart] = useState(null);
-  const [compareMode, setCompareMode] = useState(false);
-  const [compareIds, setCompareIds] = useState([]);
-  const [sortBy, setSortBy] = useState('pnl');
-  const [sortDir, setSortDir] = useState('desc');
-  useEffect(() => {
-    const load = () => api('/api/portfauxlio').then(setData).catch(e => console.error(e));
-    load();
-    const iv = setInterval(load, 15000);
-    return () => clearInterval(iv);
-  }, []);
-  useEffect(() => {
-    const loadCharts = () => api('/api/portfauxlio/charts?period=7d').then(d => {
-      const byId = {};
-      (d.portfolios || []).forEach(p => { byId[p.id] = p; });
-      setChartData(byId);
-    }).catch(() => {});
-    loadCharts();
-    const cv = setInterval(loadCharts, 120000);
-    return () => clearInterval(cv);
-  }, []);
-  const [swotData, setSwotData] = useState(null);
-  const [swotExpanded, setSwotExpanded] = useState({});
-  useEffect(() => {
-    const loadSwot = () => api('/api/portfauxlio/swot').then(setSwotData).catch(() => {});
-    loadSwot();
-    const sv = setInterval(loadSwot, 60000);
-    return () => clearInterval(sv);
-  }, []);
-
-  const stratColors = {aggressive:'#ef4444',conservative:'#3b82f6',weather:'#06b6d4',momentum:'#f59e0b',contrarian:'#8b5cf6',ai_enhanced:'#ec4899',mc_validated:'#a855f7',scalper:'#eab308',patient:'#22c55e',random:'#64748b',penny_pincher:'#94a3b8',spray_pray:'#fb923c',nickel_slots:'#a78bfa',degen_lite:'#f43f5e',politics_junkie:'#0ea5e9',double_down:'#dc2626',volatility_rider:'#14b8a6',whale:'#1d4ed8',yolo:'#e11d48',full_send:'#b91c1c',kelly_criterion:'#059669',sharpe_sniper:'#7c3aed',bayesian_blend:'#0891b2',ensemble_lock:'#c026d3',mean_revert:'#d97706',info_ratio:'#2563eb',anti_fomo:'#16a34a',nightcrawler:'#4c1d95',heatseeker:'#ea580c',quant_core:'#0d9488',etf_presidential:'#dc143c',etf_cabinet:'#ff6347',etf_congress:'#4169e1',etf_scotus:'#2f4f4f',etf_fed:'#228b22',etf_economy:'#daa520',etf_geopolitics:'#8b0000',etf_territory:'#006400',etf_climate:'#00bfff',etf_energy:'#ff8c00',etf_tech:'#7b68ee',etf_space:'#191970',etf_crypto:'#ffd700',etf_sports:'#32cd32',etf_entertainment:'#ff1493',etf_legal:'#708090',etf_financials:'#4682b4',etf_govreform:'#b22222',etf_global_social:'#9370db',etf_broad_market:'#c0a050'};
-  const stratEmojis = {aggressive:'🔥',conservative:'🏛️',weather:'⛈️',momentum:'🚀',contrarian:'🔄',ai_enhanced:'🧠',mc_validated:'🎲',scalper:'⚡',patient:'🐢',random:'🎰',penny_pincher:'🪙',spray_pray:'💦',nickel_slots:'🎰',degen_lite:'😈',politics_junkie:'🏛️',double_down:'⬆️',volatility_rider:'🏄',whale:'🐳',yolo:'🧨',full_send:'💣',kelly_criterion:'🎰',sharpe_sniper:'🎯',bayesian_blend:'🧮',ensemble_lock:'🔐',mean_revert:'↩️',info_ratio:'📊',anti_fomo:'🧘',nightcrawler:'🦇',heatseeker:'🔬',quant_core:'∑',etf_presidential:'🏛️',etf_cabinet:'💼',etf_congress:'🗳️',etf_scotus:'⚖️',etf_fed:'🏦',etf_economy:'📈',etf_geopolitics:'🌍',etf_territory:'🗺️',etf_climate:'🌡️',etf_energy:'⛽',etf_tech:'🤖',etf_space:'🚀',etf_crypto:'₿',etf_sports:'⚽',etf_entertainment:'🎬',etf_legal:'🔨',etf_financials:'💹',etf_govreform:'✂️',etf_global_social:'⛪',etf_broad_market:'🌐'};
-  const stratNames = {aggressive:'Alpha',conservative:'Beta',weather:'Storm',momentum:'Momentum',contrarian:'Contrarian',ai_enhanced:'Nova',mc_validated:'Oracle',scalper:'Shark',patient:'Turtle',random:'Wildcard',penny_pincher:'Penny',spray_pray:'Spray',nickel_slots:'Nickel',degen_lite:'Degen',politics_junkie:'Capitol',double_down:'Doubler',volatility_rider:'Rider',whale:'Moby',yolo:'YOLO',full_send:'Cannon',kelly_criterion:'Kelly',sharpe_sniper:'Sniper',bayesian_blend:'Bayes',ensemble_lock:'Voltron',mean_revert:'Revert',info_ratio:'Signal',anti_fomo:'Zen',nightcrawler:'Shadow',heatseeker:'Heatseek',quant_core:'Quant',etf_presidential:'PRES',etf_cabinet:'Cabinet',etf_congress:'Congress',etf_scotus:'SCOTUS',etf_fed:'Fed',etf_economy:'Economy',etf_geopolitics:'Geo',etf_territory:'Territory',etf_climate:'Climate',etf_energy:'Energy',etf_tech:'Tech',etf_space:'Space',etf_crypto:'Crypto',etf_sports:'Sports',etf_entertainment:'Showbiz',etf_legal:'Legal',etf_financials:'Finance',etf_govreform:'GovReform',etf_global_social:'Global',etf_broad_market:'Broad Mkt'};
-  const stratDesc = {aggressive:'High edge (>15%), low confidence — speculative swings',conservative:'High confidence (>70%) — quality over quantity',weather:'Weather/climate markets only',momentum:'High agreement + articles — follows the trend',contrarian:'BUY_NO only — bets against the crowd',ai_enhanced:'Gemini AI enhanced signals only',mc_validated:'Monte Carlo validated, tight spread',scalper:'Small edges (3-10%) — quantity plays',patient:'Needs everything: conf>80%, MC, 5+ articles',random:'20% random — control group for luck testing',penny_pincher:'Any edge >=1% — micro bets, max volume',spray_pray:'Random 50% of all signals — luck vs skill',nickel_slots:'Edge >=2% + conf >=20% — low bar catcher',degen_lite:'Edge >=5%, ignores confidence — pure edge',politics_junkie:'Political markets only — elections/congress',double_down:'Edge >=20% — goes big on massive edges',volatility_rider:'High MC stddev + edge — volatile markets',whale:'Edge >=25% + conf >=60% — rare massive sniper',yolo:'Edge >=30% — max degen, no filters',full_send:'Takes EVERY signal — pipeline profitability test',kelly_criterion:'Kelly fraction: edge/(1+edge) — bankroll math',sharpe_sniper:'Signal-to-noise ratio >2.0 — edge/stddev',bayesian_blend:'Bayesian posterior: conf * evidence * edge',ensemble_lock:'ALL must align: AI + MC + conf + edge + articles',mean_revert:'High volatility + moderate edge — reversion play',info_ratio:'(articles * agreement) / noise — quality intel',anti_fomo:'Small edge + high conf — calm, certain plays',nightcrawler:'BUY_NO + AI + MC — sophisticated shorts',heatseeker:'Multi-factor composite: 40e + 30c + 20a + 10n',quant_core:'Geometric mean sqrt(edge*conf) — pure math',etf_presidential:'Presidential election & 2028 race markets',etf_cabinet:'Cabinet confirmations & departures',etf_congress:'House, Senate & legislative markets',etf_scotus:'Supreme Court rulings & retirements',etf_fed:'Federal Reserve, rates & monetary policy',etf_economy:'GDP, jobs & economic indicators',etf_geopolitics:'Wars, treaties, sanctions & foreign affairs',etf_territory:'Greenland, statehood & territorial expansion',etf_climate:'Weather, warming & climate events',etf_energy:'LNG, oil, power grid & energy commodities',etf_tech:'AI, fusion, tech companies & semiconductors',etf_space:'SpaceX, NASA, Mars & Moon missions',etf_crypto:'Bitcoin, Ethereum & digital assets',etf_sports:'NBA, NFL, MLB & sports outcomes',etf_entertainment:'Celebrity, media, awards & pop culture',etf_legal:'Pardons, impeachment & legal proceedings',etf_financials:'Bonds, yields & financial markets',etf_govreform:'DOGE, agency cuts & government reform',etf_global_social:'Pope, referendums & global social movements',etf_broad_market:'Index fund — high-quality signals all sectors'};
-
-  if (!data) return <div style={{textAlign:'center',padding:60,color:'var(--muted)'}}>Loading portFAUXlio data...</div>;
-
-  const { portfolios: rawPortfolios, activeTrades, tradeHistory, overlaps, summary } = data;
-  const maxInvested = Math.max(...rawPortfolios.map(p => parseInt(p.total_invested_cents) || 0), 1);
-  const filteredHistory = histFilter === 'all' ? tradeHistory : tradeHistory.filter(t => t.strategy === histFilter);
-
-  const sortFns = {
-    pnl: p => parseInt(p.pnl_cents) || 0,
-    roi: p => parseFloat(p.roi_pct) || 0,
-    invested: p => parseInt(p.total_invested_cents) || 0,
-    trades: p => (parseInt(p.trades_won)||0) + (parseInt(p.trades_lost)||0) + (parseInt(p.trades_expired)||0),
-    wins: p => parseInt(p.trades_won) || 0,
-    name: p => (p.name || '').toLowerCase(),
-    strategy: p => (p.strategy || '').toLowerCase(),
-  };
-  const getSortVal = sortFns[sortBy] || sortFns.pnl;
-  const portfolios = [...rawPortfolios].sort((a, b) => {
-    const av = getSortVal(a), bv = getSortVal(b);
-    if (typeof av === 'string') return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
-    return sortDir === 'desc' ? bv - av : av - bv;
-  });
-  const toggleSort = (col) => { if (sortBy === col) setSortDir(d => d === 'desc' ? 'asc' : 'desc'); else { setSortBy(col); setSortDir('desc'); } };
-
-  return (<div>
-    {/* ═══ Banner ═══ */}
-    <div className="pf-banner">
-      <div className="pf-banner-title">portFAUXlio</div>
-      <div className="pf-banner-sub">50 Strategy Models &bull; Paper Trading &bull; Real Signals &bull; Zero Real Money</div>
-    </div>
-
-    {/* ═══ Summary Stats ═══ */}
-    <div className="pf-summary-row">
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color: summary.totalPnl >= 0 ? 'var(--green)' : 'var(--red)'}}>
-          {summary.totalPnl >= 0 ? '+' : ''}{'$'}{(summary.totalPnl / 100).toFixed(2)}
-        </div>
-        <div className="pf-summary-lbl">Total P&L</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--blue)'}}>{'$'}{(summary.totalInvested / 100).toFixed(2)}</div>
-        <div className="pf-summary-lbl">Invested</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--purple)'}}>{summary.totalTrades}</div>
-        <div className="pf-summary-lbl">Total Trades</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--green)'}}>{summary.openTrades}</div>
-        <div className="pf-summary-lbl">Open Positions</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--cyan)'}}>{summary.uniqueMarkets}</div>
-        <div className="pf-summary-lbl">Markets</div>
-      </div>
-      <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--yellow)'}}>{summary.activeModels}/{portfolios ? portfolios.length : 50}</div>
-        <div className="pf-summary-lbl">Active Models</div>
-      </div>
-    </div>
-
-    {/* ═══ Active Trades Strip ═══ */}
-    {activeTrades.length > 0 && <div className="pf-active-strip">
-      <div className="pf-active-header">
-        <span className="dot" />
-        {activeTrades.length} Open Positions
-      </div>
-      <div className="pf-active-scroll">
-        {activeTrades.map((t, i) => {
-          const c = stratColors[t.strategy] || 'var(--muted)';
-          return (<div key={i} className="pf-active-card" style={{borderTopColor: c}}>
-            <div style={{display:'flex',alignItems:'center',gap:6,marginBottom:4}}>
-              <span style={{fontSize:14}}>{stratEmojis[t.strategy] || ''}</span>
-              <span style={{fontSize:11,fontWeight:700,color:c}}>{t.portfolio_name}</span>
-              <span style={{marginLeft:'auto',padding:'1px 6px',borderRadius:4,fontSize:9,fontWeight:700,
-                background: t.direction === 'BUY_YES' ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)',
-                color: t.direction === 'BUY_YES' ? 'var(--green)' : 'var(--red)'
-              }}>{t.direction === 'BUY_YES' ? 'YES' : 'NO'}</span>
-            </div>
-            <div style={{fontSize:10,fontWeight:600,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',marginBottom:3}} title={t.market_title || t.market_id}>
-              {t.market_title || t.market_id}
-            </div>
-            <div style={{display:'flex',justifyContent:'space-between',fontSize:10,color:'var(--muted)'}}>
-              <span>{t.contracts}x @ {t.entry_price_cents}¢</span>
-              <span>{'$'}{(t.cost_cents / 100).toFixed(2)}</span>
-              <span>{timeAgo(t.created_at)}</span>
-            </div>
-          </div>);
-        })}
-      </div>
-    </div>}
-
-    {/* ═══ Model Leaderboard ═══ */}
-    <div style={{marginBottom:18}}>
-      <div style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,marginBottom:10,color:'var(--pink)',display:'flex',alignItems:'center',gap:8}}>
-        Model Leaderboard
-        <span style={{fontSize:9,fontWeight:400,color:'var(--muted)',textTransform:'none',letterSpacing:0}}>ranked by {sortBy}</span>
-        <div style={{marginLeft:'auto',display:'flex',gap:6}}>
-          {compareMode && compareIds.length >= 2 && <button onClick={() => setCompareMode('show')} style={{padding:'3px 10px',borderRadius:6,fontSize:9,fontWeight:700,background:'rgba(6,182,212,.15)',border:'1px solid var(--cyan)',color:'var(--cyan)',cursor:'pointer'}}>
-            Compare {compareIds.length} →
-          </button>}
-          <button onClick={() => { if (compareMode) { setCompareMode(false); setCompareIds([]); } else setCompareMode(true); }} style={{padding:'3px 10px',borderRadius:6,fontSize:9,fontWeight:700,background:compareMode?'rgba(168,85,247,.15)':'transparent',border:'1px solid '+(compareMode?'var(--purple)':'var(--border)'),color:compareMode?'var(--purple)':'var(--muted)',cursor:'pointer'}}>
-            {compareMode ? '✕ Cancel' : '📊 Compare'}
-          </button>
-        </div>
-      </div>
-      <div className="pf-leaderboard">
-        <div className="pf-lb-header">
-          <span>#</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('name')}>Model {sortBy==='name' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('pnl')}>P&L {sortBy==='pnl' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('invested')}>Invested {sortBy==='invested' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('roi')}>ROI {sortBy==='roi' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('wins')}>W {sortBy==='wins' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span>L</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('trades')}>Trades {sortBy==='trades' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span style={{cursor:'pointer'}} onClick={() => toggleSort('strategy')}>Strategy {sortBy==='strategy' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
-          <span>Chart</span>
-        </div>
-        {portfolios.map((p, i) => {
-          const c = stratColors[p.strategy] || 'var(--muted)';
-          const pnl = parseInt(p.pnl_cents) || 0;
-          const invested = parseInt(p.total_invested_cents) || 0;
-          const resolved = parseInt(p.resolved_trades) || 0;
-          const roi = parseFloat(p.roi_pct) || 0;
-          const totalT = (parseInt(p.trades_won) || 0) + (parseInt(p.trades_lost) || 0) + (parseInt(p.trades_expired) || 0);
-          const openCount = activeTrades.filter(t => t.portfolio_id === p.id).length;
-          const medals = ['🥇','🥈','🥉'];
-          const cd = chartData[p.id];
-          const miniPts = cd ? (cd.snapshots || []).map(s => s.nav / 100) : (cd?.daily || []).map(d => d.cumulative / 100);
-          return (<div key={p.id} className="pf-lb-row" style={compareMode && compareIds.includes(p.id) ? {background:'rgba(168,85,247,.08)',borderLeft:'3px solid var(--purple)'} : {}} onClick={() => {
-            if (compareMode) { setCompareIds(prev => prev.includes(p.id) ? prev.filter(x => x !== p.id) : [...prev, p.id]); }
-            else if (cd) setSelectedChart({id:p.id,strategy:p.strategy,name:p.name,color:c});
-          }}>
-            <span className="pf-lb-rank">{i < 3 ? medals[i] : (i+1)}</span>
-            <span className="pf-lb-name" style={{color:c}}>
-              <span style={{fontSize:16}}>{stratEmojis[p.strategy]||''}</span>
-              {p.name}
-            </span>
-            <span className="pf-lb-pnl" style={{color: pnl >= 0 ? 'var(--green)' : 'var(--red)'}}>
-              {pnl >= 0 ? '+' : ''}{'$'}{(pnl / 100).toFixed(2)}
-            </span>
-            <span className="pf-lb-invested">{'$'}{(invested / 100).toFixed(2)}</span>
-            <span className="pf-lb-roi" style={{
-              background: roi > 0 ? 'rgba(34,197,94,.12)' : roi < 0 ? 'rgba(239,68,68,.12)' : 'rgba(255,255,255,.04)',
-              color: roi > 0 ? 'var(--green)' : roi < 0 ? 'var(--red)' : 'var(--muted)'
-            }}>{roi > 0 ? '+' : ''}{roi}%</span>
-            <span className="pf-lb-wl" style={{color:'var(--green)'}}>{p.trades_won}</span>
-            <span className="pf-lb-wl" style={{color:'var(--red)'}}>{p.trades_lost}</span>
-            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>
-              {openCount > 0 ? <span>{openCount} <span style={{fontSize:8,color:'var(--green)'}}>OPEN</span></span> : <span style={{color:'var(--muted)'}}>0</span>}
-              {resolved > 0 && <span style={{marginLeft:4,fontSize:9}}>+{resolved}</span>}
-            </span>
-            <span style={{fontSize:9,color:'var(--muted)',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={stratDesc[p.strategy]}>
-              {p.strategy}
-            </span>
-            <span style={{display:'flex',alignItems:'center',justifyContent:'center'}}>
-              {miniPts && miniPts.length >= 2 ? <MiniChart data={miniPts} color={c} /> : <span style={{fontSize:9,color:'var(--muted)'}}>--</span>}
-            </span>
-          </div>);
-        })}
-      </div>
-    </div>
-
-    {/* ═══ Overlap Analysis ═══ */}
-    {overlaps.length > 0 && <div className="pf-overlap">
-      <div className="pf-overlap-title">
-        ⚠️ Strategy Overlap — Markets with 2+ models
-      </div>
-      <div style={{display:'flex',flexDirection:'column',gap:4}}>
-        {overlaps.slice(0, 8).map((o, i) => (
-          <div key={i} style={{display:'flex',alignItems:'center',gap:8,fontSize:10,padding:'4px 0'}}>
-            <span style={{fontWeight:700,color:'var(--yellow)',minWidth:20}}>{o.num_strategies}x</span>
-            <span style={{fontWeight:600,minWidth:180,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{o.market_id}</span>
-            <span style={{color:'var(--muted)',flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
-              {(o.portfolios || []).join(', ')}
-            </span>
-            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>{'$'}{((o.total_exposure || 0) / 100).toFixed(2)}</span>
-          </div>
-        ))}
-      </div>
-    </div>}
-
-    {/* ═══ SWOT Trade Intelligence ═══ */}
-    {swotData && swotData.markets && swotData.markets.length > 0 && <div className="swot-section">
-      <div style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,marginBottom:12,color:'var(--purple)',display:'flex',alignItems:'center',gap:8}}>
-        Trade Intelligence & SWOT
-        <span style={{fontSize:9,fontWeight:400,color:'var(--muted)',textTransform:'none',letterSpacing:0}}>{swotData.markets.length} active markets &bull; {swotData.total_trades} trades this week</span>
-      </div>
-      {swotData.markets.map((m, idx) => {
-        const isOpen = swotExpanded[m.market_id];
-        return (<div key={m.market_id} className="swot-card">
-          <div className="swot-card-header" onClick={() => setSwotExpanded(prev => ({...prev, [m.market_id]: !prev[m.market_id]}))}>
-            <span style={{fontSize:16,fontWeight:900,color:'var(--muted)',minWidth:24}}>{idx+1}</span>
-            <span style={{padding:'2px 8px',borderRadius:4,fontSize:9,fontWeight:800,
-              background: m.direction === 'BUY_YES' ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)',
-              color: m.direction === 'BUY_YES' ? 'var(--green)' : 'var(--red)'
-            }}>{m.direction === 'BUY_YES' ? 'YES' : 'NO'}</span>
-            <span style={{flex:1,fontWeight:700,fontSize:11,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={m.title}>{m.title}</span>
-            <span style={{fontSize:10,color:'var(--cyan)',fontWeight:700}}>{m.model_count} models</span>
-            <span style={{fontSize:10,fontWeight:700,color:m.edge >= 10 ? 'var(--green)' : 'var(--yellow)'}}>{m.edge.toFixed(1)}% edge</span>
-            <span style={{fontSize:10,fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>{'$'}{(m.total_cost / 100).toFixed(2)}</span>
-            <span style={{fontSize:10,fontWeight:700,color: m.open_count > 0 ? 'var(--green)' : 'var(--muted)'}}>{m.open_count > 0 ? m.open_count + ' OPEN' : 'CLOSED'}</span>
-            <span style={{fontSize:14,color:'var(--muted)',transform:isOpen?'rotate(180deg)':'rotate(0)',transition:'transform .2s'}}>▼</span>
-          </div>
-          {isOpen && <div>
-            <div className="swot-grid">
-              <div className="swot-quadrant swot-quadrant-s">
-                <div className="swot-q-title" style={{color:'var(--green)'}}>Strengths</div>
-                {m.swot.strengths.map((s, i) => <div key={i} className="swot-q-item">{s}</div>)}
-              </div>
-              <div className="swot-quadrant swot-quadrant-w">
-                <div className="swot-q-title" style={{color:'var(--red)'}}>Weaknesses</div>
-                {m.swot.weaknesses.map((w, i) => <div key={i} className="swot-q-item">{w}</div>)}
-              </div>
-              <div className="swot-quadrant swot-quadrant-o">
-                <div className="swot-q-title" style={{color:'var(--blue)'}}>Opportunities</div>
-                {m.swot.opportunities.map((o, i) => <div key={i} className="swot-q-item">{o}</div>)}
-              </div>
-              <div className="swot-quadrant swot-quadrant-t">
-                <div className="swot-q-title" style={{color:'var(--yellow)'}}>Threats</div>
-                {m.swot.threats.map((t, i) => <div key={i} className="swot-q-item">{t}</div>)}
-              </div>
-            </div>
-            <div style={{padding:'8px 16px',fontSize:10,color:'var(--muted)',lineHeight:1.5,borderTop:'1px solid rgba(255,255,255,.03)'}}>
-              <strong style={{color:'var(--text)'}}>Signal:</strong> {m.reasoning}
-            </div>
-            <div style={{padding:'4px 16px 6px',fontSize:10,color:'var(--muted)',display:'flex',gap:16,flexWrap:'wrap'}}>
-              <span>Conf: <strong style={{color:'var(--cyan)'}}>{Math.round(m.confidence * 100)}%</strong></span>
-              <span>Entry: <strong>{m.entry_price}¢</strong></span>
-              {m.mc_mean > 0 && <span>MC: <strong>{m.mc_mean.toFixed(1)}% ±{m.mc_std.toFixed(1)}%</strong></span>}
-              {m.articles > 0 && <span>News: <strong>{m.articles} articles</strong></span>}
-              {m.agreement > 0 && <span>Agree: <strong>{m.agreement}%</strong></span>}
-            </div>
-            <div className="swot-models">
-              {m.trades.map((t, i) => (
-                <span key={i} className="swot-model-tag" style={{color:stratColors[t.strategy]||'var(--muted)',borderColor:(stratColors[t.strategy]||'var(--border)')+'40'}}>
-                  {stratEmojis[t.strategy]||''} {t.portfolio} &bull; {t.contracts}x &bull; {t.status === 'open' ? 'OPEN' : (t.pnl >= 0 ? '+' : '') + '$' + (t.pnl / 100).toFixed(2)}
-                </span>
-              ))}
-            </div>
-          </div>}
-        </div>);
-      })}
-    </div>}
-
-    {/* ═══ Trade History ═══ */}
-    <div className="pf-history">
-      <div className="pf-history-header">
-        <span style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,color:'var(--cyan)'}}>Trade History</span>
-        <div style={{display:'flex',gap:4}}>
-          <button onClick={() => setHistFilter('all')} style={{padding:'3px 10px',borderRadius:6,border:'1px solid '+(histFilter==='all'?'var(--cyan)':'var(--border)'),background:histFilter==='all'?'rgba(6,182,212,.1)':'transparent',color:histFilter==='all'?'var(--cyan)':'var(--muted)',fontSize:10,fontWeight:600,cursor:'pointer'}}>All</button>
-          {portfolios.filter(p => parseInt(p.total_invested_cents) > 0).map(p => (
-            <button key={p.strategy} onClick={() => setHistFilter(p.strategy)} style={{padding:'3px 10px',borderRadius:6,border:'1px solid '+(histFilter===p.strategy?stratColors[p.strategy]:'var(--border)'),background:histFilter===p.strategy?stratColors[p.strategy]+'18':'transparent',color:histFilter===p.strategy?stratColors[p.strategy]:'var(--muted)',fontSize:10,fontWeight:600,cursor:'pointer'}}>
-              {stratEmojis[p.strategy] || ''} {p.name}
-            </button>
-          ))}
-        </div>
-      </div>
-      <div className="pf-history-list">
-        {filteredHistory.length === 0 && <div style={{textAlign:'center',padding:30,color:'var(--muted)',fontSize:12}}>No trades for this filter</div>}
-        {filteredHistory.slice(0, 100).map((t, i) => {
-          const c = stratColors[t.strategy] || 'var(--muted)';
-          const pnl = parseInt(t.pnl_cents) || 0;
-          return (<div key={i} className="pf-history-row">
-            <span style={{width:6,height:6,borderRadius:'50%',flexShrink:0,background:
-              t.status === 'open' ? 'var(--green)' : t.status === 'won' ? 'var(--blue)' : t.status === 'lost' ? 'var(--red)' : 'var(--muted)'
-            }} />
-            <span style={{minWidth:60,fontWeight:700,color:c,fontSize:10}}>{t.portfolio_name}</span>
-            <span style={{padding:'1px 6px',borderRadius:3,fontSize:9,fontWeight:700,
-              background: t.direction === 'BUY_YES' ? 'rgba(34,197,94,.12)' : 'rgba(239,68,68,.12)',
-              color: t.direction === 'BUY_YES' ? 'var(--green)' : 'var(--red)'
-            }}>{t.direction === 'BUY_YES' ? 'YES' : 'NO'}</span>
-            <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',color:'var(--text)'}} title={t.market_title || t.market_id}>
-              {t.market_title || t.market_id}
-            </span>
-            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:10}}>
-              {t.contracts}x @ {t.entry_price_cents}¢
-            </span>
-            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:10,minWidth:45,textAlign:'right'}}>
-              {'$'}{(t.cost_cents / 100).toFixed(2)}
-            </span>
-            <span style={{fontWeight:800,fontVariantNumeric:'tabular-nums',minWidth:55,textAlign:'right',fontSize:10,
-              color: t.status === 'open' ? 'var(--yellow)' : pnl >= 0 ? 'var(--green)' : 'var(--red)'
-            }}>
-              {t.status === 'open' ? 'OPEN' : (pnl >= 0 ? '+' : '') + (pnl / 100).toFixed(2)}
-            </span>
-            <span style={{fontSize:9,color:'var(--muted)',minWidth:55,textAlign:'right'}}>{timeAgo(t.created_at)}</span>
-          </div>);
-        })}
-      </div>
-    </div>
-
-    {/* ═══ FullChart Modal ═══ */}
-    {selectedChart && chartData[selectedChart.id] && <FullChart
-      chartData={chartData[selectedChart.id]}
-      portfolioId={selectedChart.id}
-      strategy={selectedChart.strategy}
-      name={selectedChart.name}
-      color={selectedChart.color || stratColors[selectedChart.strategy] || '#8b5cf6'}
-      emoji={stratEmojis[selectedChart.strategy] || ''}
-      desc={stratDesc[selectedChart.strategy] || ''}
-      onClose={() => setSelectedChart(null)}
-    />}
-
-    {/* ═══ Comparison Chart Modal ═══ */}
-    {compareMode === 'show' && compareIds.length >= 2 && <ComparisonChart
-      portfolioIds={compareIds}
-      allChartData={chartData}
-      stratColors={stratColors}
-      stratNames={stratNames}
-      stratEmojis={stratEmojis}
-      onClose={() => { setCompareMode(false); setCompareIds([]); }}
-    />}
-
-    {/* ═══ Strategy Guide ═══ */}
-    <div style={{marginTop:18,padding:16,background:'var(--card)',border:'1px solid var(--border)',borderRadius:14}}>
-      <div style={{fontSize:11,fontWeight:700,color:'var(--muted)',textTransform:'uppercase',letterSpacing:.5,marginBottom:10}}>Strategy Guide</div>
-      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(280px,1fr))',gap:6}}>
-        {Object.entries(stratDesc).map(([strat, desc]) => (
-          <div key={strat} style={{display:'flex',alignItems:'flex-start',gap:8,fontSize:10,padding:'4px 0',color:'var(--muted)'}}>
-            <span style={{fontSize:14,flexShrink:0}}>{stratEmojis[strat]||''}</span>
-            <div>
-              <span style={{fontWeight:700,color:stratColors[strat]}}>{stratNames[strat]}</span>
-              <span style={{margin:'0 4px'}}>—</span>
-              {desc}
-            </div>
-          </div>
-        ))}
-      </div>
-    </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>);
-}
-
-// ── Sparkline SVG Component ──
-function Sparkline({ data, width = 120, height = 30, color = '#8b5cf6', showDots = false, showArea = true, label }) {
-  if (!data || data.length < 2) return null;
-  const max = Math.max(...data);
-  const min = Math.min(...data);
-  const range = max - min || 1;
-  const gradId = 'grad-' + color.replace('#','') + '-' + width;
-  const points = data.map((v, i) => {
-    const x = (i / (data.length - 1)) * width;
-    const y = height - ((v - min) / range) * (height - 6) - 3;
-    return x + ',' + y;
-  }).join(' ');
-  const areaPoints = points + ' ' + width + ',' + height + ' 0,' + height;
-  const lastVal = data[data.length - 1];
-  const prevVal = data[data.length - 2];
-  const trending = lastVal >= prevVal;
-  return (
-    <div style={{position:'relative'}}>
-      <svg className="sparkline-svg" width={width} height={height} viewBox={'0 0 ' + width + ' ' + height}>
-        <defs><linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
-          <stop offset="0%" stopColor={color} stopOpacity="0.25"/><stop offset="100%" stopColor={color} stopOpacity="0.01"/>
-        </linearGradient></defs>
-        {showArea && <polygon points={areaPoints} fill={'url(#' + gradId + ')'} />}
-        <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
-        {showDots && data.map((v, i) => {
-          const x = (i / (data.length - 1)) * width;
-          const y = height - ((v - min) / range) * (height - 6) - 3;
-          return <circle key={i} cx={x} cy={y} r={i === data.length - 1 ? 3 : 1.5} fill={i === data.length - 1 ? color : 'transparent'} stroke={color} strokeWidth={i === data.length - 1 ? 0 : 0.5} opacity={i === data.length - 1 ? 1 : 0.5} />;
-        })}
-      </svg>
-      {label && <div style={{position:'absolute',top:2,right:4,fontSize:9,color:trending?'var(--green)':'var(--red)',fontWeight:700}}>
-        {trending?'▲':'▼'} {lastVal.toLocaleString()}
-      </div>}
-    </div>
-  );
-}
-
-// ── MiniChart: Canvas-based sparkline for leaderboard rows ──
-function MiniChart({ data, color, width = 140, height = 36 }) {
-  const canvasRef = React.useRef(null);
-  const chartRef = React.useRef(null);
-  React.useEffect(() => {
-    if (!data || data.length < 2 || !canvasRef.current) return;
-    if (chartRef.current) chartRef.current.destroy();
-    const ctx = canvasRef.current.getContext('2d');
-    const gradient = ctx.createLinearGradient(0, 0, 0, height);
-    gradient.addColorStop(0, color + '40');
-    gradient.addColorStop(1, color + '05');
-    chartRef.current = new Chart(ctx, {
-      type: 'line',
-      data: { labels: data.map((_, i) => i), datasets: [{ data, borderColor: color, borderWidth: 1.5, backgroundColor: gradient, fill: true, pointRadius: 0, tension: 0.4 }] },
-      options: { responsive: false, animation: { duration: 800, easing: 'easeOutQuart' }, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { display: false } } }
-    });
-    return () => { if (chartRef.current) chartRef.current.destroy(); };
-  }, [data, color]);
-  return <canvas ref={canvasRef} width={width} height={height} style={{borderRadius:4}} />;
-}
-
-// ── FullChart: Modal chart for a single portfolio with period selector ──
-function FullChart({ chartData: initialData, portfolioId, strategy, name, color, emoji, desc, onClose }) {
-  const canvasRef = React.useRef(null);
-  const chartRef = React.useRef(null);
-  const clr = color || '#8b5cf6';
-  const [period, setPeriod] = React.useState('7d');
-  const [liveData, setLiveData] = React.useState(initialData);
-  const periods = ['1d','3d','7d','14d','30d'];
-
-  React.useEffect(() => {
-    if (period === '7d') { setLiveData(initialData); return; }
-    api('/api/portfauxlio/charts?period=' + period).then(d => {
-      const match = (d.portfolios || []).find(p => p.id === portfolioId);
-      if (match) setLiveData(match);
-    }).catch(() => {});
-  }, [period, portfolioId]);
-
-  React.useEffect(() => {
-    const cd = liveData || initialData;
-    const snaps = cd?.snapshots || [];
-    const daily = cd?.daily || [];
-    const dataPoints = snaps.length > 1 ? snaps.map(s => ({ x: new Date(s.time), y: s.nav / 100 }))
-      : daily.length > 1 ? daily.map(d => ({ x: new Date(d.date), y: d.cumulative / 100 })) : [];
-    if (!dataPoints.length || !canvasRef.current) return;
-    if (chartRef.current) chartRef.current.destroy();
-    const ctx = canvasRef.current.getContext('2d');
-    const gradient = ctx.createLinearGradient(0, 0, 0, 300);
-    gradient.addColorStop(0, clr + '30');
-    gradient.addColorStop(1, clr + '02');
-    const zeroLine = { id: 'zeroLine', beforeDraw(chart) {
-      const yScale = chart.scales.y;
-      if (!yScale) return;
-      const y0 = yScale.getPixelForValue(0);
-      if (y0 < yScale.top || y0 > yScale.bottom) return;
-      const cctx = chart.ctx;
-      cctx.save();
-      cctx.strokeStyle = 'rgba(255,255,255,0.15)';
-      cctx.lineWidth = 1;
-      cctx.setLineDash([4, 4]);
-      cctx.beginPath();
-      cctx.moveTo(chart.chartArea.left, y0);
-      cctx.lineTo(chart.chartArea.right, y0);
-      cctx.stroke();
-      cctx.restore();
-    }};
-    chartRef.current = new Chart(ctx, {
-      type: 'line',
-      data: { datasets: [{ label: 'NAV', data: dataPoints, borderColor: clr, borderWidth: 2, backgroundColor: gradient, fill: true, pointRadius: dataPoints.length > 50 ? 0 : 2, pointHoverRadius: 6, pointBackgroundColor: clr, tension: 0.3 }] },
-      plugins: [zeroLine],
-      options: {
-        responsive: true, maintainAspectRatio: false,
-        animation: { duration: 1200, easing: 'easeInOutQuart' },
-        interaction: { intersect: false, mode: 'index' },
-        plugins: {
-          legend: { display: false },
-          tooltip: { backgroundColor: 'rgba(15,15,35,0.95)', borderColor: clr + '60', borderWidth: 1, titleColor: '#e2e8f0', bodyColor: '#e2e8f0', padding: 12, displayColors: false,
-            callbacks: { label: (tipCtx) => 'NAV: ' + (tipCtx.parsed.y >= 0 ? '+' : '') + '$' + tipCtx.parsed.y.toFixed(2) }
-          }
-        },
-        scales: {
-          x: { type: 'time', time: { unit: snaps.length > 1 ? 'hour' : 'day' }, ticks: { color: '#64748b', maxRotation: 45, font: { size: 9 } }, grid: { color: 'rgba(255,255,255,.03)' } },
-          y: { ticks: { color: '#64748b', callback: (v) => '$' + v.toFixed(0), font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }
-        }
-      }
-    });
-    return () => { if (chartRef.current) chartRef.current.destroy(); };
-  }, [liveData, strategy, clr]);
-  return (
-    <div className="chart-modal" onClick={onClose}>
-      <div className="chart-modal-content" onClick={e => e.stopPropagation()}>
-        <div className="chart-modal-header">
-          <span style={{fontSize:18}}>{emoji||''}</span>
-          <span style={{fontWeight:800,color:clr}}>{name || strategy}</span>
-          <span style={{fontSize:10,color:'var(--muted)',flex:1}}>{desc||''}</span>
-          <div className="chart-period-btns">
-            {periods.map(p => (
-              <button key={p} className={period === p ? 'chart-period-active' : 'chart-period'} onClick={() => setPeriod(p)}>{p}</button>
-            ))}
-          </div>
-          <button className="chart-close" onClick={onClose}>✕</button>
-        </div>
-        <div style={{height:340,position:'relative'}}><canvas ref={canvasRef} /></div>
-      </div>
-    </div>
-  );
-}
-
-// ── ComparisonChart: Overlay multiple portfolios on one chart ──
-function ComparisonChart({ portfolioIds, allChartData, stratColors, stratNames, stratEmojis, onClose }) {
-  const canvasRef = React.useRef(null);
-  const chartRef = React.useRef(null);
-  const [period, setPeriod] = React.useState('7d');
-  const [compareData, setCompareData] = React.useState(allChartData);
-  const periods = ['1d','3d','7d','14d','30d'];
-
-  React.useEffect(() => {
-    if (period === '7d') { setCompareData(allChartData); return; }
-    api('/api/portfauxlio/charts?period=' + period).then(d => {
-      const byId = {};
-      (d.portfolios || []).forEach(p => { byId[p.id] = p; });
-      setCompareData(byId);
-    }).catch(() => {});
-  }, [period]);
-
-  React.useEffect(() => {
-    if (!canvasRef.current) return;
-    if (chartRef.current) chartRef.current.destroy();
-    const datasets = portfolioIds.map(id => {
-      const cd = compareData[id];
-      if (!cd) return null;
-      const strat = cd.strategy || '';
-      const clr = stratColors[strat] || '#8b5cf6';
-      const snaps = cd.snapshots || [];
-      const daily = cd.daily || [];
-      const pts = snaps.length > 1 ? snaps.map(s => ({ x: new Date(s.time), y: s.nav / 100 }))
-        : daily.length > 1 ? daily.map(d => ({ x: new Date(d.date), y: d.cumulative / 100 })) : [];
-      if (pts.length < 2) return null;
-      return { label: (stratEmojis[strat]||'') + ' ' + (cd.name || strat), data: pts, borderColor: clr, borderWidth: 2, backgroundColor: 'transparent', fill: false, pointRadius: 0, pointHoverRadius: 5, tension: 0.3 };
-    }).filter(Boolean);
-    if (!datasets.length) return;
-    const ctx = canvasRef.current.getContext('2d');
-    chartRef.current = new Chart(ctx, {
-      type: 'line', data: { datasets },
-      options: {
-        responsive: true, maintainAspectRatio: false,
-        animation: { duration: 1000, easing: 'easeOutQuart' },
-        interaction: { intersect: false, mode: 'index' },
-        plugins: {
-          legend: { display: true, position: 'bottom', labels: { color: '#94a3b8', font: { size: 10 }, boxWidth: 12, padding: 8 } },
-          tooltip: { backgroundColor: 'rgba(15,15,35,0.95)', borderWidth: 1, titleColor: '#e2e8f0', bodyColor: '#e2e8f0', padding: 12 }
-        },
-        scales: {
-          x: { type: 'time', ticks: { color: '#64748b', maxRotation: 45, font: { size: 9 } }, grid: { color: 'rgba(255,255,255,.03)' } },
-          y: { ticks: { color: '#64748b', callback: (v) => '$' + v.toFixed(0), font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }
-        }
-      }
-    });
-    return () => { if (chartRef.current) chartRef.current.destroy(); };
-  }, [compareData, portfolioIds]);
-
-  return (
-    <div className="chart-modal" onClick={onClose}>
-      <div className="chart-modal-content" onClick={e => e.stopPropagation()} style={{maxWidth:1100}}>
-        <div className="chart-modal-header">
-          <span style={{fontSize:16}}>📊</span>
-          <span style={{fontWeight:800,color:'var(--cyan)'}}>Compare {portfolioIds.length} Portfolios</span>
-          <div className="chart-period-btns" style={{marginLeft:'auto'}}>
-            {periods.map(p => (
-              <button key={p} className={period === p ? 'chart-period-active' : 'chart-period'} onClick={() => setPeriod(p)}>{p}</button>
-            ))}
-          </div>
-          <button className="chart-close" onClick={onClose}>✕</button>
-        </div>
-        <div style={{height:400,position:'relative'}}><canvas ref={canvasRef} /></div>
-      </div>
-    </div>
-  );
-}
-
-// ── Vertical Bar Chart ──
-function BarChart({ data, maxVal, height = 80, color = '#8b5cf6' }) {
-  if (!data || data.length === 0) return null;
-  const mx = maxVal || Math.max(...data.map(d => d.value)) || 1;
-  return (
-    <div className="mkt-bar-chart" style={{ height }}>
-      {data.map((d, i) => (
-        <div key={i} className="mkt-bar" style={{ height: Math.max(2, (d.value / mx) * height) + 'px', background: d.color || color, opacity: 0.7 + (d.value / mx) * 0.3 }}>
-          <div className="mkt-bar-label">{d.label}: {d.value}</div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-// ── Confidence Histogram ──
-function ConfHistogram({ data }) {
-  if (!data || data.length === 0) return null;
-  const max = Math.max(...data.map(d => d.count)) || 1;
-  const colors = ['#ef4444','#f97316','#f59e0b','#eab308','#84cc16','#22c55e','#14b8a6','#06b6d4','#3b82f6','#8b5cf6'];
-  return (
-    <div className="histogram">
-      {data.map((d, i) => (
-        <div key={i} className="hist-bar" style={{
-          height: Math.max(2, (d.count / max) * 70) + 'px',
-          background: colors[i] || 'var(--muted)',
-          opacity: d.count > 0 ? 0.8 : 0.2,
-        }}>
-          <div className="hist-label">{d.range}</div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-// ── Time ago helper ──
-function timeAgo(ts) {
-  if (!ts) return '';
-  const diff = Date.now() - new Date(ts).getTime();
-  if (diff < 60000) return Math.floor(diff / 1000) + 's ago';
-  if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
-  if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
-  return Math.floor(diff / 86400000) + 'd ago';
-}
-
-// ── Market Data Tab — Bloomberg-style terminal ──
-function MarketDataTab() {
-  const [mkt, setMkt] = useState(null);
-  const [tick, setTick] = useState(0);
-
-  useEffect(() => {
-    const load = () => api('/api/inc/market-data').then(setMkt).catch(e => console.error(e));
-    load();
-    const iv = setInterval(load, 8000);
-    return () => clearInterval(iv);
-  }, []);
-  useEffect(() => { const iv = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(iv); }, []);
-
-  if (!mkt) return <div style={{textAlign:'center',padding:60,color:'var(--muted)'}}>
-    <div style={{fontSize:24,marginBottom:8,animation:'avatarFloat 2s ease-in-out infinite'}}>Loading market data...</div>
-  </div>;
-
-  const signalVals = (mkt.signalTrend || []).map(s => s.signals);
-  const edgeVals = (mkt.signalTrend || []).map(s => s.avgEdge * 100);
-  const sentVals = (mkt.sentimentTrend || []).map(s => s.articles);
-  const sentScoreVals = (mkt.sentimentTrend || []).map(s => s.avgSentiment);
-  const totalSent24h = sentVals.reduce((a, b) => a + b, 0);
-  const totalSignals = signalVals.reduce((a, b) => a + b, 0);
-  const at = mkt.approvalTrend || {};
-  const changeArrow = at.change > 0 ? '▲' : at.change < 0 ? '▼' : '—';
-  const changeClass = at.change > 0 ? 'up' : at.change < 0 ? 'down' : 'flat';
-  const totalPnl = (mkt.portfolios || []).reduce((s, p) => s + (p.pnl_cents || 0), 0);
-  const totalTrades = (mkt.portfolios || []).reduce((s, p) => s + (p.total_trades || 0), 0);
-  const avgEdge = edgeVals.length > 0 ? (edgeVals.reduce((a,b)=>a+b,0) / edgeVals.length) : 0;
-
-  // Ticker items
-  const tickerItems = [
-    { label: 'P&L', value: (totalPnl >= 0 ? '+' : '') + (totalPnl / 100).toFixed(2), color: totalPnl >= 0 ? 'var(--green)' : 'var(--red)' },
-    { label: 'SIGNALS', value: totalSignals, color: 'var(--blue)' },
-    { label: 'APPROVAL', value: at.current + '%', change: at.change, color: 'var(--green)' },
-    { label: 'SENTIMENT', value: totalSent24h.toLocaleString(), color: 'var(--pink)' },
-    { label: 'AVG EDGE', value: avgEdge.toFixed(1) + '%', color: 'var(--purple)' },
-    { label: 'TRADES', value: totalTrades, color: 'var(--yellow)' },
-    { label: 'MARKETS', value: mkt.activeMarkets, color: 'var(--cyan)' },
-    { label: 'GEMINI', value: mkt.gemini.callsToday + '/' + mkt.gemini.dailyLimit, color: 'var(--teal)' },
-  ];
-  const doubledTicker = tickerItems.concat(tickerItems);
-
-  const stratColors = {aggressive:'#ef4444',conservative:'#3b82f6',weather:'#06b6d4',momentum:'#f59e0b',contrarian:'#8b5cf6',ai_enhanced:'#ec4899',mc_validated:'#a855f7',scalper:'#eab308',patient:'#22c55e',random:'#64748b',penny_pincher:'#94a3b8',spray_pray:'#fb923c',nickel_slots:'#a78bfa',degen_lite:'#f43f5e',politics_junkie:'#0ea5e9',double_down:'#dc2626',volatility_rider:'#14b8a6',whale:'#1d4ed8',yolo:'#e11d48',full_send:'#b91c1c',kelly_criterion:'#059669',sharpe_sniper:'#7c3aed',bayesian_blend:'#0891b2',ensemble_lock:'#c026d3',mean_revert:'#d97706',info_ratio:'#2563eb',anti_fomo:'#16a34a',nightcrawler:'#4c1d95',heatseeker:'#ea580c',quant_core:'#0d9488',etf_presidential:'#dc143c',etf_cabinet:'#ff6347',etf_congress:'#4169e1',etf_scotus:'#2f4f4f',etf_fed:'#228b22',etf_economy:'#daa520',etf_geopolitics:'#8b0000',etf_territory:'#006400',etf_climate:'#00bfff',etf_energy:'#ff8c00',etf_tech:'#7b68ee',etf_space:'#191970',etf_crypto:'#ffd700',etf_sports:'#32cd32',etf_entertainment:'#ff1493',etf_legal:'#708090',etf_financials:'#4682b4',etf_govreform:'#b22222',etf_global_social:'#9370db',etf_broad_market:'#c0a050'};
-  const stratEmojis = {aggressive:'🔥',conservative:'🏛️',weather:'⛈️',momentum:'🚀',contrarian:'🔄',ai_enhanced:'🧠',mc_validated:'🎲',scalper:'⚡',patient:'🐢',random:'🎰',penny_pincher:'🪙',spray_pray:'💦',nickel_slots:'🎰',degen_lite:'😈',politics_junkie:'🏛️',double_down:'⬆️',volatility_rider:'🏄',whale:'🐳',yolo:'🧨',full_send:'💣',kelly_criterion:'🎰',sharpe_sniper:'🎯',bayesian_blend:'🧮',ensemble_lock:'🔐',mean_revert:'↩️',info_ratio:'📊',anti_fomo:'🧘',nightcrawler:'🦇',heatseeker:'🔬',quant_core:'∑',etf_presidential:'🏛️',etf_cabinet:'💼',etf_congress:'🗳️',etf_scotus:'⚖️',etf_fed:'🏦',etf_economy:'📈',etf_geopolitics:'🌍',etf_territory:'🗺️',etf_climate:'🌡️',etf_energy:'⛽',etf_tech:'🤖',etf_space:'🚀',etf_crypto:'₿',etf_sports:'⚽',etf_entertainment:'🎬',etf_legal:'🔨',etf_financials:'💹',etf_govreform:'✂️',etf_global_social:'⛪',etf_broad_market:'🌐'};
-
-  return (<div>
-    {/* ═══ Hero P&L Banner ═══ */}
-    <div style={{textAlign:'center',padding:'20px 0 8px',borderBottom:'1px solid var(--border)'}}>
-      <div style={{fontSize:10,color:'var(--muted)',textTransform:'uppercase',letterSpacing:2,marginBottom:4}}>Total Portfolio P&L</div>
-      <div className="pnl-hero" style={{color: totalPnl >= 0 ? 'var(--green)' : 'var(--red)', animation: totalPnl !== 0 ? 'pnlPulse 3s ease-in-out infinite' : 'none'}}>
-        {totalPnl >= 0 ? '+' : ''}{(totalPnl / 100).toFixed(2)}
-      </div>
-      <div style={{display:'flex',justifyContent:'center',gap:24,marginTop:8,fontSize:11,color:'var(--muted)'}}>
-        <span>{totalTrades} trades</span>
-        <span>{(mkt.portfolios||[]).filter(p => p.pnl_cents > 0).length} winning</span>
-        <span>{(mkt.portfolios||[]).filter(p => p.pnl_cents < 0).length} losing</span>
-        <span>{avgEdge.toFixed(1)}% avg edge</span>
-      </div>
-    </div>
-
-    {/* ═══ Scrolling Ticker ═══ */}
-    <div className="mkt-ticker-strip">
-      <div className="mkt-ticker-track">
-        {doubledTicker.map((t, i) => (
-          <div key={i} className="mkt-ticker-item">
-            <span style={{color:'var(--muted)',fontSize:9,textTransform:'uppercase',letterSpacing:.5}}>{t.label}</span>
-            <span style={{color:t.color,fontWeight:800}}>{t.value}</span>
-            {t.change !== undefined && <span className={'mkt-change ' + (t.change > 0 ? 'up' : t.change < 0 ? 'down' : 'flat')}>
-              {t.change > 0 ? '▲' : t.change < 0 ? '▼' : ''}{Math.abs(t.change)}%
-            </span>}
-          </div>
-        ))}
-      </div>
-    </div>
-
-    <div className="mkt-grid">
-      {/* ═══ Live Signal Feed ═══ */}
-      <div className="mkt-card" style={{gridColumn:'span 2'}}>
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--blue)'}}>
-            <span style={{display:'inline-block',width:6,height:6,borderRadius:'50%',background:'var(--green)',marginRight:6,animation:'statusPulse 2s ease infinite'}}></span>
-            Live Signal Feed
-          </span>
-          <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{(mkt.latestSignals||[]).length} latest</span>
-        </div>
-        <div className="signal-feed">
-          {(mkt.latestSignals || []).map((sig, i) => (
-            <div key={i} className="signal-row">
-              <div className="signal-approved" style={{background: sig.approved ? 'var(--green)' : 'var(--red)'}} title={sig.approved ? 'Approved' : 'Rejected'} />
-              <span className={'signal-dir ' + sig.direction}>{sig.direction}</span>
-              <span className="signal-market" title={sig.market}>{sig.market}</span>
-              <span className="signal-edge" style={{color: sig.edge > 0 ? 'var(--green)' : 'var(--red)'}}>
-                {sig.edge > 0 ? '+' : ''}{(sig.edge * 100).toFixed(1)}%
-              </span>
-              <span className="signal-conf" title="Confidence">{(sig.confidence * 100).toFixed(0)}%</span>
-              <span className="signal-time">{timeAgo(sig.time)}</span>
-            </div>
-          ))}
-          {(!mkt.latestSignals || mkt.latestSignals.length === 0) && <div style={{padding:20,textAlign:'center',color:'var(--muted)',fontSize:12}}>No recent signals</div>}
-        </div>
-      </div>
-
-      {/* ═══ Signal Volume Sparkline ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--blue)'}}>Signal Volume</span>
-          <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{totalSignals}</span>
-        </div>
-        <Sparkline data={signalVals} width={300} height={50} color="#3b82f6" showDots={true} label={true} />
-        <BarChart data={(mkt.signalTrend || []).map((s) => ({
-          value: s.signals,
-          label: new Date(s.hour).toLocaleTimeString('en-US', {hour:'numeric',hour12:true}),
-          color: s.approved > s.signals * 0.5 ? 'rgba(34,197,94,.6)' : 'rgba(239,68,68,.4)',
-        }))} height={45} />
-      </div>
-
-      {/* ═══ Sentiment Flow ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--pink)'}}>Sentiment Flow</span>
-          <span className="mkt-card-badge" style={{background:'rgba(236,72,153,.15)',color:'var(--pink)'}}>{totalSent24h.toLocaleString()}</span>
-        </div>
-        <Sparkline data={sentVals} width={300} height={50} color="#ec4899" label={true} />
-        <div style={{display:'flex',justifyContent:'space-between',marginTop:8,fontSize:10,color:'var(--muted)'}}>
-          <span>Avg: <strong style={{color: sentScoreVals.length > 0 && sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length > 0 ? 'var(--green)' : 'var(--red)'}}>
-            {sentScoreVals.length > 0 ? (sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length).toFixed(3) : '—'}
-          </strong></span>
-          <span>{(mkt.sourceMix||[]).length} sources</span>
-        </div>
-      </div>
-
-      {/* ═══ Approval Rate ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--green)'}}>Approval Rate</span>
-          <span className={'mkt-change ' + changeClass} style={{fontSize:14}}>{changeArrow} {Math.abs(at.change)}%</span>
-        </div>
-        <div style={{display:'flex',alignItems:'baseline',gap:12,marginBottom:8}}>
-          <span style={{fontSize:42,fontWeight:800,color:'var(--green)',fontVariantNumeric:'tabular-nums'}}>{at.current}%</span>
-          <span style={{fontSize:12,color:'var(--muted)'}}>vs {at.prior}% prior</span>
-        </div>
-        <div className="mkt-gauge">
-          <div className="mkt-gauge-fill" style={{width: at.current + '%', background:'linear-gradient(90deg,var(--green),var(--teal))'}} />
-          <div className="mkt-gauge-label">{at.recentSignals} signals</div>
-        </div>
-      </div>
-
-      {/* ═══ Edge Trend ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--purple)'}}>Edge Trend</span>
-          <span className="mkt-card-badge" style={{background:'rgba(139,92,246,.15)',color:'var(--purple)'}}>
-            {avgEdge.toFixed(1)}% avg
-          </span>
-        </div>
-        <Sparkline data={edgeVals} width={300} height={50} color="#8b5cf6" showDots={true} label={true} />
-      </div>
-
-      {/* ═══ Market Heatmap ═══ */}
-      {(mkt.marketStats || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Market Heatmap (24h)</span>
-          <span className="mkt-card-badge" style={{background:'rgba(245,158,11,.15)',color:'var(--yellow)'}}>{(mkt.marketStats||[]).length} markets</span>
-        </div>
-        <div className="heatmap-grid">
-          {(mkt.marketStats || []).map((m, i) => {
-            const edge = m.avgEdge * 100;
-            const intensity = Math.min(1, Math.abs(edge) / 25);
-            const bg = edge >= 0
-              ? 'rgba(34,197,94,' + (0.08 + intensity * 0.2) + ')'
-              : 'rgba(239,68,68,' + (0.08 + intensity * 0.2) + ')';
-            const borderC = edge >= 0 ? 'rgba(34,197,94,' + (0.2 + intensity * 0.3) + ')' : 'rgba(239,68,68,' + (0.2 + intensity * 0.3) + ')';
-            return (<div key={i} className="heatmap-cell" style={{background: bg, borderColor: borderC}}>
-              <div className="heatmap-market">{m.market}</div>
-              <div className="heatmap-edge" style={{color: edge >= 0 ? 'var(--green)' : 'var(--red)'}}>
-                {edge >= 0 ? '+' : ''}{edge.toFixed(1)}%
-              </div>
-              <div className="heatmap-meta">{m.signals} sig | {m.approved} appr | {(m.avgConf * 100).toFixed(0)}% conf</div>
-            </div>);
-          })}
-        </div>
-      </div>}
-
-      {/* ═══ Confidence Distribution ═══ */}
-      {(mkt.confDistribution || []).length > 0 && <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--cyan)'}}>Confidence Distribution</span>
-        </div>
-        <ConfHistogram data={mkt.confDistribution} />
-        <div style={{marginTop:18,display:'flex',justifyContent:'space-between',fontSize:9,color:'var(--muted)'}}>
-          <span>Low conf</span><span>High conf</span>
-        </div>
-      </div>}
-
-      {/* ═══ Intelligence Sources ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--cyan)'}}>Intelligence Sources</span>
-          <span className="mkt-card-badge" style={{background:'rgba(6,182,212,.15)',color:'var(--cyan)'}}>{(mkt.sourceMix||[]).length}</span>
-        </div>
-        <div className="mkt-source-list">
-          {(mkt.sourceMix || []).slice(0, 12).map((s, i) => {
-            const maxC = (mkt.sourceMix || [])[0]?.count || 1;
-            const colors = ['#3b82f6','#8b5cf6','#ec4899','#ef4444','#f59e0b','#22c55e','#06b6d4','#14b8a6','#6366f1','#d946ef','#f97316','#a855f7'];
-            return (<div key={i} className="mkt-source-row">
-              <span className="mkt-source-name">{s.source}</span>
-              <div style={{flex:1,display:'flex',alignItems:'center',gap:4}}>
-                <div className="mkt-source-bar" style={{width: (s.count / maxC * 100) + '%', background: colors[i % colors.length]}} />
-              </div>
-              <span className="mkt-source-val">{s.count.toLocaleString()}</span>
-              <span style={{fontSize:9,color: s.avgSentiment > 0 ? 'var(--green)' : s.avgSentiment < 0 ? 'var(--red)' : 'var(--muted)', minWidth:35, textAlign:'right'}}>
-                {s.avgSentiment > 0 ? '+' : ''}{s.avgSentiment.toFixed(2)}
-              </span>
-            </div>);
-          })}
-        </div>
-      </div>
-
-      {/* ═══ Gemini + Categories row ═══ */}
-      <div className="mkt-card">
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--teal)'}}>Gemini AI</span>
-          <span className="mkt-card-badge" style={{background: mkt.gemini.pctUsed > 80 ? 'rgba(239,68,68,.15)' : 'rgba(20,184,166,.15)', color: mkt.gemini.pctUsed > 80 ? 'var(--red)' : 'var(--teal)'}}>
-            {mkt.gemini.pctUsed}%
-          </span>
-        </div>
-        <div style={{display:'flex',alignItems:'baseline',gap:8,marginBottom:8}}>
-          <span style={{fontSize:32,fontWeight:800,color:'var(--teal)',fontVariantNumeric:'tabular-nums'}}>{mkt.gemini.callsToday}</span>
-          <span style={{fontSize:11,color:'var(--muted)'}}>/ {mkt.gemini.dailyLimit}</span>
-        </div>
-        <div className="mkt-gauge">
-          <div className="mkt-gauge-fill" style={{width: mkt.gemini.pctUsed + '%', background: mkt.gemini.pctUsed > 80 ? 'linear-gradient(90deg,var(--yellow),var(--red))' : 'linear-gradient(90deg,var(--teal),var(--cyan))'}} />
-          <div className="mkt-gauge-label">{mkt.gemini.pctUsed}%</div>
-        </div>
-        {/* Categories inline */}
-        <div style={{marginTop:14,display:'flex',flexWrap:'wrap',gap:4}}>
-          {(mkt.categories || []).slice(0, 8).map((c, i) => {
-            const colors = ['#f59e0b','#3b82f6','#ec4899','#22c55e','#8b5cf6','#ef4444','#06b6d4','#14b8a6'];
-            return (<div key={i} style={{padding:'3px 10px',borderRadius:6,background:'rgba(255,255,255,.03)',border:'1px solid var(--border)',fontSize:9,display:'flex',alignItems:'center',gap:4}}>
-              <div style={{width:4,height:4,borderRadius:'50%',background:colors[i % colors.length]}} />
-              <span style={{fontWeight:600}}>{c.category}</span>
-              <span style={{color:'var(--muted)'}}>{c.count}</span>
-            </div>);
-          })}
-        </div>
-      </div>
-
-      {/* ═══ Recent Trades ═══ */}
-      {(mkt.recentTrades || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Recent Trades</span>
-          <span className="mkt-card-badge" style={{background:'rgba(245,158,11,.15)',color:'var(--yellow)'}}>{totalTrades} total</span>
-        </div>
-        <div className="trade-log">
-          {(mkt.recentTrades || []).map((t, i) => {
-            const c = stratColors[t.strategy] || 'var(--muted)';
-            return (<div key={i} className="trade-row">
-              <span className="trade-portfolio" style={{color:c}}>{t.portfolio_name}</span>
-              <span className={'signal-dir ' + t.direction} style={{fontSize:9}}>{t.direction}</span>
-              <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',color:'var(--muted)'}}>{t.market_id}</span>
-              <span style={{fontSize:10,color:'var(--muted)'}}>{t.contracts}x @ {t.entry_price_cents}¢</span>
-              <span className="trade-pnl" style={{color: t.pnl_cents >= 0 ? 'var(--green)' : 'var(--red)'}}>
-                {t.pnl_cents >= 0 ? '+' : ''}{(t.pnl_cents / 100).toFixed(2)}
-              </span>
-              <span className="signal-time">{timeAgo(t.created_at)}</span>
-            </div>);
-          })}
-        </div>
-      </div>}
-
-      {/* ═══ Portfolio Leaderboard ═══ */}
-      {(mkt.portfolios || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
-        <div className="mkt-card-header">
-          <span className="mkt-card-title" style={{color:'var(--pink)'}}>Portfolio Leaderboard</span>
-          <span className="mkt-card-badge" style={{background:'rgba(255,105,180,.15)',color:'var(--pink)'}}>{totalTrades} trades</span>
-        </div>
-        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(220px,1fr))',gap:8}}>
-          {(mkt.portfolios || []).map((p, i) => {
-            const c = stratColors[p.strategy] || 'var(--muted)';
-            const emoji = stratEmojis[p.strategy] || '';
-            const pnl = p.pnl_cents || 0;
-            const invested = p.total_invested_cents || 0;
-            const roi = invested > 0 ? ((pnl / invested) * 100).toFixed(1) : '0.0';
-            return (<div key={i} style={{padding:'12px 14px',background:'rgba(255,255,255,.02)',border:'1px solid var(--border)',borderRadius:10,transition:'all .3s',borderLeftColor:c,borderLeftWidth:3,position:'relative',overflow:'hidden'}}>
-              {/* Rank badge */}
-              <div style={{position:'absolute',top:6,right:8,fontSize:8,color:'var(--muted)',fontWeight:800}}>#{i+1}</div>
-              <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:6}}>
-                <span style={{fontSize:20}}>{emoji}</span>
-                <div style={{flex:1}}>
-                  <div style={{fontSize:13,fontWeight:700,color:c}}>{p.name}</div>
-                  <div style={{fontSize:9,color:'var(--muted)',textTransform:'uppercase',letterSpacing:.3}}>{p.strategy}</div>
-                </div>
-                <div style={{textAlign:'right'}}>
-                  <div style={{fontSize:18,fontWeight:900,color: pnl >= 0 ? 'var(--green)' : 'var(--red)',fontVariantNumeric:'tabular-nums'}}>
-                    {pnl >= 0 ? '+' : ''}{(pnl / 100).toFixed(2)}
-                  </div>
-                  <div style={{fontSize:9,color:'var(--muted)'}}>ROI: {roi}%</div>
-                </div>
-              </div>
-              <div style={{display:'flex',gap:4,fontSize:9,flexWrap:'wrap'}}>
-                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(34,197,94,.1)',color:'var(--green)'}}>W:{p.trades_won}</span>
-                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(239,68,68,.1)',color:'var(--red)'}}>L:{p.trades_lost}</span>
-                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(255,255,255,.04)',color:'var(--muted)'}}>WR:{p.win_rate}%</span>
-                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(255,255,255,.04)',color:'var(--muted)'}}>{p.total_trades} trades</span>
-                {p.streak !== 0 && <span style={{padding:'2px 7px',borderRadius:4,background: p.streak > 0 ? 'rgba(34,197,94,.1)' : 'rgba(239,68,68,.1)',color: p.streak > 0 ? 'var(--green)' : 'var(--red)'}}>{p.streak > 0 ? '+' : ''}{p.streak} streak</span>}
-              </div>
-              {invested > 0 && <div style={{marginTop:6}}>
-                <div style={{height:3,borderRadius:2,background:'rgba(255,255,255,.05)',overflow:'hidden'}}>
-                  <div style={{height:'100%',borderRadius:2,width: Math.min(100, Math.abs(parseFloat(roi))) + '%',background: pnl >= 0 ? 'var(--green)' : 'var(--red)',transition:'width .5s'}} />
-                </div>
-              </div>}
-            </div>);
-          })}
-        </div>
-      </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 || !data) 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('market')} onClick={()=>setTab('market')}>Market Data</button>
-        <button style={tabStyle('portfolios')} onClick={()=>setTab('portfolios')}>portFAUXlio</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 === 'market' && <MarketDataTab />}
-      {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; 50 paper portfolios &bull;
-        <a href="/"> Back to Dashboard</a>
-      </div>
-    </div>
-  );
-}
-
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
-</script>
-</body>
-</html>`;
-
-// ════════════════════════════════════════
-// HTTP SERVER
-// ════════════════════════════════════════
-
-const server = http.createServer(async (req, res) => {
-  // CORS preflight
-  if (req.method === 'OPTIONS') {
-    res.writeHead(200, {
-      'Access-Control-Allow-Origin': '*',
-      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
-      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
-    });
-    res.end();
-    return;
-  }
-
-  // Route key for matching
-  const urlPath = req.url.split('?')[0];
-  const routeKey = `${req.method} ${urlPath}`;
-
-  // Public endpoints (no auth required)
-  const publicPaths = [
-    '/api/auth/login',
-    '/api/auth/logout',
-    '/api/auth/session',
-    '/api/auth/google',
-    '/api/auth/google-config',
-    '/api/health',
-    '/api/portfauxlio',
-    '/api/portfauxlio/charts',
-    '/api/portfauxlio/swot',
-    '/api/markets',
-    '/api/signals',
-    '/api/energy-arb',
-    '/api/predictions',
-    '/api/portfolios',
-    '/api/scan-config',
-    '/api/dashboard',
-  ];
-  const isPublic = publicPaths.includes(urlPath);
-
-  // Enforce auth on all other API routes
-  if (!isPublic && urlPath.startsWith('/api/') && !isAuthenticated(req)) {
-    return json(res, { error: 'Authentication required' }, 401);
-  }
-
-  // Check route handlers
-  if (routes[routeKey]) {
-    try {
-      await routes[routeKey](req, res);
-    } catch (err) {
-      console.error(`[ERROR] ${routeKey}:`, err.message);
-      json(res, { error: err.message }, 500);
-    }
-    return;
-  }
-
-  // Unknown API route
-  if (urlPath.startsWith('/api/')) {
-    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;
-  }
-
-  // ── Tournament Arena — 3D Team Competition ──
-  if (urlPath === '/arena' || urlPath === '/arena/') {
-    res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
-    res.end(ARENA_HTML);
-    return;
-  }
-
-  // ── PortFAUXlios Dashboard ──
-  if (urlPath === '/portfauxlios' || urlPath === '/portfauxlios/') {
-    try {
-      const dashHtml = fs.readFileSync('/root/DW-Agents/ken-portfauxlios-dashboard.html', 'utf8');
-      res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
-      res.end(dashHtml);
-    } catch (e) {
-      res.writeHead(500);
-      res.end('Dashboard not found: ' + e.message);
-    }
-    return;
-  }
-
-  // ── Serve Static Files / SPA Routing ──
-  let filePath = path.join(DIST, urlPath === '/' ? 'index.html' : urlPath);
-
-  // Security: prevent directory traversal
-  if (!filePath.startsWith(DIST)) {
-    res.writeHead(403);
-    res.end('Forbidden');
-    return;
-  }
-
-  // Check if file exists; if not, serve index.html for SPA routing
-  if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
-    filePath = path.join(DIST, 'index.html');
-  }
-
-  try {
-    const content = fs.readFileSync(filePath);
-    const ext = path.extname(filePath);
-    const contentType = MIME[ext] || 'application/octet-stream';
-
-    // Cache static assets (hashed filenames from Vite)
-    const cacheHeader = ext === '.html'
-      ? 'no-cache, no-store, must-revalidate'
-      : 'public, max-age=31536000, immutable';
-
-    res.writeHead(200, {
-      'Content-Type': contentType,
-      'Cache-Control': cacheHeader,
-    });
-    res.end(content);
-  } catch {
-    res.writeHead(404);
-    res.end('Not found');
-  }
-});
-
-// ══════════════════════════════════════════════════════════════
-// KEN AUTONOMOUS TRADING BRAIN — 24/7 MARKET INTELLIGENCE
-// Scans every 15 min: Kalshi markets, Reddit, news, Polymarket
-// Stores everything in PostgreSQL, tracks own performance
-// ══════════════════════════════════════════════════════════════
-
-const KEN_DB = 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
-const kenPool = new pg.Pool({ connectionString: KEN_DB, max: 20, idleTimeoutMillis: 30000 });
-async function kenQ(text, params) { return kenPool.query(text, params); }
-
-// ── Mutable scan/pipeline intervals (adjustable via Settings UI) ──
-let SCAN_INTERVAL_MS = 8 * 60 * 1000; // 8 minutes — market scans
-let REDDIT_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes — intelligence sweep
-let SIGNAL_PIPELINE_MS = 2 * 60 * 1000; // 2 minutes — signal pipeline
-let RESOLVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes — trade resolution
-const SLACK_COOLDOWN_MS = 4 * 60 * 60 * 1000;
-let lastSlackTime = 0;
-let cachedActiveMarkets = [];
-let cachedMomentumMap = {};
-
-// Interval handles (stored so we can clear + reset when config changes)
-let scanIntervalId = null;
-let redditIntervalId = null;
-let signalIntervalId = null;
-let resolveIntervalId = null;
-
-// Load scan config from DB on startup
-async function loadScanConfig() {
-  try {
-    await initConfigTable();
-    const { rows } = await kenQ("SELECT value FROM ken_config WHERE key = 'scan_config'");
-    if (rows.length > 0) {
-      const cfg = rows[0].value;
-      if (cfg.scan_interval_ms) SCAN_INTERVAL_MS = cfg.scan_interval_ms;
-      if (cfg.reddit_interval_ms) REDDIT_INTERVAL_MS = cfg.reddit_interval_ms;
-      if (cfg.signal_pipeline_ms) SIGNAL_PIPELINE_MS = cfg.signal_pipeline_ms;
-      if (cfg.resolve_interval_ms) RESOLVE_INTERVAL_MS = cfg.resolve_interval_ms;
-      if (cfg.min_edge_threshold !== undefined) RISK_LIMITS.minEdgeThreshold = cfg.min_edge_threshold;
-      if (cfg.min_liquidity !== undefined) RISK_LIMITS.minLiquidity = cfg.min_liquidity;
-      console.log(`[Ken] Loaded scan_config: scan=${SCAN_INTERVAL_MS/1000}s, signal=${SIGNAL_PIPELINE_MS/1000}s, sweep=${REDDIT_INTERVAL_MS/1000}s, resolve=${RESOLVE_INTERVAL_MS/1000}s, edge=${RISK_LIMITS.minEdgeThreshold}, liq=${RISK_LIMITS.minLiquidity}`);
-    }
-  } catch (e) {
-    console.log(`[Ken] Failed to load scan config: ${e.message}`);
-  }
-}
-
-async function saveScanConfig() {
-  try {
-    await kenQ(`
-      INSERT INTO ken_config (key, value, updated_at) VALUES ('scan_config', $1, NOW())
-      ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()
-    `, [JSON.stringify({
-      scan_interval_ms: SCAN_INTERVAL_MS,
-      reddit_interval_ms: REDDIT_INTERVAL_MS,
-      signal_pipeline_ms: SIGNAL_PIPELINE_MS,
-      resolve_interval_ms: RESOLVE_INTERVAL_MS,
-      min_edge_threshold: RISK_LIMITS.minEdgeThreshold,
-      min_liquidity: RISK_LIMITS.minLiquidity,
-    })]);
-  } catch (e) {
-    console.log(`[Ken] Failed to save scan config: ${e.message}`);
-  }
-}
-
-function restartIntervals() {
-  if (scanIntervalId) clearInterval(scanIntervalId);
-  if (redditIntervalId) clearInterval(redditIntervalId);
-  if (signalIntervalId) clearInterval(signalIntervalId);
-  if (resolveIntervalId) clearInterval(resolveIntervalId);
-
-  scanIntervalId = setInterval(() => runScanSafe(autonomousScan), SCAN_INTERVAL_MS);
-  redditIntervalId = setInterval(() => runSweepSafe('Reddit', redditIntelligenceLoop), REDDIT_INTERVAL_MS);
-  signalIntervalId = setInterval(async () => {
-    if (cachedActiveMarkets.length > 0) {
-      try { await signalPipeline(cachedActiveMarkets); } catch (e) { console.log(`[Ken] Fast pipeline error: ${e.message}`); }
-    }
-  }, SIGNAL_PIPELINE_MS);
-  resolveIntervalId = setInterval(() => {
-    resolvePortfolioTrades().catch(e => console.error('[Ken] Portfolio resolution error:', e.message));
-  }, RESOLVE_INTERVAL_MS);
-
-  console.log(`[Ken] Intervals restarted: scan=${SCAN_INTERVAL_MS/60000}m, signal=${SIGNAL_PIPELINE_MS/60000}m, sweep=${REDDIT_INTERVAL_MS/60000}m, resolve=${RESOLVE_INTERVAL_MS/60000}m`);
-}
-
-// ── Reddit Scraper (no auth needed — uses .json API) ──
-async function scrapeReddit(subreddit, searchTerm, limit = 25) {
-  try {
-    const encoded = encodeURIComponent(searchTerm);
-    const url = `https://www.reddit.com/r/${subreddit}/search.json?q=${encoded}&sort=new&restrict_sr=on&limit=${limit}&t=week`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, {
-      headers: { 'User-Agent': 'Ken-Bot/2.0 (trading intelligence)' },
-      signal: controller.signal,
-    });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const data = await res.json();
-    const posts = (data?.data?.children || []).map(c => ({
-      title: c.data.title,
-      selftext: (c.data.selftext || '').substring(0, 500),
-      score: c.data.score || 0,
-      num_comments: c.data.num_comments || 0,
-      url: `https://reddit.com${c.data.permalink}`,
-      created: new Date(c.data.created_utc * 1000),
-      subreddit: c.data.subreddit,
-    }));
-    return posts;
-  } catch (e) {
-    console.log(`[Ken] Reddit scrape failed r/${subreddit} "${searchTerm}": ${e.message}`);
-    return [];
-  }
-}
-
-// ── Scrape Reddit hot/top posts for general sentiment ──
-async function scrapeSubredditHot(subreddit, limit = 15) {
-  try {
-    const url = `https://www.reddit.com/r/${subreddit}/hot.json?limit=${limit}`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, {
-      headers: { 'User-Agent': 'Ken-Bot/2.0 (trading intelligence)' },
-      signal: controller.signal,
-    });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data?.data?.children || []).map(c => ({
-      title: c.data.title,
-      selftext: (c.data.selftext || '').substring(0, 500),
-      score: c.data.score || 0,
-      num_comments: c.data.num_comments || 0,
-      url: `https://reddit.com${c.data.permalink}`,
-      created: new Date(c.data.created_utc * 1000),
-      subreddit: c.data.subreddit,
-    }));
-  } catch (e) {
-    return [];
-  }
-}
-
-// ── Google News RSS scraper ──
-async function scrapeGoogleNews(query, limit = 10) {
-  try {
-    const encoded = encodeURIComponent(query);
-    const url = `https://news.google.com/rss/search?q=${encoded}&hl=en-US&gl=US&ceid=US:en`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, { signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const xml = await res.text();
-    // Simple XML parse for RSS items
-    const items = [];
-    const itemRegex = /<item>([\s\S]*?)<\/item>/g;
-    let match;
-    while ((match = itemRegex.exec(xml)) && items.length < limit) {
-      const block = match[1];
-      const title = (block.match(/<title>([\s\S]*?)<\/title>/) || [])[1] || '';
-      const link = (block.match(/<link>([\s\S]*?)<\/link>/) || [])[1] || '';
-      const pubDate = (block.match(/<pubDate>([\s\S]*?)<\/pubDate>/) || [])[1] || '';
-      const source = (block.match(/<source[^>]*>([\s\S]*?)<\/source>/) || [])[1] || '';
-      items.push({
-        title: title.replace(/<!\[CDATA\[|\]\]>/g, '').trim(),
-        url: link.trim(),
-        source: source.replace(/<!\[CDATA\[|\]\]>/g, '').trim(),
-        published: pubDate ? new Date(pubDate) : new Date(),
-      });
-    }
-    return items;
-  } catch (e) {
-    console.log(`[Ken] News scrape failed "${query}": ${e.message}`);
-    return [];
-  }
-}
-
-// ── Polymarket cross-reference (public events API) ──
-async function scrapePolymarketEvents(limit = 100) {
-  try {
-    const url = `https://gamma-api.polymarket.com/events?closed=false&limit=${limit}&order=volume&ascending=false`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 15000);
-    const res = await fetch(url, { signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const events = await res.json();
-    const results = [];
-    for (const evt of (events || [])) {
-      const mkts = (evt.markets || []).sort((a, b) => (b.volume || 0) - (a.volume || 0));
-      for (const m of mkts.slice(0, 3)) {
-        let price = null;
-        try { price = Math.round(parseFloat(JSON.parse(m.outcomePrices)[0]) * 100); } catch {}
-        if (price !== null && price > 0 && price < 100) {
-          results.push({
-            event_title: evt.title || '',
-            question: m.question || '',
-            price,
-            volume: Math.round(m.volume || 0),
-            slug: evt.slug || m.slug || '',
-          });
-        }
-      }
-    }
-    return results;
-  } catch (e) {
-    console.log(`[Ken] Polymarket scrape failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── Simple keyword sentiment scorer ──
-function scoreSentiment(text) {
-  const t = (text || '').toLowerCase();
-  const bullish = ['confirmed', 'likely', 'expected', 'approved', 'passed', 'agreed', 'winning', 'surge', 'soaring', 'breaking', 'imminent', 'certain', 'guarantee', 'official', 'announced', 'done deal'];
-  const bearish = ['unlikely', 'denied', 'rejected', 'failed', 'blocked', 'withdrawn', 'canceled', 'impossible', 'no chance', 'not going', 'walked back', 'reversed', 'postponed', 'delayed', 'collapse'];
-  const uncertain = ['maybe', 'could', 'might', 'rumor', 'speculation', 'sources say', 'unconfirmed', 'developing', 'unclear'];
-
-  let score = 0;
-  for (const w of bullish) if (t.includes(w)) score += 0.15;
-  for (const w of bearish) if (t.includes(w)) score -= 0.15;
-  for (const w of uncertain) if (t.includes(w)) score *= 0.7;
-  return Math.max(-1, Math.min(1, score));
-}
-
-// ── Map keywords in text to Kalshi event tickers ──
-function matchTextToMarkets(text, activeMarkets) {
-  const t = (text || '').toLowerCase();
-  const matches = [];
-  const keywords = {
-    'fed chair': 'KXFEDCHAIRNOM',
-    'federal reserve': 'KXFEDCHAIRNOM',
-    'kevin warsh': 'KXFEDCHAIRNOM',
-    'greenland': 'KXGREEN',
-    'cabinet': 'KXCABOUT',
-    'gabbard': 'KXCABOUT',
-    'tulsi': 'KXCABOUT',
-    'hegseth': 'KXCABOUT',
-    'defense secretary': 'KXTRUMPDEFCOUNT',
-    'insurrection act': 'KXINSURRECTION',
-    'supreme court': 'KXSCOTUS',
-    'seattle basketball': 'KXNBASEATTLE',
-    'seattle nba': 'KXNBASEATTLE',
-    '2028 election': 'KXPRESPERSON',
-    'presidential': 'KXPRESPERSON',
-  };
-  for (const [kw, prefix] of Object.entries(keywords)) {
-    if (t.includes(kw)) {
-      const related = activeMarkets.filter(m => m.event_ticker?.startsWith(prefix) || m.ticker?.startsWith(prefix));
-      for (const m of related) {
-        if (!matches.find(x => x.ticker === m.ticker)) matches.push(m);
-      }
-    }
-  }
-  return matches;
-}
-
-function generateSyntheticWeather(cities) {
-  const month = new Date().getMonth();
-  const results = [];
-  for (const city of cities) {
-    const coords = CITY_COORDS[city]; if (!coords) continue;
-    const baseTemp = [35,38,48,58,68,78,85,83,75,62,48,38][month] + (coords.lat < 35 ? 15 : coords.lat > 42 ? -8 : 0);
-    const temps = Array.from({length: 14}, (_, i) => baseTemp + Math.round((Math.random() - 0.5) * 12) + (i < 7 ? 3 : -2));
-    const precips = Array.from({length: 14}, () => Math.round(Math.random() * 60));
-    const winds = Array.from({length: 14}, () => 5 + Math.round(Math.random() * 15));
-    const mkStat = (arr) => ({ mean: arr.reduce((a,b)=>a+b)/arr.length, min: Math.min(...arr), max: Math.max(...arr), latest: arr[0] });
-    results.push({
-      city, synthetic: true,
-      temperature: { variable: 'temperature', unit: 'F', values: temps, ...mkStat(temps) },
-      precipitation: { variable: 'precipitation', unit: 'percent', values: precips, ...mkStat(precips) },
-      wind: { variable: 'wind', unit: 'mph', values: winds, ...mkStat(winds) },
-    });
-  }
-  return results;
-}
-
-// ── Hacker News scraper (top stories + search) ──
-// ── GDELT TV News mentions (covers CNN, MSNBC, Fox News, BBC captions) ──
-async function scrapeGDELTtv(query, station = 'CNN', limit = 15) {
-  try {
-    const url = `https://api.gdeltproject.org/api/v2/tv/tv?query=${encodeURIComponent(query)}%20station:${station}&mode=clipgallery&format=json&maxrecords=${limit}&last24hours=yes`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 15000);
-    const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'Ken-Bot/2.0' } });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const text = await res.text();
-    if (!text.startsWith('{')) return []; // API returns error text, not JSON
-    const data = JSON.parse(text);
-    return (data.clips || []).map(c => ({
-      title: c.snippet || '',
-      url: c.preview_url || c.url || '',
-      source: c.station || 'TV News',
-      show: c.show || '',
-      date: c.date || '',
-    }));
-  } catch (e) {
-    return [];
-  }
-}
-
-// ── YouTube RSS search (public API, no key needed) ──
-async function scrapeYouTubeRSS(query) {
-  try {
-    const url = `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}&sp=CAISBAgBEAE%253D`; // last hour filter
-    // YouTube doesn't have direct RSS search, use channel feeds for major news channels
-    const channels = [
-      { id: 'UCupvZG-5ko_eiXAupbDfxWw', name: 'CNN' },
-      { id: 'UCaXkIU1QidjPwiAYu6GcHjg', name: 'MSNBC' },
-      { id: 'UCXIJgqnII2ZOINSWNOGFThA', name: 'Fox News' },
-      { id: 'UC16niRr50-MSBwiO3YDb3RA', name: 'Bloomberg TV' },
-      { id: 'UCvJJ_dzjViJCoLf5uKUTwoA', name: 'CNBC' },
-      { id: 'UCknLrEdhRCp1aegoMqRaCZg', name: 'NBC News' },
-      { id: 'UCBi2mrWuNuyYy4gbM6fU18Q', name: 'ABC News' },
-      { id: 'UC8p1vwvWtl6T73JiExfWs1g', name: 'CBS News' },
-      { id: 'UC52X5wxOL_s5yw0dQk7NtgA', name: 'PBS NewsHour' },
-    ];
-    const results = [];
-    for (const ch of channels) {
-      try {
-        const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${ch.id}`;
-        const items = await scrapeRSSFeed(feedUrl, `YT ${ch.name}`, 5);
-        results.push(...items);
-      } catch {}
-    }
-    return results;
-  } catch { return []; }
-}
-
-// ── Lemmy (federated Reddit) — public JSON API, no auth needed ──
-async function scrapeLemmy(sort = 'Hot', limit = 20) {
-  const instances = ['lemmy.world', 'lemmy.ml', 'sh.itjust.works'];
-  const results = [];
-  for (const inst of instances) {
-    try {
-      const controller = new AbortController();
-      const timeout = setTimeout(() => controller.abort(), 10000);
-      const res = await fetch(`https://${inst}/api/v3/post/list?sort=${sort}&limit=${limit}&type_=All`, {
-        signal: controller.signal,
-        headers: { 'Accept': 'application/json' },
-      });
-      clearTimeout(timeout);
-      if (!res.ok) continue;
-      const data = await res.json();
-      for (const p of (data.posts || [])) {
-        const post = p.post || {};
-        const counts = p.counts || {};
-        if (post.name && post.name.length > 10) {
-          results.push({
-            title: post.name.substring(0, 500),
-            url: post.ap_id || `https://${inst}/post/${post.id}`,
-            source: `Lemmy ${inst}`,
-            score: counts.score || 0,
-            num_comments: counts.comments || 0,
-          });
-        }
-      }
-      if (results.length > 0) break; // got data from one instance
-    } catch {}
-  }
-  return results;
-}
-
-// ── Lobsters (tech/politics news) — public JSON API ──
-async function scrapeLobsters(limit = 25) {
-  try {
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch('https://lobste.rs/hottest.json', { signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const stories = await res.json();
-    return stories.slice(0, limit).map(s => ({
-      title: s.title || '',
-      url: s.url || s.short_id_url || '',
-      source: 'Lobsters',
-      score: s.score || 0,
-      num_comments: s.comment_count || 0,
-      author: s.submitter_user?.username || '',
-      tags: (s.tags || []).join(', '),
-    }));
-  } catch { return []; }
-}
-
-// ── Tildes (curated discussion) — RSS feed ──
-async function scrapeTildes() {
-  try {
-    return await scrapeRSSFeed('https://tildes.net/topics.rss', 'Tildes', 20);
-  } catch { return []; }
-}
-
-// ── Manifold Markets (open prediction market — public API) ──
-async function scrapeManifold(queries) {
-  const results = [];
-  for (const q of queries) {
-    try {
-      const controller = new AbortController();
-      const timeout = setTimeout(() => controller.abort(), 8000);
-      const res = await fetch(`https://manifold.markets/api/v0/search-markets?term=${encodeURIComponent(q)}&sort=liquidity&limit=10`, {
-        signal: controller.signal, headers: { 'Accept': 'application/json' },
-      });
-      clearTimeout(timeout);
-      if (!res.ok) continue;
-      const markets = await res.json();
-      for (const m of (markets || [])) {
-        if (m.question && m.probability != null && (m.volume || 0) > 100) {
-          results.push({
-            title: m.question,
-            url: m.url || `https://manifold.markets/${m.creatorUsername}/${m.slug}`,
-            source: 'Manifold',
-            probability: Math.round((m.probability || 0) * 100),
-            volume: Math.round(m.volume || 0),
-            liquidity: Math.round(m.totalLiquidity || 0),
-          });
-        }
-      }
-    } catch {}
-    await new Promise(r => setTimeout(r, 500));
-  }
-  return results;
-}
-
-// ── Mastodon/Fediverse trending (public API, no auth) ──
-async function scrapeMastodon() {
-  const instances = ['mastodon.social', 'mas.to'];
-  const results = [];
-  for (const inst of instances) {
-    try {
-      const controller = new AbortController();
-      const timeout = setTimeout(() => controller.abort(), 8000);
-      const res = await fetch(`https://${inst}/api/v1/trends/statuses?limit=10`, {
-        signal: controller.signal,
-      });
-      clearTimeout(timeout);
-      if (!res.ok) continue;
-      const statuses = await res.json();
-      for (const s of statuses) {
-        const text = (s.content || '').replace(/<[^>]+>/g, '').trim();
-        if (text.length > 20) {
-          results.push({
-            title: text.substring(0, 500),
-            url: s.url || s.uri || '',
-            source: `Mastodon ${inst}`,
-            score: s.favourites_count || 0,
-            num_comments: s.replies_count || 0,
-          });
-        }
-      }
-    } catch {}
-  }
-  return results;
-}
-
-// ── Bluesky trending (public API — using feed generators, not searchPosts which is 403) ──
-async function scrapeBluesky() {
-  const feeds = [
-    { uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot', name: 'whats-hot' },
-    { uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic', name: 'hot-classic' },
-  ];
-  const results = [];
-  for (const feed of feeds) {
-    try {
-      const controller = new AbortController();
-      const timeout = setTimeout(() => controller.abort(), 10000);
-      const url = `https://public.api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${encodeURIComponent(feed.uri)}&limit=20`;
-      const res = await fetch(url, { signal: controller.signal, headers: { 'Accept': 'application/json' } });
-      clearTimeout(timeout);
-      if (!res.ok) continue;
-      const data = await res.json();
-      for (const item of (data.feed || [])) {
-        const p = item.post || {};
-        const text = (p.record?.text || '').trim();
-        if (text.length > 20) {
-          results.push({
-            title: text.substring(0, 500),
-            url: `https://bsky.app/profile/${p.author?.handle}/post/${p.uri?.split('/').pop()}`,
-            source: `Bluesky ${feed.name}`,
-            author: p.author?.handle || '',
-            score: (p.likeCount || 0) + (p.repostCount || 0),
-            num_comments: p.replyCount || 0,
-          });
-        }
-      }
-    } catch {}
-  }
-  return results;
-}
-
-async function scrapeHackerNews(query, limit = 15) {
-  try {
-    const url = query
-      ? `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(query)}&tags=story&hitsPerPage=${limit}`
-      : `https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=${limit}`;
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, { signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.hits || []).map(h => ({
-      title: h.title || '',
-      url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,
-      score: h.points || 0,
-      num_comments: h.num_comments || 0,
-      created: new Date(h.created_at),
-      source: 'hackernews',
-    }));
-  } catch (e) {
-    console.log(`[Ken] HN scrape failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── AP News RSS scraper ──
-// ── Generic RSS feed scraper ──
-async function scrapeRSSFeed(url, source, limit = 15) {
-  try {
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, {
-      signal: controller.signal,
-      headers: { 'User-Agent': 'Ken-Bot/2.0 (RSS Reader)' },
-    });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const xml = await res.text();
-    const items = [];
-    // Handle both <item> (RSS) and <entry> (Atom) formats
-    const itemRegex = /<(?:item|entry)>([\s\S]*?)<\/(?:item|entry)>/g;
-    let match;
-    while ((match = itemRegex.exec(xml)) && items.length < limit) {
-      const block = match[1];
-      const title = (block.match(/<title[^>]*>([\s\S]*?)<\/title>/) || [])[1] || '';
-      const link = (block.match(/<link[^>]*href="([^"]+)"/) || block.match(/<link>([\s\S]*?)<\/link>/) || [])[1] || '';
-      const cleanTitle = title.replace(/<!\[CDATA\[|\]\]>/g, '').replace(/<[^>]+>/g, '').trim();
-      if (cleanTitle && cleanTitle.length > 10) {
-        items.push({ title: cleanTitle, url: link.trim(), source });
-      }
-    }
-    return items;
-  } catch { return []; }
-}
-
-// ── All news channel feeds ──
-const NEWS_FEEDS = [
-  // ═══ POLITICS & POLICY (Kalshi political markets) ═══
-  { url: 'https://feeds.bbci.co.uk/news/world/rss.xml', source: 'BBC World' },
-  { url: 'https://feeds.bbci.co.uk/news/politics/rss.xml', source: 'BBC Politics' },
-  { url: 'https://feeds.bbci.co.uk/news/us_and_canada/rss.xml', source: 'BBC US' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Politics.xml', source: 'NYT Politics' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/World.xml', source: 'NYT World' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/US.xml', source: 'NYT US' },
-  { url: 'https://feeds.washingtonpost.com/rss/politics', source: 'WaPo Politics' },
-  { url: 'https://feeds.washingtonpost.com/rss/world', source: 'WaPo World' },
-  { url: 'https://www.politico.com/rss/politicopicks.xml', source: 'Politico' },
-  { url: 'https://www.politico.com/rss/congress.xml', source: 'Politico Congress' },
-  { url: 'https://thehill.com/feed/', source: 'The Hill' },
-  { url: 'https://feeds.npr.org/1001/rss.xml', source: 'NPR News' },
-  { url: 'https://feeds.npr.org/1014/rss.xml', source: 'NPR Politics' },
-  { url: 'https://www.aljazeera.com/xml/rss/all.xml', source: 'Al Jazeera' },
-  { url: 'https://rss.dw.com/xml/rss-en-all', source: 'DW News' },
-  { url: 'https://feeds.skynews.com/feeds/rss/world.xml', source: 'Sky News World' },
-  { url: 'https://feeds.skynews.com/feeds/rss/us.xml', source: 'Sky News US' },
-  { url: 'https://www.theguardian.com/us-news/rss', source: 'Guardian US' },
-  { url: 'https://www.theguardian.com/world/rss', source: 'Guardian World' },
-  { url: 'https://rss.cbc.ca/lineup/world.xml', source: 'CBC World' },
-  { url: 'https://feeds.abcnews.com/abcnews/politicsheadlines', source: 'ABC Politics' },
-  { url: 'http://feeds.foxnews.com/foxnews/politics', source: 'Fox Politics' },
-  { url: 'https://www.axios.com/feeds/feed.rss', source: 'Axios' },
-  { url: 'https://www.vox.com/rss/index.xml', source: 'Vox' },
-
-  // ═══ ECONOMICS & FINANCIAL MARKETS (Kalshi econ/rate markets) ═══
-  { url: 'https://feeds.bloomberg.com/politics/news.rss', source: 'Bloomberg Politics' },
-  { url: 'https://feeds.bloomberg.com/markets/news.rss', source: 'Bloomberg Markets' },
-  { url: 'https://www.cnbc.com/id/10000664/device/rss/rss.html', source: 'CNBC Top' },
-  { url: 'https://www.cnbc.com/id/10001147/device/rss/rss.html', source: 'CNBC Economy' },
-  { url: 'https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=15839069', source: 'CNBC Politics' },
-  { url: 'https://www.cnbc.com/id/20910258/device/rss/rss.html', source: 'CNBC Finance' },
-  { url: 'https://feeds.marketwatch.com/marketwatch/topstories/', source: 'MarketWatch' },
-  { url: 'https://feeds.marketwatch.com/marketwatch/marketpulse/', source: 'MarketWatch Pulse' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Business.xml', source: 'NYT Business' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Economy.xml', source: 'NYT Economy' },
-  { url: 'https://www.ft.com/rss/home/us', source: 'FT US' },
-  { url: 'https://www.investing.com/rss/news.rss', source: 'Investing.com' },
-  { url: 'https://www.federalreserve.gov/feeds/press_all.xml', source: 'Fed Reserve' },
-  { url: 'https://www.bis.org/doclist/bis_fsi_publs.rss', source: 'BIS' },
-  { url: 'https://seekingalpha.com/feed.xml', source: 'Seeking Alpha' },
-  { url: 'https://finance.yahoo.com/news/rssindex', source: 'Yahoo Finance' },
-
-  // ═══ CRYPTO & PREDICTION MARKETS (cross-ref with Polymarket) ═══
-  { url: 'https://cointelegraph.com/rss', source: 'CoinTelegraph' },
-  { url: 'https://www.coindesk.com/arc/outboundfeeds/rss/', source: 'CoinDesk' },
-  { url: 'https://decrypt.co/feed', source: 'Decrypt' },
-  { url: 'https://thedefiant.io/feed', source: 'The Defiant' },
-
-  // ═══ PREDICTION & POLLING ANALYSIS ═══
-  { url: 'https://fivethirtyeight.com/features/feed/', source: 'FiveThirtyEight' },
-  { url: 'https://www.realclearpolitics.com/index.xml', source: 'RealClearPolitics' },
-  { url: 'https://electionbettingodds.com/feed', source: 'ElectionBettingOdds' },
-  { url: 'https://www.predictit.org/feed', source: 'PredictIt' },
-
-  // ═══ TECH & AI POLICY (Kalshi tech markets) ═══
-  { url: 'https://www.theverge.com/rss/index.xml', source: 'The Verge' },
-  { url: 'https://techcrunch.com/feed/', source: 'TechCrunch' },
-  { url: 'https://arstechnica.com/feed/', source: 'Ars Technica' },
-  { url: 'https://www.wired.com/feed/rss', source: 'WIRED' },
-  { url: 'https://feeds.feedburner.com/venturebeat/SZYF', source: 'VentureBeat' },
-
-  // ═══ WEATHER & CLIMATE (Kalshi weather markets - temps, snow, etc.) ═══
-  { url: 'https://www.weather.gov/rss_page.php?site_name=nws', source: 'NWS' },
-  { url: 'https://weather.com/feeds/enhancedconditions/rss', source: 'Weather.com' },
-  { url: 'https://www.accuweather.com/en/us/national/current-weather/rss', source: 'AccuWeather' },
-  { url: 'https://www.climate.gov/rss.xml', source: 'NOAA Climate' },
-
-  // ═══ SPORTS (Kalshi has sports-adjacent markets) ═══
-  { url: 'https://www.espn.com/espn/rss/news', source: 'ESPN' },
-  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Sports.xml', source: 'NYT Sports' },
-
-  // ═══ ENTERTAINMENT & CULTURE (Kalshi awards/culture markets) ═══
-  { url: 'https://variety.com/feed/', source: 'Variety' },
-  { url: 'https://deadline.com/feed/', source: 'Deadline' },
-  { url: 'https://www.hollywoodreporter.com/feed/', source: 'Hollywood Reporter' },
-
-  // ═══ ENERGY & COMMODITIES (Kalshi gas/oil markets) ═══
-  { url: 'https://oilprice.com/rss/main', source: 'OilPrice' },
-  { url: 'https://www.eia.gov/rss/todayinenergy.xml', source: 'EIA Energy' },
-
-  // ═══ DEFENSE & GEOPOLITICS (conflict/territory markets) ═══
-  { url: 'https://www.defensenews.com/arc/outboundfeeds/rss/category/global/?outputType=xml', source: 'Defense News' },
-  { url: 'https://foreignpolicy.com/feed/', source: 'Foreign Policy' },
-  { url: 'https://warontherocks.com/feed/', source: 'War On The Rocks' },
-
-  // ═══ SCIENCE & SPACE (Kalshi space markets) ═══
-  { url: 'https://www.nasa.gov/rss/dyn/breaking_news.rss', source: 'NASA' },
-  { url: 'https://www.space.com/feeds/all', source: 'Space.com' },
-  { url: 'https://www.nature.com/nature.rss', source: 'Nature' },
-  { url: 'https://www.science.org/rss/news_current.xml', source: 'Science' },
-];
-
-async function scrapeAllNewsFeeds() {
-  const allArticles = [];
-  // Process feeds in batches of 15 concurrently for speed (maxed for 16 cores)
-  const batchSize = 15;
-  for (let i = 0; i < NEWS_FEEDS.length; i += batchSize) {
-    const batch = NEWS_FEEDS.slice(i, i + batchSize);
-    const results = await Promise.allSettled(
-      batch.map(feed => scrapeRSSFeed(feed.url, feed.source, 8))
-    );
-    for (const result of results) {
-      if (result.status === 'fulfilled') allArticles.push(...result.value);
-    }
-    await new Promise(r => setTimeout(r, 100)); // minimal pause between batches
-  }
-  return allArticles;
-}
-
-// ── Metaculus predictions scraper (prediction market cross-ref) ──
-async function scrapeMetaculus() {
-  try {
-    const url = 'https://www.metaculus.com/api2/questions/?limit=20&order_by=-activity&status=open&type=forecast';
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 10000);
-    const res = await fetch(url, { headers: { 'Accept': 'application/json' }, signal: controller.signal });
-    clearTimeout(timeout);
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.results || []).map(q => ({
-      title: q.title || '',
-      url: `https://www.metaculus.com/questions/${q.id}`,
-      community_prediction: q.community_prediction?.full?.q2,
-      num_predictions: q.number_of_predictions || 0,
-      source: 'metaculus',
-    }));
-  } catch (e) {
-    console.log(`[Ken] Metaculus scrape failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── PredictIt API (another prediction market for cross-reference) ──
-async function scrapePredictIt() {
-  try {
-    const controller = new AbortController();
-    setTimeout(() => controller.abort(), 8000);
-    const res = await fetch('https://www.predictit.org/api/marketdata/all/', {
-      signal: controller.signal,
-      headers: { 'User-Agent': 'Ken-Bot/1.0' }
-    });
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.markets || []).slice(0, 100).map(m => ({
-      title: m.name,
-      url: m.url || `https://www.predictit.org/markets/detail/${m.id}`,
-      score: Math.round((m.contracts?.[0]?.lastTradePrice || 0) * 100),
-      num_comments: m.contracts?.length || 0,
-      source: 'predictit',
-      extra: JSON.stringify({
-        contracts: (m.contracts || []).slice(0, 5).map(c => ({
-          name: c.name, lastPrice: c.lastTradePrice, bestBuy: c.bestBuyYesCost, bestSell: c.bestSellYesCost
-        }))
-      })
-    }));
-  } catch (e) {
-    console.log(`[Ken] PredictIt scrape failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── FRED Economic Data API (real economic indicators — no key needed for some endpoints) ──
-async function scrapeFredIndicators() {
-  const indicators = [
-    { series: 'CPIAUCSL', name: 'CPI Inflation' },
-    { series: 'UNRATE', name: 'Unemployment Rate' },
-    { series: 'GDP', name: 'GDP Growth' },
-    { series: 'FEDFUNDS', name: 'Federal Funds Rate' },
-    { series: 'T10Y2Y', name: '10Y-2Y Treasury Spread' },
-    { series: 'VIXCLS', name: 'VIX Volatility Index' },
-    { series: 'DGS10', name: '10-Year Treasury Yield' },
-    { series: 'DCOILWTICO', name: 'WTI Crude Oil Price' },
-  ];
-  const results = [];
-  for (const ind of indicators) {
-    try {
-      const controller = new AbortController();
-      setTimeout(() => controller.abort(), 5000);
-      const res = await fetch(`https://fred.stlouisfed.org/graph/fredgraph.csv?id=${ind.series}&cosd=${new Date(Date.now() - 90*86400000).toISOString().split('T')[0]}`, {
-        signal: controller.signal,
-        headers: { 'User-Agent': 'Ken-Bot/1.0' }
-      });
-      if (!res.ok) continue;
-      const csv = await res.text();
-      const lines = csv.trim().split('\n');
-      if (lines.length < 2) continue;
-      const lastLine = lines[lines.length - 1];
-      const [date, value] = lastLine.split(',');
-      if (value && value !== '.') {
-        results.push({
-          title: `${ind.name}: ${value} (${date})`,
-          url: `https://fred.stlouisfed.org/series/${ind.series}`,
-          score: Math.round(parseFloat(value) * 100) || 0,
-          source: 'fred_economic',
-          keyword: ind.series,
-        });
-      }
-    } catch {}
-  }
-  return results;
-}
-
-// ── NWS Active Weather Alerts (real severe weather data for weather markets) ──
-async function scrapeNWSAlerts() {
-  try {
-    const controller = new AbortController();
-    setTimeout(() => controller.abort(), 8000);
-    const res = await fetch('https://api.weather.gov/alerts/active?status=actual&message_type=alert&limit=50', {
-      signal: controller.signal,
-      headers: { 'User-Agent': 'Ken-Bot/1.0 (ken@designerwallcoverings.com)', 'Accept': 'application/geo+json' }
-    });
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.features || []).map(f => ({
-      title: `${f.properties.event}: ${f.properties.headline || f.properties.description?.substring(0, 200) || ''}`.substring(0, 500),
-      url: f.properties['@id'] || 'https://alerts.weather.gov',
-      score: f.properties.severity === 'Extreme' ? 100 : f.properties.severity === 'Severe' ? 75 : f.properties.severity === 'Moderate' ? 50 : 25,
-      num_comments: 0,
-      source: 'nws_alerts',
-      keyword: f.properties.event || 'weather',
-    }));
-  } catch (e) {
-    console.log(`[Ken] NWS Alerts failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── Congressional/Government Activity (GovTrack API) ──
-async function scrapeGovTrack() {
-  try {
-    const controller = new AbortController();
-    setTimeout(() => controller.abort(), 8000);
-    const res = await fetch('https://www.govtrack.us/api/v2/vote?order_by=-created&limit=20', {
-      signal: controller.signal,
-      headers: { 'User-Agent': 'Ken-Bot/1.0' }
-    });
-    if (!res.ok) return [];
-    const data = await res.json();
-    return (data.objects || []).map(v => ({
-      title: `${v.chamber === 'senate' ? 'Senate' : 'House'} Vote: ${v.question || v.result || 'Unknown'}`.substring(0, 500),
-      url: `https://www.govtrack.us${v.link || ''}`,
-      score: v.total_plus || 0,
-      num_comments: v.total_minus || 0,
-      source: 'govtrack',
-      keyword: v.category || 'legislation',
-    }));
-  } catch (e) {
-    console.log(`[Ken] GovTrack failed: ${e.message}`);
-    return [];
-  }
-}
-
-// ── X/Twitter via Nitter instances (public, no auth needed) ──
-async function scrapeNitterTrending() {
-  const nitterInstances = [
-    'https://nitter.poast.org',
-    'https://nitter.privacydev.net',
-    'https://nitter.cz',
-  ];
-  const searchTerms = ['kalshi', 'polymarket', 'prediction market', 'fed rate decision', 'hurricane season'];
-  const results = [];
-  for (const term of searchTerms) {
-    for (const instance of nitterInstances) {
-      try {
-        const controller = new AbortController();
-        setTimeout(() => controller.abort(), 6000);
-        const url = `${instance}/search?f=tweets&q=${encodeURIComponent(term)}&since=`;
-        const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'Mozilla/5.0' } });
-        if (!res.ok) continue;
-        const html = await res.text();
-        // Extract tweet text from Nitter HTML
-        const tweetMatches = html.match(/<div class="tweet-content[^"]*"[^>]*>([\s\S]*?)<\/div>/g) || [];
-        for (const match of tweetMatches.slice(0, 5)) {
-          const text = match.replace(/<[^>]+>/g, '').trim().substring(0, 500);
-          if (text.length > 10) {
-            results.push({
-              title: text,
-              url: `${instance}/search?q=${encodeURIComponent(term)}`,
-              score: 0,
-              num_comments: 0,
-              source: 'twitter_nitter',
-              keyword: term,
-            });
-          }
-        }
-        break; // One working instance per term is enough
-      } catch {} // Nitter instances are unreliable, fail silently
-    }
-    await new Promise(r => setTimeout(r, 2000));
-  }
-  return results;
-}
-
-// ── Discord Bot Intelligence (reads messages from joined servers) ──
-const DISCORD_BOT_TOKEN = 'MTQ3MjYyNDg2OTk3NDg2ODA2Mg.GifeyF.C5l9KqQUia-Ex54Ldg23fuxislxwf2QtmkXYII';
-const DISCORD_API = 'https://discord.com/api/v10';
-
-// Channel names to monitor (matched case-insensitive) — covers all Kalshi/Polymarket categories
-const DISCORD_CHANNEL_KEYWORDS = [
-  // Core
-  'general', 'chat', 'discussion', 'lounge', 'main',
-  // Trading & Markets
-  'trading', 'markets', 'predictions', 'analysis', 'strategy', 'signals', 'alerts',
-  'arbitrage', 'polymarket', 'kalshi', 'bets', 'picks', 'plays', 'odds',
-  // News & Facts
-  'news', 'breaking', 'headlines', 'updates', 'osint', 'intel', 'events',
-  // Weather
-  'weather', 'hurricane', 'tropical', 'storm', 'forecast', 'severe', 'climate',
-  // Politics & Elections
-  'politics', 'election', 'polling', 'congress', 'senate', 'trump', 'biden',
-  'policy', 'legislation', 'vote', 'debate',
-  // Economics & Finance
-  'economics', 'economy', 'finance', 'stocks', 'fed', 'inflation', 'rates',
-  'macro', 'earnings', 'options', 'futures', 'bonds',
-  // Sports
-  'sports', 'nfl', 'nba', 'mlb', 'nhl', 'soccer', 'football', 'basketball',
-  'baseball', 'ufc', 'mma', 'boxing', 'f1', 'golf', 'tennis',
-  // Crypto
-  'crypto', 'bitcoin', 'btc', 'ethereum', 'eth', 'defi', 'altcoin', 'solana',
-  // Geopolitics
-  'geopolitics', 'ukraine', 'russia', 'china', 'military', 'conflict', 'war', 'nato',
-  // Entertainment
-  'entertainment', 'movies', 'film', 'oscars', 'grammys', 'awards', 'box-office',
-  'music', 'tv', 'streaming', 'celebrity',
-  // Science & Space
-  'space', 'spacex', 'nasa', 'launch', 'science', 'rocket',
-  // Tech & AI
-  'tech', 'ai', 'artificial', 'openai', 'gpt', 'machine-learning', 'nvidia',
-];
-
-async function discordFetch(endpoint) {
-  const controller = new AbortController();
-  const timeout = setTimeout(() => controller.abort(), 10000);
-  const res = await fetch(`${DISCORD_API}${endpoint}`, {
-    headers: { Authorization: `Bot ${DISCORD_BOT_TOKEN}`, 'Content-Type': 'application/json' },
-    signal: controller.signal,
-  });
-  clearTimeout(timeout);
-  if (!res.ok) {
-    if (res.status === 429) {
-      const retry = await res.json().catch(() => ({}));
-      const wait = (retry.retry_after || 5) * 1000;
-      console.log(`[Ken] Discord rate limited, waiting ${wait}ms`);
-      await new Promise(r => setTimeout(r, wait));
-      return null;
-    }
-    return null;
-  }
-  return res.json();
-}
-
-async function scrapeDiscord(limit = 50) {
-  const results = [];
-  try {
-    // 1. Get all guilds the bot is in
-    const guilds = await discordFetch('/users/@me/guilds');
-    if (!guilds || !Array.isArray(guilds)) {
-      console.log('[Ken] Discord: no guilds found (bot may not have joined any servers yet)');
-      return [];
-    }
-    console.log(`[Ken] Discord: bot is in ${guilds.length} server(s): ${guilds.map(g => g.name).join(', ')}`);
-
-    for (const guild of guilds) {
-      // 2. Get channels for this guild
-      const channels = await discordFetch(`/guilds/${guild.id}/channels`);
-      if (!channels || !Array.isArray(channels)) continue;
-
-      // 3. Filter to text channels matching keywords
-      const textChannels = channels.filter(ch => {
-        if (ch.type !== 0) return false; // type 0 = text channel
-        const name = (ch.name || '').toLowerCase();
-        return DISCORD_CHANNEL_KEYWORDS.some(kw => name.includes(kw));
-      });
-
-      console.log(`[Ken] Discord [${guild.name}]: monitoring ${textChannels.length}/${channels.filter(c => c.type === 0).length} text channels`);
-
-      for (const channel of textChannels.slice(0, 15)) { // cap at 15 channels per guild
-        try {
-          const messages = await discordFetch(`/channels/${channel.id}/messages?limit=${Math.min(limit, 50)}`);
-          if (!messages || !Array.isArray(messages)) continue;
-
-          // Only messages from last 2 hours (fresh intel)
-          const cutoff = Date.now() - (2 * 60 * 60 * 1000);
-          for (const msg of messages) {
-            const msgTime = new Date(msg.timestamp).getTime();
-            if (msgTime < cutoff) continue;
-            if (!msg.content || msg.content.length < 20) continue;
-            if (msg.author?.bot) continue; // skip bot messages
-
-            results.push({
-              title: msg.content.substring(0, 500),
-              url: `https://discord.com/channels/${guild.id}/${channel.id}/${msg.id}`,
-              source: `Discord ${guild.name}`,
-              channel: channel.name,
-              author: msg.author?.username || 'unknown',
-              score: (msg.reactions || []).reduce((sum, r) => sum + (r.count || 0), 0),
-              num_comments: msg.referenced_message ? 1 : 0, // thread indicator
-              created: new Date(msg.timestamp),
-            });
-          }
-          // Rate limit: 500ms between channel reads
-          await new Promise(r => setTimeout(r, 500));
-        } catch (e) {
-          console.log(`[Ken] Discord channel ${channel.name} error: ${e.message}`);
-        }
-      }
-      await new Promise(r => setTimeout(r, 1000)); // between guilds
-    }
-    console.log(`[Ken] Discord: ${results.length} messages from ${guilds.length} server(s)`);
-  } catch (e) {
-    console.log(`[Ken] Discord scrape failed: ${e.message}`);
-  }
-  return results;
-}
-
-// ══════════════════════════════════════════════
-// INTELLIGENCE LOOP (every 30 min) — Reddit, HN, News, Metaculus, Discord
-// ══════════════════════════════════════════════
-function logMemory(label) {
-  const mem = process.memoryUsage();
-  const rss = Math.round(mem.rss / 1024 / 1024);
-  const heap = Math.round(mem.heapUsed / 1024 / 1024);
-  if (rss > 500) console.log(`[Ken] MEM ${label}: RSS=${rss}MB Heap=${heap}MB`);
-}
-
-async function redditIntelligenceLoop() {
-  console.log('[Ken] Running Reddit intelligence sweep...');
-  logMemory('sweep-start');
-  let totalPosts = 0;
-  try {
-    // Get tracking config from DB
-    const { rows: trackers } = await kenQ('SELECT * FROM ken_reddit_tracking WHERE enabled = true');
-
-    // Process tracked subreddits in parallel batches of 12 (maxed for 16 cores)
-    const TRACKER_BATCH = 12;
-    for (let i = 0; i < trackers.length; i += TRACKER_BATCH) {
-      const batch = trackers.slice(i, i + TRACKER_BATCH);
-      const results = await Promise.allSettled(
-        batch.map(tracker => scrapeReddit(tracker.subreddit, tracker.search_term, 20)
-          .then(posts => ({ tracker, posts })))
-      );
-      for (const result of results) {
-        if (result.status !== 'fulfilled') continue;
-        const { tracker, posts } = result.value;
-        for (const post of posts) {
-          const sentiment = scoreSentiment(post.title + ' ' + post.selftext);
-          if (post.score > 2 || post.num_comments > 1) {
-            await kenQ(`
-              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, relevance_ticker, raw_text)
-              VALUES ('reddit', $1, $2, $3, $4, $5, $6, $7, $8, $9)
-              ON CONFLICT DO NOTHING
-            `, [
-              tracker.subreddit, tracker.search_term, post.title, post.url,
-              post.score, post.num_comments, sentiment,
-              (tracker.related_tickers || [])[0] || null,
-              (post.selftext || '').substring(0, 1000),
-            ]);
-            totalPosts++;
-          }
-        }
-      }
-      await new Promise(r => setTimeout(r, 500)); // 0.5s between batches (was 1.5s)
-    }
-
-    // Also scrape hot posts from key subreddits for general intelligence
-    // Categories: Weather | Politics | Economics | Sports | Crypto | Geopolitics | Entertainment | Science/Space | Tech/AI | General Sentiment
-    const hotSubs = [
-      // ── Core prediction markets ──
-      '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',
-      // ── Economics & Finance ──
-      'economics', 'wallstreetbets', 'economy', 'inflation', 'FederalReserve',
-      'StockMarket', 'investing', 'RealEstate', 'finance', 'commodities',
-      // ── Sports ──
-      'nfl', 'nba', 'baseball', 'soccer', 'MMA', 'CFB', 'CollegeBasketball',
-      'formula1', 'golf', 'hockey', 'tennis', 'boxing',
-      // ── Crypto ──
-      'CryptoMarkets', 'Bitcoin', 'ethereum', 'CryptoCurrency', 'BitcoinMarkets', 'solana',
-      // ── Geopolitics ──
-      'worldnews', 'geopolitics', 'UkrainianConflict', 'ukraine', 'China',
-      'MiddleEastNews', 'foreignpolicy', 'military',
-      // ── Entertainment (awards/box office markets) ──
-      'movies', 'boxoffice', 'television', 'Oscars', 'Music', 'entertainment',
-      // ── Science & Space (launch/milestone markets) ──
-      'SpaceX', 'space', 'nasa', 'science', 'Futurology',
-      // ── Tech & AI ──
-      'technology', 'artificial', 'MachineLearning', 'OpenAI', 'singularity', 'Nvidia',
-      // ── General sentiment ──
-      'news', 'OutOfTheLoop', 'AskReddit', 'TrueReddit', 'dataisbeautiful',
-    ];
-    // Process subreddits in parallel batches of 12 (maxed for 16 cores)
-    const REDDIT_BATCH = 12;
-    for (let i = 0; i < hotSubs.length; i += REDDIT_BATCH) {
-      const batch = hotSubs.slice(i, i + REDDIT_BATCH);
-      const results = await Promise.allSettled(batch.map(sub => scrapeSubredditHot(sub, 8)));
-      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) {
-          if (post.score > 30) { // lower threshold to catch more sentiment signals
-            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 ('reddit_hot', $1, 'hot', $2, $3, $4, $5, $6, $7)
-              ON CONFLICT DO NOTHING
-            `, [sub, post.title, post.url, post.score, post.num_comments, sentiment, '']);
-            totalPosts++;
-          }
-        }
-      }
-      await new Promise(r => setTimeout(r, 500)); // 0.5s between batches (was 1.5s)
-    }
-    console.log(`[Ken] Reddit hot: ${hotSubs.length} subreddits across 10 categories`);
-    logMemory('after-reddit');
-    if (global.gc) global.gc();
-
-    // Scrape Google News for top market topics (covers all Kalshi categories)
-    const newsQueries = [
-      // Politics & elections
-      'trump cabinet resign', 'federal reserve chair nomination', '2028 presidential election',
-      'trump executive order', 'government shutdown', 'supreme court ruling',
-      // Economics
-      'CPI inflation report', 'federal reserve rate decision', 'jobs report unemployment',
-      'GDP growth', 'recession forecast',
-      // Weather
-      'hurricane forecast atlantic', 'severe weather outbreak', 'temperature record',
-      // Geopolitics
-      'ukraine russia ceasefire', 'china taiwan', 'middle east conflict',
-      // Crypto
-      'bitcoin price prediction', 'SEC crypto regulation',
-      // Entertainment
-      'oscar nominations', 'box office weekend',
-      // Space
-      'SpaceX launch', 'NASA artemis',
-    ];
-    // Process Google News in batches of 5 (21 serial × 1s = too slow)
-    const NEWS_QUERY_BATCH = 5;
-    for (let i = 0; i < newsQueries.length; i += NEWS_QUERY_BATCH) {
-      const batch = newsQueries.slice(i, i + NEWS_QUERY_BATCH);
-      const results = await Promise.allSettled(
-        batch.map(nq => scrapeGoogleNews(nq, 5).then(articles => ({ nq, articles })))
-      );
-      for (const result of results) {
-        if (result.status !== 'fulfilled') continue;
-        const { nq, articles } = result.value;
-        for (const article of articles) {
-          const sentiment = scoreSentiment(article.title);
-          await kenQ(`
-            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score)
-            VALUES ('google_news', $1, $2, $3, $4, 0, $5)
-            ON CONFLICT DO NOTHING
-          `, [article.source || 'news', nq, article.title, article.url, sentiment]);
-          totalPosts++;
-        }
-      }
-      await new Promise(r => setTimeout(r, 1000));
-    }
-
-    // ── Hacker News (tech/policy sentiment) ──
-    const hnQueries = ['trump policy', 'federal reserve', 'tariff', 'ukraine russia', null]; // null = front page
-    for (const hq of hnQueries) {
-      try {
-        const stories = await scrapeHackerNews(hq, 10);
-        for (const story of stories) {
-          if (story.score > 5 || story.num_comments > 3) {
-            const sentiment = scoreSentiment(story.title);
-            await kenQ(`
-              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-              VALUES ('hackernews', 'hackernews', $1, $2, $3, $4, $5, $6, '')
-              ON CONFLICT DO NOTHING
-            `, [hq || 'front_page', story.title, story.url, story.score, story.num_comments, sentiment]);
-            totalPosts++;
-          }
-        }
-      } catch {}
-      await new Promise(r => setTimeout(r, 1500));
-    }
-
-    // ── All News Channels (BBC, NYT, WaPo, Politico, CNBC, Bloomberg, etc.) ──
-    try {
-      let newsStored = 0;
-      // Process feeds in small batches to limit memory — don't hold all articles at once
-      const batchSize = 5;
-      for (let i = 0; i < NEWS_FEEDS.length; i += batchSize) {
-        const batch = NEWS_FEEDS.slice(i, i + batchSize);
-        const results = await Promise.allSettled(
-          batch.map(feed => scrapeRSSFeed(feed.url, feed.source, 6))
-        );
-        for (const result of results) {
-          if (result.status !== 'fulfilled') continue;
-          for (const article of result.value) {
-            const sentiment = scoreSentiment(article.title);
-            await kenQ(`
-              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-              VALUES ($1, $2, 'news', $3, $4, 0, $5, '')
-              ON CONFLICT DO NOTHING
-            `, [article.source.toLowerCase().replace(/\s+/g, '_'), article.source, article.title, article.url, sentiment]);
-            newsStored++;
-          }
-        }
-        await new Promise(r => setTimeout(r, 300));
-      }
-      totalPosts += newsStored;
-      console.log(`[Ken] News channels: ${newsStored} articles from ${NEWS_FEEDS.length} feeds`);
-    } catch (e) {
-      console.log(`[Ken] News channels failed: ${e.message}`);
-    }
-    logMemory('after-news');
-    if (global.gc) global.gc();
-
-    // ── Metaculus (prediction market community consensus) ──
-    try {
-      const metaQuestions = await scrapeMetaculus();
-      for (const mq of metaQuestions) {
-        const sentiment = scoreSentiment(mq.title);
-        const predText = mq.community_prediction ? `Community: ${Math.round(mq.community_prediction * 100)}%` : '';
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('metaculus', 'metaculus', 'predictions', $1, $2, $3, $4, $5, $6)
-          ON CONFLICT DO NOTHING
-        `, [mq.title, mq.url, mq.num_predictions || 0, 0, sentiment, predText]);
-        totalPosts++;
-      }
-      console.log(`[Ken] Metaculus: ${metaQuestions.length} predictions`);
-    } catch (e) {
-      console.log(`[Ken] Metaculus failed: ${e.message}`);
-    }
-
-    // ── Manifold Markets (cross-reference prediction probabilities) ──
-    let manifoldTotal = 0;
-    try {
-      const manifoldQueries = ['trump', 'fed rate', 'election 2028', 'ukraine', 'tariff'];
-      const manifoldMkts = await scrapeManifold(manifoldQueries);
-      for (const m of manifoldMkts) {
-        const sentiment = scoreSentiment(m.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('manifold', 'manifold', $1, $2, $3, $4, 0, $5, $6)
-          ON CONFLICT DO NOTHING
-        `, [m.source, m.title, m.url, m.volume || 0, sentiment, `prob:${m.probability}% vol:${m.volume}`]);
-        manifoldTotal++;
-      }
-    } catch {}
-    console.log(`[Ken] Manifold Markets: ${manifoldTotal} prediction markets`);
-
-    // ── Kalshi & Polymarket Reddit search (community sentiment on specific trades) ──
-    let predMarketRedditTotal = 0;
-    const pmSearchTerms = ['arbitrage', 'free money', 'mispriced', 'edge'];
-    for (const term of pmSearchTerms) {
-      for (const sub of ['Kalshi', 'Polymarket']) {
-        try {
-          const posts = await scrapeReddit(sub, term, 5);
-          for (const post of posts) {
-            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 ('reddit_predmkt', $1, $2, $3, $4, $5, $6, $7, '')
-              ON CONFLICT DO NOTHING
-            `, [sub, term, post.title, post.url, post.score || 0, post.num_comments || 0, sentiment]);
-            predMarketRedditTotal++;
-          }
-        } catch {}
-      }
-      await new Promise(r => setTimeout(r, 2000));
-    }
-    console.log(`[Ken] Prediction market Reddit: ${predMarketRedditTotal} posts from r/Kalshi + r/Polymarket`);
-    if (global.gc) global.gc();
-
-    // ── GDELT TV News (CNN, Fox closed captions) ──
-    const tvStations = ['CNN', 'FOXNEWS'];
-    const tvQueries = ['trump', 'tariff', 'ukraine', 'election'];
-    let tvTotal = 0;
-    for (const station of tvStations) {
-      for (const tq of tvQueries) {
-        try {
-          const clips = await scrapeGDELTtv(tq, station, 5);
-          for (const clip of clips) {
-            if (clip.title && clip.title.length > 15) {
-              const sentiment = scoreSentiment(clip.title);
-              await kenQ(`
-                INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-                VALUES ('tv_news', $1, $2, $3, $4, 0, $5, $6)
-                ON CONFLICT DO NOTHING
-              `, [clip.source || station, tq, clip.title.substring(0, 500), clip.url, sentiment, clip.show || '']);
-              tvTotal++;
-            }
-          }
-        } catch {}
-        await new Promise(r => setTimeout(r, 500));
-      }
-    }
-    console.log(`[Ken] GDELT TV: ${tvTotal} clips from ${tvStations.length} stations`);
-
-    // ── YouTube News Channels (CNN, MSNBC, Fox, Bloomberg, CNBC, etc.) ──
-    try {
-      const ytVideos = await scrapeYouTubeRSS();
-      let ytStored = 0;
-      for (const video of ytVideos) {
-        const sentiment = scoreSentiment(video.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-          VALUES ('youtube_news', $1, 'video', $2, $3, 0, $4, '')
-          ON CONFLICT DO NOTHING
-        `, [video.source, video.title, video.url, sentiment]);
-        ytStored++;
-      }
-      totalPosts += ytStored;
-      console.log(`[Ken] YouTube News: ${ytStored} videos from 9 channels`);
-    } catch (e) {
-      console.log(`[Ken] YouTube News failed: ${e.message}`);
-    }
-    if (global.gc) global.gc();
-
-    // ── Lemmy (federated Reddit — public API, no auth) ──
-    let lemmyTotal = 0;
-    try {
-      const lemmyPosts = await scrapeLemmy('Hot', 25);
-      for (const p of lemmyPosts) {
-        const sentiment = scoreSentiment(p.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('lemmy', $1, 'hot', $2, $3, $4, $5, $6, '')
-          ON CONFLICT DO NOTHING
-        `, [p.source, p.title, p.url, p.score || 0, p.num_comments || 0, sentiment]);
-        lemmyTotal++;
-      }
-    } catch {}
-    console.log(`[Ken] Lemmy: ${lemmyTotal} posts`);
-
-    // ── Lobsters (tech/policy news — public JSON API) ──
-    let lobstersTotal = 0;
-    try {
-      const stories = await scrapeLobsters(25);
-      for (const s of stories) {
-        const sentiment = scoreSentiment(s.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('lobsters', $1, $2, $3, $4, $5, $6, $7, '')
-          ON CONFLICT DO NOTHING
-        `, [s.author || 'lobsters', s.tags || 'tech', s.title, s.url, s.score || 0, s.num_comments || 0, sentiment]);
-        lobstersTotal++;
-      }
-    } catch {}
-    console.log(`[Ken] Lobsters: ${lobstersTotal} stories`);
-
-    // ── Tildes (curated discussion — RSS) ──
-    let tildesTotal = 0;
-    try {
-      const tildesPosts = await scrapeTildes();
-      for (const p of tildesPosts) {
-        const sentiment = scoreSentiment(p.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-          VALUES ('tildes', 'tildes', 'discussion', $1, $2, 0, $3, '')
-          ON CONFLICT DO NOTHING
-        `, [p.title, p.url, sentiment]);
-        tildesTotal++;
-      }
-    } catch {}
-    console.log(`[Ken] Tildes: ${tildesTotal} discussions`);
-
-    // ── Bluesky (trending feeds — getFeed API) ──
-    let bskyTotal = 0;
-    try {
-      const posts = await scrapeBluesky();
-      for (const p of posts) {
-        if ((p.score || 0) > 2 || (p.num_comments || 0) > 1 || p.title.length > 30) {
-          const sentiment = scoreSentiment(p.title);
-          await kenQ(`
-            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-            VALUES ('bluesky', $1, $2, $3, $4, $5, $6, $7, '')
-            ON CONFLICT DO NOTHING
-          `, [p.author || 'bluesky', p.source || 'trending', p.title, p.url, p.score || 0, p.num_comments || 0, sentiment]);
-          bskyTotal++;
-        }
-      }
-    } catch {}
-    console.log(`[Ken] Bluesky: ${bskyTotal} posts`);
-
-    // ── Mastodon/Fediverse trending ──
-    try {
-      const mastodonPosts = await scrapeMastodon();
-      let mastoTotal = 0;
-      for (const p of mastodonPosts) {
-        const sentiment = scoreSentiment(p.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('mastodon', $1, 'trending', $2, $3, $4, $5, $6, '')
-          ON CONFLICT DO NOTHING
-        `, [p.source, p.title, p.url, p.score || 0, p.num_comments || 0, sentiment]);
-        mastoTotal++;
-      }
-      totalPosts += mastoTotal;
-      console.log(`[Ken] Mastodon: ${mastoTotal} trending`);
-    } catch (e) {
-      console.log(`[Ken] Mastodon failed: ${e.message}`);
-    }
-
-    totalPosts += tvTotal + lemmyTotal + lobstersTotal + tildesTotal + bskyTotal;
-    totalPosts += manifoldTotal + predMarketRedditTotal;
-
-    // ── PredictIt (cross-reference prediction market) ──
-    let predictItTotal = 0;
-    try {
-      const piMarkets = await scrapePredictIt();
-      for (const m of piMarkets) {
-        const sentiment = scoreSentiment(m.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('predictit', 'predictit', 'market', $1, $2, $3, $4, $5, $6)
-          ON CONFLICT DO NOTHING
-        `, [m.title, m.url, m.score, m.num_comments || 0, sentiment, m.extra || '']);
-        predictItTotal++;
-      }
-      totalPosts += predictItTotal;
-      console.log(`[Ken] PredictIt: ${predictItTotal} markets`);
-    } catch (e) {
-      console.log(`[Ken] PredictIt failed: ${e.message}`);
-    }
-
-    // ── FRED Economic Indicators (real data for economic markets) ──
-    let fredTotal = 0;
-    try {
-      const fredData = await scrapeFredIndicators();
-      for (const ind of fredData) {
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
-          VALUES ('fred_economic', 'fred', $1, $2, $3, $4, 0, '')
-          ON CONFLICT DO NOTHING
-        `, [ind.keyword, ind.title, ind.url, ind.score]);
-        fredTotal++;
-      }
-      totalPosts += fredTotal;
-      console.log(`[Ken] FRED Economic: ${fredTotal} indicators`);
-    } catch (e) {
-      console.log(`[Ken] FRED failed: ${e.message}`);
-    }
-
-    // ── NWS Active Weather Alerts (severe weather data for weather markets) ──
-    let nwsAlertTotal = 0;
-    try {
-      const alerts = await scrapeNWSAlerts();
-      for (const alert of alerts) {
-        const sentiment = scoreSentiment(alert.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('nws_alerts', 'weather', $1, $2, $3, $4, 0, $5, '')
-          ON CONFLICT DO NOTHING
-        `, [alert.keyword, alert.title, alert.url, alert.score, sentiment]);
-        nwsAlertTotal++;
-      }
-      totalPosts += nwsAlertTotal;
-      console.log(`[Ken] NWS Alerts: ${nwsAlertTotal} active alerts`);
-    } catch (e) {
-      console.log(`[Ken] NWS Alerts failed: ${e.message}`);
-    }
-
-    // ── GovTrack Congressional Votes (legislation for political markets) ──
-    let govTrackTotal = 0;
-    try {
-      const votes = await scrapeGovTrack();
-      for (const v of votes) {
-        const sentiment = scoreSentiment(v.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('govtrack', 'congress', $1, $2, $3, $4, $5, $6, '')
-          ON CONFLICT DO NOTHING
-        `, [v.keyword, v.title, v.url, v.score, v.num_comments || 0, sentiment]);
-        govTrackTotal++;
-      }
-      totalPosts += govTrackTotal;
-      console.log(`[Ken] GovTrack: ${govTrackTotal} congressional votes`);
-    } catch (e) {
-      console.log(`[Ken] GovTrack failed: ${e.message}`);
-    }
-
-    // ── Twitter/X via Nitter (public sentiment, no auth needed) ──
-    let nitterTotal = 0;
-    try {
-      const tweets = await scrapeNitterTrending();
-      for (const t of tweets) {
-        const sentiment = scoreSentiment(t.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('twitter_nitter', 'twitter', $1, $2, $3, 0, 0, $4, '')
-          ON CONFLICT DO NOTHING
-        `, [t.keyword, t.title, t.url, sentiment]);
-        nitterTotal++;
-      }
-      totalPosts += nitterTotal;
-      console.log(`[Ken] Twitter/Nitter: ${nitterTotal} tweets`);
-    } catch (e) {
-      console.log(`[Ken] Twitter/Nitter failed: ${e.message}`);
-    }
-
-    // ── Discord (Kalshi, Polymarket, Prediction Markets servers) ──
-    let discordTotal = 0;
-    try {
-      const discordMessages = await scrapeDiscord(30);
-      for (const msg of discordMessages) {
-        const sentiment = scoreSentiment(msg.title);
-        await kenQ(`
-          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
-          VALUES ('discord', $1, $2, $3, $4, $5, $6, $7, $8)
-          ON CONFLICT DO NOTHING
-        `, [
-          msg.source, msg.channel || 'general', msg.title.substring(0, 500),
-          msg.url, msg.score || 0, msg.num_comments || 0, sentiment,
-          (msg.author || '').substring(0, 200),
-        ]);
-        discordTotal++;
-      }
-      totalPosts += discordTotal;
-      console.log(`[Ken] Discord: ${discordTotal} messages`);
-    } catch (e) {
-      console.log(`[Ken] Discord failed: ${e.message}`);
-    }
-    if (global.gc) global.gc();
-
-    logMemory('sweep-end');
-    if (global.gc) global.gc();
-    console.log(`[Ken] ═══ FULL SWEEP: ${totalPosts} items (Reddit, HN, ${NEWS_FEEDS.length} feeds, TV, YT, Manifold, r/Kalshi, r/Polymarket, Lemmy, Lobsters, Tildes, Bluesky, Mastodon, Metaculus, PredictIt, FRED, NWS Alerts, GovTrack, Twitter, Discord) ═══`);
-    return totalPosts;
-  } catch (e) {
-    console.error('[Ken] Intelligence sweep error:', e.message);
-    logMemory('sweep-error');
-    return 0;
-  }
-}
-
-// ══════════════════════════════════════════════════════════════════════════════
-// SIGNAL PIPELINE: News → Sentiment → Event Map → Probability → Signals → Risk
-// Pipeline: narrative shifts → probability inference → edge detection
-// ══════════════════════════════════════════════════════════════════════════════
-
-// ── Enhanced Event-Specific Sentiment Engine ──
-// Unlike scoreSentiment() which is general, this maps headlines to specific market directions
-const EVENT_SENTIMENT_RULES = [
-  // Weather markets
-  { pattern: /hurricane|tropical storm|cyclone/i, category: 'weather', direction: 'severity', boost: 0.3 },
-  { pattern: /category\s*[3-5]|major hurricane|extreme/i, category: 'weather', direction: 'severity', boost: 0.5 },
-  { pattern: /weakens|dissipates|downgrade/i, category: 'weather', direction: 'severity', boost: -0.4 },
-  { pattern: /record heat|heat wave|above normal/i, category: 'weather', direction: 'temperature_high', boost: 0.4 },
-  { pattern: /cold snap|polar vortex|below normal|freeze/i, category: 'weather', direction: 'temperature_low', boost: 0.4 },
-  { pattern: /heavy rain|flood|rainfall record/i, category: 'weather', direction: 'precipitation_high', boost: 0.4 },
-  { pattern: /drought|dry spell|no rain/i, category: 'weather', direction: 'precipitation_low', boost: 0.4 },
-  // Political markets
-  { pattern: /resign|step down|quit|fired/i, category: 'politics', direction: 'negative', boost: 0.5 },
-  { pattern: /appointed|confirmed|sworn in|nominated/i, category: 'politics', direction: 'positive', boost: 0.4 },
-  { pattern: /impeach|investigation|scandal|indicted/i, category: 'politics', direction: 'negative', boost: 0.4 },
-  { pattern: /leading|ahead|polling|surge in polls/i, category: 'politics', direction: 'positive', boost: 0.3 },
-  { pattern: /trailing|behind|falling|losing ground/i, category: 'politics', direction: 'negative', boost: 0.3 },
-  { pattern: /shutdown|deadlock|stalemate/i, category: 'politics', direction: 'shutdown_yes', boost: 0.5 },
-  { pattern: /deal reached|agreement|bipartisan|passed/i, category: 'politics', direction: 'shutdown_no', boost: -0.4 },
-  // Economic markets
-  { pattern: /rate cut|dovish|easing/i, category: 'economics', direction: 'rate_down', boost: 0.4 },
-  { pattern: /rate hike|hawkish|tightening/i, category: 'economics', direction: 'rate_up', boost: 0.4 },
-  { pattern: /inflation.*(?:hot|higher|above|surge|accelerat)/i, category: 'economics', direction: 'inflation_up', boost: 0.4 },
-  { pattern: /inflation.*(?:cool|lower|below|slow|deceler)/i, category: 'economics', direction: 'inflation_down', boost: 0.4 },
-  { pattern: /recession|contraction|GDP.*(?:negative|shrink)/i, category: 'economics', direction: 'recession_yes', boost: 0.5 },
-  { pattern: /growth|expansion|GDP.*(?:beat|strong|positive)/i, category: 'economics', direction: 'recession_no', boost: -0.4 },
-  { pattern: /jobs.*(?:beat|strong|added more)/i, category: 'economics', direction: 'jobs_strong', boost: 0.3 },
-  { pattern: /jobs.*(?:miss|weak|fewer)/i, category: 'economics', direction: 'jobs_weak', boost: 0.3 },
-  // Crypto markets
-  { pattern: /bitcoin.*(?:new high|surge|rally|breakout|100k|150k)/i, category: 'crypto', direction: 'btc_up', boost: 0.4 },
-  { pattern: /bitcoin.*(?:crash|plunge|selloff|bear)/i, category: 'crypto', direction: 'btc_down', boost: 0.4 },
-  { pattern: /SEC.*(?:approve|green light)/i, category: 'crypto', direction: 'btc_up', boost: 0.3 },
-  { pattern: /SEC.*(?:reject|deny|sue)/i, category: 'crypto', direction: 'btc_down', boost: 0.3 },
-  // Sports/entertainment
-  { pattern: /favou?rite|odds.*increase|bet.*heavy/i, category: 'sports', direction: 'favourite_stronger', boost: 0.3 },
-  { pattern: /upset|underdog|surprise/i, category: 'sports', direction: 'favourite_weaker', boost: 0.3 },
-  { pattern: /injured|out for|suspended|DNP/i, category: 'sports', direction: 'team_weaker', boost: 0.4 },
-  // Space/tech
-  { pattern: /launch.*(?:success|orbit|nominal)/i, category: 'space', direction: 'launch_success', boost: 0.5 },
-  { pattern: /launch.*(?:scrub|delay|abort|fail)/i, category: 'space', direction: 'launch_fail', boost: 0.5 },
-  // Geopolitics
-  { pattern: /ceasefire|peace.*(?:deal|talks|agreement)/i, category: 'geopolitics', direction: 'de_escalation', boost: 0.5 },
-  { pattern: /escalat|offensive|invasion|attack|strike/i, category: 'geopolitics', direction: 'escalation', boost: 0.4 },
-];
-
-function scoreEventSentiment(headline, marketTitle = '') {
-  const text = `${headline} ${marketTitle}`.toLowerCase();
-  const result = {
-    sentiment: 0,
-    confidence: 0,
-    magnitude: 0,
-    category: 'general',
-    direction: 'neutral',
-    matchedRules: [],
-  };
-
-  // Apply rule-based event-specific scoring
-  for (const rule of EVENT_SENTIMENT_RULES) {
-    if (rule.pattern.test(text)) {
-      result.sentiment += rule.boost;
-      result.magnitude += Math.abs(rule.boost);
-      result.category = rule.category;
-      result.direction = rule.direction;
-      result.matchedRules.push(rule.direction);
-    }
-  }
-
-  // Certainty language modifiers
-  const certainWords = /confirm|official|announce|breaking|just.*happen/i;
-  const uncertainWords = /may|might|could|rumor|unconfirm|speculat|sources say/i;
-  if (certainWords.test(text)) { result.confidence += 0.3; result.magnitude *= 1.2; }
-  if (uncertainWords.test(text)) { result.confidence -= 0.2; result.magnitude *= 0.7; }
-
-  // Multiple source agreement boosts confidence
-  if (result.matchedRules.length > 1) result.confidence += 0.15;
-
-  // Normalize
-  result.sentiment = Math.max(-1, Math.min(1, result.sentiment));
-  result.confidence = Math.max(0, Math.min(1, 0.5 + result.confidence)); // base confidence 0.5
-  result.magnitude = Math.min(1, result.magnitude);
-
-  return result;
-}
-
-// ── Event Mapper: Map news entities → Kalshi market tickers ──
-// Uses keyword-based + fuzzy matching to connect news to specific contracts
-const MARKET_CATEGORY_KEYWORDS = {
-  // Weather
-  'temperature': ['temperature', 'temp', 'heat', 'cold', 'warm', 'degree', 'fahrenheit', 'celsius'],
-  'hurricane': ['hurricane', 'tropical', 'cyclone', 'storm', 'named storm'],
-  'rainfall': ['rain', 'rainfall', 'precipitation', 'flood', 'drought', 'inches'],
-  // Politics
-  'election': ['election', 'vote', 'ballot', 'primary', 'nominee', 'candidate', 'poll'],
-  'government': ['resign', 'cabinet', 'secretary', 'appointment', 'nomination', 'impeach'],
-  'legislation': ['bill', 'act', 'congress', 'senate', 'house', 'passed', 'shutdown'],
-  // Economics
-  'fed_rate': ['federal reserve', 'fed rate', 'interest rate', 'fomc', 'rate cut', 'rate hike', 'powell'],
-  'inflation': ['cpi', 'inflation', 'consumer price', 'pce', 'core inflation'],
-  'gdp': ['gdp', 'gross domestic', 'economic growth', 'recession', 'expansion'],
-  'jobs': ['jobs report', 'unemployment', 'nonfarm', 'payroll', 'jobless claims'],
-  // Crypto
-  'bitcoin': ['bitcoin', 'btc', 'cryptocurrency', 'crypto'],
-  'ethereum': ['ethereum', 'eth', 'ether'],
-  // Space
-  'spacex': ['spacex', 'falcon', 'starship', 'rocket launch', 'orbit'],
-  'nasa': ['nasa', 'artemis', 'moon', 'mars mission'],
-};
-
-// Map ticker suffixes to searchable names for multi-outcome events
-const TICKER_ENTITY_MAP = {
-  // KXCABOUT - Cabinet members
-  'RFK':  ['rfk', 'kennedy', 'robert kennedy', 'rfk jr', 'health and human'],
-  'KNOE': ['noem', 'kristi noem', 'homeland security'],
-  'SDUF': ['duffy', 'sean duffy', 'transportation'],
-  'BROL': ['rollins', 'brooke rollins', 'agriculture'],
-  'JRAT': ['ratcliffe', 'john ratcliffe', 'cia director'],
-  'SBES': ['bessent', 'scott bessent', 'treasury secretary'],
-  'MRUB': ['rubio', 'marco rubio', 'state department', 'secretary of state'],
-  'PHEG': ['hegseth', 'pete hegseth', 'defense secretary'],
-  'TGAB': ['gabbard', 'tulsi gabbard', 'intelligence director'],
-  'MWAL': ['waltz', 'mike waltz', 'national security'],
-  'HLUT': ['lutnick', 'howard lutnick', 'commerce secretary'],
-  'DBUR': ['burgum', 'doug burgum', 'interior secretary'],
-  'CWRI': ['chris wright', 'energy secretary'],
-  'LZEL': ['zeldin', 'lee zeldin', 'epa administrator'],
-  'DCOL': ['collins', 'doug collins', 'veterans affairs'],
-  'LMCM': ['mcmahon', 'linda mcmahon', 'education secretary'],
-  'PBON': ['bondi', 'pam bondi', 'attorney general'],
-  'RVOU': ['vought', 'russell vought', 'omb director'],
-  'STUR': ['turner', 'scott turner', 'hud secretary'],
-  'LCDR': ['cruz', 'linda cruz', 'sba administrator'],
-  'MKRA': ['kratsios', 'michael kratsios'],
-  'SWIL': ['wilson', 'sean wilson'],
-  // KXPRESPERSON - 2028 Presidential election
-  'DTRU':   ['trump', 'donald trump', 'president trump', 'former president trump'],
-  'DTRUJR': ['trump jr', 'donald trump jr', 'don jr', 'trump junior'],
-  'RDES':   ['desantis', 'ron desantis', 'florida governor'],
-  'JVAN':   ['vance', 'jd vance', 'vice president vance', 'j.d. vance'],
-  'GYOU':   ['youngkin', 'glenn youngkin', 'virginia governor'],
-  // KXFEDCHAIRNOM - Fed Chair nominees
-  'JS':   ['dimon', 'jamie dimon'],
-  'KW':   ['warsh', 'kevin warsh'],
-  'KH':   ['hassett', 'kevin hassett'],
-  'CWAL': ['waller', 'chris waller', 'christopher waller'],
-  'MBOW': ['bowman', 'michelle bowman'],
-};
-
-function extractMarketEntity(market) {
-  // Extract ticker suffix for entity lookup first (most reliable)
-  const parts = market.ticker.split('-');
-  const suffix = parts[parts.length - 1];
-  if (TICKER_ENTITY_MAP[suffix]) return TICKER_ENTITY_MAP[suffix];
-  // Try subtitle only if it's a real description (not just the ticker repeated)
-  if (market.subtitle && market.subtitle !== market.ticker && !market.subtitle.startsWith('KX')) {
-    return market.subtitle.toLowerCase().split(/[\s\-]+/).filter(w => w.length > 2);
-  }
-  // Try the yes_sub_title field
-  if (market.yes_sub_title && market.yes_sub_title !== market.ticker) {
-    return market.yes_sub_title.toLowerCase().split(/[\s\-]+/).filter(w => w.length > 2);
-  }
-  return [];
-}
-
-function isMultiOutcomeEvent(ticker) {
-  // Multi-outcome events have a common prefix followed by person/answer suffix
-  // e.g., KXCABOUT-29JAN-RFK, KXCABOUT-29JAN-KNOE all share KXCABOUT-29JAN
-  const parts = ticker.split('-');
-  return parts.length >= 3; // Event prefix + date + answer
-}
-
-function getEventPrefix(ticker) {
-  const parts = ticker.split('-');
-  if (parts.length >= 3) return parts.slice(0, -1).join('-');
-  return ticker;
-}
-
-function mapNewsToMarkets(headline, activeMarkets) {
-  const headlineLower = headline.toLowerCase();
-  const matches = [];
-
-  // Step 1: Identify which categories the headline relates to
-  const relevantCategories = [];
-  for (const [category, keywords] of Object.entries(MARKET_CATEGORY_KEYWORDS)) {
-    for (const kw of keywords) {
-      if (headlineLower.includes(kw)) {
-        relevantCategories.push(category);
-        break;
-      }
-    }
-  }
-
-  // Step 2: Group markets by event to detect multi-outcome events
-  const eventGroups = {};
-  for (const market of activeMarkets) {
-    const prefix = getEventPrefix(market.ticker);
-    if (!eventGroups[prefix]) eventGroups[prefix] = [];
-    eventGroups[prefix].push(market);
-  }
-
-  // Step 3: Find matching Kalshi markets by title/subtitle keywords
-  for (const market of activeMarkets) {
-    const marketText = `${market.title} ${market.subtitle || ''} ${market.yes_sub_title || ''}`.toLowerCase();
-    let matchScore = 0;
-
-    // Direct keyword overlap
-    const headlineWords = headlineLower.split(/\s+/).filter(w => w.length > 3);
-    for (const word of headlineWords) {
-      if (marketText.includes(word)) matchScore += 1;
-    }
-
-    // Category match bonus
-    for (const cat of relevantCategories) {
-      const catKws = MARKET_CATEGORY_KEYWORDS[cat];
-      if (catKws.some(kw => marketText.includes(kw))) matchScore += 2;
-    }
-
-    // Entity matching (city names, person names, etc.)
-    const cities = ['phoenix', 'houston', 'miami', 'chicago', 'new york', 'los angeles', 'atlanta', 'dallas', 'denver', 'seattle', 'boston', 'detroit', 'minneapolis', 'philadelphia'];
-    for (const city of cities) {
-      if (headlineLower.includes(city) && marketText.includes(city)) matchScore += 5;
-    }
-
-    // Political figure matching
-    const figures = ['trump', 'biden', 'harris', 'desantis', 'newsom', 'vance', 'powell', 'yellen'];
-    for (const fig of figures) {
-      if (headlineLower.includes(fig) && marketText.includes(fig)) matchScore += 5;
-    }
-
-    // MULTI-OUTCOME EVENT HANDLING: For events with multiple answer markets,
-    // require headline to mention the specific answer entity, not just the general topic
-    const eventPrefix = getEventPrefix(market.ticker);
-    const siblings = eventGroups[eventPrefix] || [];
-    if (siblings.length > 1 && isMultiOutcomeEvent(market.ticker)) {
-      const entityTerms = extractMarketEntity(market);
-      const entityMatch = entityTerms.some(term => headlineLower.includes(term));
-      if (entityMatch) {
-        matchScore += 8; // Strong boost for specific entity match
-      } else {
-        // Headline mentions the topic but NOT this specific answer — reject entirely
-        // With 20+ siblings, even small scores cause massive clustering
-        matchScore = 0;
-      }
-    }
-
-    if (matchScore >= 3) {
-      matches.push({ market, matchScore });
-    }
-  }
-
-  // Sort by match score descending
-  matches.sort((a, b) => b.matchScore - a.matchScore);
-  return matches.slice(0, 5); // Top 5 matches
-}
-
-// ── Probability Shift Model (Bayesian Update) ──
-// adjustedProb = priorProb + (sentiment * weight * confidence)
-// With decay for older signals, source diversity bonus, and category-specific weights
-const CATEGORY_SENTIMENT_WEIGHTS = {
-  politics: 0.18,    // Political events move markets fast
-  economics: 0.16,   // Economic data is highly impactful
-  crypto: 0.22,      // Crypto is volatile, sentiment-driven
-  weather: 0.10,     // Weather has model data, less sentiment-dependent
-  geopolitics: 0.20, // Geopolitical events create large moves
-  sports: 0.12,      // Sports have existing odds infrastructure
-  space: 0.14,       // Launch outcomes are binary
-  general: 0.12,     // Default
-};
-
-function calculateProbabilityShift(priorProb, sentimentSignals) {
-  if (!sentimentSignals.length) return { adjustedProb: priorProb, impact: 0, numArticles: 0, agreement: 0, sourceDiversity: 0 };
-
-  const RECENCY_DECAY = 0.85; // Faster decay — old news matters less
-  const AGREEMENT_BONUS = 1.5; // Stronger bonus for agreement
-  const DISAGREEMENT_PENALTY = 0.3; // Stronger penalty for mixed signals
-
-  let totalImpact = 0;
-  let totalWeight = 0;
-  const now = Date.now();
-
-  // Sort by recency
-  const sorted = [...sentimentSignals].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
-
-  // Calculate source diversity (unique sources boost signal)
-  const uniqueSources = new Set(sorted.map(s => s.source));
-  const sourceDiversity = Math.min(1, uniqueSources.size / 4); // 4+ unique sources = max diversity
-
-  for (const sig of sorted) {
-    const hoursAgo = Math.max(0, (now - (sig.timestamp || now)) / 3600000);
-    const recencyWeight = Math.pow(RECENCY_DECAY, hoursAgo);
-    // Use category-specific weight instead of flat 0.08
-    const catWeight = CATEGORY_SENTIMENT_WEIGHTS[sig.category] || CATEGORY_SENTIMENT_WEIGHTS.general;
-    // Magnitude amplifier: strong signals (|sentiment| > 0.3) get more weight
-    const magnitudeBoost = 1 + Math.min(0.5, (sig.magnitude || 0) * 0.8);
-    const impact = sig.sentiment * catWeight * sig.confidence * recencyWeight * magnitudeBoost;
-    totalImpact += impact;
-    totalWeight += recencyWeight * sig.confidence;
-  }
-
-  // Source diversity bonus: multiple independent sources confirming = stronger signal
-  totalImpact *= (1 + sourceDiversity * 0.4);
-
-  // Check agreement: if most signals agree on direction, boost; if mixed, penalize
-  const posSignals = sorted.filter(s => s.sentiment > 0).length;
-  const negSignals = sorted.filter(s => s.sentiment < 0).length;
-  const total = posSignals + negSignals;
-  if (total >= 2) {
-    const agreementRatio = Math.max(posSignals, negSignals) / total;
-    if (agreementRatio > 0.8) totalImpact *= AGREEMENT_BONUS; // Strong agreement
-    else if (agreementRatio > 0.6) totalImpact *= 1.1; // Mild agreement
-    else totalImpact *= DISAGREEMENT_PENALTY; // Mixed signals — dampen hard
-  }
-
-  // Diminishing returns: each additional article adds less new information
-  // 1 article = 100%, 4 = 50%, 9 = 33%, 16 = 25%
-  const articleDiminishing = Math.sqrt(sorted.length) / sorted.length;
-  totalImpact *= articleDiminishing;
-
-  // Prior-anchored capping: prevent unrealistic shifts on low-probability markets
-  // A 4% market shouldn't jump to 60% from sentiment alone; cap shift at 5x prior or 15%, whichever is higher
-  const maxShift = Math.max(0.15, priorProb * 5);
-  totalImpact = Math.sign(totalImpact) * Math.min(Math.abs(totalImpact), maxShift);
-
-  // Clamp adjusted probability to [0.01, 0.99]
-  const adjustedProb = Math.max(0.01, Math.min(0.99, priorProb + totalImpact));
-
-  return {
-    adjustedProb,
-    impact: totalImpact,
-    numArticles: sorted.length,
-    agreement: total > 0 ? Math.max(posSignals, negSignals) / total : 0,
-    sourceDiversity,
-  };
-}
-
-// ── Risk Engine ──
-const RISK_LIMITS = {
-  maxExposurePerMarket: 5000, // cents ($50)
-  maxExposurePerTheme: 15000, // cents ($150)
-  maxTotalExposure: 50000, // cents ($500)
-  maxContractsPerMarket: 25,
-  minEdgeThreshold: 0.03, // 3% edge minimum (lowered from 5% — entity-specific matching yields smaller edges)
-  maxDailyLoss: 5000, // cents ($50)
-  cooldownAfterLoss: 30, // minutes
-  timeDecayPenalty: 0.02, // Reduce edge by 2% per hour of signal age
-  minLiquidity: 100, // Minimum 24h volume
-};
-
-function riskCheck(signal, currentExposure = {}) {
-  const reasons = [];
-  let approved = true;
-
-  // Edge threshold
-  if (Math.abs(signal.expectedEdge) < RISK_LIMITS.minEdgeThreshold) {
-    approved = false;
-    reasons.push(`Edge ${(signal.expectedEdge * 100).toFixed(1)}% below ${(RISK_LIMITS.minEdgeThreshold * 100)}% threshold`);
-  }
-
-  // Market exposure
-  const marketExposure = currentExposure[signal.marketId] || 0;
-  if (marketExposure >= RISK_LIMITS.maxExposurePerMarket) {
-    approved = false;
-    reasons.push(`Market exposure $${(marketExposure / 100).toFixed(2)} at limit`);
-  }
-
-  // Liquidity check
-  if ((signal.volume24h || 0) < RISK_LIMITS.minLiquidity) {
-    approved = false;
-    reasons.push(`Low liquidity: ${signal.volume24h || 0} < ${RISK_LIMITS.minLiquidity} volume`);
-  }
-
-  // Time decay — use absolute edge values so BUY_NO signals aren't unfairly penalized
-  const signalAgeHours = (Date.now() - (signal.timestamp || Date.now())) / 3600000;
-  const absEdge = Math.abs(signal.expectedEdge);
-  const decayedAbsEdge = absEdge - (RISK_LIMITS.timeDecayPenalty * signalAgeHours);
-  if (decayedAbsEdge < RISK_LIMITS.minEdgeThreshold) {
-    approved = false;
-    reasons.push(`Edge decayed from ${(absEdge * 100).toFixed(1)}% to ${(decayedAbsEdge * 100).toFixed(1)}%`);
-  }
-
-  // Size recommendation based on edge and confidence
-  let sizeRecommendation = 1;
-  if (approved) {
-    const edgeFactor = Math.min(3, Math.abs(signal.expectedEdge) / RISK_LIMITS.minEdgeThreshold);
-    const confFactor = signal.confidence || 0.5;
-    sizeRecommendation = Math.max(1, Math.min(RISK_LIMITS.maxContractsPerMarket, Math.floor(edgeFactor * confFactor * 10)));
-  }
-
-  return { approved, reasons, sizeRecommendation, decayedEdge: decayedAbsEdge };
-}
-
-// ── Fast Signal Pipeline (runs every 5 minutes) ──
-async function signalPipeline(activeMarkets) {
-  const pipelineStart = Date.now();
-  let signalsGenerated = 0;
-
-  try {
-    // Step 1: Fetch recent news/sentiment from last 30 minutes
-    const { rows: recentArticles } = await kenQ(`
-      SELECT id, source, keyword, post_title as headline, post_url as url, sentiment_score, captured_at
-      FROM ken_sentiment
-      WHERE captured_at > NOW() - INTERVAL '30 minutes'
-      ORDER BY captured_at DESC
-      LIMIT 500
-    `);
-
-    if (recentArticles.length === 0) {
-      console.log('[Ken] Signal Pipeline: no new articles in last 30 min');
-      return 0;
-    }
-
-    // Step 2: Score each article with event-specific sentiment
-    const eventScores = [];
-    for (const article of recentArticles) {
-      const evtScore = scoreEventSentiment(article.headline);
-      if (evtScore.magnitude > 0.1) { // Only process articles with detectable event signal
-        // Step 3: Map to relevant markets
-        const mappedMarkets = mapNewsToMarkets(article.headline, activeMarkets);
-
-        for (const { market, matchScore } of mappedMarkets) {
-          eventScores.push({
-            articleId: String(article.id),
-            headline: article.headline,
-            source: article.source,
-            marketTicker: market.ticker,
-            marketTitle: market.title,
-            sentiment: evtScore.sentiment,
-            confidence: evtScore.confidence * (matchScore / 10), // Scale confidence by match quality
-            magnitude: evtScore.magnitude,
-            category: evtScore.category,
-            direction: evtScore.direction,
-            timestamp: new Date(article.captured_at).getTime(),
-          });
-
-          // Store event-specific sentiment
-          await kenQ(`
-            INSERT INTO ken_event_sentiment (article_id, event_tag, market_ticker, sentiment, confidence, magnitude, headline, source)
-            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
-          `, [String(article.id), evtScore.direction, market.ticker, evtScore.sentiment,
-              evtScore.confidence, evtScore.magnitude, article.headline?.substring(0, 500), article.source]);
-        }
-      }
-    }
-
-    console.log(`[Ken] Signal Pipeline: ${eventScores.length} event-specific scores from ${recentArticles.length} articles`);
-
-    // Step 4: Group by market and calculate probability shifts
-    const marketSignals = {};
-    for (const score of eventScores) {
-      if (!marketSignals[score.marketTicker]) marketSignals[score.marketTicker] = [];
-      marketSignals[score.marketTicker].push(score);
-    }
-
-    // Step 5: For each market with signals, compute probability shift
-    for (const [ticker, signals] of Object.entries(marketSignals)) {
-      const market = activeMarkets.find(m => m.ticker === ticker);
-      if (!market) continue;
-
-      const priorProb = (market.yes_bid + market.yes_ask) / 200; // Convert cents to probability
-      const { adjustedProb, impact, numArticles, agreement, sourceDiversity } = calculateProbabilityShift(priorProb, signals);
-      const marketProb = market.last_price / 100;
-      const edge = adjustedProb - marketProb;
-
-      // Store probability shift
-      await kenQ(`
-        INSERT INTO ken_probability_shifts (market_id, prior_probability, sentiment_impact, adjusted_probability, market_probability, edge, num_articles)
-        VALUES ($1, $2, $3, $4, $5, $6, $7)
-      `, [ticker, priorProb, impact, adjustedProb, marketProb, edge, numArticles]);
-
-      // Step 6: Generate signal if edge exceeds threshold
-      if (Math.abs(edge) >= RISK_LIMITS.minEdgeThreshold) {
-        const direction = edge > 0 ? 'BUY_YES' : 'BUY_NO';
-
-        // Step 6b: Monte Carlo simulation for probability validation (CPU-intensive)
-        const momentum = cachedMomentumMap?.[ticker] || { delta_2h: 0, delta_6h: 0 };
-        const mcResult = monteCarloSimulation({
-          priorProb,
-          sentimentImpact: impact,
-          momentum2h: momentum.delta_2h,
-          momentum6h: momentum.delta_6h,
-          volume24h: market.volume_24h || 0,
-          spread: (market.yes_ask || 0) - (market.yes_bid || 0),
-          articleCount: numArticles,
-          agreement: agreement || 0.5,
-        }, 15000); // 15k iterations for higher accuracy
-
-        // Blend Monte Carlo with statistical model
-        const mcEdge = mcResult.edgeConsistent ? (edge * 0.65 + mcResult.medianEdge * 0.35) : edge * 0.8;
-
-        // Dedup: skip if we already generated a signal for this market+direction in last 30 min
-        // Compare MC-blended edge (mcEdge) against stored MC-blended edge to avoid false positives
-        const recentDup = await kenQ(`
-          SELECT id, expected_edge, confidence FROM ken_strategy_signals
-          WHERE market_id = $1 AND direction = $2 AND created_at > NOW() - INTERVAL '30 minutes'
-          ORDER BY created_at DESC LIMIT 1
-        `, [ticker, direction]);
-        if (recentDup.rows.length > 0) {
-          const prevEdge = parseFloat(recentDup.rows[0].expected_edge);
-          if (Math.abs(Math.abs(mcEdge) - Math.abs(prevEdge)) < 0.02) continue;
-        }
-
-        // Improved confidence using MC validation
-        const edgeScore = Math.min(1, Math.pow(Math.abs(mcEdge) / 0.15, 1.5));
-        const volumeScore = Math.min(1, numArticles / 8);
-        const agreementScore = Math.pow(agreement || 0.5, 0.7);
-        const diversityScore = sourceDiversity || 0;
-        const mcConfBoost = mcResult.confidence > 0.6 ? 0.08 : mcResult.confidence > 0.4 ? 0.04 : 0;
-        const consistencyBoost = mcResult.edgeConsistent ? 0.05 : -0.05;
-        const confidence = Math.min(0.95, Math.max(0.15,
-          edgeScore * 0.30 +
-          agreementScore * 0.18 +
-          volumeScore * 0.18 +
-          diversityScore * 0.12 +
-          mcConfBoost +
-          consistencyBoost +
-          (numArticles >= 5 ? 0.10 : numArticles >= 3 ? 0.05 : 0)
-        ));
-
-        let signal = {
-          marketId: ticker,
-          direction,
-          expectedEdge: mcEdge,
-          confidence,
-          volume24h: market.volume_24h || 0,
-          timestamp: Date.now(),
-          agreement,
-          mcSimulation: {
-            meanEdge: mcResult.meanEdge,
-            stdDev: mcResult.stdDev,
-            percentiles: mcResult.percentiles,
-            consistent: mcResult.edgeConsistent,
-          },
-        };
-
-        // Step 6c: AI Enhancement for high-edge signals (Gemini)
-        if (Math.abs(mcEdge) >= 0.08 && confidence >= 0.4 && numArticles >= 3) {
-          signal = await aiEnhanceSignal(signal, signals, market);
-        }
-
-        // Step 7: Risk check
-        const risk = riskCheck(signal);
-
-        const mcInfo = `MC: μ=${(mcResult.meanEdge * 100).toFixed(1)}%±${(mcResult.stdDev * 100).toFixed(1)}%, p5-p95=[${(mcResult.percentiles.p5 * 100).toFixed(1)}%,${(mcResult.percentiles.p95 * 100).toFixed(1)}%]`;
-        const aiInfo = signal.aiEnhanced ? ` | AI: ${signal.aiReasoning || 'enhanced'}` : '';
-        const reasoning = `${direction} on "${market.title}" — edge ${(mcEdge * 100).toFixed(1)}% from ${numArticles} articles (${signals[0]?.category}/${signals[0]?.direction}). Agreement: ${((agreement || 0) * 100).toFixed(0)}%. ${mcInfo}${aiInfo}. ${risk.approved ? 'RISK APPROVED' : `RISK BLOCKED: ${risk.reasons.join('; ')}`}`;
-
-        await kenQ(`
-          INSERT INTO ken_strategy_signals (market_id, direction, expected_edge, size_recommendation, confidence, reasoning, sources, risk_approved)
-          VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
-        `, [ticker, direction, mcEdge, risk.sizeRecommendation, confidence, reasoning,
-            JSON.stringify(signals.slice(0, 5).map(s => ({ headline: s.headline?.substring(0, 200), source: s.source, sentiment: s.sentiment }))),
-            risk.approved]);
-
-        signalsGenerated++;
-
-        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 50 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));
-        }
-      }
-    }
-
-    const duration = Date.now() - pipelineStart;
-    console.log(`[Ken] Signal Pipeline: ${signalsGenerated} signals from ${Object.keys(marketSignals).length} markets | ${duration}ms`);
-    return signalsGenerated;
-
-  } catch (e) {
-    console.error('[Ken] Signal Pipeline error:', e.message);
-    return 0;
-  }
-}
-
-// ── Auto-mapping: Build market mappings from active Kalshi markets ──
-async function updateMarketMappings(activeMarkets) {
-  try {
-    let mapped = 0;
-    for (const m of activeMarkets) {
-      const titleLower = `${m.title} ${m.subtitle || ''}`.toLowerCase();
-      let category = 'other';
-      const keywords = [];
-
-      for (const [cat, kws] of Object.entries(MARKET_CATEGORY_KEYWORDS)) {
-        for (const kw of kws) {
-          if (titleLower.includes(kw)) {
-            category = cat;
-            keywords.push(kw);
-          }
-        }
-      }
-
-      if (keywords.length > 0) {
-        await kenQ(`
-          INSERT INTO ken_market_mappings (market_id, keywords, category, contract_type)
-          VALUES ($1, $2, $3, 'binary')
-          ON CONFLICT (id) DO NOTHING
-        `, [m.ticker, keywords, category]);
-        mapped++;
-      }
-    }
-    return mapped;
-  } catch (e) {
-    console.log(`[Ken] Market mapping update failed: ${e.message}`);
-    return 0;
-  }
-}
-
-// ══════════════════════════════════════════════
-// POLYMARKET CROSS-REFERENCE (fuzzy matching)
-// ══════════════════════════════════════════════
-const STOP_WORDS = new Set(['will', 'the', 'this', 'that', 'what', 'when', 'where', 'which', 'with', 'from', 'have', 'been', 'before', 'after', 'than', 'more', 'less', 'next', 'last', 'does', 'would', 'could', 'should', 'about', 'into', 'over', 'under', 'between', 'through', 'during', 'each', 'every', 'other', 'some', 'most', 'many', 'much', 'also', 'just', 'only', 'very', 'still', 'even', 'back', 'first', 'year', 'years']);
-
-function extractKeywords(text) {
-  return (text || '').toLowerCase()
-    .replace(/[^a-z0-9\s]/g, ' ')
-    .split(/\s+/)
-    .filter(w => w.length > 2 && !STOP_WORDS.has(w));
-}
-
-function fuzzyMatchScore(textA, textB) {
-  const wordsA = extractKeywords(textA);
-  const wordsB = new Set(extractKeywords(textB));
-  if (wordsA.length === 0 || wordsB.size === 0) return 0;
-  let matches = 0;
-  for (const w of wordsA) {
-    if (wordsB.has(w)) matches++;
-    // Partial match for longer words (e.g. "greenland" matches "greenlands")
-    else if (w.length > 5) {
-      for (const wb of wordsB) {
-        if (wb.length > 5 && (wb.startsWith(w.slice(0, 5)) || w.startsWith(wb.slice(0, 5)))) { matches += 0.5; break; }
-      }
-    }
-  }
-  return matches / Math.min(wordsA.length, wordsB.size);
-}
-
-async function polymarketCrossRef(kalshiMarkets) {
-  const crossRefs = [];
-  try {
-    const polyEvents = await scrapePolymarketEvents(100);
-    if (polyEvents.length === 0) return [];
-    console.log(`[Ken] Polymarket: ${polyEvents.length} markets from top events`);
-
-    // Build index of Kalshi market text for faster matching
-    const kalshiIndex = kalshiMarkets.map(km => ({
-      ...km,
-      searchText: ((km.title || '') + ' ' + (km.subtitle || '')).toLowerCase(),
-    }));
-
-    for (const pm of polyEvents) {
-      const pmText = pm.event_title + ' ' + pm.question;
-      let bestMatch = null;
-      let bestScore = 0;
-
-      for (const km of kalshiIndex) {
-        const score = fuzzyMatchScore(pmText, km.searchText);
-        if (score > bestScore) {
-          bestScore = score;
-          bestMatch = km;
-        }
-      }
-
-      // Require at least 30% keyword overlap for a match
-      if (bestScore >= 0.3 && bestMatch && bestMatch.yes_ask > 0) {
-        const priceDiff = pm.price - bestMatch.yes_ask;
-        if (Math.abs(priceDiff) >= 3) { // 3¢+ divergence
-          crossRefs.push({
-            kalshi_ticker: bestMatch.ticker,
-            event_ticker: bestMatch.event_ticker,
-            kalshi_title: bestMatch.title,
-            kalshi_subtitle: bestMatch.subtitle,
-            kalshi_price: bestMatch.yes_ask,
-            poly_question: pm.question,
-            poly_event: pm.event_title,
-            poly_price: pm.price,
-            poly_volume: pm.volume,
-            poly_slug: pm.slug,
-            price_diff: priceDiff,
-            match_score: Math.round(bestScore * 100),
-            arb_side: priceDiff > 0 ? 'BUY on Kalshi (cheaper)' : 'SHORT on Kalshi (overpriced)',
-          });
-        }
-      }
-    }
-    // Dedupe: keep best match per Kalshi ticker
-    const seen = new Map();
-    for (const ref of crossRefs) {
-      const key = ref.kalshi_ticker;
-      if (!seen.has(key) || Math.abs(ref.price_diff) > Math.abs(seen.get(key).price_diff)) {
-        seen.set(key, ref);
-      }
-    }
-    return [...seen.values()].sort((a, b) => Math.abs(b.price_diff) - Math.abs(a.price_diff));
-  } catch (e) {
-    console.log(`[Ken] Polymarket cross-ref error: ${e.message}`);
-    return [];
-  }
-}
-
-// ══════════════════════════════════════════════
-// LNG / ENERGY CROSS-MARKET ARBITRAGE
-// Compares energy-related predictions across Kalshi, Polymarket, and Manifold
-// ══════════════════════════════════════════════
-const ENERGY_SEARCH_TERMS = [
-  'oil price', 'crude oil', 'natural gas', 'LNG', 'OPEC',
-  'energy price', 'gas price', 'heating oil', 'petroleum',
-  'electricity price', 'EIA', 'oil production', 'pipeline',
-  'renewable energy', 'solar', 'wind energy', 'nuclear',
-  'carbon price', 'emissions', 'electric vehicle', 'EV',
-  'barrel', 'WTI', 'Brent', 'refinery',
-];
-
-async function energyArbitrageScan(kalshiMarkets) {
-  const arbs = [];
-  try {
-    // 1. Get energy markets from Manifold
-    const manifoldEnergy = await scrapeManifold(ENERGY_SEARCH_TERMS.slice(0, 8));
-    console.log(`[Ken/Energy] Manifold: ${manifoldEnergy.length} energy-related markets`);
-
-    // 2. Get energy events from Polymarket
-    let polyEnergy = [];
-    try {
-      const polyAll = await scrapePolymarketEvents(200);
-      polyEnergy = polyAll.filter(p => {
-        const t = (p.event_title + ' ' + p.question).toLowerCase();
-        return /oil|gas|lng|opec|energy|crude|barrel|petroleum|electric|solar|wind|nuclear|carbon|emission|ev market|renewable/i.test(t);
-      });
-      console.log(`[Ken/Energy] Polymarket: ${polyEnergy.length} energy-related markets`);
-    } catch {}
-
-    // 3. Filter Kalshi markets for energy/commodities
-    const kalshiEnergy = kalshiMarkets.filter(m => {
-      const t = ((m.title || '') + ' ' + (m.subtitle || '') + ' ' + (m.category || '')).toLowerCase();
-      return /oil|gas|lng|opec|energy|crude|barrel|petroleum|electric|solar|wind|nuclear|carbon|emission|ev |renewable|consumption|climate/i.test(t);
-    });
-    console.log(`[Ken/Energy] Kalshi: ${kalshiEnergy.length} energy-related markets`);
-
-    // 4. Cross-reference Manifold vs Kalshi
-    for (const mf of manifoldEnergy) {
-      for (const km of kalshiEnergy) {
-        const score = fuzzyMatchScore(mf.title, (km.title + ' ' + (km.subtitle || '')).toLowerCase());
-        if (score >= 0.25) {
-          const priceDiff = mf.probability - km.yes_ask;
-          if (Math.abs(priceDiff) >= 3) {
-            arbs.push({
-              type: 'manifold_kalshi',
-              kalshi_ticker: km.ticker,
-              event_ticker: km.event_ticker,
-              kalshi_title: km.title,
-              kalshi_price: km.yes_ask,
-              other_title: mf.title,
-              other_price: mf.probability,
-              other_source: 'Manifold',
-              other_url: mf.url,
-              other_volume: mf.volume,
-              price_diff: priceDiff,
-              match_score: Math.round(score * 100),
-              category: 'energy',
-            });
-          }
-        }
-      }
-    }
-
-    // 5. Cross-reference Polymarket vs Kalshi (energy-specific)
-    for (const pm of polyEnergy) {
-      for (const km of kalshiEnergy) {
-        const score = fuzzyMatchScore(pm.event_title + ' ' + pm.question, (km.title + ' ' + (km.subtitle || '')).toLowerCase());
-        if (score >= 0.25) {
-          const priceDiff = pm.price - km.yes_ask;
-          if (Math.abs(priceDiff) >= 3) {
-            arbs.push({
-              type: 'poly_kalshi_energy',
-              kalshi_ticker: km.ticker,
-              event_ticker: km.event_ticker,
-              kalshi_title: km.title,
-              kalshi_price: km.yes_ask,
-              other_title: pm.question,
-              other_price: pm.price,
-              other_source: 'Polymarket',
-              other_url: `https://polymarket.com/event/${pm.slug}`,
-              other_volume: pm.volume,
-              price_diff: priceDiff,
-              match_score: Math.round(score * 100),
-              category: 'energy',
-            });
-          }
-        }
-      }
-    }
-
-    // 6. Cross-reference Manifold vs Polymarket (energy only, no Kalshi needed)
-    for (const mf of manifoldEnergy) {
-      for (const pm of polyEnergy) {
-        const score = fuzzyMatchScore(mf.title, pm.event_title + ' ' + pm.question);
-        if (score >= 0.3) {
-          const priceDiff = mf.probability - pm.price;
-          if (Math.abs(priceDiff) >= 5) {
-            arbs.push({
-              type: 'manifold_poly_energy',
-              kalshi_ticker: null,
-              event_ticker: null,
-              kalshi_title: `${mf.title} (cross-platform)`,
-              kalshi_price: mf.probability,
-              other_title: pm.question,
-              other_price: pm.price,
-              other_source: 'Manifold↔Polymarket',
-              other_url: mf.url,
-              other_volume: Math.max(mf.volume, pm.volume),
-              price_diff: priceDiff,
-              match_score: Math.round(score * 100),
-              category: 'energy',
-            });
-          }
-        }
-      }
-    }
-
-    // Dedupe by best price diff per Kalshi ticker
-    const seen = new Map();
-    for (const arb of arbs) {
-      const key = arb.kalshi_ticker || arb.other_title;
-      if (!seen.has(key) || Math.abs(arb.price_diff) > Math.abs(seen.get(key).price_diff)) {
-        seen.set(key, arb);
-      }
-    }
-    const result = [...seen.values()].sort((a, b) => Math.abs(b.price_diff) - Math.abs(a.price_diff));
-    if (result.length > 0) {
-      console.log(`[Ken/Energy] Found ${result.length} energy arbitrage opportunities (top: ${result[0].kalshi_title} ${result[0].price_diff > 0 ? '+' : ''}${result[0].price_diff}¢)`);
-    }
-    return result;
-  } catch (e) {
-    console.log(`[Ken/Energy] Arbitrage scan error: ${e.message}`);
-    return [];
-  }
-}
-
-// ══════════════════════════════════════════════
-// AUTO-TRADER ($20 budget, risk-managed)
-// ══════════════════════════════════════════════
-const TRADE_CONFIG = {
-  enabled: true,               // Master kill switch
-  budget_cents: 2000,          // $20 total budget
-  max_position_cents: 200,     // $2 max per trade
-  max_open_positions: 5,       // Max concurrent positions
-  daily_loss_limit_cents: 400, // $4 daily stop-loss
-  min_confidence: 80,          // Only trade 80%+ confidence signals
-  min_sources: 2,              // Must have 2+ corroborating sources
-  take_profit_pct: 80,         // Take profit at 80% of max (e.g. sell YES at 90+ if bought at 50)
-  stop_loss_pct: 50,           // Stop loss: exit if position loses 50% of cost
-  fee_rate: 0.07,              // Kalshi ~7% fee on winnings
-  cooldown_minutes: 30,        // Don't re-trade same ticker within 30 min
-};
-
-// ── TRADE_CONFIG Persistence ──
-async function initConfigTable() {
-  try {
-    await kenQ(`
-      CREATE TABLE IF NOT EXISTS ken_config (
-        key TEXT PRIMARY KEY,
-        value JSONB NOT NULL,
-        updated_at TIMESTAMPTZ DEFAULT NOW()
-      )
-    `);
-  } catch (e) {
-    console.log(`[Ken] Config table init error: ${e.message}`);
-  }
-}
-
-async function loadTradeConfig() {
-  try {
-    await initConfigTable();
-    const { rows } = await kenQ("SELECT value FROM ken_config WHERE key = 'trade_config'");
-    if (rows.length > 0) {
-      const saved = rows[0].value;
-      // Merge saved values into TRADE_CONFIG (preserves new keys added in code)
-      for (const [k, v] of Object.entries(saved)) {
-        if (k in TRADE_CONFIG) TRADE_CONFIG[k] = v;
-      }
-      console.log(`[Ken] Loaded TRADE_CONFIG from DB: budget=$${(TRADE_CONFIG.budget_cents/100).toFixed(0)}, conf=${TRADE_CONFIG.min_confidence}%, enabled=${TRADE_CONFIG.enabled}`);
-    }
-  } catch (e) {
-    console.log(`[Ken] Failed to load trade config: ${e.message}`);
-  }
-}
-
-async function saveTradeConfig() {
-  try {
-    await kenQ(`
-      INSERT INTO ken_config (key, value, updated_at) VALUES ('trade_config', $1, NOW())
-      ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()
-    `, [JSON.stringify(TRADE_CONFIG)]);
-  } catch (e) {
-    console.log(`[Ken] Failed to save trade config: ${e.message}`);
-  }
-}
-
-async function getTraderState() {
-  try {
-    const openTrades = await kenQ('SELECT * FROM ken_trades WHERE status = $1 ORDER BY created_at DESC', ['open']);
-    const today = new Date().toISOString().split('T')[0];
-    const todayPnL = await kenQ("SELECT COALESCE(SUM(pnl_cents), 0) as total FROM ken_trades WHERE closed_at::date = $1::date", [today]);
-    const totalInvested = (openTrades.rows || []).reduce((sum, t) => sum + (t.cost_cents || 0), 0);
-    const dailyLoss = Math.abs(Math.min(0, todayPnL.rows[0]?.total || 0));
-    return {
-      openPositions: openTrades.rows || [],
-      openCount: (openTrades.rows || []).length,
-      totalInvested,
-      dailyLoss,
-      available: TRADE_CONFIG.budget_cents - totalInvested,
-    };
-  } catch (e) {
-    console.log(`[Ken] Trader state error: ${e.message}`);
-    return { openPositions: [], openCount: 0, totalInvested: 0, dailyLoss: 0, available: 0 };
-  }
-}
-
-async function autoTrader(signals) {
-  if (!TRADE_CONFIG.enabled) return;
-
-  try {
-    // Check if API keys are configured
-    const cfgRow = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
-    const cfg = cfgRow.rows[0]?.config || {};
-    if (!cfg.kalshi_api_key || !cfg.kalshi_private_key) {
-      return; // Silent - no keys configured yet
-    }
-
-    const state = await getTraderState();
-
-    // Daily loss limit check
-    if (state.dailyLoss >= TRADE_CONFIG.daily_loss_limit_cents) {
-      console.log(`[Ken] TRADER: Daily loss limit hit ($${(state.dailyLoss/100).toFixed(2)}). No new trades today.`);
-      return;
-    }
-
-    // ── 1. CHECK EXISTING POSITIONS FOR EXIT ──
-    for (const trade of state.openPositions) {
-      try {
-        // Get current market price
-        const mktData = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(trade.ticker)}`);
-        const mkt = mktData.market || mktData;
-        const currentPrice = trade.side === 'yes' ? (mkt.yes_ask || mkt.last_price || 0) : (mkt.no_ask || mkt.last_price || 0);
-
-        // Market resolved?
-        if (mkt.status === 'settled' || mkt.result) {
-          const won = (mkt.result === trade.side);
-          const pnl = won ? (100 - trade.price_cents) * trade.count : -(trade.cost_cents);
-          const feeAdjusted = won ? Math.round(pnl * (1 - TRADE_CONFIG.fee_rate)) : pnl;
-          await kenQ('UPDATE ken_trades SET status=$1, exit_price=$2, pnl_cents=$3, closed_at=NOW() WHERE id=$4',
-            ['resolved', won ? 100 : 0, feeAdjusted, trade.id]);
-          const emoji = feeAdjusted > 0 ? ':moneybag:' : ':chart_with_downwards_trend:';
-          await sendSlack(`${emoji} *Ken Trade Resolved*\n*${trade.title}* (${trade.ticker})\nSide: ${trade.side.toUpperCase()} @ ${trade.price_cents}¢ → Result: ${mkt.result?.toUpperCase()}\nP&L: *${feeAdjusted > 0 ? '+' : ''}${(feeAdjusted/100).toFixed(2)}* (after fees)`);
-          continue;
-        }
-
-        // Take profit check
-        if (currentPrice >= TRADE_CONFIG.take_profit_pct) {
-          try {
-            const sellOrder = await kalshiFetch('POST', '/portfolio/orders', {
-              ticker: trade.ticker, side: trade.side, action: 'sell', type: 'market', count: trade.count,
-            });
-            const sellPrice = currentPrice;
-            const pnl = Math.round((sellPrice - trade.price_cents) * trade.count * (1 - TRADE_CONFIG.fee_rate));
-            await kenQ('UPDATE ken_trades SET status=$1, exit_price=$2, pnl_cents=$3, closed_at=NOW(), order_id=$4 WHERE id=$5',
-              ['profit', sellPrice, pnl, sellOrder.order?.order_id || trade.order_id, trade.id]);
-            await sendSlack(`:rocket: *Ken Take Profit!*\n*${trade.title}* (${trade.ticker})\nBought ${trade.side.toUpperCase()} @ ${trade.price_cents}¢ → Sold @ ${sellPrice}¢\nP&L: *+$${(pnl/100).toFixed(2)}* :moneybag:`);
-          } catch (e) {
-            console.log(`[Ken] TRADER: Take profit failed for ${trade.ticker}: ${e.message}`);
-          }
-          continue;
-        }
-
-        // Stop loss check
-        const unrealizedLoss = (trade.price_cents - currentPrice) * trade.count;
-        if (unrealizedLoss > trade.cost_cents * (TRADE_CONFIG.stop_loss_pct / 100)) {
-          try {
-            const sellOrder = await kalshiFetch('POST', '/portfolio/orders', {
-              ticker: trade.ticker, side: trade.side, action: 'sell', type: 'market', count: trade.count,
-            });
-            const sellPrice = currentPrice;
-            const pnl = (sellPrice - trade.price_cents) * trade.count;
-            await kenQ('UPDATE ken_trades SET status=$1, exit_price=$2, pnl_cents=$3, closed_at=NOW() WHERE id=$4',
-              ['stopped', sellPrice, pnl, trade.id]);
-            await sendSlack(`:stop_sign: *Ken Stop Loss*\n*${trade.title}* (${trade.ticker})\nBought ${trade.side.toUpperCase()} @ ${trade.price_cents}¢ → Stopped @ ${sellPrice}¢\nP&L: *-$${(Math.abs(pnl)/100).toFixed(2)}*`);
-          } catch (e) {
-            console.log(`[Ken] TRADER: Stop loss failed for ${trade.ticker}: ${e.message}`);
-          }
-          continue;
-        }
-      } catch (e) {
-        console.log(`[Ken] TRADER: Position check failed for ${trade.ticker}: ${e.message}`);
-      }
-    }
-
-    // ── 2. LOOK FOR NEW TRADES ──
-    // Refresh state after exits
-    const freshState = await getTraderState();
-    if (freshState.openCount >= TRADE_CONFIG.max_open_positions) {
-      console.log(`[Ken] TRADER: Max positions (${freshState.openCount}/${TRADE_CONFIG.max_open_positions}). Waiting for exits.`);
-      return;
-    }
-
-    // Filter signals: high confidence, multiple sources, tradeable price
-    const tradeable = signals.filter(s =>
-      s.confidence >= TRADE_CONFIG.min_confidence &&
-      (s.sources?.length || 0) >= TRADE_CONFIG.min_sources &&
-      s.entry_price > 5 && s.entry_price < 90 && // Don't trade near extremes
-      s.ticker
-    ).sort((a, b) => b.confidence - a.confidence);
-
-    // Check cooldown: don't re-trade same ticker
-    const recentTickers = new Set();
-    try {
-      const recent = await kenQ("SELECT ticker FROM ken_trades WHERE created_at > NOW() - INTERVAL '" + TRADE_CONFIG.cooldown_minutes + " minutes'");
-      for (const r of (recent.rows || [])) recentTickers.add(r.ticker);
-    } catch {}
-    // Also skip tickers we already have open positions on
-    for (const t of freshState.openPositions) recentTickers.add(t.ticker);
-
-    let tradesPlaced = 0;
-    for (const sig of tradeable) {
-      if (freshState.openCount + tradesPlaced >= TRADE_CONFIG.max_open_positions) break;
-      if (recentTickers.has(sig.ticker)) continue;
-
-      // Calculate position size: max $2, but respect available budget
-      const priceCents = sig.entry_price;
-      const costPerContract = priceCents; // Cost = price in cents for YES; (100 - price) for NO
-      const maxContracts = Math.floor(Math.min(TRADE_CONFIG.max_position_cents, freshState.available) / costPerContract);
-      if (maxContracts < 1) continue;
-      const count = Math.min(maxContracts, 3); // Max 3 contracts per trade
-      const totalCost = count * costPerContract;
-
-      try {
-        const side = sig.side === 'NO' ? 'no' : 'yes';
-        const order = await kalshiFetch('POST', '/portfolio/orders', {
-          ticker: sig.ticker,
-          side: side,
-          action: 'buy',
-          type: 'limit',
-          count: count,
-          yes_price: side === 'yes' ? priceCents : undefined,
-          no_price: side === 'no' ? (100 - priceCents) : undefined,
-          time_in_force: 'gtc',
-        });
-
-        const orderId = order.order?.order_id || `ken_${Date.now()}`;
-        await kenQ(`INSERT INTO ken_trades (ticker, event_ticker, title, side, action, price_cents, count, cost_cents, order_id, signal_type, confidence, reasoning, status)
-          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'open')`,
-          [sig.ticker, sig.event_ticker || '', sig.market || '', side, 'buy', priceCents, count, totalCost,
-           orderId, sig.signal_type || '', sig.confidence, sig.reason || '']);
-
-        recentTickers.add(sig.ticker);
-        tradesPlaced++;
-        freshState.available -= totalCost;
-
-        const kLink = kalshiLink(sig.ticker, sig.event_ticker);
-        await sendSlack(`:chart_with_upwards_trend: *Ken Auto-Trade Placed!*\n${confBadge(sig.confidence)} *${sig.market}*\n${side.toUpperCase()} x${count} @ ${priceCents}¢ = $${(totalCost/100).toFixed(2)}\nSignal: ${sig.signal_type} | ${sig.reason}\n${kLink}\n_Budget remaining: $${(freshState.available/100).toFixed(2)}_`);
-        console.log(`[Ken] TRADER: ${side.toUpperCase()} x${count} ${sig.ticker} @ ${priceCents}¢ ($${(totalCost/100).toFixed(2)}) — ${sig.signal_type} ${sig.confidence}%`);
-
-      } catch (e) {
-        console.log(`[Ken] TRADER: Order failed for ${sig.ticker}: ${e.message}`);
-      }
-    }
-
-    if (tradesPlaced > 0) {
-      console.log(`[Ken] TRADER: ${tradesPlaced} new trades placed. Open: ${freshState.openCount + tradesPlaced}/${TRADE_CONFIG.max_open_positions}`);
-    }
-  } catch (e) {
-    console.log(`[Ken] TRADER ERROR: ${e.message}`);
-  }
-}
-
-// ══════════════════════════════════════════════
-// MAIN AUTONOMOUS SCAN (every 15 min, 24/7)
-// ══════════════════════════════════════════════
-async function autonomousScan() {
-  const scanStart = Date.now();
-  console.log('[Ken] ═══ AUTONOMOUS SCAN STARTING ═══');
-  try {
-    // 1. Refresh weather data
-    const cities = ['New York', 'Chicago', 'Miami', 'Denver', 'Los Angeles', 'Seattle', 'Houston', 'Phoenix', 'Minneapolis', 'Boston'];
-    const weatherData = generateSyntheticWeather(cities);
-
-    // Cache in DB
-    const row = (await q('SELECT id, config FROM risk_state ORDER BY updated_at DESC LIMIT 1')).rows[0];
-    if (row) {
-      const newCfg = { ...(row.config || {}), weather_cache: { results: weatherData, fetched_at: new Date().toISOString(), synthetic: true } };
-      await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newCfg), row.id]);
-    }
-
-    // 2. Fetch ALL live Kalshi events with nested markets (retry + fallback to prod)
-    let allEvents = [];
-    let weatherEvents = [];
-    let allMarkets = [];
-    const apiUrls = [KALSHI_BASE_URL, 'https://api.elections.kalshi.com/trade-api/v2', 'https://demo-api.kalshi.co/trade-api/v2'];
-    for (let attempt = 0; attempt < apiUrls.length; attempt++) {
-      try {
-        const url = apiUrls[attempt] + '/events?status=open&limit=200&with_nested_markets=true';
-        const controller = new AbortController();
-        const timeout = setTimeout(() => controller.abort(), 15000);
-        const res = await fetch(url, { headers: { 'Accept': 'application/json' }, signal: controller.signal });
-        clearTimeout(timeout);
-        if (!res.ok) throw new Error(`HTTP ${res.status}`);
-        const evData = await res.json();
-        allEvents = evData.events || [];
-        if (allEvents.length > 0) {
-          if (attempt > 0) console.log(`[Ken] Fallback API ${attempt + 1} worked`);
-          break;
-        }
-      } catch (e) {
-        console.log(`[Ken] API ${attempt + 1}/${apiUrls.length} failed: ${e.message}`);
-        if (attempt < apiUrls.length - 1) await new Promise(r => setTimeout(r, 3000));
-      }
-    }
-    // Build market list from events
-    for (const evt of allEvents) {
-      if (evt.markets) {
-        for (const m of evt.markets) {
-          allMarkets.push({
-            ticker: m.ticker,
-            event_ticker: evt.event_ticker,
-            title: evt.title,
-            subtitle: m.subtitle || m.ticker,
-            volume: m.volume || 0,
-            volume_24h: m.volume_24h || 0,
-            open_interest: m.open_interest || 0,
-            yes_bid: m.yes_bid || 0,
-            yes_ask: m.yes_ask || 0,
-            no_bid: m.no_bid || 0,
-            no_ask: m.no_ask || 0,
-            last_price: m.last_price || 0,
-            prev_price: m.previous_price || m.last_price || 0,
-            status: m.status,
-            category: evt.category || '',
-          });
-        }
-      }
-    }
-    // Filter weather events for model-based predictions
-    weatherEvents = allEvents.filter(e => {
-      const t = ((e.category || '') + ' ' + (e.title || '')).toLowerCase();
-      return t.includes('weather') || t.includes('temperature') || t.includes('temp ') ||
-             t.includes('rain') || t.includes('snow') || t.includes('precipitation') ||
-             t.includes('hurricane') || t.includes('wind') || t.includes('°f');
-    });
-    console.log(`[Ken] Kalshi: ${allEvents.length} events, ${allMarkets.length} markets, ${weatherEvents.length} weather`);
-
-    const month = new Date().getMonth() + 1;
-    const predictions = [];
-
-    // 3. Score weather events against our weather model
-    for (const evt of weatherEvents) {
-      const city = extractCity(evt.title);
-      if (!city) continue;
-      const spec = parseWeatherEvent(evt.title);
-      if (!spec || spec.variable === 'unknown') continue;
-      const cityWeather = weatherData.find(w => w.city === city);
-      if (!cityWeather) continue;
-      const features = cityWeather[spec.variable === 'snow' ? 'precipitation' : spec.variable] || cityWeather.temperature;
-      if (!features?.values?.length) continue;
-
-      const rawProb = ensembleExceedance(features, spec.threshold, spec.operator);
-      const prior = climatologicalPrior(spec.variable, spec.threshold, month);
-      const combined = bayesianUpdate(prior, rawProb ?? 0.5);
-      const pYes = Math.max(0.01, Math.min(0.99, combined));
-
-      let marketPrice = 50;
-      const mkt = (evt.markets || [])[0];
-      if (mkt) {
-        const p = mkt.yes_ask || mkt.last_price || 50;
-        if (p > 0 && p < 100) marketPrice = p;
-      }
-
-      const edgeBps = Math.round((pYes * 100 - marketPrice) * 100);
-      predictions.push({
-        market: evt.title,
-        city,
-        side: edgeBps > 0 ? 'yes' : 'no',
-        p_yes: Math.round(pYes * 1000) / 10,
-        price_cents: marketPrice,
-        edge_bps: edgeBps,
-        variable: spec.variable,
-        threshold: spec.threshold,
-        type: 'weather_model',
-      });
-    }
-
-    // 4. Scan ALL high-volume markets for volume-based signals
-    const volumeIdeas = [];
-
-    // Sort by 24h volume to find what's hot
-    const activeMarkets = allMarkets
-      .filter(m => m.yes_bid > 0 && m.yes_ask > 0 && m.yes_ask < 100 && m.last_price > 0)
-      .sort((a, b) => b.volume_24h - a.volume_24h);
-
-    // Cache for 5-minute signal pipeline
-    cachedActiveMarkets = activeMarkets;
-
-    // Load price momentum from DB (compare current vs 2h ago and 6h ago)
-    let momentumMap = {};
-    // Also update cached version for signal pipeline MC simulations
-    try {
-      const { rows: momentumRows } = await kenQ(`
-        WITH latest AS (
-          SELECT DISTINCT ON (ticker) ticker, last_price as current_price, yes_bid, yes_ask, captured_at
-          FROM ken_market_snapshots ORDER BY ticker, captured_at DESC
-        ),
-        two_hours AS (
-          SELECT DISTINCT ON (ticker) ticker, last_price as price_2h
-          FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '2 hours'
-          ORDER BY ticker, captured_at DESC
-        ),
-        six_hours AS (
-          SELECT DISTINCT ON (ticker) ticker, last_price as price_6h
-          FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '6 hours'
-          ORDER BY ticker, captured_at DESC
-        )
-        SELECT l.ticker, l.current_price, t.price_2h, s.price_6h,
-               l.current_price - COALESCE(t.price_2h, l.current_price) as delta_2h,
-               l.current_price - COALESCE(s.price_6h, l.current_price) as delta_6h
-        FROM latest l LEFT JOIN two_hours t ON l.ticker = t.ticker LEFT JOIN six_hours s ON l.ticker = s.ticker
-      `);
-      for (const r of momentumRows) {
-        momentumMap[r.ticker] = { delta_2h: parseInt(r.delta_2h) || 0, delta_6h: parseInt(r.delta_6h) || 0, price_2h: r.price_2h, price_6h: r.price_6h };
-      }
-    } catch (e) { console.log('[Ken] Momentum query error:', e.message); }
-    cachedMomentumMap = momentumMap; // Update shared cache for signal pipeline
-
-    // ═══ INSTANT SLACK ALERT: Major Price Movements (>=10¢ in 2h or >=15¢ in 6h) ═══
-    try {
-      const bigMovers = Object.entries(momentumMap)
-        .filter(([, m]) => Math.abs(m.delta_2h) >= 10 || Math.abs(m.delta_6h) >= 15)
-        .sort((a, b) => Math.abs(b[1].delta_2h) - Math.abs(a[1].delta_2h));
-      if (bigMovers.length > 0) {
-        const moverLines = bigMovers.slice(0, 8).map(([ticker, m]) => {
-          const mkt = activeMarkets.find(am => am.ticker === ticker);
-          const title = mkt ? mkt.title : ticker;
-          const d2 = m.delta_2h > 0 ? `+${m.delta_2h}` : `${m.delta_2h}`;
-          const d6 = m.delta_6h > 0 ? `+${m.delta_6h}` : `${m.delta_6h}`;
-          const icon = Math.abs(m.delta_2h) >= 10 ? (m.delta_2h > 0 ? ':chart_with_upwards_trend:' : ':chart_with_downwards_trend:') : ':left_right_arrow:';
-          const et = mkt?.event_ticker || ticker?.replace(/-[^-]+$/, '') || ticker;
-          const link = `<https://kalshi.com/markets/${et}/${ticker}|View>`;
-          return `  ${icon} *${title}*\n     2h: *${d2}¢* | 6h: *${d6}¢* | Now: ${mkt?.last_price || '?'}¢ | ${link}`;
-        }).join('\n');
-        const timeStr = new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles', hour: 'numeric', minute: '2-digit', hour12: true });
-        await sendSlack(`:rotating_light: *Major Price Movements* (${timeStr} PT)\n_${bigMovers.length} market(s) moved 10+¢ in 2h or 15+¢ in 6h_\n\n${moverLines}\n\nDashboard: http://45.61.58.125:7810`);
-        console.log(`[Ken] SLACK ALERT: ${bigMovers.length} major price movement(s) sent`);
-      }
-    } catch (e) { console.log('[Ken] Price movement alert error:', e.message); }
-
-    for (const m of activeMarkets.slice(0, 200)) {
-      const spread = m.yes_ask - m.yes_bid;
-      const noPrice = 100 - m.yes_ask;
-      const momentum = momentumMap[m.ticker] || { delta_2h: 0, delta_6h: 0 };
-
-      // ── SHORT: YES >= 75¢ (buy NO cheap, wider net than before) ──
-      if (m.yes_bid >= 75 && noPrice <= 25) {
-        let shortConf = 40;
-        if (noPrice <= 5) shortConf += 15;      // dirt cheap NO
-        else if (noPrice <= 10) shortConf += 10;
-        else if (noPrice <= 15) shortConf += 5;
-        if (momentum.delta_2h < 0) shortConf += 10;  // price dropping = good short
-        if (momentum.delta_6h < -3) shortConf += 10;  // strong downtrend
-        if (m.volume_24h > 5000) shortConf += 5;
-        if (m.open_interest > 100000) shortConf += 5;
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'NO',
-          reason: `YES at ${m.yes_bid}¢ → NO at ${noPrice}¢${momentum.delta_2h ? ` (${momentum.delta_2h > 0 ? '+' : ''}${momentum.delta_2h}¢/2h)` : ''}`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'short_high_price', confidence: Math.min(90, shortConf),
-        });
-      }
-
-      // ── MOMENTUM SHORT: price dropping fast (was 50+, fell 5+ in 2h) ──
-      if (momentum.delta_2h <= -5 && m.last_price >= 30 && m.last_price <= 80) {
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'NO',
-          reason: `Dropping ${momentum.delta_2h}¢/2h (${momentum.delta_6h || '?'}¢/6h) — momentum sell`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'momentum_short', confidence: Math.min(80, 50 + Math.abs(momentum.delta_2h) * 2),
-        });
-      }
-
-      // ── MOMENTUM LONG: price rising fast (was low, jumped 5+ in 2h) ──
-      if (momentum.delta_2h >= 5 && m.last_price <= 70 && m.last_price >= 10) {
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'YES',
-          reason: `Rising +${momentum.delta_2h}¢/2h (+${momentum.delta_6h || '?'}¢/6h) — momentum buy`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'momentum_long', confidence: Math.min(80, 50 + momentum.delta_2h * 2),
-        });
-      }
-
-      // ── LONG CHEAP: YES <= 20¢ with volume (wider net) ──
-      if (m.yes_ask <= 20 && m.volume_24h > 100) {
-        let longConf = 35;
-        if (m.yes_ask <= 5) longConf += 5;        // lottery ticket
-        if (m.volume_24h > 1000) longConf += 10;  // lots of interest
-        if (m.volume_24h > 5000) longConf += 10;
-        if (momentum.delta_2h > 0) longConf += 10; // price rising
-        if (momentum.delta_6h > 2) longConf += 10;
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'YES',
-          reason: `YES at ${m.yes_ask}¢, vol ${m.volume_24h.toLocaleString()}${momentum.delta_2h ? ` (${momentum.delta_2h > 0 ? '+' : ''}${momentum.delta_2h}¢/2h)` : ''}`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'long_cheap', confidence: Math.min(80, longConf),
-        });
-      }
-
-      // ── CONTRARIAN SHORT: YES at 60-84¢ with negative momentum ──
-      if (m.yes_bid >= 60 && m.yes_bid < 85 && momentum.delta_2h < -2 && m.volume_24h > 200) {
-        volumeIdeas.push({
-          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'NO',
-          reason: `YES fading from ${(m.last_price + Math.abs(momentum.delta_2h))}¢→${m.last_price}¢, NO at ${noPrice}¢`,
-          volume_24h: m.volume_24h, open_interest: m.open_interest,
-          yes_price: m.yes_ask, no_price: noPrice, spread,
-          type: 'contrarian_short', confidence: Math.min(75, 45 + Math.abs(momentum.delta_2h) * 3),
-        });
-      }
-
-      // ── VOLUME SPIKE (kept but enhanced with momentum) ──
-      if (m.volume > 0 && m.volume_24h / m.volume > 0.05 && m.volume_24h > 500) {
-        volumeIdeas.push({
-          market: m.title,
-          subtitle: m.subtitle,
-          ticker: m.ticker,
-          event_ticker: m.event_ticker,
-          side: m.last_price > m.prev_price ? 'YES momentum' : 'NO momentum',
-          reason: `24h vol ${m.volume_24h.toLocaleString()} = ${Math.round(m.volume_24h / m.volume * 100)}% of all-time`,
-          volume_24h: m.volume_24h,
-          open_interest: m.open_interest,
-          yes_price: m.yes_ask,
-          no_price: noPrice,
-          spread,
-          type: 'volume_spike',
-        });
-      }
-
-      // Spread opportunity: wide spread with volume = market-making opportunity
-      if (spread >= 5 && m.volume_24h > 100 && m.open_interest > 1000) {
-        volumeIdeas.push({
-          market: m.title,
-          subtitle: m.subtitle,
-          ticker: m.ticker,
-          event_ticker: m.event_ticker,
-          side: 'SPREAD',
-          reason: `${spread}¢ spread, bid ${m.yes_bid}¢ / ask ${m.yes_ask}¢`,
-          volume_24h: m.volume_24h,
-          open_interest: m.open_interest,
-          yes_price: m.yes_ask,
-          no_price: noPrice,
-          spread,
-          type: 'wide_spread',
-        });
-      }
-    }
-
-    // Deduplicate volume ideas by ticker (keep highest confidence)
-    const deduped = new Map();
-    for (const idea of volumeIdeas) {
-      const existing = deduped.get(idea.ticker);
-      if (!existing || (idea.confidence || 50) > (existing.confidence || 50)) {
-        deduped.set(idea.ticker, idea);
-      }
-    }
-    const uniqueVolumeIdeas = [...deduped.values()];
-
-    logMemory('scan-after-signals');
-
-    // 5. Store market snapshots in DB (batch of 150 to capture more markets)
-    for (const m of activeMarkets.slice(0, 150)) {
-      try {
-        await kenQ(`
-          INSERT INTO ken_market_snapshots (ticker, event_ticker, title, subtitle, category, yes_bid, yes_ask, no_bid, no_ask, last_price, volume, volume_24h, open_interest, spread)
-          VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
-        `, [m.ticker, m.event_ticker, m.title, m.subtitle, m.category, m.yes_bid, m.yes_ask, m.no_bid, m.no_ask, m.last_price, m.volume, m.volume_24h, m.open_interest, m.yes_ask - m.yes_bid]);
-      } catch {}
-    }
-
-    // 6. Get sentiment data from last 2 hours for signal boosting
-    let sentimentBoosts = {};
-    try {
-      const { rows: recentSentiment } = await kenQ(`
-        SELECT relevance_ticker, AVG(sentiment_score) as avg_sent, COUNT(*) as post_count, SUM(score) as total_score
-        FROM ken_sentiment
-        WHERE captured_at > NOW() - INTERVAL '2 hours' AND relevance_ticker IS NOT NULL
-        GROUP BY relevance_ticker
-      `);
-      for (const s of recentSentiment) {
-        sentimentBoosts[s.relevance_ticker] = {
-          avgSentiment: parseFloat(s.avg_sent) || 0,
-          postCount: parseInt(s.post_count) || 0,
-          totalScore: parseInt(s.total_score) || 0,
-        };
-      }
-    } catch {}
-
-    // Get recent news sentiment (non-ticker-specific)
-    let newsSentiment = {};
-    try {
-      const { rows: newsRows } = await kenQ(`
-        SELECT keyword, AVG(sentiment_score) as avg_sent, COUNT(*) as cnt
-        FROM ken_sentiment
-        WHERE source = 'google_news' AND captured_at > NOW() - INTERVAL '4 hours'
-        GROUP BY keyword
-      `);
-      for (const n of newsRows) {
-        newsSentiment[n.keyword] = { avg: parseFloat(n.avg_sent) || 0, count: parseInt(n.cnt) || 0 };
-      }
-    } catch {}
-
-    // 7. Polymarket cross-reference for arbitrage
-    let polyArbs = [];
-    try {
-      polyArbs = await polymarketCrossRef(activeMarkets);
-    } catch (e) {
-      console.log('[Ken] Polymarket cross-ref failed:', e.message);
-    }
-
-    // 7b. Energy/LNG cross-market arbitrage scan
-    let energyArbs = [];
-    try {
-      energyArbs = await energyArbitrageScan(activeMarkets);
-    } catch (e) {
-      console.log('[Ken] Energy arb scan failed:', e.message);
-    }
-
-    // 8. Combine ALL signals with confidence scoring
-    const buySignals = predictions.filter(p => p.edge_bps > 200);
-    const shortSignals = predictions.filter(p => p.edge_bps < -200);
-    const longIdeas = uniqueVolumeIdeas.filter(v => v.side === 'YES' || v.side === 'YES momentum');
-    const shortIdeas = uniqueVolumeIdeas.filter(v => ['short_high_price', 'momentum_short', 'contrarian_short'].includes(v.type));
-    const spikeIdeas = uniqueVolumeIdeas.filter(v => v.type === 'volume_spike');
-    const momentumIdeas = uniqueVolumeIdeas.filter(v => v.type === 'momentum_long' || v.type === 'momentum_short');
-    const spreadIdeas = uniqueVolumeIdeas.filter(v => v.type === 'wide_spread');
-
-    // 9. Composite confidence with sentiment + momentum
-    const allSignals = [];
-    for (const p of [...buySignals, ...shortSignals]) {
-      let confidence = 50 + Math.min(30, Math.abs(p.edge_bps) / 50);
-      const sentBoost = sentimentBoosts[p.market] || {};
-      if (sentBoost.postCount > 3) confidence += sentBoost.avgSentiment > 0 ? 10 : -5;
-      const mom = momentumMap[p.ticker] || {};
-      if (p.edge_bps > 0 && mom.delta_2h > 0) confidence += 8;   // momentum confirms long
-      if (p.edge_bps < 0 && mom.delta_2h < 0) confidence += 8;   // momentum confirms short
-      const sources = ['weather_model'];
-      if (sentBoost.postCount) sources.push('reddit');
-      if (mom.delta_2h) sources.push('momentum');
-      allSignals.push({ ...p, confidence: Math.min(95, Math.round(confidence)), sources, signal_type: p.edge_bps > 0 ? 'long' : 'short' });
-    }
-    for (const v of uniqueVolumeIdeas) {
-      let confidence = v.confidence || 45; // Use pre-calculated confidence from signal detection
-      // Boost with sentiment
-      const sentKey = Object.keys(sentimentBoosts).find(k => v.ticker?.startsWith(k));
-      const sentBoost = sentKey ? sentimentBoosts[sentKey] : null;
-      if (sentBoost && sentBoost.postCount > 3) {
-        if (v.side === 'YES' && sentBoost.avgSentiment > 0.05) confidence += 10;
-        if (v.side === 'NO' && sentBoost.avgSentiment < -0.05) confidence += 10;
-      }
-      // Boost with news
-      for (const [nk, nv] of Object.entries(newsSentiment)) {
-        const titleLower = (v.market || '').toLowerCase();
-        if (nk.split(' ').some(w => w.length > 4 && titleLower.includes(w))) {
-          if (v.side === 'YES' && nv.avg > 0.05) confidence += 5;
-          if (v.side === 'NO' && nv.avg < -0.05) confidence += 5;
-          break;
-        }
-      }
-      const sources = ['volume_analysis'];
-      if (v.type?.includes('momentum')) sources.push('momentum');
-      if (sentBoost) sources.push('reddit');
-      const isShort = v.side === 'NO' || ['short_high_price', 'momentum_short', 'contrarian_short'].includes(v.type);
-      allSignals.push({
-        market: v.market, ticker: v.ticker, event_ticker: v.event_ticker, subtitle: v.subtitle,
-        side: isShort ? 'NO' : v.side === 'SPREAD' ? 'YES' : 'YES',
-        confidence: Math.min(95, Math.round(confidence)),
-        entry_price: isShort ? v.no_price : v.yes_price,
-        edge_bps: isShort ? Math.round((100 - (v.yes_price || 50)) * 100) : 0,
-        signal_type: v.type || 'volume',
-        reason: v.reason, sources, volume_24h: v.volume_24h,
-      });
-    }
-    for (const arb of polyArbs) {
-      allSignals.push({
-        market: arb.kalshi_title, ticker: arb.kalshi_ticker, event_ticker: arb.event_ticker,
-        side: arb.price_diff > 0 ? 'YES' : 'NO',
-        confidence: Math.min(90, 55 + Math.abs(arb.price_diff) * 2),
-        entry_price: arb.kalshi_price,
-        edge_bps: arb.price_diff * 100,
-        signal_type: 'polymarket_arb',
-        reason: `Kalshi ${arb.kalshi_price}¢ vs Poly ${arb.poly_price}¢ (${arb.price_diff > 0 ? '+' : ''}${arb.price_diff}¢ gap | ${arb.match_score}% match)`,
-        sources: ['polymarket_crossref'],
-      });
-    }
-    // Energy/LNG cross-market arb signals
-    for (const arb of energyArbs) {
-      if (arb.kalshi_ticker) {
-        allSignals.push({
-          market: arb.kalshi_title, ticker: arb.kalshi_ticker, event_ticker: arb.event_ticker,
-          side: arb.price_diff > 0 ? 'YES' : 'NO',
-          confidence: Math.min(92, 60 + Math.abs(arb.price_diff) * 2),
-          entry_price: arb.kalshi_price,
-          edge_bps: arb.price_diff * 100,
-          signal_type: 'energy_arb',
-          reason: `ENERGY ARB: Kalshi ${arb.kalshi_price}¢ vs ${arb.other_source} ${arb.other_price}¢ (${arb.price_diff > 0 ? '+' : ''}${arb.price_diff}¢ gap) — BUY ${arb.price_diff > 0 ? 'Kalshi' : arb.other_source}, SELL ${arb.price_diff > 0 ? arb.other_source : 'Kalshi'}`,
-          sources: ['energy_crossmarket_arb', arb.other_source.toLowerCase()],
-        });
-      }
-    }
-
-    // 10. Store predictions in DB
-    for (const sig of allSignals) {
-      try {
-        await kenQ(`
-          INSERT INTO ken_predictions (ticker, event_ticker, title, signal_type, side, confidence, entry_price, edge_bps, reasoning, sources)
-          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
-        `, [
-          sig.ticker || '', sig.event_ticker || '', sig.market || '', sig.signal_type,
-          sig.side, sig.confidence, sig.entry_price || sig.price_cents || 0, sig.edge_bps || 0,
-          sig.reason || sig.reasoning || `${sig.signal_type} signal`, JSON.stringify(sig.sources || []),
-        ]);
-      } catch {}
-    }
-
-    // 10b. AUTO-TRADER: Execute trades on high-confidence signals
-    try {
-      await autoTrader(allSignals);
-    } catch (e) {
-      console.log(`[Ken] Auto-trader error: ${e.message}`);
-    }
-
-    // 10c. PORTFOLIO TRADER: Feed scan signals into paper portfolios
-    let portfolioTradesPlaced = 0;
-    for (const sig of allSignals.filter(s => s.confidence >= 40 && s.ticker)) {
-      try {
-        await autoPortfolioTrade({
-          marketId: sig.ticker,
-          market_title: sig.market || sig.ticker,
-          direction: sig.side === 'YES' ? 'BUY_YES' : 'BUY_NO',
-          expectedEdge: (sig.edge_bps || 0) / 10000,
-          confidence: (sig.confidence || 50) / 100,
-          reasoning: sig.reason || sig.reasoning || sig.signal_type,
-          signal_type: sig.signal_type,
-          aiEnhanced: (sig.sources || []).includes('gemini'),
-          mcSimulation: { consistent: true, stdDev: 0.05 },
-          num_articles: (sig.sources || []).length,
-          agreement: (sig.confidence || 50) / 100,
-        });
-        portfolioTradesPlaced++;
-      } catch {}
-    }
-    if (portfolioTradesPlaced > 0) console.log(`[Ken] Portfolio trades: ${portfolioTradesPlaced} signals fed to paper portfolios`);
-
-    const scanDuration = Date.now() - scanStart;
-
-    // 11. Log performance
-    try {
-      await kenQ(`
-        INSERT INTO ken_performance (total_markets, active_markets, predictions_made, long_signals, short_signals, sentiment_items, scan_duration_ms, slack_sent)
-        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
-      `, [
-        allMarkets.length, activeMarkets.length, allSignals.length,
-        allSignals.filter(s => s.side === 'YES').length,
-        allSignals.filter(s => s.side === 'NO').length,
-        Object.keys(sentimentBoosts).length,
-        scanDuration, false,
-      ]);
-    } catch {}
-
-    console.log(`[Ken] Scan complete: ${allMarkets.length} markets, ${activeMarkets.length} active, ${allSignals.length} signals (${allSignals.filter(s=>s.side==='YES').length} long, ${allSignals.filter(s=>s.side==='NO').length} short), ${polyArbs.length} arbs, ${Object.keys(sentimentBoosts).length} sentiment boosts | ${scanDuration}ms`);
-
-    // 12a. Run Signal Pipeline (News → Sentiment → Probability → Signals → Risk)
-    try {
-      const pipelineSignals = await signalPipeline(activeMarkets);
-      if (pipelineSignals > 0) {
-        console.log(`[Ken] Signal Pipeline produced ${pipelineSignals} risk-assessed signals`);
-      }
-    } catch (e) {
-      console.log(`[Ken] Signal Pipeline error: ${e.message}`);
-    }
-
-    // 12b. Update market mappings
-    try {
-      await updateMarketMappings(activeMarkets);
-    } catch {}
-
-    // 12. Build and send Slack alert (max once per hour to not spam)
-    const now = Date.now();
-    const shouldSlack = (now - lastSlackTime > SLACK_COOLDOWN_MS) || allSignals.some(s => s.confidence >= 75);
-
-    if (shouldSlack && allSignals.length > 0) {
-      lastSlackTime = now;
-      const timeStr = new Date().toLocaleString('en-US', { timeZone: 'America/New_York', hour: 'numeric', minute: '2-digit', hour12: true });
-      const parts = [];
-
-      parts.push(`:dog: *Ken — Autonomous Scan* (${timeStr} ET)`);
-      parts.push(`_${allMarkets.length} markets | ${activeMarkets.length} active | ${allSignals.length} signals | ${KALSHI_ENV}_\n`);
-
-      // Helper: build Kalshi market link
-      const kalshiLink = (ticker, eventTicker) => {
-        const et = eventTicker || ticker?.replace(/-[^-]+$/, '') || ticker;
-        return `<https://kalshi.com/markets/${et}/${ticker}|Trade on Kalshi>`;
-      };
-
-      // Helper: confidence badge with color (green→yellow→orange→red)
-      const confBadge = (conf) => {
-        if (conf >= 80) return `:large_green_circle: *${conf}%*`;
-        if (conf >= 65) return `:large_yellow_circle: *${conf}%*`;
-        if (conf >= 50) return `:large_orange_circle: *${conf}%*`;
-        return `:red_circle: *${conf}%*`;
-      };
-
-      // Confidence legend
-      parts.push(`_:large_green_circle: 80%+ Strong  :large_yellow_circle: 65-79% Good  :large_orange_circle: 50-64% Moderate  :red_circle: <50% Speculative_`);
-
-      // High-confidence signals first (>= 70)
-      const highConf = allSignals.filter(s => s.confidence >= 70).sort((a, b) => b.confidence - a.confidence);
-      if (highConf.length > 0) {
-        const lines = highConf.slice(0, 5).map((s, i) => {
-          const icon = s.side === 'YES' ? ':chart_with_upwards_trend:' : ':small_red_triangle_down:';
-          const link = kalshiLink(s.ticker, s.event_ticker);
-          return `  ${icon} ${confBadge(s.confidence)} *${s.market}*\n     ${s.side} | ${s.reason || `Edge ${s.edge_bps}bps`}\n     :link: ${link}`;
-        }).join('\n');
-        parts.push(`:rotating_light: *High Confidence Signals*\n${lines}`);
-      }
-
-      // Short ideas
-      const shorts = allSignals.filter(s => s.side === 'NO' && s.confidence >= 50).sort((a, b) => b.confidence - a.confidence);
-      if (shorts.length > 0 && highConf.filter(s => s.side === 'NO').length === 0) {
-        const lines = shorts.slice(0, 3).map((s, i) => {
-          const link = kalshiLink(s.ticker, s.event_ticker);
-          return `  ${confBadge(s.confidence)} *${s.market}* — BUY NO\n     ${s.reason || ''}\n     :link: ${link}`;
-        }).join('\n');
-        parts.push(`:moneybag: *Short Ideas (Buy NO)*\n${lines}`);
-      }
-
-      // Volume spikes
-      if (spikeIdeas.length > 0) {
-        const lines = spikeIdeas.sort((a, b) => b.volume_24h - a.volume_24h).slice(0, 3)
-          .map((p, i) => {
-            const link = kalshiLink(p.ticker, p.event_ticker);
-            const conf = p.confidence || 45;
-            return `  ${confBadge(conf)} *${p.market}*\n     ${p.reason}\n     :link: ${link}`;
-          }).join('\n');
-        parts.push(`:fire: *Volume Spikes*\n${lines}`);
-      }
-
-      // Momentum moves
-      if (momentumIdeas.length > 0) {
-        const lines = momentumIdeas.sort((a, b) => (b.confidence || 0) - (a.confidence || 0)).slice(0, 4)
-          .map((p, i) => {
-            const icon = p.type === 'momentum_long' ? ':arrow_up:' : ':arrow_down:';
-            const link = kalshiLink(p.ticker, p.event_ticker);
-            const conf = p.confidence || 50;
-            return `  ${icon} ${confBadge(conf)} *${p.market}*\n     ${p.subtitle || ''} — ${p.reason}\n     :link: ${link}`;
-          }).join('\n');
-        parts.push(`:rocket: *Momentum Moves*\n${lines}`);
-      }
-
-      // Polymarket arbitrage
-      if (polyArbs.length > 0) {
-        const lines = polyArbs.slice(0, 5)
-          .map(a => {
-            const conf = Math.min(90, 55 + Math.abs(a.price_diff) * 2);
-            const kLink = kalshiLink(a.kalshi_ticker, a.event_ticker);
-            const pLink = `<https://polymarket.com/event/${a.poly_slug}|Polymarket>`;
-            return `  ${confBadge(conf)} *${a.kalshi_title}* (${a.kalshi_subtitle || ''})\n     Kalshi: ${a.kalshi_price}¢ vs Poly: ${a.poly_price}¢ → *${a.arb_side}*\n     ${kLink} | ${pLink} | Match: ${a.match_score}%`;
-          }).join('\n');
-        parts.push(`:scales: *Polymarket Arbitrage (${polyArbs.length} found)*\n${lines}`);
-      }
-
-      // Reddit buzz
-      const buzzKeys = Object.entries(sentimentBoosts).filter(([, v]) => v.postCount >= 3).sort((a, b) => b[1].totalScore - a[1].totalScore);
-      if (buzzKeys.length > 0) {
-        const lines = buzzKeys.slice(0, 3).map(([ticker, v]) => {
-          const sentIcon = v.avgSentiment > 0.1 ? ':large_green_circle:' : v.avgSentiment < -0.1 ? ':red_circle:' : ':large_yellow_circle:';
-          return `  ${sentIcon} ${ticker}: ${v.postCount} posts, sentiment ${v.avgSentiment > 0 ? '+' : ''}${v.avgSentiment.toFixed(2)}, karma ${v.totalScore}`;
-        }).join('\n');
-        parts.push(`:speech_balloon: *Reddit Buzz*\n${lines}`);
-      }
-
-      // Auto-trader portfolio summary
-      try {
-        const traderState = await getTraderState();
-        const closed = await kenQ("SELECT COUNT(*) as total, 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, COALESCE(SUM(pnl_cents), 0) as pnl FROM ken_trades WHERE status != 'open'");
-        const cs = closed.rows[0] || {};
-        const pnl = parseInt(cs.pnl || 0);
-        const pnlStr = `${pnl >= 0 ? '+' : ''}$${(pnl/100).toFixed(2)}`;
-        const pnlEmoji = pnl > 0 ? ':moneybag:' : pnl < 0 ? ':chart_with_downwards_trend:' : ':neutral_face:';
-        const openLines = traderState.openPositions.map(t =>
-          `  • ${t.side.toUpperCase()} ${t.ticker} x${t.count} @ ${t.price_cents}¢ ($${(t.cost_cents/100).toFixed(2)})`
-        ).join('\n');
-        parts.push(`:bank: *Auto-Trader Portfolio*\n` +
-          `Budget: $${(TRADE_CONFIG.budget_cents/100).toFixed(2)} | Invested: $${(traderState.totalInvested/100).toFixed(2)} | Available: $${(traderState.available/100).toFixed(2)}\n` +
-          `Open: ${traderState.openCount}/${TRADE_CONFIG.max_open_positions} positions` +
-          (openLines ? `\n${openLines}` : '') + `\n` +
-          `${pnlEmoji} P&L: *${pnlStr}* (${cs.wins || 0}W/${cs.losses || 0}L) | Today loss: $${(traderState.dailyLoss/100).toFixed(2)}/$${(TRADE_CONFIG.daily_loss_limit_cents/100).toFixed(2)}`);
-      } catch {}
-
-      // Performance stats
-      try {
-        const { rows: perfRows } = await kenQ(`
-          SELECT COUNT(*) as total_preds,
-            SUM(CASE WHEN outcome='win' THEN 1 ELSE 0 END) as wins,
-            SUM(CASE WHEN outcome='loss' THEN 1 ELSE 0 END) as losses
-          FROM ken_predictions WHERE created_at > NOW() - INTERVAL '24 hours'
-        `);
-        const perf = perfRows[0];
-        if (perf && parseInt(perf.total_preds) > 0) {
-          parts.push(`_24h stats: ${perf.total_preds} predictions, ${perf.wins || 0}W/${perf.losses || 0}L_`);
-        }
-      } catch {}
-
-      parts.push(`\nDashboard: http://45.61.58.125:7810`);
-
-      await sendSlack(parts.join('\n\n'));
-
-      // Update performance row
-      try {
-        await kenQ(`UPDATE ken_performance SET slack_sent = true WHERE id = (SELECT MAX(id) FROM ken_performance)`);
-      } catch {}
-    } else if (allSignals.length > 0) {
-      const cooldownLeft = Math.max(0, Math.round((SLACK_COOLDOWN_MS - (now - lastSlackTime)) / 60000));
-      console.log(`[Ken] ${allSignals.length} signals found, Slack cooldown: ${cooldownLeft}min left`);
-    } else {
-      console.log(`[Ken] No signals this cycle (${allMarkets.length} markets scanned)`);
-    }
-
-  } catch (err) {
-    console.error('[Ken] Autonomous scan error:', err.message);
-    try { await sendSlack(`:warning: *Ken Error* — Scan failed: ${err.message}`); } catch {}
-  }
-}
-
-// ══════════════════════════════════════════════
-// AUTO-PORTFOLIO TRADING ENGINE — 10 personalities, $20/day each
-// ══════════════════════════════════════════════
-
-// Portfolio strategy filters — each personality decides differently
-// ═══ Portfolio Strategy Filters ═══
-// Each strategy is deliberately exclusive — a signal should match 1-3 portfolios, not all 10.
-// Strategies use edge/confidence/direction/article count/MC/AI to create real diversification.
-const PORTFOLIO_STRATEGIES = {
-  // Alpha: High edge (>15%) with LOWER confidence (<70%). Big edges, less certainty — speculative.
-  aggressive: (sig) => Math.abs(sig.edge) >= 0.15 && sig.confidence < 0.70,
-
-  // Beta: High confidence (>70%) with ANY edge. The "safe" bet — trusts confidence over edge size.
-  conservative: (sig) => sig.confidence >= 0.70 && Math.abs(sig.edge) >= 0.05,
-
-  // Storm: Weather/climate markets identified by market_id patterns or reasoning keywords
-  weather: (sig) => /weather|temp|rain|snow|wind|hurricane|precip|storm|°f|heat|cold|frost|drought|flood|tornado|KXTEMP|KXRAIN|KXSNOW|KXHURR/i.test((sig.reasoning || '') + ' ' + (sig.market_id || '')),
-
-  // Momentum: Only trades when edge increased (high agreement = many articles agree).
-  // Needs 3+ articles with >65% agreement — evidence of growing consensus.
-  momentum: (sig) => (sig.agreement || 0) >= 0.65 && (sig.num_articles || 0) >= 3 && Math.abs(sig.edge) >= 0.05,
-
-  // Contrarian: ONLY BUY_NO trades. Bets against the crowd. Never buys YES.
-  contrarian: (sig) => sig.direction === 'BUY_NO',
-
-  // Nova: Only Gemini AI-enhanced signals. If AI didn't weigh in, skip.
-  ai_enhanced: (sig) => sig.ai_enhanced === true,
-
-  // Oracle: MC-validated WITH tight spread (stdDev < 8%) AND moderate edge. The "careful" bot.
-  mc_validated: (sig) => sig.mc_consistent === true && (sig.mc_stddev || 99) < 0.08 && Math.abs(sig.edge) >= 0.05 && Math.abs(sig.edge) < 0.20,
-
-  // Shark: Small edges (3-10%), rapid fire. Takes quantity over quality. Rejects big edges.
-  scalper: (sig) => Math.abs(sig.edge) >= 0.03 && Math.abs(sig.edge) < 0.10 && sig.confidence >= 0.30,
-
-  // Turtle: Ultra-patient. Needs EVERYTHING: 5+ articles, MC consistent, confidence >80%, moderate edge.
-  patient: (sig) => sig.confidence >= 0.80 && sig.mc_consistent === true && (sig.num_articles || 0) >= 5 && Math.abs(sig.edge) >= 0.05,
-
-  // Wildcard: Random 20% of signals — true control group. Lower chance than before to create separation.
-  random: (sig) => Math.random() < 0.20,
-
-  // ═══ RISKY PORTFAUXLIOS: Micro → Macro ═══
-
-  // Penny: Any edge >= 1%. Catches everything. Micro bets, max volume.
-  penny_pincher: (sig) => Math.abs(sig.edge) >= 0.01,
-
-  // Spray: Random 50% of ALL signals. Shotgun approach — tests luck vs skill.
-  spray_pray: (sig) => Math.random() < 0.50,
-
-  // Nickel: Low bar (edge >= 2%, conf >= 20%). Catches most signals.
-  nickel_slots: (sig) => Math.abs(sig.edge) >= 0.02 && sig.confidence >= 0.20,
-
-  // Degen: Edge >= 5%, ignores confidence entirely. Pure edge chaser.
-  degen_lite: (sig) => Math.abs(sig.edge) >= 0.05,
-
-  // Politics: Only political markets. KXPRES, KXSEN, KXGOV, KXCONGRESS, KXPRESPERSON, etc.
-  politics_junkie: (sig) => /KXPRES|KXSEN|KXGOV|KXCONG|KXHOUSE|KXELEC|KXPARTY|KXPRESPERSON|political|election|congress|senate|president|governor/i.test((sig.market_id || '') + ' ' + (sig.reasoning || '')),
-
-  // Double: Only high-edge signals (>20%). Goes big when edge is massive.
-  double_down: (sig) => Math.abs(sig.edge) >= 0.20,
-
-  // Night: Only signals with high MC stddev (>8%) — volatile markets others reject.
-  volatility_rider: (sig) => (sig.mc_stddev || 0) >= 0.08 && Math.abs(sig.edge) >= 0.05,
-
-  // Whale: Edge >25% AND confidence >60%. Rare but massive. The sniper.
-  whale: (sig) => Math.abs(sig.edge) >= 0.25 && sig.confidence >= 0.60,
-
-  // YOLO: Edge >30%. Doesn't care about anything else. Max degen.
-  yolo: (sig) => Math.abs(sig.edge) >= 0.30,
-
-  // Full Send: Takes EVERY signal. Max exposure. Tests whether the signal pipeline itself is profitable.
-  full_send: (sig) => true,
-
-  // ═══ CPU-POWERED: Computational / Multi-Factor Strategies ═══
-
-  // Kelly: Only trades when Kelly fraction (edge / odds) > 0.10. Classic bankroll math.
-  // Kelly f* = (bp - q) / b where b=odds, p=prob_win, q=prob_lose. Simplified: edge/(1+edge).
-  kelly_criterion: (sig) => {
-    const e = Math.abs(sig.edge);
-    const kelly = e / (1 + e); // simplified Kelly fraction
-    return kelly >= 0.10 && sig.confidence >= 0.40;
-  },
-
-  // Sharpe: Signal-to-noise ratio. edge / mc_stddev > 2.0. High SNR = clean signal, low noise.
-  sharpe_sniper: (sig) => {
-    const snr = Math.abs(sig.edge) / Math.max(sig.mc_stddev, 0.01);
-    return snr >= 2.0 && sig.mc_consistent;
-  },
-
-  // Bayesian: Combines confidence with article count as evidence weight.
-  // More articles = stronger prior. posterior = conf * (1 - 1/(1+articles)) * edge_factor.
-  bayesian_blend: (sig) => {
-    const evidenceWeight = 1 - 1 / (1 + (sig.num_articles || 0));
-    const posterior = sig.confidence * evidenceWeight * (1 + Math.abs(sig.edge));
-    return posterior >= 0.55;
-  },
-
-  // Ensemble: ALL factors must align — AI + MC + confidence + edge. Maximum consensus required.
-  ensemble_lock: (sig) => sig.ai_enhanced && sig.mc_consistent && sig.confidence >= 0.60 && Math.abs(sig.edge) >= 0.10 && (sig.num_articles || 0) >= 2,
-
-  // Mean Revert: High MC volatility (stddev > 10%) + moderate edge. Bets on reversion to mean.
-  mean_revert: (sig) => (sig.mc_stddev || 0) >= 0.10 && Math.abs(sig.edge) >= 0.08 && Math.abs(sig.edge) <= 0.25,
-
-  // Info Ratio: (articles * agreement) / max(mc_stddev, 0.01). Quality of information per unit noise.
-  info_ratio: (sig) => {
-    const ir = ((sig.num_articles || 0) * (sig.agreement || 0)) / Math.max(sig.mc_stddev, 0.01);
-    return ir >= 15 && Math.abs(sig.edge) >= 0.05;
-  },
-
-  // Anti-FOMO: Small edge (<12%) but HIGH confidence (>75%). Calm, certain, steady plays.
-  anti_fomo: (sig) => Math.abs(sig.edge) >= 0.03 && Math.abs(sig.edge) <= 0.12 && sig.confidence >= 0.75,
-
-  // Nightcrawler: BUY_NO only + AI enhanced + MC consistent. Sophisticated short-bias strategy.
-  nightcrawler: (sig) => sig.direction === 'BUY_NO' && sig.ai_enhanced && sig.mc_consistent,
-
-  // Heatseeker: Composite multi-factor score. Weighted blend of all signal dimensions.
-  // score = edge*40 + conf*30 + agreement*20 + min(articles/10,1)*10. Trade when score > 35.
-  heatseeker: (sig) => {
-    const score = Math.abs(sig.edge) * 40 + sig.confidence * 30 + (sig.agreement || 0) * 20 + Math.min((sig.num_articles || 0) / 10, 1) * 10;
-    return score >= 35;
-  },
-
-  // Quant: Geometric mean of edge and confidence, weighted by MC consistency. Pure math bot.
-  // geoMean = sqrt(edge * conf). Trade when geoMean > 0.15 and MC agrees.
-  quant_core: (sig) => {
-    const geo = Math.sqrt(Math.abs(sig.edge) * Math.max(sig.confidence, 0.001));
-    return geo >= 0.15 && (sig.mc_consistent || sig.confidence >= 0.80);
-  },
-
-  // ═══ ETF CATEGORY SPECIALISTS: Sector-Focused Portfolios ═══
-
-  etf_presidential: (sig) => /KXPRESPERSON|KXPRESPARTY/i.test(sig.market_id) ||
-    /president|2028 election|presidential race|white house|oval office|nominee/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_cabinet: (sig) => /KXCABOUT|KXCABLEAVE/i.test(sig.market_id) ||
-    /cabinet|secretary of|confirmation hearing|appointee|gabbard|hegseth|rubio|noem|rfk|bessent|lutnick/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_congress: (sig) => /KXCONG|KXHOUSE|SENATE|KXNEXTSPEAKER|KXCAPCONTROL|KXVETOOVERRIDE/i.test(sig.market_id) ||
-    /congress|senate|house of rep|speaker|legislation|bill pass|shutdown|filibuster|debt ceiling/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_scotus: (sig) => /KXSCOURT/i.test(sig.market_id) ||
-    /supreme court|scotus|justice|ruling|overturn|constitutional|judicial review/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_fed: (sig) => /KXFEDCHAIRNOM|KXBALANCE/i.test(sig.market_id) ||
-    /federal reserve|fed chair|interest rate|fomc|rate cut|rate hike|powell|warsh|monetary policy|quantitative/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_economy: (sig) => /KXGDP/i.test(sig.market_id) ||
-    /\bgdp\b|recession|economic growth|jobs report|unemployment|nonfarm payroll|consumer spending|retail sales|pce\b|cpi\b/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_geopolitics: (sig) => /KXZELENSKYPUTIN|KXNEXTUKPM|KXNEXTIRANLEADER|KXEUEXIT|KXTAIWANLVL|KXFTA/i.test(sig.market_id) ||
-    /geopolit|war\b|invasion|sanction|nato|treaty|ceasefire|diplomat|putin|zelensky|china|taiwan|iran|tariff|trade war/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_territory: (sig) => /KXGREENLAND|KXGREENTERRITORY|KXGREENLANDPRICE|KXGTAPRICE|KXSTATE51|KXUSAEXPANDTERRITORY/i.test(sig.market_id) ||
-    /greenland|statehood|territory|annex|state 51|panama canal|territorial/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_climate: (sig) => /KXWARMING|KXTEMP|KXRAIN|KXSNOW|KXHURR/i.test(sig.market_id) ||
-    /warming|climate|temperature|hurricane|tornado|flood|drought|polar vortex|heat wave|cold snap|freeze|precipitation|storm/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_energy: (sig) => /KXPRIMEENGCONSUMPTION|KXENERG/i.test(sig.market_id) ||
-    /\benergy\b|lng\b|natural gas|oil price|petroleum|electricity|power grid|blackout|eia\b|opec|crude|pipeline|refinery|utility|heating bill/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_tech: (sig) => /KXOAIANTH|KXMUSKTRILLION|KXFUSION/i.test(sig.market_id) ||
-    /artificial intelligence|\bai\b|openai|anthropic|chatgpt|gpt-5|machine learning|musk.*trillion|tesla|nvidia|semiconductor|quantum computing|fusion/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_space: (sig) => /KXMOONMAN|KXCOLONIZEMARS|KXSPACEX/i.test(sig.market_id) ||
-    /spacex|nasa|rocket|orbit|mars|moon|starship|falcon|launch|artemis|blue origin|astronaut|satellite/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_crypto: (sig) => /KXCRYPTO|KXBTC|KXETH/i.test(sig.market_id) ||
-    /bitcoin|btc|ethereum|eth|crypto|blockchain|defi|stablecoin|sec.*crypto|digital asset|web3|binance|coinbase/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_sports: (sig) => /KXNBA|KXNFL|KXNBASEATTLE|KXCANADACUP/i.test(sig.market_id) ||
-    /\bnba\b|\bnfl\b|\bmlb\b|\bnhl\b|sports|playoff|championship|super bowl|world series|draft|mvp|coach/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_entertainment: (sig) => /KXSWIFTKELCE|KXTAYLORSWIFT|KXACTORSONNYCROCKETT|KXLIVENATION/i.test(sig.market_id) ||
-    /taylor swift|celebrity|movie|oscar|grammy|concert|entertainment|viral|streaming|netflix|album|tour/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_legal: (sig) => /KXTRUMPPARDONS|KXINSURRECTION|KXIMPEACH|KXTRUMPREMOVE|KXTRUMPRESIGN/i.test(sig.market_id) ||
-    /pardon|impeach|indictment|trial|guilty|conviction|sentencing|insurrection|special counsel|subpoena|contempt/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_financials: (sig) => /KXBOND/i.test(sig.market_id) ||
-    /bond yield|treasury|10-year|yield curve|s&p|nasdaq|dow jones|stock market|bear market|bull market|correction|ipo|banking/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_govreform: (sig) => /KXAGENCYELIM|KXGOVTCUTS|KXWITHDRAW/i.test(sig.market_id) ||
-    /\bdoge\b|government efficiency|agency.*elim|department.*cut|federal workforce|deregulat|budget cut|spending cut|executive order/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_global_social: (sig) => /KXNEWPOPE|KXALBERTAREFYES/i.test(sig.market_id) ||
-    /pope|vatican|catholic|referendum|independence|separatist|social movement|protest|migration|refugee/i.test(sig.reasoning + ' ' + sig.market_title),
-
-  etf_broad_market: (sig) => Math.abs(sig.edge) >= 0.08 && sig.confidence >= 0.50 && (sig.mc_consistent || sig.ai_enhanced),
-
-  // ═══ SECTOR-BASED PORTFOLIOS (IDs 51-100): Granular sub-sector filters ═══
-
-  // — Energy & Commodities (51-55) —
-  lng_exports: (sig) => /lng|liquefied natural gas|natural gas export|gas terminal|lng shipment|henry hub/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  oil_price_band: (sig) => /\boil\b|crude|brent|wti|petroleum|barrel|oil price|oil production/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  natgas_seasonal: (sig) => /natural gas|natgas|gas price|gas storage|heating season|gas futures/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  opec_decisions: (sig) => /opec|oil cartel|production cut|oil output|saudi.*oil|oil supply/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  power_grid: (sig) => /power grid|blackout|electricity|energy grid|brownout|utility|power outage|grid fail/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — AI & Tech Stocks (56-60) —
-  semiconductor: (sig) => /semiconductor|chip|nvidia|tsmc|intel|amd|chip act|wafer|fab\b|gpu shortage/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  ai_regulation: (sig) => /ai regulation|ai safety|ai executive order|ai bill|regulate.*ai|ai governance|ai act/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  tech_earnings: (sig) => /tech.*earning|earning.*tech|apple.*revenue|google.*revenue|meta.*revenue|amazon.*revenue|magnificent seven|big tech.*report/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  autonomous_vehicles: (sig) => /autonomous|self.driving|robotaxi|waymo|cruise|lidar|adas|driverless/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  cloud_market: (sig) => /cloud computing|aws|azure|google cloud|cloud revenue|saas|cloud market|cloud spend/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Crypto & Digital (61-65) —
-  btc_price_bands: (sig) => /bitcoin|btc|bitcoin price|btc.*\$|bitcoin.*\$|bitcoin.*above|bitcoin.*below/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  eth_upgrades: (sig) => /ethereum|eth\b|eth.*upgrade|ethereum.*upgrade|ethereum.*merge|eth.*fork|ether.*price/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  crypto_regulation: (sig) => /crypto.*regulat|sec.*crypto|crypto.*ban|crypto.*bill|digital asset.*law|crypto.*legal|stablecoin.*act/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  stablecoin_policy: (sig) => /stablecoin|usdt|usdc|tether|circle|stablecoin.*regulat|stablecoin.*bill|cbdc/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  defi_events: (sig) => /defi|decentralized finance|dex|uniswap|aave|compound|liquidity pool|yield farm|tvl/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Macro Economics (66-70) —
-  cpi_monthly: (sig) => /\bcpi\b|consumer price|inflation.*rate|inflation.*report|core inflation|price index/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  nfp_monthly: (sig) => /nonfarm|non.farm|payroll|jobs report|jobs.*added|employment.*report|unemployment.*rate|jobless.*claim/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  housing_data: (sig) => /housing|home.*sale|home.*price|mortgage|existing home|housing start|case.shiller|real estate.*data/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  gdp_quarterly: (sig) => /\bgdp\b|gross domestic|economic growth|gdp.*quarter|gdp.*annual|recession.*gdp/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  yield_spread: (sig) => /yield curve|yield spread|inverted yield|2.year.*10.year|treasury spread|curve inversion/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Finance & Markets (71-75) —
-  sp500_levels: (sig) => /s&p|s&p 500|sp500|spx|s&p.*above|s&p.*below|stock market.*level/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  fed_rate_path: (sig) => /fed.*rate|interest rate|fomc|rate cut|rate hike|federal funds|fed.*meeting|monetary policy|powell.*rate/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  bank_stress: (sig) => /bank.*stress|bank.*fail|banking crisis|bank run|fdic|bank.*collapse|regional bank|svb|silicon valley bank/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  bond_yields: (sig) => /bond.*yield|treasury.*yield|10.year.*yield|bond.*market|treasury.*rate|bond.*price/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  volatility_index: (sig) => /\bvix\b|volatility index|volatility.*spike|cboe|fear.*index|market.*volatility/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Politics & Policy (76-80) —
-  state_elections: (sig) => /KXSEN|KXGOV|state election|governor.*race|senate.*race|state house|state legislature|primary.*election/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  congress_votes: (sig) => /KXCONG|KXHOUSE|congress.*vote|house.*vote|senate.*vote|bill.*pass|legislation.*vote|floor vote/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  tariff_policy: (sig) => /tariff|trade war|import duty|trade deficit|trade policy|customs duty|trade agreement|wto/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  immigration_policy: (sig) => /immigration|border|migrant|asylum|deportation|visa|daca|immigration.*reform|border.*wall/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  tax_legislation: (sig) => /tax.*bill|tax.*reform|tax.*cut|tax.*hike|tax.*code|irs|estate tax|capital gains.*tax|corporate tax/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Sports (81-85) —
-  nba_outcomes: (sig) => /KXNBA|\bnba\b|basketball|nba.*final|nba.*champion|nba.*mvp|nba.*draft/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  nfl_outcomes: (sig) => /KXNFL|\bnfl\b|football|super bowl|nfl.*draft|nfl.*champion|quarterback|touchdown/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  soccer_global: (sig) => /soccer|football.*world|world cup|premier league|champions league|mls|fifa|la liga|bundesliga/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  mlb_outcomes: (sig) => /KXMLB|\bmlb\b|baseball|world series|mlb.*draft|home run|batting|pitcher/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  combat_sports: (sig) => /\bufc\b|\bmma\b|boxing|fight.*night|ufc.*champion|knockout|title fight|heavyweight/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Health & Science (86-90) —
-  fda_approvals: (sig) => /fda.*approv|fda.*reject|drug.*approv|new drug|biologic.*approv|fda.*review|pdufa/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  disease_tracking: (sig) => /outbreak|pandemic|epidemic|covid|h5n1|bird flu|mpox|disease.*spread|cdc.*alert|who.*emergency/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  biotech_catalysts: (sig) => /biotech|clinical trial|phase 3|gene therapy|mrna|crispr|drug.*trial|biotech.*stock/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  health_metrics: (sig) => /life expectancy|obesity|opioid|drug overdose|health.*metric|public health|mortality rate|vaccination rate/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  space_milestones: (sig) => /spacex|nasa|rocket.*launch|mars|moon.*landing|starship|artemis|blue origin|orbit|satellite.*launch/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Media & Culture (91-95) —
-  box_office: (sig) => /box office|movie.*gross|film.*revenue|opening weekend|blockbuster|cinema.*sales/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  awards_shows: (sig) => /oscar|grammy|emmy|golden globe|academy award|best picture|best actor|awards.*ceremony/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  streaming_subs: (sig) => /netflix.*subscriber|disney.*subscriber|streaming.*war|subscriber.*count|streaming.*growth|hulu|paramount/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  social_trends: (sig) => /viral|tiktok|social media.*trend|influencer|twitter.*trend|internet.*culture|meme/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  reality_outcomes: (sig) => /reality tv|bachelor|survivor|big brother|reality.*show|dancing.*stars|masked singer/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-
-  // — Global & Geopolitics (96-100) —
-  global_conflicts: (sig) => /war\b|invasion|ceasefire|military.*conflict|armed conflict|escalation|airstrike|troops.*deploy/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  sanctions_policy: (sig) => /sanction|embargo|trade restriction|asset freeze|sanctions.*russia|sanctions.*china|sanctions.*iran/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  climate_extremes: (sig) => /climate.*extreme|extreme weather|heat.*record|cold.*record|flood|wildfire|hurricane.*category|tornado|drought/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  forex_events: (sig) => /forex|currency.*crisis|dollar.*index|yen|euro.*dollar|fx.*rate|currency.*devaluation|exchange rate/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-  geopolitical_shock: (sig) => /black swan|geopolitical.*shock|coup|assassination|regime.*change|emergency.*declaration|martial law|unexpected.*event/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
-};
-
-// Position sizing per strategy — differentiated risk profiles
-const PORTFOLIO_SIZING = {
-  aggressive: (edge, conf) => Math.max(2, Math.min(5, Math.round(Math.abs(edge) * 30))),     // 2-5 contracts, edge-weighted
-  conservative: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),               // 1-3 contracts, confidence-weighted
-  weather: (edge, conf) => 2,                                                                  // Fixed 2 contracts
-  momentum: (edge, conf) => Math.max(1, Math.min(4, Math.round((conf + Math.abs(edge)) * 5))),// 1-4, blended
-  contrarian: (edge, conf) => 1,                                                               // Always 1 contract (high risk)
-  ai_enhanced: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),                // 1-3, conservative
-  mc_validated: (edge, conf) => Math.max(2, Math.min(4, Math.round(conf * 5))),               // 2-4, confidence-driven
-  scalper: (edge, conf) => 1,                                                                  // Always 1 (high volume, small bets)
-  patient: (edge, conf) => Math.max(3, Math.min(5, Math.round(conf * 6))),                    // 3-5, big when confident
-  random: (edge, conf) => Math.max(1, Math.min(3, Math.ceil(Math.random() * 3))),             // 1-3 random
-
-  // ═══ RISKY SIZING: Micro → Macro ═══
-  penny_pincher: () => 1,                                                                       // Always 1 — micro
-  spray_pray: () => Math.ceil(Math.random() * 2),                                               // 1-2 random
-  nickel_slots: () => 1,                                                                         // Fixed 1
-  degen_lite: (edge) => Math.max(1, Math.min(3, Math.round(Math.abs(edge) * 20))),             // 1-3, edge-scaled
-  politics_junkie: (edge, conf) => Math.max(2, Math.min(3, Math.round(conf * 4))),             // 2-3, confidence
-  double_down: (edge) => Math.max(4, Math.min(8, Math.round(Math.abs(edge) * 30))),            // 4-8, big edge bets
-  volatility_rider: (edge) => Math.max(2, Math.min(4, Math.round(Math.abs(edge) * 25))),       // 2-4, edge-weighted
-  whale: (edge, conf) => Math.max(5, Math.min(10, Math.round(conf * 12))),                     // 5-10, confidence-heavy
-  yolo: (edge) => Math.max(8, Math.min(15, Math.round(Math.abs(edge) * 40))),                  // 8-15, MAX edge scale
-  full_send: (edge, conf) => Math.max(1, Math.min(25, Math.round(Math.abs(edge) * conf * 50))),// 1-25, edge*conf scaled
-
-  // ═══ CPU-POWERED SIZING: Computational approaches ═══
-  kelly_criterion: (edge, conf) => Math.max(1, Math.min(8, Math.round((edge / (1 + edge)) * 20))),       // Kelly fraction scaled to 1-8
-  sharpe_sniper: (edge, conf) => Math.max(2, Math.min(6, Math.round(conf * 8))),                         // 2-6, conf-weighted (high SNR = trust it)
-  bayesian_blend: (edge, conf) => Math.max(1, Math.min(5, Math.round(Math.sqrt(edge * conf) * 15))),     // 1-5, geometric scaling
-  ensemble_lock: (edge, conf) => Math.max(3, Math.min(7, Math.round((edge + conf) * 5))),                // 3-7, sum-scaled (all factors aligned)
-  mean_revert: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),                          // 1-4, moderate sizing for reversion plays
-  info_ratio: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),                            // 2-5, confidence-driven
-  anti_fomo: (edge, conf) => Math.max(2, Math.min(4, Math.round(conf * 5))),                             // 2-4, steady sizing for calm plays
-  nightcrawler: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),                          // 1-3, conservative shorts
-  heatseeker: (edge, conf) => Math.max(2, Math.min(6, Math.round((edge * 20 + conf * 3) / 2))),         // 2-6, composite score-driven
-  quant_core: (edge, conf) => Math.max(2, Math.min(8, Math.round(Math.sqrt(edge * conf) * 25))),         // 2-8, geometric mean heavy
-
-  // ═══ ETF CATEGORY SIZING ═══
-  etf_presidential: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  etf_cabinet: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_congress: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_scotus: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  etf_fed: (edge, conf) => Math.max(2, Math.min(5, Math.round(edge * 20 + conf * 2))),
-  etf_economy: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),
-  etf_geopolitics: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_territory: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  etf_climate: () => 2,
-  etf_energy: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 18))),
-  etf_tech: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),
-  etf_space: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  etf_crypto: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 20))),
-  etf_sports: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_entertainment: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  etf_legal: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  etf_financials: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),
-  etf_govreform: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  etf_global_social: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
-  etf_broad_market: (edge, conf) => Math.max(2, Math.min(6, Math.round(edge * 15 + conf * 3))),
-
-  // ═══ SECTOR-BASED SIZING (IDs 51-100): Conservative niche bets ═══
-
-  // — Energy & Commodities (51-55) — Moderate sizing, energy can be volatile
-  lng_exports: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  oil_price_band: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),
-  natgas_seasonal: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  opec_decisions: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  power_grid: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-
-  // — AI & Tech Stocks (56-60) — Slightly larger, higher conviction sector
-  semiconductor: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  ai_regulation: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  tech_earnings: (edge, conf) => Math.max(2, Math.min(5, Math.round(edge * 18))),
-  autonomous_vehicles: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  cloud_market: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-
-  // — Crypto & Digital (61-65) — Volatile, keep positions smaller
-  btc_price_bands: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 16))),
-  eth_upgrades: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  crypto_regulation: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  stablecoin_policy: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  defi_events: (edge, conf) => Math.max(1, Math.min(2, Math.round(edge * 10))),
-
-  // — Macro Economics (66-70) — Data-driven, moderate confidence sizing
-  cpi_monthly: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  nfp_monthly: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  housing_data: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  gdp_quarterly: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),
-  yield_spread: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-
-  // — Finance & Markets (71-75) — Established signals, moderate-to-large
-  sp500_levels: (edge, conf) => Math.max(2, Math.min(5, Math.round(edge * 20))),
-  fed_rate_path: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),
-  bank_stress: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  bond_yields: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
-  volatility_index: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 18))),
-
-  // — Politics & Policy (76-80) — Binary outcomes, confidence-weighted
-  state_elections: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  congress_votes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  tariff_policy: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  immigration_policy: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  tax_legislation: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-
-  // — Sports (81-85) — Event-driven, fixed small sizing
-  nba_outcomes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  nfl_outcomes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  soccer_global: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  mlb_outcomes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
-  combat_sports: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-
-  // — Health & Science (86-90) — High-impact but rare, small positions
-  fda_approvals: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  disease_tracking: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  biotech_catalysts: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 10))),
-  health_metrics: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
-  space_milestones: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-
-  // — Media & Culture (91-95) — Fun bets, keep small
-  box_office: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  awards_shows: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  streaming_subs: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
-  social_trends: (edge, conf) => Math.max(1, Math.min(2, Math.round(edge * 8))),
-  reality_outcomes: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
-
-  // — Global & Geopolitics (96-100) — High uncertainty, conservative
-  global_conflicts: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
-  sanctions_policy: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
-  climate_extremes: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 10))),
-  forex_events: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
-  geopolitical_shock: (edge, conf) => Math.max(1, Math.min(2, Math.round(edge * 8))),
-};
-
-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 balance: bankrupt portfolios can't trade (balance = 0 → move to bottom)
-      if (pf.balance_cents <= 0) continue; // BANKRUPT — no more trades
-
-      // 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 || '',
-        market_id: signal.marketId || signal.market_id || '',
-        ai_enhanced: signal.aiEnhanced || signal.ai_enhanced || false,
-        mc_consistent: signal.mcSimulation?.consistent || signal.mc_consistent || false,
-        mc_stddev: signal.mcSimulation?.stdDev || 0.99,
-        volume24h: signal.volume24h || signal.volume_24h || 0,
-        num_articles: signal.num_articles || 0,
-        agreement: signal.agreement || 0,
-        market_title: signal.market_title || signal.marketTitle || '',
-      };
-
-      if (!strategy(sigData)) continue;
-
-      // Hyper-trade: max 10 OPEN positions per market per portfolio (no daily cap)
-      const { rows: dupes } = await kenQ(`
-        SELECT id FROM ken_portfolio_trades
-        WHERE portfolio_id = $1 AND market_id = $2 AND status = 'open'
-      `, [pf.id, signal.marketId || signal.market_id]);
-      if (dupes.length >= 10) continue;
-
-      // Per-strategy position sizing
-      const absEdge = Math.abs(parseFloat(sigData.edge));
-      const conf = parseFloat(sigData.confidence);
-      const sizeFn = PORTFOLIO_SIZING[pf.strategy] || ((e, c) => Math.max(1, Math.min(3, Math.round(e * c * 15))));
-      const contracts = sizeFn(absEdge, conf);
-      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 (cost > pf.balance_cents) continue; // Not enough balance for this trade
-
-      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]);
-
-      // Deduct from balance + update stats
-      await kenQ(`UPDATE ken_portfolios SET balance_cents = balance_cents - $1, total_invested_cents = total_invested_cents + $1, last_trade_at = NOW() WHERE id = $2`, [cost, pf.id]);
-      pf.balance_cents -= cost; // Update in-memory too for subsequent iterations
-
-      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);
-  }
-}
-
-// Capture NAV snapshots for charting — runs every 30 minutes
-async function capturePortfolioSnapshots() {
-  try {
-    const { rows: portfolios } = await kenQ('SELECT * FROM ken_portfolios');
-    for (const pf of portfolios) {
-      const { rows: openTrades } = await kenQ(`
-        SELECT entry_price_cents, contracts, direction, market_id
-        FROM ken_portfolio_trades WHERE portfolio_id = $1 AND status = 'open'
-      `, [pf.id]);
-
-      let openValue = 0;
-      for (const t of openTrades) {
-        const { rows: snaps } = await kenQ(`
-          SELECT last_price FROM ken_market_snapshots WHERE ticker = $1 ORDER BY captured_at DESC LIMIT 1
-        `, [t.market_id]);
-        if (snaps.length > 0) {
-          const curr = parseInt(snaps[0].last_price);
-          openValue += t.direction === 'BUY_YES'
-            ? (curr - t.entry_price_cents) * t.contracts
-            : (t.entry_price_cents - curr) * t.contracts;
-        }
-      }
-
-      const nav = (parseInt(pf.total_returned_cents) || 0) - (parseInt(pf.total_invested_cents) || 0) + openValue;
-      await kenQ(`
-        INSERT INTO ken_portfolio_snapshots (portfolio_id, nav_cents, open_positions) VALUES ($1, $2, $3)
-      `, [pf.id, nav, openTrades.length]);
-    }
-    console.log(`[Ken/Charts] NAV snapshots captured for ${portfolios.length} portfolios`);
-  } catch (e) {
-    console.error('[Ken/Charts] Snapshot error:', e.message);
-  }
-}
-
-// Resolve portfolio trades — HYPER MODE: quick take-profit/stop-loss + aging out
-async function resolvePortfolioTrades() {
-  try {
-    // Check ALL open trades (no minimum age for hyper-trading)
-    const { rows: openTrades } = await kenQ(`
-      SELECT pt.*, p.name as portfolio_name, p.strategy as portfolio_strategy
-      FROM ken_portfolio_trades pt
-      JOIN ken_portfolios p ON p.id = pt.portfolio_id
-      WHERE pt.status = 'open'
-      ORDER BY pt.created_at ASC
-      LIMIT 500
-    `);
-
-    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);
-      const ageMinutes = (Date.now() - new Date(trade.created_at).getTime()) / 60000;
-
-      let pnl = 0;
-      if (trade.direction === 'BUY_YES') {
-        pnl = (currentPrice - trade.entry_price_cents) * trade.contracts;
-      } else {
-        pnl = (trade.entry_price_cents - currentPrice) * trade.contracts;
-      }
-
-      // Hyper-trade exit rules:
-      // 1. Take profit: +3¢ per contract (minimum 5 min hold)
-      // 2. Stop loss: -5¢ per contract (minimum 5 min hold)
-      // 3. Age out: close after 2 hours regardless
-      let status = null;
-      const profitPerContract = pnl / Math.max(1, trade.contracts);
-
-      if (ageMinutes >= 5 && profitPerContract >= 3) {
-        status = 'won'; // Take profit
-      } else if (ageMinutes >= 5 && profitPerContract <= -5) {
-        status = 'lost'; // Stop loss
-      } else if (ageMinutes >= 120) {
-        // Age out after 2 hours — mark based on current P&L
-        status = pnl > 0 ? 'won' : pnl < 0 ? 'lost' : 'expired';
-      }
-
-      if (!status) continue; // Hold position
-
-      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 streakVal = status === 'won' ? 1 : status === 'lost' ? -1 : 0;
-      // total_returned = cost_cents + pnl for winning trades (you get your money back + profit)
-      const returned = pnl > 0 ? trade.cost_cents + pnl : (pnl === 0 ? trade.cost_cents : 0);
-      const wonInc = status === 'won' ? 1 : 0;
-      const lostInc = status === 'lost' ? 1 : 0;
-      const expInc = status === 'expired' ? 1 : 0;
-      await kenQ(`
-        UPDATE ken_portfolios SET
-          balance_cents = balance_cents + $1,
-          total_returned_cents = total_returned_cents + $1,
-          trades_won = trades_won + $2,
-          trades_lost = trades_lost + $3,
-          trades_expired = trades_expired + $4,
-          streak = CASE WHEN $5 > 0 AND streak > 0 THEN streak + 1 WHEN $5 < 0 AND streak < 0 THEN streak - 1 ELSE $5 END,
-          best_trade_cents = GREATEST(best_trade_cents, $6),
-          worst_trade_cents = LEAST(worst_trade_cents, $6)
-        WHERE id = $7
-      `, [returned, wonInc, lostInc, expInc, streakVal, pnl, trade.portfolio_id]);
-
-      // 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
-// ══════════════════════════════════════════════
-async function resolveOldPredictions() {
-  try {
-    // Get pending predictions older than 6 hours
-    const { rows } = await kenQ(`
-      SELECT id, ticker, side, entry_price FROM ken_predictions
-      WHERE outcome = 'pending' AND created_at < NOW() - INTERVAL '6 hours'
-      LIMIT 50
-    `);
-    for (const pred of rows) {
-      // Get latest snapshot for this ticker
-      const { rows: snaps } = await kenQ(`
-        SELECT last_price FROM ken_market_snapshots
-        WHERE ticker = $1 ORDER BY captured_at DESC LIMIT 1
-      `, [pred.ticker]);
-      if (snaps.length > 0) {
-        const currentPrice = snaps[0].last_price;
-        const entryPrice = pred.entry_price || 50;
-        let outcome = 'expired';
-        let pnl = 0;
-        if (pred.side === 'YES') {
-          pnl = currentPrice - entryPrice;
-          outcome = pnl > 0 ? 'win' : pnl < 0 ? 'loss' : 'expired';
-        } else {
-          pnl = entryPrice - currentPrice; // NO side profits when price drops
-          outcome = pnl > 0 ? 'win' : pnl < 0 ? 'loss' : 'expired';
-        }
-        await kenQ(`
-          UPDATE ken_predictions SET outcome = $1, exit_price = $2, pnl_cents = $3, resolved_at = NOW()
-          WHERE id = $4
-        `, [outcome, currentPrice, pnl, pred.id]);
-      }
-    }
-    if (rows.length > 0) console.log(`[Ken] Resolved ${rows.length} old predictions`);
-  } catch (e) {
-    console.error('[Ken] Prediction resolution error:', e.message);
-  }
-}
-
-// ══════════════════════════════════════════════
-// CLEANUP — prune old data (daily)
-// ══════════════════════════════════════════════
-async function cleanupOldData() {
-  try {
-    await kenQ(`DELETE FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '7 days'`);
-    await kenQ(`DELETE FROM ken_sentiment WHERE captured_at < NOW() - INTERVAL '14 days'`);
-    await kenQ(`DELETE FROM ken_performance WHERE scan_time < NOW() - INTERVAL '30 days'`);
-    await kenQ(`DELETE FROM ken_portfolio_snapshots WHERE snapshot_at < NOW() - INTERVAL '60 days'`);
-    console.log('[Ken] Old data cleaned up (including portfolio snapshots 60d retention)');
-  } catch (e) {
-    console.error('[Ken] Cleanup error:', e.message);
-  }
-}
-
-// ── Start Server ──
-server.listen(PORT, '0.0.0.0', () => {
-  console.log(`[Ken] Server running on http://0.0.0.0:${PORT}`);
-  console.log(`[Ken] Kalshi environment: ${KALSHI_ENV.toUpperCase()} (${KALSHI_BASE_URL})`);
-  console.log(`[Ken] Static files: ${DIST}`);
-  console.log(`[Ken] Auth: session cookie + IP whitelist + basic auth`);
-  console.log(`[Ken] ═══════════════════════════════════════`);
-  console.log(`[Ken] AUTONOMOUS MODE: 24/7 MARKET INTELLIGENCE`);
-  console.log(`[Ken] Market scan: every 8 minutes (CPU-boosted, was 15m)`);
-  console.log(`[Ken] Signal pipeline: every 2 minutes (MC simulation + Gemini AI enhanced)`);
-  console.log(`[Ken] Reddit/news: every 15 minutes (doubled, was 30m)`);
-  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 (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] ═══════════════════════════════════════`);
-
-  // Separate mutexes: sweep (heavy Reddit/news) and scan (light Kalshi API) can run concurrently
-  let sweepRunning = false;
-  let scanRunning = false;
-
-  async function runSweepSafe(name, fn) {
-    if (sweepRunning) { console.log(`[Ken] Skipping ${name} — another sweep in progress`); return; }
-    sweepRunning = true;
-    try { await fn(); } catch (e) { console.error(`[Ken] ${name} error:`, e.message); }
-    sweepRunning = false;
-    if (global.gc) global.gc();
-  }
-
-  async function runScanSafe(fn) {
-    if (scanRunning) { console.log('[Ken] Skipping Scan — another scan in progress'); return; }
-    scanRunning = true;
-    try { await fn(); } catch (e) { console.error(`[Ken] Scan error:`, e.message); }
-    scanRunning = false;
-    if (global.gc) global.gc();
-  }
-
-  // Load stored Kalshi env preference, trade config, and scan config from DB
-  refreshKalshiEnv().then(() => {
-    console.log(`[Ken] Kalshi environment: ${KALSHI_ENV.toUpperCase()} (${KALSHI_BASE_URL})`);
-  });
-  loadTradeConfig();
-  loadScanConfig();
-
-  // Initial: run reddit sweep and market scan (scan uses separate mutex, can overlap)
-  setTimeout(async () => {
-    runSweepSafe('Reddit init', redditIntelligenceLoop);
-    setTimeout(() => runScanSafe(autonomousScan), 10000);
-  }, 5000);
-
-  // Start all recurring intervals via stored handles (restartIntervals can update later)
-  scanIntervalId = setInterval(() => runScanSafe(autonomousScan), SCAN_INTERVAL_MS);
-  redditIntervalId = setInterval(() => runSweepSafe('Reddit', redditIntelligenceLoop), REDDIT_INTERVAL_MS);
-  signalIntervalId = setInterval(async () => {
-    if (cachedActiveMarkets.length > 0) {
-      try { await signalPipeline(cachedActiveMarkets); } catch (e) { console.log(`[Ken] Fast pipeline error: ${e.message}`); }
-    }
-  }, SIGNAL_PIPELINE_MS);
-
-  // Resolve old predictions every hour
-  setInterval(() => {
-    resolveOldPredictions().catch(e => console.error('[Ken] Resolution error:', e.message));
-  }, 60 * 60 * 1000);
-
-  // HYPER-TRADE: Resolve portfolio trades (interval stored in handle for UI control)
-  resolveIntervalId = setInterval(() => {
-    resolvePortfolioTrades().catch(e => console.error('[Ken] Portfolio resolution error:', e.message));
-  }, RESOLVE_INTERVAL_MS);
-  setTimeout(() => resolvePortfolioTrades().catch(() => {}), 30000);
-
-  // Portfolio NAV snapshots every 30 minutes for charting
-  setInterval(() => capturePortfolioSnapshots().catch(e => console.error('[Ken/Charts] Error:', e.message)), 30 * 60 * 1000);
-  setTimeout(() => capturePortfolioSnapshots().catch(() => {}), 25000); // Initial after startup
-
-  // 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);
-
-  // Weekly portfolio auto-refill: $100/portfolio every Monday (4-week trial)
-  // Checks every hour, only refills on Mondays between 0:00-0:59 UTC
-  const WEEKLY_REFILL_CENTS = 10000; // $100
-  const REFILL_WEEKS_REMAINING_KEY = 'ken_refill_weeks';
-  setInterval(async () => {
-    try {
-      const now = new Date();
-      if (now.getUTCDay() !== 1 || now.getUTCHours() !== 0) return; // Monday 0:00 UTC only
-      // Check if we already refilled this week
-      const { rows: [config] } = await kenQ(
-        `SELECT value FROM ken_config WHERE key = 'last_refill_date'`
-      ).catch(() => ({ rows: [] }));
-      const today = now.toISOString().split('T')[0];
-      if (config && config.value === today) return; // Already refilled today
-      // Check weeks remaining
-      const { rows: [weeksRow] } = await kenQ(
-        `SELECT value FROM ken_config WHERE key = $1`, [REFILL_WEEKS_REMAINING_KEY]
-      ).catch(() => ({ rows: [] }));
-      const weeksLeft = weeksRow ? parseInt(weeksRow.value) : 4;
-      if (weeksLeft <= 0) { console.log('[Ken] Weekly refill: 0 weeks remaining, skipping'); return; }
-      // Refill all portfolios
-      const { rowCount } = await kenQ(
-        `UPDATE ken_portfolios SET balance_cents = balance_cents + $1`, [WEEKLY_REFILL_CENTS]
-      );
-      // Update tracking (value column is jsonb, so wrap in JSON.stringify)
-      await kenQ(`INSERT INTO ken_config (key, value) VALUES ('last_refill_date', $1::jsonb)
-        ON CONFLICT (key) DO UPDATE SET value = $1::jsonb, updated_at = NOW()`, [JSON.stringify(today)]);
-      await kenQ(`INSERT INTO ken_config (key, value) VALUES ($1, $2::jsonb)
-        ON CONFLICT (key) DO UPDATE SET value = $2::jsonb, updated_at = NOW()`, [REFILL_WEEKS_REMAINING_KEY, JSON.stringify(String(weeksLeft - 1))]);
-      console.log(`[Ken] Weekly refill: +$${WEEKLY_REFILL_CENTS/100} to ${rowCount} portfolios. ${weeksLeft - 1} weeks remaining.`);
-    } catch (e) { console.error('[Ken] Weekly refill error:', e.message); }
-  }, 60 * 60 * 1000); // Check every hour
-
-  // Cleanup old data every 24 hours
-  setInterval(() => {
-    cleanupOldData().catch(e => console.error('[Ken] Cleanup error:', e.message));
-  }, 24 * 60 * 60 * 1000);
-});
diff --git a/react-dash/server.js b/react-dash/server.js
index 8477233..2a2bd67 100644
--- a/react-dash/server.js
+++ b/react-dash/server.js
@@ -30,7 +30,7 @@ const ALLOWED_EMAILS = (process.env.ALLOWED_EMAILS || '').split(',').filter(Bool
 const googleClient = GOOGLE_CLIENT_ID ? new OAuth2Client(GOOGLE_CLIENT_ID) : null;
 
 const pool = new pg.Pool({
-  connectionString: 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/bertha_betting',
+  connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/bertha_betting'),
   max: 10,
   idleTimeoutMillis: 30000,
 });
diff --git a/src/lib/db.js b/src/lib/db.js
index 031d0d3..c2e0c46 100644
--- a/src/lib/db.js
+++ b/src/lib/db.js
@@ -1,7 +1,7 @@
 const { Pool } = require('pg');
 
 const pool = new Pool({
-  connectionString: 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/bertha_betting',
+  connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/bertha_betting'),
   max: 10,
   idleTimeoutMillis: 30000,
   connectionTimeoutMillis: 5000,

← 1b70167 snapshot: 11 file(s) changed, +3 new, ~2 modified, -6 remove  ·  back to Bertha  ·  fix: replace backticks in comment inside KEN_INC_HTML templa 074ac28 →