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 (
{title}
{value}
{subtitle &&
{subtitle}
}
{progress != null && (
)}
)
}
function CustomTooltip({ active, payload, label }) {
if (!active || !payload?.length) return null
return (
{label}
{payload.map((p) => (
P&L: {fmtUSD(p.value)}
))}
)
}
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 (
)
}
if (error && !summary) {
return (
Error conectando con la API: {error}
)
}
const pnlColor = summary.total_pnl >= 0 ? 'var(--green)' : 'var(--red)'
return (
{/* Header */}
Polymarket Bot
{summary.paper_mode && PAPER}
{lastUpdate && (
Actualizado: {lastUpdate.toLocaleTimeString('es-ES')}
)}
{/* Promotion banner */}
{summary.promotion_ready && (
🚀
Listo para producción — El bot cumple los criterios de
rendimiento. Considera desactivar PAPER_MODE.
)}
{/* Metrics grid */}
{fmtUSD(summary.total_pnl)}}
subtitle={`${fmtPct(summary.total_pnl / summary.paper_bankroll)} sobre bankroll`}
progress={summary.total_pnl / summary.paper_bankroll + 0.5}
progressColor={pnlColor}
/>
= 0.52 ? 'var(--green)' : 'var(--amber)'}
/>
= 0.5 ? 'var(--green)' : 'var(--amber)'}
/>
= 0.7 ? 'var(--green)' : 'var(--amber)'}
/>
{/* Performance chart */}
Performance Over Time
{history.length === 0 ? (
Sin datos históricos aún
) : (
`$${v}`}
tick={{ fill: 'var(--text-muted)', fontSize: 11 }}
axisLine={false}
tickLine={false}
width={60}
/>
} />
)}
{/* Recent trades table */}
Recent Trades
{trades.length === 0 ? (
Sin trades registrados aún
) : (
| Time |
Market |
Dir |
Size |
Price |
Shares |
Fee |
{trades.map((t) => (
|
{fmtTime(t.timestamp)}
|
{t.question} |
{t.direction}
|
{fmtUSD(t.size_usdc)} |
{fmt(t.entry_price, 3)} |
{fmt(t.shares, 2)} |
{fmtUSD(t.fee_usdc)} |
))}
)}
)
}