'use client'; import { useState, useEffect, useCallback } from 'react'; import { api } from '@/lib/trpc'; import { useActiveSymbol } from '@/stores/active-symbol-store'; import { CollapsibleSection } from '@/components/CollapsibleSection'; import { SymbolAutocomplete } from '@/components/SymbolAutocomplete'; import { ConfluenceChart } from '@/components/ConfluenceChart'; // --------------------------------------------------------------------------- // Types (subset of server types for display) // --------------------------------------------------------------------------- type Quality = 'strong-bullish' | 'moderate-bullish' | 'weak-bullish' | 'mixed' | 'weak-bearish' | 'moderate-bearish' | 'strong-bearish' | 'sparse'; const QUALITY_COLOR: Record = { 'strong-bullish': 'bg-green-600/20 text-green-300 border-green-600/40', 'moderate-bullish': 'bg-green-600/10 text-green-400 border-green-600/20', 'weak-bullish': 'bg-green-600/5 text-green-500 border-green-600/10', 'mixed': 'bg-zinc-700/30 text-zinc-300 border-zinc-600/30', 'weak-bearish': 'bg-red-600/5 text-red-500 border-red-600/10', 'moderate-bearish': 'bg-red-600/10 text-red-400 border-red-600/20', 'strong-bearish': 'bg-red-600/20 text-red-300 border-red-600/40', 'sparse': 'bg-zinc-800/50 text-zinc-500 border-zinc-700/30', }; const QUALITY_LABEL: Record = { 'strong-bullish': 'Strong Bullish', 'moderate-bullish': 'Moderate Bullish', 'weak-bullish': 'Weak Bullish', 'mixed': 'Mixed', 'weak-bearish': 'Weak Bearish', 'moderate-bearish': 'Moderate Bearish', 'strong-bearish': 'Strong Bearish', 'sparse': 'Sparse Data', }; const BODY_BADGE: Record = { bull: 'bg-green-600/15 text-green-400', bear: 'bg-red-600/15 text-red-400', exit: 'bg-amber-600/15 text-amber-400', }; // --------------------------------------------------------------------------- // Panel // --------------------------------------------------------------------------- export function ConfluencePanel() { const activeSymbol = useActiveSymbol((s) => s.activeSymbol); const setActiveSymbol = useActiveSymbol((s) => s.setActive); const [symbol, setSymbol] = useState(activeSymbol); const [searchQuery, setSearchQuery] = useState(activeSymbol); const [selectedRackId, setSelectedRackId] = useState(null); const pickSymbol = useCallback((sym: string) => { setSymbol(sym); setSearchQuery(sym); setActiveSymbol(sym); }, [setActiveSymbol]); // Sync searchQuery when activeSymbol changes externally (e.g., from another panel) useEffect(() => { setSearchQuery(activeSymbol); }, [activeSymbol]); // Racks const [racks, setRacks] = useState<{ system: Array<{ id: string; name: string }>; user: Array<{ id: string; name: string }> }>({ system: [], user: [] }); useEffect(() => { api.confluence.racks().then(setRacks).catch(() => {}); }, []); // Evaluation const [evaluation, setEvaluation] = useState | null>(null); const rackId = selectedRackId ?? racks.system[0]?.id ?? null; useEffect(() => { if (!symbol) return; api.confluence.evaluation(symbol, { rackId: rackId ?? undefined, limit: 6 }) .then((r) => setEvaluation(r as Record)) .catch(() => setEvaluation(null)); }, [symbol, rackId]); // Scorecard const [scorecard, setScorecard] = useState<{ slots: Array>; note: string } | null>(null); useEffect(() => { if (!symbol) return; api.confluence.scorecard(symbol) .then((r) => setScorecard(r as { slots: Array>; note: string })) .catch(() => setScorecard(null)); }, [symbol]); // Slots const [slotMeta, setSlotMeta] = useState>(new Map()); useEffect(() => { api.confluence.slots() .then((r) => setSlotMeta(new Map(r.slots.map((s) => [s.id, { name: s.name, body: s.body, explain: s.explain }])))) .catch(() => {}); }, []); const [evaluating, setEvaluating] = useState(false); const runEvaluation = async () => { setEvaluating(true); try { await api.confluence.runEvaluationNow({ symbol }); window.location.reload(); } finally { setEvaluating(false); } }; const latest = (evaluation?.latest as Record) ?? null; const quality = (latest?.quality as Quality) ?? null; const assessments = (latest?.assessments as Array<{ id: string; state: string; note?: string }>) ?? []; const bullEvidence = Number(latest?.bullEvidence ?? 0); const bearEvidence = Number(latest?.bearEvidence ?? 0); const change = evaluation?.change as { changed: boolean; changeType: string; previous: string | null } | null; return (
{/* Explanatory text */}
Signal Confluence combines technical, seasonal, and valuation signals into a single directional read. Each slot fires (green dot) when its condition is met, or stays dormant. The overall quality grade reflects how many slots fired and in which direction. Use the rack selector to switch between different signal subsets.
{/* Header */}
pickSymbol(raw.toUpperCase())} placeholder="Search symbol…" ariaLabel="Select symbol for signal confluence" inputClassName="w-full bg-surface-sunken border border-line rounded-lg px-3 py-1.5 text-xs text-fg placeholder:text-fg-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
{/* Quality + evidence summary */} {quality ? (
{QUALITY_LABEL[quality]}
Bull {bullEvidence.toFixed(1)} {' '}·{' '} Bear {bearEvidence.toFixed(1)} {' '}·{' '} Net = 0 ? 'text-green-400' : 'text-red-400'}> {(bullEvidence - bearEvidence).toFixed(1)}
{change?.changed && (
{change.changeType === 'improved' ? 'Improved' : 'Deteriorated'} from {change.previous ?? '?'}
)}
) : evaluation !== null ? (
No evaluations yet for {symbol}.
) : null} {/* Signal Timeline chart */} {symbol && ( )} {/* Slot evidence grid */} {assessments.length > 0 && (
{assessments.map((a) => { const meta = slotMeta.get(a.id); const fired = a.state === 'fired'; return (
{meta?.name ?? a.id} {meta?.body ?? '?'}
{a.note &&
{a.note}
}
); })}
)} {/* Scorecard */} {scorecard && scorecard.slots.length > 0 && (

{scorecard.note}

{scorecard.slots .sort((a, b) => ((b.hitRate as number) ?? 0) - ((a.hitRate as number) ?? 0)) .map((s) => ( ))}
Slot Fires Hit Rate Reliability
{slotMeta.get(s.slotId as string)?.name ?? (s.slotId as string)} {s.fires as number} {s.hitRate != null ? `${((s.hitRate as number) * 100).toFixed(0)}%` : '-'} {(s.reliabilityWeight as number).toFixed(2)}
)}
); }