378 lines
16 KiB
HTML
378 lines
16 KiB
HTML
<!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>
|