Files
auto-trade/features/trade/hooks/useTradeTickSubscription.ts

111 lines
3.7 KiB
TypeScript

import { useState, useRef, useEffect } from "react";
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
import type { DashboardRealtimeTradeTick } from "@/features/trade/types/trade.types";
import { useKisWebSocketStore } from "@/features/kis-realtime/stores/kisWebSocketStore";
import {
parseKisRealtimeTickBatch,
resolveTradeTrIds,
} from "@/features/trade/utils/kisRealtimeUtils";
import type { DomesticKisSession } from "@/lib/kis/domestic-market-session";
const MAX_TRADE_TICKS = 10;
interface UseTradeTickSubscriptionParams {
symbol: string | undefined;
isVerified: boolean;
credentials: KisRuntimeCredentials | null;
marketSession: DomesticKisSession;
onTick?: (tick: DashboardRealtimeTradeTick) => void;
}
/**
* @description 실시간 체결가(Tick) 구독 로직을 담당하는 훅입니다.
* - 웹소켓을 통해 들어오는 체결 데이터를 파싱(parsing)하고
* - 중복 데이터(deduplication)를 필터링하며
* - 최근 N개의 체결 내역(recentTradeTicks)과 최신 체결가(latestTick) 상태를 관리합니다.
*/
export function useTradeTickSubscription({
symbol,
isVerified,
credentials,
marketSession,
onTick,
}: UseTradeTickSubscriptionParams) {
const [latestTick, setLatestTick] =
useState<DashboardRealtimeTradeTick | null>(null);
const [recentTradeTicks, setRecentTradeTicks] = useState<
DashboardRealtimeTradeTick[]
>([]);
const [lastTickAt, setLastTickAt] = useState<number | null>(null);
const seenTickRef = useRef<Set<string>>(new Set());
const { subscribe, connect } = useKisWebSocketStore();
const onTickRef = useRef(onTick);
useEffect(() => {
onTickRef.current = onTick;
}, [onTick]);
// 1. 심볼이 변경되면 상태를 초기화합니다.
// 1. 심볼 변경 시 상태 초기화 (Render-time adjustment)
const [prevSymbol, setPrevSymbol] = useState(symbol);
if (symbol !== prevSymbol) {
setPrevSymbol(symbol);
setLatestTick(null);
setRecentTradeTicks([]);
setLastTickAt(null);
}
// Ref는 렌더링 도중 수정하면 안 되므로 useEffect에서 초기화
useEffect(() => {
seenTickRef.current.clear();
}, [symbol]);
// 2. 실시간 데이터 구독
useEffect(() => {
if (!symbol || !isVerified || !credentials) return;
connect();
const trIds = resolveTradeTrIds(credentials.tradingEnv, marketSession);
const unsubscribers: Array<() => void> = [];
const handleTradeMessage = (data: string) => {
const ticks = parseKisRealtimeTickBatch(data, symbol);
if (ticks.length === 0) return;
const meaningfulTicks = ticks.filter((tick) => tick.tradeVolume > 0);
if (meaningfulTicks.length === 0) return;
const dedupedTicks = meaningfulTicks.filter((tick) => {
const key = `${tick.tickTime}-${tick.price}-${tick.tradeVolume}`;
if (seenTickRef.current.has(key)) return false;
seenTickRef.current.add(key);
if (seenTickRef.current.size > 5_000) seenTickRef.current.clear();
return true;
});
const latest = meaningfulTicks[meaningfulTicks.length - 1];
setLatestTick(latest);
setLastTickAt(Date.now());
if (dedupedTicks.length > 0) {
setRecentTradeTicks((prev) =>
[...dedupedTicks.reverse(), ...prev].slice(0, MAX_TRADE_TICKS),
);
}
onTickRef.current?.(latest);
};
for (const trId of trIds) {
unsubscribers.push(subscribe(trId, symbol, handleTradeMessage));
}
return () => {
unsubscribers.forEach((unsub) => unsub());
};
}, [symbol, isVerified, credentials, marketSession, connect, subscribe]);
return { latestTick, recentTradeTicks, lastTickAt };
}