[object Object]

← back to Bertha

Ken portFAUXlio: chart integration, sortable columns, SWOT trade intelligence

87be00a702c4d40c056eca0cee0758c84e1a522e · 2026-02-16 17:33:55 +0000 · DW Commit Agent

- Wired MiniChart sparklines into leaderboard rows with Chart.js
- Added FullChart modal (click portfolio row to open NAV chart)
- Fixed FullChart scope isolation (pass color/emoji/desc as props)
- Added sortable leaderboard columns (P&L, ROI, Invested, Wins, Trades, Strategy)
- New /api/portfauxlio/swot endpoint with per-market SWOT analysis
- SWOT parses reasoning for edge/MC/articles/agreement, generates S/W/O/T
- Expandable accordion cards with 2x2 SWOT grid, model tags, signal context
- Updated model count 10→50 in banner and active counter
- All 50 portfolios verified, 3 APIs confirmed, zero errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 87be00a702c4d40c056eca0cee0758c84e1a522e
Author: DW Commit Agent <claude@dw-agents.com>
Date:   Mon Feb 16 17:33:55 2026 +0000

    Ken portFAUXlio: chart integration, sortable columns, SWOT trade intelligence
    
    - Wired MiniChart sparklines into leaderboard rows with Chart.js
    - Added FullChart modal (click portfolio row to open NAV chart)
    - Fixed FullChart scope isolation (pass color/emoji/desc as props)
    - Added sortable leaderboard columns (P&L, ROI, Invested, Wins, Trades, Strategy)
    - New /api/portfauxlio/swot endpoint with per-market SWOT analysis
    - SWOT parses reasoning for edge/MC/articles/agreement, generates S/W/O/T
    - Expandable accordion cards with 2x2 SWOT grid, model tags, signal context
    - Updated model count 10→50 in banner and active counter
    - All 50 portfolios verified, 3 APIs confirmed, zero errors
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 kalshi-dash/server.js | 554 ++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 537 insertions(+), 17 deletions(-)

diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index 95e9332..c92b5b4 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -1492,9 +1492,16 @@ const routes = {
         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,
-        probabilityShifts: shifts,
+        signals: enrichWithUrl(signals),
+        probabilityShifts: enrichWithUrl(shifts),
         eventSentiment: recentSentiment,
         pipeline: {
           scanInterval: '15 min',
@@ -1667,6 +1674,164 @@ const routes = {
     } 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); }
