feat: 핫종목 렌즈 + 뉴스 파서 견고화 + 텔레그램 알림 2회 축소
- news-collector: EXAONE JSON 파서 견고화(max_tokens 800 + 관대 파싱 → 파싱실패율 9.3%→3.3%), 평일 18:30 종목뉴스 스위프(중형주 신선도 사각지대 해소), /process/raw reprocess 플래그, 파싱실패→Gemini 폴백 라우팅(일일캡 1→50 상수화) - score-engine: /hot 모멘텀 렌즈(뉴스+거래량, 백테스트로 가격 제외) + /hot/backtest, 텔레그램 정기 브리핑 2회로 축소(중복 send_briefing 자동스케줄 제거) - dashboard-api: /api/hot 프록시 + 🔥 지금뜨는 탭 - telegram-bot: /hot 명령 + 한글(뜨는/핫/급등) 라우팅 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+534
-71
@@ -7,7 +7,7 @@
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;800;900&family=JetBrains+Mono:wght@400;600;700&display=swap');
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#0D1117;color:#E0E0E0;font-family:'Noto Sans KR',sans-serif;padding:0 24px 40px}
|
||||
body{background:#0D1117;color:#E0E0E0;font-family:'Noto Sans KR',sans-serif;padding:0}
|
||||
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.1);border-radius:3px}
|
||||
.wrap{max-width:1400px;margin:0 auto}
|
||||
.header{display:flex;justify-content:space-between;align-items:center;padding:20px 0;border-bottom:1px solid rgba(255,255,255,0.06);margin-bottom:24px}
|
||||
@@ -15,9 +15,26 @@ body{background:#0D1117;color:#E0E0E0;font-family:'Noto Sans KR',sans-serif;padd
|
||||
.header .sub{font-size:12px;color:#546E7A;margin-top:4px}
|
||||
.header .time{font-size:13px;color:#607D8B;font-family:'JetBrains Mono',monospace}
|
||||
.btn{background:rgba(0,230,118,0.1);border:1px solid rgba(0,230,118,0.2);border-radius:8px;padding:8px 16px;color:#00E676;font-size:12px;cursor:pointer;font-weight:600}
|
||||
.tabs{display:flex;gap:4px;margin-bottom:24px}
|
||||
.tab{background:transparent;border:1px solid transparent;border-radius:8px;padding:8px 16px;color:#607D8B;font-size:13px;font-weight:500;cursor:pointer}
|
||||
.sidebar{position:fixed;left:0;top:0;bottom:0;width:212px;background:#0A0E14;border-right:1px solid rgba(255,255,255,0.06);padding:18px 12px;overflow-y:auto;z-index:200;transition:transform .25s ease}
|
||||
.brand{font-size:16px;font-weight:800;letter-spacing:-0.5px;padding:4px 10px 16px;display:flex;align-items:center;gap:7px;color:#E0E0E0}
|
||||
.brand span{color:#00E676}
|
||||
.tabs{display:flex;flex-direction:column;gap:2px;margin:0}
|
||||
.tab{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:transparent;border:1px solid transparent;border-radius:8px;padding:10px 12px;color:#90A4AE;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit;transition:background .15s}
|
||||
.tab:hover{background:rgba(255,255,255,0.04);color:#CFD8DC}
|
||||
.tab.active{background:rgba(0,230,118,0.1);border-color:rgba(0,230,118,0.2);color:#00E676;font-weight:700}
|
||||
.main{margin-left:212px;padding:0 24px 40px;min-height:100vh}
|
||||
.hamburger{display:none;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;width:40px;height:40px;color:#CFD8DC;font-size:18px;cursor:pointer;flex-shrink:0;align-items:center;justify-content:center}
|
||||
.sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:150}
|
||||
@media(max-width:860px){
|
||||
.sidebar{transform:translateX(-100%);width:248px;box-shadow:6px 0 28px rgba(0,0,0,0.55)}
|
||||
.sidebar.open{transform:translateX(0)}
|
||||
.main{margin-left:0;padding:0 14px 40px}
|
||||
.hamburger{display:flex}
|
||||
.sidebar-overlay.show{display:block}
|
||||
.header{flex-wrap:wrap}
|
||||
.header h1{font-size:17px}
|
||||
.header .sub{display:none}
|
||||
}
|
||||
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:24px}
|
||||
.card{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:16px;padding:24px 20px}
|
||||
.card .label{font-size:13px;color:#90A4AE;letter-spacing:1px;margin-bottom:8px}
|
||||
@@ -25,6 +42,7 @@ body{background:#0D1117;color:#E0E0E0;font-family:'Noto Sans KR',sans-serif;padd
|
||||
.card .sub{font-size:12px;color:#607D8B;margin-top:8px}
|
||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||
@media(max-width:900px){.grid2{grid-template-columns:1fr}}
|
||||
@media(max-width:680px){.usbrief-grid{grid-template-columns:1fr !important}}
|
||||
.panel{background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.05);border-radius:16px;padding:20px;max-height:600px;overflow:auto}
|
||||
.panel h3{font-size:14px;font-weight:700;color:#90A4AE;margin-bottom:16px}
|
||||
.rank-item{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.03)}
|
||||
@@ -108,7 +126,7 @@ tr[data-stock]:hover{background:rgba(0,230,118,0.06) !important}
|
||||
body.auth-required .wrap, body.auth-required #chat-btn, body.auth-required #chat-panel { display:none !important; }
|
||||
body:not(.auth-required) #login-screen { display:none !important; }
|
||||
|
||||
#login-screen{position:fixed;inset:0;background:#0E141A;display:flex;align-items:center;justify-content:center;padding:20px;z-index:100;overflow-y:auto}
|
||||
#login-screen{position:fixed;inset:0;background:#0E141A;display:flex;align-items:center;justify-content:center;padding:20px;z-index:300;overflow-y:auto}
|
||||
.login-card{background:#141B22;border:1px solid rgba(0,230,118,0.18);border-radius:20px;padding:40px;width:420px;max-width:100%;box-shadow:0 12px 60px rgba(0,0,0,0.6)}
|
||||
.login-logo{font-size:24px;font-weight:800;color:#E0E0E0;margin-bottom:6px;letter-spacing:0.5px}
|
||||
.login-logo span{color:#00E676}
|
||||
@@ -174,8 +192,33 @@ body:not(.auth-required) #login-screen { display:none !important; }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="brand"><span>◆</span> Trading AI</div>
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="showTab('overview',this)">📊 종합</button>
|
||||
<button class="tab" onclick="showTab('performance',this)">🎯 추천성과</button>
|
||||
<button class="tab" onclick="showTab('signals',this)">💰 매매시그널</button>
|
||||
<button class="tab" onclick="showTab('surge',this)">🚀 거래량급증</button>
|
||||
<button class="tab" onclick="showTab('hot',this)">🔥 지금뜨는</button>
|
||||
<button class="tab" onclick="showTab('portfolio',this)">💼 포트폴리오</button>
|
||||
<button class="tab" onclick="showTab('mystock',this)">📂 내 종목 분석</button>
|
||||
<button class="tab" onclick="showTab('recommend',this)">🏆 추천종목</button>
|
||||
<button class="tab" onclick="showTab('news',this)">📰 뉴스피드</button>
|
||||
<button class="tab" onclick="showTab('supplydemand',this)">💹 수급</button>
|
||||
<button class="tab" onclick="showTab('alerts',this)">🚨 알림</button>
|
||||
<button class="tab" onclick="showTab('formulas',this)">🧮 10공식</button>
|
||||
<button class="tab" onclick="showTab('backtest',this)">📈 백테스트</button>
|
||||
<button class="tab" onclick="showTab('usmarket',this)">🌎 미증시</button>
|
||||
<button class="tab" onclick="showTab('macro',this)">🌐 매크로·섹터</button>
|
||||
<button class="tab" onclick="showTab('glossary',this)">📖 용어설명</button>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="sidebar-overlay" id="sbOverlay" onclick="toggleSidebar(false)"></div>
|
||||
|
||||
<div class="main">
|
||||
<div class="wrap">
|
||||
<div class="header">
|
||||
<button class="hamburger" onclick="toggleSidebar()">☰</button>
|
||||
<div><h1><span>◆</span> Trading AI Dashboard</h1><div class="sub">뉴스 + DART 공시 기반 종목 분석 시스템</div></div>
|
||||
<div style="display:flex;align-items:center;gap:16px">
|
||||
<div class="time" id="clock"></div>
|
||||
@@ -190,26 +233,11 @@ body:not(.auth-required) #login-screen { display:none !important; }
|
||||
<button onclick="doSearch()" style="background:rgba(0,230,118,0.1);border:1px solid rgba(0,230,118,0.2);border-radius:8px;padding:10px 20px;color:#00E676;font-size:13px;cursor:pointer;font-weight:600;white-space:nowrap">🔍 검색</button>
|
||||
</div>
|
||||
<div id="searchResults" style="display:none;margin-bottom:24px"></div>
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="showTab('overview',this)">📊 종합</button>
|
||||
<button class="tab" onclick="showTab('performance',this)">🎯 추천성과</button>
|
||||
<button class="tab" onclick="showTab('signals',this)">💰 매매시그널</button>
|
||||
<button class="tab" onclick="showTab('surge',this)">🚀 거래량급증</button>
|
||||
<button class="tab" onclick="showTab('portfolio',this)">💼 포트폴리오</button>
|
||||
<button class="tab" onclick="showTab('mystock',this)">📂 내 종목 분석</button>
|
||||
<button class="tab" onclick="showTab('recommend',this)">🏆 추천종목</button>
|
||||
<button class="tab" onclick="showTab('news',this)">📰 뉴스피드</button>
|
||||
<button class="tab" onclick="showTab('supplydemand',this)">💹 수급</button>
|
||||
<button class="tab" onclick="showTab('alerts',this)">🚨 알림</button>
|
||||
<button class="tab" onclick="showTab('formulas',this)">🧮 10공식</button>
|
||||
<button class="tab" onclick="showTab('backtest',this)">📈 백테스트</button>
|
||||
<button class="tab" onclick="showTab('usmarket',this)">🌎 미증시</button>
|
||||
<button class="tab" onclick="showTab('macro',this)">🌐 매크로·섹터</button>
|
||||
</div>
|
||||
<div id="overview"></div>
|
||||
<div id="performance" style="display:none"></div>
|
||||
<div id="signals" style="display:none"></div>
|
||||
<div id="surge" style="display:none"></div>
|
||||
<div id="hot" style="display:none"></div>
|
||||
<div id="portfolio" style="display:none"></div>
|
||||
<div id="mystock" style="display:none"></div>
|
||||
<div id="recommend" style="display:none"></div>
|
||||
@@ -220,8 +248,10 @@ body:not(.auth-required) #login-screen { display:none !important; }
|
||||
<div id="backtest" style="display:none"></div>
|
||||
<div id="usmarket" style="display:none"></div>
|
||||
<div id="macro" style="display:none"></div>
|
||||
<div id="glossary" style="display:none"></div>
|
||||
<div class="footer">Trading AI System • 뉴스/공시 기반 자동 분석 • 투자 판단의 참고 자료로만 활용하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const API = window.location.origin + '/api';
|
||||
const SCOL = {호재:'#00E676',악재:'#FF5252',중립:'#78909C'};
|
||||
@@ -231,20 +261,117 @@ const RCOL = {강력매수:'#00E676',매수관심:'#69F0AE',관망:'#78909C',매
|
||||
function ftime(ts){if(!ts)return'-';const d=new Date(ts),m=Math.floor((Date.now()-d)/60000);if(m<60)return m+'분 전';if(m<1440)return Math.floor(m/60)+'시간 전';return d.toLocaleDateString('ko-KR',{month:'short',day:'numeric'})}
|
||||
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
||||
|
||||
function toggleSidebar(force){
|
||||
const sb=document.getElementById('sidebar'), ov=document.getElementById('sbOverlay');
|
||||
if(!sb)return;
|
||||
const open = force===undefined ? !sb.classList.contains('open') : !!force;
|
||||
sb.classList.toggle('open',open);
|
||||
if(ov)ov.classList.toggle('show',open);
|
||||
}
|
||||
|
||||
function showTab(id,el){
|
||||
['overview','performance','signals','surge','portfolio','mystock','recommend','news','supplydemand','alerts','formulas','backtest','usmarket','macro'].forEach(t=>{
|
||||
['overview','performance','signals','surge','hot','portfolio','mystock','recommend','news','supplydemand','alerts','formulas','backtest','usmarket','macro','glossary'].forEach(t=>{
|
||||
document.getElementById(t).style.display=t===id?'block':'none'});
|
||||
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||||
if(el)el.classList.add('active');
|
||||
toggleSidebar(false);
|
||||
if(id==='mystock'){ if(isLoggedIn()) loadPortfolio().then(()=>renderMyStock()); else renderMyStock(); }
|
||||
if(id==='portfolio') renderPortfolio();
|
||||
if(id==='performance') renderPerformance();
|
||||
if(id==='supplydemand') renderSupplyDemand();
|
||||
if(id==='surge') renderVolumeSurge();
|
||||
if(id==='hot') renderHot();
|
||||
if(id==='formulas') renderFormulas();
|
||||
if(id==='backtest') renderBacktest();
|
||||
if(id==='usmarket') renderUsMarket();
|
||||
if(id==='macro') renderMacro();
|
||||
if(id==='glossary') renderGlossary();
|
||||
}
|
||||
|
||||
// ── 용어 설명 ────────────────────────────────────────────────
|
||||
const GLOSSARY=[
|
||||
{t:'📊 차트 보는 법',items:[
|
||||
['캔들(봉)','하루 주가 움직임을 막대 하나로 표시. 끝가가 시작가보다 <b>높으면 빨강(양봉=올랐다)</b>, <b>낮으면 파랑(음봉=내렸다)</b>. 위·아래로 삐져나온 가는 선은 그날의 최고가·최저가.'],
|
||||
['거래량','그날 사고판 주식 수. 많을수록 시장 관심이 크다는 뜻. 거래량 없이 슬금슬금 오르는 상승은 힘이 약하다.'],
|
||||
['시가/종가/고가/저가','시가=장 시작 가격, 종가=장 끝 가격, 고가=그날 최고가, 저가=그날 최저가.'],
|
||||
]},
|
||||
{t:'📈 이동평균선 (MA = Moving Average)',items:[
|
||||
['MA5','최근 <b>5일</b> 종가의 평균선. 약 1주일치 단기 흐름.'],
|
||||
['MA20','최근 <b>20일</b> 평균선. 약 한 달 흐름이라 "생명선"이라 부른다. 주가가 이 위면 단기 분위기 양호.'],
|
||||
['MA60','최근 <b>60일</b> 평균선. 약 3개월, 중기 추세.'],
|
||||
['MA120','최근 <b>120일</b> 평균선. 약 6개월, 장기 추세라 "경기선"이라 부른다.'],
|
||||
['정배열 / 역배열','MA5>MA20>MA60 순으로 짧은 선이 위에 있으면 <b>정배열=상승 추세</b>. 거꾸로면 역배열=하락 추세.'],
|
||||
['골든크로스 / 데드크로스','짧은 선이 긴 선을 아래→위로 뚫으면 <b>골든크로스(매수 신호)</b>, 위→아래로 뚫으면 데드크로스(매도 신호).'],
|
||||
]},
|
||||
{t:'🎯 기술적 지표 (매매 타이밍)',items:[
|
||||
['RSI','0~100 숫자. <b>70 이상이면 과열</b>(너무 많이 사서 비쌈), <b>30 이하면 과매도</b>(너무 많이 팔려 쌈). 되돌림 가능성을 본다.'],
|
||||
['MACD','단기·장기 이동평균의 차이로 추세 전환을 잡는 지표. 0선 위로 올라오면 상승 힘이 붙는 것.'],
|
||||
['볼린저밴드','주가가 보통 움직이는 범위를 상단·중간·하단 띠로 표시. 상단에 닿으면 단기 비쌈, 하단이면 쌈.'],
|
||||
['스토캐스틱','RSI처럼 과열/과매도를 보는 지표. 20 이하 과매도, 80 이상 과열.'],
|
||||
['OBV','거래량으로 돈이 들어오는지(매집)·나가는지(분산)를 추적. 주가보다 먼저 움직이기도 한다.'],
|
||||
['VWAP','거래량 가중 평균가. 기관 투자자가 "본전" 기준으로 삼는 가격.'],
|
||||
['일목균형표','구름대(저항·지지 구간)로 추세를 보는 일본식 종합 지표.'],
|
||||
]},
|
||||
{t:'💰 재무 지표 (회사가 돈을 잘 버나)',items:[
|
||||
['ROE','자기자본이익률. 주주 돈 100원으로 1년에 얼마 벌었나. <b>높을수록 좋고 15% 이상이면 우수</b>. 워런 버핏이 가장 중시하는 숫자.'],
|
||||
['영업이익률','매출 100원 중 본업으로 남긴 이익 비율. 높을수록 사업 경쟁력이 강하다.'],
|
||||
['순이익률','매출에서 모든 비용·이자·세금을 다 빼고 최종으로 남은 비율.'],
|
||||
['부채비율','자기자본 대비 빚의 비율. <b>100% 이하면 안전</b>, 200%를 넘으면 위험 신호.'],
|
||||
['FCF비율','잉여현금흐름(Free Cash Flow). 회사가 벌어서 쓸 거 다 쓰고 <b>실제로 남긴 진짜 현금</b>. 회계상 이익보다 정직한 숫자라 플러스면 건강.'],
|
||||
['매출성장률','작년 대비 올해 매출이 얼마나 늘었나. 회사가 커가고 있는지를 본다.'],
|
||||
]},
|
||||
{t:'🏷️ 가치 지표 (주가가 싼가 비싼가)',items:[
|
||||
['PER','주가 ÷ 주당순이익. 회사 이익 대비 주가가 몇 배인가. <b>낮을수록 싸다</b>. 보통 10배 이하면 저평가 경향.'],
|
||||
['PBR','주가 ÷ 주당순자산. <b>1배 미만이면 회사가 가진 자산보다 주가가 싸게</b> 거래되는 것.'],
|
||||
['EPS / BPS','EPS=주당순이익(1주가 1년에 번 돈). BPS=주당순자산(회사 청산 시 1주가 받을 몫).'],
|
||||
['시가총액','주가 × 총 주식 수 = 회사 전체의 시장 몸값.'],
|
||||
['배당수익률','주가 대비 1년간 받는 배당금 비율. 은행 이자처럼 보면 된다.'],
|
||||
]},
|
||||
{t:'🧮 이 시스템의 점수',items:[
|
||||
['종합점수 (-100~100)','재무·뉴스·기술·공시·수급을 다 합친 최종 점수. <b>높을수록 매수 매력</b>이 크다.'],
|
||||
['펀더멘털 점수','회사의 본질 가치 점수 (ROE·이익률·부채·현금흐름 등 기반).'],
|
||||
['기술점수','차트·기술 지표 기반의 단기 매매 타이밍 점수.'],
|
||||
['뉴스점수','최근 7일 뉴스의 호재/악재를 종합한 점수.'],
|
||||
['10공식 보팅','매직포뮬러·F-Score 등 학계·실전 검증된 10개 투자 공식이 각자 매수/매도를 투표한 결과.'],
|
||||
['추천등급','강력매수 > 매수관심 > 관망 > 매도관심 > 강력매도 순. 점수와 공식 투표를 함께 본다.'],
|
||||
]},
|
||||
{t:'🏛️ 시장 제도·거래 용어',items:[
|
||||
['공매도','내가 갖고 있지도 않은 주식을 <b>빌려서 먼저 판 뒤</b>, 값이 떨어지면 싸게 되사서 갚아 차익을 먹는 기법. 주가 <b>하락에 베팅</b>하는 것. 공매도 물량이 많으면 하락 압력 신호로 본다 (이 시스템의 공매도 점수에 반영).'],
|
||||
['사이드카','선물 가격이 ±5~6% 급등락하면 <b>프로그램 매매(컴퓨터 자동매매)를 5분간 정지</b>시키는 장치. 시장 쏠림을 잠깐 식히는 가벼운 브레이크. 5분 후 자동 해제.'],
|
||||
['서킷브레이커','사이드카보다 센 비상정지. 지수가 8%·15%·20% 급락하면 <b>주식 거래 전체를 20분간 멈춤</b>. 패닉 매도를 막는 장치.'],
|
||||
['VI (변동성완화장치)','개별 종목이 갑자기 급등락하면 2~3분간 단일가 매매로 바꿔 진정시키는 장치. 사이드카의 개별 종목 버전.'],
|
||||
['상한가 / 하한가','하루에 주가가 오를·내릴 수 있는 한계. 한국은 전일 종가 대비 <b>±30%</b>.'],
|
||||
['ISA','개인종합자산관리계좌. 주식·펀드·예금을 한 계좌에 담아 굴리는 <b>절세 계좌</b>. 수익 일정액까지 세금 면제·저율 과세. 의무 보유기간(보통 3년) 있음. "세금 혜택 받는 장기투자용 계좌".'],
|
||||
['배당','회사가 번 이익의 일부를 주주에게 나눠주는 돈. 1년에 한 번(결산배당)이 보통이고 분기마다 주는 회사도 있다.'],
|
||||
]},
|
||||
{t:'💡 투자 스타일·철학',items:[
|
||||
['가치투자','회사의 본질가치보다 <b>싸게 거래되는 주식</b>을 사서 제값을 찾아갈 때까지 기다리는 방식. 대표: 벤저민 그레이엄·워런 버핏. 이 시스템의 기본 관점.'],
|
||||
['성장투자','지금 좀 비싸도 <b>매출·이익이 빠르게 크는 회사</b>면 산다. 미래 성장이 현재 가격을 정당화한다는 관점. 대표: 필립 피셔·피터 린치.'],
|
||||
['모멘텀투자','<b>오르는 주식이 더 오른다</b>는 경험칙에 기대 추세에 올라타는 방식(추세추종). 대표: AQR·제시 리버모어. 이 시스템의 12-1 모멘텀 공식이 여기 해당.'],
|
||||
['퀀트투자','감정을 배제하고 <b>데이터·통계로 패턴을 찾아 기계적으로</b> 매매. 대표: 짐 사이먼스(르네상스). 이 시스템의 10공식 엔진 자체가 퀀트 방식.'],
|
||||
['매크로투자','개별 종목보다 <b>금리·환율·경기 같은 큰 흐름</b>에 베팅. 대표: 조지 소로스·레이 달리오.'],
|
||||
['역발상투자','남들이 공포에 팔 때 사고 열광할 때 판다 — <b>대중과 반대로</b>. 대표: 존 템플턴.'],
|
||||
['인덱스투자','시장을 이기기 어려우니 <b>지수 전체를 싸게 사서 장기 보유</b>(패시브). 대표: 존 보글. 버핏도 일반인에겐 이걸 권한다.'],
|
||||
['기술적분석','재무가 아니라 <b>차트·거래량·지표로 매매 타이밍</b>을 잡는 방식. 가격이 모든 정보를 반영한다고 본다.'],
|
||||
['배당투자','<b>배당을 꾸준히·많이 주는 회사</b>로 안정적인 현금흐름을 추구. 주가 등락보다 배당 수입을 중시.'],
|
||||
['멀티팩터','가치·성장·모멘텀·퀄리티 등 <b>여러 방식을 데이터로 섞는</b> 현대 퀀트 접근. 한 스타일도 항상 이기진 못하기 때문 — <b>이 시스템이 실제로 쓰는 방식.</b>'],
|
||||
]},
|
||||
];
|
||||
function renderGlossary(){
|
||||
const el=document.getElementById('glossary');
|
||||
let h=`<div style="font-size:13px;color:#90A4AE;margin-bottom:16px;line-height:1.6">
|
||||
📖 주식 표·차트에 나오는 용어를 <b style="color:#00E676">쉬운 말</b>로 풀이했습니다. 모르는 단어가 보이면 여기서 찾아보세요.</div>`;
|
||||
GLOSSARY.forEach(g=>{
|
||||
h+=`<div class="panel" style="max-height:none;margin-bottom:14px"><h3 style="margin-bottom:10px">${g.t}</h3>`;
|
||||
g.items.forEach(it=>{
|
||||
h+=`<div style="display:flex;gap:14px;padding:11px 2px;border-bottom:1px solid rgba(255,255,255,0.05)">
|
||||
<div style="flex:0 0 96px;color:#00E676;font-weight:700;font-size:13px">${it[0]}</div>
|
||||
<div style="flex:1;color:#B0BEC5;font-size:12.5px;line-height:1.65">${it[1]}</div>
|
||||
</div>`;
|
||||
});
|
||||
h+=`</div>`;
|
||||
});
|
||||
el.innerHTML=h;
|
||||
}
|
||||
|
||||
async function renderVolumeSurge(){
|
||||
@@ -281,6 +408,41 @@ async function renderVolumeSurge(){
|
||||
}
|
||||
}
|
||||
|
||||
async function renderHot(){
|
||||
const el=document.getElementById('hot');
|
||||
el.innerHTML=`<div style="text-align:center;color:#546E7A;padding:30px">불러오는 중...</div>`;
|
||||
try{
|
||||
const list=await api('/hot')||[];
|
||||
if(!list.length){el.innerHTML='<div class="empty">지금 뜨는 종목 없음</div>';return;}
|
||||
let h=`<div style="font-size:12px;color:#FFB74D;background:rgba(255,152,0,0.08);border:1px solid rgba(255,152,0,0.2);border-radius:8px;padding:10px 12px;margin-bottom:14px">
|
||||
🔥 <b>지금 뜨는 종목</b> — 최근 뉴스 호재 + 거래량 급증 기준. <b style="color:#FF8A65">※ 관찰용입니다.</b> 백테스트상 핫종목은 5~10일 들면 시장에 지므로(되돌림) 매수신호가 아니라 "지금 뭐가 움직이나" 보는 창으로만 쓰세요.</div>`;
|
||||
h+=`<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:10px">`;
|
||||
list.forEach((r,i)=>{
|
||||
const recoC=r.value_reco==='강력매수'?'#00E676':r.value_reco==='매수관심'?'#69F0AE':(r.value_reco||'').includes('매도')?'#FF5252':'#90A4AE';
|
||||
const p5=r.price_5d_pct||0, p5C=p5>=0?'#00E676':'#FF5252';
|
||||
h+=`<div data-stock="${r.stock_code}" data-stock-name="${esc(r.stock_name)}" style="cursor:pointer;background:rgba(255,255,255,0.03);border:1px solid rgba(255,152,0,0.12);border-radius:10px;padding:12px 14px;transition:background .15s" onmouseover="this.style.background='rgba(255,255,255,0.06)'" onmouseout="this.style.background='rgba(255,255,255,0.03)'">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:700;color:#E0E0E0">${i+1}. ${esc(r.stock_name||r.stock_code)}</div>
|
||||
<div style="font-size:10px;color:#546E7A;font-family:'JetBrains Mono',monospace">${r.stock_code}</div>
|
||||
</div>
|
||||
<div style="font-size:15px;color:#FFB74D;font-weight:700">🔥 ${(r.hot_score||0).toFixed(0)}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:#90A4AE">
|
||||
${r.pos_news_3d?`<span>호재 ${r.pos_news_3d}건</span>`:''}
|
||||
<span>5일 <b style="color:${p5C}">${p5>=0?'+':''}${p5.toFixed(1)}%</b></span>
|
||||
${r.vol_ratio>=1.5?`<span>거래량 ${(r.vol_ratio||0).toFixed(1)}배</span>`:''}
|
||||
</div>
|
||||
<div style="margin-top:6px;font-size:11px;color:#78909C">가치판단: <b style="color:${recoC}">${r.value_reco||'-'}</b>${r.value_score!=null?` (${r.value_score}점)`:''}</div>
|
||||
</div>`;
|
||||
});
|
||||
h+=`</div>`;
|
||||
el.innerHTML=h;
|
||||
}catch(e){
|
||||
el.innerHTML='<div class="empty">로드 실패: '+esc(e.message)+'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function api(path){try{const r=await fetch(API+path);return await r.json()}catch(e){return null}}
|
||||
|
||||
async function loadAll(){
|
||||
@@ -1012,11 +1174,9 @@ async function renderPortfolio(){
|
||||
const el=document.getElementById('portfolio');
|
||||
const list=getPortfolio();
|
||||
if(!list.length){
|
||||
el.innerHTML=`<div style="text-align:center;padding:80px 20px;color:#546E7A">
|
||||
<div style="font-size:48px;margin-bottom:16px">💼</div>
|
||||
<div style="font-size:16px;margin-bottom:8px;color:#78909C">보유 종목이 없습니다</div>
|
||||
<div style="font-size:13px">📂 내 종목 분석 탭에서 종목을 추가하면 여기에 표시됩니다</div>
|
||||
</div>`;
|
||||
el.innerHTML='<div class="empty">AI 추천 포트폴리오 불러오는 중...</div>';
|
||||
const reco=await api('/portfolio/recommended'+(recoAmount>0?'?amount='+recoAmount:'')).catch(()=>null);
|
||||
el.innerHTML=`<div style="background:rgba(64,196,255,0.06);border:1px solid rgba(64,196,255,0.2);border-radius:12px;padding:16px 18px;margin-bottom:16px;color:#CFD8DC;font-size:13px;line-height:1.6">💼 아직 등록된 보유 종목이 없습니다. 아래 <b style="color:#69F0AE">AI 추천 포트폴리오</b>를 참고하거나, <b>📂 내 종목 분석</b> 탭에서 종목을 추가하세요.</div>`+recoPortfolioHTML(reco);
|
||||
return;
|
||||
}
|
||||
el.innerHTML='<div class="empty">현재가 · AI점수 불러오는 중...</div>';
|
||||
@@ -1035,7 +1195,7 @@ async function renderPortfolio(){
|
||||
return{...p,cur,changePct:pd.change_pct||0,cost,val,
|
||||
pnl:val-cost,pnlPct:cost>0?(val-cost)/cost*100:0,
|
||||
techScore:pd.tech_score||0,signal:pd.signal||'관망',
|
||||
aiScore:pd.ai_score,recommendation:pd.recommendation};
|
||||
aiScore:pd.ai_score,recommendation:pd.recommendation,sector:pd.sector||'기타'};
|
||||
});
|
||||
const totalPnl=totalValue-totalCost;
|
||||
const totalPnlPct=totalCost>0?totalPnl/totalCost*100:0;
|
||||
@@ -1109,9 +1269,134 @@ async function renderPortfolio(){
|
||||
</tr>`;
|
||||
});
|
||||
h+=`</tbody></table></div></div>`;
|
||||
h+=portfolioSectorHTML(holdings);
|
||||
h+=portfolioRebalanceHTML(holdings);
|
||||
const reco=await api('/portfolio/recommended'+(recoAmount>0?'?amount='+recoAmount:'')).catch(()=>null);
|
||||
h+=recoPortfolioHTML(reco);
|
||||
el.innerHTML=h;
|
||||
}
|
||||
|
||||
let recoAmount=0;
|
||||
async function applyRecoAmount(){
|
||||
const inp=document.getElementById('reco-amt');
|
||||
const v=parseInt((inp&&inp.value)||'0',10);
|
||||
recoAmount=isNaN(v)?0:v;
|
||||
renderPortfolio();
|
||||
}
|
||||
|
||||
function portfolioSectorHTML(holdings){
|
||||
const sec={};
|
||||
holdings.forEach(s=>{ if(s.weight>0) sec[s.sector]=(sec[s.sector]||0)+s.weight; });
|
||||
const arr=Object.entries(sec).sort((a,b)=>b[1]-a[1]);
|
||||
if(!arr.length) return '';
|
||||
let h=`<div class="panel" style="max-height:none;margin-bottom:16px"><h3>📊 섹터(업종) 비중</h3>`;
|
||||
arr.forEach(([name,w])=>{
|
||||
const over=w>30, c=over?'#FF8A80':'#40C4FF';
|
||||
h+=`<div style="margin-bottom:10px">
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">
|
||||
<span style="color:#CFD8DC">${esc(name)}${over?' ⚠️':''}</span>
|
||||
<span style="color:${c};font-weight:700">${w.toFixed(1)}%</span></div>
|
||||
<div style="height:8px;background:rgba(255,255,255,0.06);border-radius:4px;overflow:hidden">
|
||||
<div style="height:100%;width:${Math.min(100,w).toFixed(0)}%;background:${c};border-radius:4px"></div></div></div>`;
|
||||
});
|
||||
if(arr[0][1]>30) h+=`<div style="font-size:11.5px;color:#FF8A80;margin-top:4px">⚠️ <b>${esc(arr[0][0])}</b> 업종이 ${arr[0][1].toFixed(0)}%로 한쪽에 쏠렸습니다. 한 업종 30% 이내로 분산하면 위험이 줄어듭니다.</div>`;
|
||||
return h+`</div>`;
|
||||
}
|
||||
|
||||
function portfolioRebalanceHTML(holdings){
|
||||
const acts=[];
|
||||
holdings.forEach(s=>{
|
||||
if(s.recommendation==='강력매도'||s.recommendation==='매도관심')
|
||||
acts.push([s,'sell',`AI가 <b>${esc(s.recommendation)}</b> 신호를 냈습니다 — 매도를 검토하세요`]);
|
||||
else if(s.cur>0&&s.pnlPct<=-10)
|
||||
acts.push([s,'cut',`손실 ${s.pnlPct.toFixed(1)}% — 손절 기준을 점검하세요`]);
|
||||
else if(s.aiScore!=null&&s.aiScore<=-30)
|
||||
acts.push([s,'sell',`AI점수 ${s.aiScore.toFixed(0)}점으로 부진 — 매도를 검토하세요`]);
|
||||
else if(s.aiScore!=null&&s.aiScore>=70&&s.weight<7)
|
||||
acts.push([s,'add',`AI점수 ${s.aiScore.toFixed(0)}점 우량인데 비중이 작습니다 — 추가 매수 고려`]);
|
||||
});
|
||||
let h=`<div class="panel" style="max-height:none;margin-bottom:16px"><h3>🔧 리밸런싱 진단</h3>`;
|
||||
if(!acts.length){
|
||||
h+=`<div style="color:#69F0AE;font-size:12.5px">✅ 보유 종목에 특별한 조치 신호가 없습니다. 그대로 유지해도 무난합니다.</div>`;
|
||||
}else{
|
||||
acts.forEach(([s,kind,msg])=>{
|
||||
const c=kind==='add'?'#69F0AE':kind==='sell'?'#FF8A80':'#FFD740';
|
||||
const ic=kind==='add'?'➕':kind==='sell'?'⚠️':'🔻';
|
||||
h+=`<div data-stock="${s.code}" data-stock-name="${esc(s.name||'')}" style="display:flex;gap:10px;align-items:center;padding:9px 0;border-bottom:1px solid rgba(255,255,255,0.05);cursor:pointer">
|
||||
<span style="font-size:15px">${ic}</span>
|
||||
<div style="flex:1;min-width:0"><div style="color:#CFD8DC;font-size:12.5px;font-weight:600">${esc(s.name||s.code)}</div>
|
||||
<div style="color:${c};font-size:11px;line-height:1.5">${msg}</div></div>
|
||||
<span style="color:${s.pnlPct>=0?'#69F0AE':'#FF8A80'};font-size:11px;font-family:'JetBrains Mono',monospace">${s.cur>0?(s.pnlPct>=0?'+':'')+s.pnlPct.toFixed(1)+'%':'-'}</span></div>`;
|
||||
});
|
||||
}
|
||||
return h+`</div>`;
|
||||
}
|
||||
|
||||
function recoPortfolioHTML(reco){
|
||||
if(!reco||!reco.holdings||!reco.holdings.length)
|
||||
return `<div class="panel" style="max-height:none"><h3>🤖 AI 추천 포트폴리오</h3><div style="color:#546E7A;font-size:12px">${esc((reco&&reco.msg)||'추천 데이터를 불러오지 못했습니다.')}</div></div>`;
|
||||
let h=`<div class="panel" style="max-height:none"><h3>🤖 AI 추천 포트폴리오 (${esc(reco.as_of||'')})</h3>
|
||||
<div style="font-size:11.5px;color:#90A4AE;margin-bottom:12px;line-height:1.6">오늘 <b style="color:#69F0AE">강력매수·매수관심</b> 종목을 변동성·점수로 비중을 매기고, 한 업종 30% 이내로 분산한 구성입니다. 투자금액을 넣으면 종목별 매수 주수를 계산합니다.</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:14px;align-items:center;flex-wrap:wrap">
|
||||
<input id="reco-amt" type="number" placeholder="투자금액 입력 (원)" value="${recoAmount||''}"
|
||||
style="background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:8px 12px;color:#E0E0E0;font-size:13px;width:180px;outline:none"
|
||||
onkeypress="if(event.key==='Enter')applyRecoAmount()">
|
||||
<button onclick="applyRecoAmount()" style="background:rgba(0,230,118,0.1);border:1px solid rgba(0,230,118,0.25);border-radius:8px;padding:8px 16px;color:#00E676;font-size:12px;font-weight:600;cursor:pointer">계산</button>
|
||||
</div>`;
|
||||
const sbArr=Object.entries(reco.sector_breakdown||{});
|
||||
if(sbArr.length){
|
||||
h+=`<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px">`;
|
||||
sbArr.forEach(([n,w])=>{ h+=`<span style="background:rgba(64,196,255,0.1);border:1px solid rgba(64,196,255,0.25);border-radius:6px;padding:3px 9px;font-size:10.5px;color:#CFD8DC">${esc(n)} ${w.toFixed(0)}%</span>`; });
|
||||
h+=`</div>`;
|
||||
}
|
||||
const hasAmt=reco.input_amount>0;
|
||||
h+=`<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
|
||||
<thead><tr style="color:#78909C;border-bottom:1px solid rgba(255,255,255,0.08)">
|
||||
<th style="text-align:left;padding:8px 6px;font-weight:600">종목</th>
|
||||
<th style="text-align:center;padding:8px 6px;font-weight:600">AI점수</th>
|
||||
<th style="text-align:right;padding:8px 6px;font-weight:600">비중</th>
|
||||
${hasAmt?'<th style="text-align:right;padding:8px 6px;font-weight:600">매수금액</th><th style="text-align:right;padding:8px 6px;font-weight:600">주수</th>':''}
|
||||
</tr></thead><tbody>`;
|
||||
reco.holdings.forEach((s,i)=>{
|
||||
const aic=s.total_score>=70?'#00E676':s.total_score>=40?'#69F0AE':'#90A4AE';
|
||||
h+=`<tr data-stock="${s.stock_code}" data-stock-name="${esc(s.stock_name)}" style="border-bottom:1px solid rgba(255,255,255,0.04);cursor:pointer;${i%2?'background:rgba(255,255,255,0.01)':''}">
|
||||
<td style="padding:9px 6px"><div style="color:#CFD8DC;font-weight:600">${esc(s.stock_name)}</div>
|
||||
<div style="font-size:10px;color:#546E7A">${esc(s.sector)} · ${esc(s.recommendation)}</div></td>
|
||||
<td style="text-align:center;padding:9px 6px;color:${aic};font-weight:700;font-family:'JetBrains Mono',monospace">${s.total_score.toFixed(0)}</td>
|
||||
<td style="text-align:right;padding:9px 6px"><span style="color:#40C4FF;font-weight:700">${s.weight_pct.toFixed(1)}%</span></td>
|
||||
${hasAmt?`<td style="text-align:right;padding:9px 6px;color:#CFD8DC;font-family:'JetBrains Mono',monospace">${fmt(s.invest_amount)}</td><td style="text-align:right;padding:9px 6px;color:#E0E0E0;font-weight:600">${s.shares.toLocaleString()}</td>`:''}
|
||||
</tr>`;
|
||||
});
|
||||
h+=`</tbody></table></div>`;
|
||||
if(hasAmt)
|
||||
h+=`<div style="margin-top:10px;font-size:11.5px;color:#90A4AE">투자금 ${fmt(reco.input_amount)}원 중 <b style="color:#69F0AE">${fmt(reco.invested_amount)}원</b> 매수 · 현금 ${fmt(reco.cash_remaining)}원 남음</div>`;
|
||||
return h+`</div>`;
|
||||
}
|
||||
|
||||
function equityCurveSVG(curve){
|
||||
if(!curve||curve.length<2) return '';
|
||||
const W=640,H=180,pad=30;
|
||||
const allv=curve.flatMap(p=>[p.strategy_pct,p.kospi_pct]).concat([0]);
|
||||
let mn=Math.min(...allv), mx=Math.max(...allv);
|
||||
if(mx===mn)mx=mn+1;
|
||||
const X=i=>pad+(W-2*pad)*i/(curve.length-1);
|
||||
const Y=v=>H-pad-(H-2*pad)*(v-mn)/(mx-mn);
|
||||
const path=k=>curve.map((p,i)=>`${i?'L':'M'}${X(i).toFixed(1)},${Y(p[k]).toFixed(1)}`).join('');
|
||||
const last=curve[curve.length-1];
|
||||
const sC=last.strategy_pct>=0?'#00E676':'#FF5252';
|
||||
return `<div class="panel" style="margin-top:14px"><h3>📈 자산곡선 — 추천을 따랐을 때 누적 수익률</h3>
|
||||
<div style="font-size:11px;color:#90A4AE;margin-bottom:8px">매일 추천 종목을 등비중으로 샀다고 가정한 누적 수익률(거래비용 차감). <b style="color:${sC}">초록=전략</b> · <b style="color:#FFB74D">주황=KOSPI</b></div>
|
||||
<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:auto">
|
||||
<line x1="${pad}" y1="${Y(0).toFixed(1)}" x2="${W-pad}" y2="${Y(0).toFixed(1)}" stroke="rgba(255,255,255,0.15)" stroke-dasharray="3"/>
|
||||
<path d="${path('kospi_pct')}" fill="none" stroke="#FFB74D" stroke-width="1.6"/>
|
||||
<path d="${path('strategy_pct')}" fill="none" stroke="${sC}" stroke-width="2.2"/>
|
||||
</svg>
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;color:#78909C;margin-top:4px">
|
||||
<span>${esc(curve[0].date)}</span>
|
||||
<span style="color:${sC};font-weight:700">전략 ${last.strategy_pct>=0?'+':''}${last.strategy_pct}% vs KOSPI ${last.kospi_pct>=0?'+':''}${last.kospi_pct}%</span>
|
||||
<span>${esc(last.date)}</span></div></div>`;
|
||||
}
|
||||
|
||||
function renderSignals(signals,cands){
|
||||
let h=`<div style="margin-bottom:20px">
|
||||
<div style="font-size:12px;color:#546E7A;margin-bottom:16px">
|
||||
@@ -1871,6 +2156,7 @@ async function renderBacktest(){
|
||||
${_btCard('MDD', oa.max_drawdown_pct, '%', true)}
|
||||
${_btCard('알파', oa.avg_alpha_pct, '%', true)}
|
||||
</div>
|
||||
${equityCurveSVG(d.equity_curve)}
|
||||
<div class="panel" style="margin-top:14px"><h3>등급별 성과 (7일)</h3>${_btTable(d.by_recommendation_7d||{})}</div>`;
|
||||
if(oa30.n){
|
||||
h += `<h3 style="color:#90A4AE;font-size:13px;margin:18px 0 10px">30일 보유</h3>
|
||||
@@ -1892,64 +2178,241 @@ async function renderBacktest(){
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 🌎 미증시 동조
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 미국 주요 티커 → 한글 회사명/설명 매핑 (페어·ETF 표시용)
|
||||
const US_TICKER_KR = {
|
||||
// 반도체
|
||||
NVDA:'엔비디아', AMD:'AMD', MU:'마이크론', INTC:'인텔', TSM:'TSMC(대만 반도체)',
|
||||
AVGO:'브로드컴', QCOM:'퀄컴', TXN:'TI', AMAT:'어플라이드머티리얼', LRCX:'램리서치', KLAC:'KLA',
|
||||
// 빅테크
|
||||
AAPL:'애플', MSFT:'마이크로소프트', GOOGL:'구글', AMZN:'아마존', META:'메타(페북)',
|
||||
TSLA:'테슬라', NFLX:'넷플릭스', ORCL:'오라클', ADBE:'어도비', CRM:'세일즈포스',
|
||||
// 금융
|
||||
JPM:'JP모건', BAC:'뱅크오브아메리카', GS:'골드만삭스', MS:'모건스탠리',
|
||||
WFC:'웰스파고', C:'씨티그룹', BLK:'블랙록',
|
||||
// 에너지/소재
|
||||
XOM:'엑손모빌', CVX:'쉐브론', COP:'코노코필립스', NUE:'뉴코어(철강)', FCX:'프리포트(구리)',
|
||||
// 헬스/바이오
|
||||
JNJ:'존슨앤존슨', PFE:'화이자', MRK:'머크', ABBV:'애브비', LLY:'일라이릴리',
|
||||
BMY:'BMS', GILD:'길리어드',
|
||||
// 소비재/유통
|
||||
WMT:'월마트', COST:'코스트코', HD:'홈디포', MCD:'맥도날드', NKE:'나이키',
|
||||
SBUX:'스타벅스', DIS:'디즈니',
|
||||
// 산업/방산
|
||||
BA:'보잉', LMT:'록히드마틴', RTX:'레이시온', GD:'제너럴다이내믹스', CAT:'캐터필러', DE:'존디어',
|
||||
// 자동차
|
||||
F:'포드', GM:'GM', TM:'도요타',
|
||||
// 2차전지/리튬
|
||||
ALB:'알버말(리튬)', RIVN:'리비안',
|
||||
// 화학
|
||||
DOW:'다우', LYB:'라이언델바젤',
|
||||
// 게임/엔터
|
||||
NTES:'넷이즈(중국게임)',
|
||||
// ETF
|
||||
SOXX:'반도체 ETF', SMH:'반도체 ETF(VanEck)', XLK:'미국 기술주 ETF', QQQ:'나스닥100 ETF',
|
||||
XBI:'바이오 ETF', IBB:'바이오 ETF(나스닥)', LIT:'리튬·전기차 ETF',
|
||||
XLE:'에너지 ETF', XLF:'금융 ETF', XLV:'헬스케어 ETF', XLI:'산업재 ETF',
|
||||
XLP:'필수소비재 ETF', XLY:'경기소비재 ETF', ITA:'항공·방산 ETF',
|
||||
};
|
||||
function usName(t){ return US_TICKER_KR[t] || ''; }
|
||||
|
||||
async function renderUsMarket(){
|
||||
const el=document.getElementById('usmarket');
|
||||
el.innerHTML=`<div style="font-size:12px;color:#546E7A;margin-bottom:14px">미국 섹터 ETF·KR↔US 페어 회귀로 한국 종목별 동조 시그널 산출 (매일 KST 08:00)</div><div style="text-align:center;color:#546E7A;padding:30px">불러오는 중...</div>`;
|
||||
el.innerHTML=`<div style="text-align:center;color:#546E7A;padding:30px">불러오는 중...</div>`;
|
||||
try{
|
||||
const [etfs, pairs, signals] = await Promise.all([
|
||||
api('/usmarket/etfs'), api('/usmarket/pairs'), api('/usmarket/signals')
|
||||
const [etfs, pairs, signals, brief] = await Promise.all([
|
||||
api('/usmarket/etfs'), api('/usmarket/pairs'), api('/usmarket/signals'), api('/usmarket/briefing')
|
||||
]);
|
||||
let h = `<div class="grid2">`;
|
||||
// 섹터 ETF
|
||||
h += `<div class="panel"><h3>섹터 ETF (${Array.isArray(etfs)?etfs.length:0})</h3>`;
|
||||
if(Array.isArray(etfs) && etfs.length){
|
||||
h += `<table style="width:100%;font-size:11px;border-collapse:separate;border-spacing:1px">
|
||||
<thead><tr style="color:#90A4AE"><th style="text-align:left;padding:6px">티커</th><th style="text-align:left">설명</th><th style="text-align:left">한국 섹터</th></tr></thead><tbody>`;
|
||||
etfs.slice(0,30).forEach(e=>{
|
||||
const kw = Array.isArray(e.sector_keywords)?e.sector_keywords.join(', '):'';
|
||||
h += `<tr><td style="padding:5px 6px;color:#69F0AE;font-family:'JetBrains Mono',monospace;font-weight:700">${esc(e.etf_ticker)}</td><td style="color:#CFD8DC">${esc(e.description||'-')}</td><td style="color:#90A4AE;font-size:10px">${esc(kw)}</td></tr>`;
|
||||
});
|
||||
h += `</tbody></table>`;
|
||||
} else h += `<div class="empty">데이터 없음</div>`;
|
||||
h += `</div>`;
|
||||
// 페어
|
||||
h += `<div class="panel"><h3>KR ↔ US 페어 (${Array.isArray(pairs)?pairs.length:0})</h3>`;
|
||||
if(Array.isArray(pairs) && pairs.length){
|
||||
h += `<table style="width:100%;font-size:11px;border-collapse:separate;border-spacing:1px">
|
||||
<thead><tr style="color:#90A4AE"><th style="text-align:left;padding:6px">US</th><th>KR</th><th title="60일 회귀 베타">β</th><th title="60일 상관">ρ</th><th>N</th></tr></thead><tbody>`;
|
||||
pairs.slice(0,30).forEach(p=>{
|
||||
const beta = p.beta_60d!=null?(+p.beta_60d).toFixed(2):'-';
|
||||
const corr = p.correlation_60d!=null?(+p.correlation_60d).toFixed(2):'-';
|
||||
h += `<tr><td style="padding:5px 6px;color:#69F0AE;font-family:'JetBrains Mono',monospace">${esc(p.us_ticker)}</td><td style="color:#CFD8DC;font-family:'JetBrains Mono',monospace">${esc(p.kr_code)}</td><td style="text-align:center;font-family:'JetBrains Mono',monospace;color:#FFD740">${beta}</td><td style="text-align:center;font-family:'JetBrains Mono',monospace;color:#90A4AE">${corr}</td><td style="text-align:center;color:#546E7A">${p.sample_size||'-'}</td></tr>`;
|
||||
});
|
||||
h += `</tbody></table>`;
|
||||
} else h += `<div class="empty">데이터 없음</div>`;
|
||||
h += `</div></div>`;
|
||||
// 시그널
|
||||
h += `<div class="panel" style="margin-top:14px"><h3>최신 동조 시그널 (${Array.isArray(signals)?signals.length:0})</h3>`;
|
||||
|
||||
// ── 0. 탭 안내 (이 탭은 뭐 하는 곳?) ──
|
||||
let h = `
|
||||
<div style="background:linear-gradient(135deg,rgba(64,196,255,0.07),rgba(105,240,174,0.05));border:1px solid rgba(64,196,255,0.25);border-radius:12px;padding:14px 18px;margin-bottom:16px">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
||||
<span style="font-size:18px">🌎</span>
|
||||
<span style="font-weight:700;color:#40C4FF;font-size:14px">이 탭은 뭐예요?</span>
|
||||
</div>
|
||||
<div style="color:#CFD8DC;font-size:12.5px;line-height:1.7">
|
||||
<b style="color:#69F0AE">미국 증시의 움직임</b>이 한국 종목에 어떤 영향을 줄지 미리 보여주는 곳입니다.
|
||||
예) <span style="color:#FFD740">엔비디아가 어제 +5% 올랐다 → SK하이닉스도 오를 가능성 ↑</span>
|
||||
<br>
|
||||
매일 한국 시장 시작 전(KST 08:00)에 미국 마감을 보고 자동 계산됩니다.
|
||||
</div>
|
||||
<div style="background:rgba(255,255,255,0.04);border-radius:8px;padding:10px 12px;margin-top:10px;font-size:11.5px;color:#90A4AE;line-height:1.7">
|
||||
<b style="color:#FFD740">📖 보는 법</b><br>
|
||||
① <b style="color:#CFD8DC">최신 동조 시그널</b>: 한국 종목별로 "오늘 미국 영향을 +/- 몇 점 받았는지". <b style="color:#69F0AE">+5 이상이면 강한 호재</b>, <b style="color:#FF8A80">-5 이하면 강한 악재</b>. 종합점수에 반영됨.<br>
|
||||
② <b style="color:#CFD8DC">섹터 ETF</b>: 미국 업종 ETF가 한국 어떤 업종에 영향 주는지. 예) 미국 반도체 ETF(SOXX) ↑ → 한국 반도체 ↑<br>
|
||||
③ <b style="color:#CFD8DC">KR↔US 페어</b>: 개별 종목 간 동조 (엔비디아↔SK하이닉스 등). β(민감도)가 클수록 강하게 따라감.
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// ── 0.5. 오늘 새벽 미국증시 브리핑 (핫/저조 + 관련 KOSPI) ──
|
||||
if(brief && !brief.err && ((brief.hot&&brief.hot.length)||(brief.cold&&brief.cold.length))){
|
||||
const aiC=(v)=> v==null?'#546E7A': v>=30?'#69F0AE': v<=-30?'#FF8A80':'#90A4AE';
|
||||
const krLine=(k)=>{
|
||||
const sc=(k.ai_score!=null)?`<span style="color:${aiC(k.ai_score)};font-family:'JetBrains Mono',monospace;font-size:11px">${k.ai_score>=0?'+':''}${k.ai_score}</span>`:'<span style="color:#546E7A;font-size:11px">점수없음</span>';
|
||||
const rec=(k.recommendation&&k.recommendation!=='-')?`<span style="color:${RCOL[k.recommendation]||'#78909C'};font-size:10px;font-weight:600">${esc(k.recommendation)}</span>`:'';
|
||||
return `<div data-stock="${esc(k.kr_code)}" data-stock-name="${esc(k.kr_name)}" style="display:flex;gap:6px;align-items:center;padding:3px 0;cursor:pointer">
|
||||
<span style="color:#CFD8DC;font-size:11.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(k.kr_name)}</span>${sc}${rec}</div>`;
|
||||
};
|
||||
const stockCard=(s,isHot)=>{
|
||||
const c=isHot?'#69F0AE':'#FF8A80';
|
||||
const krs=(s.related_kr||[]).map(krLine).join('')||'<span style="color:#546E7A;font-size:11px">연결된 한국 종목 없음</span>';
|
||||
return `<div style="background:rgba(255,255,255,0.03);border:1px solid ${c}33;border-radius:10px;padding:10px 12px;margin-bottom:8px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:8px">
|
||||
<span style="color:#E0E0E0;font-weight:700;font-size:12.5px">${esc(s.label)}</span>
|
||||
<span style="color:${c};font-weight:700;font-family:'JetBrains Mono',monospace;font-size:14px;white-space:nowrap">${s.change_pct>=0?'+':''}${s.change_pct}%</span>
|
||||
</div>
|
||||
<div style="border-top:1px solid rgba(255,255,255,0.05);padding-top:5px">
|
||||
<div style="font-size:10px;color:#607D8B;margin-bottom:2px">관련 KOSPI 종목 (AI점수 높은 순)</div>${krs}
|
||||
</div></div>`;
|
||||
};
|
||||
h += `<div class="panel"><h3>🌙 오늘 새벽 미국증시 (${esc(brief.trade_date||'')} 기준)</h3>
|
||||
<div style="font-size:11.5px;color:#90A4AE;margin-bottom:14px;line-height:1.6">간밤 미국에서 <b style="color:#69F0AE">오른 종목</b>·<b style="color:#FF8A80">내린 종목</b>과, 같이 움직이는 한국(KOSPI) 관련주입니다. 관련주의 <b style="color:#FFD740">AI점수가 높으면(+30↑)</b> 오늘 주목해볼 만합니다.</div>
|
||||
<div class="usbrief-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||||
<div><div style="color:#69F0AE;font-weight:700;font-size:12px;margin-bottom:8px">🔥 어젯밤 강했던 종목</div>
|
||||
${(brief.hot||[]).map(s=>stockCard(s,true)).join('')||'<div style="color:#546E7A;font-size:11px">데이터 없음</div>'}</div>
|
||||
<div><div style="color:#FF8A80;font-weight:700;font-size:12px;margin-bottom:8px">❄️ 어젯밤 약했던 종목</div>
|
||||
${(brief.cold||[]).map(s=>stockCard(s,false)).join('')||'<div style="color:#546E7A;font-size:11px">데이터 없음</div>'}</div>
|
||||
</div>`;
|
||||
if(brief.sector_heat&&brief.sector_heat.length){
|
||||
h += `<div style="margin-top:14px"><div style="font-size:11px;color:#607D8B;margin-bottom:6px">미국 업종 ETF 등락 — 한국 같은 업종에 영향</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px">`;
|
||||
brief.sector_heat.forEach(e=>{
|
||||
const c=e.change_pct>=0?'#69F0AE':'#FF8A80';
|
||||
h += `<span style="background:${c}1a;border:1px solid ${c}40;border-radius:6px;padding:3px 9px;font-size:10.5px;color:#CFD8DC">${esc(e.label)} <b style="color:${c}">${e.change_pct>=0?'+':''}${e.change_pct}%</b></span>`;
|
||||
});
|
||||
h += `</div></div>`;
|
||||
}
|
||||
h += `</div>`;
|
||||
}
|
||||
|
||||
// ── 1. 최신 동조 시그널 (가장 중요 — 맨 위로) ──
|
||||
h += `<div class="panel"><h3>📡 오늘의 미국→한국 영향 (${Array.isArray(signals)?signals.length:0}개 종목)</h3>`;
|
||||
h += `<div style="font-size:11px;color:#90A4AE;margin-bottom:12px">
|
||||
🟢 양수(+) = 미국 영향으로 <b style="color:#69F0AE">올라갈 압력</b> · 🔴 음수(-) = <b style="color:#FF8A80">내려갈 압력</b><br>
|
||||
"섹터"는 미국 업종 ETF, "페어"는 짝지어진 미국 개별주의 영향. 둘을 합친 게 총점.
|
||||
</div>`;
|
||||
if(Array.isArray(signals) && signals.length){
|
||||
h += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:8px">`;
|
||||
// 절대값 큰 순으로 정렬
|
||||
const sorted = signals.slice().sort((a,b)=>Math.abs(+b.total_adj||0) - Math.abs(+a.total_adj||0));
|
||||
sorted.slice(0,60).forEach(s=>{
|
||||
// 강도 그룹별로 분류
|
||||
const strong = sorted.filter(s=>Math.abs(+s.total_adj||0) >= 5);
|
||||
const medium = sorted.filter(s=>{ const a=Math.abs(+s.total_adj||0); return a>=2 && a<5; });
|
||||
const weak = sorted.filter(s=>Math.abs(+s.total_adj||0) < 2);
|
||||
const renderCard = (s)=>{
|
||||
const total = +s.total_adj || 0;
|
||||
const sec = +s.sector_adj || 0;
|
||||
const pair = +s.pair_adj || 0;
|
||||
const c = total>0?'#69F0AE':total<0?'#FF8A80':'#90A4AE';
|
||||
h += `<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.05);border-radius:10px;padding:10px 12px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span style="color:#CFD8DC;font-family:'JetBrains Mono',monospace;font-weight:700">${esc(s.kr_code||'-')}</span>
|
||||
<span style="color:${c};font-weight:700;font-family:'JetBrains Mono',monospace">${total>0?'+':''}${total.toFixed(2)}</span>
|
||||
const arrow = total>=5?'🚀':total<=-5?'⚠️':total>0?'▲':total<0?'▼':'·';
|
||||
const krName = esc(s.kr_name || s.kr_code || '-');
|
||||
// contributing_pairs에서 어떤 미국 종목이 주된 원인인지 표시
|
||||
let topReason = '';
|
||||
try{
|
||||
const cp = s.contributing_pairs;
|
||||
const pairs = (cp && cp.pairs) ? cp.pairs : (Array.isArray(cp)?cp:[]);
|
||||
if(pairs && pairs.length){
|
||||
const top = pairs.slice().sort((a,b)=>Math.abs(+b.contribution||0)-Math.abs(+a.contribution||0))[0];
|
||||
if(top){
|
||||
const usK = usName(top.us) || '';
|
||||
topReason = `${esc(top.us)}${usK?`(${esc(usK)})`:''} ${(+top.pct||0)>=0?'+':''}${(+top.pct||0).toFixed(1)}%`;
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
return `<div style="background:rgba(255,255,255,0.03);border:1px solid ${c}33;border-radius:10px;padding:12px 14px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="color:#CFD8DC;font-weight:700;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${krName}</div>
|
||||
<div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:#546E7A">${esc(s.kr_code||'')}</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div style="color:${c};font-weight:700;font-family:'JetBrains Mono',monospace;font-size:18px">${arrow} ${total>0?'+':''}${total.toFixed(1)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#90A4AE;margin-top:4px;display:flex;gap:10px">
|
||||
<span>섹터 ${sec>=0?'+':''}${sec.toFixed(2)}</span>
|
||||
<span>페어 ${pair>=0?'+':''}${pair.toFixed(2)}</span>
|
||||
<div style="font-size:10.5px;color:#78909C;margin-top:6px;display:flex;gap:10px;flex-wrap:wrap">
|
||||
<span title="섹터 ETF 영향">섹터 <span style="color:${sec>=0?'#69F0AE':'#FF8A80'}">${sec>=0?'+':''}${sec.toFixed(1)}</span></span>
|
||||
<span title="개별 페어 영향">페어 <span style="color:${pair>=0?'#69F0AE':'#FF8A80'}">${pair>=0?'+':''}${pair.toFixed(1)}</span></span>
|
||||
</div>
|
||||
${topReason?`<div style="font-size:10px;color:#90A4AE;margin-top:5px;border-top:1px solid rgba(255,255,255,0.05);padding-top:5px">📌 주원인: ${topReason}</div>`:''}
|
||||
</div>`;
|
||||
};
|
||||
const renderGroup = (title, color, arr) => {
|
||||
if(!arr.length) return '';
|
||||
return `<div style="margin-bottom:14px">
|
||||
<div style="font-size:12px;color:${color};font-weight:700;margin-bottom:8px">${title} <span style="color:#546E7A;font-weight:400">${arr.length}개</span></div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">
|
||||
${arr.slice(0,40).map(renderCard).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
h += `</div>`;
|
||||
} else h += `<div class="empty">시그널 없음 (아직 계산 전)</div>`;
|
||||
};
|
||||
h += renderGroup('🔥 강한 영향 (|점수| ≥ 5)', '#FFD740', strong);
|
||||
h += renderGroup('➡️ 보통 영향 (2 ~ 5)', '#40C4FF', medium);
|
||||
if(weak.length && (strong.length + medium.length) < 5){
|
||||
h += renderGroup('약한 영향 (< 2)', '#78909C', weak.slice(0,20));
|
||||
}
|
||||
} else h += `<div class="empty">시그널 없음 — 매일 08:00에 자동 계산됨. 처음 배포 직후라면 아직 생성 전입니다.</div>`;
|
||||
h += `</div>`;
|
||||
|
||||
// ── 2. 섹터 ETF (어떤 업종이 어떻게 매칭되는지) ──
|
||||
h += `<div class="grid2" style="margin-top:14px">`;
|
||||
h += `<div class="panel"><h3>🏭 미국 섹터 ETF → 한국 업종 매칭 (${Array.isArray(etfs)?etfs.length:0})</h3>`;
|
||||
h += `<div style="font-size:11px;color:#90A4AE;margin-bottom:10px">미국 ETF가 오르내리면 매칭된 한국 업종도 같이 영향받습니다.</div>`;
|
||||
if(Array.isArray(etfs) && etfs.length){
|
||||
h += `<table style="width:100%;font-size:11.5px;border-collapse:separate;border-spacing:0 3px">
|
||||
<thead><tr style="color:#90A4AE;font-size:10.5px">
|
||||
<th style="text-align:left;padding:4px 6px">미국 ETF</th>
|
||||
<th style="text-align:left">설명</th>
|
||||
<th style="text-align:left">한국 업종</th>
|
||||
</tr></thead><tbody>`;
|
||||
etfs.slice(0,30).forEach(e=>{
|
||||
const kw = Array.isArray(e.sector_keywords)?e.sector_keywords.join(' · '):'';
|
||||
const krDesc = usName(e.etf_ticker) || esc(e.description||'-');
|
||||
h += `<tr style="background:rgba(255,255,255,0.02)">
|
||||
<td style="padding:7px 8px;color:#69F0AE;font-family:'JetBrains Mono',monospace;font-weight:700">${esc(e.etf_ticker)}</td>
|
||||
<td style="color:#CFD8DC">${esc(krDesc)}</td>
|
||||
<td style="color:#FFD740;font-size:11px">${esc(kw)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
h += `</tbody></table>`;
|
||||
} else h += `<div class="empty">데이터 없음</div>`;
|
||||
h += `</div>`;
|
||||
|
||||
// ── 3. 개별 페어 ──
|
||||
h += `<div class="panel"><h3>🔗 개별 종목 짝짓기 (${Array.isArray(pairs)?pairs.length:0})</h3>`;
|
||||
h += `<div style="font-size:11px;color:#90A4AE;margin-bottom:10px">
|
||||
<b>민감도(β)</b>: 미국 종목이 1% 오를 때 한국 종목이 몇% 따라가는지<br>
|
||||
<b>상관계수(ρ)</b>: 같이 움직이는 정도. 0.7↑면 매우 강함, 0.5↑면 의미 있음, 0.3↓면 약함
|
||||
</div>`;
|
||||
if(Array.isArray(pairs) && pairs.length){
|
||||
// 한국명 매칭되는 페어를 우선 정렬
|
||||
const sorted = pairs.slice().sort((a,b)=>{
|
||||
const corA = Math.abs(+a.correlation_60d||0);
|
||||
const corB = Math.abs(+b.correlation_60d||0);
|
||||
return corB - corA;
|
||||
});
|
||||
h += `<table style="width:100%;font-size:11.5px;border-collapse:separate;border-spacing:0 3px">
|
||||
<thead><tr style="color:#90A4AE;font-size:10.5px">
|
||||
<th style="text-align:left;padding:4px 6px">미국</th>
|
||||
<th style="text-align:left">한국</th>
|
||||
<th title="민감도: 미국 +1%일 때 한국 +β%" style="text-align:center">민감도(β)</th>
|
||||
<th title="상관: 같이 움직이는 정도 (0~1)" style="text-align:center">상관(ρ)</th>
|
||||
</tr></thead><tbody>`;
|
||||
sorted.slice(0,40).forEach(p=>{
|
||||
const beta = p.beta_60d!=null?(+p.beta_60d).toFixed(2):'-';
|
||||
const corr = p.correlation_60d!=null?(+p.correlation_60d).toFixed(2):'-';
|
||||
const corrN = +p.correlation_60d || 0;
|
||||
const corrC = Math.abs(corrN)>=0.7?'#69F0AE':Math.abs(corrN)>=0.5?'#FFD740':Math.abs(corrN)>=0.3?'#90A4AE':'#546E7A';
|
||||
const usK = usName(p.us_ticker) || '';
|
||||
const krN = esc(p.kr_name || '');
|
||||
h += `<tr style="background:rgba(255,255,255,0.02)">
|
||||
<td style="padding:6px 8px;color:#69F0AE;font-family:'JetBrains Mono',monospace">${esc(p.us_ticker)}${usK?`<span style="color:#90A4AE;font-family:'Pretendard',sans-serif;font-size:10px;margin-left:4px">${esc(usK)}</span>`:''}</td>
|
||||
<td style="color:#CFD8DC">${krN || `<span style="font-family:'JetBrains Mono',monospace">${esc(p.kr_code)}</span>`}</td>
|
||||
<td style="text-align:center;font-family:'JetBrains Mono',monospace;color:#FFD740">${beta}</td>
|
||||
<td style="text-align:center;font-family:'JetBrains Mono',monospace;color:${corrC};font-weight:700">${corr}</td>
|
||||
</tr>`;
|
||||
});
|
||||
h += `</tbody></table>`;
|
||||
} else h += `<div class="empty">페어 없음</div>`;
|
||||
h += `</div></div>`;
|
||||
|
||||
el.innerHTML = h;
|
||||
}catch(e){el.innerHTML=`<div class="empty">오류: ${esc(String(e))}</div>`}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user