Files
auto-trade/features/trade/utils/kis-realtime.utils.ts

264 lines
8.8 KiB
TypeScript
Raw Normal View History

2026-02-10 11:16:39 +09:00
import type {
DashboardRealtimeTradeTick,
DashboardStockOrderBookResponse,
2026-02-11 16:31:28 +09:00
} from "@/features/trade/types/trade.types";
2026-02-10 11:16:39 +09:00
const REALTIME_SIGN_NEGATIVE = new Set(["4", "5"]);
2026-02-12 10:24:03 +09:00
const EXECUTED_REALTIME_TRADE_TR_IDS = new Set([
2026-02-11 11:18:15 +09:00
"H0STCNT0",
"H0STOUP0",
2026-02-12 10:24:03 +09:00
"H0UNCNT0",
"H0NXCNT0",
]);
const EXPECTED_REALTIME_TRADE_TR_IDS = new Set([
"H0STANC0",
2026-02-11 11:18:15 +09:00
"H0STOAC0",
2026-02-12 10:24:03 +09:00
"H0UNANC0",
"H0NXANC0",
2026-02-11 11:18:15 +09:00
]);
2026-02-10 11:16:39 +09:00
const TICK_FIELD_INDEX = {
symbol: 0,
tickTime: 1,
price: 2,
sign: 3,
change: 4,
changeRate: 5,
open: 7,
high: 8,
low: 9,
askPrice1: 10,
bidPrice1: 11,
tradeVolume: 12,
accumulatedVolume: 13,
sellExecutionCount: 15,
buyExecutionCount: 16,
netBuyExecutionCount: 17,
tradeStrength: 18,
2026-02-12 10:24:03 +09:00
executionClassCode: 21,
2026-02-10 11:16:39 +09:00
} as const;
/**
2026-02-11 16:31:28 +09:00
* @description KIS / .
* @see features/trade/hooks/useKisTradeWebSocket.ts / payload .
2026-02-10 11:16:39 +09:00
*/
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,
},
},
};
}
/**
2026-02-11 16:31:28 +09:00
* @description (raw) .
* - ( ) .
* - 0 .
* @see features/trade/hooks/useKisTradeWebSocket.ts onmessage .
2026-02-10 11:16:39 +09:00
*/
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[];
2026-02-11 11:18:15 +09:00
// TR ID check: regular tick / expected tick / after-hours tick.
2026-02-10 11:16:39 +09:00
const receivedTrId = parts[1];
2026-02-12 10:24:03 +09:00
const isExecutedTick = EXECUTED_REALTIME_TRADE_TR_IDS.has(receivedTrId);
const isExpectedTick = EXPECTED_REALTIME_TRADE_TR_IDS.has(receivedTrId);
// 체결 화면에는 "실제 체결 TR"만 반영하고 예상체결 TR은 제외합니다.
if (!isExecutedTick || isExpectedTick) {
2026-02-10 11:16:39 +09:00
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[];
const parsedCount =
Number.isInteger(tickCount) && tickCount > 0 ? tickCount : 1;
const fieldsPerTick = Math.floor(values.length / parsedCount);
if (fieldsPerTick <= TICK_FIELD_INDEX.tradeStrength) {
return [] as DashboardRealtimeTradeTick[];
}
2026-02-12 10:24:03 +09:00
const normalizedExpected = normalizeDomesticSymbol(expectedSymbol);
2026-02-10 11:16:39 +09:00
const ticks: DashboardRealtimeTradeTick[] = [];
for (let index = 0; index < parsedCount; index++) {
const base = index * fieldsPerTick;
const symbol = readString(values, base + TICK_FIELD_INDEX.symbol);
2026-02-12 10:24:03 +09:00
const normalizedSymbol = normalizeDomesticSymbol(symbol);
if (normalizedSymbol !== normalizedExpected) {
continue;
2026-02-10 11:16:39 +09:00
}
const price = readNumber(values, base + TICK_FIELD_INDEX.price);
if (!Number.isFinite(price) || price <= 0) continue;
const sign = readString(values, base + TICK_FIELD_INDEX.sign);
const rawChange = readNumber(values, base + TICK_FIELD_INDEX.change);
const rawChangeRate = readNumber(
values,
base + TICK_FIELD_INDEX.changeRate,
);
const change = REALTIME_SIGN_NEGATIVE.has(sign)
? -Math.abs(rawChange)
: rawChange;
const changeRate = REALTIME_SIGN_NEGATIVE.has(sign)
? -Math.abs(rawChangeRate)
: rawChangeRate;
ticks.push({
2026-02-12 10:24:03 +09:00
symbol: normalizedExpected,
2026-02-10 11:16:39 +09:00
tickTime: readString(values, base + TICK_FIELD_INDEX.tickTime),
price,
change,
changeRate,
tradeVolume: readNumber(values, base + TICK_FIELD_INDEX.tradeVolume),
accumulatedVolume: readNumber(
values,
base + TICK_FIELD_INDEX.accumulatedVolume,
),
tradeStrength: readNumber(values, base + TICK_FIELD_INDEX.tradeStrength),
askPrice1: readNumber(values, base + TICK_FIELD_INDEX.askPrice1),
bidPrice1: readNumber(values, base + TICK_FIELD_INDEX.bidPrice1),
sellExecutionCount: readNumber(
values,
base + TICK_FIELD_INDEX.sellExecutionCount,
),
buyExecutionCount: readNumber(
values,
base + TICK_FIELD_INDEX.buyExecutionCount,
),
netBuyExecutionCount: readNumber(
values,
base + TICK_FIELD_INDEX.netBuyExecutionCount,
),
2026-02-12 10:24:03 +09:00
executionClassCode: readString(
values,
base + TICK_FIELD_INDEX.executionClassCode,
),
2026-02-10 11:16:39 +09:00
open: readNumber(values, base + TICK_FIELD_INDEX.open),
high: readNumber(values, base + TICK_FIELD_INDEX.high),
low: readNumber(values, base + TICK_FIELD_INDEX.low),
});
}
return ticks;
}
/**
2026-02-11 16:31:28 +09:00
* @description KIS (H0STASP0/H0UNASP0/H0STOAA0) OrderBook .
* @see features/trade/hooks/useKisTradeWebSocket.ts onOrderBookMessage .
2026-02-10 11:16:39 +09:00
*/
export function parseKisRealtimeOrderbook(
raw: string,
expectedSymbol: string,
): DashboardStockOrderBookResponse | null {
if (!/^([01])\|/.test(raw)) return null;
const parts = raw.split("|");
if (parts.length < 4) return null;
const trId = parts[1];
if (trId !== "H0STASP0" && trId !== "H0UNASP0" && trId !== "H0STOAA0") {
return null;
}
const values = parts[3].split("^");
const levelCount = trId === "H0STOAA0" ? 9 : 10;
const symbol = values[0]?.trim() ?? "";
const normalizedSymbol = normalizeDomesticSymbol(symbol);
const normalizedExpected = normalizeDomesticSymbol(expectedSymbol);
if (normalizedSymbol !== normalizedExpected) return null;
const askPriceStart = 3;
const bidPriceStart = askPriceStart + levelCount;
const askSizeStart = bidPriceStart + levelCount;
const bidSizeStart = askSizeStart + levelCount;
const totalAskIndex = bidSizeStart + levelCount;
const totalBidIndex = totalAskIndex + 1;
const anticipatedPriceIndex = totalBidIndex + 3;
const anticipatedVolumeIndex = anticipatedPriceIndex + 1;
const anticipatedTotalVolumeIndex = anticipatedPriceIndex + 2;
const anticipatedChangeIndex = anticipatedPriceIndex + 3;
const anticipatedChangeSignIndex = anticipatedPriceIndex + 4;
const anticipatedChangeRateIndex = anticipatedPriceIndex + 5;
const accumulatedVolumeIndex = anticipatedPriceIndex + 6;
const totalAskDeltaIndex = anticipatedPriceIndex + 7;
const totalBidDeltaIndex = anticipatedPriceIndex + 8;
const minFieldLength = totalBidDeltaIndex + 1;
if (values.length < minFieldLength) return null;
const realtimeLevels = Array.from({ length: levelCount }, (_, i) => ({
askPrice: readNumber(values, askPriceStart + i),
bidPrice: readNumber(values, bidPriceStart + i),
askSize: readNumber(values, askSizeStart + i),
bidSize: readNumber(values, bidSizeStart + i),
}));
return {
symbol: normalizedExpected,
totalAskSize: readNumber(values, totalAskIndex),
totalBidSize: readNumber(values, totalBidIndex),
businessHour: readString(values, 1),
hourClassCode: readString(values, 2),
anticipatedPrice: readNumber(values, anticipatedPriceIndex),
anticipatedVolume: readNumber(values, anticipatedVolumeIndex),
anticipatedTotalVolume: readNumber(values, anticipatedTotalVolumeIndex),
anticipatedChange: readNumber(values, anticipatedChangeIndex),
anticipatedChangeSign: readString(values, anticipatedChangeSignIndex),
anticipatedChangeRate: readNumber(values, anticipatedChangeRateIndex),
accumulatedVolume: readNumber(values, accumulatedVolumeIndex),
totalAskSizeDelta: readNumber(values, totalAskDeltaIndex),
totalBidSizeDelta: readNumber(values, totalBidDeltaIndex),
levels: realtimeLevels,
source: "REALTIME",
tradingEnv: "real",
fetchedAt: new Date().toISOString(),
};
}
/**
2026-02-11 16:31:28 +09:00
* @description 6 .
* @see features/trade/utils/kis-realtime.utils.ts parseKisRealtimeOrderbook .
2026-02-10 11:16:39 +09:00
*/
function normalizeDomesticSymbol(value: string) {
const trimmed = value.trim();
const digits = trimmed.replace(/\D/g, "");
if (digits.length >= 6) {
return digits.slice(-6);
}
return trimmed;
}
function readString(values: string[], index: number) {
return (values[index] ?? "").trim();
}
function readNumber(values: string[], index: number) {
const raw = readString(values, index).replaceAll(",", "");
const value = Number(raw);
return Number.isFinite(value) ? value : 0;
}