diff --git a/app/src/components/ConfluencePanel.tsx b/app/src/components/ConfluencePanel.tsx index b1af320..03018b1 100644 --- a/app/src/components/ConfluencePanel.tsx +++ b/app/src/components/ConfluencePanel.tsx @@ -1,8 +1,9 @@ 'use client'; -import { useState, useEffect } from 'react'; +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'; // --------------------------------------------------------------------------- // Types (subset of server types for display) @@ -44,9 +45,15 @@ const BODY_BADGE: Record = { export function ConfluencePanel() { const activeSymbol = useActiveSymbol((s) => s.activeSymbol); + const setActiveSymbol = useActiveSymbol((s) => s.setActive); const [symbol, setSymbol] = useState(activeSymbol); const [selectedRackId, setSelectedRackId] = useState(null); + const pickSymbol = useCallback((sym: string) => { + setSymbol(sym); + setActiveSymbol(sym); + }, [setActiveSymbol]); + // Racks const [racks, setRacks] = useState<{ system: Array<{ id: string; name: string }>; user: Array<{ id: string; name: string }> }>({ system: [], user: [] }); useEffect(() => { @@ -80,6 +87,18 @@ export function ConfluencePanel() { .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 }>) ?? []; @@ -89,16 +108,24 @@ export function ConfluencePanel() { return (
+ {/* Explanatory text */} +
+ Slot Picture 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())} value={symbol} - onChange={(e) => setSymbol(e.target.value.toUpperCase().trim())} - className="w-24 rounded bg-bg-secondary border border-fg-muted/20 px-2 py-1.5 text-sm text-fg" - maxLength={12} + placeholder="Search symbol…" + ariaLabel="Select symbol for slot picture" />
@@ -140,7 +167,16 @@ export function ConfluencePanel() { )}
) : evaluation !== null ? ( -
No evaluations yet for {symbol}. Data will appear once the confluence evaluator runs.
+
+ No evaluations yet for {symbol}. + +
) : null} {/* Slot evidence grid */} diff --git a/app/src/components/CorridorPanel.tsx b/app/src/components/CorridorPanel.tsx index 1d48c46..f40ef0e 100644 --- a/app/src/components/CorridorPanel.tsx +++ b/app/src/components/CorridorPanel.tsx @@ -3,6 +3,7 @@ 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'; // --------------------------------------------------------------------------- // Types (subset of server types for display) @@ -83,12 +84,14 @@ const price = (v: number | null | undefined): string => export function CorridorPanel() { const activeSymbol = useActiveSymbol((s) => s.activeSymbol); + const setActiveSymbol = useActiveSymbol((s) => s.setActive); const [symbol, setSymbol] = useState(activeSymbol); const [data, setData] = useState(null); const [watchlist, setWatchlist] = useState([]); const [addInput, setAddInput] = useState(''); const [backtest, setBacktest] = useState<{ rows: BacktestRow[]; aggregate: BacktestAggregate } | null>(null); const [running, setRunning] = useState(false); + const [evaluating, setEvaluating] = useState(false); const loadCorridor = useCallback((sym: string) => { if (!sym) return; @@ -111,6 +114,11 @@ export function CorridorPanel() { .catch(() => {}); }, []); + const pickSymbol = useCallback((sym: string) => { + setSymbol(sym); + setActiveSymbol(sym); + }, [setActiveSymbol]); + const addToWatchlist = async () => { const s = addInput.trim().toUpperCase(); if (!s) return; @@ -134,6 +142,16 @@ export function CorridorPanel() { } }; + const runEvaluation = async () => { + setEvaluating(true); + try { + await api.confluence.runEvaluationNow({ symbol }); + loadCorridor(symbol); + } finally { + setEvaluating(false); + } + }; + const c = data?.corridor; const currentPE = c?.currentPE ?? null; const pctPosition = c?.pePercentile1y ?? null; @@ -148,16 +166,24 @@ export function CorridorPanel() { return (
+ {/* Explanatory text */} +
+ Price Corridor maps a stock's current P/E ratio against its 1-year historical range. + The green zone (below 33rd percentile) marks entries where valuation has been attractively low; + the amber zone (above 67th) marks stretched valuations. + Fair value is derived from the median P/E over the lookback window applied to trailing EPS. +
+ {/* Header: symbol + watchlist */}
-
+
- pickSymbol(raw.toUpperCase())} value={symbol} - onChange={(e) => setSymbol(e.target.value.toUpperCase().trim())} - className="w-24 rounded bg-bg-secondary border border-fg-muted/20 px-2 py-1.5 text-sm text-fg" - maxLength={12} + placeholder="Search symbol…" + ariaLabel="Select symbol for corridor" />
@@ -183,7 +209,7 @@ export function CorridorPanel() {
{watchlist.map((s) => ( - + ))} @@ -260,8 +286,15 @@ export function CorridorPanel() { )} {data && !data.snapshot && ( -
- No corridor snapshot for {symbol} yet. Snapshot it via the confluence evaluator, or pick a symbol from the watchlist. +
+ No corridor snapshot for {symbol} yet. +
)}