← back to Ken

kalshi-dash/src/pages/Signals.jsx

483 lines

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

const DIR_BG = { BUY_YES: 'bg-green', BUY_NO: 'bg-red' };

/* Extract human-readable title from reasoning: 'BUY_YES on "Will X happen?" — edge...' */
function extractTitle(reasoning) {
  if (!reasoning) return null;
  const m = reasoning.match(/on\s+"([^"]+)"/);
  return m ? m[1] : null;
}

/* ─── Confidence meter ─── */
function ConfBar({ value }) {
  const pct = Math.max(0, Math.min(100, value * 100));
  const fillColor = value > 0.7 ? 'var(--green)' : value > 0.5 ? 'var(--orange)' : 'var(--red)';
  return (
    <div className="flex items-center gap-2">
      <div className="progress-bar flex-1" style={{ minWidth: 64 }}>
        <div
          className="progress-fill"
          style={{
            width: `${pct}%`,
            background: `linear-gradient(90deg, ${fillColor}, ${fillColor}cc)`,
          }}
        />
      </div>
      <span className="mono text-xs" style={{ color: 'var(--text-dim)' }}>
        {pct.toFixed(0)}%
      </span>
    </div>
  );
}

/* ─── Strategy signal expandable row ─── */
function SignalRow({ s }) {
  const [expanded, setExpanded] = useState(false);
  const title = extractTitle(s.reasoning) || s.market_id;
  const edge = parseFloat(s.expected_edge) || 0;
  const conf = parseFloat(s.confidence) || 0;

  return (
    <>
      <tr
        className="cursor-pointer transition"
        onClick={() => setExpanded(!expanded)}
        style={{ background: expanded ? 'rgba(0,240,255,0.04)' : undefined }}
      >
        {/* Time */}
        <td className="whitespace-nowrap">
          <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
            {new Date(s.created_at).toLocaleTimeString()}
          </span>
        </td>

        {/* Market */}
        <td className="max-w-[280px]">
          <a
            href={s.kalshi_url || `https://kalshi.com/search?query=${encodeURIComponent((s.market_id || '').split('-')[0])}`}
            target="_blank"
            rel="noopener noreferrer"
            className="text-sm truncate block hover:underline transition heading"
            style={{ color: 'var(--cyan)' }}
            onClick={ev => ev.stopPropagation()}
          >
            {title}
          </a>
          <span className="mono text-[10px]" style={{ color: 'var(--text-muted)' }}>
            {s.market_id}
          </span>
        </td>

        {/* Direction badge */}
        <td>
          <span className={`badge ${DIR_BG[s.direction] || 'bg-yellow'}`}>
            {s.direction}
          </span>
        </td>

        {/* Edge — large cyan mono */}
        <td>
          <span
            className="mono font-bold text-base"
            style={{ color: edge > 0 ? 'var(--cyan)' : 'var(--red)' }}
          >
            {edge > 0 ? '+' : ''}{(edge * 100).toFixed(1)}%
          </span>
        </td>

        {/* Size */}
        <td>
          <span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
            {s.size_recommendation || '—'}
          </span>
        </td>

        {/* Confidence meter */}
        <td style={{ minWidth: 140 }}>
          <ConfBar value={conf} />
        </td>

        {/* Risk */}
        <td>
          <span className={`badge ${s.risk_approved ? 'bg-green' : 'bg-red'}`}>
            {s.risk_approved ? 'APPROVED' : 'BLOCKED'}
          </span>
        </td>

        {/* Expand chevron */}
        <td>
          <span style={{ color: 'var(--text-muted)' }}>{expanded ? '▾' : '▸'}</span>
        </td>
      </tr>

      {/* Expanded reasoning */}
      {expanded && (
        <tr style={{ background: 'rgba(0,240,255,0.02)' }}>
          <td colSpan={8} className="px-4 py-3">
            <p className="text-xs leading-relaxed fade-in" style={{ color: 'var(--text-dim)' }}>
              {s.reasoning}
            </p>
            {s.sources && (
              <div className="mt-2 flex flex-wrap gap-1">
                {(typeof s.sources === 'string'
                  ? s.sources.split(',')
                  : Array.isArray(s.sources) ? s.sources : []
                ).map((src, i) => (
                  <span key={i} className="badge bg-blue text-[10px]">{src.trim()}</span>
                ))}
              </div>
            )}
          </td>
        </tr>
      )}
    </>
  );
}

