quanthedge / frontend /src /utils /currencyUtils.ts
jashdoshi77's picture
whole lotta changes
e6021a3
/**
* 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,
}));