[object Object]

← back to Bertha

Ken portFAUXlio: period selector, comparison chart, snapshot cleanup

71346837606745d8fffad42fd9d0a32c25d9bbcd · 2026-02-16 17:38:03 +0000 · DW Commit Agent

- FullChart modal now has 1d/3d/7d/14d/30d period selector buttons
- Added dashed zero-line plugin for visual P&L reference
- New ComparisonChart component: overlay multiple portfolios on one chart
- Compare mode: click "Compare" button, select portfolios, view overlay
- Selected portfolios highlighted with purple border in compare mode
- Added 60-day retention cleanup for ken_portfolio_snapshots
- Leaderboard "ranked by" text now reflects actual sort column

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

Files touched

Diff

commit 71346837606745d8fffad42fd9d0a32c25d9bbcd
Author: DW Commit Agent <claude@dw-agents.com>
Date:   Mon Feb 16 17:38:03 2026 +0000

    Ken portFAUXlio: period selector, comparison chart, snapshot cleanup
    
    - FullChart modal now has 1d/3d/7d/14d/30d period selector buttons
    - Added dashed zero-line plugin for visual P&L reference
    - New ComparisonChart component: overlay multiple portfolios on one chart
    - Compare mode: click "Compare" button, select portfolios, view overlay
    - Selected portfolios highlighted with purple border in compare mode
    - Added 60-day retention cleanup for ken_portfolio_snapshots
    - Leaderboard "ranked by" text now reflects actual sort column
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 kalshi-dash/server.js | 269 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 257 insertions(+), 12 deletions(-)

diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index c92b5b4..40cf704 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -1832,6 +1832,119 @@ const routes = {
     } catch (e) { json(res, { error: e.message }, 500); }
   },
 
+  // ── Weekly Tournament System — survive or die ──
+  'GET /api/tournament/current': async (req, res) => {
+    try {
+      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
+      if (!tourney) return json(res, { active: false, message: 'No active tournament' });
+      const { rows: entries } = await kenQ(`
+        SELECT e.*, p.name, p.strategy, p.balance_cents, p.total_invested_cents, p.total_returned_cents,
+               p.trades_won, p.trades_lost, p.daily_budget_cents
+        FROM ken_tournament_entries e JOIN ken_portfolios p ON p.id = e.portfolio_id
+        WHERE e.tournament_id = $1 ORDER BY e.pnl_cents DESC NULLS LAST
+      `, [tourney.id]);
+      // Compute live P&L for each entry
+      for (const e of entries) {
+        const { rows: [stats] } = await kenQ(`
+          SELECT COUNT(*) as trades, COALESCE(SUM(cost_cents),0) as total_cost,
+                 COALESCE(SUM(CASE WHEN status='won' THEN payout_cents - cost_cents WHEN status='lost' THEN -cost_cents ELSE 0 END),0) as realized_pnl
+          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at >= $2
+        `, [e.portfolio_id, tourney.week_start]);
+        e.live_trades = parseInt(stats.trades) || 0;
+        e.live_invested = parseInt(stats.total_cost) || 0;
+        e.live_pnl = parseInt(stats.realized_pnl) || 0;
+      }
+      json(res, { active: true, tournament: tourney, entries });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  'POST /api/tournament/start': async (req, res) => {
+    try {
+      const body = await parseBody(req);
+      const portfolioIds = body.portfolio_ids || [];
+      const budgetCents = body.budget_cents || 2000; // $20 default
+      const notes = body.notes || '';
+      const weeks = body.weeks || 1;
+      if (portfolioIds.length < 1 || portfolioIds.length > 10) return json(res, { error: 'Pick 1-10 portfolios' }, 400);
+      // Check no active tournament
+      const { rows: active } = await kenQ(`SELECT id FROM ken_weekly_tournaments WHERE status = 'active'`);
+      if (active.length > 0) return json(res, { error: 'Tournament already active. Evaluate or cancel it first.', active_id: active[0].id }, 400);
+      const weekStart = new Date(); weekStart.setHours(0,0,0,0);
+      const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 7 * weeks);
+      const { rows: [tourney] } = await kenQ(`
+        INSERT INTO ken_weekly_tournaments (week_start, week_end, budget_per_portfolio_cents, notes)
+        VALUES ($1, $2, $3, $4) RETURNING *
+      `, [weekStart.toISOString().split('T')[0], weekEnd.toISOString().split('T')[0], budgetCents, notes]);
+      const entries = [];
+      for (const pid of portfolioIds) {
+        const { rows: [pf] } = await kenQ(`SELECT * FROM ken_portfolios WHERE id = $1`, [pid]);
+        if (!pf) continue;
+        // Set portfolio budget for the tournament
+        await kenQ(`UPDATE ken_portfolios SET daily_budget_cents = $1, balance_cents = $2 WHERE id = $3`, [Math.round(budgetCents / 7), budgetCents, pid]);
+        const { rows: [entry] } = await kenQ(`
+          INSERT INTO ken_tournament_entries (tournament_id, portfolio_id, start_balance_cents, status)
+          VALUES ($1, $2, $3, 'alive') RETURNING *
+        `, [tourney.id, pid, budgetCents]);
+        entries.push({ ...entry, name: pf.name, strategy: pf.strategy });
+      }
+      json(res, { tournament: tourney, entries, message: `Tournament started with ${entries.length} portfolios at $${(budgetCents/100).toFixed(2)} each` });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  'POST /api/tournament/evaluate': async (req, res) => {
+    try {
+      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
+      if (!tourney) return json(res, { error: 'No active tournament' }, 400);
+      const { rows: entries } = await kenQ(`
+        SELECT e.*, p.name, p.strategy FROM ken_tournament_entries e
+        JOIN ken_portfolios p ON p.id = e.portfolio_id WHERE e.tournament_id = $1
+      `, [tourney.id]);
+      const results = [];
+      for (const e of entries) {
+        const { rows: [stats] } = await kenQ(`
+          SELECT COUNT(*) as trades,
+                 COALESCE(SUM(CASE WHEN status='won' THEN payout_cents - cost_cents WHEN status='lost' THEN -cost_cents ELSE 0 END),0) as pnl
+          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at >= $2
+        `, [e.portfolio_id, tourney.week_start]);
+        const pnl = parseInt(stats.pnl) || 0;
+        const trades = parseInt(stats.trades) || 0;
+        const survived = pnl >= 0;
+        await kenQ(`UPDATE ken_tournament_entries SET end_balance_cents = $1, pnl_cents = $2, trades_count = $3, status = $4, elimination_reason = $5 WHERE id = $6`,
+          [e.start_balance_cents + pnl, pnl, trades, survived ? 'survived' : 'eliminated', survived ? null : `Lost $${(Math.abs(pnl)/100).toFixed(2)}`, e.id]);
+        if (!survived) await kenQ(`UPDATE ken_portfolios SET daily_budget_cents = 0 WHERE id = $1`, [e.portfolio_id]);
+        results.push({ name: e.name, strategy: e.strategy, pnl_cents: pnl, trades, status: survived ? 'SURVIVED' : 'ELIMINATED' });
+      }
+      await kenQ(`UPDATE ken_weekly_tournaments SET status = 'completed' WHERE id = $1`, [tourney.id]);
+      const survivors = results.filter(r => r.status === 'SURVIVED');
+      const eliminated = results.filter(r => r.status === 'ELIMINATED');
+      json(res, { tournament_id: tourney.id, results, survivors: survivors.length, eliminated: eliminated.length, message: `${survivors.length} survived, ${eliminated.length} eliminated` });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  'POST /api/tournament/cancel': async (req, res) => {
+    try {
+      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
+      if (!tourney) return json(res, { error: 'No active tournament' }, 400);
+      await kenQ(`UPDATE ken_weekly_tournaments SET status = 'cancelled' WHERE id = $1`, [tourney.id]);
+      await kenQ(`UPDATE ken_tournament_entries SET status = 'cancelled' WHERE tournament_id = $1`, [tourney.id]);
+      json(res, { message: 'Tournament cancelled', id: tourney.id });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  'GET /api/tournament/history': async (req, res) => {
+    try {
+      const { rows: tournaments } = await kenQ(`SELECT * FROM ken_weekly_tournaments ORDER BY id DESC LIMIT 20`);
+      for (const t of tournaments) {
+        const { rows: entries } = await kenQ(`
+          SELECT e.*, p.name, p.strategy FROM ken_tournament_entries e
+          JOIN ken_portfolios p ON p.id = e.portfolio_id WHERE e.tournament_id = $1 ORDER BY e.pnl_cents DESC NULLS LAST
+        `, [t.id]);
+        t.entries = entries;
+      }
+      json(res, tournaments);
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
   // ── PortFAUXlio Dashboard API — full simulated trading view ──
   'GET /api/portfauxlio': async (req, res) => {
     try {
@@ -2651,6 +2764,8 @@ function PortfoliosTab() {
   const [histFilter, setHistFilter] = useState('all');
   const [chartData, setChartData] = useState({});
   const [selectedChart, setSelectedChart] = useState(null);
+  const [compareMode, setCompareMode] = useState(false);
+  const [compareIds, setCompareIds] = useState([]);
   const [sortBy, setSortBy] = useState('pnl');
   const [sortDir, setSortDir] = useState('desc');
   useEffect(() => {
@@ -2778,7 +2893,15 @@ function PortfoliosTab() {
     <div style={{marginBottom:18}}>
       <div style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,marginBottom:10,color:'var(--pink)',display:'flex',alignItems:'center',gap:8}}>
         Model Leaderboard
-        <span style={{fontSize:9,fontWeight:400,color:'var(--muted)',textTransform:'none',letterSpacing:0}}>ranked by P&L</span>
+        <span style={{fontSize:9,fontWeight:400,color:'var(--muted)',textTransform:'none',letterSpacing:0}}>ranked by {sortBy}</span>
+        <div style={{marginLeft:'auto',display:'flex',gap:6}}>
+          {compareMode && compareIds.length >= 2 && <button onClick={() => setCompareMode('show')} style={{padding:'3px 10px',borderRadius:6,fontSize:9,fontWeight:700,background:'rgba(6,182,212,.15)',border:'1px solid var(--cyan)',color:'var(--cyan)',cursor:'pointer'}}>
+            Compare {compareIds.length} \\u2192
+          </button>}
+          <button onClick={() => { if (compareMode) { setCompareMode(false); setCompareIds([]); } else setCompareMode(true); }} style={{padding:'3px 10px',borderRadius:6,fontSize:9,fontWeight:700,background:compareMode?'rgba(168,85,247,.15)':'transparent',border:'1px solid '+(compareMode?'var(--purple)':'var(--border)'),color:compareMode?'var(--purple)':'var(--muted)',cursor:'pointer'}}>
+            {compareMode ? '\\u2715 Cancel' : '\\uD83D\\uDCCA Compare'}
+          </button>
+        </div>
       </div>
       <div className="pf-leaderboard">
         <div className="pf-lb-header">
@@ -2804,7 +2927,10 @@ function PortfoliosTab() {
           const medals = ['\\uD83E\\uDD47','\\uD83E\\uDD48','\\uD83E\\uDD49'];
           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})}>
+          return (<div key={p.id} className="pf-lb-row" style={compareMode && compareIds.includes(p.id) ? {background:'rgba(168,85,247,.08)',borderLeft:'3px solid var(--purple)'} : {}} onClick={() => {
+            if (compareMode) { setCompareIds(prev => prev.includes(p.id) ? prev.filter(x => x !== p.id) : [...prev, p.id]); }
+            else if (cd) setSelectedChart({id:p.id,strategy:p.strategy,name:p.name,color:c});
+          }}>
             <span className="pf-lb-rank">{i < 3 ? medals[i] : (i+1)}</span>
             <span className="pf-lb-name" style={{color:c}}>
               <span style={{fontSize:16}}>{stratEmojis[p.strategy]||''}</span>
@@ -2967,6 +3093,7 @@ function PortfoliosTab() {
     {/* ═══ FullChart Modal ═══ */}
     {selectedChart && chartData[selectedChart.id] && <FullChart
       chartData={chartData[selectedChart.id]}
+      portfolioId={selectedChart.id}
       strategy={selectedChart.strategy}
       name={selectedChart.name}
       color={selectedChart.color || stratColors[selectedChart.strategy] || '#8b5cf6'}
@@ -2975,6 +3102,16 @@ function PortfoliosTab() {
       onClose={() => setSelectedChart(null)}
     />}
 
+    {/* ═══ Comparison Chart Modal ═══ */}
+    {compareMode === 'show' && compareIds.length >= 2 && <ComparisonChart
+      portfolioIds={compareIds}
+      allChartData={chartData}
+      stratColors={stratColors}
+      stratNames={stratNames}
+      stratEmojis={stratEmojis}
+      onClose={() => { setCompareMode(false); setCompareIds([]); }}
+    />}
+
     {/* ═══ Strategy Guide ═══ */}
     <div style={{marginTop:18,padding:16,background:'var(--card)',border:'1px solid var(--border)',borderRadius:14}}>
       <div style={{fontSize:11,fontWeight:700,color:'var(--muted)',textTransform:'uppercase',letterSpacing:.5,marginBottom:10}}>Strategy Guide</div>
@@ -3102,14 +3239,27 @@ function MiniChart({ data, color, width = 140, height = 36 }) {
   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 }) {
+// ── FullChart: Modal chart for a single portfolio with period selector ──
+function FullChart({ chartData: initialData, portfolioId, strategy, name, color, emoji, desc, onClose }) {
   const canvasRef = React.useRef(null);
   const chartRef = React.useRef(null);
   const clr = color || '#8b5cf6';
+  const [period, setPeriod] = React.useState('7d');
+  const [liveData, setLiveData] = React.useState(initialData);
+  const periods = ['1d','3d','7d','14d','30d'];
+
+  React.useEffect(() => {
+    if (period === '7d') { setLiveData(initialData); return; }
+    api('/api/portfauxlio/charts?period=' + period).then(d => {
+      const match = (d.portfolios || []).find(p => p.id === portfolioId);
+      if (match) setLiveData(match);
+    }).catch(() => {});
+  }, [period, portfolioId]);
+
   React.useEffect(() => {
-    const snaps = chartData?.snapshots || [];
-    const daily = chartData?.daily || [];
+    const cd = liveData || initialData;
+    const snaps = cd?.snapshots || [];
+    const daily = cd?.daily || [];
     const dataPoints = snaps.length > 1 ? snaps.map(s => ({ x: new Date(s.time), y: s.nav / 100 }))
       : daily.length > 1 ? daily.map(d => ({ x: new Date(d.date), y: d.cumulative / 100 })) : [];
     if (!dataPoints.length || !canvasRef.current) return;
@@ -3118,9 +3268,26 @@ function FullChart({ chartData, strategy, name, color, emoji, desc, onClose }) {
     const gradient = ctx.createLinearGradient(0, 0, 0, 300);
     gradient.addColorStop(0, clr + '30');
     gradient.addColorStop(1, clr + '02');
+    const zeroLine = { id: 'zeroLine', beforeDraw(chart) {
+      const yScale = chart.scales.y;
+      if (!yScale) return;
+      const y0 = yScale.getPixelForValue(0);
+      if (y0 < yScale.top || y0 > yScale.bottom) return;
+      const cctx = chart.ctx;
+      cctx.save();
+      cctx.strokeStyle = 'rgba(255,255,255,0.15)';
+      cctx.lineWidth = 1;
+      cctx.setLineDash([4, 4]);
+      cctx.beginPath();
+      cctx.moveTo(chart.chartArea.left, y0);
+      cctx.lineTo(chart.chartArea.right, y0);
+      cctx.stroke();
+      cctx.restore();
+    }};
     chartRef.current = new Chart(ctx, {
       type: 'line',
-      data: { datasets: [{ label: 'NAV', data: dataPoints, borderColor: clr, borderWidth: 2, backgroundColor: gradient, fill: true, pointRadius: 2, pointHoverRadius: 6, pointBackgroundColor: clr, tension: 0.3 }] },
+      data: { datasets: [{ label: 'NAV', data: dataPoints, borderColor: clr, borderWidth: 2, backgroundColor: gradient, fill: true, pointRadius: dataPoints.length > 50 ? 0 : 2, pointHoverRadius: 6, pointBackgroundColor: clr, tension: 0.3 }] },
+      plugins: [zeroLine],
       options: {
         responsive: true, maintainAspectRatio: false,
         animation: { duration: 1200, easing: 'easeInOutQuart' },
@@ -3138,17 +3305,94 @@ function FullChart({ chartData, strategy, name, color, emoji, desc, onClose }) {
       }
     });
     return () => { if (chartRef.current) chartRef.current.destroy(); };
-  }, [chartData, strategy, clr]);
+  }, [liveData, strategy, clr]);
   return (
     <div className="chart-modal" onClick={onClose}>
       <div className="chart-modal-content" onClick={e => e.stopPropagation()}>
         <div className="chart-modal-header">
           <span style={{fontSize:18}}>{emoji||''}</span>
           <span style={{fontWeight:800,color:clr}}>{name || strategy}</span>
-          <span style={{fontSize:10,color:'var(--muted)'}}>{desc||''}</span>
-          <button className="chart-close" onClick={onClose}>x</button>
+          <span style={{fontSize:10,color:'var(--muted)',flex:1}}>{desc||''}</span>
+          <div className="chart-period-btns">
+            {periods.map(p => (
+              <button key={p} className={period === p ? 'chart-period-active' : 'chart-period'} onClick={() => setPeriod(p)}>{p}</button>
+            ))}
+          </div>
+          <button className="chart-close" onClick={onClose}>\\u2715</button>
+        </div>
+        <div style={{height:340,position:'relative'}}><canvas ref={canvasRef} /></div>
+      </div>
+    </div>
+  );
+}
+
+// ── ComparisonChart: Overlay multiple portfolios on one chart ──
+function ComparisonChart({ portfolioIds, allChartData, stratColors, stratNames, stratEmojis, onClose }) {
+  const canvasRef = React.useRef(null);
+  const chartRef = React.useRef(null);
+  const [period, setPeriod] = React.useState('7d');
+  const [compareData, setCompareData] = React.useState(allChartData);
+  const periods = ['1d','3d','7d','14d','30d'];
+
+  React.useEffect(() => {
+    if (period === '7d') { setCompareData(allChartData); return; }
+    api('/api/portfauxlio/charts?period=' + period).then(d => {
+      const byId = {};
+      (d.portfolios || []).forEach(p => { byId[p.id] = p; });
+      setCompareData(byId);
+    }).catch(() => {});
+  }, [period]);
+
+  React.useEffect(() => {
+    if (!canvasRef.current) return;
+    if (chartRef.current) chartRef.current.destroy();
+    const datasets = portfolioIds.map(id => {
+      const cd = compareData[id];
+      if (!cd) return null;
+      const strat = cd.strategy || '';
+      const clr = stratColors[strat] || '#8b5cf6';
+      const snaps = cd.snapshots || [];
+      const daily = cd.daily || [];
+      const pts = snaps.length > 1 ? snaps.map(s => ({ x: new Date(s.time), y: s.nav / 100 }))
+        : daily.length > 1 ? daily.map(d => ({ x: new Date(d.date), y: d.cumulative / 100 })) : [];
+      if (pts.length < 2) return null;
+      return { label: (stratEmojis[strat]||'') + ' ' + (cd.name || strat), data: pts, borderColor: clr, borderWidth: 2, backgroundColor: 'transparent', fill: false, pointRadius: 0, pointHoverRadius: 5, tension: 0.3 };
+    }).filter(Boolean);
+    if (!datasets.length) return;
+    const ctx = canvasRef.current.getContext('2d');
+    chartRef.current = new Chart(ctx, {
+      type: 'line', data: { datasets },
+      options: {
+        responsive: true, maintainAspectRatio: false,
+        animation: { duration: 1000, easing: 'easeOutQuart' },
+        interaction: { intersect: false, mode: 'index' },
+        plugins: {
+          legend: { display: true, position: 'bottom', labels: { color: '#94a3b8', font: { size: 10 }, boxWidth: 12, padding: 8 } },
+          tooltip: { backgroundColor: 'rgba(15,15,35,0.95)', borderWidth: 1, titleColor: '#e2e8f0', bodyColor: '#e2e8f0', padding: 12 }
+        },
+        scales: {
+          x: { type: 'time', ticks: { color: '#64748b', maxRotation: 45, font: { size: 9 } }, grid: { color: 'rgba(255,255,255,.03)' } },
+          y: { ticks: { color: '#64748b', callback: (v) => '$' + v.toFixed(0), font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }
+        }
+      }
+    });
+    return () => { if (chartRef.current) chartRef.current.destroy(); };
+  }, [compareData, portfolioIds]);
+
+  return (
+    <div className="chart-modal" onClick={onClose}>
+      <div className="chart-modal-content" onClick={e => e.stopPropagation()} style={{maxWidth:1100}}>
+        <div className="chart-modal-header">
+          <span style={{fontSize:16}}>\\uD83D\\uDCCA</span>
+          <span style={{fontWeight:800,color:'var(--cyan)'}}>Compare {portfolioIds.length} Portfolios</span>
+          <div className="chart-period-btns" style={{marginLeft:'auto'}}>
+            {periods.map(p => (
+              <button key={p} className={period === p ? 'chart-period-active' : 'chart-period'} onClick={() => setPeriod(p)}>{p}</button>
+            ))}
+          </div>
+          <button className="chart-close" onClick={onClose}>\\u2715</button>
         </div>
-        <div style={{height:320,position:'relative'}}><canvas ref={canvasRef} /></div>
+        <div style={{height:400,position:'relative'}}><canvas ref={canvasRef} /></div>
       </div>
     </div>
   );
@@ -7411,7 +7655,8 @@ async function cleanupOldData() {
     await kenQ(`DELETE FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '7 days'`);
     await kenQ(`DELETE FROM ken_sentiment WHERE captured_at < NOW() - INTERVAL '14 days'`);
     await kenQ(`DELETE FROM ken_performance WHERE scan_time < NOW() - INTERVAL '30 days'`);
-    console.log('[Ken] Old data cleaned up');
+    await kenQ(`DELETE FROM ken_portfolio_snapshots WHERE snapshot_at < NOW() - INTERVAL '60 days'`);
+    console.log('[Ken] Old data cleaned up (including portfolio snapshots 60d retention)');
   } catch (e) {
     console.error('[Ken] Cleanup error:', e.message);
   }

← 87be00a Ken portFAUXlio: chart integration, sortable columns, SWOT t  ·  back to Bertha  ·  chore(alshi-dash,kalshi-dash): update 7 files (.js) [+415/-5 d8e8eeb →