대시보드 실시간 기능 추가

This commit is contained in:
2026-02-13 12:17:35 +09:00
parent 12feeb2775
commit 1ac907cd27
35 changed files with 2790 additions and 1032 deletions

View File

@@ -2,6 +2,11 @@ import type {
DashboardRealtimeTradeTick,
DashboardStockOrderBookResponse,
} from "@/features/trade/types/trade.types";
import {
shouldUseAfterHoursSinglePriceTr,
shouldUseExpectedExecutionTr,
type DomesticKisSession,
} from "@/lib/kis/domestic-market-session";
const REALTIME_SIGN_NEGATIVE = new Set(["4", "5"]);
const EXECUTED_REALTIME_TRADE_TR_IDS = new Set([
@@ -38,55 +43,28 @@ const TICK_FIELD_INDEX = {
executionClassCode: 21,
} as const;
/**
* @description KIS / .
* @see features/trade/hooks/useKisTradeWebSocket.ts / payload .
*/
export function buildKisRealtimeMessage(
approvalKey: string,
symbol: string,
trId: string,
trType: "1" | "2",
) {
return {
header: {
approval_key: approvalKey,
custtype: "P",
tr_type: trType,
"content-type": "utf-8",
},
body: {
input: {
tr_id: trId,
tr_key: symbol,
},
},
};
}
/**
* @description (raw) .
* - ( ) .
* - 0 .
* @see features/trade/hooks/useKisTradeWebSocket.ts onmessage .
*/
const TRADE_TR_ID = "H0STCNT0";
const TRADE_TR_ID_EXPECTED = "H0STANC0";
const TRADE_TR_ID_OVERTIME = "H0STOUP0";
const TRADE_TR_ID_OVERTIME_EXPECTED = "H0STOAC0";
const TRADE_TR_ID_TOTAL = "H0UNCNT0";
const TRADE_TR_ID_TOTAL_EXPECTED = "H0UNANC0";
const ORDERBOOK_TR_ID = "H0STASP0";
const ORDERBOOK_TR_ID_OVERTIME = "H0STOAA0";
export function parseKisRealtimeTickBatch(raw: string, expectedSymbol: string) {
if (!/^([01])\|/.test(raw)) return [] as DashboardRealtimeTradeTick[];
const parts = raw.split("|");
if (parts.length < 4) return [] as DashboardRealtimeTradeTick[];
// TR ID check: regular tick / expected tick / after-hours tick.
const receivedTrId = parts[1];
const isExecutedTick = EXECUTED_REALTIME_TRADE_TR_IDS.has(receivedTrId);
const isExpectedTick = EXPECTED_REALTIME_TRADE_TR_IDS.has(receivedTrId);
// 체결 화면에는 "실제 체결 TR"만 반영하고 예상체결 TR은 제외합니다.
if (!isExecutedTick || isExpectedTick) {
return [] as DashboardRealtimeTradeTick[];
}
// if (parts[1] !== expectedTrId) return [] as DashboardRealtimeTradeTick[];
const tickCount = Number(parts[2] ?? "1");
const values = parts[3].split("^");
if (values.length === 0) return [] as DashboardRealtimeTradeTick[];
@@ -164,9 +142,91 @@ export function parseKisRealtimeTickBatch(raw: string, expectedSymbol: string) {
return ticks;
}
/**
* @description / TR ID를 .
*/
export function resolveTradeTrIds(
env: "real" | "mock",
session: DomesticKisSession,
) {
if (env === "mock") return [TRADE_TR_ID];
if (shouldUseAfterHoursSinglePriceTr(session)) {
return uniqueTrIds([
TRADE_TR_ID_OVERTIME,
TRADE_TR_ID_OVERTIME_EXPECTED,
TRADE_TR_ID_TOTAL,
TRADE_TR_ID_TOTAL_EXPECTED,
]);
}
if (shouldUseExpectedExecutionTr(session)) {
return uniqueTrIds([
TRADE_TR_ID_EXPECTED,
TRADE_TR_ID_TOTAL_EXPECTED,
TRADE_TR_ID,
TRADE_TR_ID_TOTAL,
]);
}
if (session === "afterCloseFixedPrice") {
return uniqueTrIds([
TRADE_TR_ID,
TRADE_TR_ID_TOTAL,
TRADE_TR_ID_OVERTIME,
TRADE_TR_ID_OVERTIME_EXPECTED,
TRADE_TR_ID_TOTAL_EXPECTED,
]);
}
return uniqueTrIds([TRADE_TR_ID, TRADE_TR_ID_TOTAL]);
}
/**
* @description TR ID .
*/
export function resolveOrderBookTrIds(
env: "real" | "mock",
session: DomesticKisSession,
) {
if (env === "mock") return [ORDERBOOK_TR_ID];
if (shouldUseAfterHoursSinglePriceTr(session)) {
return uniqueTrIds([ORDERBOOK_TR_ID_OVERTIME, ORDERBOOK_TR_ID]);
}
if (session === "afterCloseFixedPrice") {
return uniqueTrIds([ORDERBOOK_TR_ID, ORDERBOOK_TR_ID_OVERTIME]);
}
return uniqueTrIds([ORDERBOOK_TR_ID]);
}
/**
* @description (//) .
*/
export function hasMeaningfulOrderBookPayload(
data: DashboardStockOrderBookResponse,
) {
const hasLevelData = data.levels.some(
(level) =>
level.askPrice > 0 ||
level.bidPrice > 0 ||
level.askSize > 0 ||
level.bidSize > 0,
);
const hasSummaryData =
data.totalAskSize > 0 ||
data.totalBidSize > 0 ||
(data.anticipatedPrice ?? 0) > 0 ||
(data.accumulatedVolume ?? 0) > 0;
return hasLevelData || hasSummaryData;
}
/**
* @description KIS (H0STASP0/H0UNASP0/H0STOAA0) OrderBook .
* @see features/trade/hooks/useKisTradeWebSocket.ts onOrderBookMessage .
*/
export function parseKisRealtimeOrderbook(
raw: string,
@@ -182,7 +242,9 @@ export function parseKisRealtimeOrderbook(
}
const values = parts[3].split("^");
const levelCount = trId === "H0STOAA0" ? 9 : 10;
// 시간외(H0STOAA0)는 문서 버전에 따라 9레벨/10레벨이 혼재할 수 있어
// payload 길이로 레벨 수를 동적으로 판별합니다.
const levelCount = trId === "H0STOAA0" ? (values.length >= 56 ? 10 : 9) : 10;
const symbol = values[0]?.trim() ?? "";
const normalizedSymbol = normalizeDomesticSymbol(symbol);
@@ -195,7 +257,9 @@ export function parseKisRealtimeOrderbook(
const bidSizeStart = askSizeStart + levelCount;
const totalAskIndex = bidSizeStart + levelCount;
const totalBidIndex = totalAskIndex + 1;
const anticipatedPriceIndex = totalBidIndex + 3;
const overtimeTotalAskIndex = totalBidIndex + 1;
const overtimeTotalBidIndex = overtimeTotalAskIndex + 1;
const anticipatedPriceIndex = overtimeTotalBidIndex + 1;
const anticipatedVolumeIndex = anticipatedPriceIndex + 1;
const anticipatedTotalVolumeIndex = anticipatedPriceIndex + 2;
const anticipatedChangeIndex = anticipatedPriceIndex + 3;
@@ -215,10 +279,18 @@ export function parseKisRealtimeOrderbook(
bidSize: readNumber(values, bidSizeStart + i),
}));
const regularTotalAskSize = readNumber(values, totalAskIndex);
const regularTotalBidSize = readNumber(values, totalBidIndex);
const overtimeTotalAskSize = readNumber(values, overtimeTotalAskIndex);
const overtimeTotalBidSize = readNumber(values, overtimeTotalBidIndex);
return {
symbol: normalizedExpected,
totalAskSize: readNumber(values, totalAskIndex),
totalBidSize: readNumber(values, totalBidIndex),
// 장후 시간외에서는 일반 총잔량이 0이고 OVTM 총잔량만 채워지는 경우가 있습니다.
totalAskSize:
regularTotalAskSize > 0 ? regularTotalAskSize : overtimeTotalAskSize,
totalBidSize:
regularTotalBidSize > 0 ? regularTotalBidSize : overtimeTotalBidSize,
businessHour: readString(values, 1),
hourClassCode: readString(values, 2),
anticipatedPrice: readNumber(values, anticipatedPriceIndex),
@@ -239,7 +311,6 @@ export function parseKisRealtimeOrderbook(
/**
* @description 6 .
* @see features/trade/utils/kis-realtime.utils.ts parseKisRealtimeOrderbook .
*/
function normalizeDomesticSymbol(value: string) {
const trimmed = value.trim();
@@ -261,3 +332,7 @@ function readNumber(values: string[], index: number) {
const value = Number(raw);
return Number.isFinite(value) ? value : 0;
}
function uniqueTrIds(ids: string[]) {
return [...new Set(ids)];
}