Files
goldenChart/frontend/src/utils/safeFormat.ts
T
2026-06-06 00:56:45 +09:00

127 lines
4.0 KiB
TypeScript

/**
* 업비트 KRW 마켓 가격 표시 규칙 (2024년 개정 기준)
*
* 가격대 별 호가 단위:
* ≥ 2,000,000원 : 1,000원 단위 (소수점 없음) 예) 92,133,000
* ≥ 200,000원 : 100원 단위 (소수점 없음) 예) 234,100
* ≥ 10,000원 : 10원 단위 (소수점 없음) 예) 23,450
* ≥ 100원 : 1원 단위 (소수점 없음) 예) 1,234 / 452
* ≥ 10원 : 0.1원 단위 (소수점 1자리) 예) 45.3
* ≥ 1원 : 0.01원 단위 (소수점 2자리) 예) 4.53
* ≥ 0.1원 : 0.001원 단위 (소수점 3자리) 예) 0.453
* < 0.1원 : 0.0001원 단위 (소수점 4자리) 예) 0.0123
*
* 주의: 100~1,000원 구간은 2024년 이후 1원 단위(소수점 없음)로 환원됨.
*/
export function formatUpbitKrwPrice(
price: number | null | undefined,
fallback = '—',
): string {
if (price == null || !Number.isFinite(price)) return fallback;
const abs = Math.abs(price);
if (abs >= 2_000_000) {
// 1,000원 단위
return (Math.round(price / 1000) * 1000).toLocaleString('ko-KR', {
maximumFractionDigits: 0,
});
}
if (abs >= 200_000) {
// 100원 단위
return (Math.round(price / 100) * 100).toLocaleString('ko-KR', {
maximumFractionDigits: 0,
});
}
if (abs >= 10_000) {
// 10원 단위
return (Math.round(price / 10) * 10).toLocaleString('ko-KR', {
maximumFractionDigits: 0,
});
}
if (abs >= 100) {
// 1원 단위 (소수점 없음) — 100~10,000원 구간
return Math.round(price).toLocaleString('ko-KR', {
maximumFractionDigits: 0,
});
}
if (abs >= 10) {
// 0.1원 단위 (소수점 1자리)
return (Math.round(price * 10) / 10).toLocaleString('ko-KR', {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
}
if (abs >= 1) {
// 0.01원 단위 (소수점 2자리)
return (Math.round(price * 100) / 100).toLocaleString('ko-KR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
if (abs >= 0.1) {
// 0.001원 단위 (소수점 3자리)
return (Math.round(price * 1000) / 1000).toLocaleString('ko-KR', {
minimumFractionDigits: 3,
maximumFractionDigits: 3,
});
}
if (abs > 0) {
// 0.0001원 단위 (소수점 4자리)
return (Math.round(price * 10000) / 10000).toLocaleString('ko-KR', {
minimumFractionDigits: 4,
maximumFractionDigits: 4,
});
}
return '0';
}
/**
* 업비트 KRW 가격의 최소 호가 단위(tick size)를 반환한다.
* LightweightCharts priceFormat.minMove 또는 주문 입력 스텝 계산에 사용.
*/
export function upbitKrwTickSize(price: number): number {
const abs = Math.abs(price);
if (abs >= 2_000_000) return 1000;
if (abs >= 200_000) return 100;
if (abs >= 10_000) return 10;
if (abs >= 100) return 1;
if (abs >= 10) return 0.1;
if (abs >= 1) return 0.01;
if (abs >= 0.1) return 0.001;
return 0.0001;
}
/**
* API·JSON·WebSocket에서 문자열로 올 수 있는 숫자를 안전하게 처리
*/
export function coerceFiniteNumber(v: unknown): number | null {
if (v == null || v === '') return null;
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
if (typeof v === 'string') {
const trimmed = v.trim().replace(/,/g, '');
if (!trimmed) return null;
const n = Number(trimmed);
return Number.isFinite(n) ? n : null;
}
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
/** toFixed 크래시 방지 — 숫자가 아니면 '—' */
export function safeToFixed(v: unknown, digits = 2, fallback = '—'): string {
const n = coerceFiniteNumber(v);
if (n == null) return fallback;
try {
return n.toFixed(digits);
} catch {
return fallback;
}
}
export function safePercentFromRate(v: unknown, digits = 2): string {
const n = coerceFiniteNumber(v);
if (n == null) return '—';
const sign = n >= 0 ? '+' : '';
return `${sign}${safeToFixed(n * 100, digits, '0.00')}%`;
}