← 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 →