← back to Bertha
chore(alshi-dash): update 1 file (.js) [+378/-128]
65ed6d812c2fad4f4b6ee1d14fa865d41d38c730 · 2026-02-16 14:47:31 +0000 · DW Commit Agent
Files touched
Diff
commit 65ed6d812c2fad4f4b6ee1d14fa865d41d38c730
Author: DW Commit Agent <commit-agent@dw-agents.com>
Date: Mon Feb 16 14:47:31 2026 +0000
chore(alshi-dash): update 1 file (.js) [+378/-128]
---
kalshi-dash/server.js | 506 +++++++++++++++++++++++++++++++++++++-------------
1 file changed, 378 insertions(+), 128 deletions(-)
diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index da5a926..73910e7 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -1914,15 +1914,25 @@ const routes = {
out.categories = rows.map(r => ({ category: r.category, count: parseInt(r.cnt) }));
} catch { out.categories = []; }
- // 5. Portfolio P&L (current state)
+ // 5. Portfolio P&L (current state) — compute actual PnL from invested vs returned
try {
- const { rows } = await kenQ(`SELECT p.name, p.strategy, p.balance_cents, p.trades_won, p.trades_lost,
- p.total_invested_cents, p.daily_budget_cents, p.streak,
+ const { rows } = await kenQ(`SELECT p.id, p.name, p.strategy, p.trades_won, p.trades_lost, p.trades_expired,
+ p.total_invested_cents, p.total_returned_cents, p.daily_budget_cents, p.streak,
+ p.best_trade_cents, p.worst_trade_cents, p.last_trade_at,
+ (p.total_returned_cents - p.total_invested_cents) AS pnl_cents,
CASE WHEN (p.trades_won + p.trades_lost) > 0
THEN ROUND(p.trades_won * 100.0 / (p.trades_won + p.trades_lost))
- ELSE 0 END AS win_rate
- FROM ken_portfolios p ORDER BY p.balance_cents DESC`);
- out.portfolios = rows.map(r => ({ ...r, balance_cents: parseInt(r.balance_cents), win_rate: parseInt(r.win_rate) }));
+ ELSE 0 END AS win_rate,
+ (SELECT COUNT(*) FROM ken_portfolio_trades t WHERE t.portfolio_id = p.id) AS total_trades
+ FROM ken_portfolios p ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC`);
+ out.portfolios = rows.map(r => ({
+ ...r,
+ pnl_cents: parseInt(r.pnl_cents) || 0,
+ win_rate: parseInt(r.win_rate) || 0,
+ total_trades: parseInt(r.total_trades) || 0,
+ total_invested_cents: parseInt(r.total_invested_cents) || 0,
+ total_returned_cents: parseInt(r.total_returned_cents) || 0,
+ }));
} catch { out.portfolios = []; }
// 6. Active markets count and top movers
@@ -1950,6 +1960,65 @@ const routes = {
// 8. Gemini usage
out.gemini = { callsToday: geminiCallCount, dailyLimit: GEMINI_DAILY_LIMIT, pctUsed: Math.round(geminiCallCount / GEMINI_DAILY_LIMIT * 100) };
+ // 9. Latest signals feed — most recent 15 signals for live ticker
+ try {
+ const { rows } = await kenQ(`SELECT market_id, direction, expected_edge, confidence, risk_approved, created_at,
+ SUBSTRING(reasoning FROM 1 FOR 80) AS reason_short
+ FROM ken_strategy_signals ORDER BY created_at DESC LIMIT 15`);
+ out.latestSignals = rows.map(r => ({
+ market: r.market_id, direction: r.direction,
+ edge: parseFloat(parseFloat(r.expected_edge || 0).toFixed(4)),
+ confidence: parseFloat(parseFloat(r.confidence || 0).toFixed(3)),
+ approved: r.risk_approved, reason: r.reason_short,
+ time: r.created_at,
+ }));
+ } catch { out.latestSignals = []; }
+
+ // 10. Per-market signal stats — heatmap data
+ try {
+ const { rows } = await kenQ(`SELECT market_id,
+ COUNT(*) AS total_signals,
+ AVG(expected_edge::numeric) AS avg_edge,
+ AVG(confidence::numeric) AS avg_conf,
+ COUNT(*) FILTER (WHERE risk_approved) AS approved,
+ MAX(created_at) AS last_signal
+ FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'
+ GROUP BY market_id ORDER BY total_signals DESC LIMIT 20`);
+ out.marketStats = rows.map(r => ({
+ market: r.market_id, signals: parseInt(r.total_signals),
+ avgEdge: parseFloat(parseFloat(r.avg_edge || 0).toFixed(4)),
+ avgConf: parseFloat(parseFloat(r.avg_conf || 0).toFixed(3)),
+ approved: parseInt(r.approved), lastSignal: r.last_signal,
+ }));
+ } catch { out.marketStats = []; }
+
+ // 11. Recent trades with details
+ try {
+ const { rows } = await kenQ(`SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction, t.contracts,
+ t.entry_price_cents, t.pnl_cents, t.status, t.cost_cents, t.confidence, t.created_at,
+ p.name AS portfolio_name, p.strategy
+ FROM ken_portfolio_trades t
+ JOIN ken_portfolios p ON p.id = t.portfolio_id
+ ORDER BY t.created_at DESC LIMIT 20`);
+ out.recentTrades = rows.map(r => ({
+ ...r, entry_price_cents: parseInt(r.entry_price_cents) || 0, pnl_cents: parseInt(r.pnl_cents) || 0,
+ contracts: parseInt(r.contracts) || 0, cost_cents: parseInt(r.cost_cents) || 0,
+ }));
+ } catch { out.recentTrades = []; }
+
+ // 12. Confidence distribution histogram (10 buckets from 0-1)
+ try {
+ const { rows } = await kenQ(`SELECT
+ WIDTH_BUCKET(confidence::numeric, 0, 1, 10) AS bucket,
+ COUNT(*) AS cnt
+ FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'
+ GROUP BY bucket ORDER BY bucket`);
+ out.confDistribution = Array.from({length: 10}, (_, i) => {
+ const found = rows.find(r => parseInt(r.bucket) === i + 1);
+ return { range: (i * 10) + '-' + ((i + 1) * 10) + '%', count: found ? parseInt(found.cnt) : 0 };
+ });
+ } catch { out.confDistribution = []; }
+
json(res, out);
},
};
@@ -2058,35 +2127,77 @@ body::after{content:'';position:fixed;top:0;left:0;right:0;bottom:0;background-i
.sparkline-wrap{display:inline-block;vertical-align:middle}
.sparkline-svg{display:block}
-/* Market Data Panel */
-.mkt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;margin-top:20px}
-.mkt-card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;backdrop-filter:blur(10px);transition:all .3s}
-.mkt-card:hover{border-color:rgba(255,255,255,.1);transform:translateY(-2px)}
+/* ─── Market Data Terminal ─── */
+.mkt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;margin-top:16px}
+.mkt-card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;backdrop-filter:blur(10px);transition:all .3s;position:relative;overflow:hidden}
+.mkt-card::after{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--border),transparent);opacity:0;transition:opacity .3s}
+.mkt-card:hover{border-color:rgba(255,255,255,.12);transform:translateY(-2px);box-shadow:0 12px 40px rgba(0,0,0,.3)}
+.mkt-card:hover::after{opacity:1}
.mkt-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.mkt-card-title{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
.mkt-card-badge{padding:3px 8px;border-radius:6px;font-size:10px;font-weight:700;font-variant-numeric:tabular-nums}
.mkt-bar-chart{display:flex;align-items:flex-end;gap:3px;height:80px;padding-top:4px}
.mkt-bar{flex:1;border-radius:3px 3px 0 0;min-width:4px;transition:height .5s ease;position:relative}
-.mkt-bar:hover{opacity:.8}
-.mkt-bar-label{position:absolute;top:-14px;left:50%;transform:translateX(-50%);font-size:8px;color:var(--muted);white-space:nowrap;display:none}
+.mkt-bar:hover{opacity:1!important;filter:brightness(1.3)}
+.mkt-bar-label{position:absolute;top:-16px;left:50%;transform:translateX(-50%);font-size:8px;color:var(--muted);white-space:nowrap;display:none;background:var(--card);padding:1px 4px;border-radius:3px;border:1px solid var(--border)}
.mkt-bar:hover .mkt-bar-label{display:block}
-.mkt-source-list{display:flex;flex-direction:column;gap:4px}
-.mkt-source-row{display:flex;align-items:center;gap:8px;font-size:11px}
-.mkt-source-bar{height:6px;border-radius:3px;transition:width .5s}
-.mkt-source-name{min-width:80px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-.mkt-source-val{font-variant-numeric:tabular-nums;color:var(--muted);min-width:40px;text-align:right}
-.mkt-gauge{position:relative;width:100%;height:12px;background:rgba(255,255,255,.05);border-radius:6px;overflow:hidden;margin-top:8px}
-.mkt-gauge-fill{height:100%;border-radius:6px;transition:width .5s}
-.mkt-gauge-label{position:absolute;top:50%;right:8px;transform:translateY(-50%);font-size:9px;font-weight:700;color:var(--text)}
-.mkt-ticker-strip{display:flex;gap:20px;overflow:hidden;padding:10px 0;border-top:1px solid var(--border);border-bottom:1px solid var(--border);margin:16px 0}
-.mkt-ticker-track{display:flex;gap:20px;animation:tickerScroll 30s linear infinite;width:max-content}
+.mkt-source-list{display:flex;flex-direction:column;gap:5px}
+.mkt-source-row{display:flex;align-items:center;gap:8px;font-size:11px;padding:2px 0;transition:background .2s;border-radius:4px}
+.mkt-source-row:hover{background:rgba(255,255,255,.03)}
+.mkt-source-bar{height:7px;border-radius:4px;transition:width .6s cubic-bezier(.4,0,.2,1)}
+.mkt-source-name{min-width:85px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.mkt-source-val{font-variant-numeric:tabular-nums;color:var(--muted);min-width:45px;text-align:right;font-weight:600}
+.mkt-gauge{position:relative;width:100%;height:14px;background:rgba(255,255,255,.05);border-radius:7px;overflow:hidden;margin-top:8px}
+.mkt-gauge-fill{height:100%;border-radius:7px;transition:width .6s cubic-bezier(.4,0,.2,1)}
+.mkt-gauge-label{position:absolute;top:50%;right:8px;transform:translateY(-50%);font-size:9px;font-weight:700;color:var(--text);text-shadow:0 1px 2px rgba(0,0,0,.5)}
+/* Scrolling ticker */
+.mkt-ticker-strip{overflow:hidden;padding:12px 0;border-top:1px solid rgba(255,255,255,.06);border-bottom:1px solid rgba(255,255,255,.06);margin:16px 0;background:linear-gradient(90deg,rgba(5,5,16,.8),transparent 5%,transparent 95%,rgba(5,5,16,.8))}
+.mkt-ticker-track{display:flex;gap:24px;animation:tickerScroll 40s linear infinite;width:max-content}
.mkt-ticker-track:hover{animation-play-state:paused}
@keyframes tickerScroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
-.mkt-ticker-item{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums}
+.mkt-ticker-item{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums;padding:4px 12px;border-radius:6px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.04)}
.mkt-change{font-size:11px;font-weight:700}
.mkt-change.up{color:var(--green)}
.mkt-change.down{color:var(--red)}
.mkt-change.flat{color:var(--muted)}
+/* Signal feed */
+.signal-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto}
+.signal-feed::-webkit-scrollbar{width:4px}
+.signal-feed::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
+.signal-row{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;font-size:11px;transition:all .2s;border:1px solid transparent;animation:signalIn .3s ease}
+.signal-row:hover{background:rgba(255,255,255,.04);border-color:var(--border)}
+@keyframes signalIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
+.signal-dir{padding:2px 8px;border-radius:4px;font-weight:800;font-size:10px;text-transform:uppercase;letter-spacing:.5px}
+.signal-dir.yes{background:rgba(34,197,94,.15);color:var(--green)}
+.signal-dir.no{background:rgba(239,68,68,.15);color:var(--red)}
+.signal-market{font-weight:600;min-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.signal-edge{font-variant-numeric:tabular-nums;font-weight:700;min-width:50px;text-align:right}
+.signal-conf{font-variant-numeric:tabular-nums;min-width:40px;text-align:right;color:var(--muted)}
+.signal-time{font-size:9px;color:var(--muted);margin-left:auto;white-space:nowrap}
+.signal-approved{width:6px;height:6px;border-radius:50%;flex-shrink:0}
+/* Heatmap grid */
+.heatmap-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px}
+.heatmap-cell{padding:8px 10px;border-radius:8px;border:1px solid var(--border);transition:all .2s;cursor:default;text-align:center}
+.heatmap-cell:hover{transform:scale(1.03);z-index:1;box-shadow:0 4px 16px rgba(0,0,0,.4)}
+.heatmap-market{font-size:10px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:2px}
+.heatmap-edge{font-size:16px;font-weight:800;font-variant-numeric:tabular-nums}
+.heatmap-meta{font-size:8px;color:var(--muted);margin-top:2px}
+/* Histogram */
+.histogram{display:flex;align-items:flex-end;gap:2px;height:80px}
+.hist-bar{flex:1;border-radius:3px 3px 0 0;transition:height .5s;position:relative;min-width:0}
+.hist-bar:hover{filter:brightness(1.3)}
+.hist-label{position:absolute;bottom:-16px;left:50%;transform:translateX(-50%);font-size:7px;color:var(--muted);white-space:nowrap}
+/* Trade log */
+.trade-log{display:flex;flex-direction:column;gap:3px;max-height:260px;overflow-y:auto}
+.trade-log::-webkit-scrollbar{width:4px}
+.trade-log::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
+.trade-row{display:flex;align-items:center;gap:6px;padding:5px 10px;border-radius:6px;font-size:10px;border:1px solid transparent;transition:all .2s}
+.trade-row:hover{background:rgba(255,255,255,.03);border-color:var(--border)}
+.trade-portfolio{font-weight:600;min-width:70px}
+.trade-pnl{font-weight:800;font-variant-numeric:tabular-nums;min-width:55px;text-align:right}
+/* P&L big number */
+.pnl-hero{font-size:48px;font-weight:900;font-variant-numeric:tabular-nums;letter-spacing:-1px;line-height:1}
+@keyframes pnlPulse{0%,100%{text-shadow:0 0 8px currentColor}50%{text-shadow:0 0 20px currentColor}}
/* Footer */
.footer{text-align:center;padding:30px;color:var(--muted);font-size:11px;letter-spacing:1px}
@@ -2313,34 +2424,43 @@ function PredictionsTab() {
}
// ── Sparkline SVG Component ──
-function Sparkline({ data, width = 120, height = 30, color = '#8b5cf6', showDots = false }) {
+function Sparkline({ data, width = 120, height = 30, color = '#8b5cf6', showDots = false, showArea = true, label }) {
if (!data || data.length < 2) return null;
const max = Math.max(...data);
const min = Math.min(...data);
const range = max - min || 1;
+ const gradId = 'grad-' + color.replace('#','') + '-' + width;
const points = data.map((v, i) => {
const x = (i / (data.length - 1)) * width;
- const y = height - ((v - min) / range) * (height - 4) - 2;
+ const y = height - ((v - min) / range) * (height - 6) - 3;
return x + ',' + y;
}).join(' ');
const areaPoints = points + ' ' + width + ',' + height + ' 0,' + height;
+ const lastVal = data[data.length - 1];
+ const prevVal = data[data.length - 2];
+ const trending = lastVal >= prevVal;
return (
- <svg className="sparkline-svg" width={width} height={height} viewBox={'0 0 ' + width + ' ' + height}>
- <defs><linearGradient id={'grad-' + color.replace('#','')} x1="0" y1="0" x2="0" y2="1">
- <stop offset="0%" stopColor={color} stopOpacity="0.3"/><stop offset="100%" stopColor={color} stopOpacity="0.02"/>
- </linearGradient></defs>
- <polygon points={areaPoints} fill={'url(#grad-' + color.replace('#','') + ')'} />
- <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
- {showDots && data.map((v, i) => {
- const x = (i / (data.length - 1)) * width;
- const y = height - ((v - min) / range) * (height - 4) - 2;
- return <circle key={i} cx={x} cy={y} r="2" fill={color} opacity={i === data.length - 1 ? 1 : 0.4} />;
- })}
- </svg>
+ <div style={{position:'relative'}}>
+ <svg className="sparkline-svg" width={width} height={height} viewBox={'0 0 ' + width + ' ' + height}>
+ <defs><linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0%" stopColor={color} stopOpacity="0.25"/><stop offset="100%" stopColor={color} stopOpacity="0.01"/>
+ </linearGradient></defs>
+ {showArea && <polygon points={areaPoints} fill={'url(#' + gradId + ')'} />}
+ <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
+ {showDots && data.map((v, i) => {
+ const x = (i / (data.length - 1)) * width;
+ const y = height - ((v - min) / range) * (height - 6) - 3;
+ return <circle key={i} cx={x} cy={y} r={i === data.length - 1 ? 3 : 1.5} fill={i === data.length - 1 ? color : 'transparent'} stroke={color} strokeWidth={i === data.length - 1 ? 0 : 0.5} opacity={i === data.length - 1 ? 1 : 0.5} />;
+ })}
+ </svg>
+ {label && <div style={{position:'absolute',top:2,right:4,fontSize:9,color:trending?'var(--green)':'var(--red)',fontWeight:700}}>
+ {trending?'\\u25B2':'\\u25BC'} {lastVal.toLocaleString()}
+ </div>}
+ </div>
);
}
-// ── Horizontal Bar Chart ──
+// ── Vertical Bar Chart ──
function BarChart({ data, maxVal, height = 80, color = '#8b5cf6' }) {
if (!data || data.length === 0) return null;
const mx = maxVal || Math.max(...data.map(d => d.value)) || 1;
@@ -2355,7 +2475,37 @@ function BarChart({ data, maxVal, height = 80, color = '#8b5cf6' }) {
);
}
-// ── Market Data Tab ──
+// ── Confidence Histogram ──
+function ConfHistogram({ data }) {
+ if (!data || data.length === 0) return null;
+ const max = Math.max(...data.map(d => d.count)) || 1;
+ const colors = ['#ef4444','#f97316','#f59e0b','#eab308','#84cc16','#22c55e','#14b8a6','#06b6d4','#3b82f6','#8b5cf6'];
+ return (
+ <div className="histogram">
+ {data.map((d, i) => (
+ <div key={i} className="hist-bar" style={{
+ height: Math.max(2, (d.count / max) * 70) + 'px',
+ background: colors[i] || 'var(--muted)',
+ opacity: d.count > 0 ? 0.8 : 0.2,
+ }}>
+ <div className="hist-label">{d.range}</div>
+ </div>
+ ))}
+ </div>
+ );
+}
+
+// ── Time ago helper ──
+function timeAgo(ts) {
+ if (!ts) return '';
+ const diff = Date.now() - new Date(ts).getTime();
+ if (diff < 60000) return Math.floor(diff / 1000) + 's ago';
+ if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
+ if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
+ return Math.floor(diff / 86400000) + 'd ago';
+}
+
+// ── Market Data Tab — Bloomberg-style terminal ──
function MarketDataTab() {
const [mkt, setMkt] = useState(null);
const [tick, setTick] = useState(0);
@@ -2363,12 +2513,14 @@ function MarketDataTab() {
useEffect(() => {
const load = () => api('/api/inc/market-data').then(setMkt).catch(e => console.error(e));
load();
- const iv = setInterval(load, 10000);
+ const iv = setInterval(load, 8000);
return () => clearInterval(iv);
}, []);
useEffect(() => { const iv = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(iv); }, []);
- if (!mkt) return <div style={{textAlign:'center',padding:60,color:'var(--muted)'}}>Loading market data...</div>;
+ if (!mkt) return <div style={{textAlign:'center',padding:60,color:'var(--muted)'}}>
+ <div style={{fontSize:24,marginBottom:8,animation:'avatarFloat 2s ease-in-out infinite'}}>Loading market data...</div>
+ </div>;
const signalVals = (mkt.signalTrend || []).map(s => s.signals);
const edgeVals = (mkt.signalTrend || []).map(s => s.avgEdge * 100);
@@ -2377,30 +2529,50 @@ function MarketDataTab() {
const totalSent24h = sentVals.reduce((a, b) => a + b, 0);
const totalSignals = signalVals.reduce((a, b) => a + b, 0);
const at = mkt.approvalTrend || {};
- const changeArrow = at.change > 0 ? '\\u25B2' : at.change < 0 ? '\\u25BC' : '—';
+ const changeArrow = at.change > 0 ? '\\u25B2' : at.change < 0 ? '\\u25BC' : '\\u2014';
const changeClass = at.change > 0 ? 'up' : at.change < 0 ? 'down' : 'flat';
+ const totalPnl = (mkt.portfolios || []).reduce((s, p) => s + (p.pnl_cents || 0), 0);
+ const totalTrades = (mkt.portfolios || []).reduce((s, p) => s + (p.total_trades || 0), 0);
+ const avgEdge = edgeVals.length > 0 ? (edgeVals.reduce((a,b)=>a+b,0) / edgeVals.length) : 0;
- // Ticker items — key metrics with change indicators
+ // Ticker items
const tickerItems = [
- { label: 'SIGNALS/24H', value: totalSignals, color: 'var(--blue)' },
+ { label: 'P&L', value: (totalPnl >= 0 ? '+' : '') + (totalPnl / 100).toFixed(2), color: totalPnl >= 0 ? 'var(--green)' : 'var(--red)' },
+ { label: 'SIGNALS', value: totalSignals, color: 'var(--blue)' },
{ label: 'APPROVAL', value: at.current + '%', change: at.change, color: 'var(--green)' },
{ label: 'SENTIMENT', value: totalSent24h.toLocaleString(), color: 'var(--pink)' },
- { label: 'AVG EDGE', value: edgeVals.length > 0 ? (edgeVals.reduce((a,b)=>a+b,0) / edgeVals.length).toFixed(2) + '%' : '—', color: 'var(--purple)' },
+ { label: 'AVG EDGE', value: avgEdge.toFixed(1) + '%', color: 'var(--purple)' },
+ { label: 'TRADES', value: totalTrades, color: 'var(--yellow)' },
{ label: 'MARKETS', value: mkt.activeMarkets, color: 'var(--cyan)' },
- { label: 'MOMENTUM', value: mkt.momentumKeys, color: 'var(--yellow)' },
- { label: 'GEMINI', value: mkt.gemini.pctUsed + '%', color: 'var(--teal)' },
- { label: 'SOURCES', value: (mkt.sourceMix || []).length, color: 'var(--blue)' },
+ { label: 'GEMINI', value: mkt.gemini.callsToday + '/' + mkt.gemini.dailyLimit, color: 'var(--teal)' },
];
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'};
+ 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'};
+
return (<div>
- {/* Ticker Strip */}
+ {/* ═══ Hero P&L Banner ═══ */}
+ <div style={{textAlign:'center',padding:'20px 0 8px',borderBottom:'1px solid var(--border)'}}>
+ <div style={{fontSize:10,color:'var(--muted)',textTransform:'uppercase',letterSpacing:2,marginBottom:4}}>Total Portfolio P&L</div>
+ <div className="pnl-hero" style={{color: totalPnl >= 0 ? 'var(--green)' : 'var(--red)', animation: totalPnl !== 0 ? 'pnlPulse 3s ease-in-out infinite' : 'none'}}>
+ {totalPnl >= 0 ? '+' : ''}{(totalPnl / 100).toFixed(2)}
+ </div>
+ <div style={{display:'flex',justifyContent:'center',gap:24,marginTop:8,fontSize:11,color:'var(--muted)'}}>
+ <span>{totalTrades} trades</span>
+ <span>{(mkt.portfolios||[]).filter(p => p.pnl_cents > 0).length} winning</span>
+ <span>{(mkt.portfolios||[]).filter(p => p.pnl_cents < 0).length} losing</span>
+ <span>{avgEdge.toFixed(1)}% avg edge</span>
+ </div>
+ </div>
+
+ {/* ═══ Scrolling Ticker ═══ */}
<div className="mkt-ticker-strip">
<div className="mkt-ticker-track">
{doubledTicker.map((t, i) => (
<div key={i} className="mkt-ticker-item">
- <span style={{color:'var(--muted)',fontSize:10}}>{t.label}</span>
- <span style={{color:t.color}}>{t.value}</span>
+ <span style={{color:'var(--muted)',fontSize:9,textTransform:'uppercase',letterSpacing:.5}}>{t.label}</span>
+ <span style={{color:t.color,fontWeight:800}}>{t.value}</span>
{t.change !== undefined && <span className={'mkt-change ' + (t.change > 0 ? 'up' : t.change < 0 ? 'down' : 'flat')}>
{t.change > 0 ? '\\u25B2' : t.change < 0 ? '\\u25BC' : ''}{Math.abs(t.change)}%
</span>}
@@ -2410,37 +2582,62 @@ function MarketDataTab() {
</div>
<div className="mkt-grid">
- {/* Signal Volume Sparkline */}
- <div className="mkt-card">
+ {/* ═══ Live Signal Feed ═══ */}
+ <div className="mkt-card" style={{gridColumn:'span 2'}}>
<div className="mkt-card-header">
- <span className="mkt-card-title" style={{color:'var(--blue)'}}>Signal Volume (24h)</span>
- <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{totalSignals} signals</span>
+ <span className="mkt-card-title" style={{color:'var(--blue)'}}>
+ <span style={{display:'inline-block',width:6,height:6,borderRadius:'50%',background:'var(--green)',marginRight:6,animation:'statusPulse 2s ease infinite'}}></span>
+ Live Signal Feed
+ </span>
+ <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{(mkt.latestSignals||[]).length} latest</span>
</div>
- <Sparkline data={signalVals} width={300} height={50} color="#3b82f6" showDots={true} />
- <BarChart data={(mkt.signalTrend || []).map((s, i) => ({
- value: s.signals, label: new Date(s.hour).toLocaleTimeString('en-US', {hour:'numeric',hour12:true}),
- color: s.approved > 0 ? 'rgba(34,197,94,.6)' : 'rgba(59,130,246,.4)',
- }))} height={50} />
- <div style={{display:'flex',gap:12,marginTop:8,fontSize:10,color:'var(--muted)'}}>
- <span>\\u2588 Green = Approved</span><span>\\u2588 Blue = Total</span>
+ <div className="signal-feed">
+ {(mkt.latestSignals || []).map((sig, i) => (
+ <div key={i} className="signal-row">
+ <div className="signal-approved" style={{background: sig.approved ? 'var(--green)' : 'var(--red)'}} title={sig.approved ? 'Approved' : 'Rejected'} />
+ <span className={'signal-dir ' + sig.direction}>{sig.direction}</span>
+ <span className="signal-market" title={sig.market}>{sig.market}</span>
+ <span className="signal-edge" style={{color: sig.edge > 0 ? 'var(--green)' : 'var(--red)'}}>
+ {sig.edge > 0 ? '+' : ''}{(sig.edge * 100).toFixed(1)}%
+ </span>
+ <span className="signal-conf" title="Confidence">{(sig.confidence * 100).toFixed(0)}%</span>
+ <span className="signal-time">{timeAgo(sig.time)}</span>
+ </div>
+ ))}
+ {(!mkt.latestSignals || mkt.latestSignals.length === 0) && <div style={{padding:20,textAlign:'center',color:'var(--muted)',fontSize:12}}>No recent signals</div>}
</div>
</div>
- {/* Sentiment Volume */}
+ {/* ═══ Signal Volume Sparkline ═══ */}
<div className="mkt-card">
<div className="mkt-card-header">
- <span className="mkt-card-title" style={{color:'var(--pink)'}}>Sentiment Flow (24h)</span>
- <span className="mkt-card-badge" style={{background:'rgba(236,72,153,.15)',color:'var(--pink)'}}>{totalSent24h.toLocaleString()} articles</span>
+ <span className="mkt-card-title" style={{color:'var(--blue)'}}>Signal Volume</span>
+ <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{totalSignals}</span>
</div>
- <Sparkline data={sentVals} width={300} height={50} color="#ec4899" />
- <div style={{display:'flex',gap:8,marginTop:8,fontSize:10,color:'var(--muted)'}}>
- <span>Avg Sentiment: <strong style={{color: sentScoreVals.length > 0 && sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length > 0 ? 'var(--green)' : 'var(--red)'}}>
- {sentScoreVals.length > 0 ? (sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length).toFixed(3) : '—'}
+ <Sparkline data={signalVals} width={300} height={50} color="#3b82f6" showDots={true} label={true} />
+ <BarChart data={(mkt.signalTrend || []).map((s) => ({
+ value: s.signals,
+ label: new Date(s.hour).toLocaleTimeString('en-US', {hour:'numeric',hour12:true}),
+ color: s.approved > s.signals * 0.5 ? 'rgba(34,197,94,.6)' : 'rgba(239,68,68,.4)',
+ }))} height={45} />
+ </div>
+
+ {/* ═══ Sentiment Flow ═══ */}
+ <div className="mkt-card">
+ <div className="mkt-card-header">
+ <span className="mkt-card-title" style={{color:'var(--pink)'}}>Sentiment Flow</span>
+ <span className="mkt-card-badge" style={{background:'rgba(236,72,153,.15)',color:'var(--pink)'}}>{totalSent24h.toLocaleString()}</span>
+ </div>
+ <Sparkline data={sentVals} width={300} height={50} color="#ec4899" label={true} />
+ <div style={{display:'flex',justifyContent:'space-between',marginTop:8,fontSize:10,color:'var(--muted)'}}>
+ <span>Avg: <strong style={{color: sentScoreVals.length > 0 && sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length > 0 ? 'var(--green)' : 'var(--red)'}}>
+ {sentScoreVals.length > 0 ? (sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length).toFixed(3) : '\\u2014'}
</strong></span>
+ <span>{(mkt.sourceMix||[]).length} sources</span>
</div>
</div>
- {/* Approval Rate Trend */}
+ {/* ═══ Approval Rate ═══ */}
<div className="mkt-card">
<div className="mkt-card-header">
<span className="mkt-card-title" style={{color:'var(--green)'}}>Approval Rate</span>
@@ -2448,38 +2645,71 @@ function MarketDataTab() {
</div>
<div style={{display:'flex',alignItems:'baseline',gap:12,marginBottom:8}}>
<span style={{fontSize:42,fontWeight:800,color:'var(--green)',fontVariantNumeric:'tabular-nums'}}>{at.current}%</span>
- <span style={{fontSize:12,color:'var(--muted)'}}>vs {at.prior}% prior 6h</span>
+ <span style={{fontSize:12,color:'var(--muted)'}}>vs {at.prior}% prior</span>
</div>
<div className="mkt-gauge">
<div className="mkt-gauge-fill" style={{width: at.current + '%', background:'linear-gradient(90deg,var(--green),var(--teal))'}} />
- <div className="mkt-gauge-label">{at.current}%</div>
- </div>
- <div style={{display:'flex',justifyContent:'space-between',fontSize:10,color:'var(--muted)',marginTop:4}}>
- <span>Recent: {at.recentSignals} signals</span><span>Prior: {at.priorSignals} signals</span>
+ <div className="mkt-gauge-label">{at.recentSignals} signals</div>
</div>
</div>
- {/* Edge Distribution */}
+ {/* ═══ Edge Trend ═══ */}
<div className="mkt-card">
<div className="mkt-card-header">
- <span className="mkt-card-title" style={{color:'var(--purple)'}}>Edge Trend (24h)</span>
+ <span className="mkt-card-title" style={{color:'var(--purple)'}}>Edge Trend</span>
<span className="mkt-card-badge" style={{background:'rgba(139,92,246,.15)',color:'var(--purple)'}}>
- Avg: {edgeVals.length > 0 ? (edgeVals.reduce((a,b)=>a+b,0)/edgeVals.length).toFixed(2) : 0}%
+ {avgEdge.toFixed(1)}% avg
</span>
</div>
- <Sparkline data={edgeVals} width={300} height={50} color="#8b5cf6" showDots={true} />
+ <Sparkline data={edgeVals} width={300} height={50} color="#8b5cf6" showDots={true} label={true} />
</div>
- {/* Source Mix */}
+ {/* ═══ Market Heatmap ═══ */}
+ {(mkt.marketStats || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
+ <div className="mkt-card-header">
+ <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Market Heatmap (24h)</span>
+ <span className="mkt-card-badge" style={{background:'rgba(245,158,11,.15)',color:'var(--yellow)'}}>{(mkt.marketStats||[]).length} markets</span>
+ </div>
+ <div className="heatmap-grid">
+ {(mkt.marketStats || []).map((m, i) => {
+ const edge = m.avgEdge * 100;
+ const intensity = Math.min(1, Math.abs(edge) / 25);
+ const bg = edge >= 0
+ ? 'rgba(34,197,94,' + (0.08 + intensity * 0.2) + ')'
+ : 'rgba(239,68,68,' + (0.08 + intensity * 0.2) + ')';
+ const borderC = edge >= 0 ? 'rgba(34,197,94,' + (0.2 + intensity * 0.3) + ')' : 'rgba(239,68,68,' + (0.2 + intensity * 0.3) + ')';
+ return (<div key={i} className="heatmap-cell" style={{background: bg, borderColor: borderC}}>
+ <div className="heatmap-market">{m.market}</div>
+ <div className="heatmap-edge" style={{color: edge >= 0 ? 'var(--green)' : 'var(--red)'}}>
+ {edge >= 0 ? '+' : ''}{edge.toFixed(1)}%
+ </div>
+ <div className="heatmap-meta">{m.signals} sig | {m.approved} appr | {(m.avgConf * 100).toFixed(0)}% conf</div>
+ </div>);
+ })}
+ </div>
+ </div>}
+
+ {/* ═══ Confidence Distribution ═══ */}
+ {(mkt.confDistribution || []).length > 0 && <div className="mkt-card">
+ <div className="mkt-card-header">
+ <span className="mkt-card-title" style={{color:'var(--cyan)'}}>Confidence Distribution</span>
+ </div>
+ <ConfHistogram data={mkt.confDistribution} />
+ <div style={{marginTop:18,display:'flex',justifyContent:'space-between',fontSize:9,color:'var(--muted)'}}>
+ <span>Low conf</span><span>High conf</span>
+ </div>
+ </div>}
+
+ {/* ═══ Intelligence Sources ═══ */}
<div className="mkt-card">
<div className="mkt-card-header">
<span className="mkt-card-title" style={{color:'var(--cyan)'}}>Intelligence Sources</span>
- <span className="mkt-card-badge" style={{background:'rgba(6,182,212,.15)',color:'var(--cyan)'}}>{(mkt.sourceMix||[]).length} active</span>
+ <span className="mkt-card-badge" style={{background:'rgba(6,182,212,.15)',color:'var(--cyan)'}}>{(mkt.sourceMix||[]).length}</span>
</div>
<div className="mkt-source-list">
- {(mkt.sourceMix || []).slice(0, 10).map((s, i) => {
+ {(mkt.sourceMix || []).slice(0, 12).map((s, i) => {
const maxC = (mkt.sourceMix || [])[0]?.count || 1;
- const colors = ['#3b82f6','#8b5cf6','#ec4899','#ef4444','#f59e0b','#22c55e','#06b6d4','#14b8a6','#6366f1','#d946ef'];
+ const colors = ['#3b82f6','#8b5cf6','#ec4899','#ef4444','#f59e0b','#22c55e','#06b6d4','#14b8a6','#6366f1','#d946ef','#f97316','#a855f7'];
return (<div key={i} className="mkt-source-row">
<span className="mkt-source-name">{s.source}</span>
<div style={{flex:1,display:'flex',alignItems:'center',gap:4}}>
@@ -2494,79 +2724,99 @@ function MarketDataTab() {
</div>
</div>
- {/* Gemini AI Usage */}
+ {/* ═══ Gemini + Categories row ═══ */}
<div className="mkt-card">
<div className="mkt-card-header">
- <span className="mkt-card-title" style={{color:'var(--teal)'}}>Gemini AI Budget</span>
+ <span className="mkt-card-title" style={{color:'var(--teal)'}}>Gemini AI</span>
<span className="mkt-card-badge" style={{background: mkt.gemini.pctUsed > 80 ? 'rgba(239,68,68,.15)' : 'rgba(20,184,166,.15)', color: mkt.gemini.pctUsed > 80 ? 'var(--red)' : 'var(--teal)'}}>
- {mkt.gemini.pctUsed}% used
+ {mkt.gemini.pctUsed}%
</span>
</div>
<div style={{display:'flex',alignItems:'baseline',gap:8,marginBottom:8}}>
- <span style={{fontSize:36,fontWeight:800,color:'var(--teal)',fontVariantNumeric:'tabular-nums'}}>{mkt.gemini.callsToday}</span>
- <span style={{fontSize:12,color:'var(--muted)'}}>/ {mkt.gemini.dailyLimit} daily limit</span>
+ <span style={{fontSize:32,fontWeight:800,color:'var(--teal)',fontVariantNumeric:'tabular-nums'}}>{mkt.gemini.callsToday}</span>
+ <span style={{fontSize:11,color:'var(--muted)'}}>/ {mkt.gemini.dailyLimit}</span>
</div>
<div className="mkt-gauge">
- <div className="mkt-gauge-fill" style={{
- width: mkt.gemini.pctUsed + '%',
- background: mkt.gemini.pctUsed > 80 ? 'linear-gradient(90deg,var(--yellow),var(--red))' : 'linear-gradient(90deg,var(--teal),var(--cyan))'
- }} />
- <div className="mkt-gauge-label">{mkt.gemini.callsToday}/{mkt.gemini.dailyLimit}</div>
+ <div className="mkt-gauge-fill" style={{width: mkt.gemini.pctUsed + '%', background: mkt.gemini.pctUsed > 80 ? 'linear-gradient(90deg,var(--yellow),var(--red))' : 'linear-gradient(90deg,var(--teal),var(--cyan))'}} />
+ <div className="mkt-gauge-label">{mkt.gemini.pctUsed}%</div>
+ </div>
+ {/* Categories inline */}
+ <div style={{marginTop:14,display:'flex',flexWrap:'wrap',gap:4}}>
+ {(mkt.categories || []).slice(0, 8).map((c, i) => {
+ const colors = ['#f59e0b','#3b82f6','#ec4899','#22c55e','#8b5cf6','#ef4444','#06b6d4','#14b8a6'];
+ return (<div key={i} style={{padding:'3px 10px',borderRadius:6,background:'rgba(255,255,255,.03)',border:'1px solid var(--border)',fontSize:9,display:'flex',alignItems:'center',gap:4}}>
+ <div style={{width:4,height:4,borderRadius:'50%',background:colors[i % colors.length]}} />
+ <span style={{fontWeight:600}}>{c.category}</span>
+ <span style={{color:'var(--muted)'}}>{c.count}</span>
+ </div>);
+ })}
</div>
</div>
- {/* Category Distribution */}
- <div className="mkt-card" style={{gridColumn:'span 2'}}>
+ {/* ═══ Recent Trades ═══ */}
+ {(mkt.recentTrades || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
<div className="mkt-card-header">
- <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Signal Categories (24h)</span>
+ <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Recent Trades</span>
+ <span className="mkt-card-badge" style={{background:'rgba(245,158,11,.15)',color:'var(--yellow)'}}>{totalTrades} total</span>
</div>
- <div style={{display:'flex',flexWrap:'wrap',gap:6}}>
- {(mkt.categories || []).map((c, i) => {
- const maxC = (mkt.categories || [])[0]?.count || 1;
- const pct = (c.count / maxC * 100);
- const colors = ['#f59e0b','#3b82f6','#ec4899','#22c55e','#8b5cf6','#ef4444','#06b6d4','#14b8a6','#6366f1','#d946ef','#f97316','#64748b'];
- return (<div key={i} style={{
- padding:'6px 14px',borderRadius:8,background:'var(--card)',border:'1px solid var(--border)',
- fontSize:11,display:'flex',alignItems:'center',gap:6,minWidth:120,transition:'all .2s',
- }}>
- <div style={{width:6,height:6,borderRadius:'50%',background:colors[i % colors.length],flexShrink:0}} />
- <span style={{fontWeight:600}}>{c.category}</span>
- <span style={{color:'var(--muted)',marginLeft:'auto',fontVariantNumeric:'tabular-nums'}}>{c.count.toLocaleString()}</span>
+ <div className="trade-log">
+ {(mkt.recentTrades || []).map((t, i) => {
+ const c = stratColors[t.strategy] || 'var(--muted)';
+ return (<div key={i} className="trade-row">
+ <span className="trade-portfolio" style={{color:c}}>{t.portfolio_name}</span>
+ <span className={'signal-dir ' + t.direction} style={{fontSize:9}}>{t.direction}</span>
+ <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',color:'var(--muted)'}}>{t.market_id}</span>
+ <span style={{fontSize:10,color:'var(--muted)'}}>{t.contracts}x @ {t.entry_price_cents}\\u00A2</span>
+ <span className="trade-pnl" style={{color: t.pnl_cents >= 0 ? 'var(--green)' : 'var(--red)'}}>
+ {t.pnl_cents >= 0 ? '+' : ''}{(t.pnl_cents / 100).toFixed(2)}
+ </span>
+ <span className="signal-time">{timeAgo(t.created_at)}</span>
</div>);
})}
</div>
- </div>
+ </div>}
- {/* Portfolio Leaderboard */}
+ {/* ═══ Portfolio Leaderboard ═══ */}
{(mkt.portfolios || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
<div className="mkt-card-header">
<span className="mkt-card-title" style={{color:'var(--pink)'}}>Portfolio Leaderboard</span>
- <span className="mkt-card-badge" style={{background:'rgba(255,105,180,.15)',color:'var(--pink)'}}>{(mkt.portfolios||[]).length} portfolios</span>
+ <span className="mkt-card-badge" style={{background:'rgba(255,105,180,.15)',color:'var(--pink)'}}>{totalTrades} trades</span>
</div>
- <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(200px,1fr))',gap:8}}>
+ <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(220px,1fr))',gap:8}}>
{(mkt.portfolios || []).map((p, i) => {
- const stratColors = {aggressive:'#ef4444',conservative:'#3b82f6',weather:'#06b6d4',momentum:'#f59e0b',contrarian:'#8b5cf6',ai_enhanced:'#ec4899',mc_validated:'#a855f7',scalper:'#eab308',patient:'#22c55e',random:'#64748b'};
- const stratEmojis = {aggressive:'🔥',conservative:'🏛️',weather:'⛈️',momentum:'🚀',contrarian:'🔄',ai_enhanced:'🧠',mc_validated:'🎲',scalper:'⚡',patient:'🐢',random:'🎰'};
const c = stratColors[p.strategy] || 'var(--muted)';
- const pnl = parseInt(p.balance_cents) || 0;
- return (<div key={i} style={{padding:'10px 14px',background:'rgba(255,255,255,.02)',border:'1px solid var(--border)',borderRadius:10,transition:'all .2s',borderLeftColor:c,borderLeftWidth:3}}>
- <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:4}}>
- <span style={{fontSize:18}}>{stratEmojis[p.strategy]||'📊'}</span>
- <div>
- <div style={{fontSize:12,fontWeight:700,color:c}}>{p.name}</div>
- <div style={{fontSize:9,color:'var(--muted)',textTransform:'uppercase'}}>{p.strategy}</div>
+ const emoji = stratEmojis[p.strategy] || '';
+ const pnl = p.pnl_cents || 0;
+ const invested = p.total_invested_cents || 0;
+ const roi = invested > 0 ? ((pnl / invested) * 100).toFixed(1) : '0.0';
+ return (<div key={i} style={{padding:'12px 14px',background:'rgba(255,255,255,.02)',border:'1px solid var(--border)',borderRadius:10,transition:'all .3s',borderLeftColor:c,borderLeftWidth:3,position:'relative',overflow:'hidden'}}>
+ {/* Rank badge */}
+ <div style={{position:'absolute',top:6,right:8,fontSize:8,color:'var(--muted)',fontWeight:800}}>#{i+1}</div>
+ <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:6}}>
+ <span style={{fontSize:20}}>{emoji}</span>
+ <div style={{flex:1}}>
+ <div style={{fontSize:13,fontWeight:700,color:c}}>{p.name}</div>
+ <div style={{fontSize:9,color:'var(--muted)',textTransform:'uppercase',letterSpacing:.3}}>{p.strategy}</div>
</div>
- <div style={{marginLeft:'auto',textAlign:'right'}}>
- <div style={{fontSize:16,fontWeight:800,color:pnl>=0?'var(--green)':'var(--red)',fontVariantNumeric:'tabular-nums'}}>
- {pnl>=0?'+':''}{(pnl/100).toFixed(2)}
+ <div style={{textAlign:'right'}}>
+ <div style={{fontSize:18,fontWeight:900,color: pnl >= 0 ? 'var(--green)' : 'var(--red)',fontVariantNumeric:'tabular-nums'}}>
+ {pnl >= 0 ? '+' : ''}{(pnl / 100).toFixed(2)}
</div>
+ <div style={{fontSize:9,color:'var(--muted)'}}>ROI: {roi}%</div>
</div>
</div>
- <div style={{display:'flex',gap:4,fontSize:9}}>
- <span style={{padding:'1px 6px',borderRadius:3,background:'rgba(34,197,94,.1)',color:'var(--green)'}}>W:{p.trades_won}</span>
- <span style={{padding:'1px 6px',borderRadius:3,background:'rgba(239,68,68,.1)',color:'var(--red)'}}>L:{p.trades_lost}</span>
- <span style={{padding:'1px 6px',borderRadius:3,background:'rgba(255,255,255,.04)',color:'var(--muted)'}}>WR:{p.win_rate}%</span>
+ <div style={{display:'flex',gap:4,fontSize:9,flexWrap:'wrap'}}>
+ <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(34,197,94,.1)',color:'var(--green)'}}>W:{p.trades_won}</span>
+ <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(239,68,68,.1)',color:'var(--red)'}}>L:{p.trades_lost}</span>
+ <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(255,255,255,.04)',color:'var(--muted)'}}>WR:{p.win_rate}%</span>
+ <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(255,255,255,.04)',color:'var(--muted)'}}>{p.total_trades} trades</span>
+ {p.streak !== 0 && <span style={{padding:'2px 7px',borderRadius:4,background: p.streak > 0 ? 'rgba(34,197,94,.1)' : 'rgba(239,68,68,.1)',color: p.streak > 0 ? 'var(--green)' : 'var(--red)'}}>{p.streak > 0 ? '+' : ''}{p.streak} streak</span>}
</div>
+ {invested > 0 && <div style={{marginTop:6}}>
+ <div style={{height:3,borderRadius:2,background:'rgba(255,255,255,.05)',overflow:'hidden'}}>
+ <div style={{height:'100%',borderRadius:2,width: Math.min(100, Math.abs(parseFloat(roi))) + '%',background: pnl >= 0 ? 'var(--green)' : 'var(--red)',transition:'width .5s'}} />
+ </div>
+ </div>}
</div>);
})}
</div>
← 4d47305 Ken: add probability shift dampening for realistic signal ed
·
back to Bertha
·
chore(alshi-dash): update 1 file (.js) [+209/-20] eb35535 →