Files
worldmonitor/scripts/seed-earnings-calendar.mjs
Elie Habib 2939b1f4a1 feat(finance-panels): add 7 macro/market panels + Daily Brief context (issues #2245-#2253) (#2258)
* feat(fear-greed): add regime state label, action stance badge, divergence warnings

Closes #2245

* feat(finance-panels): add 7 new finance panels + Daily Brief macro context

Implements issues #2245 (F&G Regime), #2246 (Sector Heatmap bars),
#2247 (MacroTiles), #2248 (FSI), #2249 (Yield Curve), #2250 (Earnings
Calendar), #2251 (Economic Calendar), #2252 (COT Positioning),
#2253 (Daily Brief prompt extension).

New panels:
- MacroTilesPanel: CPI YoY, Unemployment, GDP, Fed Rate tiles via FRED
- FSIPanel: Financial Stress Indicator gauge (HYG/TLT/VIX/HY-spread)
- YieldCurvePanel: SVG yield curve chart with inverted/normal badge
- EarningsCalendarPanel: Finnhub earnings calendar with BMO/AMC/BEAT/MISS
- EconomicCalendarPanel: FOMC/CPI/NFP events with impact badges
- CotPositioningPanel: CFTC disaggregated COT positioning bars
- MarketPanel: adds sorted bar chart view above sector heatmap grid

New RPCs:
- ListEarningsCalendar (market/v1)
- GetCotPositioning (market/v1)
- GetEconomicCalendar (economic/v1)

Seed scripts:
- seed-earnings-calendar.mjs (Finnhub, 14-day window, TTL 12h)
- seed-economic-calendar.mjs (Finnhub, 30-day window, TTL 12h)
- seed-cot.mjs (CFTC disaggregated text file, TTL 7d)
- seed-economy.mjs: adds yield curve tenors DGS1MO/3MO/6MO/1/2/5/30
- seed-fear-greed.mjs: adds FSI computation + sector performance

Daily Brief: extends buildDailyMarketBrief with optional regime,
yield curve, and sector context fed to the LLM summarization prompt.

All panels default enabled in FINANCE_PANELS, disabled in FULL_PANELS.

