'use client'; import { useEffect, useState } from 'react'; import { useActiveSymbol } from '@/stores/active-symbol-store'; import { api, type Snapshot } from '@/lib/trpc'; /** * Market Outlook Panel — macro analysis and sector context for the active symbol. * Shows sector/industry info, ticker kind classification, and peer context. * Educational display only — no trade recommendations. */ interface SectorData { symbol: string; name: string | null; sector: string | null; industry: string | null; exchange: string | null; tickerKind: string; peers: string[] | null; } export function MarketOutlookPanel() { const symbol = useActiveSymbol((s) => s.activeSymbol); const [snap, setSnap] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); setError(null); api.market.snapshot(symbol) .then((s) => { if (!cancelled) { setSnap(s); setLoading(false); } }) .catch((e) => { if (!cancelled) { setError(e instanceof Error ? e.message : 'request failed'); setLoading(false); } }); return () => { cancelled = true; }; }, [symbol]); const sector: SectorData | null = snap?.sector ?? null; const quote = snap?.quote ?? null; const candles = snap?.candles ?? []; // Simple trend estimation from recent candles const trend = candles.length >= 20 ? (() => { const recent20 = candles.slice(-20); const firstHalf = recent20.slice(0, 10).reduce((s, c) => s + c.c, 0) / 10; const secondHalf = recent20.slice(10).reduce((s, c) => s + c.c, 0) / 10; const pctChange = ((secondHalf - firstHalf) / firstHalf) * 100; if (pctChange > 3) return { label: 'Trending Up', color: 'text-[#34d399]', icon: '▲' }; if (pctChange < -3) return { label: 'Trending Down', color: 'text-[#ef4444]', icon: '▼' }; return { label: 'Range-Bound', color: 'text-[#fbbf24]', icon: '—' }; })() : null; // Volatility estimate from recent candles const volatility = candles.length >= 20 ? (() => { const returns = candles.slice(-20).map((c, i, arr) => i > 0 ? (c.c - arr[i - 1].c) / arr[i - 1].c : 0).slice(1); const mean = returns.reduce((a, b) => a + b, 0) / returns.length; const variance = returns.reduce((s, r) => s + (r - mean) ** 2, 0) / returns.length; const dailyVol = Math.sqrt(variance); const annualizedVol = dailyVol * Math.sqrt(252) * 100; if (annualizedVol > 50) return { label: 'High', color: 'text-[#ef4444]', value: annualizedVol.toFixed(0) }; if (annualizedVol > 25) return { label: 'Moderate', color: 'text-[#fbbf24]', value: annualizedVol.toFixed(0) }; return { label: 'Low', color: 'text-[#34d399]', value: annualizedVol.toFixed(0) }; })() : null; if (loading) { return (

Market Outlook

Loading market data…

); } if (error) { return (

Market Outlook

{error}

); } return (

Market Outlook

{symbol}
{/* Sector & Industry */} {sector && (

Sector

{sector.sector ?? '—'}

Industry

{sector.industry ?? '—'}

Type

{sector.tickerKind}

Exchange

{sector.exchange ?? '—'}

)} {/* Trend & Volatility */}
{trend && (

20-Day Trend

{trend.label}

Based on the average closing price of the first 10 vs last 10 of the most recent 20 trading days.

)} {volatility && (

Volatility (Annualized)

{volatility.value}% {volatility.label}

Derived from daily returns over the last 20 trading days, annualized (×√252).

)}
{/* Peers */} {sector?.peers && sector.peers.length > 0 && (

Peers

{sector.peers.map((peer) => ( {peer} ))}
)} {/* Educational note */}

This panel provides sector and statistical context for the active symbol. Trend and volatility estimates are based on recent price history and are for educational reference only.

); }