120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
/**
|
|
* 업비트 최근 체결 (호가창 좌측 체결 목록용)
|
|
*/
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
|
export interface RecentTrade {
|
|
price: number;
|
|
volume: number;
|
|
side: 'ask' | 'bid'; // ask=매도체결, bid=매수체결
|
|
time: number;
|
|
}
|
|
|
|
const MAX_TRADES = 12;
|
|
|
|
interface UpbitTradeRaw {
|
|
market: string;
|
|
trade_price: number;
|
|
trade_volume: number;
|
|
ask_bid: 'ASK' | 'BID';
|
|
timestamp: number;
|
|
}
|
|
|
|
interface UpbitTradeWs {
|
|
type: string;
|
|
code: string;
|
|
trade_price: number;
|
|
trade_volume: number;
|
|
ask_bid: 'ASK' | 'BID';
|
|
timestamp: number;
|
|
}
|
|
|
|
export function useUpbitRecentTrades(market: string) {
|
|
const [trades, setTrades] = useState<RecentTrade[]>([]);
|
|
const [strength, setStrength] = useState<number | null>(null);
|
|
|
|
const wsRef = useRef<WebSocket | null>(null);
|
|
const mountedRef = useRef(true);
|
|
|
|
const pushTrade = useCallback((t: RecentTrade) => {
|
|
setTrades(prev => {
|
|
const next = [t, ...prev].slice(0, MAX_TRADES);
|
|
const buyVol = next.filter(x => x.side === 'bid').reduce((s, x) => s + x.volume, 0);
|
|
const sellVol = next.filter(x => x.side === 'ask').reduce((s, x) => s + x.volume, 0);
|
|
if (sellVol > 0) setStrength((buyVol / sellVol) * 100);
|
|
else if (buyVol > 0) setStrength(100);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const fetchSnapshot = useCallback(async () => {
|
|
try {
|
|
const res = await fetch(`/upbit-api/v1/trades/ticks?market=${market}&count=${MAX_TRADES}`);
|
|
if (!res.ok) return;
|
|
const data: UpbitTradeRaw[] = await res.json();
|
|
const list: RecentTrade[] = data.map(d => ({
|
|
price: d.trade_price,
|
|
volume: d.trade_volume,
|
|
side: d.ask_bid === 'ASK' ? 'ask' : 'bid',
|
|
time: d.timestamp,
|
|
}));
|
|
setTrades(list);
|
|
const buyVol = list.filter(x => x.side === 'bid').reduce((s, x) => s + x.volume, 0);
|
|
const sellVol = list.filter(x => x.side === 'ask').reduce((s, x) => s + x.volume, 0);
|
|
if (sellVol > 0) setStrength((buyVol / sellVol) * 100);
|
|
} catch { /* ignore */ }
|
|
}, [market]);
|
|
|
|
useEffect(() => {
|
|
mountedRef.current = true;
|
|
setTrades([]);
|
|
setStrength(null);
|
|
fetchSnapshot();
|
|
|
|
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
const ws = new WebSocket(`${proto}://${window.location.host}/upbit-ws`);
|
|
ws.binaryType = 'arraybuffer';
|
|
wsRef.current = ws;
|
|
|
|
ws.onopen = () => {
|
|
ws.send(JSON.stringify([
|
|
{ ticket: `tr-${market}-${Date.now()}` },
|
|
{ type: 'trade', codes: [market] },
|
|
]));
|
|
};
|
|
|
|
ws.onmessage = async (e: MessageEvent) => {
|
|
if (!mountedRef.current) return;
|
|
try {
|
|
let text: string;
|
|
if (typeof e.data === 'string') text = e.data;
|
|
else if (e.data instanceof ArrayBuffer) text = new TextDecoder().decode(e.data);
|
|
else if (e.data instanceof Blob) text = await e.data.text();
|
|
else return;
|
|
const msg: UpbitTradeWs = JSON.parse(text);
|
|
if (msg.type === 'trade' && msg.code === market) {
|
|
pushTrade({
|
|
price: msg.trade_price,
|
|
volume: msg.trade_volume,
|
|
side: msg.ask_bid === 'ASK' ? 'ask' : 'bid',
|
|
time: msg.timestamp,
|
|
});
|
|
}
|
|
} catch { /* ignore */ }
|
|
};
|
|
|
|
return () => {
|
|
mountedRef.current = false;
|
|
if (wsRef.current) {
|
|
wsRef.current.onopen = null;
|
|
wsRef.current.onmessage = null;
|
|
wsRef.current.onclose = null;
|
|
wsRef.current.close();
|
|
wsRef.current = null;
|
|
}
|
|
};
|
|
}, [market, fetchSnapshot, pushTrade]);
|
|
|
|
return { trades, strength };
|
|
}
|