Files
worldmonitor/scripts/seed-cot.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

149 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
import { loadEnvFile, CHROME_UA, runSeed } from './_seed-utils.mjs';
loadEnvFile(import.meta.url);
const COT_KEY = 'market:cot:v1';
const COT_TTL = 604800;
const TARGET_INSTRUMENTS = [
{ name: 'S&P 500 E-Mini', code: 'ES', pattern: /E-MINI S&P 500/i },
{ name: 'Nasdaq 100 E-Mini', code: 'NQ', pattern: /E-MINI NASDAQ-100/i },
{ name: '10-Year T-Note', code: 'ZN', pattern: /10-YEAR U.S. TREASURY NOTE/i },
{ name: '2-Year T-Note', code: 'ZT', pattern: /2-YEAR U.S. TREASURY NOTE/i },
{ name: 'Gold', code: 'GC', pattern: /GOLD - COMMODITY EXCHANGE/i },
{ name: 'Crude Oil (WTI)', code: 'CL', pattern: /CRUDE OIL, LIGHT SWEET/i },
{ name: 'EUR/USD', code: 'EC', pattern: /EURO FX/i },
{ name: 'USD/JPY', code: 'JY', pattern: /JAPANESE YEN/i },
];
function parseDate(raw) {
if (!raw) return '';
const s = String(raw).trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
if (/^\d{6}$/.test(s)) {
const yy = s.slice(0, 2);
const mm = s.slice(2, 4);
const dd = s.slice(4, 6);
const year = parseInt(yy, 10) >= 50 ? `19${yy}` : `20${yy}`;
return `${year}-${mm}-${dd}`;
}
return s;
}
async function fetchCotData() {
const url = 'https://www.cftc.gov/dea/newcot/c_disaggrt.txt';
let text;
try {
const resp = await fetch(url, {
headers: { 'User-Agent': CHROME_UA },
signal: AbortSignal.timeout(30_000),
});
if (!resp.ok) {
console.warn(` CFTC fetch failed: HTTP ${resp.status}`);
return { instruments: [], reportDate: '' };
}
text = await resp.text();
} catch (e) {
console.warn(` CFTC fetch error: ${e.message}`);
return { instruments: [], reportDate: '' };
}
const lines = text.split('\n').map(l => l.trimEnd());
if (lines.length < 2) {
console.warn(' CFTC: empty file');
return { instruments: [], reportDate: '' };
}
const headerLine = lines[0];
const headers = headerLine.split('|').map(h => h.trim());
const colIdx = name => {
const idx = headers.indexOf(name);
return idx;
};
const nameCol = colIdx('Market_and_Exchange_Names');
const dateCol1 = colIdx('Report_Date_as_YYYY-MM-DD');
const dateCol2 = colIdx('As_of_Date_In_Form_YYMMDD');
const dealerLongCol = colIdx('Dealer_Positions_Long_All');
const dealerShortCol = colIdx('Dealer_Positions_Short_All');
const amLongCol = colIdx('Asset_Mgr_Positions_Long_All');
const amShortCol = colIdx('Asset_Mgr_Positions_Short_All');
const levLongCol = colIdx('Lev_Money_Positions_Long_All');
const levShortCol = colIdx('Lev_Money_Positions_Short_All');
if (nameCol === -1) {
console.warn(' CFTC: Market_and_Exchange_Names column not found');
return { instruments: [], reportDate: '' };
}
const dataLines = lines.slice(1).filter(l => l.trim().length > 0);
const instruments = [];
let latestReportDate = '';
for (const target of TARGET_INSTRUMENTS) {
const matchingLines = dataLines.filter(line => {
const fields = line.split('|');
const marketName = fields[nameCol] ?? '';
return target.pattern.test(marketName);
});
if (matchingLines.length === 0) {
console.warn(` CFTC: no rows found for ${target.name}`);
continue;
}
const row = matchingLines[0];
const fields = row.split('|');
const rawDate = (dateCol1 !== -1 && fields[dateCol1]?.trim())
? fields[dateCol1].trim()
: (dateCol2 !== -1 ? fields[dateCol2]?.trim() ?? '' : '');
const reportDate = parseDate(rawDate);
if (reportDate && !latestReportDate) latestReportDate = reportDate;
const toNum = idx => {
if (idx === -1) return 0;
const v = parseInt((fields[idx] ?? '').replace(/,/g, '').trim(), 10);
return isNaN(v) ? 0 : v;
};
const dealerLong = toNum(dealerLongCol);
const dealerShort = toNum(dealerShortCol);
const amLong = toNum(amLongCol);
const amShort = toNum(amShortCol);
const levLong = toNum(levLongCol);
const levShort = toNum(levShortCol);
const netPct = ((amLong - amShort) / Math.max(amLong + amShort, 1)) * 100;
instruments.push({
name: target.name,
code: target.code,
reportDate,
assetManagerLong: amLong,
assetManagerShort: amShort,
leveragedFundsLong: levLong,
leveragedFundsShort: levShort,
dealerLong,
dealerShort,
netPct: parseFloat(netPct.toFixed(2)),
});
console.log(` ${target.code}: AM net ${netPct.toFixed(1)}% (${amLong}L / ${amShort}S), date=${reportDate}`);
}
return { instruments, reportDate: latestReportDate };
}
if (process.argv[1] && process.argv[1].endsWith('seed-cot.mjs')) {
runSeed('market', 'cot', COT_KEY, fetchCotData, {
ttlSeconds: COT_TTL,
validateFn: data => Array.isArray(data?.instruments) && data.instruments.length > 0,
recordCount: data => data?.instruments?.length ?? 0,
}).catch(err => { console.error('FATAL:', err.message || err); process.exit(1); });
}