차트 수정
This commit is contained in:
@@ -36,6 +36,10 @@ const TICK_FIELD_INDEX = {
|
||||
bidPrice1: 11,
|
||||
tradeVolume: 12,
|
||||
accumulatedVolume: 13,
|
||||
askSize1: 36,
|
||||
bidSize1: 37,
|
||||
totalAskSize: 38,
|
||||
totalBidSize: 39,
|
||||
sellExecutionCount: 15,
|
||||
buyExecutionCount: 16,
|
||||
netBuyExecutionCount: 17,
|
||||
@@ -50,6 +54,7 @@ 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_TOTAL = "H0UNASP0";
|
||||
const ORDERBOOK_TR_ID_OVERTIME = "H0STOAA0";
|
||||
export function parseKisRealtimeTickBatch(raw: string, expectedSymbol: string) {
|
||||
if (!/^([01])\|/.test(raw)) return [] as DashboardRealtimeTradeTick[];
|
||||
@@ -117,6 +122,10 @@ export function parseKisRealtimeTickBatch(raw: string, expectedSymbol: string) {
|
||||
tradeStrength: readNumber(values, base + TICK_FIELD_INDEX.tradeStrength),
|
||||
askPrice1: readNumber(values, base + TICK_FIELD_INDEX.askPrice1),
|
||||
bidPrice1: readNumber(values, base + TICK_FIELD_INDEX.bidPrice1),
|
||||
askSize1: readNumber(values, base + TICK_FIELD_INDEX.askSize1),
|
||||
bidSize1: readNumber(values, base + TICK_FIELD_INDEX.bidSize1),
|
||||
totalAskSize: readNumber(values, base + TICK_FIELD_INDEX.totalAskSize),
|
||||
totalBidSize: readNumber(values, base + TICK_FIELD_INDEX.totalBidSize),
|
||||
sellExecutionCount: readNumber(
|
||||
values,
|
||||
base + TICK_FIELD_INDEX.sellExecutionCount,
|
||||
@@ -191,15 +200,25 @@ export function resolveOrderBookTrIds(
|
||||
) {
|
||||
if (env === "mock") return [ORDERBOOK_TR_ID];
|
||||
|
||||
// 시간외(16:00~18:00): 공식 문서 TR(H0STOAA0)을 최우선 구독하고
|
||||
// 누락 대비용으로 통합/정규 TR을 뒤에 둡니다.
|
||||
if (shouldUseAfterHoursSinglePriceTr(session)) {
|
||||
return uniqueTrIds([ORDERBOOK_TR_ID_OVERTIME, ORDERBOOK_TR_ID]);
|
||||
return uniqueTrIds([
|
||||
ORDERBOOK_TR_ID_OVERTIME,
|
||||
ORDERBOOK_TR_ID_TOTAL,
|
||||
ORDERBOOK_TR_ID,
|
||||
]);
|
||||
}
|
||||
|
||||
if (session === "afterCloseFixedPrice") {
|
||||
return uniqueTrIds([ORDERBOOK_TR_ID, ORDERBOOK_TR_ID_OVERTIME]);
|
||||
return uniqueTrIds([
|
||||
ORDERBOOK_TR_ID_TOTAL,
|
||||
ORDERBOOK_TR_ID,
|
||||
ORDERBOOK_TR_ID_OVERTIME,
|
||||
]);
|
||||
}
|
||||
|
||||
return uniqueTrIds([ORDERBOOK_TR_ID]);
|
||||
return uniqueTrIds([ORDERBOOK_TR_ID, ORDERBOOK_TR_ID_TOTAL]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,71 +261,35 @@ export function parseKisRealtimeOrderbook(
|
||||
}
|
||||
|
||||
const values = parts[3].split("^");
|
||||
// 시간외(H0STOAA0)는 문서 버전에 따라 9레벨/10레벨이 혼재할 수 있어
|
||||
// payload 길이로 레벨 수를 동적으로 판별합니다.
|
||||
const levelCount = trId === "H0STOAA0" ? (values.length >= 56 ? 10 : 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 overtimeTotalAskIndex = totalBidIndex + 1;
|
||||
const overtimeTotalBidIndex = overtimeTotalAskIndex + 1;
|
||||
const anticipatedPriceIndex = overtimeTotalBidIndex + 1;
|
||||
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;
|
||||
// 시간외(H0STOAA0)는 문서 버전에 따라 9호가/10호가가 혼재할 수 있어
|
||||
// 두 형식을 모두 시도한 뒤 의미 있는 데이터 점수가 더 높은 결과를 선택합니다.
|
||||
if (trId === "H0STOAA0") {
|
||||
const parsedByNineLevels = parseOrderBookByLevelCount(
|
||||
values,
|
||||
normalizedExpected,
|
||||
9,
|
||||
);
|
||||
const parsedByTenLevels = parseOrderBookByLevelCount(
|
||||
values,
|
||||
normalizedExpected,
|
||||
10,
|
||||
);
|
||||
|
||||
if (values.length < minFieldLength) return null;
|
||||
const candidates = [parsedByNineLevels, parsedByTenLevels].filter(
|
||||
(item): item is DashboardStockOrderBookResponse => item !== null,
|
||||
);
|
||||
if (candidates.length === 0) 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 pickBestOrderBookPayload(candidates);
|
||||
}
|
||||
|
||||
const regularTotalAskSize = readNumber(values, totalAskIndex);
|
||||
const regularTotalBidSize = readNumber(values, totalBidIndex);
|
||||
const overtimeTotalAskSize = readNumber(values, overtimeTotalAskIndex);
|
||||
const overtimeTotalBidSize = readNumber(values, overtimeTotalBidIndex);
|
||||
|
||||
return {
|
||||
symbol: normalizedExpected,
|
||||
// 장후 시간외에서는 일반 총잔량이 0이고 OVTM 총잔량만 채워지는 경우가 있습니다.
|
||||
totalAskSize:
|
||||
regularTotalAskSize > 0 ? regularTotalAskSize : overtimeTotalAskSize,
|
||||
totalBidSize:
|
||||
regularTotalBidSize > 0 ? regularTotalBidSize : overtimeTotalBidSize,
|
||||
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(),
|
||||
};
|
||||
return parseOrderBookByLevelCount(values, normalizedExpected, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,3 +319,103 @@ function readNumber(values: string[], index: number) {
|
||||
function uniqueTrIds(ids: string[]) {
|
||||
return [...new Set(ids)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description levelCount(9/10)에 맞춰 호가 payload를 파싱합니다.
|
||||
* @see features/trade/utils/kisRealtimeUtils.ts parseKisRealtimeOrderbook 시간외/정규장 파서에서 공통 사용합니다.
|
||||
*/
|
||||
function parseOrderBookByLevelCount(
|
||||
values: string[],
|
||||
symbol: string,
|
||||
levelCount: 9 | 10,
|
||||
): DashboardStockOrderBookResponse | 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 overtimeTotalAskIndex = totalBidIndex + 1;
|
||||
const overtimeTotalBidIndex = overtimeTotalAskIndex + 1;
|
||||
const anticipatedPriceIndex = overtimeTotalBidIndex + 1;
|
||||
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 levels = Array.from({ length: levelCount }, (_, index) => ({
|
||||
askPrice: readNumber(values, askPriceStart + index),
|
||||
bidPrice: readNumber(values, bidPriceStart + index),
|
||||
askSize: readNumber(values, askSizeStart + index),
|
||||
bidSize: readNumber(values, bidSizeStart + index),
|
||||
}));
|
||||
|
||||
const regularTotalAskSize = readNumber(values, totalAskIndex);
|
||||
const regularTotalBidSize = readNumber(values, totalBidIndex);
|
||||
const overtimeTotalAskSize = readNumber(values, overtimeTotalAskIndex);
|
||||
const overtimeTotalBidSize = readNumber(values, overtimeTotalBidIndex);
|
||||
|
||||
return {
|
||||
symbol,
|
||||
totalAskSize:
|
||||
regularTotalAskSize > 0 ? regularTotalAskSize : overtimeTotalAskSize,
|
||||
totalBidSize:
|
||||
regularTotalBidSize > 0 ? regularTotalBidSize : overtimeTotalBidSize,
|
||||
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,
|
||||
source: "REALTIME",
|
||||
tradingEnv: "real",
|
||||
fetchedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 복수 파싱 결과 중 실제 표시 값이 풍부한 payload를 선택합니다.
|
||||
* @see features/trade/utils/kisRealtimeUtils.ts parseKisRealtimeOrderbook 시간외 9/10호가 자동 선택에 사용합니다.
|
||||
*/
|
||||
function pickBestOrderBookPayload(
|
||||
candidates: DashboardStockOrderBookResponse[],
|
||||
) {
|
||||
return [...candidates].sort((left, right) => {
|
||||
return scoreOrderBookPayload(right) - scoreOrderBookPayload(left);
|
||||
})[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 호가 payload가 실제로 얼마나 유효한지 점수화합니다.
|
||||
* @see features/trade/utils/kisRealtimeUtils.ts pickBestOrderBookPayload 시간외 파서 후보 비교용입니다.
|
||||
*/
|
||||
function scoreOrderBookPayload(payload: DashboardStockOrderBookResponse) {
|
||||
const nonZeroLevels = payload.levels.filter(
|
||||
(level) =>
|
||||
level.askPrice > 0 ||
|
||||
level.bidPrice > 0 ||
|
||||
level.askSize > 0 ||
|
||||
level.bidSize > 0,
|
||||
).length;
|
||||
|
||||
return (
|
||||
nonZeroLevels * 10 +
|
||||
(payload.totalAskSize > 0 ? 4 : 0) +
|
||||
(payload.totalBidSize > 0 ? 4 : 0) +
|
||||
((payload.anticipatedPrice ?? 0) > 0 ? 2 : 0) +
|
||||
((payload.accumulatedVolume ?? 0) > 0 ? 1 : 0)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user