fix: backfill symbol_demand for sidebar-added symbols + analyst ratings schema fix

- Add await ctx.cache.subscribe() to addSymbol mutation so symbols
  added via the sidebar get registered in symbol_demand and yfinance
  jobs are queued immediately
- Backfill PEP, WYNN, STZ, CELH into symbol_demand + adapter_queue
- Upgrade yahoo-finance2 3.15.3 -> 3.15.4 and pass validateResult:false
  to quoteSummary() to handle Yahoo schema drift
- Add error detail logging for analyst ratings schema failures
- Update .gitignore with common ignores
This commit is contained in:
Investor Flow Build
2026-07-23 18:02:24 -04:00
parent 5ef2b2f060
commit e262187c3c
204 changed files with 25014 additions and 2934 deletions
+126 -146
View File
@@ -1,164 +1,144 @@
'use client';
import { useEffect, useState } from 'react';
import { useActiveSymbol } from '@/stores/active-symbol-store';
import { api, type Snapshot } from '@/lib/trpc';
"use client";
import { useMarketOutlookSections, visibleCount } from "@/stores/market-outlook-sections-store";
import { HeavyTruckSalesChart } from "./HeavyTruckSalesChart";
import { ManufacturingPmiChart } from "./ManufacturingPmiChart";
import { MarketRotationPanel } from "./MarketRotationPanel";
import { RotationSignalAlert } from "./RotationSignalAlert";
import { MacroCommentaryPanel } from "./MacroCommentaryPanel";
import { MarketConditionStrip } from "./MarketConditionStrip";
import { SeasonalityPanel } from "./SeasonalityPanel";
import { InfoTooltip } from "./InfoTooltip";
import { SectionCollapse } from "./SectionCollapse";
/**
* 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.
* Market Outlook — market condition, relative leadership, seasonality, macro context.
*/
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<Snapshot | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<section className="rounded-xl border border-[#2a2b3a] bg-[#121318] p-5 hover:shadow-[0_2px_8px_rgba(79,140,255,0.1)] transition-shadow duration-300">
<h2 className="text-base font-semibold text-[#e6e7ec]">Market Outlook</h2>
<p className="text-sm text-[#5a5b6a] py-8">Loading market data…</p>
</section>
);
}
if (error) {
return (
<section className="rounded-xl border border-[#2a2b3a] bg-[#121318] p-5 hover:shadow-[0_2px_8px_rgba(79,140,255,0.1)] transition-shadow duration-300">
<h2 className="text-base font-semibold text-[#e6e7ec]">Market Outlook</h2>
<p className="text-sm text-[#ef4444] py-8">{error}</p>
</section>
);
}
const open = useMarketOutlookSections((s) => s.open);
const expandAll = useMarketOutlookSections((s) => s.expandAll);
const collapseAll = useMarketOutlookSections((s) => s.collapseAll);
const numVisible = visibleCount(open);
return (
<section className="rounded-xl border border-[#2a2b3a] bg-[#121318] p-5 space-y-5 hover:shadow-[0_2px_8px_rgba(79,140,255,0.1)] transition-shadow duration-300">
<div className="space-y-6">
<header className="flex items-baseline justify-between gap-4">
<h2 className="text-base font-semibold text-[#e6e7ec]">Market Outlook</h2>
<span className="text-xs text-[#8a8b9a]">{symbol}</span>
<div>
<h1 className="text-xl font-bold text-fg">Market Outlook</h1>
<p className="text-sm text-fg-faint mt-1">
Market regime, relative sector leadership, seasonal context, and macro indicators.
</p>
</div>
<div className="flex items-center gap-3">
<span className="text-[11px] text-fg-muted tabular-nums">{numVisible}/5 sections open</span>
<button
type="button"
onClick={numVisible < 5 ? expandAll : collapseAll}
className="text-[11px] text-link hover:underline"
>
{numVisible < 5 ? "Expand all" : "Collapse all"}
</button>
</div>
</header>
{/* Sector & Industry */}
{sector && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="rounded-lg bg-[#1a1b24] border border-[#2a2b3a] p-3">
<p className="text-[10px] uppercase tracking-wider text-[#5a5b6a] mb-1">Sector</p>
<p className="text-sm font-semibold text-[#e6e7ec]">{sector.sector ?? '—'}</p>
</div>
<div className="rounded-lg bg-[#1a1b24] border border-[#2a2b3a] p-3">
<p className="text-[10px] uppercase tracking-wider text-[#5a5b6a] mb-1">Industry</p>
<p className="text-sm font-semibold text-[#e6e7ec]">{sector.industry ?? '—'}</p>
</div>
<div className="rounded-lg bg-[#1a1b24] border border-[#2a2b3a] p-3">
<p className="text-[10px] uppercase tracking-wider text-[#5a5b6a] mb-1">Type</p>
<p className="text-sm font-semibold text-[#e6e7ec] capitalize">{sector.tickerKind}</p>
</div>
<div className="rounded-lg bg-[#1a1b24] border border-[#2a2b3a] p-3">
<p className="text-[10px] uppercase tracking-wider text-[#5a5b6a] mb-1">Exchange</p>
<p className="text-sm font-semibold text-[#e6e7ec]">{sector.exchange ?? '—'}</p>
<MarketConditionStrip />
<SectionCollapse
id="rotation"
title="Sector & style leadership"
info={
<InfoTooltip>
<strong>What it is:</strong> Relative performance of sector and style ETFs versus the broad market (SPY).<br />
<strong>Why it matters:</strong> Shows where strength is concentrating across the market.<br />
<strong>How to read it:</strong> Positive relative return means outperformance versus SPY; negative means underperformance. Start with the one-month column.
</InfoTooltip>
}
>
<div className="space-y-4">
<p className="text-[11px] text-fg-muted">
Values are relative to the broad market (SPY), not absolute returns and not measured fund flows.
</p>
<MarketRotationPanel />
<div className="border-t border-line pt-3">
<p className="text-xs font-medium text-fg mb-2">Leadership alerts</p>
<RotationSignalAlert />
</div>
</div>
)}
</SectionCollapse>
{/* Trend & Volatility */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{trend && (
<div className="rounded-lg bg-[#1a1b24] border border-[#2a2b3a] p-4">
<p className="text-[10px] uppercase tracking-wider text-[#5a5b6a] mb-2">20-Day Trend</p>
<div className="flex items-center gap-2">
<span className={`text-lg ${trend.color}`} aria-hidden="true">{trend.icon}</span>
<span className={`text-sm font-semibold ${trend.color}`}>{trend.label}</span>
</div>
<p className="text-[11px] text-[#5a5b6a] mt-2">
Based on the average closing price of the first 10 vs last 10 of the most recent 20 trading days.
</p>
</div>
)}
<SectionCollapse
id="seasonality"
title="Seasonality & calendar"
info={
<InfoTooltip>
<strong>What it is:</strong> Historical monthly patterns and recurring high-attention market dates.<br />
<strong>Why it matters:</strong> Certain windows (Fed decisions, jobs, inflation prints) tend to raise volatility.<br />
<strong>How to read it:</strong> Historical averages are context only; any given year can diverge.
</InfoTooltip>
}
>
<SeasonalityPanel />
</SectionCollapse>
{volatility && (
<div className="rounded-lg bg-[#1a1b24] border border-[#2a2b3a] p-4">
<p className="text-[10px] uppercase tracking-wider text-[#5a5b6a] mb-2">Volatility (Annualized)</p>
<div className="flex items-baseline gap-2">
<span className={`text-2xl font-bold font-mono ${volatility.color}`}>{volatility.value}%</span>
<span className={`text-xs ${volatility.color}`}>{volatility.label}</span>
</div>
<p className="text-[11px] text-[#5a5b6a] mt-2">
Derived from daily returns over the last 20 trading days, annualized (×√252).
</p>
</div>
)}
</div>
{/* Peers */}
{sector?.peers && sector.peers.length > 0 && (
<div>
<p className="text-[10px] uppercase tracking-wider text-[#5a5b6a] mb-2">Peers</p>
<div className="flex flex-wrap gap-1.5">
{sector.peers.map((peer) => (
<span key={peer} className="px-2 py-1 rounded-lg bg-[#1a1b24] border border-[#2a2b3a] text-xs text-[#8a8b9a] font-mono">
{peer}
</span>
))}
</div>
<SectionCollapse id="peers" title="Macro commentary">
<div className="space-y-3">
<p className="text-[11px] text-fg-muted">
Near-term and structural commentary from public economic data (growth, inflation, labor, rates).
</p>
<MacroCommentaryPanel />
</div>
)}
</SectionCollapse>
{/* Educational note */}
<div className="pt-3 border-t border-[#2a2b3a]">
<p className="text-[11px] text-[#5a5b6a]">
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.
<SectionCollapse
id="truck-sales"
title="Heavy truck sales"
info={
<InfoTooltip>
<strong>What it is:</strong> Monthly US heavy truck sales (FRED HTRUCKSSAAR).<br />
<strong>Why it matters:</strong> A real-economy activity gauge that often turns before broader slowdowns or recoveries.<br />
<strong>How to read it:</strong> Focus on multi-month direction rather than a single print.
</InfoTooltip>
}
>
<div className="space-y-4">
<p className="text-[11px] text-fg-muted">
Source: FRED (
<a href="https://fred.stlouisfed.org/series/HTRUCKSSAAR" target="_blank" rel="noopener noreferrer" className="text-link underline">
HTRUCKSSAAR
</a>
).
</p>
<HeavyTruckSalesChart />
</div>
</SectionCollapse>
<SectionCollapse
id="manufacturing-pmi"
title="Manufacturing output"
info={
<InfoTooltip>
<strong>What it is:</strong> Federal Reserve industrial production: manufacturing (IPMAN).<br />
<strong>Why it matters:</strong> Hard output measure of the goods-producing economy.<br />
<strong>How to read it:</strong> Multi-month direction indicates expansion or contraction in factory activity.
</InfoTooltip>
}
>
<div className="space-y-4">
<p className="text-[11px] text-fg-muted">
Source: FRED (
<a href="https://fred.stlouisfed.org/series/IPMAN" target="_blank" rel="noopener noreferrer" className="text-link underline">
IPMAN
</a>
).
</p>
<ManufacturingPmiChart />
</div>
</SectionCollapse>
<footer className="pt-3 border-t border-line">
<p className="text-[11px] text-fg-muted">
Educational analysis, not investment advice. Verify the underlying data; you are responsible for your own decisions.
</p>
</div>
</section>
</footer>
</div>
);
}