import { defineConnector, raw, type NormalizedBatch, type ProposedBar, type ProposedInstrument } from "@market-atlas/connector-sdk"; import type { InstrumentHint, NormalizedObservation, RawObservation } from "@market-atlas/market-model"; import { zonedTimeToUtc } from "@market-atlas/market-model"; import { US_EQUITIES, US_ETFS, US_INDICES, equityHint, indexHint } from "../_shared/us-universe.js"; import { fxHint } from "../ecb-frankfurter/index.js"; import { cryptoHint } from "../_shared/crypto.js"; const BASE = "https://www.hfmarketdata.io/v1"; const ET = "America/New_York"; interface Target { path: string; // request path after /v1 symbol: string; // Market Atlas source symbol hint: InstrumentHint; kind: "bars" | "continuous"; sessionCloseLocal: string; // local close time of the daily bar timezone: string; } const FUTURES: Array<[string, string, string]> = [ ["GC", "Gold futures (continuous)", "xcme"], ["SI", "Silver futures (continuous)", "xcme"], ["CL", "WTI crude oil futures (continuous)", "xcme"], ["NG", "Henry Hub natural gas futures (continuous)", "xcme"], ["HG", "Copper futures (continuous)", "xcme"], ["ZC", "Corn futures (continuous)", "xcme"], ["ZW", "Wheat futures (continuous)", "xcme"], ["ZS", "Soybean futures (continuous)", "xcme"], ["ES", "E-mini S&P 500 futures (continuous)", "xcme"], ["NQ", "E-mini Nasdaq-100 futures (continuous)", "xcme"], ["ZN", "10-year T-note futures (continuous)", "xcme"], ["6E", "Euro FX futures (continuous)", "xcme"], ["BTC", "Bitcoin futures (continuous)", "xcme"], ]; const FX_PAIRS = ["EURUSD", "USDJPY", "GBPUSD", "USDCHF", "USDCAD", "AUDUSD", "NZDUSD", "EURGBP", "EURJPY", "USDMXN", "USDCNH"]; const CRYPTO = ["BTC", "ETH", "SOL", "XRP", "ADA", "DOGE", "LTC", "LINK", "AVAX", "DOT"]; export const commodityHint = (root: string, name: string, exchangeId: string): InstrumentHint => ({ assetClass: ["ES", "NQ", "ZN", "6E", "BTC"].includes(root) ? "FUTURE" : "COMMODITY", name, exchangeId, mic: "XCME", currency: "USD", country: "US", securityType: "CONTINUOUS_FUTURE", metadata: { featured: ["GC", "CL", "SI", "NG", "ES"].includes(root), roll: "volume", adjust: "none" }, }); const TARGETS: Target[] = [ ...US_EQUITIES.map(([s, n, v]) => ({ path: `/bars/stock/${encodeURIComponent(s)}?timeframe=1day&limit=400&order=desc`, symbol: s, hint: equityHint(s, n, v, "EQUITY"), kind: "bars" as const, sessionCloseLocal: "16:00", timezone: ET })), ...US_ETFS.map(([s, n, v]) => ({ path: `/bars/etf/${s}?timeframe=1day&limit=400&order=desc`, symbol: s, hint: equityHint(s, n, v, "ETF"), kind: "bars" as const, sessionCloseLocal: "16:00", timezone: ET })), ...US_INDICES.filter((i) => i.hfmd).map((i) => ({ path: `/bars/index/${i.hfmd}?timeframe=1day&limit=400&order=desc`, symbol: i.cboe, hint: indexHint(i), kind: "bars" as const, sessionCloseLocal: "16:00", timezone: ET })), ...FX_PAIRS.map((p) => ({ path: `/bars/fx/${p}?timeframe=1day&limit=400&order=desc`, symbol: p, hint: fxHint(p.slice(0, 3), p.slice(3)), kind: "bars" as const, sessionCloseLocal: "17:00", timezone: ET })), ...CRYPTO.map((c) => ({ path: `/bars/crypto/${c}?timeframe=1day&limit=400&order=desc`, symbol: `${c}-USD`, hint: cryptoHint(c, "USD", "coinbase"), kind: "bars" as const, sessionCloseLocal: "23:59:59", timezone: "UTC" })), ...FUTURES.map(([root, name, ex]) => ({ path: `/futures/${root}/continuous?timeframe=1day&limit=1000&roll=volume&adjust=none`, symbol: `${root}=F`, hint: commodityHint(root, name, ex), kind: "continuous" as const, sessionCloseLocal: "17:00", timezone: "America/Chicago" })), ]; const seeds: ProposedInstrument[] = FUTURES.map(([root, name, ex]) => ({ symbol: `${root}=F`, hint: commodityHint(root, name, ex), aliases: [root, `${root}1!`] })); /** * HF Market Data (Market Atlas' sister platform, own FirstRate-derived lake): daily OHLCV history for * the curated universe + CME continuous futures (gold, oil, gas, grains, index/rates futures). * Provides chart history (bars, producer = this connector) and END_OF_DAY reference values. */ export const hfmarketdata = defineConnector({ metadata: { id: "hfmarketdata-daily", name: "HF Market Data — daily bars & continuous futures", version: "1.0.0", sourceId: "hfmarketdata", organization: "spboucher.ai", sourceType: "OFFICIAL_API", jurisdiction: "CA", rightsStatus: "LICENSED", realtimeStatus: "END_OF_DAY", expectedLatencyMs: null, supportsStreaming: false, supportsHistorical: true, assetClasses: ["EQUITY", "ETF", "INDEX", "FOREX", "CRYPTO", "COMMODITY", "FUTURE"], exchanges: ["xcme"], homepage: "https://www.hfmarketdata.io", description: "Daily OHLCV bars (up to 400 sessions) for the curated US universe, major FX pairs, crypto and CME continuous futures from the HF Market Data lake (updated weekly from FirstRate). Backfills chart history and provides end-of-day reference closes.", rightsNotes: "Licensed sister platform (same operator). Redistribution of daily bars permitted under the HF Market Data data licence.", termsUrl: "https://www.hfmarketdata.io/data-license", sourceFamily: "firstrate", enabled: true, }, seeds, defaultSymbols: TARGETS.map((t) => t.symbol), rateLimits: { "www.hfmarketdata.io": 1.5 }, schedule: { intervalMs: 6 * 60 * 60_000 }, async poll(ctx) { const key = ctx.secret("HFMD_API_KEY"); const headers: Record = key ? { authorization: `Bearer ${key}` } : {}; const out: RawObservation[] = []; // ~120 requests every 6 h through the 1.5 req/s bucket (≈ 80 s per poll) — far below quotas. // Continuous futures return the oldest rows first → ask from ~400 sessions back. const from = new Date(ctx.now() - 560 * 86_400_000).toISOString().slice(0, 10); for (const t of TARGETS) { try { const url = t.kind === "continuous" ? `${BASE}${t.path}&from=${from}` : `${BASE}${t.path}`; const { data, response } = await ctx.http.getJson(url, { headers, timeoutMs: 45_000 }); if (response.notModified) continue; out.push(raw("hfmarketdata-daily", "hfmarketdata", t.kind, data, { symbol: t.symbol })); } catch (err) { ctx.reportError(err, { symbol: t.symbol }); } } return out; }, normalize(r): NormalizedBatch { const symbol = r.meta?.symbol as string | undefined; const target = TARGETS.find((t) => t.symbol === symbol); if (!target) return { observations: [] }; const p = r.payload as { data?: Array> }; if (!Array.isArray(p.data)) return { observations: [] }; const rows = p.data .map((d) => ({ date: String(d.datetime ?? "").slice(0, 10), open: num(d.open), high: num(d.high), low: num(d.low), close: num(d.close), volume: num(d.volume), contract: typeof d.symbol === "string" ? d.symbol : null })) .filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d.date) && d.open != null && d.high != null && d.low != null && d.close != null) .sort((a, b) => a.date.localeCompare(b.date)); const bars: ProposedBar[] = rows.map((d) => ({ symbol: target.symbol, instrumentHint: target.hint, resolution: "1d", ts: Date.parse(`${d.date}T00:00:00Z`), open: d.open!, high: d.high!, low: d.low!, close: d.close!, volume: d.volume })); const last = rows[rows.length - 1]; const prev = rows[rows.length - 2]; const observations: NormalizedObservation[] = []; if (last) { const ts = zonedTimeToUtc(`${last.date}T${target.sessionCloseLocal.length === 5 ? `${target.sessionCloseLocal}:00` : target.sessionCloseLocal}`, target.timezone); const base = { symbol: target.symbol, instrumentHint: target.hint, currency: target.hint.currency ?? "USD", observationType: "EOD_CLOSE" as const, sourceTimestamp: ts, timestampTrust: "SOURCE" as const, rightsStatus: "LICENSED" as const, realtimeStatus: "END_OF_DAY" as const, meta: { session: last.date, contract: last.contract } }; observations.push({ ...base, field: "LAST_PRICE", value: last.close! }, { ...base, field: "CLOSE", value: last.close! }, { ...base, field: "OPEN", value: last.open! }, { ...base, field: "HIGH", value: last.high! }, { ...base, field: "LOW", value: last.low! }); if (last.volume != null) observations.push({ ...base, field: "VOLUME", value: last.volume }); if (prev) observations.push({ ...base, field: "PREVIOUS_CLOSE", value: prev.close!, meta: { session: prev.date } }); } return { observations, bars, stats: { bars: bars.length } }; }, async healthCheck(ctx) { const { data } = await ctx.http.getJson<{ datasets?: unknown }>(`${BASE}/status`); return { ok: !!data?.datasets }; }, fixturesDir: "fixtures", }); const num = (v: unknown): number | null => { const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN; return Number.isFinite(n) ? n : null; };