spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import { defineConnector, 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";4import { conventionalPair, fxHint } from "../ecb-frankfurter/index.js";56const BASE = "https://www.bankofcanada.ca/valet/observations";7const RATE_SERIES: Record<string, { symbol: string; name: string; assetClass: "INTEREST_RATE" | "BOND"; field: "RATE" | "YIELD" }> = {8 V39079: { symbol: "CA_POLICY_RATE", name: "Bank of Canada target for the overnight rate", assetClass: "INTEREST_RATE", field: "RATE" },9 "BD.CDN.2YR.DQ.YLD": { symbol: "CA_2Y", name: "Government of Canada 2-year benchmark bond yield", assetClass: "BOND", field: "YIELD" },10 "BD.CDN.5YR.DQ.YLD": { symbol: "CA_5Y", name: "Government of Canada 5-year benchmark bond yield", assetClass: "BOND", field: "YIELD" },11 "BD.CDN.10YR.DQ.YLD": { symbol: "CA_10Y", name: "Government of Canada 10-year benchmark bond yield", assetClass: "BOND", field: "YIELD" },12 "BD.CDN.LONG.DQ.YLD": { symbol: "CA_30Y", name: "Government of Canada long-term benchmark bond yield", assetClass: "BOND", field: "YIELD" },13};14const rateHint = (s: (typeof RATE_SERIES)[string]): InstrumentHint => ({ assetClass: s.assetClass, name: s.name, exchangeId: null, currency: "CAD", country: "CA", securityType: s.assetClass === "BOND" ? "GOVERNMENT_BOND_YIELD" : "POLICY_RATE", metadata: { featured: true, unit: "percent" } });1516const FX_CCYS = ["USD", "EUR", "GBP", "JPY", "CHF", "AUD", "CNY", "MXN", "INR", "BRL", "HKD", "KRW", "SEK", "NOK", "SGD", "NZD", "ZAR", "TRY"];17const seeds: ProposedInstrument[] = [18 ...Object.values(RATE_SERIES).map((s) => ({ symbol: s.symbol, hint: rateHint(s) })),19 ...FX_CCYS.map((c) => {20 const [b, q] = conventionalPair(c, "CAD");21 return { symbol: `${b}${q}`, hint: fxHint(b, q) };22 }),23];2425/** Bank of Canada Valet API: daily FX averages (CAD per unit) + policy rate + benchmark bond yields. Independent official source for USD/CAD. */26export const bankOfCanada = defineConnector({27 metadata: {28 id: "bank-of-canada-valet",29 name: "Bank of Canada — Valet (FX, policy rate, bond yields)",30 version: "1.0.0",31 sourceId: "bank-of-canada",32 organization: "Bank of Canada",33 sourceType: "OFFICIAL_API",34 jurisdiction: "CA",35 rightsStatus: "OFFICIAL_OPEN_DATA",36 realtimeStatus: "END_OF_DAY",37 expectedLatencyMs: null,38 supportsStreaming: false,39 supportsHistorical: true,40 assetClasses: ["FOREX", "INTEREST_RATE", "BOND"],41 exchanges: [],42 homepage: "https://www.bankofcanada.ca/valet/docs",43 description: "Bank of Canada Valet open API: daily average exchange rates (published ~16:30 ET), the target for the overnight rate and Government of Canada benchmark bond yields (2y, 5y, 10y, long).",44 rightsNotes: "Bank of Canada content is available under its terms of use with attribution; exchange rates are indicative daily averages.",45 termsUrl: "https://www.bankofcanada.ca/terms/",46 sourceFamily: "bank-of-canada",47 enabled: true,48 },49 seeds,50 defaultSymbols: seeds.map((s) => s.symbol),51 rateLimits: { "www.bankofcanada.ca": 1 },52 schedule: { intervalMs: 2 * 60 * 60_000 },53 async poll(ctx) {54 const out = [];55 const fx = await ctx.http.getJson(`${BASE}/group/FX_RATES_DAILY/json?recent=3`);56 if (!fx.response.notModified) out.push(raw("bank-of-canada-valet", "bank-of-canada", "fx", fx.data));57 const rates = await ctx.http.getJson(`${BASE}/${Object.keys(RATE_SERIES).join(",")}/json?recent=3`);58 if (!rates.response.notModified) out.push(raw("bank-of-canada-valet", "bank-of-canada", "rates", rates.data));59 return out;60 },61 normalize(r): NormalizedBatch {62 const p = r.payload as { observations?: Array<Record<string, { v?: string } | string>>; seriesDetail?: Record<string, { label?: string }> };63 if (!Array.isArray(p.observations)) return { observations: [] };64 const rows = [...p.observations].sort((a, b) => String(a.d).localeCompare(String(b.d)));65 const observations: NormalizedObservation[] = [];66 // Latest value per series (+ previous for PREVIOUS_CLOSE)67 const seriesIds = new Set(rows.flatMap((row) => Object.keys(row).filter((k) => k !== "d")));68 for (const sid of seriesIds) {69 const withValue = rows.filter((row) => typeof (row[sid] as { v?: string } | undefined)?.v === "string");70 const last = withValue[withValue.length - 1];71 const prev = withValue[withValue.length - 2];72 if (!last) continue;73 // Discontinued series (e.g. currencies the Bank stopped publishing) keep an old last value: skip anything > 10 days old.74 if (r.receivedAt - Date.parse(`${String(last.d)}T00:00:00Z`) > 10 * 86_400_000) continue;75 const emit = (row: Record<string, { v?: string } | string>, field: "LAST_PRICE" | "PREVIOUS_CLOSE" | "RATE" | "YIELD") => {76 const value = Number((row[sid] as { v?: string }).v);77 if (!Number.isFinite(value)) return;78 const date = String(row.d);79 const ts = zonedTimeToUtc(`${date}T16:30:00`, "America/Toronto");80 if (r.kind === "fx" && /^FX[A-Z]{3}CAD$/.test(sid)) {81 const foreign = sid.slice(2, 5);82 const [b, q] = conventionalPair(foreign, "CAD");83 const v = b === foreign ? value : 1 / value; // series is CAD per 1 unit of foreign84 observations.push({ symbol: `${b}${q}`, instrumentHint: fxHint(b, q), field: field === "RATE" || field === "YIELD" ? "LAST_PRICE" : field, value: Number(v.toPrecision(8)), currency: q, observationType: "OFFICIAL_FIX", sourceTimestamp: ts, timestampTrust: "SOURCE", rightsStatus: "OFFICIAL_OPEN_DATA", realtimeStatus: "END_OF_DAY", meta: { series: sid, date } });85 } else if (r.kind === "rates" && RATE_SERIES[sid]) {86 const s = RATE_SERIES[sid]!;87 observations.push({ symbol: s.symbol, instrumentHint: rateHint(s), field: field === "PREVIOUS_CLOSE" ? "PREVIOUS_CLOSE" : s.field, value, currency: "CAD", observationType: "REFERENCE_RATE", sourceTimestamp: ts, timestampTrust: "SOURCE", rightsStatus: "OFFICIAL_OPEN_DATA", realtimeStatus: "END_OF_DAY", meta: { series: sid, date, label: p.seriesDetail?.[sid]?.label } });88 if (field !== "PREVIOUS_CLOSE") observations.push({ symbol: s.symbol, instrumentHint: rateHint(s), field: "LAST_PRICE", value, currency: "CAD", observationType: "REFERENCE_RATE", sourceTimestamp: ts, timestampTrust: "SOURCE", rightsStatus: "OFFICIAL_OPEN_DATA", realtimeStatus: "END_OF_DAY", meta: { series: sid, date } });89 }90 };91 emit(last, "LAST_PRICE");92 if (prev) emit(prev, "PREVIOUS_CLOSE");93 }94 return { observations };95 },96 fixturesDir: "fixtures",97});98