// 자동매매 페이지 클라이언트 스크립트 let logLevelFilter = 'all'; // 'all' | 'action' let watchSource = { useScanner: true, selectedThemes: [] }; let localLogs = []; // 클라이언트 로그 버퍼 (오래된 것 → 최신 순) let tradeLogWS = null; document.addEventListener('DOMContentLoaded', () => { loadStatus(); loadPositions(); loadLogs(); // 기존 로그 초기 로드 (HTTP) loadRules(); loadThemeList(); loadWatchSource(); connectTradeLogWS(); // WS 실시간 연결 // 자동스크롤 체크박스: ON 전환 시 즉시 최신으로 이동 const chk = document.getElementById('autoScrollChk'); if (chk) { chk.addEventListener('change', () => { if (chk.checked) scrollLogsToBottom(); }); } // 상태: 3초 주기, 포지션: 5초 주기 (로그는 WS로 대체) setInterval(loadStatus, 3000); setInterval(loadPositions, 5000); }); // --- 엔진 상태 --- async function loadStatus() { try { const res = await fetch('/api/autotrade/status'); const data = await res.json(); updateStatusUI(data); } catch (e) { console.error('상태 조회 실패:', e); } } function updateStatusUI(data) { const dot = document.getElementById('statusDot'); const text = document.getElementById('statusText'); if (data.running) { dot.className = 'w-3 h-3 rounded-full bg-green-500'; text.textContent = '● 실행중'; text.className = 'text-sm font-medium text-green-600'; } else { dot.className = 'w-3 h-3 rounded-full bg-gray-300'; text.textContent = '○ 중지'; text.className = 'text-sm font-medium text-gray-500'; } document.getElementById('statActivePos').textContent = data.activePositions ?? 0; document.getElementById('statTradeCount').textContent = data.tradeCount ?? 0; const pl = data.totalPL ?? 0; const plEl = document.getElementById('statTotalPL'); plEl.textContent = formatMoney(pl) + '원'; plEl.className = 'text-2xl font-bold ' + (pl > 0 ? 'text-red-500' : pl < 0 ? 'text-blue-500' : 'text-gray-800'); } // --- 엔진 제어 --- async function startEngine() { if (!confirm('자동매매 엔진을 시작하시겠습니까?')) return; try { await fetch('/api/autotrade/start', { method: 'POST' }); await loadStatus(); } catch (e) { alert('엔진 시작 실패: ' + e.message); } } async function stopEngine() { if (!confirm('자동매매 엔진을 중지하시겠습니까?')) return; try { await fetch('/api/autotrade/stop', { method: 'POST' }); await loadStatus(); } catch (e) { alert('엔진 중지 실패: ' + e.message); } } async function emergencyStop() { if (!confirm('⚠ 긴급청산: 모든 포지션을 즉시 시장가 매도합니다.\n계속하시겠습니까?')) return; try { await fetch('/api/autotrade/emergency', { method: 'POST' }); await loadStatus(); await loadPositions(); } catch (e) { alert('긴급청산 실패: ' + e.message); } } // --- 규칙 --- async function loadRules() { try { const res = await fetch('/api/autotrade/rules'); const rules = await res.json(); renderRules(rules); } catch (e) { console.error('규칙 조회 실패:', e); } } function renderRules(rules) { const el = document.getElementById('rulesList'); if (!rules || rules.length === 0) { el.innerHTML = '
규칙이 없습니다.
'; return; } el.innerHTML = rules.map(r => `진입: RiseScore≥${r.minRiseScore} / 체결강도≥${r.minCntrStr}${r.requireBullish ? ' / AI호재' : ''}
청산: 손절${r.stopLossPct}% / 익절+${r.takeProfitPct}%${r.maxHoldMinutes > 0 ? ' / ' + r.maxHoldMinutes + '분' : ''}${r.exitBeforeClose ? ' / 장마감전' : ''}
주문금액: ${formatMoney(r.orderAmount)}원 / 최대${r.maxPositions}종목
보유 포지션 없음
'; return; } el.innerHTML = `| 종목 | 매수가 | 수량 | 손절가 | 상태 |
|---|---|---|---|---|
|
${escHtml(p.name)}
${p.code}
|
${formatMoney(p.buyPrice)} | ${p.qty} | ${formatMoney(p.stopLoss)} | ${statusTxt} |