← back to Ken

kalshi-dash/src/pages/Orbit.jsx

699 lines

import React, { useState, useEffect, useMemo, useRef } from 'react';
import { api } from '../api';

// ── Orbit Ring Configuration ──
const ORBIT_RINGS = [
  { radius: 80,  label: 'Core',  color: '#00F0FF', opacity: 0.5,  description: 'Highest confidence' },
  { radius: 160, label: 'Inner', color: '#00C389', opacity: 0.35, description: 'Strong signals' },
  { radius: 240, label: 'Mid',   color: '#3B82F6', opacity: 0.25, description: 'Moderate signals' },
  { radius: 320, label: 'Outer', color: '#A855F7', opacity: 0.18, description: 'Weak signals' },
  { radius: 400, label: 'Edge',  color: '#FF5C5C', opacity: 0.12, description: 'Speculative' },
];

const DIRECTION_COLORS = {
  BUY_YES: { fill: '#00C389', stroke: '#009E6F', glow: 'rgba(0,195,137,0.5)' },
  BUY_NO:  { fill: '#FF5C5C', stroke: '#CC4A4A', glow: 'rgba(255,92,92,0.5)' },
  YES:     { fill: '#00C389', stroke: '#009E6F', glow: 'rgba(0,195,137,0.5)' },
  NO:      { fill: '#FF5C5C', stroke: '#CC4A4A', glow: 'rgba(255,92,92,0.5)' },
};

function getSignalColor(confidence) {
  if (confidence >= 0.9) return '#00C389';
  if (confidence >= 0.7) return '#F7931A';
  if (confidence >= 0.5) return '#3B82F6';
  if (confidence >= 0.3) return '#A855F7';
  return '#FF5C5C';
}

function getOrbitRing(confidence) {
  if (confidence >= 0.9)  return 0;
  if (confidence >= 0.75) return 1;
  if (confidence >= 0.6)  return 2;
  if (confidence >= 0.4)  return 3;
  return 4;
}

// ── Animated Star Field Background ──
function StarField({ width, height }) {
  const stars = useMemo(() => {
    const s = [];
    for (let i = 0; i < 80; i++) {
      s.push({
        x: Math.random() * width,
        y: Math.random() * height,
        r: Math.random() * 1.2 + 0.3,
        opacity: Math.random() * 0.4 + 0.05,
        delay: Math.random() * 3,
      });
    }
    return s;
  }, [width, height]);

  return (
    <>
      {stars.map((star, i) => (
        <circle
          key={i}
          cx={star.x}
          cy={star.y}
          r={star.r}
          fill="white"
          opacity={star.opacity}
          className="animate-pulse"
          style={{ animationDelay: `${star.delay}s`, animationDuration: `${2 + Math.random() * 3}s` }}
        />
      ))}
    </>
  );
}

