Spaces:
Running
Running
File size: 5,756 Bytes
5fa7a59 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
// 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
}
}
|