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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user