프론트엔드 추가 및 자동매매 로직 개선:
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 1m42s
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 1m42s
- Svelte 기반 프론트엔드 프로젝트 초기 설정 추가 (`vite`, `tailwindcss` 등 포함). - "자동매매" 주요 상태 및 규칙 관리 페이지 구현. - 1차/2차 손절 및 익절 조건 평가 로직 추가(`calcStopTargets`, `evalExitReason` 등). - 포지션 상세 로그 및 WebSocket 기반 실시간 로그 스트림 추가. - API 서비스 및 Frontend 간 Proxy 설정(Vite 서버). - 세션 체크를 위한 `CheckSession` 핸들러 추가.
This commit is contained in:
@@ -129,7 +129,7 @@ function renderRules(rules) {
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 space-y-0.5">
|
||||
<p>진입: RiseScore≥${r.minRiseScore} / 체결강도≥${r.minCntrStr}${r.requireBullish ? ' / AI호재' : ''}</p>
|
||||
<p>청산: 손절${r.stopLossPct}% / 익절+${r.takeProfitPct}%${r.maxHoldMinutes > 0 ? ' / ' + r.maxHoldMinutes + '분' : ''}${r.exitBeforeClose ? ' / 장마감전' : ''}</p>
|
||||
<p>청산: ${r.stopLoss1Count > 0 ? `1차손절${r.stopLoss1Pct}%[${r.stopLoss1Count}회] / 2차손절${r.stopLossPct}%` : `손절${r.stopLossPct}%`} / 익절+${r.takeProfitPct}%${r.maxHoldMinutes > 0 ? ' / ' + r.maxHoldMinutes + '분' : ''}${r.exitBeforeClose ? ' / 장마감전' : ''}</p>
|
||||
<p>주문금액: ${formatMoney(r.orderAmount)}원 / 최대${r.maxPositions}종목</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -173,7 +173,9 @@ function showAddRuleModal() {
|
||||
document.getElementById('fRequireBullish').checked = false;
|
||||
document.getElementById('fOrderAmount').value = 500000;
|
||||
document.getElementById('fMaxPositions').value = 3;
|
||||
document.getElementById('fStopLoss').value = -3;
|
||||
document.getElementById('fStopLoss1').value = -2;
|
||||
document.getElementById('fStopLoss1Count').value = 3;
|
||||
document.getElementById('fStopLoss').value = -4;
|
||||
document.getElementById('fTakeProfit').value = 5;
|
||||
document.getElementById('fMaxHold').value = 60;
|
||||
document.getElementById('fExitBeforeClose').checked = true;
|
||||
@@ -190,6 +192,8 @@ function showEditRuleModal(r) {
|
||||
document.getElementById('fRequireBullish').checked = r.requireBullish;
|
||||
document.getElementById('fOrderAmount').value = r.orderAmount;
|
||||
document.getElementById('fMaxPositions').value = r.maxPositions;
|
||||
document.getElementById('fStopLoss1').value = r.stopLoss1Pct ?? -2;
|
||||
document.getElementById('fStopLoss1Count').value = r.stopLoss1Count ?? 3;
|
||||
document.getElementById('fStopLoss').value = r.stopLossPct;
|
||||
document.getElementById('fTakeProfit').value = r.takeProfitPct;
|
||||
document.getElementById('fMaxHold').value = r.maxHoldMinutes;
|
||||
@@ -211,6 +215,8 @@ async function submitRule() {
|
||||
requireBullish: document.getElementById('fRequireBullish').checked,
|
||||
orderAmount: parseInt(document.getElementById('fOrderAmount').value),
|
||||
maxPositions: parseInt(document.getElementById('fMaxPositions').value),
|
||||
stopLoss1Pct: parseFloat(document.getElementById('fStopLoss1').value),
|
||||
stopLoss1Count: parseInt(document.getElementById('fStopLoss1Count').value) || 0,
|
||||
stopLossPct: parseFloat(document.getElementById('fStopLoss').value),
|
||||
takeProfitPct: parseFloat(document.getElementById('fTakeProfit').value),
|
||||
maxHoldMinutes: parseInt(document.getElementById('fMaxHold').value),
|
||||
@@ -268,7 +274,8 @@ function renderPositions(positions) {
|
||||
<th class="pb-2 font-medium">종목</th>
|
||||
<th class="pb-2 font-medium text-right">매수가</th>
|
||||
<th class="pb-2 font-medium text-right">수량</th>
|
||||
<th class="pb-2 font-medium text-right">손절가</th>
|
||||
<th class="pb-2 font-medium text-right">1차손절</th>
|
||||
<th class="pb-2 font-medium text-right">2차손절</th>
|
||||
<th class="pb-2 font-medium text-center">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -284,7 +291,8 @@ function renderPositions(positions) {
|
||||
</td>
|
||||
<td class="py-2 text-right text-gray-700">${formatMoney(p.buyPrice)}</td>
|
||||
<td class="py-2 text-right text-gray-700">${p.qty}</td>
|
||||
<td class="py-2 text-right text-blue-500">${formatMoney(p.stopLoss)}</td>
|
||||
<td class="py-2 text-right text-orange-500">${p.stopLoss1 > 0 ? formatMoney(p.stopLoss1) + `<span class="text-xs text-gray-400 ml-0.5">[${p.stopLoss1Touches||0}회]</span>` : '-'}</td>
|
||||
<td class="py-2 text-right text-red-500">${formatMoney(p.stopLoss)}</td>
|
||||
<td class="py-2 text-center font-medium ${statusCls}">${statusTxt}</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
@@ -351,27 +351,39 @@
|
||||
// ─────────────────────────────────────────────
|
||||
function renderSidebar() {
|
||||
const list = loadList();
|
||||
Array.from(sidebarEl.children).forEach(el => {
|
||||
if (el.id !== 'watchlistEmpty') el.remove();
|
||||
});
|
||||
list.forEach(s => sidebarEl.appendChild(makeSidebarItem(s.code, s.name)));
|
||||
// 테이블 생성
|
||||
let tableEl = sidebarEl.querySelector('table');
|
||||
if (tableEl) tableEl.remove();
|
||||
if (list.length > 0) {
|
||||
tableEl = document.createElement('table');
|
||||
tableEl.className = 'w-full text-xs';
|
||||
const tbody = document.createElement('tbody');
|
||||
tbody.className = 'divide-y divide-gray-50';
|
||||
list.forEach(s => tbody.appendChild(makeSidebarItem(s.code, s.name)));
|
||||
tableEl.appendChild(tbody);
|
||||
sidebarEl.insertBefore(tableEl, emptyEl);
|
||||
}
|
||||
updateEmptyStates();
|
||||
}
|
||||
|
||||
function makeSidebarItem(code, name) {
|
||||
const li = document.createElement('li');
|
||||
li.id = `si-${code}`;
|
||||
li.className = 'flex items-center justify-between px-3 py-2.5 hover:bg-gray-50 group';
|
||||
li.innerHTML = `
|
||||
<a href="/stock/${code}" class="flex-1 min-w-0">
|
||||
<p class="text-xs font-medium text-gray-800 truncate">${name}</p>
|
||||
<p class="text-xs text-gray-400 font-mono">${code}</p>
|
||||
</a>
|
||||
<button onclick="removeStock('${code}')" title="삭제"
|
||||
class="ml-2 text-gray-300 hover:text-red-400 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-base leading-none">
|
||||
×
|
||||
</button>`;
|
||||
return li;
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = `si-${code}`;
|
||||
tr.className = 'hover:bg-gray-50 group cursor-pointer';
|
||||
tr.onclick = () => { window.location.href = `/stock/${code}`; };
|
||||
tr.innerHTML = `
|
||||
<td class="px-3 py-2">
|
||||
<p class="font-medium text-gray-800 truncate">${name}</p>
|
||||
<p class="text-gray-400 font-mono">${code}</p>
|
||||
</td>
|
||||
<td id="si-price-${code}" class="px-2 py-2 text-right font-mono text-gray-400">-</td>
|
||||
<td id="si-rate-${code}" class="px-2 py-2 text-right text-gray-400">-</td>
|
||||
<td id="si-cntr-${code}" class="px-2 py-2 text-right text-gray-400">-</td>
|
||||
<td class="pr-2 py-2 text-center">
|
||||
<button onclick="event.stopPropagation(); removeStock('${code}')" title="삭제"
|
||||
class="text-gray-300 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-base leading-none">×</button>
|
||||
</td>`;
|
||||
return tr;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
@@ -437,17 +449,21 @@
|
||||
const rateEl = document.getElementById(`wc-rate-${code}`);
|
||||
const volEl = document.getElementById(`wc-vol-${code}`);
|
||||
const cntrEl = document.getElementById(`wc-cntr-${code}`);
|
||||
if (!priceEl) return;
|
||||
|
||||
const rate = data.changeRate ?? 0;
|
||||
const colorCls = rateClass(rate);
|
||||
const bgCls = rateBadgeClass(rate);
|
||||
const sign = rate > 0 ? '+' : '';
|
||||
|
||||
priceEl.textContent = fmtNum(data.currentPrice) + '원';
|
||||
priceEl.className = `text-xl font-bold mb-2 ${colorCls}`;
|
||||
rateEl.textContent = sign + rate.toFixed(2) + '%';
|
||||
rateEl.className = `text-xs px-2 py-0.5 rounded-full font-semibold ${bgCls}`;
|
||||
// 패널 카드 업데이트
|
||||
if (priceEl) {
|
||||
priceEl.textContent = fmtNum(data.currentPrice) + '원';
|
||||
priceEl.className = `text-xl font-bold mb-2 ${colorCls}`;
|
||||
}
|
||||
if (rateEl) {
|
||||
rateEl.textContent = sign + rate.toFixed(2) + '%';
|
||||
rateEl.className = `text-xs px-2 py-0.5 rounded-full font-semibold ${bgCls}`;
|
||||
}
|
||||
if (volEl) volEl.textContent = fmtNum(data.volume);
|
||||
if (cntrEl && data.cntrStr !== undefined) {
|
||||
const cs = data.cntrStr;
|
||||
@@ -455,6 +471,24 @@
|
||||
cntrEl.className = `font-bold ${cs >= 100 ? 'text-orange-500' : 'text-blue-400'}`;
|
||||
}
|
||||
|
||||
// 사이드바 행 업데이트
|
||||
const siPrice = document.getElementById(`si-price-${code}`);
|
||||
const siRate = document.getElementById(`si-rate-${code}`);
|
||||
const siCntr = document.getElementById(`si-cntr-${code}`);
|
||||
if (siPrice) {
|
||||
siPrice.textContent = fmtNum(data.currentPrice);
|
||||
siPrice.className = `px-2 py-2 text-right font-mono ${colorCls}`;
|
||||
}
|
||||
if (siRate) {
|
||||
siRate.textContent = fmtRate(rate);
|
||||
siRate.className = `px-2 py-2 text-right ${colorCls}`;
|
||||
}
|
||||
if (siCntr && data.cntrStr !== undefined) {
|
||||
const cs = data.cntrStr;
|
||||
siCntr.textContent = fmtCntr(cs);
|
||||
siCntr.className = `px-2 py-2 text-right ${cs >= 100 ? 'text-orange-500 font-bold' : cs > 0 ? 'text-blue-400' : 'text-gray-400'}`;
|
||||
}
|
||||
|
||||
// 체결강도 히스토리 기록 + 미니 차트 갱신
|
||||
if (data.cntrStr != null && data.cntrStr !== 0) {
|
||||
recordCntr(code, data.cntrStr);
|
||||
@@ -536,7 +570,7 @@
|
||||
// ─────────────────────────────────────────────
|
||||
function updateEmptyStates() {
|
||||
const hasItems = cachedList.length > 0;
|
||||
emptyEl.classList.toggle('hidden', hasItems);
|
||||
if (emptyEl) emptyEl.classList.toggle('hidden', hasItems);
|
||||
panelEmpty?.classList.toggle('hidden', hasItems);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user