import { useState, useEffect, useCallback } from 'react' import clsx from 'clsx' import { RefreshCw, TrendingUp, TrendingDown, Minus } from 'lucide-react' import { ComposedChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Area, } from 'recharts' const API = '' // ── Types ───────────────────────────────────────────────────────────────────── interface SeriesMeta { id: string name: string category: string freq: string unit: string assets: string[] } interface TSPoint { event_date: string actual_value: number | null surprise_zscore: number | null surprise_direction: string | null } interface FFEvent { event_date: string event_time: string | null event_name: string actual_value: string | null forecast_value: string | null previous_value: string | null impact: string } interface SeriesHistory { series_id: string name: string unit: string category: string freq: string timeseries: TSPoint[] events: FFEvent[] } // ── Constants ───────────────────────────────────────────────────────────────── const CATEGORY_COLORS: Record = { employment: '#3b82f6', inflation: '#f59e0b', growth: '#10b981', monetary: '#8b5cf6', credit: '#ef4444', rates: '#06b6d4', } const CATEGORY_LABELS: Record = { employment: 'Employment', inflation: 'Inflation', growth: 'Growth', monetary: 'Monetary', credit: 'Credit', rates: 'Rates', } const RANGE_OPTIONS = [ { key: '1y', label: '1Y', months: 12 }, { key: '2y', label: '2Y', months: 24 }, { key: '5y', label: '5Y', months: 60 }, { key: '10y', label: '10Y', months: 120 }, { key: 'all', label: 'All', months: 999 }, ] // ── Helpers ─────────────────────────────────────────────────────────────────── function fromDateForRange(months: number): string { if (months >= 999) return '2007-01-01' const d = new Date() d.setMonth(d.getMonth() - months) return d.toISOString().slice(0, 10) } function fmtDate(s: string): string { return s.slice(0, 7) // YYYY-MM } function fmtVal(v: number | null, unit: string): string { if (v === null) return '—' const fixed = Math.abs(v) < 10 ? 2 : 1 return `${v.toFixed(fixed)} ${unit}` } // ── Custom Tooltip ──────────────────────────────────────────────────────────── interface TooltipPayload { event_date: string value: number | null zscore: number | null direction: string | null ff_actual?: string ff_forecast?: string ff_event?: string } function ChartTooltip({ active, payload, unit }: { active?: boolean payload?: Array<{ payload: TooltipPayload }> unit: string }) { if (!active || !payload?.length) return null const d = payload[0].payload return (
{d.event_date}
{d.value !== null ? `${d.value?.toFixed(2)} ${unit}` : '—'}
{d.zscore !== null && (
z-score: {d.zscore?.toFixed(2)}
)} {d.ff_event && (
FF Release
{d.ff_event}
{d.ff_forecast &&
Forecast: {d.ff_forecast}
} {d.ff_actual &&
Actual: {d.ff_actual}
}
)}
) } // ── Direction Icon ───────────────────────────────────────────────────────────── function DirIcon({ dir }: { dir: string | null }) { if (dir === 'bullish') return if (dir === 'bearish') return return } // ── Series Sidebar ───────────────────────────────────────────────────────────── function SeriesList({ series, selected, onSelect, }: { series: SeriesMeta[] selected: string onSelect: (id: string) => void }) { const grouped = series.reduce>((acc, s) => { if (!acc[s.category]) acc[s.category] = [] acc[s.category].push(s) return acc }, {}) return (
{Object.entries(grouped).map(([cat, items]) => (
{CATEGORY_LABELS[cat] ?? cat}
{items.map(s => ( ))}
))}
) } // ── Events Table ────────────────────────────────────────────────────────────── function EventsTable({ events, unit }: { events: FFEvent[]; unit: string }) { const recent = [...events].reverse().slice(0, 30) if (!recent.length) return (
No FF events linked to this series yet.
Import the CSV and run the bootstrap first.
) return (
{recent.map((ev, i) => { const aNum = parseFloat(ev.actual_value ?? '') const fNum = parseFloat(ev.forecast_value ?? '') const beat = !isNaN(aNum) && !isNaN(fNum) && aNum !== fNum const better = beat && aNum > fNum return ( ) })}
Date Event Actual Forecast Previous
{ev.event_date} {ev.event_name} {ev.actual_value ?? '—'} {ev.forecast_value ?? '—'} {ev.previous_value ?? '—'}
) } // ── Main Page ───────────────────────────────────────────────────────────────── export default function MacroSeriesPage() { const [seriesList, setSeriesList] = useState([]) const [selectedId, setSelectedId] = useState('PAYEMS') const [range, setRange] = useState('5y') const [history, setHistory] = useState(null) const [loading, setLoading] = useState(false) // Load series catalog useEffect(() => { fetch(`${API}/api/eco/series`).then(r => r.json()).then(setSeriesList).catch(() => {}) }, []) const loadHistory = useCallback(async () => { if (!selectedId) return setLoading(true) try { const months = RANGE_OPTIONS.find(r => r.key === range)?.months ?? 60 const from = fromDateForRange(months) const r: SeriesHistory = await fetch( `${API}/api/eco/series/${selectedId}/history?from_date=${from}` ).then(x => x.json()) setHistory(r) } catch {} finally { setLoading(false) } }, [selectedId, range]) useEffect(() => { loadHistory() }, [loadHistory]) // Build chart data — merge FRED time series + FF events by date const chartData = (() => { if (!history) return [] const ffByDate = new Map() for (const ev of history.events) ffByDate.set(ev.event_date, ev) return history.timeseries.map(pt => { const ff = ffByDate.get(pt.event_date) return { event_date: pt.event_date, label: fmtDate(pt.event_date), value: pt.actual_value, zscore: pt.surprise_zscore, direction: pt.surprise_direction, ff_event: ff?.event_name ?? null, ff_actual: ff?.actual_value ?? null, ff_forecast:ff?.forecast_value ?? null, // For reference lines — mark surprise events is_surprise: pt.surprise_zscore !== null && Math.abs(pt.surprise_zscore) >= 1.5, } }) })() const color = CATEGORY_COLORS[history?.category ?? ''] ?? '#3b82f6' // Stats const values = chartData.map(d => d.value).filter((v): v is number => v !== null) const latest = values[values.length - 1] ?? null const prev = values[values.length - 2] ?? null const minVal = values.length ? Math.min(...values) : null const maxVal = values.length ? Math.max(...values) : null const trend = latest !== null && prev !== null ? (latest > prev ? 1 : latest < prev ? -1 : 0) : 0 // Big surprises for reference lines const bigSurprises = chartData.filter(d => d.is_surprise) return (
{/* Sidebar */}
FRED Series
{ setSelectedId(id); setHistory(null) }} />
{/* Main content */}
{/* Header */}

