Skip to content

Economic Data Releases ​

News is one lane; scheduled structured data is another. A data provider answers with dated rows rather than documents, so xnews models it with three generic pieces:

  • DataSource<Row> binds one dataset to its transport. Built-in factories (fredDataSource, cotDataSource, ffiecCallDataSource, and so on) return sources; you can implement your own.
  • fetchDataRelease(source) fetches the current release and wraps it in a non-throwing DataProviderResult with the same status taxonomy as news providers.
  • createDataReleaseWatcher(source) polls and yields a result whenever a newer release appears.
ts
import { fetchDataRelease, fredDataSource } from "@xbbg/xnews";

const source = fredDataSource("UNRATE", {
  apiKey: process.env.FRED_API_KEY!,
  sortOrder: "desc",
  limit: 12,
});

const result = await fetchDataRelease(source);
if (result.status === "ok" && result.release) {
  console.log(result.release.asOf, result.rowCount, result.release.rows[0]);
} else {
  console.warn(result.status, result.warnings);
}

A DataRelease<Row> has provider, dataset, asOf (the YYYY-MM-DD date the rows describe), an optional sequence (for publishers that release many times a day), the dataset url, an optional updatedAt revision stamp, and rows. asOf is the reference date, not the publication date: COT positions are as of Tuesday and published on Friday.

Data providers never join company or topic feeds. Requesting cftc-cot, ffiec-cdr, or dtcc-sdr in a feed reports unsupported; the bridges described in Bridge releases into news turn a release into NewsItems when you need that.

Release statuses ​

StatusMeaning
okA release was returned (it may have zero rows; an empty dated release is still a release)
emptyThe source has no matching release right now
errorTransport or parse failure; see error.code and warnings
disabledA caller precondition is missing, such as an API key

Watch for new releases ​

ts
import { cotDataSource, createDataReleaseWatcher } from "@xbbg/xnews";

const controller = new AbortController();

try {
  for await (const result of createDataReleaseWatcher(cotDataSource("tff", { markets: ["ES", "ZN"] }), {
    intervalMs: 60 * 60_000,
    sinceAsOf: "2026-10-27", // resume after the last release you processed
    signal: controller.signal,
  })) {
    if (result.status === "ok" && result.release) {
      console.log("new COT week", result.release.asOf, result.rowCount);
    } else {
      console.warn("poll failed", result.status, result.error?.code, result.warnings);
    }
  }
} catch (error) {
  if (!controller.signal.aborted) throw error;
}

How the data watcher behaves:

  • It polls every intervalMs (default 15 minutes) and yields a release only when its asOf is strictly later than the last one yielded (or than sinceAsOf).
  • For sequenced sources (DTCC intraday slices), "newer" means a greater sequence. The watcher asks for the next unseen release with afterSequence and drains a backlog without sleeping. Use sinceSequence to resume without gaps.
  • Each poll passes the last asOf as ifNewerThan, so heavy sources (FFIEC archives, NIC) can answer "nothing new" from a cheap page probe instead of re-downloading.
  • Unlike the news watchers, it does not throw on failures. A failed poll is yielded as a result with status: "error" or "disabled", once per distinct failure; identical consecutive failures are suppressed until a poll succeeds. Always check result.status.
  • A malformed sinceAsOf or sinceSequence yields one disabled result and ends the generator.

FRED (fred) ​

FRED needs a free API key. Use the observation API directly, or bind a series to the data lane:

ts
import { fetchFredObservations, fetchFredSeries, searchFredSeries } from "@xbbg/xnews";

const apiKey = process.env.FRED_API_KEY!;

const matches = await searchFredSeries("unemployment rate", { apiKey, limit: 5 });
const series = await fetchFredSeries("UNRATE", { apiKey });
const observations = await fetchFredObservations("UNRATE", {
  apiKey,
  observationStart: "2025-01-01",
  sortOrder: "desc",
  limit: 12,
});

console.log(matches.items.map((item) => item.id), series?.title, series?.units);
for (const row of observations.items) {
  console.log(row.date, row.value); // FRED's "." (missing) becomes null; rawValue keeps the text
}

