← 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 • Paper Trading • Real Signals • Zero Real Money</div>
+ <div className="pf-banner-sub">50 Strategy Models • Paper Trading • Real Signals • 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 • {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} • {t.contracts}x • {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 →