import { defineConnector, parseXml, raw, type NormalizedBatch, type ProposedInstrument } from "@market-atlas/connector-sdk"; import type { InstrumentHint, NormalizedObservation } from "@market-atlas/market-model"; import { zonedTimeToUtc } from "@market-atlas/market-model"; const URL = "https://home.treasury.gov/resource-center/data-chart-center/interest-rates/pages/xml?data=daily_treasury_yield_curve&field_tdr_date_value_month="; const TENORS: Array<[string, string, string]> = [ ["BC_1MONTH", "US1M", "US Treasury 1-month"], ["BC_2MONTH", "US2M", "US Treasury 2-month"], ["BC_3MONTH", "US3M", "US Treasury 3-month"], ["BC_4MONTH", "US4M", "US Treasury 4-month"], ["BC_6MONTH", "US6M", "US Treasury 6-month"], ["BC_1YEAR", "US1Y", "US Treasury 1-year"], ["BC_2YEAR", "US2Y", "US Treasury 2-year"], ["BC_3YEAR", "US3Y", "US Treasury 3-year"], ["BC_5YEAR", "US5Y", "US Treasury 5-year"], ["BC_7YEAR", "US7Y", "US Treasury 7-year"], ["BC_10YEAR", "US10Y", "US Treasury 10-year"], ["BC_20YEAR", "US20Y", "US Treasury 20-year"], ["BC_30YEAR", "US30Y", "US Treasury 30-year"], ]; const hint = (symbol: string, name: string): InstrumentHint => ({ assetClass: "TREASURY", name: `${name} par yield`, exchangeId: null, currency: "USD", country: "US", securityType: "PAR_YIELD", metadata: { featured: ["US2Y", "US10Y", "US30Y", "US3M"].includes(symbol), unit: "percent" } }); const seeds: ProposedInstrument[] = TENORS.map(([, s, n]) => ({ symbol: s, hint: hint(s, n), aliases: [s.replace("US", "US "), `${s.slice(2)} treasury`] })); const month = (d: Date) => `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}`; /** U.S. Treasury daily par yield curve (official XML/OData feed). Publishes once per business day after ~15:30 ET. */ export const usTreasuryYieldCurve = defineConnector({ metadata: { id: "us-treasury-yield-curve", name: "U.S. Treasury — daily par yield curve", version: "1.0.0", sourceId: "us-treasury", organization: "U.S. Department of the Treasury", sourceType: "XML", jurisdiction: "US", rightsStatus: "OFFICIAL_OPEN_DATA", realtimeStatus: "END_OF_DAY", expectedLatencyMs: null, supportsStreaming: false, supportsHistorical: true, assetClasses: ["TREASURY"], exchanges: [], homepage: "https://home.treasury.gov/resource-center/data-chart-center/interest-rates/TextView?type=daily_treasury_yield_curve", description: "Daily Treasury par yield curve rates (1 month → 30 years) from the official Treasury XML feed. One value per tenor per business day.", rightsNotes: "U.S. federal government work — public domain.", termsUrl: "https://home.treasury.gov/subfooter/terms-of-use", sourceFamily: "us-treasury", enabled: true, }, seeds, defaultSymbols: seeds.map((s) => s.symbol), rateLimits: { "home.treasury.gov": 0.5 }, schedule: { intervalMs: 60 * 60_000 }, async poll(ctx) { const now = new Date(); const out = []; const cur = await ctx.http.getText(URL + month(now), { timeoutMs: 30_000 }); if (!cur.notModified) out.push(raw("us-treasury-yield-curve", "us-treasury", "month", cur.text, { month: month(now) })); // Early in the month the current file may hold < 2 rows → also read the previous month for PREVIOUS_CLOSE. if ((cur.text.match(//g) ?? []).length < 2) { const prevMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1)); const prev = await ctx.http.getText(URL + month(prevMonth), { timeoutMs: 30_000 }); out.unshift(raw("us-treasury-yield-curve", "us-treasury", "month", prev.text, { month: month(prevMonth) })); } return out; }, normalize(r): NormalizedBatch { if (r.kind !== "month" || typeof r.payload !== "string") return { observations: [] }; const doc = parseXml(r.payload); const entries: any[] = doc?.feed?.entry ? (Array.isArray(doc.feed.entry) ? doc.feed.entry : [doc.feed.entry]) : []; const rows = entries .map((e) => e?.content?.["m:properties"]) .filter(Boolean) .map((p: Record) => ({ date: text(p["d:NEW_DATE"]), values: Object.fromEntries(TENORS.map(([k]) => [k, num(p[`d:${k}`])])) })) .filter((row) => row.date) .sort((a, b) => a.date!.localeCompare(b.date!)); const last = rows[rows.length - 1]; const prev = rows[rows.length - 2]; if (!last) return { observations: [] }; const observations: NormalizedObservation[] = []; for (const [key, symbol, name] of TENORS) { const v = last.values[key]; if (v == null) continue; const ts = zonedTimeToUtc(`${last.date!.slice(0, 10)}T15:30:00`, "America/New_York"); const base = { symbol, instrumentHint: hint(symbol, name), currency: "USD", observationType: "REFERENCE_RATE" as const, sourceTimestamp: ts, timestampTrust: "SOURCE" as const, rightsStatus: "OFFICIAL_OPEN_DATA" as const, realtimeStatus: "END_OF_DAY" as const, meta: { date: last.date!.slice(0, 10) } }; observations.push({ ...base, field: "YIELD", value: v }, { ...base, field: "LAST_PRICE", value: v }); const pv = prev?.values[key]; if (pv != null) observations.push({ ...base, field: "PREVIOUS_CLOSE", value: pv, meta: { date: prev!.date!.slice(0, 10) } }); } return { observations, stats: { rows: rows.length } }; }, fixturesDir: "fixtures", }); function text(v: unknown): string | null { if (v == null) return null; if (typeof v === "string") return v; if (typeof v === "object" && typeof (v as Record)["#text"] === "string") return (v as Record)["#text"]!; return null; } function num(v: unknown): number | null { const t = text(v); if (t == null || t === "") return null; const n = Number(t); return Number.isFinite(n) ? n : null; }