🤖 Generated with Claude Sonnet 4.6 via Claude Code (https://claude.ai/claude-code) + Compound Engineering v2.40.0

Co-Authored-By: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com>

* fix(finance-panels): address code review P1/P2 findings

P1 - Security/Correctness:
- EconomicCalendarPanel: add escapeHtml on all 7 Finnhub-sourced fields
- EconomicCalendarPanel: fix panel contract (public fetchData():boolean,
  remove constructor self-init, add retry callbacks to all showError calls)
- YieldCurvePanel: fix NaN in xPos() when count <= 1 (divide-by-zero)
- seed-earnings-calendar: move Finnhub API key from URL to X-Finnhub-Token header
- seed-economic-calendar: move Finnhub API key from URL to X-Finnhub-Token header
- seed-earnings-calendar: add isMain guard around runSeed() call
- health.js + bootstrap.js: register earningsCalendar, econCalendar, cotPositioning keys
- health.js dataSize(): add earnings + instruments to property name list

P2 - Quality:
- FSIPanel: change !resp.fsiValue → resp.fsiValue <= 0 (rejects valid zero)
- data-loader: fix Promise.allSettled type inference via indexed destructure
- seed-fear-greed: allowlist cnnLabel against known values before writing to Redis
- seed-economic-calendar: remove unused sleep import
- seed-earnings-calendar + econ-calendar: increase TTL 43200 → 129600 (36h = 3x interval)
- YieldCurvePanel: use SERIES_IDS const in RPC call (single source of truth)

* fix(bootstrap): remove on-demand panel keys from bootstrap.js

earningsCalendar, econCalendar, cotPositioning panels fetch via RPC
on demand — they have no getHydratedData consumer in src/ and must
not be in api/bootstrap.js. They remain in api/health.js BOOTSTRAP_KEYS
for staleness monitoring.

* fix(compound-engineering): fix markdown lint error in local settings

* fix(finance-panels): resolve all P3 code-review findings

- 030: MacroTilesPanel: add `deltaFormat?` field to MacroTile interface,
  define per-tile delta formatters (CPI pp, GDP localeString+B),
  replace fragile tile.id switch in tileHtml with fmt = deltaFormat ?? format
- 031: FSIPanel: check getHydratedData('fearGreedIndex') at top of
  fetchData(); extract fsi/vix/hySpread from headerMetrics and render
  synchronously; fall back to live RPC only when bootstrap absent
- 032: All 6 finance panels: extract lazy module-level client singletons
  (EconomicServiceClient or MarketServiceClient) so the client is
  constructed at most once per panel module lifetime, not on every fetchData
- 033: get-fred-series-batch: add BAMLC0A0CM and SOFR to ALLOWED_SERIES
  (both seeded by seed-economy.mjs but previously unreachable via RPC)

* fix(finance-panels): health.js SEED_META, FSI calibration, seed-cot catch handler

- health.js: add SEED_META entries for earningsCalendar (1440min), econCalendar
  (1440min), cotPositioning (14400min) — without these, stopped seeds only alarm
  CRIT:EMPTY after TTL expiry instead of earlier WARN:STALE_SEED
- seed-cot.mjs: replace bare await with .catch() handler consistent with other seeds
- seed-fear-greed.mjs: recalibrate FSI thresholds to match formula output range
  (Low>=1.5, Moderate>=0.8, Elevated>=0.3; old values >=0.08/0.05/0.03 were
  calibrated for [0,0.15] but formula yields ~1-2 in normal conditions)
- FSIPanel.ts: fix gauge fillPct range to [0, 2.5] matching recalibrated thresholds
- todos: fix MD022/MD032 markdown lint errors in P3 review files

---------

Co-authored-by: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com>
2026-03-26 08:03:09 +04:00

83 lines
2.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { loadEnvFile, CHROME_UA, runSeed } from './_seed-utils.mjs';
loadEnvFile(import.meta.url);
const KEY = 'market:earnings-calendar:v1';
const TTL = 129600; // 36h — 3× a 12h cron interval
function toDateStr(d) {
return d.toISOString().slice(0, 10);
}
async function fetchAll() {
const apiKey = process.env.FINNHUB_API_KEY;
if (!apiKey) {
console.warn(' FINNHUB_API_KEY not set — skipping');
return { earnings: [], unavailable: true };
}
const from = new Date();
const to = new Date();
to.setDate(to.getDate() + 14);
const url = `https://finnhub.io/api/v1/calendar/earnings?from=${toDateStr(from)}&to=${toDateStr(to)}`;
const resp = await fetch(url, {
headers: { 'User-Agent': CHROME_UA, 'X-Finnhub-Token': apiKey },
signal: AbortSignal.timeout(15_000),
});
if (!resp.ok) {
throw new Error(`Finnhub earnings calendar HTTP ${resp.status}`);
}
const data = await resp.json();
const raw = Array.isArray(data?.earningsCalendar) ? data.earningsCalendar : [];
const earnings = raw
.filter(e => e.symbol)
.map(e => {
const epsEst = e.epsEstimate != null ? Number(e.epsEstimate) : null;
const epsAct = e.epsActual != null ? Number(e.epsActual) : null;
const revEst = e.revenueEstimate != null ? Number(e.revenueEstimate) : null;
const revAct = e.revenueActual != null ? Number(e.revenueActual) : null;
const hasActuals = epsAct != null;
let surpriseDirection = '';
if (hasActuals && epsEst != null) {
if (epsAct > epsEst) surpriseDirection = 'beat';
else if (epsAct < epsEst) surpriseDirection = 'miss';
}
return {
symbol: String(e.symbol),
company: e.name ? String(e.name) : String(e.symbol),
date: e.date ? String(e.date) : '',
hour: e.hour ? String(e.hour) : '',
epsEstimate: epsEst,
revenueEstimate: revEst,
epsActual: epsAct,
revenueActual: revAct,
hasActuals,
surpriseDirection,
};
})
.sort((a, b) => a.date.localeCompare(b.date))
.slice(0, 100);
console.log(` Fetched ${earnings.length} earnings entries`);
return { earnings, unavailable: false };
}
function validate(data) {
return Array.isArray(data?.earnings) && data.earnings.length > 0;
}
if (process.argv[1]?.endsWith('seed-earnings-calendar.mjs')) {
runSeed('market', 'earnings-calendar', KEY, fetchAll, {
validateFn: validate,
ttlSeconds: TTL,
sourceVersion: 'finnhub-v1',
}).catch(err => { console.error('FATAL:', err.message || err); process.exit(1); });
}