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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ pytest tests/ -q

## 전략 상태

승격 규칙 v3 — `core/promotion_engine.py`에서 metrics 기반 자동 판정. `tools/evaluate_and_promote.py --canonical`로 재현.
승격 규칙 v3 — `core/promotion_engine.py`에서 metrics 기반 자동 판정. `tools/evaluate_and_promote.py --canonical`로 재현하며, canonical 평가 산출물에는 종목군 구성, 데이터 범위, 수집 오류를 바탕으로 만든 `data_snapshot_hash`를 남겨 입력 데이터 변화와 평가 실패 원인을 추적한다.
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 판정.

Expand Down
1 change: 1 addition & 0 deletions docs/PROJECT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ full paper 신규 BUY는 preflight status artifact와 runtime state가 모두
| ✅ **target-weight completed rerun block 추가** | same-candidate/trade-day pilot session artifact가 이미 `execution_complete=True`이고 실제 주문 실행 수량이 있으면 `--allow-rerun`을 줘도 재실행을 차단한다. `--allow-rerun`은 부분 실행/중단 세션 복구에만 사용해 완료된 실행 증거가 중복 주문으로 오염되지 않게 한다 |
| ✅ **pilot entry fail-closed audit 추가** | `check_pilot_entry()`의 모든 blocked/allowed 결과를 `pilot_audit.jsonl`에 기록하고, runtime/evidence/notifier/order-count/position-count/gross-exposure guard 예외는 pilot entry 차단으로 처리 |
| ✅ **generic paper entry guard 추가** | `main.py --mode paper`, scheduler auto-entry, `execute_buy_quantity()` 모두 preflight/runtime 확인 실패 시 BUY를 fail-closed 차단. blocked runtime의 pilot override는 `check_pilot_entry()` 재검증을 통과해야 하며 SELL은 exit-safe 유지 |
| ✅ **canonical 평가 입력 snapshot 추가** | `tools/evaluate_and_promote.py --canonical`이 선정 종목의 유동성 데이터 범위, 벤치마크 데이터 범위, 데이터 수집 오류, `data_snapshot_hash`를 `run_metadata.json`에 기록한다. 평가 예외는 단순 0% 성과와 구분해 `evaluation_status=failed`로 남긴다 |
| ✅ **Zero-return Semantics** | cash-only/no-position day deadlock 해소 — daily_return=0.0 추론 |
| ✅ **scoring paper_only 강등** | Sharpe/PF/WF 안정성 미달. 관찰은 가능하지만 우선 pilot 후보 아님 |

Expand Down
89 changes: 89 additions & 0 deletions tests/test_evaluate_and_promote.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,95 @@ def test_run_canonical_research_candidate_rejects_unsupported_strategy():
)


def test_stable_payload_hash_is_order_independent():
from tools.evaluate_and_promote import stable_payload_hash

left = {"b": [2, 1], "a": {"x": 1, "y": 2}}
right = {"a": {"y": 2, "x": 1}, "b": [2, 1]}

assert stable_payload_hash(left) == stable_payload_hash(right)
assert stable_payload_hash(left) != stable_payload_hash({"a": {"x": 1, "y": 3}, "b": [2, 1]})


def test_summarize_ohlcv_frame_records_deterministic_coverage():
from tools.evaluate_and_promote import summarize_ohlcv_frame

df = pd.DataFrame(
{
"date": pd.to_datetime(["2025-01-03", "2025-01-02"]),
"close": [20.0, 10.0],
"volume": [200, 100],
}
)

summary = summarize_ohlcv_frame(df)

assert summary["rows"] == 2
assert summary["start"] == "2025-01-02"
assert summary["end"] == "2025-01-03"
assert summary["first_close"] == 10.0
assert summary["last_close"] == 20.0
assert summary["close_non_null"] == 2
assert summary["volume_non_null"] == 2


def test_build_data_snapshot_manifest_hash_changes_with_coverage():
from tools.evaluate_and_promote import build_data_snapshot_manifest

base_kwargs = {
"provider": "test-provider",
"universe_rule": "top liquidity",
"eval_start": "2025-01-01",
"eval_end": "2025-12-31",
"universe_lookback_start": "2024-10-01",
"universe_lookback_end": "2024-12-31",
"universe": ["005930", "000660"],
"liquidity_coverage": {
"000660": {"rows": 61, "start": "2024-10-01", "end": "2024-12-31"},
"005930": {"rows": 62, "start": "2024-10-01", "end": "2024-12-31"},
},
"benchmark_coverage": {
"005930": {"rows": 700, "start": "2025-01-01", "end": "2025-12-31"},
"000660": {"rows": 700, "start": "2025-01-01", "end": "2025-12-31"},
},
"fetch_errors": {},
}

first = build_data_snapshot_manifest(**base_kwargs)
reordered = build_data_snapshot_manifest(
**{
**base_kwargs,
"liquidity_coverage": dict(reversed(list(base_kwargs["liquidity_coverage"].items()))),
}
)
changed = build_data_snapshot_manifest(
**{
**base_kwargs,
"benchmark_coverage": {
**base_kwargs["benchmark_coverage"],
"000660": {"rows": 699, "start": "2025-01-01", "end": "2025-12-31"},
},
}
)

assert len(first["data_snapshot_hash"]) == 64
assert first["data_snapshot_hash"] == reordered["data_snapshot_hash"]
assert first["data_snapshot_hash"] != changed["data_snapshot_hash"]
assert list(first["liquidity_coverage"]) == ["000660", "005930"]


def test_failed_canonical_metrics_separates_evaluation_error_from_zero_return():
from tools.evaluate_and_promote import failed_canonical_metrics

metrics = failed_canonical_metrics(RuntimeError("data provider unavailable"), "full_period")

assert metrics["total_return"] == 0
assert metrics["evaluation_status"] == "failed"
assert metrics["evaluation_stage"] == "full_period"
assert metrics["evaluation_error_type"] == "RuntimeError"
assert "data provider unavailable" in metrics["error"]


def test_calculate_canonical_metrics_preserves_target_weight_diagnostics():
from tools.evaluate_and_promote import calculate_canonical_metrics

Expand Down
Loading
Loading