차트 수정
This commit is contained in:
@@ -3,13 +3,53 @@
|
||||
* @description StockLineChart에서 사용하는 유틸리티 함수 모음
|
||||
*/
|
||||
|
||||
import type { UTCTimestamp } from "lightweight-charts";
|
||||
import type {
|
||||
TickMarkType,
|
||||
Time,
|
||||
UTCTimestamp,
|
||||
} from "lightweight-charts";
|
||||
import type {
|
||||
DashboardChartTimeframe,
|
||||
DashboardRealtimeTradeTick,
|
||||
StockCandlePoint,
|
||||
} from "@/features/dashboard/types/dashboard.types";
|
||||
|
||||
const KRW_FORMATTER = new Intl.NumberFormat("ko-KR");
|
||||
const KST_TIME_ZONE = "Asia/Seoul";
|
||||
const KST_TIME_FORMATTER = new Intl.DateTimeFormat("ko-KR", {
|
||||
timeZone: KST_TIME_ZONE,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
const KST_TIME_SECONDS_FORMATTER = new Intl.DateTimeFormat("ko-KR", {
|
||||
timeZone: KST_TIME_ZONE,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
const KST_DATE_FORMATTER = new Intl.DateTimeFormat("ko-KR", {
|
||||
timeZone: KST_TIME_ZONE,
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
const KST_MONTH_FORMATTER = new Intl.DateTimeFormat("ko-KR", {
|
||||
timeZone: KST_TIME_ZONE,
|
||||
month: "short",
|
||||
});
|
||||
const KST_YEAR_FORMATTER = new Intl.DateTimeFormat("ko-KR", {
|
||||
timeZone: KST_TIME_ZONE,
|
||||
year: "numeric",
|
||||
});
|
||||
const KST_CROSSHAIR_FORMATTER = new Intl.DateTimeFormat("ko-KR", {
|
||||
timeZone: KST_TIME_ZONE,
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
// ─── 타입 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -186,6 +226,63 @@ export function upsertRealtimeBar(
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 실시간 체결 틱을 차트용 ChartBar로 변환합니다. (KST 날짜 + tickTime 기준)
|
||||
* @see features/dashboard/hooks/useKisTradeWebSocket.ts latestTick
|
||||
* @see features/dashboard/components/chart/StockLineChart.tsx 실시간 캔들 반영
|
||||
*/
|
||||
export function toRealtimeTickBar(
|
||||
tick: DashboardRealtimeTradeTick,
|
||||
timeframe: DashboardChartTimeframe,
|
||||
now = new Date(),
|
||||
): ChartBar | null {
|
||||
if (!Number.isFinite(tick.price) || tick.price <= 0) return null;
|
||||
|
||||
const hhmmss = normalizeTickTime(tick.tickTime);
|
||||
if (!hhmmss) return null;
|
||||
|
||||
const ymd = getKstYmd(now);
|
||||
const baseTimestamp = toKstTimestamp(ymd, hhmmss);
|
||||
const alignedTimestamp = alignTimestamp(baseTimestamp, timeframe);
|
||||
const minuteFrame = isMinuteTimeframe(timeframe);
|
||||
|
||||
return {
|
||||
time: alignedTimestamp,
|
||||
open: minuteFrame ? tick.price : Math.max(tick.open, tick.price),
|
||||
high: minuteFrame ? tick.price : Math.max(tick.high, tick.price),
|
||||
low: minuteFrame ? tick.price : Math.min(tick.low || tick.price, tick.price),
|
||||
close: tick.price,
|
||||
volume: minuteFrame
|
||||
? Math.max(tick.tradeVolume, 0)
|
||||
: Math.max(tick.accumulatedVolume, 0),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description lightweight-charts X축 라벨을 KST 기준으로 강제 포맷합니다.
|
||||
* @see features/dashboard/components/chart/StockLineChart.tsx createChart options.timeScale.tickMarkFormatter
|
||||
*/
|
||||
export function formatKstTickMark(time: Time, tickMarkType: TickMarkType) {
|
||||
const date = toDateFromChartTime(time);
|
||||
if (!date) return null;
|
||||
|
||||
if (tickMarkType === 0) return KST_YEAR_FORMATTER.format(date);
|
||||
if (tickMarkType === 1) return KST_MONTH_FORMATTER.format(date);
|
||||
if (tickMarkType === 2) return KST_DATE_FORMATTER.format(date);
|
||||
if (tickMarkType === 4) return KST_TIME_SECONDS_FORMATTER.format(date);
|
||||
return KST_TIME_FORMATTER.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description crosshair 시간 라벨을 KST로 포맷합니다.
|
||||
* @see features/dashboard/components/chart/StockLineChart.tsx createChart options.localization.timeFormatter
|
||||
*/
|
||||
export function formatKstCrosshairTime(time: Time) {
|
||||
const date = toDateFromChartTime(time);
|
||||
if (!date) return "";
|
||||
return KST_CROSSHAIR_FORMATTER.format(date);
|
||||
}
|
||||
|
||||
// ─── 포맷터 ───────────────────────────────────────────────
|
||||
|
||||
export function formatPrice(value: number) {
|
||||
@@ -203,3 +300,49 @@ export function formatSignedPercent(value: number) {
|
||||
export function isMinuteTimeframe(tf: DashboardChartTimeframe) {
|
||||
return tf === "1m" || tf === "30m" || tf === "1h";
|
||||
}
|
||||
|
||||
function normalizeTickTime(value?: string) {
|
||||
if (!value) return null;
|
||||
const normalized = value.trim();
|
||||
return /^\d{6}$/.test(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
function getKstYmd(now = new Date()) {
|
||||
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: KST_TIME_ZONE,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).formatToParts(now);
|
||||
|
||||
const map = new Map(parts.map((part) => [part.type, part.value]));
|
||||
return `${map.get("year")}${map.get("month")}${map.get("day")}`;
|
||||
}
|
||||
|
||||
function toKstTimestamp(yyyymmdd: string, hhmmss: string) {
|
||||
const y = Number(yyyymmdd.slice(0, 4));
|
||||
const m = Number(yyyymmdd.slice(4, 6));
|
||||
const d = Number(yyyymmdd.slice(6, 8));
|
||||
const hh = Number(hhmmss.slice(0, 2));
|
||||
const mm = Number(hhmmss.slice(2, 4));
|
||||
const ss = Number(hhmmss.slice(4, 6));
|
||||
return Math.floor(Date.UTC(y, m - 1, d, hh - 9, mm, ss) / 1000);
|
||||
}
|
||||
|
||||
function toDateFromChartTime(time: Time) {
|
||||
if (typeof time === "number" && Number.isFinite(time)) {
|
||||
return new Date(time * 1000);
|
||||
}
|
||||
|
||||
if (typeof time === "string") {
|
||||
const parsed = Date.parse(time);
|
||||
return Number.isFinite(parsed) ? new Date(parsed) : null;
|
||||
}
|
||||
|
||||
if (time && typeof time === "object" && "year" in time) {
|
||||
const { year, month, day } = time;
|
||||
return new Date(Date.UTC(year, month - 1, day));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user