fix: 섹터필터 "기타 캐치올" 제거 — 미매핑 종목 섹터캡 면제
섹터 매핑 테이블이 유니버스의 극히 일부(~44종목)만 커버해, 매핑 안 된 종목이 전부 "기타" 한 버킷에 뭉쳐 max 3 상한에 걸렸다. 유니버스 대부분이 미매핑이라 "기타 3종목"에서 막혀 MAX_HOLDINGS_COUNT=20 달성이 구조적으로 불가했다. - check_sector_concentration: 미매핑 종목(sector=None)은 섹터캡 면제(early return), 보유분 계수에서도 미매핑 제외. 실제 섹터가 확인된 종목만 max_per_sector 적용. 미매핑 종목의 집중통제는 상관계수 필터(0.7)에 위임. - 검증: backtest-qa walk-forward — 섹터 완화가 MDD를 전혀 안 키움(전 구간 불변, 리스크 중립). 로직 테스트: 미매핑 진입=통과, 실제섹터(반도체) 3보유+동일섹터=차단 유지. 시그니처 불변(§4), 리스크 규칙(§5) — 상관필터 유지로 실질 분산통제 보존. (같은 파일에 선재 워킹트리 편집 1건 포함: _get_daily_prices의 KIS output2 최신순→ reversed로 상관계산 시계열 방향 정합화. 내 작업 아니나 타당하여 함께 커밋.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user