Spaces:
Sleeping
Sleeping
| /** | |
| * 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<string, CurrencyInfo> = { | |
| 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 = <T extends HoldingWithCurrency>(holdings: T[]): Map<string, T[]> => { | |
| const groups = new Map<string, T[]>(); | |
| 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 = <T extends HoldingWithCurrency>(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, | |
| })); | |