Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
> **현재 상태 (2026-05-08)**:
> - GitHub 원격 브랜치 정리 완료: 완료 브랜치 삭제, 활성 PR 브랜치만 유지
> - 60영업일 Paper 실험 freeze pack 병합: `reports/experiment_freeze_pack.md`, 일/주간 ops checklist, stop condition 문서 추가
> - Paper Evidence 런타임: 일별 자동 수집 → benchmark finalization → 날짜순 canonical evidence → promotion package → launch readiness
> - Paper Evidence 런타임: v2 일별 자동 수집 → benchmark finalization → 날짜순 canonical evidence → promotion package → launch readiness
> - Paper Runtime State Machine: normal/degraded/frozen/blocked_insufficient_evidence 상태 자동 전환 + allowed_actions 제어
> - Paper Pilot Authorization: blocked 상태에서도 제한적 real paper 가능 (수동 승인 + 리스크 캡 + fail-closed/audited entry guard)
> - Paper 신규 진입 실행 경계 fail-closed: preflight 상태 누락/손상 또는 runtime 조회 실패 시 BUY 제출 전 차단, SELL 청산은 유지
Expand Down Expand Up @@ -179,7 +179,7 @@ pytest tests/ -q

승격 규칙 v3 — `core/promotion_engine.py`에서 metrics 기반 자동 판정. `tools/evaluate_and_promote.py --canonical`로 재현하며, canonical 평가 산출물에는 종목군 구성, 데이터 범위, 수집 오류를 바탕으로 만든 `data_snapshot_hash`를 남긴다. live gate와 승격 산출물 로더는 이 해시, 데이터 범위, 수집 오류, 평가 실패 상태를 다시 검증해 손상된 산출물 사용을 차단한다.
Research candidate sweep — `tools/research_candidate_sweep.py --quick --candidate-family all`로 promotion과 분리된 rotation/momentum/breakout/pullback/benchmark-relative/risk-budget/cash-switch/benchmark-aware rotation/target-weight top-N rotation 후보 랭킹 artifact를 생성. Raw EW B&H gate는 유지하되, defensive/cash-heavy 후보 해석을 위해 평균 노출률과 exposure-matched B&H excess도 진단값으로 기록합니다. target-weight 후보는 `min_score_floor_pct`로 약한 초과 모멘텀 슬롯을 현금으로 남기고, `hold_rank_buffer`로 작은 랭킹 흔들림에 따른 불필요한 교체를 줄이며, `market_exposure_mode=benchmark_risk`로 KS11 SMA/낙폭/변동성 risk-off 구간의 부분 노출 축소를 검증합니다.
Paper Evidence 체계 — `core/paper_evidence.py` 일별 22개 지표 자동 수집, `core/paper_runtime.py` entry gate, `core/paper_pilot.py` launch readiness/pilot auth 판정. 승격 패키지는 `execution_backed=True`와 `real_paper`/`pilot_paper` 출처가 명시된 기록만 승격 증거로 인정해 예전 형식·수작업 기록 오염을 차단한다.
Paper Evidence 체계 — `core/paper_evidence.py` v2 일별 22개 지표 자동 수집, `core/paper_runtime.py` entry gate, `core/paper_pilot.py` launch readiness/pilot auth 판정. scheduler는 v2 collector만 canonical 증거로 기록하며, legacy `core/evidence_collector.py`는 import 호환용 deprecated no-op입니다. 승격 패키지는 `execution_backed=True`와 `real_paper`/`pilot_paper` 출처가 명시된 기록만 승격 증거로 인정해 예전 형식·수작업 기록 오염을 차단한다.

2026-04-29 all-family quick sweep: 5종목(`005930,000660,035720,051910,068270`)에서 rotation/momentum/breakout 후보 14개를 비교했지만 모두 benchmark excess return/Sharpe를 통과하지 못해 `NO_ALPHA_CANDIDATE`로 판정. 이 결과만으로 canonical promotion이나 paper/live 승격은 진행하지 않습니다.

Expand Down
144 changes: 13 additions & 131 deletions core/evidence_collector.py
Original file line number Diff line number Diff line change
@@ -1,141 +1,23 @@
"""
Paper Evidence 런타임 수집기
Deprecated paper evidence collector.

scheduler._run_post_market()에서 호출되어 DailyEvidence를 자동 누적.
실제 포트폴리오/OrderBook/DB 상태에서 metrics를 수집. placeholder 없음.
The v1 collector schema was retired. Runtime code should call
``core.paper_evidence.collect_daily_evidence`` directly so evidence records
share the v2 canonical contract used by promotion, runtime, and launch gates.
"""
from datetime import datetime, date
from typing import Optional

from loguru import logger

