Fed Treasury Yield Curve (H.15): Gets the daily constant maturity Treasury yield curve, from 1 month to 30 years, out of the Federal Reserve's H.15 CSV download. Adds a 10y-2y spread. No key needed.
| 1 | import { defineSource } from '@jaspers-ai/sdk' |
| 2 | import { z } from 'zod' |
| 3 | |
| 4 | |
| 5 | // The Federal Reserve Board's own data download, which answers CSV without a key. FRED is the |
| 6 | // friendlier way to the same numbers and wants a free key; this does not, so the yield curve is |
| 7 | // here for nothing. |
| 8 | |
| 9 | const HOST = 'www.federalreserve.gov' |
| 10 | /** H.15, the daily selected interest rates release: the constant maturity Treasury curve. */ |
| 11 | const H15 = 'bf17364827e38702b42a58cf8eaa3f78' |
| 12 | /** The maturities in the order the release gives them, which is the order a curve is read in. */ |
| 13 | const TENORS = ['1m', '3m', '6m', '1y', '2y', '3y', '5y', '7y', '10y', '20y', '30y'] |
| 14 | |
| 15 | /** A CSV line, honouring the quotes the Fed writes its headings in. */ |
| 16 | function cells(line: string): string[] { |
| 17 | const out: string[] = [] |
| 18 | let current = '' |
| 19 | let quoted = false |
| 20 | for (let i = 0; i < line.length; i++) { |
| 21 | const char = line[i]! |
| 22 | if (quoted) { |
| 23 | if (char === '"' && line[i + 1] === '"') { current += '"'; i++ } |
| 24 | else if (char === '"') quoted = false |
| 25 | else current += char |
| 26 | } else if (char === '"') quoted = true |
| 27 | else if (char === ',') { out.push(current); current = '' } |
| 28 | else current += char |
| 29 | } |
| 30 | out.push(current) |
| 31 | return out |
| 32 | } |
| 33 | |
| 34 | export const yields = defineSource({ |
| 35 | description: |
| 36 | 'The US Treasury yield curve, daily, from the Fed\'s own H.15 release: the constant maturity yield at each tenor from one month to thirty years. One row per day, one column per tenor, so a row is a curve and a column is a series.', |
| 37 | hosts: [HOST], |
| 38 | input: z.object({ days: z.number().int().min(1).max(5000).default(90).describe('Trading days to read, newest last.') }), |
| 39 | async run({ days }, ctx) { |
| 40 | const url = `https://${HOST}/datadownload/Output.aspx?rel=H15&series=${H15}&lastobs=${days}&filetype=csv&label=include&layout=seriescolumn` |
| 41 | const response = await ctx.fetch(url, { headers: { 'User-Agent': 'Jaspers Terminal (https://jsprai.com)' } }) |
| 42 | if (!response.ok) throw new Error(`The Federal Reserve answered ${response.status}.`) |
| 43 | const lines = (await response.text()).split(/\r?\n/).filter((line) => line.trim() !== '') |
| 44 | // Five heading rows describe the series; the data starts where a line begins with a date. |
| 45 | const start = lines.findIndex((line) => /^\d{4}-\d{2}-\d{2},/.test(line)) |
| 46 | if (start === -1) throw new Error('The Fed answered in a shape this does not know how to read.') |
| 47 | const rows = lines.slice(start).map((line) => { |
| 48 | const parts = cells(line) |
| 49 | const row: Record<string, unknown> = { date: parts[0] } |
| 50 | TENORS.forEach((tenor, index) => { |
| 51 | const value = parts[index + 1] |
| 52 | // The release writes ND on a day a rate was not published. |
| 53 | row[tenor] = value && value !== 'ND' && Number.isFinite(Number(value)) ? Number(value) : null |
| 54 | }) |
| 55 | // What everyone actually watches. |
| 56 | row['10y-2y'] = typeof row['10y'] === 'number' && typeof row['2y'] === 'number' ? Number((row['10y'] - row['2y']).toFixed(2)) : null |
| 57 | return row |
| 58 | }) |
| 59 | return { tenors: TENORS, rows } |
| 60 | }, |
| 61 | }) |