Files
stockautomtion/app/templates/index.html
2026-07-16 23:55:16 +09:00

216 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock Automation Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; color: #e1e4e8; }
.header { background: #161b22; padding: 16px 24px; border-bottom: 1px solid #30363d; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 20px; color: #58a6ff; }
.header .status { font-size: 13px; color: #8b949e; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
.card h3 { font-size: 13px; color: #8b949e; margin-bottom: 8px; text-transform: uppercase; }
.card .value { font-size: 28px; font-weight: 700; }
.card .value.profit { color: #f85149; }
.card .value.positive { color: #3fb950; }
.card .value.negative { color: #f85149; }
.section { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin-bottom: 20px; }
.section h2 { font-size: 16px; margin-bottom: 12px; color: #c9d1d9; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { text-align: left; padding: 8px 12px; color: #8b949e; border-bottom: 1px solid #30363d; }
td { padding: 8px 12px; border-bottom: 1px solid #21262d; }
tr:hover { background: #1c2128; }
.badge { padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; }
.badge.buy { background: #1a3a2a; color: #3fb950; }
.badge.sell { background: #3a1a1a; color: #f85149; }
.badge.pending { background: #3a3a1a; color: #d29922; }
.badge.filled { background: #1a2a3a; color: #58a6ff; }
.actions { display: flex; gap: 8px; margin-bottom: 20px; }
.btn { padding: 8px 16px; border-radius: 6px; border: 1px solid #30363d; background: #21262d; color: #c9d1d9; cursor: pointer; font-size: 13px; }
.btn:hover { background: #30363d; }
.btn.primary { background: #238636; border-color: #2ea043; color: #fff; }
.btn.primary:hover { background: #2ea043; }
.form-row { display: flex; gap: 12px; align-items: end; margin-bottom: 12px; flex-wrap: wrap; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group label { font-size: 12px; color: #8b949e; }
.form-group input, .form-group select { padding: 6px 10px; border-radius: 4px; border: 1px solid #30363d; background: #0d1117; color: #c9d1d9; font-size: 13px; }
#ws-status { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
#ws-status.connected { background: #3fb950; }
#ws-status.disconnected { background: #f85149; }
</style>
</head>
<body>
<div class="header">
<h1>Stock Automation</h1>
<div class="status">
<span id="ws-status" class="disconnected"></span>
<span id="status-text">연결 중...</span>
</div>
</div>
<div class="container">
<div class="grid">
<div class="card">
<h3>총 투자금</h3>
<div class="value" id="total-invested">-</div>
</div>
<div class="card">
<h3>평가금액</h3>
<div class="value" id="total-evaluated">-</div>
</div>
<div class="card">
<h3>총 수익</h3>
<div class="value" id="total-profit">-</div>
</div>
<div class="card">
<h3>수익률</h3>
<div class="value" id="profit-rate">-</div>
</div>
</div>
<div class="section">
<h2>보유 종목</h2>
<table>
<thead>
<tr>
<th>종목코드</th><th>종목명</th><th>수량</th>
<th>평균단가</th><th>현재가</th><th>수익</th><th>수익률</th>
</tr>
</thead>
<tbody id="holdings-table"></tbody>
</table>
</div>
<div class="section">
<h2>최근 매매 내역</h2>
<table>
<thead>
<tr>
<th>시간</th><th>종목코드</th><th>구분</th>
<th>수량</th><th>가격</th><th>상태</th>
</tr>
</thead>
<tbody id="trades-table"></tbody>
</table>
</div>
<div class="section">
<h2>매매 주문</h2>
<div class="form-row">
<div class="form-group">
<label>종목코드</label>
<input id="order-code" type="text" placeholder="005930" />
</div>
<div class="form-group">
<label>구분</label>
<select id="order-side">
<option value="buy">매수</option>
<option value="sell">매도</option>
</select>
</div>
<div class="form-group">
<label>수량</label>
<input id="order-qty" type="number" value="1" min="1" />
</div>
<div class="form-group">
<label>가격 (0=시장가)</label>
<input id="order-price" type="number" value="0" />
</div>
<button class="btn primary" onclick="placeOrder()">주문</button>
</div>
</div>
</div>
<script>
const API = '';
let ws;
function formatPrice(n) { return n ? n.toLocaleString('ko-KR') + '원' : '-'; }
function formatRate(n) { return n >= 0 ? '+' + n.toFixed(2) + '%' : n.toFixed(2) + '%'; }
async function loadDashboard() {
try {
const res = await fetch(API + '/api/dashboard/');
const data = await res.json();
const s = data.summary;
document.getElementById('total-invested').textContent = formatPrice(s.total_invested);
document.getElementById('total-evaluated').textContent = formatPrice(s.total_evaluated);
const profitEl = document.getElementById('total-profit');
profitEl.textContent = formatPrice(s.total_profit);
profitEl.className = 'value ' + (s.total_profit >= 0 ? 'positive' : 'negative');
const rateEl = document.getElementById('profit-rate');
rateEl.textContent = formatRate(s.profit_rate);
rateEl.className = 'value ' + (s.profit_rate >= 0 ? 'positive' : 'negative');
const htb = document.getElementById('holdings-table');
htb.innerHTML = data.holdings.map(h => `
<tr>
<td>${h.stock_code}</td><td>${h.stock_name}</td><td>${h.qty}</td>
<td>${formatPrice(h.avg_price)}</td><td>${formatPrice(h.current_price)}</td>
<td class="${h.profit >= 0 ? 'positive' : 'negative'}">${formatPrice(h.profit)}</td>
<td class="${h.profit_rate >= 0 ? 'positive' : 'negative'}">${formatRate(h.profit_rate)}</td>
</tr>
`).join('');
const ttb = document.getElementById('trades-table');
ttb.innerHTML = data.recent_trades.map(t => `
<tr>
<td>${t.created_at ? new Date(t.created_at).toLocaleString('ko-KR') : '-'}</td>
<td>${t.stock_code}</td>
<td><span class="badge ${t.side}">${t.side === 'buy' ? '매수' : '매도'}</span></td>
<td>${t.qty}</td><td>${formatPrice(t.price)}</td>
<td><span class="badge ${t.status}">${t.status}</span></td>
</tr>
`).join('');
} catch (e) { console.error('대시보드 로드 실패:', e); }
}
async function placeOrder() {
const body = {
stock_code: document.getElementById('order-code').value,
side: document.getElementById('order-side').value,
qty: parseInt(document.getElementById('order-qty').value),
price: parseInt(document.getElementById('order-price').value),
};
try {
const res = await fetch(API + '/api/trading/order', {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body),
});
const data = await res.json();
alert(data.rt_cd === '0' ? '주문 성공: ' + data.order_no : '주문 실패: ' + data.msg);
loadDashboard();
} catch (e) { alert('주문 오류: ' + e.message); }
}
function connectWS() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${proto}://${location.host}/ws/realtime`);
ws.onopen = () => {
document.getElementById('ws-status').className = 'connected';
document.getElementById('status-text').textContent = '실시간 연결됨';
};
ws.onclose = () => {
document.getElementById('ws-status').className = 'disconnected';
document.getElementById('status-text').textContent = '연결 끊김 - 재연결 중...';
setTimeout(connectWS, 3000);
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'price') loadDashboard();
};
}
loadDashboard();
setInterval(loadDashboard, 10000);
connectWS();
</script>
</body>
</html>