score_compose 재설계 — 다축 복구 + 재료(catalyst) 축 신설¶
작성: 2026-05-29 (S329) 배경: 010170 대한광통신이 D7 1위로 선정됐으나 -15.76% 급락. 원인 추적 결과 score 4축 중 RS 1축만 작동 + 재료 축 부재. Status: 📋 설계 — 구현 전 PM 승인 대기
0. 실측 진단 (20260527 run, 80후보)¶
4축 작동률¶
| score축 | 비-0 비율 | 연결 도구 | 원인 |
|---|---|---|---|
| stock_rs_z | 80/80 (100%) | D3 | ✅ 유일 작동 |
| accel_z | 0/80 | D2 차트 | OHLCV 컬럼 대소문자 (데이터 충분, 코드가 못 읽음) |
| ofm_cvd_z_z | 0/80 | D5 수급/OF | 30분봉 컬럼 대소문자 (데이터 충분, 코드가 못 읽음) |
| rs_vs_theme_z | 0/80 | D4 테마 | 코드 미완성 (placeholder) |
| (재료) | 축 자체 없음 | — | discover에 재료 입력 경로 부재 |
→ final_score ≈ stock_rs_z 단일. 010170은 1년 +3517% 폭등 → RS z=4.735 극단 → 1위. 반도체 대장 SK하이닉스 RS z=0.728 → 12위.
차트·수급 결손 근본 원인 = 컬럼 대소문자 (실측 확정 2026-05-29)¶
data/backtest/s303/ohlcv/010170.parquet= 1197행 (2021~2026), 컬럼['Open','High','Low','Close','Volume','Change']대문자chart_indicators.py:29if "close" not in ohlcv.columns→ 소문자 못 찾아trend_insufficient외 6블록 전부 결손data/minute_charts/30min/010170.parquet= 900행, 컬럼['Open','High','Low','Close','Volume']대문자of_indicators.py:12{"close","volume"}.issubset(bars.columns)→ 소문자 못 찾아 cvd/bvc/pressure/absorption 전부 결손- → 데이터 부족이 아니라 컬럼명 한 글자.
position_status.py만 S329에서_col()로 고쳤고chart_indicators / of_indicators / stock_rs / theme_rs미수정 (followup A). - 결론: score_compose 수정만으로 안 됨. 컬럼 정규화(A)가 선행돼야 accel_z·ofm_cvd_z가 실값을 가짐. "모집단 제외" 우회는 임시방편일 뿐, 차트/수급을 실제로 보려면 결손 자체를 제거해야 한다.
재료 진단 (장마감 리포트는 이미 경고했음)¶
docs/daily_reports/20260526_post_market.md UNIT5/6이 010170을 정확히 판정:
- 재료강도 soft (테마 편승), verdict WATCH
- "당일 직접 catalyst 검색 미발견, 5/11 +25%·5/13 -15% 고변동성", "1년 +3517% 누적 후 모멘텀 추격 구분 필요"
반면 같은 표에서 삼성전기(MLCC)·LG이노텍·DB하이텍 = hard·TRACK. → discover가 이 리포트를 입력으로 안 받아 soft·WATCH 경고를 무시하고 RS만으로 선정.
1. score_compose 버그 수정 (4개)¶
대상: scripts/discover/indicators/score_compose.py
버그1 — rs_vs_theme 미완성 (line 14/33/39)¶
현행: rs_vs_theme = 0 하드코딩 + _z([0], 0) → 무조건 0.
수정: rs_vs_theme = stock_rs - theme_rs 실제 계산 → 전체 후보 모집단 z.
(종목이 자기 테마보다 강한가 = 테마 내 주도력)
버그2 — 펀더멘탈 곱셈 소멸 (line 45)¶
현행: final_score = raw_score * ea_w → RS 단일 시 stock_rs_z × 0.5~1.0, D6 NEGATIVE도 양수 가중이라 순위 못 바꿈.
수정: 펀더멘탈을 합산 축으로. ea_contribution = z(ea_z) 추가하거나, D6 verdict 기반 가감점(POS +α / NEG −α). 곱셈 가중 폐기.
버그3 — 결손 0치환 → 모집단 오염 (line 16/18/34/35)¶
현행: accel = d2.get("accel_ratio") or 0 — UNAVAILABLE이 0으로 모집단에 섞여 평균/표준편차 왜곡.
수정: 결손은 None → z 모집단에서 제외. 결손 종목의 해당 축 기여=0(중립)이되, 평균은 가용값만으로 계산. (verdict UNAVAILABLE/UNKNOWN = 제외)
버그4 — verdict 다수결 게이트 부재¶
현행: RS 단독 극단으로 통과 가능.
수정: score 산출 후 게이트 — 예: POSITIVE verdict 2축 미만 AND 단일축 z>3 = 과열 의심 플래그 + score 감점 또는 D7 selector에 경고 전달. (v6 백테스트 multi-axis 동시충족 가정 복원)
2. 재료(catalyst) 축 신설 (5번째 축)¶
데이터 소스 (살아있는 리포트)¶
| 파일 | 사용 UNIT | 추출 |
|---|---|---|
docs/daily_reports/{date}_post_market.md |
UNIT5 상한가/급등 WHY | 종목별 |재료강도|테마|verdict| 표 |
| UNIT6 신규 발견 | 종목별 |유형|태그|WHY|테마| 표 | |
| UNIT4 테마 강도 랭킹 | 테마별 등급(LEADING/STRONG/COOLING) | |
docs/evening_reports/{date}_evening.md |
글로벌→한국 테마 파급 | 매크로 연결 재료 |
폐기: data/news_curator/* (구 리포팅 시스템, 미사용 — PM 확인).
파싱 규칙 (마크다운 표)¶
UNIT5 표 헤더: | 종목 | 등락% | WHY | 재료 | 테마 | verdict |
- 종목명에서 (코드) 정규식 추출 → 후보 코드 매칭
- 재료강도: hard / soft / none (괄호 주석 무시, 첫 단어)
- verdict: TRACK / WATCH / NOISE
- UNIT6 태그: [NEW] / [TRACKED] / [추정] / [검증]
재료 점수화 (catalyst_z)¶
| 신호 | 점수 |
|---|---|
| 재료강도 hard | +1.0 |
| 재료강도 soft | +0.3 |
| 재료강도 none | -0.5 |
| verdict TRACK | +0.5 |
| verdict WATCH | 0 |
| verdict NOISE | -1.0 |
| "catalyst 미발견" + 급등누적(1년 +N%) | 과열 페널티 -1.0 |
| 리포트 미등장 (UNIT5/6에 없음) | 0 (중립, 결손 아님 — 급등 안 한 종목) |
→ 후보 80개의 catalyst raw score → 모집단 z = catalyst_z.
→ 검증 효과: 010170 = soft(+0.3) + WATCH(0) + 과열페널티(-1.0) = -0.7 → z 음수 → 감점. 삼성전기 = hard(+1.0)+TRACK(+0.5) = +1.5 → z 양수 → 가점.
최종 산식 (5축)¶
final_score = z(rs_vs_theme) + z(stock_rs) + z(accel) + z(ofm_cvd) + z(catalyst) + ea_contribution
(모든 z는 결손 제외 모집단)
+ 게이트: POSITIVE 2축 미만 → 과열 플래그
3. 구현 범위 (PM 승인 후)¶
| Step | 파일 | 변경 |
|---|---|---|
| 0 (선행·필수) | chart_indicators.py / of_indicators.py / stock_rs.py / theme_rs.py |
컬럼 대소문자 정규화 — _col() 헬퍼(position_status.py 기존 패턴) 일괄 적용 또는 빌더 진입 시 df.columns = [c.lower() for c in df.columns]. → accel_z·ofm_cvd_z 실값 복구. 이게 안 되면 차트/수급은 영원히 0. |
| 1 | scripts/discover/indicators/catalyst_parser.py (신규) |
post_market.md UNIT5/6 표 파싱 → 종목별 {강도,verdict,과열플래그} |
| 2 | scripts/discover/indicators/score_compose.py |
버그1-4 수정 + catalyst_z 5번째 축 + 게이트 |
| 3 | scripts/discover/builders/build_d7_input.py |
catalyst 데이터를 후보 raw_metrics에 포함 |
| 4 | .claude/agents/discover/portfolio-selector.md |
과열 플래그·재료 verdict 해석 규칙 추가 |
| 5 | 검증 | 20260527 run 재실행 → 010170 순위 하락 + 삼성전기/LG이노텍 상승 + accel_z/ofm_cvd_z 비-0 확인 |
Step 0이 살아나면 실제로 보게 되는 것 (010170 차트·수급)¶
- 차트(accel_z, D2): 1년 +3517% 급등 → ma slope_z 극단·고변동성·52w high 근접.
accel_ratio(최근 가속도)가 음수면 "급등 후 꺾임" → 과열 차트 신호. 지금은 컬럼 결손으로 이 신호 전체가 0. - 수급(ofm_cvd_z, D5): 30분봉 CVD(매수-매도 누적). 외인·기관 매도 우위면 cvd_z 음수 → "가격은 올랐는데 매도 우위" 다이버전스. 010170은 5/12 외인 277만주 순매도였으나 D5 결손으로 못 봄.
- → 차트·수급이 살아나면 010170은 RS만 양수, accel/ofm은 음수(과열·매도) → 다축 합산 시 RS 단독 우위 해소.
의존: Step 0(컬럼 정규화)이 전제. Step 0 없이 score_compose만 고치면 버그1·2·4·재료축만 효과(여전히 차트·수급은 못 봄). PM 지적대로 차트·수급을 "실제로 보려면" Step 0이 필수.
4. 기대 효과 (010170 케이스 역산)¶
현행: 010170 final=3.788(RS단독 1위) → 선정 → -15.76%. 수정 후 예상: - accel/ofm 결손 제외로 RS 가중 상대 하락 - catalyst_z = soft+WATCH+과열 = 음수 → 추가 감점 - 게이트: D2 UNAVAIL·D3 NEUTRAL·D5 UNKNOWN → POSITIVE 1축(D4)뿐 → 과열 플래그 → 010170 순위 급락, hard·TRACK 반도체주(삼성전기·LG이노텍·DB하이텍) 상위 복귀.