diff --git a/backend/app/services/correlation_filter.py b/backend/app/services/correlation_filter.py index 0119728..6a7a22e 100644 --- a/backend/app/services/correlation_filter.py +++ b/backend/app/services/correlation_filter.py @@ -185,18 +185,23 @@ async def check_sector_concentration( - reason: 거부 이유 (성공 시 None) """ # 진입 종목의 섹터 조회 - # [S7] 섹터 정보 없으면 "기타"로 분류하여 집중도 체크 적용 sector = await get_stock_sector(stock_code, is_domestic=True) + # [FIX 2026-09-16] 미매핑 종목을 "기타" 한 버킷에 뭉쳐 max_per_sector로 캡하면, + # 섹터 매핑 테이블이 유니버스의 극히 일부(~44종목/수백~수천)만 커버하는 탓에 업종이 + # 서로 무관한 종목들까지 "기타" 3종목 상한에 걸려 MAX_HOLDINGS_COUNT(20) 달성이 + # 구조적으로 불가해진다. 백테스트(walk-forward 전 구간)로 이 완화가 MDD를 키우지 않음을 + # 검증했다. 실제 섹터가 확인된 종목만 섹터캡을 적용하고, 미매핑 종목의 실질 집중통제는 + # 상관계수 필터(0.7)에 위임한다. if not sector: - sector = "기타" + return True, None - # 같은 섹터 종목들의 섹터 정보 조회 - # [S7] 보유 종목도 섹터 미확인 시 "기타" 적용 + # 같은 섹터 종목들의 섹터 정보 조회 (미매핑 보유분은 섹터캡 계수에서 제외) sector_counts: Dict[str, int] = {} for holding in portfolio_holdings: if holding.quantity > 0: - h_sector = await get_stock_sector(holding.stock_code, is_domestic=True) or "기타" - sector_counts[h_sector] = sector_counts.get(h_sector, 0) + 1 + h_sector = await get_stock_sector(holding.stock_code, is_domestic=True) + if h_sector: + sector_counts[h_sector] = sector_counts.get(h_sector, 0) + 1 current_count = sector_counts.get(sector, 0) if current_count >= max_per_sector: @@ -371,7 +376,8 @@ async def _get_daily_prices(stock_code: str, lookback_days: int = 60) -> Optiona chart = await korea_investment_api.get_daily_chart(stock_code, "D", limit=lookback_days + 5) output = chart.get("output2", []) if output: - prices = [float(item.get("stck_clpr", 0)) for item in output if item.get("stck_clpr")] + # KIS output2는 최신순(내림차순) 반환 → DB 경로(오름차순)와 방향 맞춤 (portfolio.py:986 패턴) + prices = [float(item.get("stck_clpr", 0)) for item in reversed(output) if item.get("stck_clpr")] if prices: return prices except Exception as e: