From 51c10fafdd28bf91b64f35422992429f9c1d3472 Mon Sep 17 00:00:00 2001 From: Quant Trader Date: Thu, 7 May 2026 13:49:56 +0900 Subject: [PATCH] =?UTF-8?q?target-weight=20=EC=9D=BC=EC=9D=BC=20=EC=9A=B4?= =?UTF-8?q?=EC=9A=A9=20=EC=9A=94=EC=95=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/safety-regression.yml | 4 +- README.md | 2 +- docs/PROJECT_GUIDE.md | 1 + reports/daily_ops_checklist.md | 9 + tests/test_target_weight_paper_adapter.py | 140 ++++++++++ tools/target_weight_rotation_pilot.py | 312 ++++++++++++++++++++++ 6 files changed, 466 insertions(+), 2 deletions(-) diff --git a/.github/workflows/safety-regression.yml b/.github/workflows/safety-regression.yml index 71f7e9b6..d00a2f5d 100644 --- a/.github/workflows/safety-regression.yml +++ b/.github/workflows/safety-regression.yml @@ -36,7 +36,7 @@ jobs: - name: Compile safety modules run: | - python -m py_compile core/order_executor.py core/paper_preflight.py core/scheduler.py core/live_gate.py + python -m py_compile core/order_executor.py core/paper_preflight.py core/scheduler.py core/live_gate.py core/target_weight_rotation.py tools/target_weight_rotation_pilot.py tools/paper_pilot_control.py - name: Run safety regression tests env: @@ -50,6 +50,8 @@ jobs: tests/test_paper_runtime.py \ tests/test_paper_preflight.py \ tests/test_paper_pilot.py::TestPilotSchedulerIntegration \ + tests/test_target_weight_rotation.py \ + tests/test_target_weight_paper_adapter.py \ tests/test_live_gate.py \ tests/test_paper_evidence.py \ tests/test_promotion_engine.py \ diff --git a/README.md b/README.md index f6fd7e78..16dbf39a 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ Paper Evidence 체계 — `core/paper_evidence.py` 일별 22개 지표 자동 현재 scoring은 관찰용 paper_only로 강등되었습니다. 신규 우선 실험은 risk-adjusted 기준을 통과한 후보만 대상으로 하며, Paper Evidence 체계로 승격/강등 근거를 자동 수집합니다. Paper 운영 도구: `tools/run_paper_evidence_pipeline.py` (backfill/finalize/package), `tools/paper_preflight.py`, `tools/paper_launch_readiness.py`, `tools/paper_pilot_control.py`, `tools/research_candidate_sweep.py`, `tools/target_weight_rotation_pilot.py`. target-weight 후보는 capped pilot 승인 전 `tools/target_weight_rotation_pilot.py --record-shadow-evidence`, `--shadow-days 3`, 또는 `--shadow-start-date YYYY-MM-DD --shadow-end-date YYYY-MM-DD`로 dry-run plan artifact, shadow readiness evidence, launch readiness artifact, plan 기반 cap 추천과 enable 명령이 포함된 pilot runbook을 먼저 누적합니다. 이후 `--readiness-audit`로 주문 제출이나 shadow/pilot evidence 기록 없이 clean shadow, launch readiness, active pilot caps, 중복 세션, 실행 전 포지션 드리프트, 추천 cap 충족 여부, 최근 평균 거래대금 대비 주문 비율, 비용 반영 후 현금/투자비중 한도를 JSON artifact와 Markdown 운영 리포트로 점검합니다. 유동성 preflight는 기본적으로 주문별 20일 평균 거래대금의 5% 초과를 차단하며 `--max-order-adv-pct`로 조정할 수 있습니다. pre-trade risk 검증은 주문별 수수료/세금/동적 슬리피지를 반영한 예상 체결가로 현금 부족, 최소 현금비중, 총투자비중, 종목별 비중, 최대 보유 종목 수 위반을 실행 전에 차단합니다. Markdown 리포트에는 shadow 수집, audit 재실행, 추천 cap 승인, capped paper 실행 명령이 함께 기록됩니다. `--shadow-days N`은 휴장/데이터 공백으로 같은 거래일에 매핑되는 경우 과거 평일을 추가 스캔해 N개 고유 resolved trade_day 충족을 목표로 하며, 목표 미달이나 날짜별 실패가 있으면 CLI가 non-zero로 종료해 자동화가 불완전한 증거를 성공으로 처리하지 못하게 합니다. 실행형 pilot evidence는 같은 candidate/trade_day의 기존 pilot session artifact가 없고, 주문 제출 직전 실제 paper position이 계획 입력 장부 `position_quantities_before`와 일치하고, 유동성 preflight와 pre-trade risk 검증을 통과하고, 모든 계획 주문이 성공하며, 성공 주문 결과와 당일 `TradeHistory` fill 집계의 종목/방향/수량이 계획과 일치하고, 실행 후 실제 paper position 전체가 리밸런싱 후 `target_quantities_after` 장부와 일치하고 계획 밖 양수 포지션이 없을 때만 `pilot_paper`로 수집됩니다. 같은 날짜 evidence가 이미 있으면 기존 canonical record도 `pilot_paper`/authorized/target-weight complete/liquidity-complete/pre-trade-risk-complete/fill-complete 조건을 통과해야 재사용합니다. 중복 실행은 기본 차단하며 운영자가 명시적으로 재시도해야 할 때만 `--allow-rerun`을 사용합니다. 부분 실행/중단/중복 실행/주문 결과 불일치/체결 기록 불일치/기존 evidence 검증 실패/실행 전 포지션 드리프트/유동성 preflight 실패/pre-trade risk 실패/실행 후 포지션 불일치는 execution-backed 승격 증거에서 제외됩니다. -Target-weight 60영업일 pilot manifest: `--readiness-audit`는 운영 리포트와 함께 `target_weight_paper_experiment_manifest_*.json`을 생성해 후보 snapshot, 추천 cap, 차단 사유, 실행 명령, 승격 증거 조건을 고정합니다. 공식 `reports/paper_experiment_manifest.json`도 기존 scoring 60영업일 실험과 target-weight capped paper pilot을 분리해 기록합니다. +Target-weight 60영업일 pilot manifest: `--readiness-audit`는 운영 리포트와 함께 `target_weight_paper_experiment_manifest_*.json`을 생성해 후보 snapshot, 추천 cap, 차단 사유, 실행 명령, 승격 증거 조건을 고정합니다. `--daily-ops-summary`는 readiness, risk check, verified pilot day 진행률, 다음 실행 명령을 `target_weight_daily_ops_summary_*.json/.md`로 묶어 매일 운영 판단을 한 번에 확인하게 합니다. 공식 `reports/paper_experiment_manifest.json`도 기존 scoring 60영업일 실험과 target-weight capped paper pilot을 분리해 기록합니다. Generic paper entry도 동일한 실행 경계 원칙을 따른다. `main.py --mode paper`, scheduler auto-entry, fixed-quantity paper BUY 모두 preflight status와 runtime state를 주문 생성 전에 확인하고, 확인 실패는 fail-closed로 차단한다. blocked runtime에서 pilot authorization이 활성화되어도 `check_pilot_entry()`를 다시 통과해야 하며, SELL/exit 경로는 포지션 정리를 위해 계속 허용한다. pilot evidence freshness는 달력일이 아니라 주말과 한국장 휴장일을 제외한 영업일 기준으로 계산합니다. diff --git a/docs/PROJECT_GUIDE.md b/docs/PROJECT_GUIDE.md index 81d72fd9..93ad8eb5 100644 --- a/docs/PROJECT_GUIDE.md +++ b/docs/PROJECT_GUIDE.md @@ -713,6 +713,7 @@ full paper 신규 BUY는 preflight status artifact와 runtime state가 모두 | ✅ **target-weight shadow proof 추가** | dry-run에서 `--record-shadow-evidence`를 켜면 `append_shadow_plan_evidence()`가 non-promotable `shadow_bootstrap` record를 남김. `execution_backed=False`, excess=null이라 promotion은 오염하지 않고, launch readiness의 clean final day만 채운다. `--shadow-days 3` 또는 `--shadow-start-date/--shadow-end-date`로 여러 날짜 shadow bootstrap을 한 번에 누적할 수 있고, `--shadow-days N`은 휴장/데이터 공백으로 같은 거래일에 매핑되면 과거 평일을 추가 스캔해 N개 고유 resolved trade_day 충족을 목표로 한다. 목표 미달 또는 날짜별 실패가 있으면 CLI는 non-zero로 종료해 불완전한 증거가 자동화에서 성공으로 처리되지 않게 한다. 같은 실행에서 launch readiness JSON/MD와 pilot runbook을 생성하며 session/batch artifact에는 기본 cap preview, plan 기반 최소/추천 cap, enable 명령, launch artifact 경로를 기록 | | ✅ **target-weight readiness audit 추가** | `tools/target_weight_rotation_pilot.py --readiness-audit`로 주문 제출, shadow evidence 기록, pilot session 저장 없이 capped pilot 직전 상태를 JSON artifact와 Markdown 운영 리포트로 점검. clean shadow/launch readiness, active pilot auth와 cap validation, 추천 cap, 중복 session idempotency, 실행 전 position drift, 유동성 preflight, 비용 반영 pre-trade risk, 다음 조치(`cap approval` 또는 `execute capped paper pilot`)를 함께 판정한다. Markdown 리포트는 shadow 수집/audit 재실행/추천 cap 승인/capped paper 실행 명령을 함께 남기며, cap 승인 준비가 안 된 blocker는 non-zero 종료로 자동화가 진행하지 못하게 한다 | | ✅ **target-weight 60영업일 pilot manifest 추가** | `tools/target_weight_rotation_pilot.py --readiness-audit`가 60영업일 `pilot_paper` 증거 누적 기준, 추천 cap, 차단 사유, 실행 명령을 담은 `target_weight_paper_experiment_manifest_*.json`을 함께 생성한다. `reports/paper_experiment_manifest.json`에도 기존 scoring 실험과 별도 target-weight capped paper pilot 기준을 기록해 운영 흐름을 분리한다 | +| ✅ **target-weight daily ops summary 추가** | `tools/target_weight_rotation_pilot.py --daily-ops-summary`가 readiness audit, 60영업일 pilot evidence 진행률, 유동성/pre-trade risk 상태, 다음 운영 명령을 `target_weight_daily_ops_summary_*.json/.md`로 묶어 남긴다. 주문·증거·pilot session을 쓰지 않는 no-order summary라 장전/장마감 운영 확인에 안전하게 사용할 수 있다 | | ✅ **pilot freshness 영업일 기준 보정** | `check_pilot_entry()`와 launch readiness가 evidence stale 여부를 달력일이 아니라 한국장 영업일 기준으로 계산한다. 긴 주말·휴장일 이후 정상적인 paper/pilot 흐름이 불필요하게 차단되지 않도록 `evidence_stale_unit=business_days`를 함께 남긴다 | | ✅ **target-weight pilot enable guard 추가** | `tools/paper_pilot_control.py --enable`이 target-weight 후보를 승인할 때 pilot auth 기록 전에 readiness audit을 재실행하고, 운영자가 요청한 cap이 현재 plan/launch readiness/유동성 preflight/비용 반영 pre-trade risk를 만족할 때만 승인한다. stale plan이나 추천 cap 미충족 상태에서 auth만 먼저 열리는 경로를 차단한다 | | ✅ **target-weight cap validation artifact 추가** | `--execute`가 pilot cap validation에서 막혀도 예외로 조기 종료하지 않고 session JSON artifact에 차단 사유, skipped orders, evidence block reason을 남긴다. 주문/체결/증거 수집/runtime pilot session 저장은 하지 않아 cap 조정 후 재점검할 수 있다 | diff --git a/reports/daily_ops_checklist.md b/reports/daily_ops_checklist.md index 3ca03da6..98db7ebf 100644 --- a/reports/daily_ops_checklist.md +++ b/reports/daily_ops_checklist.md @@ -45,6 +45,15 @@ - [ ] MDD 확인: 누적 ___% (15% 접근 시 주의) - [ ] 손절/익절/트레일링 스탑 발동 건수: __건 +### Target-weight pilot 전용 + +- [ ] `python tools/target_weight_rotation_pilot.py --daily-ops-summary` 실행 +- [ ] Summary status 확인: BLOCKED / READY_TO_ENABLE_CAPS / READY_TO_EXECUTE +- [ ] Verified pilot days 확인: ____ / 60 +- [ ] Invalid execution days가 0인지 확인 +- [ ] Liquidity / Pre-trade risk가 PASS인지 확인 +- [ ] 다음 명령이 cap 승인인지, capped paper 실행인지 기록 + ### 기록 ``` diff --git a/tests/test_target_weight_paper_adapter.py b/tests/test_target_weight_paper_adapter.py index a7c07ea5..5027d1ea 100644 --- a/tests/test_target_weight_paper_adapter.py +++ b/tests/test_target_weight_paper_adapter.py @@ -1004,6 +1004,146 @@ def test_build_target_weight_experiment_manifest_freezes_pilot_flow(): assert manifest["no_order_safety"]["orders_submitted"] is False +def test_summarize_target_weight_evidence_progress_counts_verified_days(monkeypatch): + from tools.target_weight_rotation_pilot import summarize_target_weight_evidence_progress + + strategy = "target_weight_candidate" + params_hash = "hash" + records = [ + { + "date": "2026-04-08", + "strategy": strategy, + "execution_backed": False, + "evidence_mode": "shadow_bootstrap", + "session_mode": "shadow_bootstrap", + }, + { + "date": "2026-04-09", + "strategy": strategy, + "execution_backed": True, + "evidence_mode": "pilot_paper", + "session_mode": "pilot_paper", + "pilot_authorized": True, + "pilot_caps_snapshot": { + "target_weight_plan": { + "candidate_id": strategy, + "trade_day": "2026-04-09", + "params_hash": params_hash, + }, + "target_weight_execution": { + "complete": True, + "params_hash": params_hash, + "liquidity_complete": True, + "pre_trade_risk_complete": True, + "order_result_complete": True, + "fill_complete": True, + "position_reconciliation": {"complete": True}, + }, + }, + }, + { + "date": "2026-04-10", + "strategy": strategy, + "execution_backed": True, + "evidence_mode": "pilot_paper", + "session_mode": "pilot_paper", + "pilot_authorized": True, + "pilot_caps_snapshot": { + "target_weight_plan": { + "candidate_id": strategy, + "trade_day": "2026-04-10", + "params_hash": params_hash, + }, + }, + }, + ] + monkeypatch.setattr( + "core.paper_evidence.get_canonical_records", + lambda candidate_id: records if candidate_id == strategy else [], + ) + + progress = summarize_target_weight_evidence_progress(strategy) + + assert progress["verified_pilot_days"] == 1 + assert progress["remaining_pilot_days"] == 59 + assert progress["shadow_days"] == 1 + assert progress["invalid_execution_days"] == 1 + assert progress["invalid_reasons"]["missing_target_weight_execution"] == 1 + assert progress["latest_verified_pilot_date"] == "2026-04-09" + + +def test_build_target_weight_daily_ops_summary_writes_operator_view(tmp_path): + from tools.target_weight_rotation_pilot import ( + build_target_weight_daily_ops_summary, + build_target_weight_experiment_manifest, + recommend_pilot_caps, + write_target_weight_daily_ops_summary, + ) + + plan = _adapter_plan() + cap_recommendation = recommend_pilot_caps(plan) + audit = { + "candidate_id": plan.candidate_id, + "trade_day": plan.trade_day, + "ready_for_cap_approval": True, + "ready_for_capped_pilot": False, + "next_action": "enable pilot with suggested caps, then rerun readiness audit", + "blocking_reasons": ["pilot_authorization: no active capped pilot authorization"], + "warning_reasons": ["preview_caps: current preview blocked"], + "operator_commands": { + "collect_shadow_days": "python tools/target_weight_rotation_pilot.py --shadow-days 3", + "rerun_readiness_audit": "python tools/target_weight_rotation_pilot.py --readiness-audit", + "enable_suggested_caps": cap_recommendation["enable_command"], + "execute_capped_paper": "python tools/target_weight_rotation_pilot.py --execute --collect-evidence", + }, + "plan_summary": { + "order_count": 3, + "target_position_count": 3, + "max_order_notional": 1_200_000.0, + "gross_exposure_after": 3_200_000.0, + }, + "liquidity_check": {"complete": True, "reason": "target_weight_liquidity_preflight_passed"}, + "pre_trade_risk_check": {"complete": True, "reason": "target_weight_pre_trade_risk_passed"}, + } + manifest = build_target_weight_experiment_manifest( + plan=plan, + cap_recommendation=cap_recommendation, + readiness_audit=audit, + ) + progress = { + "candidate_id": plan.candidate_id, + "target_days": 60, + "verified_pilot_days": 12, + "remaining_pilot_days": 48, + "progress_ratio": 0.2, + "shadow_days": 3, + "invalid_execution_days": 0, + "invalid_reasons": {}, + "non_promotable_days": 0, + "total_canonical_records": 15, + "latest_record_date": "2026-04-10", + "latest_verified_pilot_date": "2026-04-10", + "latest_shadow_date": "2026-04-03", + "ready_for_promotion_day_count": False, + } + + summary = build_target_weight_daily_ops_summary( + audit=audit, + experiment_manifest=manifest, + evidence_progress=progress, + ) + json_path, md_path = write_target_weight_daily_ops_summary(summary, output_dir=tmp_path) + payload = json.loads(json_path.read_text(encoding="utf-8")) + report = md_path.read_text(encoding="utf-8") + + assert summary["status"] == "READY_TO_ENABLE_CAPS" + assert len(summary["summary_hash"]) == 64 + assert payload["evidence_progress"]["remaining_pilot_days"] == 48 + assert payload["no_order_safety"]["summary_only"] is True + assert "Verified pilot days: 12/60" in report + assert "READY_TO_ENABLE_CAPS" in report + + def test_assess_plan_liquidity_blocks_large_adv_order(): from tools.target_weight_rotation_pilot import assess_plan_liquidity diff --git a/tools/target_weight_rotation_pilot.py b/tools/target_weight_rotation_pilot.py index aff43e87..8d20d805 100644 --- a/tools/target_weight_rotation_pilot.py +++ b/tools/target_weight_rotation_pilot.py @@ -2156,6 +2156,220 @@ def write_target_weight_experiment_manifest( return path +def summarize_target_weight_evidence_progress( + candidate_id: str, + *, + target_days: int = TARGET_WEIGHT_PILOT_TARGET_DAYS, +) -> dict[str, Any]: + """target-weight 후보의 pilot evidence 누적 상태를 운영 요약용으로 집계한다.""" + from core.paper_evidence import ( + _is_promotable_paper_evidence, + _target_weight_record_proof_status, + get_canonical_records, + ) + + records = get_canonical_records(candidate_id) + verified_dates: set[str] = set() + invalid_dates: set[str] = set() + invalid_reasons: dict[str, int] = {} + shadow_dates: set[str] = set() + non_promotable_dates: set[str] = set() + all_dates: set[str] = set() + + for record in records: + date = str(record.get("date") or "") + if date: + all_dates.add(date) + if ( + record.get("evidence_mode") == "shadow_bootstrap" + or record.get("session_mode") == "shadow_bootstrap" + ): + if date: + shadow_dates.add(date) + continue + if not _is_promotable_paper_evidence(record): + if date: + non_promotable_dates.add(date) + continue + + valid, reason = _target_weight_record_proof_status(candidate_id, record) + if valid: + if date: + verified_dates.add(date) + else: + if date: + invalid_dates.add(date) + invalid_reasons[reason] = invalid_reasons.get(reason, 0) + 1 + + verified_days = len(verified_dates) + remaining_days = max(int(target_days) - verified_days, 0) + progress_ratio = verified_days / int(target_days) if target_days else 0.0 + return { + "candidate_id": candidate_id, + "target_days": int(target_days), + "verified_pilot_days": verified_days, + "remaining_pilot_days": remaining_days, + "progress_ratio": round(progress_ratio, 4), + "shadow_days": len(shadow_dates), + "invalid_execution_days": len(invalid_dates), + "invalid_reasons": invalid_reasons, + "non_promotable_days": len(non_promotable_dates), + "total_canonical_records": len(records), + "latest_record_date": max(all_dates) if all_dates else None, + "latest_verified_pilot_date": max(verified_dates) if verified_dates else None, + "latest_shadow_date": max(shadow_dates) if shadow_dates else None, + "ready_for_promotion_day_count": verified_days >= int(target_days), + } + + +def build_target_weight_daily_ops_summary( + *, + audit: dict[str, Any], + experiment_manifest: dict[str, Any], + evidence_progress: dict[str, Any], +) -> dict[str, Any]: + """readiness audit와 evidence progress를 하루 운영 판단용 artifact로 묶는다.""" + if audit.get("ready_for_capped_pilot"): + status = "READY_TO_EXECUTE" + next_step = "승인된 cap으로 capped paper 실행" + elif audit.get("ready_for_cap_approval"): + status = "READY_TO_ENABLE_CAPS" + next_step = "추천 cap 승인 후 readiness audit 재실행" + else: + status = "BLOCKED" + next_step = "차단 사유 해소 후 shadow/readiness 재점검" + + plan = audit.get("plan_summary") or {} + liquidity = audit.get("liquidity_check") or {} + pre_trade_risk = audit.get("pre_trade_risk_check") or {} + summary = { + "artifact_type": "target_weight_daily_ops_summary", + "schema_version": 1, + "generated_at": datetime.now().isoformat(), + "candidate_id": audit["candidate_id"], + "trade_day": audit["trade_day"], + "status": status, + "next_step": next_step, + "evidence_progress": evidence_progress, + "decision": { + "ready_for_cap_approval": bool(audit.get("ready_for_cap_approval")), + "ready_for_capped_pilot": bool(audit.get("ready_for_capped_pilot")), + "readiness_next_action": audit.get("next_action", ""), + "blocking_reasons": list(audit.get("blocking_reasons") or []), + "warning_reasons": list(audit.get("warning_reasons") or []), + }, + "risk_snapshot": { + "orders": plan.get("order_count", 0), + "target_positions": plan.get("target_position_count", 0), + "max_order_notional": plan.get("max_order_notional", 0), + "gross_exposure_after": plan.get("gross_exposure_after", 0), + "liquidity_complete": bool(liquidity.get("complete", False)), + "liquidity_reason": liquidity.get("reason", "not checked"), + "pre_trade_risk_complete": bool(pre_trade_risk.get("complete", False)), + "pre_trade_risk_reason": pre_trade_risk.get("reason", "not checked"), + }, + "operator_commands": dict(audit.get("operator_commands") or {}), + "manifest_hash": experiment_manifest.get("manifest_hash"), + "no_order_safety": { + "orders_submitted": False, + "shadow_evidence_recorded": False, + "pilot_evidence_recorded": False, + "pilot_session_written": False, + "summary_only": True, + }, + } + summary["summary_hash"] = _stable_manifest_hash(summary) + return summary + + +def render_target_weight_daily_ops_markdown(summary: dict[str, Any]) -> str: + progress = summary["evidence_progress"] + decision = summary["decision"] + risk = summary["risk_snapshot"] + commands = summary.get("operator_commands", {}) + lines = [ + "# Target-weight Daily Ops Summary", + "", + f"- Candidate: `{summary['candidate_id']}`", + f"- Trade day: `{summary['trade_day']}`", + f"- Status: **{summary['status']}**", + f"- Next step: {summary['next_step']}", + "", + "## Evidence Progress", + ( + f"- Verified pilot days: " + f"{progress['verified_pilot_days']}/{progress['target_days']} " + f"({progress['progress_ratio']:.0%})" + ), + f"- Remaining pilot days: {progress['remaining_pilot_days']}", + f"- Shadow days: {progress['shadow_days']}", + f"- Invalid execution days: {progress['invalid_execution_days']}", + f"- Latest verified pilot date: {progress.get('latest_verified_pilot_date') or 'N/A'}", + "", + "## Risk Snapshot", + f"- Orders: {risk['orders']}", + f"- Target positions: {risk['target_positions']}", + f"- Max order notional: {float(risk['max_order_notional'] or 0):,.0f}", + f"- Gross exposure after: {float(risk['gross_exposure_after'] or 0):,.0f}", + f"- Liquidity: {'PASS' if risk['liquidity_complete'] else 'BLOCKED'} - {risk['liquidity_reason']}", + ( + f"- Pre-trade risk: " + f"{'PASS' if risk['pre_trade_risk_complete'] else 'BLOCKED'} - " + f"{risk['pre_trade_risk_reason']}" + ), + "", + "## Blocking Reasons", + ] + lines.extend([f"- {reason}" for reason in decision.get("blocking_reasons") or []] or ["- none"]) + lines.extend(["", "## Warnings"]) + lines.extend([f"- {reason}" for reason in decision.get("warning_reasons") or []] or ["- none"]) + lines.extend([ + "", + "## Operator Commands", + "", + "### Collect Shadow Days", + "```bash", + commands.get("collect_shadow_days", ""), + "```", + "", + "### Rerun Readiness Audit", + "```bash", + commands.get("rerun_readiness_audit", ""), + "```", + "", + "### Enable Suggested Caps", + "```bash", + commands.get("enable_suggested_caps", ""), + "```", + "", + "### Execute Capped Paper", + "```bash", + commands.get("execute_capped_paper", ""), + "```", + "", + "## Safety", + "- No orders are submitted by this summary.", + "- This summary does not imply live eligibility.", + ]) + return "\n".join(lines) + "\n" + + +def write_target_weight_daily_ops_summary( + summary: dict[str, Any], + *, + output_dir: Path = DEFAULT_OUTPUT_DIR, +) -> tuple[Path, Path]: + output_dir.mkdir(parents=True, exist_ok=True) + candidate_id = str(summary["candidate_id"]) + trade_day = str(summary["trade_day"]) + stem = f"target_weight_daily_ops_summary_{candidate_id}_{trade_day}" + json_path = output_dir / f"{stem}.json" + md_path = output_dir / f"{stem}.md" + json_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2, default=str), encoding="utf-8") + md_path.write_text(render_target_weight_daily_ops_markdown(summary), encoding="utf-8") + return json_path, md_path + + def _pilot_readiness_audit_path_stem(audit: dict[str, Any]) -> str: return f"target_weight_pilot_readiness_audit_{audit['candidate_id']}_{audit['trade_day']}" @@ -2382,6 +2596,53 @@ def run_pilot_readiness_audit( } +def run_daily_ops_summary( + *, + candidate_id: str = DEFAULT_TARGET_WEIGHT_CANDIDATE_ID, + raw_symbols: str | None = None, + as_of_date: str | None = None, + cash: float | None = None, + preview_caps: dict[str, int] | None = None, + max_order_adv_pct: float = DEFAULT_MAX_ORDER_ADV_PCT, + allow_rerun: bool = False, + output_dir: Path = DEFAULT_OUTPUT_DIR, + config: Any | None = None, + collector: Any | None = None, +) -> dict[str, Any]: + """readiness audit, manifest, evidence progress를 하루 운영 요약으로 저장한다.""" + readiness = run_pilot_readiness_audit( + candidate_id=candidate_id, + raw_symbols=raw_symbols, + as_of_date=as_of_date, + cash=cash, + preview_caps=preview_caps, + max_order_adv_pct=max_order_adv_pct, + allow_rerun=allow_rerun, + output_dir=output_dir, + config=config, + collector=collector, + ) + evidence_progress = summarize_target_weight_evidence_progress( + readiness["audit"]["candidate_id"], + ) + summary = build_target_weight_daily_ops_summary( + audit=readiness["audit"], + experiment_manifest=readiness["experiment_manifest"], + evidence_progress=evidence_progress, + ) + summary_path, summary_report_path = write_target_weight_daily_ops_summary( + summary, + output_dir=output_dir, + ) + return { + **readiness, + "evidence_progress": evidence_progress, + "daily_ops_summary": summary, + "daily_ops_summary_path": summary_path, + "daily_ops_summary_report_path": summary_report_path, + } + + def write_shadow_bootstrap_artifact( *, candidate_id: str, @@ -2939,6 +3200,11 @@ def main() -> None: action="store_true", help="Write a no-order target-weight capped pilot readiness audit artifact.", ) + parser.add_argument( + "--daily-ops-summary", + action="store_true", + help="Write a no-order daily operator summary with readiness, risk, and evidence progress.", + ) parser.add_argument( "--record-shadow-evidence", action="store_true", @@ -3007,6 +3273,8 @@ def main() -> None: or args.shadow_days is not None ) if shadow_batch: + if args.daily_ops_summary: + parser.error("--daily-ops-summary cannot be combined with shadow bootstrap batch options") if args.readiness_audit: parser.error("--readiness-audit cannot be combined with shadow bootstrap batch options") if args.execute or args.collect_evidence: @@ -3079,6 +3347,50 @@ def main() -> None: raise SystemExit(1) return + if args.daily_ops_summary: + if args.readiness_audit or args.execute or args.collect_evidence or args.record_shadow_evidence: + parser.error( + "--daily-ops-summary cannot be combined with --readiness-audit, " + "--execute, --collect-evidence, or --record-shadow-evidence" + ) + + result = run_daily_ops_summary( + candidate_id=args.candidate_id, + raw_symbols=args.symbols, + as_of_date=args.as_of_date, + cash=args.cash, + allow_rerun=args.allow_rerun, + preview_caps=preview_caps, + max_order_adv_pct=args.max_order_adv_pct, + output_dir=Path(args.output_dir), + ) + summary = result["daily_ops_summary"] + progress = summary["evidence_progress"] + print("\nTarget-weight daily ops summary") + print(f" candidate: {summary['candidate_id']}") + print(f" trade_day: {summary['trade_day']}") + print(f" status: {summary['status']}") + print( + " evidence: " + f"verified={progress['verified_pilot_days']}/{progress['target_days']} " + f"remaining={progress['remaining_pilot_days']} " + f"shadow={progress['shadow_days']} " + f"invalid={progress['invalid_execution_days']}" + ) + print(f" next: {summary['next_step']}") + for reason in summary["decision"]["blocking_reasons"][:8]: + print(f" blocker: {reason}") + remaining = len(summary["decision"]["blocking_reasons"]) - 8 + if remaining > 0: + print(f" blocker: +{remaining} more") + print(f" readiness artifact: {result['artifact_path']}") + print(f" experiment manifest: {result['experiment_manifest_path']}") + print(f" summary artifact: {result['daily_ops_summary_path']}") + print(f" summary report: {result['daily_ops_summary_report_path']}") + if summary["status"] == "BLOCKED": + raise SystemExit(1) + return + if args.readiness_audit: if args.execute or args.collect_evidence or args.record_shadow_evidence: parser.error(