from core.paper_evidence import (
DailyEvidence, save_daily_evidence, load_all_evidence,
check_anomalies, save_anomalies,
EVIDENCE_DIR,
)


def collect_daily_evidence(
strategy: str,
portfolio_summary: dict,
trade_summary: dict,
order_book: Optional[object] = None,
benchmark_return: Optional[float] = None,
initial_capital: float = 10_000_000,
paper_start_date: str = "2026-04-01",
) -> DailyEvidence:
"""장마감 시 호출: 실제 포트폴리오/거래/주문 상태에서 DailyEvidence 생성.

Args:
strategy: 전략명
portfolio_summary: PortfolioManager.get_portfolio_summary() 결과
trade_summary: get_daily_trade_summary() 결과
order_book: OrderExecutor.order_book (OrderBook 인스턴스)
benchmark_return: 당일 same-universe B&H 누적 수익률 (%)
initial_capital: 초기 자본
paper_start_date: Paper 시작일
"""
today_str = date.today().isoformat()

# day_number 계산 (중복 방지: 이미 같은 날 기록 있으면 skip)
existing = load_all_evidence(strategy)
existing_dates = {e.get("date") for e in existing}
if today_str in existing_dates:
logger.info("Paper evidence 중복 방지: {} {} 이미 기록됨", strategy, today_str)
return None

day_number = len(existing) + 1

# 수익률 계산
total_value = portfolio_summary.get("total_value", initial_capital)
cumulative_return = (total_value / initial_capital - 1) * 100
# 전일 누적 수익률
prev_cum = existing[-1].get("cumulative_return", 0) if existing else 0
absolute_return = cumulative_return - prev_cum
from __future__ import annotations

# benchmark excess
same_universe_excess = 0.0
exposure_matched_excess = 0.0
cash_adjusted_excess = 0.0
if benchmark_return is not None:
same_universe_excess = cumulative_return - benchmark_return
# exposure-matched: 벤치마크 × 투자비중
density = portfolio_summary.get("position_count", 0) > 0
exposure = 1.0 if density else 0.0
exposure_matched_excess = cumulative_return - benchmark_return * exposure
# cash-adjusted: 비투자 기간에 CMA 2.5% 가정
cash_frac = portfolio_summary.get("cash", 0) / max(total_value, 1)
cma_daily = 0.025 / 252
cash_adj = cumulative_return + cma_daily * cash_frac * day_number * 100
cash_adjusted_excess = cash_adj - benchmark_return
from typing import Any

# 실행 품질
buy_signals = trade_summary.get("buy_count", 0)
sell_signals = trade_summary.get("sell_count", 0)
buy_executed = buy_signals # paper에서는 차단 외 전량 체결
sell_executed = sell_signals

total_trades_today = trade_summary.get("total_trades", 0)
n_positions = portfolio_summary.get("position_count", 0)

# signal density: 현재 포지션 보유 여부
signal_density = 100.0 if n_positions > 0 else 0.0

# fill rate (누적 기준)
raw_fill_rate = 100.0 # paper에서는 기본 100%
effective_fill_rate = 100.0

# turnover (누적 기준 — 연환산)
cum_trades = sum(e.get("buy_executed", 0) + e.get("sell_executed", 0) for e in existing)
cum_trades += buy_executed + sell_executed
years = day_number / 252
turnover = cum_trades / max(years, 0.01)
from loguru import logger

# drawdown
mdd = portfolio_summary.get("mdd", 0)
from core.paper_evidence import DailyEvidence, EVIDENCE_DIR

# OrderBook 상태
stale_pending = 0
if order_book is not None:
try:
expired = order_book.sweep_expired(max_age_seconds=600)
stale_pending = len(expired)
except Exception:
pass

