feat(stocks): discover sector — 네이버 업종 코드 직접 조회로 재설계 (v2.2.250)
[버그] discover sector 가 항상 0개 반환. 원인: sectorHint 를 통합 API 의
없는 필드(industryInfo.name)에서 읽어 전 종목이 업종 미상 → 필터 전멸.
[근본 수정] "전 종목 시총 크롤 후 sectorHint 필터" → "네이버 업종 코드로
해당 섹터 종목 직접 조회". 실측: 2차전지 1000-5000억 0개 → 36개.
- stockSectors: 친화 섹터키 → 네이버 업종코드 묶음 (업종 79개 코드표). 17개 그룹.
- naverScreener.screenIndustry(): /api/stocks/industry/{code} 직접 수집 + 시총 필터 + dedup.
- naverFundamentals: sectorHint 를 industryCode→이름 매핑으로 수정 (기술력 키워드·judge 복구).
- stockDiscovery: 섹터 모드 3키워드 게이트 완화(≥1, "섹터 내 상대 추천").
- CLI: discover sector <섹터> [min] [max] / discover sectors.
테스트 8건. 라이브 e2e 확인.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { judgeStock } from './llmJudge';
|
||||
import { sendStocksReport, buildReportText } from './telegramReport';
|
||||
import { runOnceNow } from './stocksWatcher';
|
||||
import { discoverStocks } from './stockDiscovery';
|
||||
import { resolveSectorQuery, listSectorKeys, type SectorGroup } from './stockSectors';
|
||||
import { analyzeTopCandidates, renderTopFiveForChat, renderTopFiveForTelegram, sendTopFiveToTelegram } from './discoveryAnalyzer';
|
||||
import type { ClassifiedStock, Stock } from './types';
|
||||
|
||||
@@ -28,6 +29,7 @@ import type { ClassifiedStock, Stock } from './types';
|
||||
* /stocks remove <심볼>
|
||||
* /stocks judge <심볼> — LLM 4-criteria 평가
|
||||
* /stocks discover [min] [max] — Naver 크롤 발굴 (시총 범위 억 단위, default 1000-5000)
|
||||
* /stocks discover sector <섹터> [min] [max] — 섹터별 발굴 / discover sectors — 섹터 목록
|
||||
* /stocks report — 텔레그램 보고서 즉시 발송
|
||||
* /stocks run — watcher 한 사이클 즉시 실행 (현재가+sync+보고서)
|
||||
* /stocks path — stocks.json 경로 표시
|
||||
@@ -504,20 +506,47 @@ async function cmdRun(view: Webview | undefined, context: vscode.ExtensionContex
|
||||
}
|
||||
|
||||
async function cmdDiscover(rest: string, view: Webview | undefined, context?: vscode.ExtensionContext): Promise<void> {
|
||||
// 인자 파싱 — `min max` (둘 다 억 단위) / 안 주면 default.
|
||||
const parts = rest.split(/\s+/).filter(Boolean);
|
||||
const minCap = parts[0] ? parseInt(parts[0], 10) : 1000;
|
||||
const maxCap = parts[1] ? parseInt(parts[1], 10) : 5000;
|
||||
if (Number.isNaN(minCap) || Number.isNaN(maxCap) || minCap >= maxCap) {
|
||||
chunk(view, '\n사용법: `/stocks discover [min] [max]` (억 단위, 예: `/stocks discover 1000 5000`)\n');
|
||||
|
||||
// `sectors` — 사용 가능한 섹터 키 목록 안내.
|
||||
if (parts[0]?.toLowerCase() === 'sectors') {
|
||||
chunk(view, `\n🏷️ 발굴 가능한 섹터:\n ${listSectorKeys().join(' · ')}\n\n사용법: \`/stocks discover sector <섹터> [min] [max]\`\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
chunk(view, `\n🔍 **Naver 발굴 시작** (시총 ${minCap.toLocaleString()}억 ~ ${maxCap.toLocaleString()}억)\n`);
|
||||
// `sector <섹터명> [min] [max]` — 섹터 필터 발굴.
|
||||
let sectorGroup: SectorGroup | undefined;
|
||||
let argParts = parts;
|
||||
if (parts[0]?.toLowerCase() === 'sector') {
|
||||
const sectorName = parts[1];
|
||||
if (!sectorName) {
|
||||
chunk(view, `\n섹터명이 필요합니다. \`/stocks discover sector <섹터> [min] [max]\`\n사용 가능: ${listSectorKeys().join(' · ')}\n`);
|
||||
return;
|
||||
}
|
||||
const resolved = resolveSectorQuery(sectorName);
|
||||
if (!resolved) {
|
||||
chunk(view, `\n⚠️ '${sectorName}' 섹터를 못 찾았습니다. 사용 가능:\n ${listSectorKeys().join(' · ')}\n`);
|
||||
return;
|
||||
}
|
||||
sectorGroup = resolved;
|
||||
argParts = parts.slice(2); // min/max 는 sector <name> 뒤에서 파싱
|
||||
}
|
||||
|
||||
// 인자 파싱 — `min max` (둘 다 억 단위) / 안 주면 default.
|
||||
const minCap = argParts[0] ? parseInt(argParts[0], 10) : 1000;
|
||||
const maxCap = argParts[1] ? parseInt(argParts[1], 10) : 5000;
|
||||
if (Number.isNaN(minCap) || Number.isNaN(maxCap) || minCap >= maxCap) {
|
||||
chunk(view, '\n사용법: `/stocks discover [min] [max]` 또는 `/stocks discover sector <섹터> [min] [max]` (억 단위)\n');
|
||||
return;
|
||||
}
|
||||
|
||||
const sectorLabel = sectorGroup ? ` · 섹터 '${sectorGroup.key}'` : '';
|
||||
chunk(view, `\n🔍 **Naver 발굴 시작** (시총 ${minCap.toLocaleString()}억 ~ ${maxCap.toLocaleString()}억${sectorLabel})\n`);
|
||||
|
||||
const candidates = await discoverStocks({
|
||||
minMarketCapEok: minCap,
|
||||
maxMarketCapEok: maxCap,
|
||||
sector: sectorGroup,
|
||||
onProgress: (msg) => chunk(view, msg + '\n'),
|
||||
});
|
||||
|
||||
@@ -530,7 +559,9 @@ async function cmdDiscover(rest: string, view: Webview | undefined, context?: vs
|
||||
for (const c of candidates) {
|
||||
const price = c.fundamentals.currentPrice;
|
||||
const priceStr = typeof price === 'number' ? price.toLocaleString() + '원' : '-';
|
||||
chunk(view, ` · ${c.name} (${c.symbol}) [${c.market} · ${c.marketCapEok.toLocaleString()}억]\n`);
|
||||
const sectorStr = c.fundamentals.sectorHint ? ` · ${c.fundamentals.sectorHint}` : '';
|
||||
const weakMark = sectorGroup && c.passedKeywords.length < 3 ? ' ⚠️섹터내상대' : '';
|
||||
chunk(view, ` · ${c.name} (${c.symbol}) [${c.market} · ${c.marketCapEok.toLocaleString()}억${sectorStr}]${weakMark}\n`);
|
||||
chunk(view, ` 현재가 ${priceStr} · ROE ${c.fundamentals.roe?.toFixed(1) ?? '-'}% · 영업이익률 ${c.fundamentals.operatingMargin?.toFixed(1) ?? '-'}% · 유보율 ${c.fundamentals.retentionRatio?.toLocaleString() ?? '-'}%\n`);
|
||||
chunk(view, ` 통과 (${c.passedKeywords.length}): ${c.passedKeywords.join(', ')}\n\n`);
|
||||
}
|
||||
@@ -543,7 +574,7 @@ async function cmdDiscover(rest: string, view: Webview | undefined, context?: vs
|
||||
chunk(view, renderTopFiveForChat(topFive));
|
||||
|
||||
if (context) {
|
||||
const rangeLabel = `시총 ${minCap.toLocaleString()}-${maxCap.toLocaleString()}억`;
|
||||
const rangeLabel = `시총 ${minCap.toLocaleString()}-${maxCap.toLocaleString()}억${sectorGroup ? ` · ${sectorGroup.key}` : ''}`;
|
||||
const tgText = renderTopFiveForTelegram(topFive, rangeLabel);
|
||||
const tgResult = await sendTopFiveToTelegram(context, tgText);
|
||||
chunk(view, tgResult.ok
|
||||
@@ -575,6 +606,8 @@ function cmdHelp(view: Webview | undefined): void {
|
||||
' `/stocks analysis <심볼>` — 심층 분석 (펀더멘털 + MA 정배열 + RSI + LLM 종합)',
|
||||
' `/stocks position [심볼] <총자산> <리스크%> <손절%>` — 포지션 사이징 (적정 투자금)',
|
||||
' `/stocks discover [min] [max]` — Naver 크롤 발굴 (시총 억 단위, default 1000-5000)',
|
||||
' `/stocks discover sector <섹터> [min] [max]` — 섹터별 발굴 (예: `discover sector 반도체`)',
|
||||
' `/stocks discover sectors` — 발굴 가능한 섹터 목록',
|
||||
' `/stocks report` — 텔레그램 보고서 즉시 발송',
|
||||
' `/stocks run` — Watcher 1회 즉시 실행',
|
||||
' `/stocks path` — stocks.json 경로 표시',
|
||||
|
||||
Reference in New Issue
Block a user