주가 표시형식 수정
This commit is contained in:
@@ -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,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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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,6 +19,7 @@ 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)}%`;
|
||||||
@@ -29,7 +30,7 @@ const wonFmt= (v: number) => {
|
|||||||
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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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에서 문자열로 올 수 있는 숫자를 안전하게 처리
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user