feat(stocks): 판정 기준 정밀화 — 레버리지 가드·실측 YoY·PER 기둥 (v2.2.212)

퀀트 실증 근거(F-Score 류 품질+가격 결합) 기반 기준 강화:
- ROE: 부채비율 ≤150% 레버리지 가드 — 빚으로 부풀린 ROE 배제 (듀폰분해).
- 성장성: Naver 연간 다년치에서 매출/영업이익 YoY 실측 계산을 1순위로
  (매출 ≥10% 또는 영업이익 ≥15%). 마진 수준·상장연차는 YoY 미확보 시 폴백.
- 안정성: 부채비율 ≤100% 가드 추가 — 유보율은 자본금 왜곡되는 약한 지표.
- PER ≤12배 키워드 신설(보유 데이터인데 미사용이던 가격 매력 축),
  저평가우량주 우선순위에 PBR 다음으로 편입.
- naverFundamentals: revenueGrowthYoY/opProfitGrowthYoY 추출 추가.
- 테스트 12건 (기존 사용자 패턴 8건 유지 + 신규 규칙 4건).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 19:15:21 +09:00
parent ef3628c6eb
commit cbc2558550
5 changed files with 110 additions and 22 deletions
+36
View File
@@ -90,4 +90,40 @@ describe('stocks criteriaEval', () => {
expect(marketCapEok('1조 2,000억')).toBe(12000);
expect(marketCapEok('2조')).toBe(20000);
});
// ── v2.2.212 정밀화 규칙 ──────────────────────────────────────────────
test('ROE 레버리지 가드 — 부채비율 200% 면 ROE 15% 라도 미통과', () => {
const stock: Stock = { : '레버리지주', : '000010', 'ROE(25E)': '15%' };
const evHigh = evaluateCriteria(stock, { symbol: '000010', roe: 15, debtRatio: 200 }, NOW);
expect(evHigh.results.find(r => r.keyword === 'ROE')!.state).toBe('fail');
const evLow = evaluateCriteria(stock, { symbol: '000010', roe: 15, debtRatio: 80 }, NOW);
expect(evLow.results.find(r => r.keyword === 'ROE')!.state).toBe('pass');
});
test('성장성 — 실측 YoY 가 있으면 마진 폴백 대신 YoY 로 판정', () => {
const stock: Stock = { : '진짜성장주', : '000011', '영업이익률(25E)': '5%' };
// 마진 5% (폴백이면 fail) 이지만 매출 YoY 25% → pass
const grow = evaluateCriteria(stock, { symbol: '000011', operatingMargin: 5, revenueGrowthYoY: 25 }, NOW);
expect(grow.results.find(r => r.keyword === '성장성')!.state).toBe('pass');
// 마진 18% (폴백이면 pass) 이지만 매출 YoY -5%·영업이익 YoY 3% → 실측 우선으로 fail
const noGrow = evaluateCriteria(stock, { symbol: '000011', operatingMargin: 18, revenueGrowthYoY: -5, opProfitGrowthYoY: 3 }, NOW);
expect(noGrow.results.find(r => r.keyword === '성장성')!.state).toBe('fail');
});
test('안정성 부채 가드 — 유보율·시총 통과여도 부채비율 150% 면 미통과', () => {
const stock: Stock = { : '부채대형주', : '000012', : '4,000%', : '1조' };
const ev = evaluateCriteria(stock, { symbol: '000012', retentionRatio: 4000, marketCapEok: 10000, debtRatio: 150 }, NOW);
expect(ev.results.find(r => r.keyword === '안정성')!.state).toBe('fail');
});
test('PER 키워드 — ≤12배 통과, 저평가우량주 우선순위에 포함', () => {
const { ev, verdict } = judge({
: '저PER주', : '000013', : '저평가우량주',
'ROE(25E)': '11%', 'PER(25E)': '8', PBR: '0.9', : '1,200%',
});
expect(ev.results.find(r => r.keyword === 'PER')!.state).toBe('pass');
// 통과: ROE, 유동성, PBR, PER → 우선순위 [PBR, PER, ROE, ...]
expect(verdict.text).toBe('충족 (PBR, PER, ROE)');
});
});