// ── Planet/Market Node ──
function MarketNode({ market, cx, cy, size, isSelected, onSelect, animate }) {
  const dirColors = DIRECTION_COLORS[market.direction] || DIRECTION_COLORS.BUY_YES;
  const confidence = parseFloat(market.confidence) || 0;
  const signalColor = getSignalColor(confidence);
  const pulseSize = size + 4;
  const gradId = `gradient-${market.ticker?.replace(/[^a-zA-Z0-9]/g, '')}`;

  return (
    <g
      onClick={() => onSelect(market)}
      className="cursor-pointer"
      style={{ transition: 'all 0.5s ease-out' }}
    >
      {/* Glow pulse ring */}
      <circle
        cx={cx}
        cy={cy}
        r={pulseSize}
        fill="none"
        stroke={dirColors.glow}
        strokeWidth={1.5}
        opacity={isSelected ? 0.9 : 0.3}
        className={animate ? 'animate-ping' : ''}
        style={{ animationDuration: '2s', transformOrigin: `${cx}px ${cy}px` }}
      />

      {/* Signal strength dashed ring */}
      <circle
        cx={cx}
        cy={cy}
        r={size + 2}
        fill="none"
        stroke={signalColor}
        strokeWidth={2}
        opacity={0.65}
        strokeDasharray={`${confidence * 20} ${(1 - confidence) * 20}`}
      />

      {/* Main planet */}
      <circle
        cx={cx}
        cy={cy}
        r={size}
        fill={`url(#${gradId})`}
        stroke={isSelected ? '#00F0FF' : dirColors.stroke}
        strokeWidth={isSelected ? 2.5 : 1}
        className="transition-all duration-300"
      />

      {/* Gradient definition */}
      <defs>
        <radialGradient id={gradId}>
          <stop offset="0%"   stopColor={dirColors.fill}   stopOpacity="0.9" />
          <stop offset="70%"  stopColor={dirColors.fill}   stopOpacity="0.6" />
          <stop offset="100%" stopColor={dirColors.stroke} stopOpacity="0.4" />
        </radialGradient>
      </defs>

      {/* Edge % label */}
      {size >= 8 && (
        <text
          x={cx}
          y={cy + 1}
          textAnchor="middle"
          dominantBaseline="middle"
          fill="white"
          fontSize={Math.max(7, size * 0.6)}
          fontWeight="bold"
          fontFamily="'JetBrains Mono', monospace"
          style={{ pointerEvents: 'none' }}
        >
          {market.edge_display || ''}
        </text>
      )}

      {/* Ticker label */}
      {size >= 10 && (
        <text
          x={cx}
          y={cy + size + 12}
          textAnchor="middle"
          fill="#A8A8A8"
          fontSize={8}
          fontFamily="'JetBrains Mono', monospace"
          style={{ pointerEvents: 'none' }}
        >
          {(market.shortTitle || '').substring(0, 18)}
        </text>
      )}
    </g>
  );
}

// ── Orbit Detail Panel ──
function OrbitDetail({ market, onClose }) {
  if (!market) return null;

  const conf = parseFloat(market.confidence) || 0;
  const edge = parseFloat(market.expected_edge) || 0;
  const dirColors = DIRECTION_COLORS[market.direction] || DIRECTION_COLORS.BUY_YES;
  const ringLabel = ORBIT_RINGS[getOrbitRing(conf)]?.label || 'Edge';

  return (
    <div className="card-glow fade-in">
      <div className="flex items-start justify-between mb-4">
        <div className="flex-1 min-w-0 mr-4">
          <div className="flex items-center gap-2 mb-2">
            <span className="inline-block w-3 h-3 rounded-full" style={{ backgroundColor: dirColors.fill }} />
            <span
              className="badge"
              style={
                market.direction === 'BUY_YES' || market.direction === 'YES'
                  ? { background: 'var(--green-dim)', color: 'var(--green)' }
                  : { background: 'var(--red-dim)', color: 'var(--red)' }
              }
            >
              {market.direction}
            </span>
            {market.risk_approved ? (
              <span className="badge bg-green">APPROVED</span>
            ) : (
              <span className="badge bg-red">BLOCKED</span>
            )}
          </div>
          <h3 className="heading text-sm font-bold" style={{ color: 'var(--text)' }}>
            {market.title || market.ticker}
          </h3>
          <p className="mono text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{market.ticker}</p>
        </div>
        <button onClick={onClose} className="btn btn-ghost text-lg leading-none" style={{ padding: '0 6px' }}>
          &times;
        </button>
      </div>

      <div className="grid grid-cols-3 gap-3 mb-4">
        {[
          { label: 'Edge',        value: `${(edge * 100).toFixed(1)}%`,  color: 'var(--orange)' },
          { label: 'Confidence',  value: `${(conf * 100).toFixed(0)}%`,  color: getSignalColor(conf) },
          { label: 'Orbit Ring',  value: ringLabel,                       color: 'var(--purple)' },
        ].map(s => (
          <div
            key={s.label}
            className="rounded-lg p-2.5 text-center"
            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
          >
            <p className="stat-label mb-1">{s.label}</p>
            <p className="stat-v text-lg" style={{ color: s.color }}>{s.value}</p>
          </div>
        ))}
      </div>

      {market.reasoning && (
        <div
          className="text-[11px] rounded-lg px-3 py-2 mb-3 leading-relaxed"
          style={{
            color: 'var(--text-dim)',
            background: 'var(--surface-el)',
            border: '1px solid var(--border)',
          }}
        >
          {market.reasoning}
        </div>
      )}

      {market.sources && market.sources.length > 0 && (
        <div className="space-y-1.5">
          <p className="stat-label mb-1">Signal Sources</p>
          {market.sources.map((src, i) => (
            <div
              key={i}
              className="flex items-center gap-2 text-xs rounded-lg px-3 py-2"
              style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
            >
              <span className="badge bg-blue">{src.source}</span>
              <span className="truncate flex-1" style={{ color: 'var(--text-dim)' }}>{src.headline}</span>
              <span
                className="mono text-[10px]"
                style={{ color: src.sentiment > 0 ? 'var(--green)' : src.sentiment < 0 ? 'var(--red)' : 'var(--text-muted)' }}
              >
                {src.sentiment > 0 ? '+' : ''}{src.sentiment?.toFixed(2)}
              </span>
            </div>
          ))}
        </div>
      )}

      <div className="mt-3 text-[10px]" style={{ color: 'var(--text-muted)' }}>
        Signal created:{' '}
        {market.created_at
          ? new Date(market.created_at).toLocaleString('en-US', {
              timeZone: 'America/Los_Angeles',
              hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric',
            })
          : 'Unknown'}
      </div>
    </div>
  );
}

