Files
trading/dashboard-api/cards.html
T
kyu 6d3b0bacc0 Initial commit: Korean stock value-investing AI pipeline
- 19개 마이크로서비스 (news-collector, score-engine, ta-engine, dart-collector,
  aux-signal, us-market, graph-engine, telegram-bot, dashboard-api, kis-api 등)
- 가치투자 스코어링 + 10공식 앙상블 보팅 (매직포뮬러/F-Score/Altman/PEG/
  모멘텀/Beneish/GP-A/G-Score/Amihud/BAB)
- 뉴스 수집→형태소→임베딩→중복제거→AI분석 파이프라인
- 기술적분석 + GAT 그래프신경망 + 미증시 동조 시그널

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:33:56 +09:00

293 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Trading AI · 종목 카드</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans KR", sans-serif;
background: #0d1117; color: #e6edf3; line-height: 1.5;
padding: 16px; max-width: 1400px; margin: 0 auto;
}
header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #30363d;
}
h1 { font-size: 20px; font-weight: 600; }
.regime {
padding: 6px 14px; border-radius: 20px; font-size: 13px; font-weight: 500;
}
.regime.강세 { background: #1f3d1f; color: #4ade80; }
.regime.약세 { background: #3d1f1f; color: #f87171; }
.regime.중립 { background: #2d2d3a; color: #a5a5b8; }
.summary-bar {
display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap;
}
.summary-pill {
background: #161b22; border: 1px solid #30363d; padding: 8px 14px;
border-radius: 6px; font-size: 13px;
}
.summary-pill .v { color: #58a6ff; font-weight: 600; margin-left: 6px; }
.tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.tab {
padding: 8px 18px; background: #161b22; border: 1px solid #30363d;
border-radius: 6px; cursor: pointer; font-size: 14px;
}
.tab.active { background: #1f6feb; border-color: #1f6feb; color: white; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 14px; }
.card {
background: #161b22; border: 1px solid #30363d; border-radius: 10px;
padding: 16px; transition: transform .15s, border-color .15s;
}
.card:hover { transform: translateY(-2px); border-color: #58a6ff; }
.card-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
.name { font-size: 17px; font-weight: 600; color: #e6edf3; }
.code { font-size: 12px; color: #7d8590; margin-top: 2px; }
.score-badge {
text-align: right;
}
.score {
font-size: 28px; font-weight: 700; line-height: 1;
}
.score.강력매수 { color: #4ade80; }
.score.매수관심 { color: #58a6ff; }
.score.관망 { color: #a5a5b8; }
.score.매도관심, .score.강력매도 { color: #f87171; }
.rec {
font-size: 11px; padding: 2px 8px; border-radius: 4px;
margin-top: 4px; display: inline-block; font-weight: 500;
}
.rec.강력매수 { background: #1f3d1f; color: #4ade80; }
.rec.매수관심 { background: #1f2d3d; color: #58a6ff; }
.rec.매도관심, .rec.강력매도 { background: #3d1f1f; color: #f87171; }
.price-line {
display: flex; align-items: baseline; gap: 8px; margin-bottom: 14px;
}
.price { font-size: 20px; font-weight: 600; }
.change { font-size: 14px; font-weight: 500; }
.change.up { color: #4ade80; }
.change.down { color: #f87171; }
.change.flat { color: #7d8590; }
.sector-tag {
font-size: 11px; padding: 2px 8px; background: #21262d;
border-radius: 4px; color: #a5a5b8;
}
.target-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;
background: #0d1117; padding: 10px; border-radius: 6px; margin-bottom: 12px;
}
.target-cell { font-size: 12px; }
.target-cell .label { color: #7d8590; margin-bottom: 2px; }
.target-cell .v { font-weight: 600; }
.target-cell .pct { color: #7d8590; margin-left: 4px; font-size: 11px; }
.target-cell .v.t { color: #4ade80; }
.target-cell .v.s { color: #f87171; }
.metrics {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
font-size: 12px; padding: 10px 0; border-top: 1px solid #21262d; border-bottom: 1px solid #21262d;
margin-bottom: 10px;
}
.m-cell .label { color: #7d8590; font-size: 10px; margin-bottom: 2px; }
.m-cell .v { font-weight: 500; }
.m-cell .v.good { color: #4ade80; }
.m-cell .v.bad { color: #f87171; }
.position-line {
display: flex; justify-content: space-between; padding: 8px 10px;
background: #1c2839; border-radius: 6px; margin-bottom: 10px; font-size: 12px;
}
.pos-label { color: #7d8590; }
.pos-val { color: #58a6ff; font-weight: 600; }
.reasons { font-size: 12px; color: #a5a5b8; line-height: 1.6; }
.reason-item {
padding: 4px 0; padding-left: 14px; position: relative;
}
.reason-item:before { content: "→"; position: absolute; left: 0; color: #58a6ff; }
.empty { text-align: center; color: #7d8590; padding: 60px 20px; font-size: 14px; }
.loader { text-align: center; padding: 40px; color: #7d8590; }
</style>
</head>
<body>
<header>
<h1>📊 Trading AI · 종목 카드</h1>
<div class="regime" id="regime">로딩…</div>
</header>
<div class="summary-bar" id="summary"></div>
<div class="tabs">
<div class="tab active" data-kind="recommendations">🟢 매수 추천</div>
<div class="tab" data-kind="avoid">🔴 회피 종목</div>
</div>
<div id="cards-container">
<div class="loader">불러오는 중…</div>
</div>
<script>
const API = location.origin;
function fmt(n) {
if (n === null || n === undefined) return "-";
if (typeof n !== "number") return String(n);
return n.toLocaleString("ko-KR");
}
function pctClass(v) { return v > 0 ? "up" : v < 0 ? "down" : "flat"; }
function pctSign(v) { return (v > 0 ? "+" : "") + v + "%"; }
function metricCls(v, good) { return v >= good ? "good" : v < 0 ? "bad" : ""; }
async function loadSummary() {
try {
const r = await fetch(`${API}/api/summary`);
const d = await r.json();
document.getElementById("regime").textContent =
`시장 ${d.market_regime} (${d.market_regime_adj > 0 ? "+" : ""}${d.market_regime_adj})`;
document.getElementById("regime").className = "regime " + (d.market_regime || "중립");
document.getElementById("summary").innerHTML = `
<div class="summary-pill">강력매수<span class="v">${d.strong_buy || 0}</span></div>
<div class="summary-pill">매수관심<span class="v">${d.interest_buy || 0}</span></div>
<div class="summary-pill">매도관심<span class="v">${d.interest_sell || 0}</span></div>
<div class="summary-pill">강력매도<span class="v">${d.strong_sell || 0}</span></div>
<div class="summary-pill">7일 호재 비율<span class="v">${d.sentiment_ratio || 0}%</span></div>
<div class="summary-pill">분석 종목<span class="v">${d.stocks_analyzed || 0}</span></div>
`;
} catch (e) { console.error(e); }
}
function renderCard(s) {
const recCls = s.recommendation || "관망";
const ch = s.change_pct || 0;
const mosCls = s.margin_of_safety > 25 ? "good" : s.margin_of_safety < -25 ? "bad" : "";
const eqCls = metricCls(s.earnings_quality || 0, 5);
const reasons = (s.reasons || []).map(r =>
`<div class="reason-item">${r}</div>`).join("");
return `
<div class="card">
<div class="card-head">
<div>
<div class="name">${s.stock_name}</div>
<div class="code">${s.stock_code} · <span class="sector-tag">${s.sector || "기타"}</span></div>
</div>
<div class="score-badge">
<div class="score ${recCls}">${s.score}</div>
<div class="rec ${recCls}">${s.recommendation}</div>
</div>
</div>
<div class="price-line">
<div class="price">${fmt(s.price)}원</div>
<div class="change ${pctClass(ch)}">${pctSign(ch)}</div>
${s.market_cap_eok ? `<div class="sector-tag">시총 ${fmt(s.market_cap_eok)}억</div>` : ""}
</div>
${s.t1 ? `
<div class="target-grid">
<div class="target-cell">
<div class="label">진입가</div>
<div class="v">${fmt(s.entry_price)}원</div>
</div>
<div class="target-cell">
<div class="label">손절</div>
<div class="v s">${fmt(s.stop_loss)}원</div>
</div>
<div class="target-cell">
<div class="label">T1 (50% 매도)</div>
<div class="v t">${fmt(s.t1)}<span class="pct">${pctSign(s.t1_pct)}</span></div>
</div>
<div class="target-cell">
<div class="label">Trailing (ATR×2)</div>
<div class="v">${fmt(s.trailing_stop)}원</div>
</div>
<div class="target-cell">
<div class="label">T2 (30%)</div>
<div class="v t">${fmt(s.t2)}<span class="pct">${pctSign(s.t2_pct)}</span></div>
</div>
<div class="target-cell">
<div class="label">T3 (20%)</div>
<div class="v t">${fmt(s.t3)}<span class="pct">${pctSign(s.t3_pct)}</span></div>
</div>
</div>` : `<div class="loader" style="padding:8px;font-size:11px;">목표가 데이터 없음 (다음 기술분석 cron에서 갱신)</div>`}
${s.position_size_pct ? `
<div class="position-line">
<span class="pos-label">추천 매수 비중</span>
<span class="pos-val">${s.position_size_pct}% · 변동성 ${s.volatility_60d || "-"}%</span>
</div>` : ""}
<div class="metrics">
<div class="m-cell">
<div class="label">ROE</div>
<div class="v ${metricCls(s.roe || 0, 10)}">${s.roe ?? "-"}%</div>
</div>
<div class="m-cell">
<div class="label">영업이익률</div>
<div class="v ${metricCls(s.operating_margin || 0, 10)}">${s.operating_margin ?? "-"}%</div>
</div>
<div class="m-cell">
<div class="label">부채비율</div>
<div class="v ${(s.debt_ratio||100) <= 50 ? "good" : (s.debt_ratio||0) > 80 ? "bad" : ""}">${s.debt_ratio ?? "-"}%</div>
</div>
<div class="m-cell">
<div class="label">PER</div>
<div class="v">${s.per ?? "-"}</div>
</div>
<div class="m-cell">
<div class="label">안전마진</div>
<div class="v ${mosCls}">${s.margin_of_safety ?? "-"}%</div>
</div>
<div class="m-cell">
<div class="label">이익품질</div>
<div class="v ${eqCls}">${s.earnings_quality ?? "-"}</div>
</div>
<div class="m-cell">
<div class="label">외국인지분</div>
<div class="v">${s.foreign_ratio_pct ?? "-"}%</div>
</div>
<div class="m-cell">
<div class="label">공매도비중</div>
<div class="v ${(s.short_weight_pct||0) > 5 ? "bad" : ""}">${s.short_weight_pct ?? "-"}%</div>
</div>
<div class="m-cell">
<div class="label">FCF</div>
<div class="v ${metricCls(s.fcf_ratio || 0, 5)}">${s.fcf_ratio ?? "-"}%</div>
</div>
</div>
${reasons ? `<div class="reasons">${reasons}</div>` : ""}
</div>
`;
}
async function loadCards(kind) {
const c = document.getElementById("cards-container");
c.innerHTML = '<div class="loader">불러오는 중…</div>';
try {
const r = await fetch(`${API}/api/${kind}?days=1&limit=30`);
const data = await r.json();
if (!data.length) {
c.innerHTML = '<div class="empty">해당 카테고리에 종목이 없습니다.</div>';
return;
}
c.innerHTML = `<div class="cards">${data.map(renderCard).join("")}</div>`;
} catch (e) {
c.innerHTML = `<div class="empty">에러: ${e.message}</div>`;
}
}
document.querySelectorAll(".tab").forEach(t => {
t.addEventListener("click", () => {
document.querySelectorAll(".tab").forEach(x => x.classList.remove("active"));
t.classList.add("active");
loadCards(t.dataset.kind);
});
});
loadSummary();
loadCards("recommendations");
setInterval(loadSummary, 60000);
</script>
</body>
</html>