/** * @file lib/kis/domestic-market-session.ts * @description KRX market-session helpers based on KST (Asia/Seoul) */ export type DomesticKisSession = | "openAuction" | "regular" | "closeAuction" | "afterCloseFixedPrice" | "afterHoursSinglePrice" | "closed"; export const DOMESTIC_KIS_SESSION_OVERRIDE_HEADER = "x-kis-session-override"; export const DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY = "KIS_SESSION_OVERRIDE"; const OPEN_AUCTION_START_MINUTES = 8 * 60 + 30; // 08:30 const OPEN_AUCTION_END_MINUTES = 9 * 60; // 09:00 const REGULAR_START_MINUTES = 9 * 60; // 09:00 const REGULAR_END_MINUTES = 15 * 60 + 20; // 15:20 const CLOSE_AUCTION_START_MINUTES = 15 * 60 + 20; // 15:20 const CLOSE_AUCTION_END_MINUTES = 15 * 60 + 30; // 15:30 const AFTER_CLOSE_FIXED_START_MINUTES = 15 * 60 + 40; // 15:40 const AFTER_CLOSE_FIXED_END_MINUTES = 16 * 60; // 16:00 const AFTER_HOURS_SINGLE_START_MINUTES = 16 * 60; // 16:00 const AFTER_HOURS_SINGLE_END_MINUTES = 18 * 60; // 18:00 /** * @description Converts external string to strict session enum. * @see lib/kis/domestic.ts getDomesticOrderBook * @see features/dashboard/hooks/useKisTradeWebSocket.ts resolveSessionInClient */ export function parseDomesticKisSession(value?: string | null) { if (!value) return null; const normalized = value.trim(); if (!normalized) return null; const allowed: DomesticKisSession[] = [ "openAuction", "regular", "closeAuction", "afterCloseFixedPrice", "afterHoursSinglePrice", "closed", ]; return allowed.includes(normalized as DomesticKisSession) ? (normalized as DomesticKisSession) : null; } /** * @description Returns current session in KST. * @see features/dashboard/hooks/useKisTradeWebSocket.ts WebSocket TR switching * @see lib/kis/domestic.ts REST orderbook source switching */ export function getDomesticKisSessionInKst(now = new Date()): DomesticKisSession { const { weekday, totalMinutes } = toKstWeekdayAndMinutes(now); if (weekday === "Sat" || weekday === "Sun") { return "closed"; } if ( totalMinutes >= OPEN_AUCTION_START_MINUTES && totalMinutes < OPEN_AUCTION_END_MINUTES ) { return "openAuction"; } if ( totalMinutes >= REGULAR_START_MINUTES && totalMinutes < REGULAR_END_MINUTES ) { return "regular"; } if ( totalMinutes >= CLOSE_AUCTION_START_MINUTES && totalMinutes < CLOSE_AUCTION_END_MINUTES ) { return "closeAuction"; } if ( totalMinutes >= AFTER_CLOSE_FIXED_START_MINUTES && totalMinutes < AFTER_CLOSE_FIXED_END_MINUTES ) { return "afterCloseFixedPrice"; } if ( totalMinutes >= AFTER_HOURS_SINGLE_START_MINUTES && totalMinutes < AFTER_HOURS_SINGLE_END_MINUTES ) { return "afterHoursSinglePrice"; } return "closed"; } /** * @description If override is valid, use it. Otherwise use real KST time. * @see app/api/kis/domestic/orderbook/route.ts session override header * @see features/dashboard/hooks/useKisTradeWebSocket.ts localStorage override */ export function resolveDomesticKisSession( override?: string | null, now = new Date(), ) { return parseDomesticKisSession(override) ?? getDomesticKisSessionInKst(now); } /** * @description Maps detailed KIS session to dashboard phase. * @see lib/kis/domestic.ts getDomesticOverview */ export function mapDomesticKisSessionToMarketPhase( session: DomesticKisSession, ): "regular" | "afterHours" { if ( session === "regular" || session === "openAuction" || session === "closeAuction" ) { return "regular"; } return "afterHours"; } /** * @description Whether orderbook should use overtime REST API. * @see lib/kis/domestic.ts getDomesticOrderBook */ export function shouldUseOvertimeOrderBookApi(session: DomesticKisSession) { return ( session === "afterCloseFixedPrice" || session === "afterHoursSinglePrice" ); } /** * @description Whether trade tick should use expected-execution TR. * @see features/dashboard/hooks/useKisTradeWebSocket.ts resolveTradeTrId */ export function shouldUseExpectedExecutionTr(session: DomesticKisSession) { return session === "openAuction" || session === "closeAuction"; } /** * @description Whether trade tick/orderbook should use after-hours single-price TR. * @see features/dashboard/hooks/useKisTradeWebSocket.ts resolveTradeTrId */ export function shouldUseAfterHoursSinglePriceTr(session: DomesticKisSession) { return session === "afterHoursSinglePrice"; } function toKstWeekdayAndMinutes(now: Date) { const parts = new Intl.DateTimeFormat("en-US", { timeZone: "Asia/Seoul", weekday: "short", hour: "2-digit", minute: "2-digit", hour12: false, }).formatToParts(now); const partMap = new Map(parts.map((part) => [part.type, part.value])); const weekday = partMap.get("weekday") ?? "Sun"; const hour = Number(partMap.get("hour") ?? "0"); const minute = Number(partMap.get("minute") ?? "0"); const totalMinutes = hour * 60 + minute; return { weekday, totalMinutes }; }