{history?.name ?? selectedId}

{history?.category && ( {CATEGORY_LABELS[history.category]} )} {history?.freq} · FRED/{selectedId}
{/* Range selector */}
{RANGE_OPTIONS.map(r => ( ))}
{/* KPI cards */} {history && values.length > 0 && (
{[ { label: 'Latest', value: fmtVal(latest, history.unit), sub: history.timeseries[history.timeseries.length - 1]?.event_date, accent: trend === 1 ? 'text-emerald-400' : trend === -1 ? 'text-red-400' : 'text-slate-200', icon: trend === 1 ? : trend === -1 ? : , }, { label: 'Previous', value: fmtVal(prev, history.unit), sub: history.timeseries[history.timeseries.length - 2]?.event_date, accent: 'text-slate-300', icon: null }, { label: 'Min (period)', value: fmtVal(minVal, history.unit), sub: null, accent: 'text-sky-400', icon: null }, { label: 'Max (period)', value: fmtVal(maxVal, history.unit), sub: null, accent: 'text-amber-400', icon: null }, ].map(card => (
{card.label}
{card.icon}{card.value}
{card.sub &&
{card.sub}
}
))}
)} {/* Chart */}
Time Series {history && ({chartData.length} data points)} {bigSurprises.length > 0 && ( {bigSurprises.length} big surprises (|z| ≥ 1.5) )}
{loading && (
Loading…
)} {!loading && chartData.length === 0 && (
No data — run FRED bootstrap first in the Calendar page.
)} {!loading && chartData.length > 0 && ( `${v}${history?.unit === '%' || history?.unit === 'pp' ? '' : ''}`} /> } /> {/* Zero line if series crosses zero */} {minVal !== null && maxVal !== null && minVal < 0 && maxVal > 0 && ( )} {/* Big surprise markers */} {bigSurprises.map(s => ( ))} )} {/* Legend for reference lines */} {bigSurprises.length > 0 && (
Bullish surprise (|z|≥1.5) Bearish surprise
)}
{/* Events table */}
FF Calendar Events {history && ({history.events.length} linked)}
{history && }
{/* Z-score context */} {history && chartData.some(d => d.zscore !== null) && (
Recent Surprises (FRED z-score)
{chartData .filter(d => d.zscore !== null && d.zscore !== 0) .slice(-15) .reverse() .map((d, i) => (
{d.event_date}
{d.zscore!.toFixed(2)}σ {d.value?.toFixed(2)} {history.unit}
))}
)}
) }