← 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 · {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>
);
}