mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* 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>
83 lines
2.5 KiB
JavaScript
83 lines
2.5 KiB
JavaScript
#!/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); });
|
||
}
|