/* ─── Probability shift row ─── */
function ShiftRow({ s }) {
  const edge = parseFloat(s.edge) || 0;
  const sentImpact = parseFloat(s.sentiment_impact) || 0;
  const title = s.market_title || s.market_id;

  return (
    <tr>
      <td className="whitespace-nowrap">
        <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
          {new Date(s.created_at).toLocaleTimeString()}
        </span>
      </td>

      <td className="max-w-[250px]">
        <a
          href={s.kalshi_url || `https://kalshi.com/search?query=${encodeURIComponent((s.market_id || '').split('-')[0])}`}
          target="_blank"
          rel="noopener noreferrer"
          className="text-sm truncate block hover:underline transition heading"
          style={{ color: 'var(--cyan)' }}
        >
          {title}
        </a>
        <span className="mono text-[10px]" style={{ color: 'var(--text-muted)' }}>
          {s.market_id}
        </span>
      </td>

      <td>
        <span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
          {(parseFloat(s.prior_probability) * 100).toFixed(1)}%
        </span>
      </td>

      <td>
        <span className="mono text-sm font-semibold" style={{ color: sentImpact > 0 ? 'var(--green)' : 'var(--red)' }}>
          {sentImpact > 0 ? '+' : ''}{(sentImpact * 100).toFixed(2)}%
        </span>
      </td>

      <td>
        <span className="mono text-sm" style={{ color: 'var(--orange)' }}>
          {(parseFloat(s.adjusted_probability) * 100).toFixed(1)}%
        </span>
      </td>

      <td>
        <span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
          {(parseFloat(s.market_probability) * 100).toFixed(1)}%
        </span>
      </td>

      <td>
        <span
          className="mono text-sm font-bold"
          style={{ color: edge > 0 ? 'var(--green)' : edge < 0 ? 'var(--red)' : 'var(--text-muted)' }}
        >
          {edge > 0 ? '+' : ''}{(edge * 100).toFixed(1)}%
        </span>
      </td>

      <td>
        <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
          {s.num_articles}
        </span>
      </td>
    </tr>
  );
}

/* ─── Source card ─── */
function SourceCard({ s }) {
  const sent = parseFloat(s.avg_sent) || 0;
  return (
    <div
      className="card flex items-center justify-between"
      style={{ padding: '0.75rem 1rem' }}
    >
      <span className="text-sm font-medium heading">{s.source}</span>
      <div className="flex items-center gap-3">
        <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>{s.cnt} items</span>
        <span
          className="mono text-xs font-bold"
          style={{ color: sent > 0.05 ? 'var(--green)' : sent < -0.05 ? 'var(--red)' : 'var(--text-muted)' }}
        >
          {sent > 0 ? '+' : ''}{(sent * 100).toFixed(0)}%
        </span>
      </div>
    </div>
  );
}

/* ─── Stat card ─── */
function StatCard({ label, value, color }) {
  return (
    <div className="card">
      <p className="stat-label mb-2">{label}</p>
      <p className="stat-v" style={{ color }}>{value}</p>
    </div>
  );
}

/* ─── Tab button ─── */
function TabBtn({ active, onClick, children }) {
  return (
    <button
      onClick={onClick}
      className="btn btn-sm transition"
      style={{
        background: active ? 'var(--cyan-dim)' : 'transparent',
        color: active ? 'var(--cyan)' : 'var(--text-muted)',
        border: active ? '1px solid rgba(0,240,255,0.3)' : '1px solid transparent',
        borderRadius: 'var(--radius-md)',
      }}
    >
      {children}
    </button>
  );
}

