← back to Bertha
kalshi-dash/server.js
9146 lines
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
// ══════════════════════════════════════════════════════
// Audit 2026-05-04: literal AIzaSy… key removed from source. Now env-driven.
// Previous in-source key needs rotation if still active. Fail-soft — when unset,
// geminiAnalyze() returns null (handled downstream).
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || '';
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();
try {
const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
logGemini(data, { app: 'kalshi-dash', note: 'ken-signal-analyze', model: GEMINI_MODEL });
} catch {}
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: (process.env.DATABASE_URL || 'postgresql://dw_admin@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 = (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); }
// ── 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);
});