Spaces:
Running
Running
| // 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 | |
| } | |
| } | |