주가 표시형식 수정

This commit is contained in:
Macbook
2026-06-06 00:56:45 +09:00
parent 1d4e606f77
commit 94d3f92bb9
18 changed files with 156 additions and 57 deletions
+2 -1
View File
@@ -10,6 +10,7 @@ import {
type ProcessMonitorDto, type ProcessMonitorDto,
type SystemMonitorDto, type SystemMonitorDto,
} from '../utils/backendApi'; } from '../utils/backendApi';
import { formatUpbitKrwPrice } from '../utils/safeFormat';
import DashboardGauge from './dashboard/DashboardGauge'; import DashboardGauge from './dashboard/DashboardGauge';
import DashboardSegmentBar from './dashboard/DashboardSegmentBar'; import DashboardSegmentBar from './dashboard/DashboardSegmentBar';
import DashboardSparkChart from './dashboard/DashboardSparkChart'; import DashboardSparkChart from './dashboard/DashboardSparkChart';
@@ -339,7 +340,7 @@ const DashboardPage: React.FC<Props> = ({ theme, onGoChart }) => {
<button type="button" className="gc-signal-main" onClick={() => onGoChart?.(s.market)}> <button type="button" className="gc-signal-main" onClick={() => onGoChart?.(s.market)}>
<strong>{buy ? 'BUY' : 'SELL'}</strong> <strong>{buy ? 'BUY' : 'SELL'}</strong>
<span>{s.market.replace('KRW-', '')}</span> <span>{s.market.replace('KRW-', '')}</span>
<span className="gc-signal-price">@ {s.price?.toLocaleString('ko-KR')}</span> <span className="gc-signal-price">@ {s.price != null ? formatUpbitKrwPrice(s.price) : ''}</span>
</button> </button>
<span className="gc-signal-time">{s.createdAt?.slice(11, 16) ?? ''}</span> <span className="gc-signal-time">{s.createdAt?.slice(11, 16) ?? ''}</span>
</li> </li>
+2 -1
View File
@@ -2,6 +2,7 @@ import React, { useRef, useEffect, useCallback, useState } from 'react';
import type { ChartManager } from '../utils/ChartManager'; import type { ChartManager } from '../utils/ChartManager';
import type { Drawing, DrawingPoint, DrawingToolId, Theme } from '../types'; import type { Drawing, DrawingPoint, DrawingToolId, Theme } from '../types';
import { formatUnixDateTime } from '../utils/timezone'; import { formatUnixDateTime } from '../utils/timezone';
import { formatUpbitKrwPrice } from '../utils/safeFormat';
// ─── Hit-test ────────────────────────────────────────────────────────────── // ─── Hit-test ──────────────────────────────────────────────────────────────
const HIT_RADIUS = 7; // px (CSS 좌표) const HIT_RADIUS = 7; // px (CSS 좌표)
@@ -1088,7 +1089,7 @@ function renderInfoLine(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManag
ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1; ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1;
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cssW, y); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cssW, y); ctx.stroke();
const price = d.points[0].price; const price = d.points[0].price;
const label = price >= 1000 ? price.toLocaleString('ko-KR') : price.toFixed(4); const label = formatUpbitKrwPrice(price);
ctx.font = 'bold 10px sans-serif'; ctx.font = 'bold 10px sans-serif';
const tw = ctx.measureText(label).width + 8; const tw = ctx.measureText(label).width + 8;
ctx.fillStyle = d.color; ctx.fillStyle = d.color;
+2 -6
View File
@@ -13,6 +13,7 @@ import {
getFavorites, saveFavorites, toggleFavorite, getFavorites, saveFavorites, toggleFavorite,
getHoldings, addHolding, removeHolding, getHoldings, addHolding, removeHolding,
} from '../utils/marketStorage'; } from '../utils/marketStorage';
import { formatUpbitKrwPrice } from '../utils/safeFormat';
// ─── 타입 ────────────────────────────────────────────────────────────────── // ─── 타입 ──────────────────────────────────────────────────────────────────
type Tab = 'krw' | 'btc' | 'hold' | 'fav'; type Tab = 'krw' | 'btc' | 'hold' | 'fav';
type SortKey = 'name' | 'price' | 'change'; type SortKey = 'name' | 'price' | 'change';
@@ -29,12 +30,7 @@ export interface MarketPanelProps {
// ─── 가격 포맷 헬퍼 ──────────────────────────────────────────────────────── // ─── 가격 포맷 헬퍼 ────────────────────────────────────────────────────────
function fmtKrw(price: number | null | undefined): string { function fmtKrw(price: number | null | undefined): string {
if (price == null || !isFinite(price)) return '-'; return formatUpbitKrwPrice(price, '-');
if (price >= 1_000_000) return price.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
if (price >= 1_000) return price.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
if (price >= 1) return price.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
if (price >= 0.01) return price.toFixed(4);
return price.toFixed(8);
} }
function fmtUsd(krwPrice: number | null | undefined, rate: number): string { function fmtUsd(krwPrice: number | null | undefined, rate: number): string {
+2 -3
View File
@@ -7,12 +7,11 @@ import { formatNowClock, useDisplayTimezone } from '../utils/timezone';
import type { OrderbookDisplayUnit, WsStatus } from '../hooks/useUpbitOrderbook'; import type { OrderbookDisplayUnit, WsStatus } from '../hooks/useUpbitOrderbook';
import type { RecentTrade } from '../hooks/useUpbitRecentTrades'; import type { RecentTrade } from '../hooks/useUpbitRecentTrades';
import { getKoreanName } from '../utils/marketNameCache'; import { getKoreanName } from '../utils/marketNameCache';
import { formatUpbitKrwPrice } from '../utils/safeFormat';
function fmtPrice(p: number): string { function fmtPrice(p: number): string {
if (p == null || !isFinite(p)) return '-'; if (p == null || !isFinite(p)) return '-';
if (p >= 1_000) return p.toLocaleString('ko-KR', { maximumFractionDigits: 0 }); return formatUpbitKrwPrice(p, '-');
if (p >= 1) return p.toFixed(2);
return p.toFixed(6);
} }
function fmtSize(s: number): string { function fmtSize(s: number): string {
+2 -1
View File
@@ -3,6 +3,7 @@ import React, { useState } from 'react';
import { useDraggablePanel } from '../hooks/useDraggablePanel'; import { useDraggablePanel } from '../hooks/useDraggablePanel';
import { placePaperOrder, placeLiveOrder, loadLiveSummary } from '../utils/backendApi'; import { placePaperOrder, placeLiveOrder, loadLiveSummary } from '../utils/backendApi';
import { formatSignalTime } from '../utils/tradeSignalDisplay'; import { formatSignalTime } from '../utils/tradeSignalDisplay';
import { formatUpbitKrwPrice } from '../utils/safeFormat';
import { TRADE_BUY_COLOR, TRADE_SELL_COLOR } from '../utils/tradeSignalColors'; import { TRADE_BUY_COLOR, TRADE_SELL_COLOR } from '../utils/tradeSignalColors';
import { useTradeAlertTimeFormat } from '../utils/tradeAlertTimeFormat'; import { useTradeAlertTimeFormat } from '../utils/tradeAlertTimeFormat';
@@ -30,7 +31,7 @@ interface Props {
} }
const fmt = (n: number): string => const fmt = (n: number): string =>
n > 0 ? n.toLocaleString('ko-KR', { maximumFractionDigits: 0 }) : '0'; n > 0 ? formatUpbitKrwPrice(n, '0') : '0';
function execLabel(type?: string, candleType?: string): string { function execLabel(type?: string, candleType?: string): string {
+14 -6
View File
@@ -7,7 +7,7 @@ import { getKoreanName } from '../utils/marketNameCache';
import { MarketSearchPanel } from './MarketSearchPanel'; import { MarketSearchPanel } from './MarketSearchPanel';
import type { TradeOrderFillRequest } from '../types'; import type { TradeOrderFillRequest } from '../types';
import { placePaperOrder } from '../utils/backendApi'; import { placePaperOrder } from '../utils/backendApi';
import { coerceFiniteNumber } from '../utils/safeFormat'; import { coerceFiniteNumber, formatUpbitKrwPrice, upbitKrwTickSize } from '../utils/safeFormat';
export type OrderKind = 'limit' | 'market' | 'stop_limit'; export type OrderKind = 'limit' | 'market' | 'stop_limit';
@@ -39,7 +39,7 @@ export interface TradeOrderPanelProps {
export function fmtKrw(n: number): string { export function fmtKrw(n: number): string {
if (!Number.isFinite(n)) return '0'; if (!Number.isFinite(n)) return '0';
return Math.round(n).toLocaleString('ko-KR'); return formatUpbitKrwPrice(n, '0');
} }
function parseNum(s: string): number { function parseNum(s: string): number {
@@ -47,11 +47,19 @@ function parseNum(s: string): number {
return Number.isFinite(v) ? v : 0; return Number.isFinite(v) ? v : 0;
} }
/** KRW 가격: 숫자만 허용, 천 단위 쉼표 표시 */ /** KRW 가격: 숫자·소수점(필요 시)만 허용, 업비트 규칙으로 표시 */
function sanitizePriceInput(raw: string): string { function sanitizePriceInput(raw: string): string {
const digits = raw.replace(/\D/g, ''); const cleaned = raw.replace(/[^\d.]/g, '');
if (!digits) return ''; if (!cleaned) return '';
return Number(digits).toLocaleString('ko-KR'); const n = Number(cleaned);
if (!Number.isFinite(n)) return '';
return formatUpbitKrwPrice(n, cleaned);
}
/** 현재 가격에 맞는 호가 단위만큼 증감 */
export function stepPriceByTick(price: number, direction: 1 | -1): number {
const tick = upbitKrwTickSize(price);
return Math.round((price + direction * tick) / tick) * tick;
} }
/** 주문 수량: 숫자와 소수점(1개)만 허용 */ /** 주문 수량: 숫자와 소수점(1개)만 허용 */
+2 -2
View File
@@ -2,6 +2,7 @@ import React, { useRef, useEffect, useLayoutEffect, useState, useCallback } from
import type { MouseEventParams, Time } from 'lightweight-charts'; import type { MouseEventParams, Time } from 'lightweight-charts';
import type { OHLCVBar, ChartType, Theme, IndicatorConfig, LegendData, Drawing, ChartMode, Timeframe } from '../types'; import type { OHLCVBar, ChartType, Theme, IndicatorConfig, LegendData, Drawing, ChartMode, Timeframe } from '../types';
import { ChartManager } from '../utils/ChartManager'; import { ChartManager } from '../utils/ChartManager';
import { formatUpbitKrwPrice } from '../utils/safeFormat';
import { useChartTimeFormat } from '../utils/chartTimeFormat'; import { useChartTimeFormat } from '../utils/chartTimeFormat';
import { setIndicatorChartContext } from '../utils/indicatorRegistry'; import { setIndicatorChartContext } from '../utils/indicatorRegistry';
import { DISPLAY_COUNT } from '../hooks/useUpbitData'; import { DISPLAY_COUNT } from '../hooks/useUpbitData';
@@ -1675,8 +1676,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
container.style.cursor = isHit ? 'pointer' : ''; container.style.cursor = isHit ? 'pointer' : '';
}, []); }, []);
const fmtCtxPrice = (p: number) => const fmtCtxPrice = (p: number) => formatUpbitKrwPrice(p);
p >= 1000 ? Math.round(p).toLocaleString('ko-KR') : p.toFixed(p >= 1 ? 2 : 6);
const closeCtxAndRequest = useCallback((side: 'buy' | 'sell') => { const closeCtxAndRequest = useCallback((side: 'buy' | 'sell') => {
if (!ctxMenu || !market) return; if (!ctxMenu || !market) return;
@@ -19,17 +19,18 @@ import {
drawdownSeries, drawdownSeries,
} from '../../utils/analysisReportMetrics'; } from '../../utils/analysisReportMetrics';
import { repairUtf8Mojibake } from '../../utils/textEncoding'; import { repairUtf8Mojibake } from '../../utils/textEncoding';
import { formatUpbitKrwPrice } from '../../utils/safeFormat';
/* ── 포맷 유틸 ── */ /* ── 포맷 유틸 ── */
const pct = (v: number, dec = 2) => `${v >= 0 ? '+' : ''}${(v * 100).toFixed(dec)}%`; const pct = (v: number, dec = 2) => `${v >= 0 ? '+' : ''}${(v * 100).toFixed(dec)}%`;
const pctAbs= (v: number, dec = 1) => `${(v * 100).toFixed(dec)}%`; const pctAbs= (v: number, dec = 1) => `${(v * 100).toFixed(dec)}%`;
const num = (v: number, dec = 2) => (isFinite(v) ? v.toFixed(dec) : '—'); const num = (v: number, dec = 2) => (isFinite(v) ? v.toFixed(dec) : '—');
const wonFmt= (v: number) => { const wonFmt = (v: number) => {
const abs = Math.abs(v); const abs = Math.abs(v);
const sign = v < 0 ? '-' : v > 0 ? '+' : ''; const sign = v < 0 ? '-' : v > 0 ? '+' : '';
if (abs >= 1e8) return `${sign}${(abs / 1e8).toFixed(1)}`; if (abs >= 1e8) return `${sign}${(abs / 1e8).toFixed(1)}`;
if (abs >= 1e4) return `${sign}${(abs / 1e4).toFixed(0)}`; if (abs >= 1e4) return `${sign}${(abs / 1e4).toFixed(0)}`;
return `${sign}${abs.toLocaleString('ko-KR')}`; return `${sign}${formatUpbitKrwPrice(abs, '0')}`;
}; };
const colorCls = (v: number) => (v > 0 ? 'up' : v < 0 ? 'down' : ''); const colorCls = (v: number) => (v > 0 ? 'up' : v < 0 ? 'down' : '');
@@ -617,8 +618,8 @@ function LogsView({ ctx }: { ctx: AnalysisReportContext }) {
<td>{r.dateTime}</td> <td>{r.dateTime}</td>
<td className="qcc-sym">{r.symbol.replace('KRW-', 'KRW-')}</td> <td className="qcc-sym">{r.symbol.replace('KRW-', 'KRW-')}</td>
<td className="qcc-side">{r.side}</td> <td className="qcc-side">{r.side}</td>
<td className="qcc-num">{r.entryPrice.toLocaleString('ko-KR')}</td> <td className="qcc-num">{formatUpbitKrwPrice(r.entryPrice)}</td>
<td className="qcc-num">{r.exitPrice.toLocaleString('ko-KR')}</td> <td className="qcc-num">{formatUpbitKrwPrice(r.exitPrice)}</td>
<td className="qcc-num">{r.qty.toFixed(3)}</td> <td className="qcc-num">{r.qty.toFixed(3)}</td>
<td className={r.pnlPct >= 0 ? 'up' : 'down'}>{r.pnlPct >= 0 ? '+' : ''}{r.pnlPct.toFixed(1)}%</td> <td className={r.pnlPct >= 0 ? 'up' : 'down'}>{r.pnlPct >= 0 ? '+' : ''}{r.pnlPct.toFixed(1)}%</td>
<td className="qcc-dur">{r.durationSec > 0 ? fmtDuration(r.durationSec) : '—'}</td> <td className="qcc-dur">{r.durationSec > 0 ? fmtDuration(r.durationSec) : '—'}</td>
@@ -658,7 +659,7 @@ function AllocationView({ ctx }: { ctx: AnalysisReportContext }) {
<span className="qcc-alloc-dot" style={{ background: sl.color }} /> <span className="qcc-alloc-dot" style={{ background: sl.color }} />
<span className="qcc-alloc-sym">{sl.symbol.replace('KRW-', 'KRW-')}</span> <span className="qcc-alloc-sym">{sl.symbol.replace('KRW-', 'KRW-')}</span>
<span className="qcc-alloc-pct">{(sl.pct * 100).toFixed(2)}%</span> <span className="qcc-alloc-pct">{(sl.pct * 100).toFixed(2)}%</span>
<span className="qcc-alloc-krw">{Math.round(sl.capitalKrw).toLocaleString('ko-KR')}</span> <span className="qcc-alloc-krw">{formatUpbitKrwPrice(sl.capitalKrw)}</span>
</div> </div>
))} ))}
</div> </div>
@@ -3,6 +3,7 @@ import type { BacktestAnalysis } from '../../utils/backendApi';
import BacktestSparkline from './BacktestSparkline'; import BacktestSparkline from './BacktestSparkline';
import type { EquityPoint } from '../../utils/backtestEquity'; import type { EquityPoint } from '../../utils/backtestEquity';
import { krw, pct, pctAbs } from '../../utils/backtestUiUtils'; import { krw, pct, pctAbs } from '../../utils/backtestUiUtils';
import { formatUpbitKrwPrice } from '../../utils/safeFormat';
interface Props { interface Props {
analysis: BacktestAnalysis | null; analysis: BacktestAnalysis | null;
@@ -21,7 +22,7 @@ interface Props {
const num = (v: number, dec = 2) => (isFinite(v) ? v.toFixed(dec) : ''); const num = (v: number, dec = 2) => (isFinite(v) ? v.toFixed(dec) : '');
const fmtWon = (v: number) => const fmtWon = (v: number) =>
isFinite(v) ? `${Math.round(v).toLocaleString('ko-KR')}` : ''; isFinite(v) ? `${formatUpbitKrwPrice(v, '')}` : '';
function MetricCell({ function MetricCell({
label, label,
@@ -1,6 +1,6 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { useUpbitOrderbook } from '../../hooks/useUpbitOrderbook'; import { useUpbitOrderbook } from '../../hooks/useUpbitOrderbook';
import { coerceFiniteNumber, safeToFixed } from '../../utils/safeFormat'; import { coerceFiniteNumber, safeToFixed, formatUpbitKrwPrice } from '../../utils/safeFormat';
interface Props { interface Props {
market: string; market: string;
@@ -14,8 +14,7 @@ interface Props {
function fmtPrice(p: unknown): string { function fmtPrice(p: unknown): string {
const n = coerceFiniteNumber(p); const n = coerceFiniteNumber(p);
if (n == null) return '-'; return formatUpbitKrwPrice(n, '-');
return n >= 1000 ? n.toLocaleString('ko-KR', { maximumFractionDigits: 0 }) : safeToFixed(n, 4);
} }
function fmtSize(s: unknown): string { function fmtSize(s: unknown): string {
@@ -1,5 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import type { TrendSearchResultDto } from '../../utils/trendSearchApi'; import type { TrendSearchResultDto } from '../../utils/trendSearchApi';
import { formatUpbitKrwPrice } from '../../utils/safeFormat';
interface Props { interface Props {
results: TrendSearchResultDto[]; results: TrendSearchResultDto[];
@@ -10,9 +11,7 @@ interface Props {
} }
function fmtPrice(n: number): string { function fmtPrice(n: number): string {
if (n >= 1_000_000) return n.toLocaleString('ko-KR', { maximumFractionDigits: 0 }); return formatUpbitKrwPrice(n, '-');
if (n >= 100) return n.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
return n.toLocaleString('ko-KR', { maximumFractionDigits: 4 });
} }
function fmtPct(n: number): string { function fmtPct(n: number): string {
@@ -1,15 +1,10 @@
import React, { memo, useEffect, useRef, useState } from 'react'; import React, { memo, useEffect, useRef, useState } from 'react';
import type { ChangeDir, TickerData } from '../../hooks/useMarketTicker'; import type { ChangeDir, TickerData } from '../../hooks/useMarketTicker';
import { coerceFiniteNumber, safePercentFromRate, safeToFixed } from '../../utils/safeFormat'; import { coerceFiniteNumber, safePercentFromRate, formatUpbitKrwPrice } from '../../utils/safeFormat';
function fmtKrw(price: number | null | undefined | unknown): string { function fmtKrw(price: number | null | undefined | unknown): string {
const p = coerceFiniteNumber(price); const p = coerceFiniteNumber(price);
if (p == null) return '-'; return formatUpbitKrwPrice(p, '-');
if (p >= 1_000_000) return p.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
if (p >= 1_000) return p.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
if (p >= 1) return p.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
if (p >= 0.01) return safeToFixed(p, 4, '-');
return safeToFixed(p, 8, '-');
} }
function fmtPct(rate: number | null | undefined | unknown): string { function fmtPct(rate: number | null | undefined | unknown): string {
+5 -4
View File
@@ -25,6 +25,7 @@ import { BollingerBandFillPrimitive } from './BollingerBandFillPrimitive';
import { resolveBbBandBackground } from './bollingerConfig'; import { resolveBbBandBackground } from './bollingerConfig';
import type { OHLCVBar, ChartType, Theme, IndicatorConfig, Timeframe } from '../types'; import type { OHLCVBar, ChartType, Theme, IndicatorConfig, Timeframe } from '../types';
import { formatChartAxisPrice, formatIndicatorAxisPrice } from './dataGenerator'; import { formatChartAxisPrice, formatIndicatorAxisPrice } from './dataGenerator';
import { formatUpbitKrwPrice } from './safeFormat';
import { TRADE_BUY_COLOR, TRADE_SELL_COLOR } from './tradeSignalColors'; import { TRADE_BUY_COLOR, TRADE_SELL_COLOR } from './tradeSignalColors';
import { import {
DEFAULT_CHART_TIME_FORMAT, DEFAULT_CHART_TIME_FORMAT,
@@ -1271,7 +1272,7 @@ export class ChartManager {
: buy ? '매수' : buy ? '매수'
: '매도'; : '매도';
const text = showPrice const text = showPrice
? `${label} : ${s.price.toLocaleString()}` ? `${label} : ${formatUpbitKrwPrice(s.price)}`
: label; : label;
return { return {
time: s.time, time: s.time,
@@ -1304,7 +1305,7 @@ export class ChartManager {
const buy = m.signal === 'BUY'; const buy = m.signal === 'BUY';
const label = buy ? '매수' : '매도'; const label = buy ? '매수' : '매도';
const text = showPrice const text = showPrice
? `${label} : ${m.price.toLocaleString()}` ? `${label} : ${formatUpbitKrwPrice(m.price)}`
: label; : label;
return { return {
time: m.time, time: m.time,
@@ -1988,7 +1989,7 @@ export class ChartManager {
if (!this.mainSeries) return; if (!this.mainSeries) return;
const line = this.mainSeries.createPriceLine({ const line = this.mainSeries.createPriceLine({
price, color: '#FFB300', lineWidth: 1, lineStyle: LineStyle.Dashed, price, color: '#FFB300', lineWidth: 1, lineStyle: LineStyle.Dashed,
axisLabelVisible: true, title: label || `Alert ${price.toFixed(2)}`, axisLabelVisible: true, title: label || `Alert ${formatUpbitKrwPrice(price)}`,
}); });
this.alertEntries.push({ price, label, line }); this.alertEntries.push({ price, label, line });
} }
@@ -2051,7 +2052,7 @@ export class ChartManager {
this.drawingPoints = []; this.drawingPoints = [];
} else if (tool === 'trendline' && this.drawingPoints.length === 2) { } else if (tool === 'trendline' && this.drawingPoints.length === 2) {
const [p1, p2] = this.drawingPoints; const [p1, p2] = this.drawingPoints;
const l = this.mainSeries.createPriceLine({ price: (p1.price + p2.price) / 2, color: '#2962FF', lineWidth: 1, lineStyle: LineStyle.Solid, axisLabelVisible: false, title: `${p1.price.toFixed(2)}${p2.price.toFixed(2)}` }); const l = this.mainSeries.createPriceLine({ price: (p1.price + p2.price) / 2, color: '#2962FF', lineWidth: 1, lineStyle: LineStyle.Solid, axisLabelVisible: false, title: `${formatUpbitKrwPrice(p1.price)}${formatUpbitKrwPrice(p2.price)}` });
this.drawingLines.push(l); this.drawingLines.push(l);
this.drawingPoints = []; this.drawingPoints = [];
} else if (tool === 'measure' && this.drawingPoints.length === 2) { } else if (tool === 'measure' && this.drawingPoints.length === 2) {
+6 -2
View File
@@ -1,4 +1,5 @@
import type { ChartType, Timeframe } from '../types'; import type { ChartType, Timeframe } from '../types';
import { formatUpbitKrwPrice } from './safeFormat';
export function toUpbitMarket(symbol: string): string { export function toUpbitMarket(symbol: string): string {
return symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`; return symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
@@ -79,8 +80,11 @@ export const pct = (v: number, dec = 1) =>
export const pctAbs = (v: number, dec = 0) => export const pctAbs = (v: number, dec = 0) =>
isFinite(v) ? `${(v * 100).toFixed(dec)}%` : '0%'; isFinite(v) ? `${(v * 100).toFixed(dec)}%` : '0%';
export const krw = (v: number) => export const krw = (v: number) => {
isFinite(v) ? `${v >= 0 ? '+' : ''}${Math.round(v).toLocaleString()}` : ''; if (!isFinite(v)) return '';
const sign = v >= 0 ? '+' : '-';
return `${sign}${formatUpbitKrwPrice(Math.abs(v), '0')}`;
};
const TF_SEC: Record<string, number> = { const TF_SEC: Record<string, number> = {
'1m': 60, '3m': 180, '5m': 300, '10m': 600, '15m': 900, '30m': 1800, '1m': 60, '3m': 180, '5m': 300, '10m': 600, '15m': 900, '30m': 1800,
+2 -1
View File
@@ -3,6 +3,7 @@
*/ */
import type { TickerData } from '../hooks/useMarketTicker'; import type { TickerData } from '../hooks/useMarketTicker';
import type { OHLCVBar } from '../types'; import type { OHLCVBar } from '../types';
import { formatUpbitKrwPrice } from './safeFormat';
export interface ChartLiveQuote { export interface ChartLiveQuote {
price: number | null; price: number | null;
@@ -41,7 +42,7 @@ export function resolveChartLiveQuote(
export function formatChartLivePrice(price: number | null): string { export function formatChartLivePrice(price: number | null): string {
if (price == null || !Number.isFinite(price)) return '—'; if (price == null || !Number.isFinite(price)) return '—';
return `${Math.round(price).toLocaleString('ko-KR')} KRW`; return `${formatUpbitKrwPrice(price)} KRW`;
} }
export function formatChartLiveChangePct(changePct: number | null): string { export function formatChartLiveChangePct(changePct: number | null): string {
+8 -10
View File
@@ -1,5 +1,6 @@
import type { OHLCVBar, Timeframe } from '../types'; import type { OHLCVBar, Timeframe } from '../types';
import { formatUnixForChart } from './timezone'; import { formatUnixForChart } from './timezone';
import { formatUpbitKrwPrice } from './safeFormat';
export const SYMBOLS = [ export const SYMBOLS = [
'BTCUSD','ETHUSD','BNBUSD','SOLUSD','ADAUSD', 'BTCUSD','ETHUSD','BNBUSD','SOLUSD','ADAUSD',
@@ -82,25 +83,22 @@ export function generateBars(symbol: string, timeframe: Timeframe): OHLCVBar[] {
} }
export function formatPrice(price: number): string { export function formatPrice(price: number): string {
if (price >= 10000) return price.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); return formatUpbitKrwPrice(price, String(price));
if (price >= 100) return price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
if (price >= 1) return price.toLocaleString('en-US', { minimumFractionDigits: 3, maximumFractionDigits: 4 });
return price.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 6 });
} }
/** 실시간 차트 우측 가격축·크로스헤어·시리즈 라벨 (KRW 주가 — 소수점 없음) */ /**
* 실시간 차트 우측 가격축·크로스헤어·시리즈 라벨 — 업비트 KRW 가격 표시 규칙 적용.
* LightweightCharts formatter 콜백으로 사용되므로 항상 string 반환.
*/
export function formatChartAxisPrice(price: number): string { export function formatChartAxisPrice(price: number): string {
if (!Number.isFinite(price)) return String(price); if (!Number.isFinite(price)) return String(price);
return Math.round(price).toLocaleString('en-US', { return formatUpbitKrwPrice(price, String(price));
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
} }
/** 보조지표 pane 우측 가격축·시리즈 라벨 (소수점 2자리 고정) */ /** 보조지표 pane 우측 가격축·시리즈 라벨 (소수점 2자리 고정) */
export function formatIndicatorAxisPrice(price: number): string { export function formatIndicatorAxisPrice(price: number): string {
if (!Number.isFinite(price)) return String(price); if (!Number.isFinite(price)) return String(price);
return price.toLocaleString('en-US', { return price.toLocaleString('ko-KR', {
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2,
}); });
+93
View File
@@ -1,3 +1,96 @@
/**
* 업비트 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에서 문자열로 올 수 있는 숫자를 안전하게 처리 * API·JSON·WebSocket에서 문자열로 올 수 있는 숫자를 안전하게 처리
*/ */
+2 -1
View File
@@ -1,6 +1,7 @@
/** /**
* 매매 시그널 알림 표시용 포맷·라벨 * 매매 시그널 알림 표시용 포맷·라벨
*/ */
import { formatUpbitKrwPrice } from './safeFormat';
import { formatUnixWithChartPattern, splitChartTimePattern } from './chartTimeFormat'; import { formatUnixWithChartPattern, splitChartTimePattern } from './chartTimeFormat';
import { getKoreanName } from './marketNameCache'; import { getKoreanName } from './marketNameCache';
import { getTradeAlertTimeFormat } from './tradeAlertTimeFormat'; import { getTradeAlertTimeFormat } from './tradeAlertTimeFormat';
@@ -27,7 +28,7 @@ export function parseMarket(market: string) {
export function formatSignalPrice(price: number): string { export function formatSignalPrice(price: number): string {
if (!Number.isFinite(price) || price <= 0) return '—'; if (!Number.isFinite(price) || price <= 0) return '—';
return `${price.toLocaleString('ko-KR', { maximumFractionDigits: 0 })}`; return `${formatUpbitKrwPrice(price, '—')}`;
} }
export function formatSignalTime(unixSec: number, withDate = true): string { export function formatSignalTime(unixSec: number, withDate = true): string {