fredDataSource labels releases with the upper-cased series id, sets asOf to the newest observation date, and redacts the key from requestUrls. FRED_PROVIDER_POLICY records the ceiling of 120 requests per minute. Follow the FRED API Terms of Use, including this required notice:

This product uses the FRED® API but is not endorsed or certified by the Federal Reserve Bank of St. Louis.

CFTC Commitments of Traders (cftc-cot) ​

ts
import { COT_MARKETS, fetchCotReport } from "@xbbg/xnews";

// Latest week of Traders in Financial Futures for two markets.
const release = await fetchCotReport("tff", { markets: ["ZN", "ES"] });

for (const row of release?.rows ?? []) {
  const net = row.leveragedFunds.long - row.leveragedFunds.short;
  console.log(row.asOf, row.marketName, "leveraged funds net:", net);
}

console.log(COT_MARKETS.map((market) => market.symbol)); // ES, NQ, YM, RTY, ZT, ZF, ZN, ZB, UB, VIX
  • Families: legacy, disaggregated, and tff (futures-only by default, futures-and-options with combined: true), plus the combined-only cit supplemental. Rows are typed per family (CotLegacyRow, CotDisaggregatedRow, CotTffRow, CotCitRow) with positions { long, short, spreading?, longChange?, shortChange?, spreadingChange? } in contracts.
  • Markets: markets accepts preset symbols and aliases from COT_MARKETS (for example "ZN" or "10y") or raw CFTC contract market codes.
  • Window: without since/until, a fetch resolves the latest report week in one probe and returns exactly that week. With a window, rows come back newest first up to limit (default 5,000, maximum 50,000).
  • Throttling: unauthenticated Socrata clients share an IP throttling pool. Pass a free appToken for sustained polling.
  • Cadence: positions are as of Tuesday and normally published Friday at 15:30 ET. asOf carries the Tuesday.

US bank regulatory data ​

xnews covers the FFIEC and Federal Reserve bank-data family end to end. All of these are free and keyless; most are large archives, so each raises its own response ceiling instead of using the shared 32 MiB default.

DataProvider idFetchData sourceNotes
Call Reports and UBPR (CDR bulk)ffiec-cdrfetchFfiecCallReport, downloadFfiecBulkDataffiecCallDataSourceDrives the CDR's stateful ASP.NET postback chain. Call archives are about 6 MB per quarter; ceiling FFIEC_BULK_MAX_BYTES (512 MiB)
Institution structure (NIC)ffiec-nicfetchNicData, fetchNicBulkPagenicDataSourceFive products: attributes-active, attributes-closed, attributes-branches, relationships, transformations
FR Y-9 holding companiesffiec-fry9fetchFry9Data, fetchFry9Periodsfry9DataSourcefry9c (consolidated), fry9lp (parent only), fry9sp (small HCs, semiannual). Never sum Y-9C and Y-9LP
FFIEC 002 (US branches of foreign banks)ffiec-002fetchFfiec002Reportffiec002DataSourceOne institution and quarter per request
E.16 country exposureffiec-e16fetchFfiecE16Data, listFfiecE16ReleasesffiecE16DataSourceQuarterly XLSX read natively
HMDA mortgage datahmdafetchHmdaCount, fetchHmdaAggregations, fetchHmdaFilers, fetchHmdaLoanRecordshmdaDataSourceCFPB Data Browser API; public LAR is a privacy-modified LAR
CRA small business and farm lendingffiec-crafetchCraFlatFile, fetchCraAvailableYearscraDataSourceFixed-width annual archives, 1996 onward
Census tractsffiec-censusfetchFfiecCensus, fetchFfiecGeocodeffiecCensusDataSourceMatch the census year to the activity year you analyze

RSSD ids join these datasets: Call Report rows, NIC attributes and relationships, FR Y-9 (RSSD9001), and FFIEC 002 all key on them.

Call Reports ​

ts
import { createDataReleaseWatcher, fetchFfiecCallReport, ffiecCallDataSource } from "@xbbg/xnews";

