Files
Neon-Desk/lib/server/prices.ts
francy51 db01f207a5 Expand financials surfaces with ratios, KPIs, and cadence support
- Add bundled financial modeling pipeline (ratios, KPI dimensions/notes, trend series, standardization)
- Introduce company financial bundles storage (Drizzle migration + repo wiring)
- Refactor financials page/API/query flow to use surfaceKind + cadence and new response shapes
2026-03-07 15:16:35 -05:00

211 lines
5.7 KiB
TypeScript

const YAHOO_BASE = 'https://query1.finance.yahoo.com/v8/finance/chart';
function fallbackQuote(ticker: string) {
const normalized = ticker.trim().toUpperCase();
let hash = 0;
for (const char of normalized) {
hash = (hash * 31 + char.charCodeAt(0)) % 100000;
}
return 40 + (hash % 360) + ((hash % 100) / 100);
}
export async function getQuote(ticker: string): Promise<number> {
const normalizedTicker = ticker.trim().toUpperCase();
try {
const response = await fetch(`${YAHOO_BASE}/${normalizedTicker}?interval=1d&range=1d`, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; FiscalClone/3.0)'
},
cache: 'no-store'
});
if (!response.ok) {
return fallbackQuote(normalizedTicker);
}
const payload = await response.json() as {
chart?: {
result?: Array<{ meta?: { regularMarketPrice?: number } }>;
};
};
const price = payload.chart?.result?.[0]?.meta?.regularMarketPrice;
if (typeof price !== 'number' || !Number.isFinite(price)) {
return fallbackQuote(normalizedTicker);
}
return price;
} catch {
return fallbackQuote(normalizedTicker);
}
}
export async function getQuoteOrNull(ticker: string): Promise<number | null> {
const normalizedTicker = ticker.trim().toUpperCase();
try {
const response = await fetch(`${YAHOO_BASE}/${normalizedTicker}?interval=1d&range=1d`, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; FiscalClone/3.0)'
},
cache: 'no-store'
});
if (!response.ok) {
return null;
}
const payload = await response.json() as {
chart?: {
result?: Array<{ meta?: { regularMarketPrice?: number } }>;
};
};
const price = payload.chart?.result?.[0]?.meta?.regularMarketPrice;
return typeof price === 'number' && Number.isFinite(price) ? price : null;
} catch {
return null;
}
}
export async function getHistoricalClosingPrices(ticker: string, dates: string[]) {
const normalizedTicker = ticker.trim().toUpperCase();
const normalizedDates = dates
.map((value) => {
const parsed = Date.parse(value);
return Number.isFinite(parsed)
? { raw: value, iso: new Date(parsed).toISOString().slice(0, 10), epoch: parsed }
: null;
})
.filter((entry): entry is { raw: string; iso: string; epoch: number } => entry !== null);
if (normalizedDates.length === 0) {
return {} as Record<string, number | null>;
}
try {
const response = await fetch(`${YAHOO_BASE}/${normalizedTicker}?interval=1d&range=10y`, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; FiscalClone/3.0)'
},
cache: 'no-store'
});
if (!response.ok) {
return Object.fromEntries(normalizedDates.map((entry) => [entry.raw, null]));
}
const payload = await response.json() as {
chart?: {
result?: Array<{
timestamp?: number[];
indicators?: {
quote?: Array<{
close?: Array<number | null>;
}>;
};
}>;
};
};
const result = payload.chart?.result?.[0];
const timestamps = result?.timestamp ?? [];
const closes = result?.indicators?.quote?.[0]?.close ?? [];
const points = timestamps
.map((timestamp, index) => {
const close = closes[index];
if (typeof close !== 'number' || !Number.isFinite(close)) {
return null;
}
return {
epoch: timestamp * 1000,
close
};
})
.filter((entry): entry is { epoch: number; close: number } => entry !== null);
return Object.fromEntries(normalizedDates.map((entry) => {
const point = [...points]
.reverse()
.find((candidate) => candidate.epoch <= entry.epoch) ?? null;
return [entry.raw, point?.close ?? null];
}));
} catch {
return Object.fromEntries(normalizedDates.map((entry) => [entry.raw, null]));
}
}
export async function getPriceHistory(ticker: string): Promise<Array<{ date: string; close: number }>> {
const normalizedTicker = ticker.trim().toUpperCase();
try {
const response = await fetch(`${YAHOO_BASE}/${normalizedTicker}?interval=1wk&range=1y`, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; FiscalClone/3.0)'
},
cache: 'no-store'
});
if (!response.ok) {
throw new Error('Quote history unavailable');
}
const payload = await response.json() as {
chart?: {
result?: Array<{
timestamp?: number[];
indicators?: {
quote?: Array<{
close?: Array<number | null>;
}>;
};
}>;
};
};
const result = payload.chart?.result?.[0];
const timestamps = result?.timestamp ?? [];
const closes = result?.indicators?.quote?.[0]?.close ?? [];
const points = timestamps
.map((timestamp, index) => {
const close = closes[index];
if (typeof close !== 'number' || !Number.isFinite(close)) {
return null;
}
return {
date: new Date(timestamp * 1000).toISOString(),
close
};
})
.filter((entry): entry is { date: string; close: number } => entry !== null);
if (points.length > 0) {
return points;
}
} catch {
// fall through to deterministic synthetic history
}
const now = Date.now();
const base = fallbackQuote(normalizedTicker);
return Array.from({ length: 26 }, (_, index) => {
const step = 25 - index;
const date = new Date(now - step * 14 * 24 * 60 * 60 * 1000).toISOString();
const wave = Math.sin(index / 3.5) * 0.05;
const trend = (index - 13) * 0.006;
const close = Math.max(base * (1 + wave + trend), 1);
return {
date,
close: Number(close.toFixed(2))
};
});
}