Files
auto-trade/features/trade/components/orderbook/orderbook-utils.ts

211 lines
6.2 KiB
TypeScript
Raw Normal View History

2026-02-26 09:05:17 +09:00
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;
}