← back to Bertha

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>
  );
}