// One quarterly release, filtered to two banks' balance sheet and income statement.
const release = await fetchFfiecCallReport({
  rssdIds: [852218, 480228], // JPMorgan Chase Bank NA, Bank of America NA
  schedules: ["RC", "RI"],
});
console.log(release?.asOf, release?.updatedAt, release?.rows[0]?.values);

// Watch for new quarters. Unchanged quarters cost one page probe, not a 6 MB download.
for await (const result of createDataReleaseWatcher(ffiecCallDataSource({ rssdIds: [852218] }), {
  intervalMs: 6 * 60 * 60_000,
})) {
  if (result.status === "ok") console.log("new quarter", result.release?.asOf);
}

Each row is one filer and schedule with MDRM item values as verbatim strings. asOf is the reporting period end; updatedAt is the CDR's "Call Updated" stamp. The watcher keys on asOf, so a late refiling of an already-seen quarter moves updatedAt without re-yielding. When revisions matter, compare fetchFfiecBulkPage() callUpdated against your stored updatedAt and refetch that quarter with period pinned.

downloadFfiecBulkData({ product, period, format }) downloads any of the six CDR bulk products (call-single, call-four-period, ubpr-ratio-single, ubpr-ratio-four, ubpr-rank-four, ubpr-stats-four) as TSV or XBRL bytes. The pure parsers parseFfiecCallBundle, parseFfiecFourPeriodBundle, and parseFfiecUbprBundle turn TSV archives into typed bundles. ubpr-ratio-single is offered only as XBRL (about 100 MB) and no XBRL parser ships, so you get the bytes.

Holding companies, branches, and structure ​

ts
import { fetchFfiec002Report, fetchFry9Data, fetchNicData } from "@xbbg/xnews";

const structure = await fetchNicData("relationships", { limit: 1_000 });
const holdingCompany = await fetchFry9Data("fry9c", { period: "2026-06-30", rssdIds: [1025309] });
const branch = await fetchFfiec002Report({ rssdId: 112819, reportingDate: "2026-06-30" });

console.log(structure?.asOf, structure?.rows.length);
console.log(holdingCompany?.rows[0]);
console.log(branch?.institution, branch?.rows.length);

NIC's asOf is the snapshot date the download page states, never the clock, and open-ended relationship dates (12/31/9999) are normalized rather than passed through. FR Y-9 filings can change until the 45-day deadline, so a quarter fetched early is provisional. An FFIEC 002 response for a different RSSD than requested fails closed.

Mortgage, CRA, and census data ​

ts
import {
  fetchCraAvailableYears,
  fetchFfiecGeocode,
  fetchHmdaCount,
  fetchHmdaLoanRecords,
  hmdaDataSource,
} from "@xbbg/xnews";

const dc = await fetchHmdaCount({ years: [2023], states: ["DC"] });
const originations = await fetchHmdaLoanRecords({ years: [2023], states: ["DC"], actions_taken: [1] });
const yearlyAggregates = hmdaDataSource(2023, { states: ["DC"], actions_taken: [1] });

const craYears = await fetchCraAvailableYears(); // newest first
const tract = await fetchFfiecGeocode("1600 Pennsylvania Avenue NW, Washington, DC 20500");

console.log(dc?.count, originations.length, yearlyAggregates.dataset, craYears[0], tract?.fips);
  • HMDA aggregations group by at most two dimensions (the service rejects three); HMDA_AGGREGATION_DIMENSIONS lists the twelve groupable fields and unknown fields are refused. The public loan-level data is CFPB's modified LAR, perturbed and truncated for privacy, so it does not reconcile to a lender's books. The CFPB edge rejects bot-shaped User-Agents, so HMDA requests default to a browser-shaped one.
  • CRA flat files are fixed-width, typed per record type, with the original line kept; an unrecognized record code fails the parse instead of dropping the row. craDataSource(kind, { year: "latest" }) follows the years FFIEC actually links.
  • FFIEC publishes no documented geocoding API. fetchFfiecGeocode follows the manifest that FFIEC's own geomap application publishes, a binding that can change without notice.

