feat: 4 remaining institutional reports — Earnings, VX curve, Central Bank RSS, Sentiment
New fetchers (no API keys required): - earnings_fetcher.py: yfinance EPS calendar + surprise tracking for 23 geo-relevant tickers - vx_fetcher.py: VIX term structure (^VIX/^VXV/^VXMT) + CBOE delayed futures, regime detection - central_bank_fetcher.py: Fed + ECB RSS feeds, keyword-based hawkish/dovish classification - sentiment_fetcher.py: CNN Fear & Greed (primary) + NAAIM + AAII (optional fallbacks) Wiring: - institutional_scheduler.py: all 4 now scheduled daily (≥08:00 UTC), deduplicated per day - institutional.py /refresh: all 6 types handled with _run() helper - ai_analyzer.py build_institutional_block(): limit 6→12, generic header text - InstitutionalReports.tsx: 6-type color map, individual refresh buttons, expanded filters Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,10 +81,27 @@ function AbsorptionBadge({ score }: { score: number | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
cot: 'bg-purple-900/30 text-purple-300 border-purple-700/40',
|
||||
eia: 'bg-orange-900/30 text-orange-300 border-orange-700/40',
|
||||
earnings: 'bg-blue-900/30 text-blue-300 border-blue-700/40',
|
||||
vx_curve: 'bg-rose-900/30 text-rose-300 border-rose-700/40',
|
||||
central_bank: 'bg-cyan-900/30 text-cyan-300 border-cyan-700/40',
|
||||
sentiment: 'bg-yellow-900/30 text-yellow-300 border-yellow-700/40',
|
||||
}
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
cot: 'COT',
|
||||
eia: 'EIA',
|
||||
earnings: 'Earnings',
|
||||
vx_curve: 'VX Curve',
|
||||
central_bank: 'Central Banks',
|
||||
sentiment: 'Sentiment',
|
||||
}
|
||||
|
||||
function ReportCard({ report }: { report: InstitutionalReport }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const isCot = report.report_type === 'cot'
|
||||
const typeColor = isCot ? 'bg-purple-900/30 text-purple-300 border-purple-700/40' : 'bg-orange-900/30 text-orange-300 border-orange-700/40'
|
||||
const typeColor = TYPE_COLORS[report.report_type] ?? 'bg-slate-800 text-slate-400 border-slate-700/40'
|
||||
|
||||
return (
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-lg overflow-hidden hover:border-slate-600/60 transition-colors">
|
||||
@@ -93,7 +110,7 @@ function ReportCard({ report }: { report: InstitutionalReport }) {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap mb-1.5">
|
||||
<span className={clsx('px-2 py-0.5 rounded border text-xs font-bold uppercase tracking-wider', typeColor)}>
|
||||
{report.report_type}
|
||||
{TYPE_LABELS[report.report_type] ?? report.report_type}
|
||||
</span>
|
||||
<ImportanceStars n={report.importance} />
|
||||
<span className="text-slate-500 text-xs">
|
||||
@@ -214,40 +231,44 @@ export default function InstitutionalReports() {
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">Institutional Reports</h1>
|
||||
<p className="text-slate-400 text-sm mt-0.5">
|
||||
CFTC COT (weekly) + EIA Petroleum (weekly) — positioning & supply signals
|
||||
COT · EIA · Earnings · VX Curve · Central Banks · Sentiment — scheduled daily/weekly
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleRefresh('cot')}
|
||||
disabled={refresh.isPending}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-purple-900/40 text-purple-300 border border-purple-700/40 rounded hover:bg-purple-800/50 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', refresh.isPending && 'animate-spin')} />
|
||||
Refresh COT
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRefresh('eia')}
|
||||
disabled={refresh.isPending}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-orange-900/40 text-orange-300 border border-orange-700/40 rounded hover:bg-orange-800/50 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', refresh.isPending && 'animate-spin')} />
|
||||
Refresh EIA
|
||||
</button>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{([
|
||||
{ type: 'cot', label: 'COT', cls: 'bg-purple-900/40 text-purple-300 border-purple-700/40 hover:bg-purple-800/50' },
|
||||
{ type: 'eia', label: 'EIA', cls: 'bg-orange-900/40 text-orange-300 border-orange-700/40 hover:bg-orange-800/50' },
|
||||
{ type: 'earnings', label: 'Earn.', cls: 'bg-blue-900/40 text-blue-300 border-blue-700/40 hover:bg-blue-800/50' },
|
||||
{ type: 'vx_curve', label: 'VX', cls: 'bg-rose-900/40 text-rose-300 border-rose-700/40 hover:bg-rose-800/50' },
|
||||
{ type: 'central_bank', label: 'CB RSS', cls: 'bg-cyan-900/40 text-cyan-300 border-cyan-700/40 hover:bg-cyan-800/50' },
|
||||
{ type: 'sentiment', label: 'Senti.', cls: 'bg-yellow-900/40 text-yellow-300 border-yellow-700/40 hover:bg-yellow-800/50' },
|
||||
] as const).map(({ type, label, cls }) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => handleRefresh(type)}
|
||||
disabled={refresh.isPending}
|
||||
className={clsx('flex items-center gap-1 px-2.5 py-1.5 text-xs border rounded disabled:opacity-50', cls)}
|
||||
>
|
||||
<RefreshCw className={clsx('w-3 h-3', refresh.isPending && 'animate-spin')} />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats bar */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="grid grid-cols-3 sm:grid-cols-6 gap-2">
|
||||
{[
|
||||
{ label: 'Total reports', value: stats.total },
|
||||
{ label: 'COT reports', value: stats.by_type?.cot ?? 0 },
|
||||
{ label: 'EIA reports', value: stats.by_type?.eia ?? 0 },
|
||||
{ label: 'Absorbed (>30%)', value: stats.absorbed_count },
|
||||
].map(({ label, value }) => (
|
||||
{ label: 'COT', value: stats.by_type?.cot ?? 0, cls: 'text-purple-400' },
|
||||
{ label: 'EIA', value: stats.by_type?.eia ?? 0, cls: 'text-orange-400' },
|
||||
{ label: 'Earnings', value: stats.by_type?.earnings ?? 0, cls: 'text-blue-400' },
|
||||
{ label: 'VX Curve', value: stats.by_type?.vx_curve ?? 0, cls: 'text-rose-400' },
|
||||
{ label: 'Central Bank', value: stats.by_type?.central_bank ?? 0, cls: 'text-cyan-400' },
|
||||
{ label: 'Sentiment', value: stats.by_type?.sentiment ?? 0, cls: 'text-yellow-400' },
|
||||
].map(({ label, value, cls }) => (
|
||||
<div key={label} className="bg-dark-800 border border-slate-700/40 rounded p-3 text-center">
|
||||
<div className="text-2xl font-bold text-white">{value}</div>
|
||||
<div className={clsx('text-xl font-bold', cls)}>{value}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{label}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -255,19 +276,21 @@ export default function InstitutionalReports() {
|
||||
)}
|
||||
|
||||
{/* Latest dates */}
|
||||
{stats && (stats.latest_cot || stats.latest_eia) && (
|
||||
<div className="flex gap-4 text-xs text-slate-500">
|
||||
{stats.latest_cot && <span>Latest COT: <span className="text-slate-300">{stats.latest_cot}</span></span>}
|
||||
{stats.latest_eia && <span>Latest EIA: <span className="text-slate-300">{stats.latest_eia}</span></span>}
|
||||
{stats && (
|
||||
<div className="flex flex-wrap gap-4 text-xs text-slate-500">
|
||||
{stats.latest_cot && <span>COT: <span className="text-purple-400">{stats.latest_cot}</span></span>}
|
||||
{stats.latest_eia && <span>EIA: <span className="text-orange-400">{stats.latest_eia}</span></span>}
|
||||
{stats.by_type?.earnings ? <span>Earnings: <span className="text-blue-400">{Object.keys(stats.by_type).includes('earnings') ? 'tracked' : ''}</span></span> : null}
|
||||
<span className="text-slate-600">Total: {stats.total} · Absorbed: {stats.absorbed_count}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap gap-3 bg-dark-800 border border-slate-700/40 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500">Type:</span>
|
||||
<div className="flex gap-1">
|
||||
{['', 'cot', 'eia'].map(t => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(['', 'cot', 'eia', 'earnings', 'vx_curve', 'central_bank', 'sentiment'] as const).map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setFilterType(t)}
|
||||
@@ -278,16 +301,16 @@ export default function InstitutionalReports() {
|
||||
: 'bg-dark-700 text-slate-400 border-slate-700/40 hover:border-slate-600'
|
||||
)}
|
||||
>
|
||||
{t === '' ? 'All' : t.toUpperCase()}
|
||||
{t === '' ? 'All' : (TYPE_LABELS[t] ?? t)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500">Category:</span>
|
||||
<div className="flex gap-1">
|
||||
{['', 'energy', 'metals', 'equities', 'forex', 'multi'].map(c => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{['', 'energy', 'metals', 'equities', 'forex', 'volatility', 'macro', 'sentiment', 'multi'].map(c => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setFilterCategory(c)}
|
||||
@@ -349,8 +372,8 @@ export default function InstitutionalReports() {
|
||||
<div className="text-slate-600 text-4xl">📋</div>
|
||||
<div className="text-slate-400 font-medium">No institutional reports yet</div>
|
||||
<p className="text-slate-600 text-sm max-w-sm mx-auto">
|
||||
Click <strong className="text-slate-400">Refresh COT</strong> to fetch the latest CFTC positioning data,
|
||||
or configure your EIA API key in <strong className="text-slate-400">Configuration</strong> to enable petroleum reports.
|
||||
Use the refresh buttons above to fetch reports. COT and EIA require API keys.
|
||||
Earnings, VX, Central Bank RSS, and Sentiment fetch automatically.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user