SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
5.7 KB · 107 lines typescript
Raw Blame History
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