/** * Currency utilities for market-aware monetary display. * * Maps currency codes to symbols, formatting rules, and market identifiers. * Used across the entire QuantHedge frontend to ensure correct currency display. */ export interface CurrencyInfo { symbol: string; code: string; name: string; locale: string; market: string; // Market region grouping decimalPlaces: number; } export const CURRENCIES: Record = { USD: { symbol: '$', code: 'USD', name: 'US Dollar', locale: 'en-US', market: 'US', decimalPlaces: 2 }, INR: { symbol: '₹', code: 'INR', name: 'Indian Rupee', locale: 'en-IN', market: 'India', decimalPlaces: 2 }, EUR: { symbol: '€', code: 'EUR', name: 'Euro', locale: 'de-DE', market: 'Europe', decimalPlaces: 2 }, GBP: { symbol: '£', code: 'GBP', name: 'British Pound', locale: 'en-GB', market: 'UK', decimalPlaces: 2 }, JPY: { symbol: '¥', code: 'JPY', name: 'Japanese Yen', locale: 'ja-JP', market: 'Japan', decimalPlaces: 0 }, AUD: { symbol: 'A$', code: 'AUD', name: 'Australian Dollar', locale: 'en-AU', market: 'Australia', decimalPlaces: 2 }, CAD: { symbol: 'C$', code: 'CAD', name: 'Canadian Dollar', locale: 'en-CA', market: 'Canada', decimalPlaces: 2 }, CHF: { symbol: 'CHF', code: 'CHF', name: 'Swiss Franc', locale: 'de-CH', market: 'Switzerland', decimalPlaces: 2 }, HKD: { symbol: 'HK$', code: 'HKD', name: 'Hong Kong Dollar', locale: 'zh-HK', market: 'Hong Kong', decimalPlaces: 2 }, SGD: { symbol: 'S$', code: 'SGD', name: 'Singapore Dollar', locale: 'en-SG', market: 'Singapore', decimalPlaces: 2 }, CNY: { symbol: '¥', code: 'CNY', name: 'Chinese Yuan', locale: 'zh-CN', market: 'China', decimalPlaces: 2 }, KRW: { symbol: '₩', code: 'KRW', name: 'Korean Won', locale: 'ko-KR', market: 'South Korea', decimalPlaces: 0 }, BRL: { symbol: 'R$', code: 'BRL', name: 'Brazilian Real', locale: 'pt-BR', market: 'Brazil', decimalPlaces: 2 }, RUB: { symbol: '₽', code: 'RUB', name: 'Russian Ruble', locale: 'ru-RU', market: 'Russia', decimalPlaces: 2 }, ZAR: { symbol: 'R', code: 'ZAR', name: 'South African Rand', locale: 'en-ZA', market: 'South Africa', decimalPlaces: 2 }, MXN: { symbol: 'Mex$', code: 'MXN', name: 'Mexican Peso', locale: 'es-MX', market: 'Mexico', decimalPlaces: 2 }, SEK: { symbol: 'kr', code: 'SEK', name: 'Swedish Krona', locale: 'sv-SE', market: 'Sweden', decimalPlaces: 2 }, NOK: { symbol: 'kr', code: 'NOK', name: 'Norwegian Krone', locale: 'nb-NO', market: 'Norway', decimalPlaces: 2 }, BTC: { symbol: '₿', code: 'BTC', name: 'Bitcoin', locale: 'en-US', market: 'Crypto', decimalPlaces: 8 }, ETH: { symbol: 'Ξ', code: 'ETH', name: 'Ethereum', locale: 'en-US', market: 'Crypto', decimalPlaces: 6 }, }; /** Get currency symbol for a code. Falls back to the code itself. */ export const getCurrencySymbol = (code: string): string => CURRENCIES[code?.toUpperCase()]?.symbol || code || '$'; /** Get full currency info. */ export const getCurrencyInfo = (code: string): CurrencyInfo => CURRENCIES[code?.toUpperCase()] || CURRENCIES.USD; /** * Format a monetary value with the correct currency symbol and locale. * Example: formatCurrency(1500.50, 'INR') → '₹1,500.50' */ export const formatCurrency = ( value: number | null | undefined, currencyCode: string = 'USD', options?: { compact?: boolean; showCode?: boolean } ): string => { if (value == null || isNaN(value)) return '—'; const info = getCurrencyInfo(currencyCode); const absValue = Math.abs(value); const sign = value < 0 ? '-' : ''; let formatted: string; if (options?.compact && absValue >= 1_000_000) { formatted = (absValue / 1_000_000).toFixed(2) + 'M'; } else if (options?.compact && absValue >= 1_000) { formatted = (absValue / 1_000).toFixed(1) + 'K'; } else { formatted = absValue.toLocaleString(info.locale, { minimumFractionDigits: Math.min(info.decimalPlaces, 2), maximumFractionDigits: Math.min(info.decimalPlaces, 2), }); } const result = `${sign}${info.symbol}${formatted}`; return options?.showCode ? `${result} ${info.code}` : result; }; /** * Detect market/currency from ticker suffix. * e.g., 'RELIANCE.NS' → 'INR', 'VOD.L' → 'GBP', 'AAPL' → 'USD' */ export const detectCurrencyFromTicker = (ticker: string): string => { const t = ticker?.toUpperCase() || ''; if (t.endsWith('.NS') || t.endsWith('.BO')) return 'INR'; if (t.endsWith('.L')) return 'GBP'; if (t.endsWith('.T') || t.endsWith('.TYO')) return 'JPY'; if (t.endsWith('.DE') || t.endsWith('.PA') || t.endsWith('.AS') || t.endsWith('.MI')) return 'EUR'; if (t.endsWith('.AX')) return 'AUD'; if (t.endsWith('.TO') || t.endsWith('.V')) return 'CAD'; if (t.endsWith('.SW')) return 'CHF'; if (t.endsWith('.HK')) return 'HKD'; if (t.endsWith('.SI')) return 'SGD'; if (t.endsWith('.SS') || t.endsWith('.SZ')) return 'CNY'; if (t.endsWith('.KS') || t.endsWith('.KQ')) return 'KRW'; if (t.endsWith('.SA')) return 'BRL'; if (t.endsWith('.ME')) return 'RUB'; if (t.endsWith('.JO')) return 'ZAR'; if (t.endsWith('.MX')) return 'MXN'; if (t.endsWith('.ST')) return 'SEK'; if (t.endsWith('.OL')) return 'NOK'; // Crypto pairs if (t.includes('-USD') || t.includes('-USDT')) { if (t.startsWith('BTC')) return 'BTC'; if (t.startsWith('ETH')) return 'ETH'; } return 'USD'; }; /** * Get the market name for a currency code. */ export const getMarketName = (currencyCode: string): string => getCurrencyInfo(currencyCode).market; /** * Group holdings by their market/currency. * Returns a Map of marketName → holdings array. */ export interface HoldingWithCurrency { currency: string; market_value: number; [key: string]: any; } export const groupByMarket = (holdings: T[]): Map => { const groups = new Map(); for (const h of holdings) { const market = getMarketName(h.currency || 'USD'); if (!groups.has(market)) groups.set(market, []); groups.get(market)!.push(h); } return groups; }; /** * Calculate per-market subtotals from holdings. */ export const getMarketSubtotals = (holdings: T[]) => { const groups = groupByMarket(holdings); const subtotals: { market: string; currency: string; totalValue: number; count: number }[] = []; groups.forEach((items, market) => { const currency = items[0]?.currency || 'USD'; subtotals.push({ market, currency, totalValue: items.reduce((sum, h) => sum + (h.market_value || 0), 0), count: items.length, }); }); return subtotals; }; /** All supported currency codes for dropdowns. */ export const CURRENCY_OPTIONS = Object.entries(CURRENCIES).map(([code, info]) => ({ value: code, label: `${info.symbol} ${code} — ${info.name}`, market: info.market, }));