콘텐츠로 이동

S316 — 테마/종목 발굴을 위해 새로 작성한 모듈 정리

작성: 2026-05-25 (Mon) 23:16 범위: 2026-02-20 ~ 05-22 walk-forward 백테스트 e4 시스템에 S316 작업으로 추가된 모듈/함수


1. 모듈 추가/수정 한 눈에

파일 신규/수정 역할
scripts/backtest/e4_portfolio_walk.py 수정 e4 메인. 아래 함수 추가됨
scripts/backtest/e4_rank_sweep.py 신규 rank 1~5 단독/누적 알파 감쇠 측정기
data/backtest/e4_portfolio/_daily_theme_rs_byTheme.parquet 신규 캐시 테마 단위 일별 RS 시계열

재사용한 기존 자산 (재구현 안 함): - scripts/quant/earnings/e1_acceleration.pyea_panel.parquet (S312v2 산출) - scripts/quant/earnings/fnguide_loader.pyfnguide_fs.parquet (S311 백필 98종) - discovery/state_classifier.py + compute_order_flow/wyckoff/vsa_signals/volume_profile (e4가 기존부터 호출)


2. e4_portfolio_walk.py 신규 함수 (S316)

2-1. 매출 lookup (point-in-time)

build_revenue_yoy_lookup() -> dict
lookup_revenue_yoy(code, t, lookup) -> float | None
- 입력: data/dart_cache/fnguide_fs.parquet (S311 백필) - 산식: 분기 매출 / 4분기전 매출 - 1 - 가용 시점: 분기말 + 45일 보수 lag - 출력: 종목별 [(avail_from, yoy), ...] 시계열 - 커버리지: 83종 (universe 200 중)

2-2. 퀀트 lookup (point-in-time, S312v2 ea_panel 활용)

build_quant_lookup() -> dict
lookup_quant(code, t, lookup) -> dict | None
- 입력: data/backtest/e1/ea_panel.parquet (E1 모듈 산출) - 산식: 3 metric (revenue/op_income/net_income) × {g_curr (YoY 가속), ea_qoq (QoQ 가속)} 평균 - 가용 시점: 분기말 + 45일 lag - 출력: {stock_code: [(avail, {'g_avg', 'ea_qoq_avg'}), ...]} - 커버리지: 98종

2-3. 테마 leader 4기준 산식 (S314 일반화)

_stock_metrics_at(code, t, closes_daily, ohlcv_cache) -> dict
_score_theme_members_at(codes, t, ...) -> list[dict]
- 산식 출처: scripts/quant/themes/theme_leader_select.py (S314, 1시점 → t 인자화) - 4 기준 각 0~25: - cum_return : 60일 수익률 테마 내 percentile - downside_resilience : 60일 down-day 평균 손실 (작을수록 좋음) - turnover_concentration : 20일 평균 거래대금 percentile - reaction_speed : (ret_5d/5 - ret_60d/60) percentile

2-4. 통합 점수 풀 선정 (S316 23:00 PM 가르침)

select_daily_candidates_integrated(
    t, universe, theme_df, themes_map, closes_daily, ohlcv_cache,
    chart_state_df, revenue_lookup, quant_lookup,
    *, top_n=15, weights=None
) -> list[str]
- 6축 universe 내 percentile (각 0~1) → 가중 합 → 상위 N - 축: 1. theme_rs : 종목 소속 테마 중 최고 RS_60d 2. stock_rs : 종목 자체 60일 수익률 percentile 3. revenue : 매출 YoY (없으면 0.5 중간값) 4. quant : g_avg + ea_qoq 평균 (없으면 0.5) 5. chart : chart_score 6. turnover : 20일 거래대금 - 가중치: 균등 (각 1.0). 변경 가능. - 컷오프 X, 가중 합 정렬

2-5. 구 컷오프 선정 (보존, 비활성)

select_daily_candidates(..., use_integrated=True, ...)
- 기본은 통합 점수 호출. use_integrated=False로 구 방식 (테마 rank slice × leader rank slice) 사용 가능. - 구 방식 파라미터: theme_rank_start/end, leader_rank_start/end, require_revenue_positive, require_chart_positive

2-6. can_enter() — OF 게이트 (S316 21:13 PM 가르침)