+  },
+
   // ── PortFAUXlio Dashboard API — full simulated trading view ──
   'GET /api/portfauxlio': async (req, res) => {
     try {
@@ -2122,6 +2287,8 @@ const KEN_INC_HTML = `<!DOCTYPE html>
 <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}
@@ -2308,9 +2475,40 @@ body::after{content:'';position:fixed;top:0;left:0;right:0;bottom:0;background-i
 .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)}
-.pf-lb-header{display:grid;grid-template-columns:32px 100px 80px 80px 60px 50px 50px 60px 80px;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-row{display:grid;grid-template-columns:32px 100px 80px 80px 60px 50px 50px 60px 80px;gap:0;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.03);font-size:11px;transition:all .2s;cursor:default;align-items:center}
+.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:'\\u2022 '}
+.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)}
@@ -2451,29 +2649,68 @@ function SignalFeed({ signals }) {
 function PortfoliosTab() {
   const [data, setData] = useState(null);
   const [histFilter, setHistFilter] = useState('all');
+  const [chartData, setChartData] = useState({});
+  const [selectedChart, setSelectedChart] = useState(null);
+  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.portfolio_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'};
-  const stratEmojis = {aggressive:'\\uD83D\\uDD25',conservative:'\\uD83C\\uDFDB\\uFE0F',weather:'\\u26C8\\uFE0F',momentum:'\\uD83D\\uDE80',contrarian:'\\uD83D\\uDD04',ai_enhanced:'\\uD83E\\uDDE0',mc_validated:'\\uD83C\\uDFB2',scalper:'\\u26A1',patient:'\\uD83D\\uDC22',random:'\\uD83C\\uDFB0',penny_pincher:'\\uD83E\\uDE99',spray_pray:'\\uD83D\\uDCA6',nickel_slots:'\\uD83C\\uDFB0',degen_lite:'\\uD83D\\uDE08',politics_junkie:'\\uD83C\\uDFDB\\uFE0F',double_down:'\\u2B06\\uFE0F',volatility_rider:'\\uD83C\\uDFC4',whale:'\\uD83D\\uDC33',yolo:'\\uD83E\\uDDE8',full_send:'\\uD83D\\uDCA3',kelly_criterion:'\\uD83C\\uDFB0',sharpe_sniper:'\\uD83C\\uDFAF',bayesian_blend:'\\uD83E\\uDDEE',ensemble_lock:'\\uD83D\\uDD10',mean_revert:'\\u21A9\\uFE0F',info_ratio:'\\uD83D\\uDCCA',anti_fomo:'\\uD83E\\uDDD8',nightcrawler:'\\uD83E\\uDD87',heatseeker:'\\uD83D\\uDD2C',quant_core:'\\u2211'};
-  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'};
-  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'};
+  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:'\\uD83D\\uDD25',conservative:'\\uD83C\\uDFDB\\uFE0F',weather:'\\u26C8\\uFE0F',momentum:'\\uD83D\\uDE80',contrarian:'\\uD83D\\uDD04',ai_enhanced:'\\uD83E\\uDDE0',mc_validated:'\\uD83C\\uDFB2',scalper:'\\u26A1',patient:'\\uD83D\\uDC22',random:'\\uD83C\\uDFB0',penny_pincher:'\\uD83E\\uDE99',spray_pray:'\\uD83D\\uDCA6',nickel_slots:'\\uD83C\\uDFB0',degen_lite:'\\uD83D\\uDE08',politics_junkie:'\\uD83C\\uDFDB\\uFE0F',double_down:'\\u2B06\\uFE0F',volatility_rider:'\\uD83C\\uDFC4',whale:'\\uD83D\\uDC33',yolo:'\\uD83E\\uDDE8',full_send:'\\uD83D\\uDCA3',kelly_criterion:'\\uD83C\\uDFB0',sharpe_sniper:'\\uD83C\\uDFAF',bayesian_blend:'\\uD83E\\uDDEE',ensemble_lock:'\\uD83D\\uDD10',mean_revert:'\\u21A9\\uFE0F',info_ratio:'\\uD83D\\uDCCA',anti_fomo:'\\uD83E\\uDDD8',nightcrawler:'\\uD83E\\uDD87',heatseeker:'\\uD83D\\uDD2C',quant_core:'\\u2211',etf_presidential:'\\uD83C\\uDFDB\\uFE0F',etf_cabinet:'\\uD83D\\uDCBC',etf_congress:'\\uD83D\\uDDF3\\uFE0F',etf_scotus:'\\u2696\\uFE0F',etf_fed:'\\uD83C\\uDFE6',etf_economy:'\\uD83D\\uDCC8',etf_geopolitics:'\\uD83C\\uDF0D',etf_territory:'\\uD83D\\uDDFA\\uFE0F',etf_climate:'\\uD83C\\uDF21\\uFE0F',etf_energy:'\\u26FD',etf_tech:'\\uD83E\\uDD16',etf_space:'\\uD83D\\uDE80',etf_crypto:'\\u20BF',etf_sports:'\\u26BD',etf_entertainment:'\\uD83C\\uDFAC',etf_legal:'\\uD83D\\uDD28',etf_financials:'\\uD83D\\uDCB9',etf_govreform:'\\u2702\\uFE0F',etf_global_social:'\\u26EA',etf_broad_market:'\\uD83C\\uDF10'};
+  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, activeTrades, tradeHistory, overlaps, summary } = data;
-  const maxInvested = Math.max(...portfolios.map(p => parseInt(p.total_invested_cents) || 0), 1);
+  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">10 Strategy Models &bull; Paper Trading &bull; Real Signals &bull; Zero Real Money</div>
+      <div className="pf-banner-sub">50 Strategy Models &bull; Paper Trading &bull; Real Signals &bull; Zero Real Money</div>
     </div>
 
     {/* ═══ Summary Stats ═══ */}
@@ -2501,7 +2738,7 @@ function PortfoliosTab() {
         <div className="pf-summary-lbl">Markets</div>
       </div>
       <div className="pf-summary-card">
-        <div className="pf-summary-val" style={{color:'var(--yellow)'}}>{summary.activeModels}/10</div>
+        <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>
@@ -2545,7 +2782,16 @@ function PortfoliosTab() {
       </div>
       <div className="pf-leaderboard">
         <div className="pf-lb-header">
-          <span>#</span><span>Model</span><span>P&L</span><span>Invested</span><span>ROI</span><span>W</span><span>L</span><span>Trades</span><span>Strategy</span>
+          <span>#</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('name')}>Model {sortBy==='name' ? (sortDir==='asc'?'\\u25B2':'\\u25BC') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('pnl')}>P&L {sortBy==='pnl' ? (sortDir==='asc'?'\\u25B2':'\\u25BC') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('invested')}>Invested {sortBy==='invested' ? (sortDir==='asc'?'\\u25B2':'\\u25BC') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('roi')}>ROI {sortBy==='roi' ? (sortDir==='asc'?'\\u25B2':'\\u25BC') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('wins')}>W {sortBy==='wins' ? (sortDir==='asc'?'\\u25B2':'\\u25BC') : ''}</span>
+          <span>L</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('trades')}>Trades {sortBy==='trades' ? (sortDir==='asc'?'\\u25B2':'\\u25BC') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('strategy')}>Strategy {sortBy==='strategy' ? (sortDir==='asc'?'\\u25B2':'\\u25BC') : ''}</span>
+          <span>Chart</span>
         </div>
         {portfolios.map((p, i) => {
           const c = stratColors[p.strategy] || 'var(--muted)';
@@ -2556,7 +2802,9 @@ function PortfoliosTab() {
           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 = ['\\uD83E\\uDD47','\\uD83E\\uDD48','\\uD83E\\uDD49'];
-          return (<div key={p.id} className="pf-lb-row">
+          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" onClick={() => 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>
@@ -2579,6 +2827,9 @@ function PortfoliosTab() {
             <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>
@@ -2603,6 +2854,69 @@ function PortfoliosTab() {
       </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 &bull; {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'}}>\\u25BC</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}\\u00A2</strong></span>
+              {m.mc_mean > 0 && <span>MC: <strong>{m.mc_mean.toFixed(1)}% \\u00B1{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} &bull; {t.contracts}x &bull; {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">
@@ -2650,6 +2964,17 @@ function PortfoliosTab() {
       </div>
     </div>
 
+    {/* ═══ FullChart Modal ═══ */}
+    {selectedChart && chartData[selectedChart.id] && <FullChart
+      chartData={chartData[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)}
+    />}
+
     {/* ═══ 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>
@@ -2756,6 +3081,79 @@ function Sparkline({ data, width = 120, height = 30, color = '#8b5cf6', showDots
   );
 }
 
+// ── 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 ──
+function FullChart({ chartData, strategy, name, color, emoji, desc, onClose }) {
+  const canvasRef = React.useRef(null);
+  const chartRef = React.useRef(null);
+  const clr = color || '#8b5cf6';
+  React.useEffect(() => {
+    const snaps = chartData?.snapshots || [];
+    const daily = chartData?.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');
+    chartRef.current = new Chart(ctx, {
+      type: 'line',
+      data: { datasets: [{ label: 'NAV', data: dataPoints, borderColor: clr, borderWidth: 2, backgroundColor: gradient, fill: true, pointRadius: 2, pointHoverRadius: 6, pointBackgroundColor: clr, tension: 0.3 }] },
+      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(); };
+  }, [chartData, 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)'}}>{desc||''}</span>
+          <button className="chart-close" onClick={onClose}>x</button>
+        </div>
+        <div style={{height:320,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;
@@ -2844,8 +3242,8 @@ function MarketDataTab() {
   ];
   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'};
-  const stratEmojis = {aggressive:'\\uD83D\\uDD25',conservative:'\\uD83C\\uDFDB\\uFE0F',weather:'\\u26C8\\uFE0F',momentum:'\\uD83D\\uDE80',contrarian:'\\uD83D\\uDD04',ai_enhanced:'\\uD83E\\uDDE0',mc_validated:'\\uD83C\\uDFB2',scalper:'\\u26A1',patient:'\\uD83D\\uDC22',random:'\\uD83C\\uDFB0',penny_pincher:'\\uD83E\\uDE99',spray_pray:'\\uD83D\\uDCA6',nickel_slots:'\\uD83C\\uDFB0',degen_lite:'\\uD83D\\uDE08',politics_junkie:'\\uD83C\\uDFDB\\uFE0F',double_down:'\\u2B06\\uFE0F',volatility_rider:'\\uD83C\\uDFC4',whale:'\\uD83D\\uDC33',yolo:'\\uD83E\\uDDE8',full_send:'\\uD83D\\uDCA3',kelly_criterion:'\\uD83C\\uDFB0',sharpe_sniper:'\\uD83C\\uDFAF',bayesian_blend:'\\uD83E\\uDDEE',ensemble_lock:'\\uD83D\\uDD10',mean_revert:'\\u21A9\\uFE0F',info_ratio:'\\uD83D\\uDCCA',anti_fomo:'\\uD83E\\uDDD8',nightcrawler:'\\uD83E\\uDD87',heatseeker:'\\uD83D\\uDD2C',quant_core:'\\u2211'};
+  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:'\\uD83D\\uDD25',conservative:'\\uD83C\\uDFDB\\uFE0F',weather:'\\u26C8\\uFE0F',momentum:'\\uD83D\\uDE80',contrarian:'\\uD83D\\uDD04',ai_enhanced:'\\uD83E\\uDDE0',mc_validated:'\\uD83C\\uDFB2',scalper:'\\u26A1',patient:'\\uD83D\\uDC22',random:'\\uD83C\\uDFB0',penny_pincher:'\\uD83E\\uDE99',spray_pray:'\\uD83D\\uDCA6',nickel_slots:'\\uD83C\\uDFB0',degen_lite:'\\uD83D\\uDE08',politics_junkie:'\\uD83C\\uDFDB\\uFE0F',double_down:'\\u2B06\\uFE0F',volatility_rider:'\\uD83C\\uDFC4',whale:'\\uD83D\\uDC33',yolo:'\\uD83E\\uDDE8',full_send:'\\uD83D\\uDCA3',kelly_criterion:'\\uD83C\\uDFB0',sharpe_sniper:'\\uD83C\\uDFAF',bayesian_blend:'\\uD83E\\uDDEE',ensemble_lock:'\\uD83D\\uDD10',mean_revert:'\\u21A9\\uFE0F',info_ratio:'\\uD83D\\uDCCA',anti_fomo:'\\uD83E\\uDDD8',nightcrawler:'\\uD83E\\uDD87',heatseeker:'\\uD83D\\uDD2C',quant_core:'\\u2211',etf_presidential:'\\uD83C\\uDFDB\\uFE0F',etf_cabinet:'\\uD83D\\uDCBC',etf_congress:'\\uD83D\\uDDF3\\uFE0F',etf_scotus:'\\u2696\\uFE0F',etf_fed:'\\uD83C\\uDFE6',etf_economy:'\\uD83D\\uDCC8',etf_geopolitics:'\\uD83C\\uDF0D',etf_territory:'\\uD83D\\uDDFA\\uFE0F',etf_climate:'\\uD83C\\uDF21\\uFE0F',etf_energy:'\\u26FD',etf_tech:'\\uD83E\\uDD16',etf_space:'\\uD83D\\uDE80',etf_crypto:'\\u20BF',etf_sports:'\\u26BD',etf_entertainment:'\\uD83C\\uDFAC',etf_legal:'\\uD83D\\uDD28',etf_financials:'\\uD83D\\uDCB9',etf_govreform:'\\u2702\\uFE0F',etf_global_social:'\\u26EA',etf_broad_market:'\\uD83C\\uDF10'};
 
   return (<div>
     {/* ═══ Hero P&L Banner ═══ */}
@@ -6405,6 +6803,67 @@ const PORTFOLIO_STRATEGIES = {
     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),
 };
 
 // Position sizing per strategy — differentiated risk profiles
@@ -6443,6 +6902,28 @@ const PORTFOLIO_SIZING = {
   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))),
 };
 
 let tradeCounter = 0; // For wildcard portfolio
@@ -6480,6 +6961,7 @@ async function autoPortfolioTrade(signal) {
         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;
@@ -6521,6 +7003,40 @@ async function autoPortfolioTrade(signal) {
   }
 }
 
+// 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 — check if markets settled
 async function resolvePortfolioTrades() {
   try {
@@ -6976,6 +7492,10 @@ server.listen(PORT, '0.0.0.0', () => {
     resolvePortfolioTrades().catch(e => console.error('[Ken] Portfolio resolution error:', e.message));
   }, 60 * 60 * 1000);
 
+  // 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));

← 2875eb9 chore(alshi-dash): update 1 file (.js) [+118/-6]  ·  back to Bertha  ·  Ken portFAUXlio: period selector, comparison chart, snapshot 7134683 →