// ── Main Orbit Visualization ──
export default function Orbit() {
  const [data, setData] = useState(null);
  const [signals, setSignals] = useState(null);
  const [loading, setLoading] = useState(true);
  const [selectedTicker, setSelectedTicker] = useState(null);
  const [filter, setFilter] = useState('all'); // 'all' | 'approved' | 'yes' | 'no'
  const [animating, setAnimating] = useState(true);
  const svgRef = useRef(null);

  useEffect(() => {
    load();
    const i = setInterval(load, 30000);
    return () => clearInterval(i);
  }, []);

  async function load() {
    try {
      const [d, s, sd] = await Promise.all([
        api('/dashboard'),
        api('/signals'),
        api('/signals/dashboard'),
      ]);
      setData(d);
      setSignals({ signals: s?.signals || [], dashboard: sd });
    } catch {}
    setLoading(false);
  }

  const { orbitalMarkets, stats } = useMemo(() => {
    if (!signals?.signals) return { orbitalMarkets: [], stats: {} };

    let items = signals.signals.map(s => {
      const conf = parseFloat(s.confidence) || 0;
      const edge = parseFloat(s.expected_edge) || 0;
      const ring = getOrbitRing(conf);

      const parts = (s.ticker || '').split('-');
      const shortTitle = parts[0]?.replace('KX', '').substring(0, 12) || s.ticker;

      return {
        ...s,
        shortTitle,
        title: s.reasoning?.split('"')[1] || s.ticker,
        ring,
        conf,
        edge,
        edge_display: `${(edge * 100).toFixed(0)}%`,
        size: Math.max(6, Math.min(22, edge * 120 + 6)),
      };
    });

    // Apply filter
    if (filter === 'approved') items = items.filter(i => i.risk_approved);
    if (filter === 'yes') items = items.filter(i => i.direction === 'BUY_YES');
    if (filter === 'no') items = items.filter(i => i.direction === 'BUY_NO');

    // Limit to top 60 by edge
    items.sort((a, b) => b.edge - a.edge);
    items = items.slice(0, 60);

    const approved = items.filter(i => i.risk_approved).length;
    const buyYes = items.filter(i => i.direction === 'BUY_YES').length;
    const buyNo = items.filter(i => i.direction === 'BUY_NO').length;
    const avgConf = items.length > 0 ? items.reduce((s, i) => s + i.conf, 0) / items.length : 0;
    const avgEdge = items.length > 0 ? items.reduce((s, i) => s + i.edge, 0) / items.length : 0;

    return {
      orbitalMarkets: items,
      stats: { total: items.length, approved, buyYes, buyNo, avgConf, avgEdge },
    };
  }, [signals, filter]);

  const selectedMarket = orbitalMarkets.find(m => m.ticker === selectedTicker);

  if (loading) return (
    <div className="flex items-center justify-center h-64">
      <div
        className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
        style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
      />
    </div>
  );

  const CX = 450;
  const CY = 430;
  const SVG_W = 900;
  const SVG_H = 860;

  // Position markets on orbit rings with angular distribution
  const ringCounts = {};
  orbitalMarkets.forEach(m => {
    ringCounts[m.ring] = (ringCounts[m.ring] || 0) + 1;
  });
  const ringIndices = {};

  const positionedMarkets = orbitalMarkets.map(m => {
    const ring = ORBIT_RINGS[m.ring];
    if (!ringIndices[m.ring]) ringIndices[m.ring] = 0;
    const idx = ringIndices[m.ring]++;
    const count = ringCounts[m.ring];

    const angleStep = (2 * Math.PI) / count;
    const angle = angleStep * idx + (m.ring * 0.4);
    const jitter = (Math.sin(idx * 7.3) * 0.15);
    const effectiveAngle = angle + jitter;
    const radiusJitter = ring.radius + (Math.cos(idx * 3.7) * 15);

    return {
      ...m,
      cx: CX + radiusJitter * Math.cos(effectiveAngle),
      cy: CY + radiusJitter * Math.sin(effectiveAngle),
    };
  });

  const filterOptions = [
    { key: 'all', label: 'All' },
    { key: 'approved', label: 'Approved' },
    { key: 'yes', label: 'BUY YES' },
    { key: 'no', label: 'BUY NO' },
  ];

  const summaryStats = [
    { label: 'Active Signals', value: stats.total || 0,                           color: 'var(--cyan)' },
    { label: 'Risk Approved',  value: stats.approved || 0,                         color: 'var(--green)' },
    { label: 'BUY YES',        value: stats.buyYes || 0,                           color: 'var(--green)' },
    { label: 'BUY NO',         value: stats.buyNo || 0,                            color: 'var(--red)' },
    { label: 'Avg Confidence', value: `${((stats.avgConf || 0) * 100).toFixed(0)}%`, color: 'var(--blue)' },
    { label: 'Avg Edge',       value: `${((stats.avgEdge || 0) * 100).toFixed(1)}%`, color: 'var(--purple)' },
  ];

  return (
    <div className="space-y-6">
      {/* Page Header */}
      <div className="flex items-center justify-between">
        <div>
          <h1 className="heading text-2xl font-bold flex items-center gap-2">
            <span style={{ color: 'var(--cyan)' }}>&#9680;</span>
            <span className="gradient-cyan">Signal Orbit</span>
          </h1>
          <p className="section-subtitle mt-1">
            Radial visualization — distance from center = confidence, size = edge strength
          </p>
        </div>
        <div className="flex items-center gap-3">
          {/* Filter toggle group */}
          <div
            className="flex items-center gap-0.5 rounded-lg p-0.5"
            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
          >
            {filterOptions.map(f => (
              <button
                key={f.key}
                onClick={() => setFilter(f.key)}
                className="btn btn-sm"
                style={filter === f.key
                  ? { background: 'var(--cyan)', color: 'var(--bg)', fontWeight: 700 }
                  : { background: 'transparent', color: 'var(--text-muted)' }
                }
              >
                {f.label}
              </button>
            ))}
          </div>

          <label className="flex items-center gap-2 text-xs cursor-pointer" style={{ color: 'var(--text-dim)' }}>
            <input
              type="checkbox"
              checked={animating}
              onChange={e => setAnimating(e.target.checked)}
              className="rounded"
              style={{ accentColor: 'var(--cyan)' }}
            />
            Animate
          </label>

          <button onClick={load} className="btn btn-o btn-sm">Refresh</button>
        </div>
      </div>

      {/* Stats Row */}
      <div className="grid grid-cols-6 gap-3">
        {summaryStats.map(s => (
          <div key={s.label} className="card">
            <p className="stat-label mb-1">{s.label}</p>
            <p className="stat-v" style={{ color: s.color }}>{s.value}</p>
          </div>
        ))}
      </div>

      {/* Orbit SVG Visualization */}
      <div
        className="card p-0 overflow-hidden relative"
        style={{ background: 'radial-gradient(ellipse at center, #0D1B2E 0%, #080D14 60%, var(--bg) 100%)' }}
      >
        <svg
          ref={svgRef}
          viewBox={`0 0 ${SVG_W} ${SVG_H}`}
          className="w-full"
          style={{ maxHeight: '75vh' }}
        >
          {/* Star field */}
          <StarField width={SVG_W} height={SVG_H} />

          {/* Orbit rings */}
          {ORBIT_RINGS.map((ring, i) => (
            <g key={i}>
              <circle
                cx={CX}
                cy={CY}
                r={ring.radius}
                fill="none"
                stroke={ring.color}
                strokeWidth={0.8}
                opacity={ring.opacity}
                strokeDasharray="4 4"
              />
              <text
                x={CX + ring.radius + 6}
                y={CY - 4}
                fill={ring.color}
                fontSize={9}
                opacity={ring.opacity + 0.2}
                fontFamily="'JetBrains Mono', monospace"
              >
                {ring.label}
              </text>
            </g>
          ))}

          {/* Center sun — cyan glow */}
          <defs>
            <radialGradient id="sun-gradient">
              <stop offset="0%"   stopColor="#00F0FF" stopOpacity="1" />
              <stop offset="50%"  stopColor="#00B8C8" stopOpacity="0.8" />
              <stop offset="100%" stopColor="#006070" stopOpacity="0.4" />
            </radialGradient>
            <filter id="sun-glow">
              <feGaussianBlur stdDeviation="10" result="blur" />
              <feMerge>
                <feMergeNode in="blur" />
                <feMergeNode in="SourceGraphic" />
              </feMerge>
            </filter>
          </defs>

          <circle cx={CX} cy={CY} r={40} fill="url(#sun-gradient)" filter="url(#sun-glow)" opacity={0.55} />
          <circle cx={CX} cy={CY} r={25} fill="url(#sun-gradient)" />

          {/* KEN label in center */}
          <text
            x={CX} y={CY - 4}
            textAnchor="middle"
            fill="#0E0E10"
            fontSize={12}
            fontWeight="bold"
            fontFamily="'JetBrains Mono', monospace"
          >
            KEN
          </text>
          <text
            x={CX} y={CY + 10}
            textAnchor="middle"
            fill="#0E0E10"
            fontSize={7}
            fontFamily="'JetBrains Mono', monospace"
            opacity={0.7}
          >
            TRADER
          </text>

          {/* Connection lines from markets to center */}
          {positionedMarkets.map(m => {
            const dirColors = DIRECTION_COLORS[m.direction] || DIRECTION_COLORS.BUY_YES;
            return (
              <line
                key={`line-${m.ticker}`}
                x1={CX}
                y1={CY}
                x2={m.cx}
                y2={m.cy}
                stroke={dirColors.fill}
                strokeWidth={0.4}
                opacity={selectedTicker === m.ticker ? 0.6 : 0.1}
              />
            );
          })}

          {/* Market nodes */}
          {positionedMarkets.map(m => (
            <MarketNode
              key={m.ticker}
              market={m}
              cx={m.cx}
              cy={m.cy}
              size={m.size}
              isSelected={selectedTicker === m.ticker}
              onSelect={market => setSelectedTicker(market.ticker === selectedTicker ? null : market.ticker)}
              animate={animating && m.risk_approved}
            />
          ))}
        </svg>

        {/* Bottom legend overlay */}
        <div
          className="absolute bottom-3 left-3 rounded-lg p-3"
          style={{
            background: 'rgba(14,14,16,0.85)',
            backdropFilter: 'blur(8px)',
            border: '1px solid var(--border)',
          }}
        >
          <div className="flex items-center gap-4 text-[10px]">
            <div className="flex items-center gap-1.5">
              <div className="w-3 h-3 rounded-full" style={{ backgroundColor: 'var(--green)' }} />
              <span style={{ color: 'var(--text-dim)' }}>BUY YES</span>
            </div>
            <div className="flex items-center gap-1.5">
              <div className="w-3 h-3 rounded-full" style={{ backgroundColor: 'var(--red)' }} />
              <span style={{ color: 'var(--text-dim)' }}>BUY NO</span>
            </div>
            <span style={{ color: 'var(--border-light)' }}>|</span>
            <span style={{ color: 'var(--text-muted)' }}>Closer to center = higher confidence</span>
            <span style={{ color: 'var(--text-muted)' }}>Larger = bigger edge</span>
          </div>
        </div>

        {/* Top-right ring legend */}
        <div
          className="absolute top-3 right-3 rounded-lg p-2.5"
          style={{
            background: 'rgba(14,14,16,0.85)',
            backdropFilter: 'blur(8px)',
            border: '1px solid var(--border)',
          }}
        >
          <p className="stat-label mb-1.5">Orbit Rings</p>
          <div className="space-y-1">
            {ORBIT_RINGS.map((ring, i) => (
              <div key={i} className="flex items-center gap-2 text-[10px]">
                <div className="w-2 h-2 rounded-full" style={{ backgroundColor: ring.color, opacity: ring.opacity + 0.3 }} />
                <span style={{ color: ring.color }}>{ring.label}</span>
                <span style={{ color: 'var(--text-muted)' }}>{ring.description}</span>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* Selected Market Detail */}
      {selectedMarket && (
        <OrbitDetail market={selectedMarket} onClose={() => setSelectedTicker(null)} />
      )}

      {/* Top Signals Table */}
      <div className="card">
        <div className="flex items-center gap-2 mb-4">
          <h2 className="section-title">Orbital Signal Registry</h2>
          <span className="text-xs" style={{ color: 'var(--text-muted)' }}>
            {orbitalMarkets.length} signals in orbit
          </span>
        </div>
        <div className="overflow-x-auto">
          <table>
            <thead>
              <tr>
                <th>Ring</th>
                <th>Ticker</th>
                <th>Direction</th>
                <th>Edge</th>
                <th>Confidence</th>
                <th>Risk</th>
                <th>Sources</th>
              </tr>
            </thead>
            <tbody>
              {orbitalMarkets.slice(0, 20).map(m => {
                const ring = ORBIT_RINGS[m.ring];
                return (
                  <tr
                    key={m.ticker}
                    onClick={() => setSelectedTicker(m.ticker === selectedTicker ? null : m.ticker)}
                    className="cursor-pointer"
                    style={selectedTicker === m.ticker ? { background: 'var(--cyan-glow)' } : {}}
                  >
                    <td>
                      <span className="text-xs font-bold heading" style={{ color: ring.color }}>
                        {ring.label}
                      </span>
                    </td>
                    <td className="mono text-xs" style={{ color: 'var(--text-dim)' }}>{m.ticker}</td>
                    <td>
                      <span
                        className="badge"
                        style={
                          m.direction === 'BUY_YES'
                            ? { background: 'var(--green-dim)', color: 'var(--green)' }
                            : { background: 'var(--red-dim)', color: 'var(--red)' }
                        }
                      >
                        {m.direction}
                      </span>
                    </td>
                    <td className="mono font-bold text-sm" style={{ color: 'var(--orange)' }}>
                      {(m.edge * 100).toFixed(1)}%
                    </td>
                    <td className="mono text-sm" style={{ color: getSignalColor(m.conf) }}>
                      {(m.conf * 100).toFixed(0)}%
                    </td>
                    <td>
                      {m.risk_approved ? (
                        <span className="text-xs font-bold" style={{ color: 'var(--green)' }}>OK</span>
                      ) : (
                        <span className="text-xs font-bold" style={{ color: 'var(--red)' }}>BLOCKED</span>
                      )}
                    </td>
                    <td className="text-xs" style={{ color: 'var(--text-muted)' }}>
                      {m.sources?.length || 0}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}