spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import { defineConnector, parseXml, raw, type NormalizedBatch, type ProposedInstrument } from "@market-atlas/connector-sdk";2import type { InstrumentHint, NormalizedObservation } from "@market-atlas/market-model";3import { zonedTimeToUtc } from "@market-atlas/market-model";45const URL = "https://home.treasury.gov/resource-center/data-chart-center/interest-rates/pages/xml?data=daily_treasury_yield_curve&field_tdr_date_value_month=";6const TENORS: Array<[string, string, string]> = [7 ["BC_1MONTH", "US1M", "US Treasury 1-month"],8 ["BC_2MONTH", "US2M", "US Treasury 2-month"],9 ["BC_3MONTH", "US3M", "US Treasury 3-month"],10 ["BC_4MONTH", "US4M", "US Treasury 4-month"],11 ["BC_6MONTH", "US6M", "US Treasury 6-month"],12 ["BC_1YEAR", "US1Y", "US Treasury 1-year"],13 ["BC_2YEAR", "US2Y", "US Treasury 2-year"],14 ["BC_3YEAR", "US3Y", "US Treasury 3-year"],15 ["BC_5YEAR", "US5Y", "US Treasury 5-year"],16 ["BC_7YEAR", "US7Y", "US Treasury 7-year"],17 ["BC_10YEAR", "US10Y", "US Treasury 10-year"],18 ["BC_20YEAR", "US20Y", "US Treasury 20-year"],19 ["BC_30YEAR", "US30Y", "US Treasury 30-year"],20];21const 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" } });22const seeds: ProposedInstrument[] = TENORS.map(([, s, n]) => ({ symbol: s, hint: hint(s, n), aliases: [s.replace("US", "US "), `${s.slice(2)} treasury`] }));2324const month = (d: Date) => `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}`;2526/** U.S. Treasury daily par yield curve (official XML/OData feed). Publishes once per business day after ~15:30 ET. */27export const usTreasuryYieldCurve = defineConnector({28 metadata: {29 id: "us-treasury-yield-curve",30 name: "U.S. Treasury — daily par yield curve",31 version: "1.0.0",32 sourceId: "us-treasury",33 organization: "U.S. Department of the Treasury",34 sourceType: "XML",35 jurisdiction: "US",36 rightsStatus: "OFFICIAL_OPEN_DATA",37 realtimeStatus: "END_OF_DAY",38 expectedLatencyMs: null,39 supportsStreaming: false,40 supportsHistorical: true,41 assetClasses: ["TREASURY"],42 exchanges: [],43 homepage: "https://home.treasury.gov/resource-center/data-chart-center/interest-rates/TextView?type=daily_treasury_yield_curve",44 description: "Daily Treasury par yield curve rates (1 month → 30 years) from the official Treasury XML feed. One value per tenor per business day.",45 rightsNotes: "U.S. federal government work — public domain.",46 termsUrl: "https://home.treasury.gov/subfooter/terms-of-use",47 sourceFamily: "us-treasury",48 enabled: true,49 },50 seeds,51 defaultSymbols: seeds.map((s) => s.symbol),52 rateLimits: { "home.treasury.gov": 0.5 },53 schedule: { intervalMs: 60 * 60_000 },54 async poll(ctx) {55 const now = new Date();56 const out = [];57 const cur = await ctx.http.getText(URL + month(now), { timeoutMs: 30_000 });58 if (!cur.notModified) out.push(raw("us-treasury-yield-curve", "us-treasury", "month", cur.text, { month: month(now) }));59 // Early in the month the current file may hold < 2 rows → also read the previous month for PREVIOUS_CLOSE.60 if ((cur.text.match(/<entry>/g) ?? []).length < 2) {61 const prevMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));62 const prev = await ctx.http.getText(URL + month(prevMonth), { timeoutMs: 30_000 });63 out.unshift(raw("us-treasury-yield-curve", "us-treasury", "month", prev.text, { month: month(prevMonth) }));64 }65 return out;66 },67 normalize(r): NormalizedBatch {68 if (r.kind !== "month" || typeof r.payload !== "string") return { observations: [] };69 const doc = parseXml<any>(r.payload);70 const entries: any[] = doc?.feed?.entry ? (Array.isArray(doc.feed.entry) ? doc.feed.entry : [doc.feed.entry]) : [];71 const rows = entries72 .map((e) => e?.content?.["m:properties"])73 .filter(Boolean)74 .map((p: Record<string, unknown>) => ({ date: text(p["d:NEW_DATE"]), values: Object.fromEntries(TENORS.map(([k]) => [k, num(p[`d:${k}`])])) }))75 .filter((row) => row.date)76 .sort((a, b) => a.date!.localeCompare(b.date!));77 const last = rows[rows.length - 1];78 const prev = rows[rows.length - 2];79 if (!last) return { observations: [] };80 const observations: NormalizedObservation[] = [];81 for (const [key, symbol, name] of TENORS) {82 const v = last.values[key];83 if (v == null) continue;84 const ts = zonedTimeToUtc(`${last.date!.slice(0, 10)}T15:30:00`, "America/New_York");85 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) } };86 observations.push({ ...base, field: "YIELD", value: v }, { ...base, field: "LAST_PRICE", value: v });87 const pv = prev?.values[key];88 if (pv != null) observations.push({ ...base, field: "PREVIOUS_CLOSE", value: pv, meta: { date: prev!.date!.slice(0, 10) } });89 }90 return { observations, stats: { rows: rows.length } };91 },92 fixturesDir: "fixtures",93});9495function text(v: unknown): string | null {96 if (v == null) return null;97 if (typeof v === "string") return v;98 if (typeof v === "object" && typeof (v as Record<string, unknown>)["#text"] === "string") return (v as Record<string, string>)["#text"]!;99 return null;100}101function num(v: unknown): number | null {102 const t = text(v);103 if (t == null || t === "") return null;104 const n = Number(t);105 return Number.isFinite(n) ? n : null;106}107