| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export const EVENT_SERIES = { |
| CPI: { series: 'CPIAUCSL', transform: 'pct_mom', freq: 'm' }, |
| 'Nonfarm Payrolls': { series: 'PAYEMS', transform: 'diff_k', freq: 'm' }, |
| GDP: { series: 'A191RL1Q225SBEA', transform: 'direct', freq: 'q' }, |
| PCE: { series: 'PCEPI', transform: 'pct_mom', freq: 'm' }, |
| 'Retail Sales': { series: 'RSAFS', transform: 'pct_mom', freq: 'm' }, |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function observationMatchesRelease(eventDateISO, obsDateISO, freq) { |
| const eventDate = new Date(eventDateISO); |
| const obsDate = new Date(obsDateISO); |
| if (!Number.isFinite(eventDate.getTime()) || !Number.isFinite(obsDate.getTime())) return false; |
| const monthDiff = (eventDate.getUTCFullYear() - obsDate.getUTCFullYear()) * 12 |
| + (eventDate.getUTCMonth() - obsDate.getUTCMonth()); |
| if (freq === 'q') return monthDiff >= 1 && monthDiff <= 5; |
| return monthDiff === 1; |
| } |
|
|
| |
| function parseObs(observation) { |
| const value = Number.parseFloat(observation?.value); |
| return Number.isFinite(value) ? { date: observation.date, value } : null; |
| } |
|
|
| |
| |
| |
| |
| const MAX_ADJACENT_GAP_MS = 45 * 86400_000; |
| function isAdjacent(newer, older) { |
| const gap = new Date(newer.date).getTime() - new Date(older.date).getTime(); |
| return Number.isFinite(gap) && gap > 0 && gap <= MAX_ADJACENT_GAP_MS; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function computePrintValues(observations, transform) { |
| const empty = { actual: '', previous: '', obsDate: '' }; |
| const raw = Array.isArray(observations) ? observations : []; |
| |
| |
| |
| const lead = parseObs(raw[0]); |
| if (!lead) return empty; |
| const usable = raw.map(parseObs).filter(Boolean); |
|
|
| |
| |
| if (transform === 'direct') { |
| const prev = usable.length > 1 ? usable[1] : null; |
| return { |
| actual: lead.value.toFixed(1), |
| previous: prev ? prev.value.toFixed(1) : '', |
| obsDate: lead.date, |
| }; |
| } |
| if (usable.length < 2 || !isAdjacent(usable[0], usable[1])) return empty; |
| if (transform === 'diff_k') { |
| const actual = usable[0].value - usable[1].value; |
| const hasPrev = usable.length > 2 && isAdjacent(usable[1], usable[2]); |
| const previous = hasPrev ? usable[1].value - usable[2].value : null; |
| return { |
| actual: `${actual >= 0 ? '+' : ''}${Math.round(actual)}`, |
| previous: previous === null ? '' : `${previous >= 0 ? '+' : ''}${Math.round(previous)}`, |
| obsDate: usable[0].date, |
| }; |
| } |
| |
| if (usable[1].value === 0) return empty; |
| const actual = (usable[0].value / usable[1].value - 1) * 100; |
| const hasPrev = usable.length > 2 && isAdjacent(usable[1], usable[2]) && usable[2].value !== 0; |
| const previous = hasPrev ? (usable[1].value / usable[2].value - 1) * 100 : null; |
| return { |
| actual: `${actual >= 0 ? '+' : ''}${actual.toFixed(1)}`, |
| previous: previous === null ? '' : `${previous >= 0 ? '+' : ''}${previous.toFixed(1)}`, |
| obsDate: usable[0].date, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function fillEventActuals(events, printsByEvent, todayISO) { |
| let filled = 0; |
| for (const event of Array.isArray(events) ? events : []) { |
| if (event.actual) continue; |
| const print = printsByEvent[event.event]; |
| if (!print || !print.actual) continue; |
| if (typeof event.date !== 'string' || event.date > todayISO) continue; |
| |
| |
| |
| const freq = EVENT_SERIES[event.event]?.freq ?? 'm'; |
| if (!observationMatchesRelease(event.date, print.obsDate, freq)) continue; |
| event.actual = print.actual; |
| if (!event.previous && print.previous) event.previous = print.previous; |
| filled++; |
| } |
| return filled; |
| } |
|
|