Files
polymarket-bot/dashboard/src/App.jsx
T
chemavxandClaude Sonnet 4.6 9bdafaa51e
CI/CD / build-and-push (push) Successful in 2m24s
feat: add dashboard source code with Vite + React + Recharts and CI/CD build
- Reconstruct dashboard from compiled container: App.jsx, main.jsx, index.css
- nginx.conf with SPA routing and /api proxy to api:8000
- Multi-stage Dockerfile: node:20-alpine build + nginx:alpine serve
- Add third kaniko build step in ci.yml for chemavx/polymarket-bot-dashboard
- Update k8s manifest sed to patch deployment-dashboard.yaml image on each push

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:18:32 +00:00

311 lines
9.2 KiB
React

import { useEffect, useState, useCallback } from 'react'
import {
ResponsiveContainer,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
} from 'recharts'
const REFRESH_MS = 30_000
function fmt(n, decimals = 2) {
if (n == null) return '—'
return Number(n).toFixed(decimals)
}
function fmtUSD(n) {
if (n == null) return '—'
const abs = Math.abs(n)
const sign = n < 0 ? '-' : ''
return `${sign}$${abs.toFixed(2)}`
}
function fmtPct(n) {
if (n == null) return '—'
return `${(n * 100).toFixed(1)}%`
}
function fmtTime(ts) {
if (!ts) return '—'
const d = new Date(ts)
return d.toLocaleString('es-ES', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
function MetricCard({ title, value, subtitle, progress, progressColor }) {
return (
<div className="metric-card">
<div className="metric-header">
<span className="metric-title">{title}</span>
</div>
<div className="metric-value">{value}</div>
{subtitle && <div className="metric-subtitle">{subtitle}</div>}
{progress != null && (
<div className="progress-bar">
<div
className="progress-fill"
style={{
width: `${Math.min(100, Math.max(0, progress * 100))}%`,
background: progressColor || 'var(--blue)',
}}
/>
</div>
)}
</div>
)
}
function CustomTooltip({ active, payload, label }) {
if (!active || !payload?.length) return null
return (
<div style={{
background: 'var(--surface2)',
border: '1px solid var(--border)',
borderRadius: 8,
padding: '10px 14px',
fontSize: 12,
}}>
<div style={{ color: 'var(--text-muted)', marginBottom: 4 }}>{label}</div>
{payload.map((p) => (
<div key={p.dataKey} style={{ color: p.color }}>
P&amp;L: {fmtUSD(p.value)}
</div>
))}
</div>
)
}
export default function App() {
const [summary, setSummary] = useState(null)
const [trades, setTrades] = useState([])
const [history, setHistory] = useState([])
const [lastUpdate, setLastUpdate] = useState(null)
const [error, setError] = useState(null)
const fetchAll = useCallback(async () => {
try {
const [sumRes, tradesRes, metricsRes] = await Promise.all([
fetch('/api/summary'),
fetch('/api/trades?limit=50'),
fetch('/api/metrics'),
])
if (!sumRes.ok || !tradesRes.ok || !metricsRes.ok) throw new Error('API error')
const [sumData, tradesData, metricsData] = await Promise.all([
sumRes.json(),
tradesRes.json(),
metricsRes.json(),
])
setSummary(sumData)
setTrades(tradesData.trades || [])
const raw = (metricsData.history || []).slice().reverse()
setHistory(
raw.map((r) => ({
date: new Date(r.timestamp).toLocaleDateString('es-ES', { month: 'short', day: 'numeric' }),
pnl: r.total_pnl,
}))
)
setLastUpdate(new Date())
setError(null)
} catch (e) {
setError(e.message)
}
}, [])
useEffect(() => {
fetchAll()
const id = setInterval(fetchAll, REFRESH_MS)
return () => clearInterval(id)
}, [fetchAll])
if (!summary && !error) {
return (
<div className="loading">
<div className="spinner" />
<span>Cargando...</span>
</div>
)
}
if (error && !summary) {
return (
<div className="loading">
<span>Error conectando con la API: {error}</span>
</div>
)
}
const pnlColor = summary.total_pnl >= 0 ? 'var(--green)' : 'var(--red)'
return (
<div className="app">
{/* Header */}
<div className="header">
<div className="header-left">
<h1>
Polymarket Bot
{summary.paper_mode && <span className="badge paper">PAPER</span>}
</h1>
</div>
<div className="header-right">
{lastUpdate && (
<span className="last-update">
Actualizado: {lastUpdate.toLocaleTimeString('es-ES')}
</span>
)}
</div>
</div>
{/* Promotion banner */}
{summary.promotion_ready && (
<div className="promotion-banner">
<span className="banner-icon">🚀</span>
<div>
<strong>Listo para producción</strong> El bot cumple los criterios de
rendimiento. Considera desactivar <code>PAPER_MODE</code>.
</div>
</div>
)}
{/* Metrics grid */}
<div className="metrics-grid">
<MetricCard
title="Bankroll"
value={fmtUSD(summary.paper_bankroll)}
subtitle="Capital inicial (paper)"
/>
<MetricCard
title="P&L Total"
value={<span style={{ color: pnlColor }}>{fmtUSD(summary.total_pnl)}</span>}
subtitle={`${fmtPct(summary.total_pnl / summary.paper_bankroll)} sobre bankroll`}
progress={summary.total_pnl / summary.paper_bankroll + 0.5}
progressColor={pnlColor}
/>
<MetricCard
title="Win Rate"
value={fmtPct(summary.win_rate)}
subtitle="Objetivo ≥ 52%"
progress={summary.win_rate}
progressColor={summary.win_rate >= 0.52 ? 'var(--green)' : 'var(--amber)'}
/>
<MetricCard
title="Sharpe"
value={fmt(summary.sharpe_ratio)}
subtitle="Objetivo ≥ 0.5"
progress={Math.min(1, summary.sharpe_ratio / 2)}
progressColor={summary.sharpe_ratio >= 0.5 ? 'var(--green)' : 'var(--amber)'}
/>
<MetricCard
title="Calibration"
value={fmt(summary.calibration_score)}
subtitle="Objetivo ≥ 0.7"
progress={summary.calibration_score}
progressColor={summary.calibration_score >= 0.7 ? 'var(--green)' : 'var(--amber)'}
/>
<MetricCard
title="Capital Deployed"
value={fmtUSD(summary.total_deployed)}
subtitle={`${summary.total_trades} trades`}
/>
</div>
{/* Performance chart */}
<div className="section">
<h2>Performance Over Time</h2>
{history.length === 0 ? (
<div className="empty-chart">Sin datos históricos aún</div>
) : (
<div className="chart-container">
<ResponsiveContainer width="100%" height={240}>
<AreaChart data={history} margin={{ top: 4, right: 16, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="pnlGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--blue)" stopOpacity={0.3} />
<stop offset="95%" stopColor="var(--blue)" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis
dataKey="date"
tick={{ fill: 'var(--text-muted)', fontSize: 11 }}
axisLine={false}
tickLine={false}
/>
<YAxis
tickFormatter={(v) => `$${v}`}
tick={{ fill: 'var(--text-muted)', fontSize: 11 }}
axisLine={false}
tickLine={false}
width={60}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="pnl"
stroke="var(--blue)"
strokeWidth={2}
fill="url(#pnlGrad)"
dot={false}
activeDot={{ r: 4, fill: 'var(--blue)' }}
/>
</AreaChart>
</ResponsiveContainer>
</div>
)}
</div>
{/* Recent trades table */}
<div className="section">
<h2>Recent Trades</h2>
{trades.length === 0 ? (
<div className="empty-table">Sin trades registrados aún</div>
) : (
<div className="table-wrapper">
<table className="trade-table">
<thead>
<tr>
<th>Time</th>
<th>Market</th>
<th>Dir</th>
<th>Size</th>
<th>Price</th>
<th>Shares</th>
<th>Fee</th>
</tr>
</thead>
<tbody>
{trades.map((t) => (
<tr key={t.id}>
<td style={{ whiteSpace: 'nowrap', color: 'var(--text-muted)' }}>
{fmtTime(t.timestamp)}
</td>
<td className="market-question">{t.question}</td>
<td>
<span className={`direction-badge ${t.direction?.toLowerCase()}`}>
{t.direction}
</span>
</td>
<td>{fmtUSD(t.size_usdc)}</td>
<td>{fmt(t.entry_price, 3)}</td>
<td>{fmt(t.shares, 2)}</td>
<td style={{ color: 'var(--text-muted)' }}>{fmtUSD(t.fee_usdc)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)
}