initial commit
This commit is contained in:
377
frontend/trade.html
Normal file
377
frontend/trade.html
Normal file
@@ -0,0 +1,377 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>매매 - KisStock AI</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.tab-menu {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.tab-btn.active {
|
||||
color: var(--accent-color);
|
||||
border-bottom-color: var(--accent-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
color: var(--text-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.btn-buy { background: #ef4444; color: white; }
|
||||
.btn-sell { background: #3b82f6; color: white; }
|
||||
.btn-cancel { background: #f59e0b; color: white; padding: 0.4rem; font-size: 0.8rem; width: auto;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<div class="logo">KisStock AI</div>
|
||||
<div class="nav-links">
|
||||
<a href="/">대시보드</a>
|
||||
<a href="/trade" class="active">매매</a>
|
||||
<a href="/settings">설정</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="card">
|
||||
<div class="tab-menu">
|
||||
<button class="tab-btn active" onclick="showTab('order')">주문하기</button>
|
||||
<button class="tab-btn" onclick="showTab('cancel')">정정/취소</button>
|
||||
<button class="tab-btn" onclick="showTab('history')">체결내역</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: Order -->
|
||||
<div id="tab-order" class="tab-content active">
|
||||
<h2>⚡ 간편 매매</h2>
|
||||
<div class="grid" style="grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||
<div>
|
||||
<label>종목코드</label>
|
||||
<input type="text" id="code" placeholder="예: 005930">
|
||||
<button class="btn" style="background:var(--card-bg); border:1px solid #fff;" onclick="searchStock()">현재가 조회</button>
|
||||
|
||||
<div id="price-display" style="margin-top:1rem; font-size:1.2rem;"></div>
|
||||
|
||||
<hr style="margin: 1rem 0; border-color: rgba(255,255,255,0.1);">
|
||||
|
||||
<h3>🤖 자동매매 감시</h3>
|
||||
<div class="form-group">
|
||||
<label>목표가 (익절)</label>
|
||||
<input type="number" id="target-price" placeholder="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>손절가 (로스컷)</label>
|
||||
<input type="number" id="loss-price" placeholder="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="auto-active" style="width:auto;"> 감시 활성화
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn" style="background:var(--accent-color); color:white;" onclick="saveAutoSettings()">감시 설정 저장</button>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<label>수량</label>
|
||||
<input type="number" id="qty" value="1">
|
||||
|
||||
<label>가격 (0 = 시장가)</label>
|
||||
<input type="number" id="price" value="0">
|
||||
|
||||
<div class="grid" style="grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<button class="btn btn-buy" onclick="placeOrder('buy')">매수</button>
|
||||
<button class="btn btn-sell" onclick="placeOrder('sell')">매도</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Cancel -->
|
||||
<div id="tab-cancel" class="tab-content">
|
||||
<h2>♻ 미체결/취소 가능 주문</h2>
|
||||
<button class="btn" onclick="fetchCancelable()" style="background:#475569; margin-bottom:1rem;">목록 갱신</button>
|
||||
<div style="overflow-x:auto;">
|
||||
<table id="cancel-table" style="width:100%; text-align:left;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>주문번호</th>
|
||||
<th>종목</th>
|
||||
<th>구분</th>
|
||||
<th>수량/잔량</th>
|
||||
<th>가격</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: History -->
|
||||
<div id="tab-history" class="tab-content">
|
||||
<h2>📜 금일 체결 내역</h2>
|
||||
<button class="btn" onclick="fetchHistory()" style="background:#475569; margin-bottom:1rem;">내역 조회</button>
|
||||
<div style="overflow-x:auto;">
|
||||
<table id="history-table" style="width:100%; text-align:left;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시간</th>
|
||||
<th>주문번호</th>
|
||||
<th>종목</th>
|
||||
<th>구분</th>
|
||||
<th>체결수량</th>
|
||||
<th>체결단가</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Init: Parse URL params
|
||||
window.onload = function() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get('code');
|
||||
const market = params.get('market');
|
||||
const type = params.get('type');
|
||||
|
||||
if(code) {
|
||||
document.getElementById('code').value = code;
|
||||
searchStock(code); // Auto-load price
|
||||
}
|
||||
// Optional: Handle 'market' if we had a market selector in trade page (we might need one for overseas order API)
|
||||
// Optional: Handle 'type' (buy/sell) - could switch button focus or auto-prepare?
|
||||
// For now, user clicks buttons manually, but we could highlight.
|
||||
|
||||
if(type) {
|
||||
// visually highlight button?
|
||||
}
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
function showTab(name) {
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById('tab-'+name).classList.add('active');
|
||||
// Find button logic if needed, but simple onclick works for now
|
||||
// We need to match the button that called this.
|
||||
// Better to iterate buttons and regex or id match?
|
||||
// Let's just reset buttons manually or pass 'this'
|
||||
const buttons = document.querySelectorAll('.tab-btn');
|
||||
buttons.forEach(btn => {
|
||||
if(btn.innerText.includes(name === 'order' ? '주문' : (name === 'cancel' ? '취소' : '체결'))) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
if(name === 'cancel') fetchCancelable();
|
||||
if(name === 'history') fetchHistory();
|
||||
}
|
||||
|
||||
async function searchStock(paramCode) {
|
||||
const code = paramCode || document.getElementById('code').value;
|
||||
if(!code) return;
|
||||
try {
|
||||
const res = await fetch(`/api/price/${code}`);
|
||||
const data = await res.json();
|
||||
document.getElementById('price-display').innerText =
|
||||
`${data.bstp_kor_isnm || ''} 현재가: ${parseInt(data.stck_prpr || 0).toLocaleString()}원`;
|
||||
|
||||
// Also load auto settings
|
||||
loadAutoSettings(code);
|
||||
} catch(e) { /* Ignore or alert */ }
|
||||
}
|
||||
|
||||
async function placeOrder(type) {
|
||||
const code = document.getElementById('code').value;
|
||||
const qty = document.getElementById('qty').value;
|
||||
const price = document.getElementById('price').value;
|
||||
|
||||
// Need Market info?
|
||||
// 1. Get from URL if present
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let market = params.get('market') || "DOMESTIC";
|
||||
// Note: If user typed code manually, we default to DOMESTIC logic unless we fetch info.
|
||||
|
||||
if(!code || !qty) return alert("입력값을 확인하세요");
|
||||
|
||||
if(!confirm(`${type === 'buy' ? '매수' : '매도'} 주문을 전송하시겠습니까?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/order', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ code, type, qty: parseInt(qty), price: parseInt(price), market })
|
||||
});
|
||||
const data = await res.json();
|
||||
if(data.rt_cd === '0') {
|
||||
alert("주문 전송 성공: " + data.msg1);
|
||||
} else {
|
||||
alert("주문 실패: " + data.msg1);
|
||||
}
|
||||
} catch(e) { alert("Error: " + e.message); }
|
||||
}
|
||||
|
||||
async function fetchCancelable() {
|
||||
try {
|
||||
const res = await fetch('/api/orders/cancelable');
|
||||
const data = await res.json();
|
||||
const tbody = document.querySelector('#cancel-table tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if(!data.output) {
|
||||
tbody.innerHTML = '<tr><td colspan="6">데이터가 없거나 조회 실패</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.output.forEach(item => {
|
||||
// Only show if psbl_qty > 0 (Cancelable)
|
||||
if (parseInt(item.psbl_qty) <= 0) return;
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${item.odno}</td>
|
||||
<td>${item.prdt_name} (${item.pdno})</td>
|
||||
<td>${item.sll_buy_dvsn_cd_name}</td>
|
||||
<td>${item.ord_qty} / <span style="color:#f59e0b">${item.psbl_qty}</span></td>
|
||||
<td>${parseInt(item.ord_unpr).toLocaleString()}</td>
|
||||
<td>
|
||||
<button class="btn btn-cancel" onclick="cancelOrder('${item.krx_fwdg_ord_orgno}', '${item.odno}', ${item.psbl_qty}, '${item.pdno}')">취소</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
if(tbody.children.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6">취소 가능한 주문이 없습니다.</td></tr>';
|
||||
}
|
||||
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function cancelOrder(orgNo, orderNo, qty, code) {
|
||||
if(!confirm(`주문번호 ${orderNo} (${qty}주)를 취소하시겠습니까?`)) return;
|
||||
try {
|
||||
const res = await fetch('/api/order/cancel', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
org_no: orgNo,
|
||||
order_no: orderNo,
|
||||
qty: parseInt(qty),
|
||||
is_buy: true,
|
||||
price: 0
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
alert(data.msg1);
|
||||
fetchCancelable();
|
||||
} catch(e) { alert(e.message); }
|
||||
}
|
||||
|
||||
async function fetchHistory() {
|
||||
try {
|
||||
const res = await fetch('/api/orders/daily');
|
||||
const data = await res.json();
|
||||
const tbody = document.querySelector('#history-table tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if(!data.output1) {
|
||||
tbody.innerHTML = '<tr><td colspan="6">데이터가 없거나 조회 실패</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.output1.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${item.ord_dt} ${item.ord_tmd}</td>
|
||||
<td>${item.odno}</td>
|
||||
<td>${item.prdt_name}</td>
|
||||
<td>${item.sll_buy_dvsn_cd_name}</td>
|
||||
<td>${item.tot_ccld_qty} / ${item.ord_qty}</td>
|
||||
<td>${parseInt(item.avg_prvs).toLocaleString()}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function loadAutoSettings(code) {
|
||||
try {
|
||||
const res = await fetch('/api/trade_settings');
|
||||
const data = await res.json();
|
||||
const setting = data.find(s => s.code === code);
|
||||
|
||||
if (setting) {
|
||||
document.getElementById('target-price').value = setting.target_price || '';
|
||||
document.getElementById('loss-price').value = setting.stop_loss_price || '';
|
||||
document.getElementById('auto-active').checked = setting.is_active;
|
||||
} else {
|
||||
document.getElementById('target-price').value = '';
|
||||
document.getElementById('loss-price').value = '';
|
||||
document.getElementById('auto-active').checked = false;
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function saveAutoSettings() {
|
||||
const code = document.getElementById('code').value;
|
||||
if (!code) return alert('종목 코드 입력 필요');
|
||||
|
||||
const target = document.getElementById('target-price').value;
|
||||
const loss = document.getElementById('loss-price').value;
|
||||
const active = document.getElementById('auto-active').checked;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/trade_settings', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
code: code,
|
||||
target_price: target ? parseFloat(target) : null,
|
||||
stop_loss_price: loss ? parseFloat(loss) : null,
|
||||
is_active: active
|
||||
})
|
||||
});
|
||||
if(res.ok) alert('감시 설정 저장 완료');
|
||||
} catch(e) { alert("저장 실패"); }
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user