← 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
M kalshi-dash/server.jsD kalshi-dash/server.js.bak-20260422-160936D kalshi-dash/server.js.pre-scrub-2026-05-07.bakM react-dash/server.jsM src/lib/db.js
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">← 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 • Paper Trading • Real Signals • 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 • {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} • {t.contracts}x • {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 • {d.system.cpuCores} cores • {d.system.uptime_hours}h uptime • 50 paper portfolios •
- <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">← 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 • Paper Trading • Real Signals • 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 • {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} • {t.contracts}x • {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 • {d.system.cpuCores} cores • {d.system.uptime_hours}h uptime • 50 paper portfolios •
- <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 →