evidence = DailyEvidence(
date=today_str,
strategy=strategy,
day_number=day_number,
absolute_return=round(absolute_return, 4),
cumulative_return=round(cumulative_return, 4),
same_universe_excess=round(same_universe_excess, 4),
exposure_matched_excess=round(exposure_matched_excess, 4),
cash_adjusted_excess=round(cash_adjusted_excess, 4),
turnover=round(turnover, 1),
signal_density=round(signal_density, 1),
raw_fill_rate=round(raw_fill_rate, 1),
effective_fill_rate=round(effective_fill_rate, 1),
drawdown=round(mdd, 4),
stale_pending_count=stale_pending,
buy_signals=buy_signals,
buy_executed=buy_executed,
sell_signals=sell_signals,
sell_executed=sell_executed,
portfolio_value=round(total_value, 0),
cash=round(portfolio_summary.get("cash", 0), 0),
n_positions=n_positions,
def collect_daily_evidence(*_args: Any, **_kwargs: Any) -> DailyEvidence | None:
"""Legacy no-op kept only so old imports fail closed instead of crashing."""
logger.warning(
"core.evidence_collector is deprecated; use core.paper_evidence.collect_daily_evidence"
)

# 저장
save_daily_evidence(evidence)

# anomaly 체크
anomalies = check_anomalies(evidence)
if anomalies:
save_anomalies(anomalies)
for a in anomalies:
logger.warning("Paper anomaly: {} — {}", a.rule, a.detail)

return evidence
return None
23 changes: 0 additions & 23 deletions core/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1068,29 +1068,6 @@ def _run_post_market(self):

# paper 모드: 장마감 시 Paper Evidence 수집 + 실전 전환 준비 자동 평가 + 주간 리포트
if self._is_paper_like_mode():
# [DEPRECATED] Legacy evidence_collector (v1 schema).
# v1 record는 runtime/promotion 계산에서 quarantine되어 미반영됨.
# 현재는 하위 paper_evidence (v2) 호출이 canonical evidence를 생성함.
# TODO: legacy evidence_collector 완전 제거 (운영상 무해하나 중복 호출)
try:
from core.evidence_collector import collect_daily_evidence
ob = self._order_executor.order_book if self._order_executor else None
executor_stats = ob.get_stats() if ob else {}
initial_capital = self.config.risk_params.get(
"position_sizing", {}
).get("initial_capital", 10_000_000)
collect_daily_evidence(
strategy=self.strategy_name,
portfolio_summary=summary,
trade_summary=trade_summary,
order_book=ob,
initial_capital=initial_capital,
executor_stats=executor_stats,
restart_recovery_count=self._restart_recovery_count,
)
except Exception as ev_err:
logger.warning("Paper Evidence 수집 실패 (legacy): {}", ev_err)

self._check_live_readiness()
# 금요일이면 주간 리포트 자동 생성
if datetime.now().weekday() == 4:
Expand Down
2 changes: 2 additions & 0 deletions docs/BACKTEST_IMPROVEMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
| **벤치마크 비용 미반영** (v5.0 수정) | `_buy_and_hold_metrics`에 거래비용 미적용 → 전략 alpha 0.2~0.5%p 과대평가 | **수정 완료** — commission/tax/slippage 반영 |
| **pilot guard fail-open 위험** | pilot authorization 이후 runtime/evidence/notifier/cap guard 오류가 예외 처리로 삼켜지면 제한 주문이 허용될 수 있음 | **수정 완료** — `check_pilot_entry()` 모든 blocked/allowed 결과 audit, guard 예외는 fail-closed block, notifier health missing/corrupt도 차단 |
| **paper evidence append 순서 의존** | backfill/shadow/finalize 기록이 JSONL 뒤쪽에 섞이면 최신 evidence, 최근 10일, promotion period가 append 순서 기준으로 왜곡될 수 있음 | **수정 완료** — canonical view를 날짜별 최신 record + 날짜순 반환으로 고정하고, 중복 확인은 전체 파일의 해당 날짜 최신 record를 조회 |
| **legacy evidence E2E 수집 경로** | 오래된 v1 `save_daily_evidence`/`evidence_collector` 계약이 남아 pytest collection과 scheduler 장마감 legacy 호출을 깨뜨릴 수 있음 | **수정 완료** — E2E를 v2 JSONL/canonical/report 계약으로 교체, scheduler legacy 호출 제거, `core.evidence_collector`는 import 호환 no-op shim으로 축소 |
| **방어형 후보 raw benchmark 해석** | cash-switch처럼 평균 노출이 낮은 후보는 full B&H 대비 excess가 과도하게 나빠 보일 수 있음 | **진단 추가** — research sweep에 exposure-matched B&H return/sharpe/MDD/excess 기록. 단, promotion gate는 raw benchmark excess 유지 |
| **회전 전략의 sparse signal 한계** | 월간 상대강도 후보가 BUY/SELL 신호만 내면 목표 top-N을 지속적으로 채우지 못해 평균 노출이 낮게 측정될 수 있음 | **검증 완료** — target-weight top-N research backtester로 avg exposure 85%대까지 개선. 5종목 smoke는 raw excess 음수였지만 canonical top-20 full sweep은 alpha 후보 확인. `hold_rank_buffer` 적용 후 turnover gate 통과, `benchmark_risk` overlay 적용 후 best=`target_weight_rotation_top5_60_120_floor0_hold3_risk60_35`가 return=+210.24%, raw excess=+60.85%p, Sharpe=1.60, MDD=-19.24%, turnover/year=858.0%, WF positive/Sh+ 100%로 research sweep 기준 provisional gate 통과 |
| **target-weight 후보의 paper 연결 부재** | research-only evaluator에서 provisional 후보가 나와도 기존 canonical/paper 경로는 등록 전략만 평가 | **대부분 해결** — canonical bundle 재현 완료 + `core/target_weight_rotation.py`, `tools/target_weight_rotation_pilot.py`로 전용 paper/pilot adapter 추가. dry-run은 `--record-shadow-evidence`, `--shadow-days 3`, 또는 `--shadow-start-date/--shadow-end-date`로 non-promotable shadow readiness evidence, launch readiness artifact, pilot runbook을 남기고 cap preview와 plan 기반 최소/추천 cap + enable 명령으로 pilot 승인 전 캡 적합성을 확인. 이후 `--readiness-audit`가 주문 제출, evidence 기록, pilot session 저장 없이 clean shadow/launch readiness, active pilot auth와 cap validation, 중복 session idempotency, 실행일/장 시간, 실행 전 position drift, 추천 cap 충족 여부, 유동성 preflight, 비용 반영 pre-trade risk를 JSON artifact와 Markdown 운영 리포트로 판정한다. 유동성 preflight는 최근 20일 평균 거래대금 대비 주문 notional 비율을 계산하고 기본 5%(`--max-order-adv-pct`) 초과 주문은 readiness와 실행을 fail-closed 차단한다. pre-trade risk는 `RiskManager.calculate_transaction_costs()`의 수수료/세금/동적 슬리피지 예상 체결가를 재사용해 현금 부족, 최소 현금비중, 총투자비중, 종목별 비중, 보유 종목 수 위반을 주문 제출 전에 차단한다. Markdown 리포트는 shadow 수집, audit 재실행, 추천 cap 승인, capped paper 실행 명령을 함께 남긴다. `--shadow-days N`은 휴장/데이터 공백으로 같은 거래일에 매핑될 때 과거 평일을 추가 스캔해 N개 고유 resolved trade_day 충족을 목표로 하며, 목표 미달이나 날짜별 실패는 non-zero 종료로 fail-closed 처리한다. 실행형 `pilot_paper` evidence는 같은 candidate/trade_day의 기존 pilot session artifact가 없고, 주문 제출 직전 실제 paper position이 계획 입력 장부 `position_quantities_before`와 일치하고, KST 실행일과 KRX 정규장 주문 가능 시간을 통과하고, 유동성 preflight와 pre-trade risk를 통과하고, 계획 주문 전부 성공 및 주문 결과 payload와 당일 `TradeHistory` fill 집계가 plan과 일치하며, 실행 후 실제 paper position 전체가 리밸런싱 후 `target_quantities_after` 장부와 일치하고 계획 밖 양수 포지션이 없을 때만 수집한다. 같은 날짜가 이미 기록된 경우에도 기존 canonical evidence가 `pilot_paper`/authorized/execution-backed이고 target-weight plan hash와 complete/execution-market-session/liquidity/pre-trade-risk/order/fill/position 검증을 통과해야 재사용해 중복 실행/stale plan/장 외 실행/부분 실행/중단/주문 결과 불일치/체결 기록 불일치/기존 evidence 검증 실패/포지션 불일치일이 승격 증거로 섞이지 않게 한다. 명시 재시도는 `--allow-rerun`으로만 허용한다. 다음 과제는 shadow clean days 충족 후 정규장 capped pilot_paper execution-backed evidence 축적 |
Expand Down Expand Up @@ -131,6 +132,7 @@
| 벤치마크 거래비용 반영 | 높음 | **완료 — `_buy_and_hold_metrics`에 commission/tax/slippage 적용** |
| Paper Evidence 수집 체계 | 높음 | **완료 — `core/paper_evidence.py` 일별 22개 지표, 6 anomaly rule, 9 approval gate** |
| Paper Evidence canonical 정렬 | 높음 | 완료 — append-only JSONL에서 같은 날짜의 최신 record만 canonical로 유지하고 날짜순으로 반환. backfill/finalize/shadow append 순서와 무관하게 freshness, 최근 10일, promotion period가 실제 날짜 기준으로 계산됨 |
| Legacy evidence E2E 정리 | 높음 | 완료 — v1 helper API 기반 `tests/test_evidence_e2e.py`를 v2 smoke/E2E로 교체하고 scheduler의 deprecated v1 collector 호출 제거 |
| Paper Runtime State Machine | 높음 | **완료 — `core/paper_runtime.py` 5개 상태(normal/degraded/frozen/blocked/research_disabled), schema quarantine** |
| Paper Pilot Authorization | 높음 | **완료 — `core/paper_pilot.py` launch readiness + pilot auth + 리스크 캡 + fail-closed/audited entry guard** |
| Paper Preflight Check | 높음 | **완료 — `core/paper_preflight.py` 운영 준비 상태 점검** |
Expand Down
Loading
Loading