can_enter(row) -> bool
- PM 가르침: "오른 종목 = 더 오를 가능성, 쉬는 종목 = 다시 오를 가능성. OF는 동력이 살아있는가 확인." - 상황 B (state=IMBALANCE_UP): not_exhausted AND cvd_alive - 상황 A (state=BALANCE + best_rs_60d > 0): not_exhausted AND cvd_alive - 공통 차단: of_avoid, chart_score ≤ -0.3, best_state == DEAD - 현재 상태: buy_intent 폐기됨 (PM 가르침에 명시 없어서). 이로 인해 "매수 우위 약한" 종목도 통과 가능 (현대건설 사례)

2-7. compute_theme_rs_for_dates() — 출력 확장

compute_theme_rs_for_dates(dates) -> tuple[pd.DataFrame, pd.DataFrame]
- 기존: 종목 단위 best_state만 - S316 추가: 테마 단위 일별 RS 시계열도 반환 (date, theme, state, rs_5d/20d/60d, n_members) - 신규 캐시 _daily_theme_rs_byTheme.parquet (5,103행)

2-8. build_universe() — 시총 → 거래대금 (S316 22:48 PM 가르침)

build_universe() -> list[str]
- 기존: 시총 상위 100 - S316: 20일 평균 거래대금 상위 200 (30분봉+일봉 가용) - 결과: 시총100과 거래대금200 교집합 98종, 추가된 102종은 소형 거래대금 큰 종목


3. 흐름 (S316 v5 — 현재 상태)

[Phase A] build_universe()
  거래대금 상위 200종 → universe

[Phase B] compute_daily_state_for_universe()
  종목 × 날짜 × 일봉모듈(wyckoff/vsa/laws/volprofile) + 30분봉 OF
  → chart_state_df (state/phase/chart_score/of_*)
  캐시: _daily_chart_state.parquet (12,580 rows)

[Phase C] compute_theme_rs_for_dates()
  ats_main themes × KOSPI 일봉 + universe 일봉
  → (stock_df, theme_df) 2개
  캐시: _daily_theme_rs.parquet (15,532) + _daily_theme_rs_byTheme.parquet (5,103)

[Phase C-2] build_revenue_yoy_lookup() + build_quant_lookup()
  fnguide_fs.parquet + ea_panel.parquet
  → 종목별 point-in-time 시계열

[Phase D] run_portfolio()
  매일 종가:
    1) 보유 종목 should_exit() 체크 → 청산
    2) select_daily_candidates() = 통합 점수 top 15
    3) can_enter() 통과 + compute_score >= 0.3 → 진입
    4) NAV 기록

4. 현재 결함 (PM 지적 미해결)

결함 위치 영향
풀 선정 점수와 진입 점수 비일관 compute_score() line 535 풀 1위(삼성)와 진입 1위(현대건설) 다름
1종 100% 몰빵 로직 line 580-589 (1.5배 격차 시 1종) + line 609 (cash=0) 첫 진입 후 cash 소진 → 추가 진입 0
OF buy_intent 폐기 can_enter() "매수 우위 약한" 종목도 통과 (현대건설 buy_pres < sell_pres)
매출 데이터 부족 fnguide_fs 98종, universe 200 중 83종 커버 매출 percentile에 NaN 다수 → 중간값 0.5 부여로 약화

5. 결과 변화 (NAV / KOSPI 대비)

버전 풀 선정 진입 점수 NAV α vs KOSPI
S315 시총100 정적 4축 AND OF -10.76% -45.87pp
v1 강한테마 top3 × leader top3 (컷오프) 옛 공식 +2.45% -32.66pp
v2 동일 + OF 정정 옛 공식 +58.10% +22.99pp
v3 동일 + 매출/차트 추가 옛 공식 +47.40% +12.29pp
v4 동일 + universe 200 (거래대금) 옛 공식 +47.40% +12.29pp
v5 (현재) 통합 점수 top 15 옛 공식 (불일치) +17.15% -17.96pp

v5가 v4보다 NAV 떨어진 이유 = 풀은 다양화됐는데 진입 점수가 옛 공식 그대로라서 풀 1위 못 사고 옛 공식 1위(현대건설) 1종에 몰빵.


6. 다음 단계 (PM 결정 대기)

  1. compute_score() 폐기 → 풀 통합 점수를 그대로 진입 score로
  2. 1종 100% 몰빵 로직 폐기 → 통합 점수 상위 N개 균등/score 비례 분산
  3. can_enter()에 buy_intent 복구
  4. 매출 백필 확장 (98종 → universe 200 전체)