SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
2.0 KB · 45 lines typescript
Raw Blame History
1import { and, desc, eq, lte } from 'drizzle-orm';2import { fxRates } from '@rareindex/database';3import { toDateOnly, type CurrencyCode } from '@rareindex/shared';4import { db } from './db.ts';56/**7 * Historical FX lookup (§136): rate quoted as 1 <base> = rate <quote>, base USD in fx_rates.8 * Converts `amount` in `currency` to USD using the latest rate on or before `date`9 * (weekends/holidays fall back to the previous business day). Returns null when no rate exists —10 * callers must then trigger an FX sync or leave price_usd unresolved, never guess.11 */12const cache = new Map<string, { rate: number; date: string } | null>();1314export async function usdRateFor(currency: string, date: Date): Promise<{ rate: number; date: string } | null> {15  if (currency === 'USD') return { rate: 1, date: toDateOnly(date) };16  const key = `${currency}|${toDateOnly(date)}`;17  if (cache.has(key)) return cache.get(key)!;18  const [row] = await db()19    .select({ rate: fxRates.rate, date: fxRates.date })20    .from(fxRates)21    .where(and(eq(fxRates.base, 'USD'), eq(fxRates.quote, currency), lte(fxRates.date, toDateOnly(date))))22    .orderBy(desc(fxRates.date))23    .limit(1);24  const out = row ? { rate: row.rate, date: row.date } : null;25  // Only cache misses briefly (map grows per date otherwise)26  if (cache.size > 50_000) cache.clear();27  cache.set(key, out);28  return out;29}3031export async function toUsd(amount: number, currency: string, date: Date): Promise<{ usd: number; rate: number; fxDate: string } | null> {32  const r = await usdRateFor(currency, date);33  if (!r || r.rate <= 0) return null;34  // fx_rates.rate = quote units per 1 USD → USD = amount / rate35  return { usd: amount / r.rate, rate: r.rate, fxDate: r.date };36}3738export function pickRate(rates: Array<{ date: string; rate: number }>, date: string): { date: string; rate: number } | null {39  let best: { date: string; rate: number } | null = null;40  for (const r of rates) if (r.date <= date && (!best || r.date > best.date)) best = r;41  return best;42}4344export type { CurrencyCode };45