DTCC swap dissemination (dtcc-sdr) ​

DTCC's Public Price Dissemination publishes swap transactions from its CFTC and SEC repositories, free and keyless, two ways: intraday slices listed in a catalog that only covers recent days, and cumulative end-of-day files.

ts
import {
  createDataReleaseWatcher,
  dtccSliceDataSource,
  fetchDtccCumulativeEvents,
  fetchDtccSliceCatalog,
  fetchDtccSliceEvents,
} from "@xbbg/xnews";

// Newest end-of-day file (walks back from today).
const eod = await fetchDtccCumulativeEvents();
console.log(eod?.businessDate, eod?.events.length);

// One intraday slice.
const catalog = await fetchDtccSliceCatalog({ agency: "cftc", assetClass: "credits" });
const latest = catalog.at(-1);
if (latest) {
  const events = await fetchDtccSliceEvents(latest);
  console.log(latest.sliceId, events[0]?.notionalAmountLeg1);
}

// Tail every slice in order; the watcher drains backlogs without sleeping.
for await (const result of createDataReleaseWatcher(dtccSliceDataSource({ assetClass: "rates" }), {
  intervalMs: 60_000,
})) {
  console.log(result.release?.sequence, result.rowCount);
}

agency is "cftc" (default) or "sec"; assetClass is "credits" (default), "rates", "equities", "forex", or "commodities". Events carry typed identity, lifecycle, product, and price fields (disseminationId, lineageId, actionType, executionTimestamp, notionalAmountLeg1, spreadLeg1, price, uniqueProductIdentifier, and more) plus the full row in raw. Numeric-looking values stay strings: DTCC formats notionals with thousands separators and caps block trades with a trailing + ("25,000,000+"), so parsing them is your decision. dtccCumulativeDataSource binds the end-of-day files to the same watcher.

Bridge releases into news ​

Three bridges turn a release into NewsItem records with kind: "data", so data can flow through the same pipeline as headlines:

ts
import { cotDataSource, cotReleaseToNewsItems, fetchDataRelease } from "@xbbg/xnews";

const result = await fetchDataRelease(cotDataSource("legacy", { markets: ["ZN"] }));
if (result.release) {
  const items = cotReleaseToNewsItems(result.release);
  console.log(items.map((item) => `${item.kind} ${item.title}`));
}

cotReleaseToNewsItems, ffiecReleaseToNewsItems, and dtccReleaseToNewsItems cover COT, CDR Call Reports, and DTCC releases.

Write your own data source ​

Any object with provider, dataset, requestUrls(), and fetchRelease() works with fetchDataRelease and createDataReleaseWatcher:

ts
import { createDataReleaseWatcher, type DataFetchOptions, type DataRelease, type DataSource } from "@xbbg/xnews";

interface RateRow {
  readonly tenor: string;
  readonly rate: number;
}

const url = "https://data.example.com/curve/latest.json";

const curveSource: DataSource<RateRow> = {
  provider: "example-curve",
  dataset: "par-curve",
  requestUrls: () => [url],
  async fetchRelease(options?: DataFetchOptions): Promise<DataRelease<RateRow> | undefined> {
    const response = await fetch(url, options?.signal ? { signal: options.signal } : {});
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const body = (await response.json()) as { asOf: string; rates: RateRow[] };
    if (options?.ifNewerThan !== undefined && body.asOf <= options.ifNewerThan) return undefined;
    return { provider: "example-curve", dataset: "par-curve", asOf: body.asOf, url, rows: body.rates };
  },
};

for await (const result of createDataReleaseWatcher(curveSource, { intervalMs: 30 * 60_000 })) {
  console.log(result.status, result.release?.asOf);
}

The release must echo the source's provider and dataset, asOf must be a real YYYY-MM-DD date, and sequence, if present, a safe integer; otherwise fetchDataRelease reports an error. A thrown error becomes status: "error" (or "disabled" for an XnewsFetchError with code config).

xbbg is independent open-source software and is not affiliated with, endorsed by, sponsored by, or approved by Bloomberg Finance L.P.