import { expect, test } from "bun:test"; import { fetchYahooChart, fetchYahooQuoteSupplement } from "./requests"; import type { YahooHttpClient } from "./http"; function chart(granularity: unknown) { return { chart: { result: [{ meta: { symbol: "MSFT", currency: "USD", dataGranularity: granularity }, timestamp: [Date.parse("1965-01-01") / 1000, Date.parse("2026-08-01") / 1000], indicators: { quote: [{ open: [2, 4], high: [4, 6], low: [1, 3], close: [3, 5], volume: [12, 34] }] }, }] } }; } function http(raw: unknown, urls: URL[]) { return { fetchJson: async (url: string) => { urls.push(new URL(url)); return structuredClone(raw); } } as unknown as YahooHttpClient; } test("native full history requests explicit bounds and retains exact prices and pre-1970 source dates", async () => { const urls: URL[] = [];const raw = chart("1mo"); const result = await fetchYahooChart(http(raw, urls), "MSFT", "max", "1mo"); expect(urls[0]!.searchParams.has("range")).toBe(false); expect(Number(urls[0]!.searchParams.get("period1"))).toBeLessThan(Date.parse("1965-01-01") / 1000); expect(Number(urls[0]!.searchParams.get("period2"))).toBeGreaterThan(Date.now() / 1000 - 10); expect(result.history.map((p) => p.date.toISOString().slice(0, 10))).toEqual(["1965-01-01", "2026-08-01"]); expect(result.history.map((p) => [p.open, p.high, p.low, p.close, p.volume])).toEqual([[2, 4, 1, 3, 12], [4, 6, 3, 5, 34]]); expect(raw.chart.result[0]!.indicators.quote[0]!.close).toEqual([3, 5]); }); test("native history rejects weekly or quarterly data mislabeled as monthly, and missing cadence", async () => { for (const served of ["1wk", "3mo", "1d", undefined]) { await expect(fetchYahooChart(http(chart(served), []), "SHIB-USD", "max", "1mo")).rejects.toThrow("requested 1mo"); } }); test("finite windows preserve range and equivalent hourly cadence is accepted", async () => { const urls: URL[] = []; await fetchYahooChart(http(chart("60m"), urls), "MSFT", "1mo", "1h"); expect(urls[0]!.searchParams.get("range")).toBe("1mo"); expect(urls[0]!.searchParams.has("period1")).toBe(false); }); test("the current period is completed from the chart's own regular-market facts", async () => { // NVDA as Yahoo served it before the open on 2026-09-23. const meta = { symbol: "NVDA", currency: "USD", regularMarketPrice: 228.87, regularMarketChangePercent: 0.655, regularMarketTime: Date.parse("2026-09-22T20:00:00Z") / 1000 }; const live = { t: "2026-09-22T20:00:00Z", o: 226.91, h: 229.98, l: 226.5, c: 228.87, v: 93296546 }; const served = (granularity: string, rows: Array) => ({ chart: { result: [{ meta: { ...meta, dataGranularity: granularity }, timestamp: rows.map((row) => Date.parse(row.t) / 1000), indicators: { quote: [{ open: rows.map((row) => row.o), high: rows.map((row) => row.h), low: rows.map((row) => row.l), close: rows.map((row) => row.c), volume: rows.map((row) => row.v) }] } }] } }); const bars = async (granularity: string, rows: Parameters[1]) => (await fetchYahooChart(http(served(granularity, rows), []), "NVDA", "3mo", granularity)).history .map((p) => [p.date.toISOString().slice(0, 10), p.high, p.low, p.close, p.volume]); // Yahoo leaves the finished session's daily close empty for hours. expect(await bars("1d", [{ t: "2026-09-21T13:30:00Z", o: 222.94, h: 228.5, l: 221.56, c: 227.38, v: 109806100 }, { ...live, t: "2026-09-22T13:30:00Z", c: null }, { t: "2026-09-23T13:30:00Z", o: 0, h: 0, l: 0, c: null, v: 0 }])) .toEqual([["2026-09-21", 228.5, 221.56, 227.38, 109806100], ["2026-09-22", 229.98, 226.5, 228.87, 93296546]]); // The weekly row stops at Monday; the trailing observation is Tuesday. expect(await bars("1wk", [{ t: "2026-09-21T04:00:00Z", o: 222.94, h: 228.5, l: 221.56, c: 227.38, v: 109806100 }, live])) .toEqual([["2026-09-21", 229.98, 221.56, 228.87, 203102646]]); // Mid-session the row can already hold part of Tuesday: newer prices, never Tuesday's volume twice. expect(await bars("1wk", [{ t: "2026-09-21T04:00:00Z", o: 222.94, h: 229.5, l: 221.56, c: 228.4, v: 150000000 }, live])) .toEqual([["2026-09-21", 229.98, 221.56, 228.87, 150000000]]); // The monthly row already includes it. expect(await bars("1mo", [{ t: "2026-09-01T04:00:00Z", o: 216.75, h: 234.76, l: 208.93, c: 228.87, v: 1744259600 }, live])) .toEqual([["2026-09-01", 234.76, 208.93, 228.87, 1744259600]]); }); test("FX calendar bars take the London date Yahoo stamps at 23:00 UTC the evening before", async () => { const raw = { chart: { result: [{ meta: { symbol: "EURUSD=X", currency: "USD", exchangeTimezoneName: "Europe/London", dataGranularity: "1d" }, timestamp: ["2026-09-17T23:00:00Z", "2026-09-20T23:00:00Z", "2026-12-07T00:00:00Z"].map((date) => Date.parse(date) / 1000), indicators: { quote: [{ close: [1.1476, 1.14797, 1.1] }] }, }] } }; const result = await fetchYahooChart(http(raw, []), "EURUSD=X", "1y", "1d"); // Friday, Monday, and a winter Monday already at 00:00 UTC. expect(result.history.map((p) => p.date.toISOString())).toEqual([ "2026-09-18T00:00:00.000Z", "2026-09-21T00:00:00.000Z", "2026-12-07T00:00:00.000Z", ]); }); test("FX daily ranges cover an open and close a few pips outside them, so close-to-close risk keeps the pair", async () => { // EURUSD=X as Yahoo served 2025-10-08: open and close above the high. const raw = { chart: { result: [{ meta: { symbol: "EURUSD=X", currency: "USD", instrumentType: "CURRENCY", exchangeTimezoneName: "Europe/London", dataGranularity: "1d" }, timestamp: [Date.parse("2025-10-07T23:00:00Z") / 1000], indicators: { quote: [{ open: [1.16546], high: [1.1653], low: [1.16036], close: [1.16546], volume: [0] }] }, }] } }; const [bar] = (await fetchYahooChart(http(raw, []), "EURUSD=X", "1y", "1d")).history; expect([bar!.open, bar!.high, bar!.low, bar!.close]).toEqual([1.16546, 1.16546, 1.16036, 1.16546]); }); test("the trailing trade row of an intraday chart has no known volume", async () => { const raw = { chart: { result: [{ meta: { symbol: "AAPL", currency: "USD", dataGranularity: "1m" }, timestamp: ["2026-09-23T17:44:00Z", "2026-09-23T17:45:24Z"].map((date) => Date.parse(date) / 1000), indicators: { quote: [{ open: [337.3, 337.46], high: [337.4, 337.46], low: [337.2, 337.46], close: [337.35, 337.46], volume: [24863, 0] }] }, }] } }; const history = (await fetchYahooChart(http(raw, []), "AAPL", "1d", "1m")).history; expect(history.map((point) => point.volume)).toEqual([24863, undefined]); }); test("a continuous futures alias is named after the contract its price belongs to", async () => { const supplement = async (symbol: string, price: Record) => { const urls: URL[] = []; const client = { fetchJsonWithCrumb: async (url: string) => { urls.push(new URL(url)); return { quoteSummary: { result: [{ summaryDetail: { previousClose: { raw: 18.56 } }, price }] } }; } } as unknown as YahooHttpClient; return { result: await fetchYahooQuoteSupplement(client, symbol), modules: urls[0]!.searchParams.get("modules") }; }; // SB=F kept "Oct 26" in its name while quoting the March contract. expect(await supplement("SB=F", { shortName: "Sugar #11 Oct 26", underlyingSymbol: "SBH27.NYB" })) .toEqual({ result: { previousClose: 18.56, name: "Sugar #11 Mar 27" }, modules: "summaryDetail,price" }); expect((await supplement("ZB=F", { shortName: "U.S. Treasury Bond Futures,Dec-", underlyingSymbol: "ZBH27.CBT" })).result.name) .toBe("U.S. Treasury Bond Futures Mar 27"); // No month in the underlying, another root, or a plain listing: the chart name stands. expect((await supplement("BZ=F", { shortName: "Brent Crude Oil Last Day Financ", underlyingSymbol: "BZ.NYM" })).result.name).toBeUndefined(); expect((await supplement("SB=F", { shortName: "Sugar #11 Oct 26", underlyingSymbol: "KCZ26.NYB" })).result.name).toBeUndefined(); expect(await supplement("AAPL", { shortName: "Apple Inc." })).toEqual({ result: { previousClose: 18.56 }, modules: "summaryDetail" }); });