/* ─── Main Signals page ─── */
export default function Signals() {
  const [data, setData] = useState(null);
  const [dash, setDash] = useState(null);
  const [loading, setLoading] = useState(true);
  const [tab, setTab] = useState('signals');

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

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

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

  const stats = dash?.stats || {};
  const signals = data?.signals || [];
  const shifts = data?.probabilityShifts || [];
  const sources = dash?.sourceBreakdown || [];

  return (
    <div className="space-y-6 fade-in">

      {/* ── Page Header ── */}
      <div className="flex items-center justify-between">
        <div>
          <div className="flex items-center gap-3">
            {/* Lightning bolt icon */}
            <span className="text-2xl" style={{ color: 'var(--cyan)' }}>⚡</span>
            <h1 className="heading text-3xl font-bold" style={{ color: 'var(--text)' }}>
              Signals
            </h1>
            {/* Live dot */}
            <span className="flex items-center gap-1.5">
              <span
                className="inline-block w-2 h-2 rounded-full pulse-cyan"
                style={{ background: 'var(--cyan)' }}
              />
              <span className="text-xs" style={{ color: 'var(--cyan)' }}>LIVE</span>
            </span>
          </div>
          <p className="section-subtitle mt-1">
            News → Sentiment → Probability → Signals → Risk &nbsp;·&nbsp; {data?.pipeline?.cachedMarkets || 0} markets tracked
          </p>
        </div>
        <button onClick={load} className="btn btn-o btn-sm">↻ Refresh</button>
      </div>

      {/* ── Stats Grid ── */}
      <div className="grid grid-cols-6 gap-3">
        <StatCard label="Signals 24h"    value={stats.total_signals || 0}                                              color="var(--orange)" />
        <StatCard label="Approved"        value={stats.approved_signals || 0}                                           color="var(--green)" />
        <StatCard label="BUY YES"         value={stats.buy_yes || 0}                                                    color="var(--green)" />
        <StatCard label="BUY NO"          value={stats.buy_no || 0}                                                     color="var(--red)" />
        <StatCard label="Avg Edge"        value={stats.avg_edge ? `${(stats.avg_edge * 100).toFixed(1)}%` : '—'}        color="var(--cyan)" />
        <StatCard label="Avg Confidence"  value={stats.avg_confidence ? `${(stats.avg_confidence * 100).toFixed(0)}%` : '—'} color="var(--blue)" />
      </div>

      {/* ── Tab Bar ── */}
      <div
        className="flex gap-2 pb-3"
        style={{ borderBottom: '1px solid var(--border)' }}
      >
        <TabBtn active={tab === 'signals'} onClick={() => setTab('signals')}>
          Strategy Signals ({signals.length})
        </TabBtn>
        <TabBtn active={tab === 'shifts'} onClick={() => setTab('shifts')}>
          Probability Shifts ({shifts.length})
        </TabBtn>
        <TabBtn active={tab === 'sources'} onClick={() => setTab('sources')}>
          Source Breakdown ({sources.length})
        </TabBtn>
      </div>

      {/* ── Strategy Signals Table ── */}
      {tab === 'signals' && (
        <div className="card overflow-x-auto fade-in" style={{ padding: 0 }}>
          <table>
            <thead>
              <tr>
                <th>Time</th>
                <th>Market</th>
                <th>Direction</th>
                <th>Edge</th>
                <th>Size</th>
                <th>Confidence</th>
                <th>Risk</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {signals.length === 0 ? (
                <tr>
                  <td colSpan={8} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
                    <div className="flex flex-col items-center gap-2">
                      <span className="text-2xl">⚡</span>
                      <span className="text-sm">No signals generated yet</span>
                    </div>
                  </td>
                </tr>
              ) : (
                signals.map(s => <SignalRow key={s.id} s={s} />)
              )}
            </tbody>
          </table>
        </div>
      )}

      {/* ── Probability Shifts Table ── */}
      {tab === 'shifts' && (
        <div className="card overflow-x-auto fade-in" style={{ padding: 0 }}>
          <table>
            <thead>
              <tr>
                <th>Time</th>
                <th>Market</th>
                <th>Prior</th>
                <th>Impact</th>
                <th>Adjusted</th>
                <th>Market Price</th>
                <th>Edge</th>
                <th>Articles</th>
              </tr>
            </thead>
            <tbody>
              {shifts.length === 0 ? (
                <tr>
                  <td colSpan={8} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
                    <div className="flex flex-col items-center gap-2">
                      <span className="text-2xl">📊</span>
                      <span className="text-sm">No probability shifts yet</span>
                    </div>
                  </td>
                </tr>
              ) : (
                shifts.map((s, i) => <ShiftRow key={i} s={s} />)
              )}
            </tbody>
          </table>
        </div>
      )}

      {/* ── Source Breakdown ── */}
      {tab === 'sources' && (
        <div className="grid grid-cols-2 gap-4 fade-in">
          {/* Active sources */}
          <div>
            <p className="section-title mb-3" style={{ fontSize: '1rem' }}>
              Active Sources
              <span className="badge bg-cyan ml-2">{sources.length}</span>
            </p>
            <div className="space-y-2">
              {sources.length === 0 ? (
                <p className="text-sm" style={{ color: 'var(--text-muted)' }}>No recent source data</p>
              ) : (
                sources.map((s, i) => <SourceCard key={i} s={s} />)
              )}
            </div>
          </div>

          {/* Pipeline config */}
          <div>
            <p className="section-title mb-3" style={{ fontSize: '1rem' }}>Pipeline Config</p>
            <div className="card space-y-2">
              {[
                { label: 'Market Scan',         value: 'Every 15 min' },
                { label: 'Signal Pipeline',     value: 'Every 5 min' },
                { label: 'Intelligence Sweep',  value: 'Every 30 min' },
                {
                  label: 'Min Edge Threshold',
                  value: `${((data?.pipeline?.riskLimits?.minEdgeThreshold || 0.05) * 100).toFixed(0)}%`,
                  highlight: true,
                },
                {
                  label: 'Min Liquidity',
                  value: `${data?.pipeline?.riskLimits?.minLiquidity || 100} vol`,
                  highlight: true,
                },
                {
                  label: 'Cached Markets',
                  value: data?.pipeline?.cachedMarkets || 0,
                  highlight: true,
                },
              ].map(row => (
                <div
                  key={row.label}
                  className="flex justify-between items-center p-2 rounded"
                  style={{ background: 'var(--surface-el)' }}
                >
                  <span className="text-sm" style={{ color: 'var(--text-muted)' }}>{row.label}</span>
                  <span
                    className="mono text-sm font-semibold"
                    style={{ color: row.highlight ? 'var(--cyan)' : 'var(--text)' }}
                  >
                    {row.value}
                  </span>
                </div>
              ))}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}