import { describe, expect, it } from "vitest"; import type { Exchange } from "@market-atlas/market-model"; import { MarketCalendar } from "./calendar.js"; const xnys: Exchange = { id: "xnys", mic: "XNYS", name: "NYSE", operator: null, country: "US", city: null, timezone: "America/New_York", currency: "USD", website: null, lat: null, lon: null, assetClasses: ["EQUITY"], sessions: { regular: [{ open: "09:30", close: "16:00" }], pre: { open: "04:00", close: "09:30" }, post: { open: "16:00", close: "20:00" } } }; const xtks: Exchange = { ...xnys, id: "xtks", timezone: "Asia/Tokyo", sessions: { regular: [{ open: "09:00", close: "11:30" }, { open: "12:30", close: "15:30" }] } }; const xsau: Exchange = { ...xnys, id: "xsau", timezone: "Asia/Riyadh", sessions: { regular: [{ open: "10:00", close: "15:00" }], weekdays: [0, 1, 2, 3, 4] } }; const coinbase: Exchange = { ...xnys, id: "coinbase", timezone: "UTC", sessions: { regular: [], continuous: true } }; const cal = new MarketCalendar(); cal.load([xnys, xtks, xsau, coinbase], [ { exchangeId: "xnys", date: "2026-09-07", name: "Labor Day", kind: "CLOSED", sourceId: "t" }, { exchangeId: "xnys", date: "2026-11-27", name: "Early close", kind: "EARLY_CLOSE", closeTime: "13:00", sourceId: "t" }, ]); const at = (iso: string) => Date.parse(iso); describe("market calendar", () => { it("NYSE regular, pre, post, closed and weekend", () => { expect(cal.status("xnys", at("2026-09-11T14:00:00Z")).state).toBe("OPEN"); // 10:00 EDT Friday expect(cal.status("xnys", at("2026-09-11T12:00:00Z")).state).toBe("PRE"); // 08:00 EDT expect(cal.status("xnys", at("2026-09-11T21:00:00Z")).state).toBe("POST"); // 17:00 EDT expect(cal.status("xnys", at("2026-09-12T14:00:00Z")).state).toBe("CLOSED"); // Saturday expect(cal.status("xnys", at("2026-09-11T02:00:00Z")).state).toBe("CLOSED"); // 22:00 EDT Thursday }); it("holidays and early closes", () => { const labor = cal.status("xnys", at("2026-09-07T14:00:00Z")); expect(labor.state).toBe("CLOSED"); expect(labor.isHoliday).toBe(true); expect(labor.holidayName).toBe("Labor Day"); expect(cal.status("xnys", at("2026-11-27T17:00:00Z")).state).toBe("OPEN"); // 12:00 EST expect(cal.status("xnys", at("2026-11-27T18:30:00Z")).state).toBe("CLOSED"); // 13:30 EST after early close }); it("DST: winter session in UTC shifts by one hour", () => { expect(cal.status("xnys", at("2026-01-15T14:00:00Z")).state).toBe("PRE"); // 09:00 EST expect(cal.status("xnys", at("2026-01-15T14:30:00Z")).state).toBe("OPEN"); }); it("Tokyo lunch break and Riyadh Sunday trading", () => { expect(cal.status("xtks", at("2026-09-11T01:00:00Z")).state).toBe("OPEN"); // 10:00 JST expect(cal.status("xtks", at("2026-09-11T03:00:00Z")).state).toBe("CLOSED"); // 12:00 JST lunch expect(cal.status("xtks", at("2026-09-11T04:00:00Z")).state).toBe("OPEN"); // 13:00 JST expect(cal.status("xsau", at("2026-09-13T08:00:00Z")).state).toBe("OPEN"); // Sunday 11:00 AST expect(cal.status("xsau", at("2026-09-11T08:00:00Z")).state).toBe("CLOSED"); // Friday }); it("continuous venues are always open; next transition computed for NYSE", () => { expect(cal.status("coinbase", at("2026-09-12T03:00:00Z")).state).toBe("OPEN"); const s = cal.status("xnys", at("2026-09-11T14:00:00Z")); expect(s.nextTransition?.state).toBe("POST"); expect(new Date(s.nextTransition!.at).toISOString()).toBe("2026-09-11T20:00:00.000Z"); }); it("session start for the trading day", () => { expect(new Date(cal.sessionStart("xnys", at("2026-09-11T14:00:00Z"))!).toISOString()).toBe("2026-09-11T08:00:00.000Z"); // pre-market 04:00 EDT }); });