← back to Ken
react-dash/src/pages/Polymarket.jsx
326 lines
import React, { useState, useEffect } from 'react';
import { api, post } from '../api';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts';
export default function Polymarket() {
const [status, setStatus] = useState(null);
const [clobOk, setClobOk] = useState(null);
const [markets, setMarkets] = useState([]);
const [prices, setPrices] = useState([]);
const [selectedToken, setSelectedToken] = useState(null);
const [orderbook, setOrderbook] = useState(null);
const [loading, setLoading] = useState(true);
const [liveLoading, setLiveLoading] = useState(false);
const [obLoading, setObLoading] = useState(false);
useEffect(() => {
loadAll();
}, []);
async function loadAll() {
setLoading(true);
try {
const [s, m] = await Promise.all([
post('/polymarket', { action: 'status' }),
api('/markets'),
]);
setStatus(s);
setMarkets(m.markets || []);
} catch {}
setLoading(false);
}
async function testClob() {
setClobOk('testing');
try {
const r = await post('/polymarket', { action: 'test_public' });
setClobOk(r.success ? r : { error: true });
} catch {
setClobOk({ error: true });
}
}
async function fetchLivePrices() {
setLiveLoading(true);
try {
const r = await post('/polymarket', { action: 'get_markets' });
if (r.success) setPrices(r.tracked_prices || []);
} catch {}
setLiveLoading(false);
}
async function fetchOrderbook(tokenId) {
setSelectedToken(tokenId);
setObLoading(true);
setOrderbook(null);
try {
const r = await post('/polymarket', { action: 'get_orderbook', token_id: tokenId });
if (r.success) setOrderbook(r.orderbook);
} catch {}
setObLoading(false);
}
const dot = (v) => v ? 'bg-green-500' : 'bg-gray-600';
// Build orderbook chart data
const obChartData = [];
if (orderbook) {
const bids = (orderbook.bids || []).slice(0, 10).map(b => ({
price: parseFloat(b.price),
size: parseFloat(b.size),
side: 'bid',
}));
const asks = (orderbook.asks || []).slice(0, 10).map(a => ({
price: parseFloat(a.price),
size: parseFloat(a.size),
side: 'ask',
}));
bids.reverse().forEach(b => obChartData.push(b));
asks.forEach(a => obChartData.push(a));
}
// Price chart for tracked tokens
const priceChartData = prices.map(p => ({
name: (p.question || '').substring(0, 30) + '...',
price: parseFloat(p.price || 0),
outcome: p.outcome,
}));
if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-purple-500 border-t-transparent rounded-full" /></div>;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Polymarket</h1>
<p className="text-gray-500 text-sm">Live CLOB data, orderbooks & market prices</p>
</div>
<div className="flex gap-3">
<button onClick={testClob} className={`btn btn-s text-sm ${clobOk?.ok ? 'text-green-400 border-green-500/30' : ''}`}>
{clobOk === 'testing' ? 'Testing...' : clobOk?.ok ? 'CLOB OK' : clobOk?.error ? 'Error' : 'Test CLOB'}
</button>
<button onClick={fetchLivePrices} disabled={liveLoading} className="btn btn-p text-sm">
{liveLoading ? 'Fetching...' : 'Fetch Live Prices'}
</button>
</div>
</div>
{/* Connection Status Bar */}
<div className="card">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${dot(status?.connected)}`} />
<span className="text-sm">{status?.connected ? 'API Connected' : 'Not Connected'}</span>
</div>
{status?.api_key && <span className="text-xs text-gray-500 font-mono">Key: {status.api_key}</span>}
{status?.wallet && <span className="text-xs text-gray-500 font-mono">Wallet: {status.wallet}</span>}
{clobOk?.server_time && <span className="text-xs text-gray-500">Server: {new Date(parseInt(clobOk.server_time) * 1000).toLocaleTimeString()}</span>}
<span className="text-xs text-gray-500 ml-auto">{markets.length} tracked markets</span>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-4 gap-4">
<div className="card">
<p className="text-xs text-gray-500 uppercase mb-1">Tracked Markets</p>
<p className="stat-v text-purple-400">{markets.length}</p>
</div>
<div className="card">
<p className="text-xs text-gray-500 uppercase mb-1">Active</p>
<p className="stat-v text-green-400">{markets.filter(m => m.active).length}</p>
</div>
<div className="card">
<p className="text-xs text-gray-500 uppercase mb-1">Live Prices</p>
<p className="stat-v text-blue-400">{prices.length}</p>
</div>
<div className="card">
<p className="text-xs text-gray-500 uppercase mb-1">API Status</p>
<p className={`stat-v ${status?.connected ? 'text-green-400' : 'text-gray-500'}`}>{status?.connected ? 'LIVE' : 'OFF'}</p>
</div>
</div>
{/* Live Prices Chart */}
{priceChartData.length > 0 && (
<div className="card">
<h2 className="font-semibold mb-3">Live Token Prices</h2>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={priceChartData} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis type="number" domain={[0, 1]} tickFormatter={v => `${(v * 100).toFixed(0)}%`} stroke="#6b7280" />
<YAxis type="category" dataKey="name" width={200} tick={{ fontSize: 11, fill: '#9ca3af' }} />
<Tooltip formatter={v => `${(v * 100).toFixed(1)}%`} contentStyle={{ background: '#1f2937', border: '1px solid #374151', borderRadius: 8 }} />
<Bar dataKey="price" radius={[0, 4, 4, 0]}>
{priceChartData.map((d, i) => (
<Cell key={i} fill={d.outcome === 'YES' ? '#22c55e' : '#ef4444'} fillOpacity={0.7} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
)}
{/* Markets Table with Live Prices */}
<div className="card overflow-x-auto">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold">Weather Markets</h2>
<span className="text-xs text-gray-500">{markets.length} markets</span>
</div>
<table>
<thead>
<tr><th>Question</th><th>Category</th><th>Liquidity</th><th>End Date</th><th>Live Price</th><th>Orderbook</th></tr>
</thead>
<tbody>
{markets.length === 0 ? (
<tr><td colSpan={6} className="text-center text-gray-500 py-8">No markets tracked. Go to Markets page to ingest.</td></tr>
) : markets.map(m => {
const livePrice = prices.find(p => p.condition_id === m.condition_id);
return (
<tr key={m.condition_id}>
<td className="max-w-[300px]">
<p className="truncate font-medium">{m.question}</p>
<p className="text-xs text-gray-600 truncate font-mono">{m.condition_id?.substring(0, 12)}...</p>
</td>
<td><span className="badge bg-purple">{m.category || 'weather'}</span></td>
<td className="font-mono text-sm">${parseFloat(m.liquidity || 0).toFixed(0)}</td>
<td className="text-gray-400 text-sm">{m.end_ts ? new Date(m.end_ts).toLocaleDateString() : '-'}</td>
<td>
{livePrice ? (
<span className="font-mono text-green-400 font-medium">{(parseFloat(livePrice.price) * 100).toFixed(1)}%</span>
) : (
<span className="text-gray-600 text-xs">—</span>
)}
</td>
<td>
{m.tokens?.length > 0 || livePrice?.token_id ? (
<button
onClick={() => fetchOrderbook(livePrice?.token_id || m.tokens?.[0]?.token_id)}
className="text-xs text-purple-400 hover:text-purple-300"
>
View
</button>
) : (
<span className="text-gray-600 text-xs">—</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Orderbook View */}
{(orderbook || obLoading) && (
<div className="card">
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold">Orderbook</h2>
<div className="flex items-center gap-3">
<span className="text-xs text-gray-500 font-mono">{selectedToken?.substring(0, 12)}...</span>
<button onClick={() => { setOrderbook(null); setSelectedToken(null); }} className="text-xs text-gray-500 hover:text-gray-300">Close</button>
</div>
</div>
{obLoading ? (
<div className="flex items-center justify-center h-32"><div className="animate-spin w-6 h-6 border-2 border-purple-500 border-t-transparent rounded-full" /></div>
) : orderbook ? (
<div className="grid grid-cols-2 gap-6">
{/* Bids */}
<div>
<h3 className="text-sm font-medium text-green-400 mb-2">Bids (Buy Orders)</h3>
<div className="space-y-1">
<div className="flex justify-between text-[10px] text-gray-500 uppercase px-2">
<span>Price</span><span>Size</span>
</div>
{(orderbook.bids || []).slice(0, 12).map((b, i) => (
<div key={i} className="flex justify-between text-sm px-2 py-1 rounded relative overflow-hidden">
<div className="absolute inset-0 bg-green-500/10" style={{ width: `${Math.min(parseFloat(b.size) * 10, 100)}%` }} />
<span className="font-mono text-green-400 relative z-10">{parseFloat(b.price).toFixed(3)}</span>
<span className="font-mono text-gray-400 relative z-10">{parseFloat(b.size).toFixed(1)}</span>
</div>
))}
{(!orderbook.bids || orderbook.bids.length === 0) && <p className="text-gray-600 text-xs text-center py-4">No bids</p>}
</div>
</div>
{/* Asks */}
<div>
<h3 className="text-sm font-medium text-red-400 mb-2">Asks (Sell Orders)</h3>
<div className="space-y-1">
<div className="flex justify-between text-[10px] text-gray-500 uppercase px-2">
<span>Price</span><span>Size</span>
</div>
{(orderbook.asks || []).slice(0, 12).map((a, i) => (
<div key={i} className="flex justify-between text-sm px-2 py-1 rounded relative overflow-hidden">
<div className="absolute inset-0 bg-red-500/10 right-0" style={{ width: `${Math.min(parseFloat(a.size) * 10, 100)}%` }} />
<span className="font-mono text-red-400 relative z-10">{parseFloat(a.price).toFixed(3)}</span>
<span className="font-mono text-gray-400 relative z-10">{parseFloat(a.size).toFixed(1)}</span>
</div>
))}
{(!orderbook.asks || orderbook.asks.length === 0) && <p className="text-gray-600 text-xs text-center py-4">No asks</p>}
</div>
</div>
</div>
) : null}
{/* Orderbook Depth Chart */}
{obChartData.length > 0 && (
<div className="mt-4 pt-4 border-t border-gray-800">
<h3 className="text-sm font-medium text-gray-400 mb-2">Depth Chart</h3>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={obChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis dataKey="price" tickFormatter={v => v.toFixed(2)} stroke="#6b7280" />
<YAxis stroke="#6b7280" />
<Tooltip formatter={v => v.toFixed(2)} contentStyle={{ background: '#1f2937', border: '1px solid #374151', borderRadius: 8 }} />
<Bar dataKey="size" radius={[4, 4, 0, 0]}>
{obChartData.map((d, i) => (
<Cell key={i} fill={d.side === 'bid' ? '#22c55e' : '#ef4444'} fillOpacity={0.6} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
)}
</div>
)}
{/* Quick Reference */}
<div className="card">
<h2 className="font-semibold mb-3">CLOB Quick Reference</h2>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<p className="text-gray-500 text-xs uppercase mb-2">Public Endpoints</p>
<ul className="space-y-1 text-gray-400 text-xs font-mono">
<li>/ok — Health check</li>
<li>/time — Server time</li>
<li>/book?token_id=X — Orderbook</li>
<li>/price?token_id=X — Best price</li>
<li>/midpoint?token_id=X — Mid price</li>
<li>/spread?token_id=X — Bid-ask spread</li>
</ul>
</div>
<div>
<p className="text-gray-500 text-xs uppercase mb-2">Auth Endpoints</p>
<ul className="space-y-1 text-gray-400 text-xs font-mono">
<li>POST /order — Place order</li>
<li>DELETE /order — Cancel order</li>
<li>POST /orders — Batch orders</li>
<li>GET /data/trades — User trades</li>
<li>GET /data/orders — User orders</li>
</ul>
</div>
<div>
<p className="text-gray-500 text-xs uppercase mb-2">Trading Notes</p>
<ul className="space-y-1 text-gray-400 text-xs">
<li>Chain: Polygon (137)</li>
<li>Collateral: USDC (6 decimals)</li>
<li>Conditional: 6 decimals</li>
<li>Min order: varies per market</li>
<li>Paper mode: no real funds needed</li>
</ul>
</div>
</div>
</div>
</div>
);
}