127 lines
4.0 KiB
TypeScript
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')}%`;
|
|
}
|