← back to Bertha
chore(alshi-dash,kalshi-dash): update 2 files (.html) [+563/-1]
1124be99e744c3cd8ee3191ed19322661723f2d2 · 2026-02-15 17:00:25 +0000 · DW Commit Agent
Files touched
M kalshi-dash/dist/index.htmlM kalshi-dash/server.js
Diff
commit 1124be99e744c3cd8ee3191ed19322661723f2d2
Author: DW Commit Agent <commit-agent@dw-agents.com>
Date: Sun Feb 15 17:00:25 2026 +0000
chore(alshi-dash,kalshi-dash): update 2 files (.html) [+563/-1]
---
kalshi-dash/dist/index.html | 25 ++
kalshi-dash/server.js | 539 +++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 563 insertions(+), 1 deletion(-)
diff --git a/kalshi-dash/dist/index.html b/kalshi-dash/dist/index.html
index 27da6a5..345ed5e 100644
--- a/kalshi-dash/dist/index.html
+++ b/kalshi-dash/dist/index.html
@@ -10,6 +10,31 @@
<link rel="stylesheet" crossorigin href="/assets/index-BjOCoYyH.css">
</head>
<body>
+ <style>
+ .agent-avatar{width:56px;height:56px;border-radius:50%;border:2px solid #FF69B4;object-fit:cover}
+ .agent-avatar-fallback{width:56px;height:56px;border-radius:50%;border:2px solid #FF69B4;background:#FF69B4;color:#fff;font-size:22px;font-weight:700;display:none;align-items:center;justify-content:center}
+ </style>
<div id="root"></div>
+ <script>
+ (function(){
+ function injectAvatar(){
+ var h=document.querySelector('header,nav,[class*="header"],[class*="Header"],[class*="navbar"],[class*="Navbar"],.top-bar');
+ if(!h||h.querySelector('.agent-avatar'))return;
+ var img=document.createElement('img');
+ img.className='agent-avatar';
+ img.src='http://45.61.58.125/agent-avatars/ken.png';
+ img.onerror=function(){this.style.display='none';this.nextElementSibling.style.display='flex';};
+ var fb=document.createElement('div');
+ fb.className='agent-avatar-fallback';
+ fb.textContent='K';
+ h.insertBefore(fb,h.firstChild);
+ h.insertBefore(img,h.firstChild);
+ }
+ var obs=new MutationObserver(function(){injectAvatar();});
+ obs.observe(document.body,{childList:true,subtree:true});
+ setTimeout(injectAvatar,1000);
+ setTimeout(injectAvatar,3000);
+ })();
+ </script>
</body>
</html>
diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index 2d123ad..7e76488 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -1200,6 +1200,82 @@ const routes = {
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
+ `);
+ json(res, {
+ signals,
+ probabilityShifts: 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);
+ }
+ },
};
// ════════════════════════════════════════
@@ -1302,8 +1378,10 @@ async function kenQ(text, params) { return kenPool.query(text, params); }
const SCAN_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
const REDDIT_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes for Reddit
+const SIGNAL_PIPELINE_MS = 5 * 60 * 1000; // 5 minutes — fast cycle for news → signal pipeline
const SLACK_COOLDOWN_MS = 4 * 60 * 60 * 1000; // Slack alert every 4 hours
let lastSlackTime = 0;
+let cachedActiveMarkets = []; // Shared market cache for fast pipeline cycle
// ── Reddit Scraper (no auth needed — uses .json API) ──
async function scrapeReddit(subreddit, searchTerm, limit = 25) {
@@ -2671,6 +2749,435 @@ async function redditIntelligenceLoop() {
}
}
+// ══════════════════════════════════════════════════════════════════════════════
+// 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'],
+};
+
+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: Find matching Kalshi markets by title/subtitle keywords
+ for (const market of activeMarkets) {
+ const marketText = `${market.title} ${market.subtitle || ''}`.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;
+ }
+
+ 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 and disagreement penalty
+function calculateProbabilityShift(priorProb, sentimentSignals) {
+ if (!sentimentSignals.length) return { adjustedProb: priorProb, impact: 0, numArticles: 0 };
+
+ const SENTIMENT_WEIGHT = 0.08; // How much sentiment moves probability (conservative)
+ const RECENCY_DECAY = 0.9; // Decay factor per hour
+ const AGREEMENT_BONUS = 1.3; // Bonus when multiple sources agree
+ const DISAGREEMENT_PENALTY = 0.5; // Penalty when sources disagree
+
+ 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));
+
+ for (const sig of sorted) {
+ const hoursAgo = Math.max(0, (now - (sig.timestamp || now)) / 3600000);
+ const recencyWeight = Math.pow(RECENCY_DECAY, hoursAgo);
+ const impact = sig.sentiment * SENTIMENT_WEIGHT * sig.confidence * recencyWeight;
+ totalImpact += impact;
+ totalWeight += recencyWeight * sig.confidence;
+ }
+
+ // 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.75) totalImpact *= AGREEMENT_BONUS; // Strong agreement
+ else if (agreementRatio < 0.6) totalImpact *= DISAGREEMENT_PENALTY; // Mixed signals
+ }
+
+ // 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,
+ };
+}
+
+// ── Risk Engine ──
+const RISK_LIMITS = {
+ maxExposurePerMarket: 5000, // cents ($50)
+ maxExposurePerTheme: 15000, // cents ($150)
+ maxTotalExposure: 50000, // cents ($500)
+ maxContractsPerMarket: 25,
+ minEdgeThreshold: 0.05, // 5% edge minimum to generate signal
+ 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
+ const signalAgeHours = (Date.now() - (signal.timestamp || Date.now())) / 3600000;
+ const decayedEdge = signal.expectedEdge - (RISK_LIMITS.timeDecayPenalty * signalAgeHours);
+ if (decayedEdge < RISK_LIMITS.minEdgeThreshold) {
+ approved = false;
+ reasons.push(`Edge decayed from ${(signal.expectedEdge * 100).toFixed(1)}% to ${(decayedEdge * 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 };
+}
+
+// ── 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 } = 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';
+ const confidence = Math.min(0.95,
+ (agreement || 0.5) * 0.3 + // Source agreement
+ Math.min(1, numArticles / 10) * 0.3 + // Volume of evidence
+ Math.min(1, Math.abs(edge) / 0.2) * 0.4 // Edge magnitude
+ );
+
+ const signal = {
+ marketId: ticker,
+ direction,
+ expectedEdge: edge,
+ confidence,
+ volume24h: market.volume_24h || 0,
+ timestamp: Date.now(),
+ };
+
+ // Step 7: Risk check
+ const risk = riskCheck(signal);
+
+ const reasoning = `${direction} on "${market.title}" — edge ${(edge * 100).toFixed(1)}% from ${numArticles} articles (${signals[0]?.category}/${signals[0]?.direction}). Agreement: ${((agreement || 0) * 100).toFixed(0)}%. ${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, edge, 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 ${(edge * 100).toFixed(1)}%, conf ${(confidence * 100).toFixed(0)}%, size ${risk.sizeRecommendation}`);
+ }
+ }
+ }
+
+ 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)
// ══════════════════════════════════════════════
@@ -3079,6 +3586,9 @@ async function autonomousScan() {
.filter(m => m.volume_24h > 50 && m.yes_bid > 0 && m.yes_ask > 0 && m.yes_ask < 100)
.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 = {};
try {
@@ -3385,6 +3895,21 @@ async function autonomousScan() {
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);
@@ -3597,9 +4122,10 @@ server.listen(PORT, '0.0.0.0', () => {
console.log(`[Ken] ═══════════════════════════════════════`);
console.log(`[Ken] AUTONOMOUS MODE: 24/7 MARKET INTELLIGENCE`);
console.log(`[Ken] Market scan: every 15 minutes`);
+ console.log(`[Ken] Signal pipeline: every 5 minutes (news→sentiment→probability→signals→risk)`);
console.log(`[Ken] Reddit/news: every 30 minutes`);
console.log(`[Ken] Slack alerts: every 4 hours (high-conf bypass)`);
- console.log(`[Ken] Data sources: Kalshi | Reddit (70+) | ${NEWS_FEEDS.length} news feeds | HN | GDELT TV | YouTube | Manifold | r/Kalshi | r/Polymarket | Lemmy | Lobsters | Tildes | Bluesky | Mastodon | Metaculus | Polymarket | Discord`);
+ console.log(`[Ken] Data sources: Kalshi | Reddit (70+) | ${NEWS_FEEDS.length} news feeds | HN | GDELT TV | YouTube | Manifold | r/Kalshi | r/Polymarket | Lemmy | Lobsters | Tildes | Bluesky | Mastodon | Metaculus | Polymarket | PredictIt | FRED Economic | NWS Alerts | GovTrack | Twitter/Nitter | Discord`);
console.log(`[Ken] ═══════════════════════════════════════`);
// Mutex: prevent concurrent sweeps from overlapping and doubling memory
@@ -3625,6 +4151,17 @@ server.listen(PORT, '0.0.0.0', () => {
// Reddit/news every 30 minutes (skips if market scan running)
setInterval(() => runSweepSafe('Reddit', redditIntelligenceLoop), REDDIT_INTERVAL_MS);
+ // Fast Signal Pipeline every 5 minutes — news → sentiment → probability → signals
+ 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));
← fd178e4 chore(alshi-dash): update 1 file (.js) [+256/-1]
·
back to Bertha
·
Ken agent: signal quality fix, live trading toggle, OOM fix, bf4915a →