initial commit

This commit is contained in:
2026-02-04 00:16:34 +09:00
commit ae11528dd9
867 changed files with 209640 additions and 0 deletions

377
frontend/trade.html Normal file
View 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>