대시보드 실시간 기능 추가
This commit is contained in:
@@ -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)];
|
||||
}
|
||||
Reference in New Issue
Block a user