[object Object]

← back to Bertha

chore(alshi-dash): update 1 file (.js) [+361/-4]

1d9f769b2fb361c7246cfc5196f76da0e6fbcccc · 2026-02-16 07:17:30 +0000 · DW Commit Agent

Files touched

Diff

commit 1d9f769b2fb361c7246cfc5196f76da0e6fbcccc
Author: DW Commit Agent <commit-agent@dw-agents.com>
Date:   Mon Feb 16 07:17:30 2026 +0000

    chore(alshi-dash): update 1 file (.js) [+361/-4]
---
 kalshi-dash/server.js | 365 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 361 insertions(+), 4 deletions(-)

diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index 7458969..bf22f5b 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -1706,10 +1706,10 @@ const routes = {
     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, created_at
+               score, sentiment_score, raw_text, captured_at
         FROM ken_sentiment
         WHERE source IN ('heating_intel', 'noaa_alert', 'noaa_forecast')
-        ORDER BY created_at DESC
+        ORDER BY captured_at DESC
         LIMIT 50
       `);
       const { rows: stats } = await kenQ(`
@@ -1718,7 +1718,7 @@ const routes = {
                ROUND(AVG(score)::numeric, 0) as avg_score
         FROM ken_sentiment
         WHERE source IN ('heating_intel', 'noaa_alert', 'noaa_forecast')
-          AND created_at > NOW() - INTERVAL '7 days'
+          AND captured_at > NOW() - INTERVAL '7 days'
         GROUP BY source
       `);
       json(res, { posts, stats, keywords: HEATING_KEYWORDS });
@@ -1856,6 +1856,99 @@ const routes = {
       })),
     });
   },
+
+  // ── 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)
+    try {
+      const { rows } = await kenQ(`SELECT p.name, p.strategy, p.balance_cents, p.trades_won, p.trades_lost, p.win_rate,
+        p.total_invested_cents, p.daily_budget_cents, p.budget_remaining, p.trades_today, p.streak
+        FROM ken_portfolios p ORDER BY p.balance_cents DESC`);
+      out.portfolios = rows;
+    } 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) };
+
+    json(res, out);
+  },
 };
 
 // ════════════════════════════════════════
@@ -1958,6 +2051,40 @@ body::after{content:'';position:fixed;top:0;left:0;right:0;bottom:0;background-i
 .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 Panel */
+.mkt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;margin-top:20px}
+.mkt-card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;backdrop-filter:blur(10px);transition:all .3s}
+.mkt-card:hover{border-color:rgba(255,255,255,.1);transform:translateY(-2px)}
+.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:.8}
+.mkt-bar-label{position:absolute;top:-14px;left:50%;transform:translateX(-50%);font-size:8px;color:var(--muted);white-space:nowrap;display:none}
+.mkt-bar:hover .mkt-bar-label{display:block}
+.mkt-source-list{display:flex;flex-direction:column;gap:4px}
+.mkt-source-row{display:flex;align-items:center;gap:8px;font-size:11px}
+.mkt-source-bar{height:6px;border-radius:3px;transition:width .5s}
+.mkt-source-name{min-width:80px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.mkt-source-val{font-variant-numeric:tabular-nums;color:var(--muted);min-width:40px;text-align:right}
+.mkt-gauge{position:relative;width:100%;height:12px;background:rgba(255,255,255,.05);border-radius:6px;overflow:hidden;margin-top:8px}
+.mkt-gauge-fill{height:100%;border-radius:6px;transition:width .5s}
+.mkt-gauge-label{position:absolute;top:50%;right:8px;transform:translateY(-50%);font-size:9px;font-weight:700;color:var(--text)}
+.mkt-ticker-strip{display:flex;gap:20px;overflow:hidden;padding:10px 0;border-top:1px solid var(--border);border-bottom:1px solid var(--border);margin:16px 0}
+.mkt-ticker-track{display:flex;gap:20px;animation:tickerScroll 30s 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:6px;font-size:12px;font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums}
+.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)}
+
 /* Footer */
 .footer{text-align:center;padding:30px;color:var(--muted);font-size:11px;letter-spacing:1px}
 .footer a{color:var(--pink);text-decoration:none}
@@ -2182,6 +2309,234 @@ function PredictionsTab() {
   </div>);
 }
 
+// ── Sparkline SVG Component ──
+function Sparkline({ data, width = 120, height = 30, color = '#8b5cf6', showDots = false }) {
+  if (!data || data.length < 2) return null;
+  const max = Math.max(...data);
+  const min = Math.min(...data);
+  const range = max - min || 1;
+  const points = data.map((v, i) => {
+    const x = (i / (data.length - 1)) * width;
+    const y = height - ((v - min) / range) * (height - 4) - 2;
+    return x + ',' + y;
+  }).join(' ');
+  const areaPoints = points + ' ' + width + ',' + height + ' 0,' + height;
+  return (
+    <svg className="sparkline-svg" width={width} height={height} viewBox={'0 0 ' + width + ' ' + height}>
+      <defs><linearGradient id={'grad-' + color.replace('#','')} x1="0" y1="0" x2="0" y2="1">
+        <stop offset="0%" stopColor={color} stopOpacity="0.3"/><stop offset="100%" stopColor={color} stopOpacity="0.02"/>
+      </linearGradient></defs>
+      <polygon points={areaPoints} fill={'url(#grad-' + color.replace('#','') + ')'} />
+      <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 - 4) - 2;
+        return <circle key={i} cx={x} cy={y} r="2" fill={color} opacity={i === data.length - 1 ? 1 : 0.4} />;
+      })}
+    </svg>
+  );
+}
+
+// ── Horizontal 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>
+  );
+}
+
+// ── Market Data Tab ──
+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, 10000);
+    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)'}}>Loading market data...</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 ? '\\u25B2' : at.change < 0 ? '\\u25BC' : '—';
+  const changeClass = at.change > 0 ? 'up' : at.change < 0 ? 'down' : 'flat';
+
+  // Ticker items — key metrics with change indicators
+  const tickerItems = [
+    { label: 'SIGNALS/24H', 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: edgeVals.length > 0 ? (edgeVals.reduce((a,b)=>a+b,0) / edgeVals.length).toFixed(2) + '%' : '—', color: 'var(--purple)' },
+    { label: 'MARKETS', value: mkt.activeMarkets, color: 'var(--cyan)' },
+    { label: 'MOMENTUM', value: mkt.momentumKeys, color: 'var(--yellow)' },
+    { label: 'GEMINI', value: mkt.gemini.pctUsed + '%', color: 'var(--teal)' },
+    { label: 'SOURCES', value: (mkt.sourceMix || []).length, color: 'var(--blue)' },
+  ];
+  const doubledTicker = tickerItems.concat(tickerItems);
+
+  return (<div>
+    {/* Ticker Strip */}
+    <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:10}}>{t.label}</span>
+            <span style={{color:t.color}}>{t.value}</span>
+            {t.change !== undefined && <span className={'mkt-change ' + (t.change > 0 ? 'up' : t.change < 0 ? 'down' : 'flat')}>
+              {t.change > 0 ? '\\u25B2' : t.change < 0 ? '\\u25BC' : ''}{Math.abs(t.change)}%
+            </span>}
+          </div>
+        ))}
+      </div>
+    </div>
+
+    <div className="mkt-grid">
+      {/* Signal Volume Sparkline */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--blue)'}}>Signal Volume (24h)</span>
+          <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{totalSignals} signals</span>
+        </div>
+        <Sparkline data={signalVals} width={300} height={50} color="#3b82f6" showDots={true} />
+        <BarChart data={(mkt.signalTrend || []).map((s, i) => ({
+          value: s.signals, label: new Date(s.hour).toLocaleTimeString('en-US', {hour:'numeric',hour12:true}),
+          color: s.approved > 0 ? 'rgba(34,197,94,.6)' : 'rgba(59,130,246,.4)',
+        }))} height={50} />
+        <div style={{display:'flex',gap:12,marginTop:8,fontSize:10,color:'var(--muted)'}}>
+          <span>\\u2588 Green = Approved</span><span>\\u2588 Blue = Total</span>
+        </div>
+      </div>
+
+      {/* Sentiment Volume */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--pink)'}}>Sentiment Flow (24h)</span>
+          <span className="mkt-card-badge" style={{background:'rgba(236,72,153,.15)',color:'var(--pink)'}}>{totalSent24h.toLocaleString()} articles</span>
+        </div>
+        <Sparkline data={sentVals} width={300} height={50} color="#ec4899" />
+        <div style={{display:'flex',gap:8,marginTop:8,fontSize:10,color:'var(--muted)'}}>
+          <span>Avg Sentiment: <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>
+        </div>
+      </div>
+
+      {/* Approval Rate Trend */}
+      <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 6h</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.current}%</div>
+        </div>
+        <div style={{display:'flex',justifyContent:'space-between',fontSize:10,color:'var(--muted)',marginTop:4}}>
+          <span>Recent: {at.recentSignals} signals</span><span>Prior: {at.priorSignals} signals</span>
+        </div>
+      </div>
+
+      {/* Edge Distribution */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--purple)'}}>Edge Trend (24h)</span>
+          <span className="mkt-card-badge" style={{background:'rgba(139,92,246,.15)',color:'var(--purple)'}}>
+            Avg: {edgeVals.length > 0 ? (edgeVals.reduce((a,b)=>a+b,0)/edgeVals.length).toFixed(2) : 0}%
+          </span>
+        </div>
+        <Sparkline data={edgeVals} width={300} height={50} color="#8b5cf6" showDots={true} />
+      </div>
+
+      {/* Source Mix */}
+      <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} active</span>
+        </div>
+        <div className="mkt-source-list">
+          {(mkt.sourceMix || []).slice(0, 10).map((s, i) => {
+            const maxC = (mkt.sourceMix || [])[0]?.count || 1;
+            const colors = ['#3b82f6','#8b5cf6','#ec4899','#ef4444','#f59e0b','#22c55e','#06b6d4','#14b8a6','#6366f1','#d946ef'];
+            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 AI Usage */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--teal)'}}>Gemini AI Budget</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}% used
+          </span>
+        </div>
+        <div style={{display:'flex',alignItems:'baseline',gap:8,marginBottom:8}}>
+          <span style={{fontSize:36,fontWeight:800,color:'var(--teal)',fontVariantNumeric:'tabular-nums'}}>{mkt.gemini.callsToday}</span>
+          <span style={{fontSize:12,color:'var(--muted)'}}>/ {mkt.gemini.dailyLimit} daily limit</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.callsToday}/{mkt.gemini.dailyLimit}</div>
+        </div>
+      </div>
+
+      {/* Category Distribution */}
+      <div className="mkt-card" style={{gridColumn:'span 2'}}>
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Signal Categories (24h)</span>
+        </div>
+        <div style={{display:'flex',flexWrap:'wrap',gap:6}}>
+          {(mkt.categories || []).map((c, i) => {
+            const maxC = (mkt.categories || [])[0]?.count || 1;
+            const pct = (c.count / maxC * 100);
+            const colors = ['#f59e0b','#3b82f6','#ec4899','#22c55e','#8b5cf6','#ef4444','#06b6d4','#14b8a6','#6366f1','#d946ef','#f97316','#64748b'];
+            return (<div key={i} style={{
+              padding:'6px 14px',borderRadius:8,background:'var(--card)',border:'1px solid var(--border)',
+              fontSize:11,display:'flex',alignItems:'center',gap:6,minWidth:120,transition:'all .2s',
+            }}>
+              <div style={{width:6,height:6,borderRadius:'50%',background:colors[i % colors.length],flexShrink:0}} />
+              <span style={{fontWeight:600}}>{c.category}</span>
+              <span style={{color:'var(--muted)',marginLeft:'auto',fontVariantNumeric:'tabular-nums'}}>{c.count.toLocaleString()}</span>
+            </div>);
+          })}
+        </div>
+      </div>
+    </div>
+  </div>);
+}
+
 function App() {
   const [data, setData] = useState(null);
   const [expandedAgent, setExpandedAgent] = useState(null);
@@ -2233,6 +2588,7 @@ function App() {
 
       <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')}>Portfolios (10)</button>
         <button style={tabStyle('predictions')} onClick={()=>setTab('predictions')}>Predictions</button>
         <button style={tabStyle('signals')} onClick={()=>setTab('signals')}>Live Signals</button>
@@ -2257,6 +2613,7 @@ function App() {
         </div>
       </>}
 
+      {tab === 'market' && <MarketDataTab />}
       {tab === 'portfolios' && <PortfoliosTab />}
       {tab === 'predictions' && <PredictionsTab />}
 
@@ -4104,7 +4461,7 @@ const RISK_LIMITS = {
   maxExposurePerTheme: 15000, // cents ($150)
   maxTotalExposure: 50000, // cents ($500)
   maxContractsPerMarket: 25,
-  minEdgeThreshold: 0.05, // 5% edge minimum to generate signal
+  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

← 198a6d0 chore(alshi-dash): update 1 file (.js) [+1097/-2]  ·  back to Bertha  ·  Ken: market data refinements and portfolio query fixes df73f40 →