first commit

This commit is contained in:
hayato5246
2026-03-31 19:32:59 +09:00
commit d10b794c9f
78 changed files with 1671595 additions and 0 deletions

145
static/js/orderbook.js Normal file
View File

@@ -0,0 +1,145 @@
/**
* 실시간 호가창 (OrderBook) 렌더러
* 0D: 주식호가잔량, 0w: 프로그램매매
*/
// 호가창 초기화
function initOrderBook() {
renderOrderBook(null);
renderProgram(null);
}
// 호가창 렌더링
// asks[0] = 매도1호가(최우선, 가장 낮은 매도가), asks[9] = 매도10호가
// bids[0] = 매수1호가(최우선, 가장 높은 매수가), bids[9] = 매수10호가
function renderOrderBook(ob) {
const tbody = document.getElementById('orderbookBody');
if (!tbody) return;
if (!ob) {
tbody.innerHTML = `<tr><td colspan="3" class="text-center py-4 text-xs text-gray-400">호가 데이터 수신 대기 중...</td></tr>`;
return;
}
// 최대 잔량 (진행바 비율 계산용)
const maxVol = Math.max(
...ob.asks.map(a => a.volume),
...ob.bids.map(b => b.volume),
1
);
let html = '';
// 매도호가: 10호가부터 1호가 순으로 위에서 아래 (asks[9]→asks[0])
for (let i = 9; i >= 0; i--) {
const ask = ob.asks[i] || { price: 0, volume: 0 };
const pct = Math.round((ask.volume / maxVol) * 100);
html += `
<tr class="ask-row border-b border-gray-50 hover:bg-red-50 transition-colors cursor-pointer" data-price="${ask.price}">
<td class="py-1.5 px-2 text-right">
<div class="relative h-6 flex items-center justify-end">
<div class="absolute right-0 top-0 h-full bg-red-100 rounded-l" style="width:${pct}%"></div>
<span class="relative text-xs text-gray-600 font-mono z-10">${ask.volume > 0 ? ask.volume.toLocaleString('ko-KR') : ''}</span>
</div>
</td>
<td class="py-1.5 px-2 text-center">
<span class="text-sm font-bold text-red-500">${ask.price > 0 ? ask.price.toLocaleString('ko-KR') : '-'}</span>
</td>
<td class="py-1.5 px-2"></td>
</tr>`;
}
// 예상체결 행 (스프레드 사이)
if (ob.expectedPrc > 0) {
html += `
<tr class="bg-yellow-50 border-y-2 border-yellow-300">
<td class="py-1.5 px-2 text-right text-xs text-yellow-700">예상</td>
<td class="py-1.5 px-2 text-center text-sm font-bold text-yellow-700">${ob.expectedPrc.toLocaleString('ko-KR')}</td>
<td class="py-1.5 px-2 text-left text-xs text-yellow-700">${ob.expectedVol > 0 ? ob.expectedVol.toLocaleString('ko-KR') : ''}</td>
</tr>`;
}
// 매수호가: 1호가부터 10호가 순으로 위에서 아래 (bids[0]→bids[9])
for (let i = 0; i < ob.bids.length; i++) {
const bid = ob.bids[i] || { price: 0, volume: 0 };
const pct = Math.round((bid.volume / maxVol) * 100);
html += `
<tr class="bid-row border-b border-gray-50 hover:bg-blue-50 transition-colors cursor-pointer" data-price="${bid.price}">
<td class="py-1.5 px-2"></td>
<td class="py-1.5 px-2 text-center">
<span class="text-sm font-bold text-blue-500">${bid.price > 0 ? bid.price.toLocaleString('ko-KR') : '-'}</span>
</td>
<td class="py-1.5 px-2 text-left">
<div class="relative h-6 flex items-center">
<div class="absolute left-0 top-0 h-full bg-blue-100 rounded-r" style="width:${pct}%"></div>
<span class="relative text-xs text-gray-600 font-mono z-10">${bid.volume > 0 ? bid.volume.toLocaleString('ko-KR') : ''}</span>
</div>
</td>
</tr>`;
}
tbody.innerHTML = html;
// 호가행 클릭 → 주문창 단가 자동 입력
tbody.querySelectorAll('tr[data-price]').forEach(row => {
row.addEventListener('click', () => {
const price = parseInt(row.dataset.price, 10);
if (price > 0 && window.setOrderPrice) window.setOrderPrice(price);
});
});
// 총잔량 업데이트
const totalAsk = document.getElementById('totalAskVol');
const totalBid = document.getElementById('totalBidVol');
if (totalAsk) totalAsk.textContent = ob.totalAskVol.toLocaleString('ko-KR');
if (totalBid) totalBid.textContent = ob.totalBidVol.toLocaleString('ko-KR');
// 호가 시간 업데이트
const askTime = document.getElementById('askTime');
if (askTime && ob.askTime && ob.askTime.length >= 6) {
const t = ob.askTime;
askTime.textContent = `${t.slice(0,2)}:${t.slice(2,4)}:${t.slice(4,6)}`;
}
}
// 프로그램 매매 렌더링
function renderProgram(pg) {
const container = document.getElementById('programTrading');
if (!container) return;
if (!pg) {
container.innerHTML = `<span class="text-xs text-gray-400">프로그램 매매 데이터 수신 대기 중...</span>`;
return;
}
const netClass = pg.netBuyVolume >= 0 ? 'text-red-500' : 'text-blue-500';
const netSign = pg.netBuyVolume >= 0 ? '+' : '';
container.innerHTML = `
<div class="grid grid-cols-3 gap-3 text-xs">
<div class="text-center">
<p class="text-gray-400 mb-0.5">매도</p>
<p class="font-semibold text-blue-500">${(pg.sellVolume||0).toLocaleString('ko-KR')}</p>
<p class="text-gray-500">${formatMoney(pg.sellAmount)}원</p>
</div>
<div class="text-center border-x border-gray-100">
<p class="text-gray-400 mb-0.5">순매수</p>
<p class="font-semibold ${netClass}">${netSign}${(pg.netBuyVolume||0).toLocaleString('ko-KR')}</p>
<p class="${netClass}">${netSign}${formatMoney(pg.netBuyAmount)}원</p>
</div>
<div class="text-center">
<p class="text-gray-400 mb-0.5">매수</p>
<p class="font-semibold text-red-500">${(pg.buyVolume||0).toLocaleString('ko-KR')}</p>
<p class="text-gray-500">${formatMoney(pg.buyAmount)}원</p>
</div>
</div>`;
}
// 금액을 억/만 단위로 포맷
function formatMoney(n) {
if (!n) return '0';
const abs = Math.abs(n);
if (abs >= 100000000) return (n / 100000000).toFixed(1) + '억';
if (abs >= 10000) return Math.round(n / 10000) + '만';
return n.toLocaleString('ko-KR');
}