// Trading calendar utilities for NYSE trading days // This implementation relies on the 'nyse-holidays' package for holiday detection. let nyseModule = null let nyseLoadAttempted = false const holidayCache = new Map() // key: YYYY-MM-DD, value: boolean async function loadNyseModule() { if (nyseLoadAttempted) return nyseModule nyseLoadAttempted = true try { // dynamic import so the app still runs even if the package isn't installed nyseModule = await import('nyse-holidays') } catch (_) { nyseModule = null } return nyseModule } function toIsoDateString(dateLike) { if (typeof dateLike === 'string') return dateLike.slice(0, 10) try { return new Date(dateLike).toISOString().slice(0, 10) } catch { return '' } } function isWeekend(dateLike) { try { const d = new Date(dateLike) const day = d.getUTCDay() return day === 0 || day === 6 } catch { return false } } export async function isNyseHoliday(dateLike) { const iso = toIsoDateString(dateLike) if (!iso) return false if (holidayCache.has(iso)) return holidayCache.get(iso) await loadNyseModule() let isHoliday = false try { if (nyseModule) { const mod = nyseModule const fn = (mod && typeof mod.isHoliday === 'function') ? mod.isHoliday : (mod && mod.default && typeof mod.default.isHoliday === 'function') ? mod.default.isHoliday : null if (fn) { // Pass mid-day UTC to avoid timezone shifting to previous/next day isHoliday = !!fn(new Date(`${iso}T12:00:00Z`)) } } } catch (_) { isHoliday = false } holidayCache.set(iso, isHoliday) return isHoliday } export async function isNyseTradingDay(dateLike) { if (isWeekend(dateLike)) return false return !(await isNyseHoliday(dateLike)) } export async function filterRowsToNyseTradingDays(rows) { const list = Array.isArray(rows) ? rows : [] const out = [] for (const r of list) { if (!r || !r.date) continue if (await isNyseTradingDay(r.date)) out.push(r) } return out } export async function countMissingNyseTradingDaysBetween(startIso, endIso, presentDateSet) { if (!startIso || !endIso) return 0 const present = new Set(Array.from(presentDateSet || []).map(toIsoDateString)) let missing = 0 try { const start = new Date(startIso) const end = new Date(endIso) for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { const iso = d.toISOString().slice(0, 10) if (await isNyseTradingDay(iso)) { if (!present.has(iso)) missing++ } } } catch { return 0 } return missing } export async function isTradingDayForAsset(asset, dateLike) { // Crypto trades 24/7, all calendar days are trading days if (asset === 'BTC' || asset === 'ETH') return true // Default to NYSE for stocks return await isNyseTradingDay(dateLike) } export async function countMissingTradingDaysBetweenForAsset(asset, startIso, endIso, presentDateSet) { if (!startIso || !endIso) return 0 const present = new Set(Array.from(presentDateSet || []).map(toIsoDateString)) let missing = 0 try { const start = new Date(startIso) const end = new Date(endIso) for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { const iso = d.toISOString().slice(0, 10) if (await isTradingDayForAsset(asset, iso)) { if (!present.has(iso)) missing++ } } } catch { return 0 } return missing } export async function listMissingTradingDaysBetweenForAsset(asset, startIso, endIso, presentDateSet) { if (!startIso || !endIso) return [] const present = new Set(Array.from(presentDateSet || []).map(toIsoDateString)) const missing = [] try { const start = new Date(startIso) const end = new Date(endIso) for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { const iso = d.toISOString().slice(0, 10) if (await isTradingDayForAsset(asset, iso)) { if (!present.has(iso)) missing.push(iso) } } } catch { return [] } return missing } export async function countNonTradingDaysBetweenForAsset(asset, startIso, endIso) { if (!startIso || !endIso) return 0 // Crypto has no closed days by definition here if (asset === 'BTC' || asset === 'ETH') return 0 let closed = 0 try { const start = new Date(startIso) const end = new Date(endIso) for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { const iso = d.toISOString().slice(0, 10) if (!(await isNyseTradingDay(iso))) closed++ } } catch { return 0 } return closed } export async function listNonTradingDaysBetweenForAsset(asset, startIso, endIso) { if (!startIso || !endIso) return [] if (asset === 'BTC' || asset === 'ETH') return [] const closed = [] try { const start = new Date(startIso) const end = new Date(endIso) for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { const iso = d.toISOString().slice(0, 10) if (!(await isNyseTradingDay(iso))) closed.push(iso) } } catch { return [] } return closed } export async function countTradingDaysBetweenForAsset(asset, startIso, endIso) { if (!startIso || !endIso) return 0 try { const start = new Date(startIso) const end = new Date(endIso) if (asset === 'BTC' || asset === 'ETH') { const days = Math.max(0, Math.floor((end - start) / 86400000) + 1) return days } let count = 0 for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { const iso = d.toISOString().slice(0, 10) if (await isNyseTradingDay(iso)) count++ } return count } catch { return 0 } }