src/assets/marketHours.test.ts
v0.3.0 · 7.6 KB
import { describe, expect, it, vi } from 'vitest';
import { easternTimeParts, getMarketSession, isMarketOpen, NYSE_CALENDAR } from './marketHours.js';
const at = (iso: string) => new Date(iso);
describe('isMarketOpen: knowledge-base test cases', () => {
it('Tue 2026-06-02: 03:00 EDT closed; 04:30 / 11:00 / 19:59 open; 20:30 closed', () => {
expect(isMarketOpen('xyz:TSLA', at('2026-06-02T07:00:00Z'))).toBe(false);
expect(isMarketOpen('xyz:TSLA', at('2026-06-02T08:30:00Z'))).toBe(true);
expect(isMarketOpen('xyz:TSLA', at('2026-06-02T15:00:00Z'))).toBe(true);
expect(isMarketOpen('xyz:TSLA', at('2026-06-02T23:59:00Z'))).toBe(true);
expect(isMarketOpen('xyz:TSLA', at('2026-06-03T00:30:00Z'))).toBe(false);
});
it('Saturday is closed', () => {
expect(getMarketSession('xyz:SP500', at('2026-06-06T15:00:00Z'))).toMatchObject({ open: false, reason: 'weekend' });
});
it('holidays are closed: observed Independence Day 2026-07-03, Christmas 2026-12-25', () => {
expect(getMarketSession('xyz:NVDA', at('2026-07-03T15:00:00Z'))).toMatchObject({ open: false, reason: 'holiday' });
expect(isMarketOpen('xyz:NVDA', at('2026-12-25T16:00:00Z'))).toBe(false);
});
it('half-day 2026-11-27 closes at 13:00 EST: 11:00 open, 14:00 closed', () => {
expect(getMarketSession('xyz:HOOD', at('2026-11-27T16:00:00Z'))).toMatchObject({ open: true, halfDay: true });
expect(getMarketSession('xyz:HOOD', at('2026-11-27T19:00:00Z'))).toMatchObject({ open: false, reason: 'after_close', halfDay: true });
});
it('Wed 2026-11-25 14:00 EST is a normal open day', () => {
expect(isMarketOpen('xyz:HOOD', at('2026-11-25T19:00:00Z'))).toBe(true);
});
it('xyz:CL (oil) is always on: Saturday and holiday night', () => {
expect(getMarketSession('xyz:CL', at('2026-06-06T15:00:00Z'))).toMatchObject({ open: true, reason: 'always_on' });
expect(isMarketOpen('xyz:CL', at('2026-07-03T03:00:00Z'))).toBe(true);
});
});
describe('isMarketOpen: boundaries and DST', () => {
it('20:00 ET is exclusive, 04:00 ET inclusive', () => {
expect(isMarketOpen('xyz:TSLA', at('2026-06-03T00:00:00Z'))).toBe(false); // 20:00 EDT
expect(isMarketOpen('xyz:TSLA', at('2026-06-02T08:00:00Z'))).toBe(true); // 04:00 EDT
});
it('follows EST before and EDT after the 2026-03-08 transition', () => {
expect(isMarketOpen('xyz:TSLA', at('2026-03-06T08:59:00Z'))).toBe(false); // Fri 03:59 EST
expect(isMarketOpen('xyz:TSLA', at('2026-03-06T09:00:00Z'))).toBe(true); // Fri 04:00 EST
expect(isMarketOpen('xyz:TSLA', at('2026-03-09T07:59:00Z'))).toBe(false); // Mon 03:59 EDT
expect(isMarketOpen('xyz:TSLA', at('2026-03-09T08:00:00Z'))).toBe(true); // Mon 04:00 EDT
});
it('uses the ET calendar date, not the UTC one', () => {
// Sat 2026-06-06 01:00Z is still Friday 21:00 EDT: closed by the session, not by the weekend
expect(getMarketSession('xyz:TSLA', at('2026-06-06T01:00:00Z'))).toMatchObject({ reason: 'after_close', etDate: '2026-06-05' });
// Mon 2026-06-08 02:00Z is Sunday 22:00 EDT
expect(getMarketSession('xyz:TSLA', at('2026-06-08T02:00:00Z'))).toMatchObject({ reason: 'weekend', etDate: '2026-06-07' });
});
});
describe('easternTimeParts', () => {
it('formats midnight as hour 0 (hourCycle h23 pitfall: hour12:false yields 24)', () => {
const p = easternTimeParts(at('2026-06-02T04:30:00Z'));
expect(p).toMatchObject({ ymd: '2026-06-02', hour: 0, minute: 30, weekday: 2 });
expect(getMarketSession('xyz:TSLA', at('2026-06-02T04:30:00Z')).etMinutes).toBe(30);
});
it('returns null for invalid dates', () => {
expect(easternTimeParts(new Date('nope'))).toBeNull();
expect(easternTimeParts(Number.NaN)).toBeNull();
});
});
describe('getMarketSession: scope and options', () => {
it('does not gate main-dex crypto or other HIP-3 dexes by default', () => {
expect(getMarketSession('BTC', at('2026-06-06T15:00:00Z'))).toMatchObject({ open: true, reason: 'not_gated' });
expect(isMarketOpen('flx:ABC', at('2026-06-06T15:00:00Z'))).toBe(true);
expect(isMarketOpen('PURR/USDC', at('2026-06-06T15:00:00Z'))).toBe(true);
});
it('matches the dex prefix case-insensitively', () => {
expect(isMarketOpen('XYZ:TSLA', at('2026-06-06T15:00:00Z'))).toBe(false);
});
it('supports custom gated dexes and always-on lists', () => {
expect(isMarketOpen('flx:ABC', at('2026-06-06T15:00:00Z'), { gatedDexes: ['xyz', 'flx'] })).toBe(false);
expect(isMarketOpen('xyz:GOLD', at('2026-06-06T15:00:00Z'), { alwaysOn: ['xyz:GOLD'] })).toBe(true);
// replacing the list removes the CL default
expect(isMarketOpen('xyz:CL', at('2026-06-06T15:00:00Z'), { alwaysOn: ['xyz:GOLD'] })).toBe(false);
});
it('unknown year: holidays are not applied, warning fires once per year', () => {
const warn = vi.fn();
// 2028-07-04 is a Tuesday; the calendar has no 2028 table
const s = getMarketSession('xyz:TSLA', at('2028-07-04T15:00:00Z'), { onUnknownYear: warn });
expect(s).toMatchObject({ open: true, calendarKnown: false });
isMarketOpen('xyz:TSLA', at('2028-07-05T15:00:00Z'), { onUnknownYear: warn });
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toHaveBeenCalledWith(2028);
// weekends still apply outside the table
expect(isMarketOpen('xyz:TSLA', at('2028-07-08T15:00:00Z'), { onUnknownYear: warn })).toBe(false);
});
it('accepts a custom calendar', () => {
const cal = { years: [2026], fullHolidays: ['2026-06-02'], halfDays: [] };
expect(getMarketSession('xyz:TSLA', at('2026-06-02T15:00:00Z'), { calendar: cal })).toMatchObject({ reason: 'holiday' });
});
it('fails closed on invalid dates for gated coins only', () => {
expect(getMarketSession('xyz:TSLA', new Date('invalid'))).toMatchObject({ open: false, reason: 'invalid_date' });
expect(isMarketOpen('BTC', new Date('invalid'))).toBe(true);
});
it('ships the 2026-2027 NYSE tables', () => {
expect(NYSE_CALENDAR.fullHolidays).toHaveLength(20);
expect(NYSE_CALENDAR.halfDays).toEqual(['2026-11-27', '2026-12-24', '2027-11-26']);
});
});
describe('isMarketOpen: calendar edges', () => {
it('2027 floating and observed holidays are closed', () => {
expect(getMarketSession('xyz:TSLA', at('2027-03-26T14:00:00Z'))).toMatchObject({ reason: 'holiday' }); // Good Friday
expect(getMarketSession('xyz:TSLA', at('2027-06-18T14:00:00Z'))).toMatchObject({ reason: 'holiday' }); // Juneteenth observed
expect(getMarketSession('xyz:TSLA', at('2027-12-24T15:00:00Z'))).toMatchObject({ reason: 'holiday' }); // Christmas observed
});
it('half-day 2026-12-24: 12:59 EST open, 13:00 EST closed', () => {
expect(isMarketOpen('xyz:SP500', at('2026-12-24T17:59:00Z'))).toBe(true);
expect(getMarketSession('xyz:SP500', at('2026-12-24T18:00:00Z'))).toMatchObject({ open: false, reason: 'after_close', halfDay: true });
});
it('New Year: 2026-12-31 19:59 EST open while UTC is already 2027; 2027-01-01 is a holiday', () => {
expect(isMarketOpen('xyz:TSLA', at('2027-01-01T00:59:00Z'))).toBe(true);
expect(getMarketSession('xyz:TSLA', at('2027-01-01T15:00:00Z'))).toMatchObject({ reason: 'holiday', etDate: '2027-01-01' });
});
it('DST fall back 2026-11-01: Monday 04:00 EST = 09:00Z', () => {
expect(isMarketOpen('xyz:TSLA', at('2026-11-02T08:59:00Z'))).toBe(false);
expect(isMarketOpen('xyz:TSLA', at('2026-11-02T09:00:00Z'))).toBe(true);
});
it('accepts epoch milliseconds (pass HL server time: local clocks drift)', () => {
expect(isMarketOpen('xyz:TSLA', Date.parse('2026-09-14T20:39:37Z'))).toBe(true); // Mon 16:39 EDT
expect(getMarketSession('xyz:CL', Number.NaN)).toMatchObject({ open: true, reason: 'always_on' });
});
});