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%
8.7 KB · 145 lines typescript
Raw Blame History
1import { defineConnector, raw, type NormalizedBatch, type ProposedBar, type ProposedInstrument } from "@market-atlas/connector-sdk";2import type { InstrumentHint, NormalizedObservation, RawObservation } from "@market-atlas/market-model";3import { zonedTimeToUtc } from "@market-atlas/market-model";4import { US_EQUITIES, US_ETFS, US_INDICES, equityHint, indexHint } from "../_shared/us-universe.js";5import { fxHint } from "../ecb-frankfurter/index.js";6import { cryptoHint } from "../_shared/crypto.js";78const BASE = "https://www.hfmarketdata.io/v1";9const ET = "America/New_York";1011interface Target {12  path: string; // request path after /v113  symbol: string; // Market Atlas source symbol14  hint: InstrumentHint;15  kind: "bars" | "continuous";16  sessionCloseLocal: string; // local close time of the daily bar17  timezone: string;18}1920const FUTURES: Array<[string, string, string]> = [21  ["GC", "Gold futures (continuous)", "xcme"],22  ["SI", "Silver futures (continuous)", "xcme"],23  ["CL", "WTI crude oil futures (continuous)", "xcme"],24  ["NG", "Henry Hub natural gas futures (continuous)", "xcme"],25  ["HG", "Copper futures (continuous)", "xcme"],26  ["ZC", "Corn futures (continuous)", "xcme"],27  ["ZW", "Wheat futures (continuous)", "xcme"],28  ["ZS", "Soybean futures (continuous)", "xcme"],29  ["ES", "E-mini S&P 500 futures (continuous)", "xcme"],30  ["NQ", "E-mini Nasdaq-100 futures (continuous)", "xcme"],31  ["ZN", "10-year T-note futures (continuous)", "xcme"],32  ["6E", "Euro FX futures (continuous)", "xcme"],33  ["BTC", "Bitcoin futures (continuous)", "xcme"],34];35const FX_PAIRS = ["EURUSD", "USDJPY", "GBPUSD", "USDCHF", "USDCAD", "AUDUSD", "NZDUSD", "EURGBP", "EURJPY", "USDMXN", "USDCNH"];36const CRYPTO = ["BTC", "ETH", "SOL", "XRP", "ADA", "DOGE", "LTC", "LINK", "AVAX", "DOT"];3738export const commodityHint = (root: string, name: string, exchangeId: string): InstrumentHint => ({39  assetClass: ["ES", "NQ", "ZN", "6E", "BTC"].includes(root) ? "FUTURE" : "COMMODITY",40  name,41  exchangeId,42  mic: "XCME",43  currency: "USD",44  country: "US",45  securityType: "CONTINUOUS_FUTURE",46  metadata: { featured: ["GC", "CL", "SI", "NG", "ES"].includes(root), roll: "volume", adjust: "none" },47});4849const TARGETS: Target[] = [50  ...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 })),51  ...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 })),52  ...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 })),53  ...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 })),54  ...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" })),55  ...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" })),56];5758const seeds: ProposedInstrument[] = FUTURES.map(([root, name, ex]) => ({ symbol: `${root}=F`, hint: commodityHint(root, name, ex), aliases: [root, `${root}1!`] }));5960/**61 * HF Market Data (Market Atlas' sister platform, own FirstRate-derived lake): daily OHLCV history for62 * the curated universe + CME continuous futures (gold, oil, gas, grains, index/rates futures).63 * Provides chart history (bars, producer = this connector) and END_OF_DAY reference values.64 */65export const hfmarketdata = defineConnector({66  metadata: {67    id: "hfmarketdata-daily",68    name: "HF Market Data — daily bars & continuous futures",69    version: "1.0.0",70    sourceId: "hfmarketdata",71    organization: "spboucher.ai",72    sourceType: "OFFICIAL_API",73    jurisdiction: "CA",74    rightsStatus: "LICENSED",75    realtimeStatus: "END_OF_DAY",76    expectedLatencyMs: null,77    supportsStreaming: false,78    supportsHistorical: true,79    assetClasses: ["EQUITY", "ETF", "INDEX", "FOREX", "CRYPTO", "COMMODITY", "FUTURE"],80    exchanges: ["xcme"],81    homepage: "https://www.hfmarketdata.io",82    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.",83    rightsNotes: "Licensed sister platform (same operator). Redistribution of daily bars permitted under the HF Market Data data licence.",84    termsUrl: "https://www.hfmarketdata.io/data-license",85    sourceFamily: "firstrate",86    enabled: true,87  },88  seeds,89  defaultSymbols: TARGETS.map((t) => t.symbol),90  rateLimits: { "www.hfmarketdata.io": 1.5 },91  schedule: { intervalMs: 6 * 60 * 60_000 },92  async poll(ctx) {93    const key = ctx.secret("HFMD_API_KEY");94    const headers: Record<string, string> = key ? { authorization: `Bearer ${key}` } : {};95    const out: RawObservation[] = [];96    // ~120 requests every 6 h through the 1.5 req/s bucket (≈ 80 s per poll) — far below quotas.97    // Continuous futures return the oldest rows first → ask from ~400 sessions back.98    const from = new Date(ctx.now() - 560 * 86_400_000).toISOString().slice(0, 10);99    for (const t of TARGETS) {100      try {101        const url = t.kind === "continuous" ? `${BASE}${t.path}&from=${from}` : `${BASE}${t.path}`;102        const { data, response } = await ctx.http.getJson(url, { headers, timeoutMs: 45_000 });103        if (response.notModified) continue;104        out.push(raw("hfmarketdata-daily", "hfmarketdata", t.kind, data, { symbol: t.symbol }));105      } catch (err) {106        ctx.reportError(err, { symbol: t.symbol });107      }108    }109    return out;110  },111  normalize(r): NormalizedBatch {112    const symbol = r.meta?.symbol as string | undefined;113    const target = TARGETS.find((t) => t.symbol === symbol);114    if (!target) return { observations: [] };115    const p = r.payload as { data?: Array<Record<string, unknown>> };116    if (!Array.isArray(p.data)) return { observations: [] };117    const rows = p.data118      .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 }))119      .filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d.date) && d.open != null && d.high != null && d.low != null && d.close != null)120      .sort((a, b) => a.date.localeCompare(b.date));121    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 }));122    const last = rows[rows.length - 1];123    const prev = rows[rows.length - 2];124    const observations: NormalizedObservation[] = [];125    if (last) {126      const ts = zonedTimeToUtc(`${last.date}T${target.sessionCloseLocal.length === 5 ? `${target.sessionCloseLocal}:00` : target.sessionCloseLocal}`, target.timezone);127      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 } };128      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! });129      if (last.volume != null) observations.push({ ...base, field: "VOLUME", value: last.volume });130      if (prev) observations.push({ ...base, field: "PREVIOUS_CLOSE", value: prev.close!, meta: { session: prev.date } });131    }132    return { observations, bars, stats: { bars: bars.length } };133  },134  async healthCheck(ctx) {135    const { data } = await ctx.http.getJson<{ datasets?: unknown }>(`${BASE}/status`);136    return { ok: !!data?.datasets };137  },138  fixturesDir: "fixtures",139});140141const num = (v: unknown): number | null => {142  const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;143  return Number.isFinite(n) ? n : null;144};145