[object Object]

← 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

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 →