import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { LogoMark } from '@/components/brand/Logo' import { TVChart, ChartSkeleton } from '@/components/viz/TVChart' import { LiveFeed } from '@/components/viz/LiveFeed' import { Orchestra } from '@/components/viz/Orchestra' import { AgentWorkflow } from '@/components/viz/AgentWorkflow' import { Onboarding, TOUR_EVENT } from '@/components/Onboarding' import { useScreen } from '@/hooks/useHyperliquid' import { fmtPrice, fmtPct, fmtCompact } from '@/lib/format' import type { Direction, Vote } from '@/lib/signals' const COINS = ['BTC', 'ETH', 'SOL'] const TIMEFRAMES = ['1m', '5m', '15m', '1h', '4h', '1d'] as const const AGENT_META: Record = { TrendAgent: { label: 'Trend', reads: 'EMA 9/21 · RSI' }, BreakoutAgent: { label: 'Breakout', reads: 'Bollinger %B' }, MeanReversionAgent: { label: 'Mean-Rev', reads: 'RSI extremes' }, FundingAgent: { label: 'Funding', reads: 'funding-Z' }, } const AGENT_RULES: Record = { TrendAgent: 'Trend — LONG when EMA9 > EMA21 and RSI > 50; SHORT on the reverse; else SKIP.', BreakoutAgent: 'Breakout — LONG when price pushes the upper Bollinger band (%B > 0.92); SHORT at the lower (< 0.08).', MeanReversionAgent: 'Mean-Rev — fade extremes: LONG when RSI < 32 in the lower band; SHORT when RSI > 68 at the upper.', FundingAgent: 'Funding — fade crowded funding: SHORT when funding-Z > 1.5; LONG when < −1.5.', } const METRIC_INFO: Record = { Mark: 'Mark price — the fair price Hyperliquid uses for PnL & liquidation, not the last trade.', '24h Δ': 'Price change over the last 24 hours.', Funding: 'Funding rate — periodic longs↔shorts payment. Positive = longs pay (crowded longs).', ATR: 'Average True Range — volatility; it scales the verdict TP / SL.', 'Open interest': 'Open interest — total USD value of open perpetual contracts.', '24h volume': '24-hour traded volume, in USD.', 'RSI (14)': 'RSI — momentum 0–100; >70 overbought, <30 oversold. Feeds Mean-Rev.', 'Bollinger %B': 'Where price sits in the Bollinger band (0 = lower, 1 = upper). Feeds Breakout.', 'EMA 9/21': 'Fast vs slow moving average — trend direction. Feeds Trend.', 'Funding-Z': 'Normalised funding skew. Feeds the Funding agent.', 'MACD hist': 'MACD histogram — momentum (MACD line minus its signal).', 'Net vote': 'Aggregate of the 4 agents (LONG +, SHORT −). Drives the verdict.', } const INK = 'var(--color-bone)' const clampN = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n)) const dirColor = (d: Direction) => d === 'LONG' ? 'var(--color-long)' : d === 'SHORT' ? 'var(--color-short)' : 'var(--color-muted)' const dirBg = (d: Direction) => d === 'LONG' ? 'var(--color-long-dim)' : d === 'SHORT' ? 'var(--color-short-dim)' : 'var(--color-track)' const sign = (n: number) => (n >= 0 ? '+' : '') type Inspect = (s: string | null) => void /* ── primitives ───────────────────────────────────────────────────────────── */ function Panel({ title, meta, className = '', children, }: { title: React.ReactNode meta?: React.ReactNode className?: string children: React.ReactNode }) { return (
{title} {meta != null && {meta}}
{children}
) } function Cell({ label, value, tone, sub, bar, barTone, info, onInspect, loading, }: { label: string value: string tone?: string sub?: string bar?: number barTone?: string info?: string onInspect?: Inspect loading?: boolean }) { return (
onInspect(info) : undefined} onMouseLeave={info && onInspect && !loading ? () => onInspect(null) : undefined} >
{label}
{loading ? (
) : (
{value}
)} {sub && !loading &&
{sub}
} {bar != null && !loading && (
)}
) } function DivergeBar({ direction, score }: { direction: Direction; score: number }) { const col = dirColor(direction) return (
{direction === 'LONG' && (
)} {direction === 'SHORT' && (
)} {direction === 'SKIP' && (
)}
) } /* ── command bar ──────────────────────────────────────────────────────────── */ function StatusBar({ live, ago, stale, onReload }: { live: boolean; ago: number | null; stale: boolean; onReload: () => void }) { const [now, setNow] = useState(() => new Date()) const [spin, setSpin] = useState(false) useEffect(() => { const id = setInterval(() => setNow(new Date()), 1000) return () => clearInterval(id) }, []) const reload = () => { onReload() setSpin(true) setTimeout(() => setSpin(false), 600) } const clock = now.toISOString().slice(11, 19) const statusCol = stale ? 'var(--color-accent)' : live ? 'var(--color-long)' : 'var(--color-muted)' const statusText = stale ? 'DATA STALE' : live ? 'LIVE · HL' : 'CONNECTING' return (
Quant Agent Terminal
{statusText} {clock} UTC
) } /* ── compact verdict bar (the call — dense, not a hero) ───────────────────── */ function VerdictBar({ decision, confidence, sizePct, tpPct, slPct, net, votes, reasoning, live, loading }: { decision: Direction; confidence: number; sizePct: number; tpPct: number; slPct: number; net: number; votes: Vote[]; reasoning: string; live: boolean; loading: boolean }) { if (loading) { return (
Verdict
) } const active = votes.filter((v) => v.direction !== 'SKIP') const agree = active.filter((v) => v.direction === decision) const rr = slPct ? tpPct / slPct : 0 const col = dirColor(decision) const Sep = () => const Field = ({ k, v, c }: { k: string; v: string; c?: string }) => ( {k} {v} ) return (
Verdict {decision} 0 ? 'var(--color-long)' : net < 0 ? 'var(--color-short)' : INK} /> {decision === 'SKIP' ? 'no consensus' : `${agree.length}/${active.length} agree`} {!live && ( seed )} {reasoning}
) } /* ── committee (selectable, dense) ────────────────────────────────────────── */ function Committee({ votes, net, activeAgent, onSelect, onHover, loading }: { votes: Vote[]; net: number; activeAgent?: string | null; onSelect?: (a: string | null) => void; onHover?: (a: string | null) => void; loading?: boolean }) { if (loading) { return (
{[0, 1, 2, 3].map((i) => (
))}
) } const active = votes.filter((v) => v.direction !== 'SKIP') const lean = active.length ? clampN(net / active.length, -1, 1) : 0 return (
{votes.map((v) => { const meta = AGENT_META[v.agent] ?? { label: v.agent.replace('Agent', ''), reads: '' } const signal = v.direction !== 'SKIP' const col = dirColor(v.direction) const on = activeAgent === v.agent return ( ) })}
net lean
{lean >= 0 ? (
) : (
)}
{net.toFixed(1)}
) } /* ── help card (opened from the ? in the toolbar) ─────────────────────────── */ const GUIDE = [ { t: 'The verdict', d: "The committee's single call — LONG (up), SHORT (down) or SKIP (no edge), with confidence and the suggested size / TP / SL." }, { t: 'The committee', d: 'Four strategies each vote. The graph shows them feeding the verdict; the list shows each vote and why.' }, { t: 'Interact', d: 'Switch timeframe & coin up top, hover any field to inspect it, click an agent to pin it, ↻ to refresh. Keys: 1/2/3 coin · r refresh · Esc clear.' }, ] function GuideCard({ onClose }: { onClose: () => void }) { return (
How to read this terminal
{GUIDE.map((g, i) => (
{i + 1} {g.t}

{g.d}

))}
) } /* ── page ─────────────────────────────────────────────────────────────────── */ export function Terminal() { const [coin, setCoin] = useState('BTC') const [tf, setTf] = useState('1h') const [selectedAgent, setSelectedAgent] = useState(null) const [hoverAgent, setHoverAgent] = useState(null) const [inspect, setInspect] = useState(null) const [guideOpen, setGuideOpen] = useState(false) const activeAgent = hoverAgent ?? selectedAgent const { data, live, updatedAt, error, reload } = useScreen(coin, tf, 12_000) const ta = data?.ta const v = data?.verdict const stat = data?.stat const votes = data?.votes ?? [] const candles = data?.candles ?? [] const [, setTick] = useState(0) useEffect(() => { const id = setInterval(() => setTick((t) => t + 1), 1000) return () => clearInterval(id) }, []) useEffect(() => { const onKey = (e: KeyboardEvent) => { const el = e.target as HTMLElement | null if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable)) return if (e.metaKey || e.ctrlKey || e.altKey) return if (e.key === '1' || e.key === '2' || e.key === '3') setCoin(COINS[+e.key - 1]) else if (e.key.toLowerCase() === 'r') reload() else if (e.key === 'Escape') { setSelectedAgent(null) setGuideOpen(false) } } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [reload]) const ago = updatedAt ? Math.max(0, Math.round((Date.now() - updatedAt) / 1000)) : null const stale = Boolean(error) || (ago != null && ago > 40) const loading = updatedAt === 0 // no live data for this instrument yet → show skeletons const changePct = stat?.changePct ?? 0 const changeTone = changePct >= 0 ? 'var(--color-long)' : 'var(--color-short)' const funding = stat?.funding ?? 0 const fundingTone = funding >= 0 ? 'var(--color-short)' : 'var(--color-long)' const macdHist = ta?.macdHist ?? 0 const macdTone = macdHist >= 0 ? 'var(--color-long)' : 'var(--color-short)' const emaBull = (ta?.ema9 ?? 0) >= (ta?.ema21 ?? 0) const emaTone = emaBull ? 'var(--color-long)' : 'var(--color-short)' const net = v?.net ?? 0 const netTone = net > 0.001 ? 'var(--color-long)' : net < -0.001 ? 'var(--color-short)' : INK const rsi = ta?.rsi ?? 50 const rsiTone = rsi < 32 ? 'var(--color-long)' : rsi > 68 ? 'var(--color-short)' : INK const decision = (v?.decision ?? 'SKIP') as Direction const marketMeta = live ? 'LIVE · HL' : updatedAt > 0 ? 'STALE' : 'CONNECTING…' // brief live summary per Orchestra status category (live / computed / the call / paper) const orchestraStatus = { live: live ? `$${fmtPrice(stat?.mark ?? ta?.price ?? 0)} · ${sign(changePct)}${changePct.toFixed(2)}%` : updatedAt > 0 ? 'stale feed' : 'connecting…', computed: ta ? `RSI ${rsi.toFixed(0)} · MACD ${macdHist >= 0 ? '▲' : '▼'} · ATR ${(ta.atrPct ?? 0).toFixed(2)}%` : '—', call: `${decision} · conf ${Math.round((v?.confidence ?? 0.5) * 100)}%`, callTone: dirColor(decision), paper: decision === 'SKIP' ? 'no position' : `${decision} ${(v?.sizePct ?? 0).toFixed(1)}%`, } const onAgentHover = (a: string | null) => { setHoverAgent(a) setInspect(a ? AGENT_RULES[a] ?? null : null) } return (
{/* ── command bar — terminal grid: identity · instrument · timeframe ── */}
{/* identity + help */}

{coin}-PERP

${fmtPrice(ta?.price ?? 0)} {sign(changePct)}{changePct.toFixed(2)}%
{/* instrument switch — uniform grid cells */}
Instrument
{COINS.map((c) => { const on = c === coin return ( ) })}
{/* timeframe switch — uniform grid cells */}
Timeframe
{TIMEFRAMES.map((t) => { const on = t === tf return ( ) })}
{guideOpen && setGuideOpen(false)} />} {/* ── workspace · chart with side blocks (moved to top — first) ── */}
{/* LEFT — stats + pipeline */}
= 0 ? '▲' : '▼'} ${macdHist.toFixed(2)}`} tone={macdTone} info={METRIC_INFO['MACD hist']} onInspect={setInspect} loading={loading} />
{/* CENTER — chart (fills the row height; fixed height only when stacked) */}
{updatedAt > 0 ? : }
{/* RIGHT — committee */}
{/* ── the call (compact) ── */} {/* ── pipeline row · Orchestra (left) ‖ Agent flow graph (right) ── */}
{/* ── live feed · the agent chain streaming in real time ── */} {/* ── inspector / status footer ── */}
Inspect {inspect ?? 'Hover any field, agent or pipeline stage to inspect it.'}
) }