스킬 정리 및 리팩토링
This commit is contained in:
210
features/trade/components/orderbook/orderbook-utils.ts
Normal file
210
features/trade/components/orderbook/orderbook-utils.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import type {
|
||||
DashboardRealtimeTradeTick,
|
||||
DashboardStockOrderBookResponse,
|
||||
} from "@/features/trade/types/trade.types";
|
||||
|
||||
type OrderBookLevels = DashboardStockOrderBookResponse["levels"];
|
||||
|
||||
export interface BookRow {
|
||||
price: number;
|
||||
size: number;
|
||||
changeValue: number | null;
|
||||
isHighlighted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 실시간 호가 레벨 데이터가 실제 값을 포함하는지 검사합니다.
|
||||
* @see features/trade/components/orderbook/OrderBook.tsx levels 계산에서 WS 호가 유효성 판단에 사용합니다.
|
||||
*/
|
||||
export function hasOrderBookLevelData(levels: OrderBookLevels) {
|
||||
return levels.some(
|
||||
(level) =>
|
||||
level.askPrice > 0 ||
|
||||
level.bidPrice > 0 ||
|
||||
level.askSize > 0 ||
|
||||
level.bidSize > 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description H0STOAA0 미수신 상황에서 체결(H0UNCNT0) 데이터로 최소 1호가를 구성합니다.
|
||||
* @see features/trade/utils/kisRealtimeUtils.ts parseKisRealtimeTickBatch 체결 데이터에서 1호가/잔량/총잔량을 파싱합니다.
|
||||
*/
|
||||
export function buildFallbackLevelsFromTick(
|
||||
latestTick: DashboardRealtimeTradeTick | null,
|
||||
) {
|
||||
if (!latestTick) return [] as OrderBookLevels;
|
||||
if (latestTick.askPrice1 <= 0 && latestTick.bidPrice1 <= 0) {
|
||||
return [] as OrderBookLevels;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
askPrice: latestTick.askPrice1,
|
||||
bidPrice: latestTick.bidPrice1,
|
||||
askSize: Math.max(latestTick.askSize1, 0),
|
||||
bidSize: Math.max(latestTick.bidSize1, 0),
|
||||
},
|
||||
] satisfies OrderBookLevels;
|
||||
}
|
||||
|
||||
/** 천단위 구분 포맷 */
|
||||
export function fmt(v: number) {
|
||||
return Number.isFinite(v) ? v.toLocaleString("ko-KR") : "0";
|
||||
}
|
||||
|
||||
/** 부호 포함 퍼센트 */
|
||||
export function fmtPct(v: number) {
|
||||
return `${v > 0 ? "+" : ""}${v.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
/** 등락률 계산 */
|
||||
export function pctChange(price: number, base: number) {
|
||||
return base > 0 ? ((price - base) / base) * 100 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 증감 숫자를 부호 포함 문자열로 포맷합니다.
|
||||
* @see features/trade/components/orderbook/orderbook-sections.tsx BookSideRows
|
||||
*/
|
||||
export function fmtSignedChange(v: number) {
|
||||
if (!Number.isFinite(v)) return "-";
|
||||
if (v > 0) return `+${fmt(v)}`;
|
||||
if (v < 0) return `-${fmt(Math.abs(v))}`;
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 증감값에 따라 색상 톤 클래스를 반환합니다.
|
||||
* @see features/trade/components/orderbook/orderbook-sections.tsx BookSideRows
|
||||
*/
|
||||
export function getChangeToneClass(
|
||||
changeValue: number | null,
|
||||
neutralClass = "text-muted-foreground",
|
||||
) {
|
||||
if (changeValue === null) {
|
||||
return neutralClass;
|
||||
}
|
||||
if (changeValue > 0) {
|
||||
return "text-red-500";
|
||||
}
|
||||
if (changeValue < 0) {
|
||||
return "text-blue-600 dark:text-blue-400";
|
||||
}
|
||||
return neutralClass;
|
||||
}
|
||||
|
||||
/** 체결 시각 포맷 */
|
||||
export function fmtTime(hms: string) {
|
||||
if (!hms || hms.length !== 6) return "--:--:--";
|
||||
return `${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 체결 틱 데이터에서 체결 주도 방향(매수/매도/중립)을 계산합니다.
|
||||
* @see features/trade/components/orderbook/orderbook-sections.tsx TradeTape 체결량 글자색 결정에 사용합니다.
|
||||
*/
|
||||
export function resolveTickExecutionSide(
|
||||
tick: DashboardRealtimeTradeTick,
|
||||
olderTick?: DashboardRealtimeTradeTick,
|
||||
) {
|
||||
const executionClassCode = (tick.executionClassCode ?? "").trim();
|
||||
if (executionClassCode === "1" || executionClassCode === "2") {
|
||||
return "buy" as const;
|
||||
}
|
||||
if (executionClassCode === "4" || executionClassCode === "5") {
|
||||
return "sell" as const;
|
||||
}
|
||||
|
||||
if (olderTick) {
|
||||
const netBuyDelta =
|
||||
tick.netBuyExecutionCount - olderTick.netBuyExecutionCount;
|
||||
if (netBuyDelta > 0) return "buy" as const;
|
||||
if (netBuyDelta < 0) return "sell" as const;
|
||||
|
||||
const buyCountDelta = tick.buyExecutionCount - olderTick.buyExecutionCount;
|
||||
const sellCountDelta =
|
||||
tick.sellExecutionCount - olderTick.sellExecutionCount;
|
||||
if (buyCountDelta > sellCountDelta) return "buy" as const;
|
||||
if (buyCountDelta < sellCountDelta) return "sell" as const;
|
||||
}
|
||||
|
||||
if (tick.askPrice1 > 0 && tick.bidPrice1 > 0) {
|
||||
if (tick.price >= tick.askPrice1 && tick.price > tick.bidPrice1) {
|
||||
return "buy" as const;
|
||||
}
|
||||
if (tick.price <= tick.bidPrice1 && tick.price < tick.askPrice1) {
|
||||
return "sell" as const;
|
||||
}
|
||||
}
|
||||
|
||||
if (tick.tradeStrength > 100) return "buy" as const;
|
||||
if (tick.tradeStrength < 100) return "sell" as const;
|
||||
|
||||
return "neutral" as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 호가 레벨을 화면 렌더링용 행 모델로 변환합니다.
|
||||
* @summary UI 흐름: OrderBook(levels/basePrice/latestPrice) -> buildBookRows -> BookSideRows -> 가격/증감 반영
|
||||
* @see features/trade/components/orderbook/OrderBook.tsx OrderBook askRows/bidRows 계산
|
||||
*/
|
||||
export function buildBookRows({
|
||||
levels,
|
||||
side,
|
||||
basePrice,
|
||||
latestPrice,
|
||||
}: {
|
||||
levels: OrderBookLevels;
|
||||
side: "ask" | "bid";
|
||||
basePrice: number;
|
||||
latestPrice: number;
|
||||
}) {
|
||||
const normalizedLevels = side === "ask" ? [...levels].reverse() : levels;
|
||||
|
||||
return normalizedLevels.map((level) => {
|
||||
const price = side === "ask" ? level.askPrice : level.bidPrice;
|
||||
const size = side === "ask" ? level.askSize : level.bidSize;
|
||||
const changeValue = resolvePriceChange(price, basePrice);
|
||||
|
||||
return {
|
||||
price,
|
||||
size: Math.max(size, 0),
|
||||
changeValue,
|
||||
isHighlighted: latestPrice > 0 && price === latestPrice,
|
||||
} satisfies BookRow;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 호가/체결 증감 표시용 기준가(전일종가)를 결정합니다.
|
||||
* @summary UI 흐름: OrderBook props -> resolveReferencePrice -> 호가행/체결가 증감 계산 반영
|
||||
* @see features/trade/components/orderbook/OrderBook.tsx basePrice 계산
|
||||
*/
|
||||
export function resolveReferencePrice({
|
||||
referencePrice,
|
||||
latestTick,
|
||||
}: {
|
||||
referencePrice?: number;
|
||||
latestTick: DashboardRealtimeTradeTick | null;
|
||||
}) {
|
||||
if ((referencePrice ?? 0) > 0) {
|
||||
return referencePrice!;
|
||||
}
|
||||
|
||||
if (latestTick?.price && Number.isFinite(latestTick.change)) {
|
||||
const derivedPrevClose = latestTick.price - latestTick.change;
|
||||
if (derivedPrevClose > 0) {
|
||||
return derivedPrevClose;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function resolvePriceChange(price: number, basePrice: number) {
|
||||
if (price <= 0 || basePrice <= 0) {
|
||||
return null;
|
||||
}
|
||||
return price - basePrice;
|
||||
}
|
||||
Reference in New Issue
Block a user