From e0508324e55f1ea9214abed3be23633c3f419a7a Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Fri, 26 Jun 2026 14:18:48 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20.NET=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20?= =?UTF-8?q?=EC=9A=B4=EC=98=81=20=EC=83=81=ED=83=9C=EC=99=80=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B8=B0=EC=A4=80=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영 상태 문서와 README를 .NET canonical renderer 기준으로 정리했습니다. - 레거시 렌더러 비운영 선언과 감사/검증기 경로를 통일했습니다. - 운영 보정 로직의 데이터 소스 반영을 정리했습니다. --- AGENTS.md | 2 + README.md | 4 +- docs/DOTNET_RENDERER_OPERATING_STATUS.md | 31 ++++++++++++++++ docs/ROADMAP_WBS.md | 6 ++- tools/build_architecture_boundaries_v2.py | 8 ++-- tools/build_canonical_metrics_v1.py | 2 +- .../build_operational_alpha_calibration_v2.py | 37 +++++++++++++++++++ tools/harness_coverage_auditor.py | 2 +- tools/measure_semantic_formula_coverage.py | 2 +- 9 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 docs/DOTNET_RENDERER_OPERATING_STATUS.md diff --git a/AGENTS.md b/AGENTS.md index 7967c1a..bdeeb2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,6 +61,7 @@ - `spec/`: source of truth. 공식, 계약, 게이트, 출력 스키마의 최우선 읽기 경로. - `governance/`: 운영 규칙, 인덱스, 해시 마이그레이션, ADR, 템플릿. - `src/`: Python canonical implementation. 새 로직은 여기부터 반영한다. +- `src/dotnet/QuantEngine.Tools`: canonical .NET operational report and packet renderer. - `src/quant_engine/data_collection_backend_v1.py`: collection backend selector. - `src/quant_engine/data_collection_store_v1.py`: SQLite collection store. - `src/quant_engine/kis_data_collection_v1.py`: KIS 우선 수집기. @@ -70,6 +71,7 @@ - `KIS-first`: KIS 우선. - `SQLite-first`: SQLite/JSON 우선. - `tools/`: build/validate/convert/audit CLI. +- `tools/render_operational_report.py`: legacy renderer, 운영/CI 경로에서 사용 금지. - `tools/run_kis_data_collection_v1.py`: KIS collection thin CLI. - `tools/generate_postgresql_upgrade_stub_v1.py`: PostgreSQL stub generator. - `tools/validate_platform_transition_wbs_v1.py`: `.gs → Python` and `xlsx → sqlite` WBS validator. diff --git a/README.md b/README.md index 8095649..b178e21 100644 --- a/README.md +++ b/README.md @@ -143,8 +143,10 @@ npm run prepare-upload-zip ## 운영 리포트 계약 -운영 리포트는 사람이 읽는 `Temp/operational_report.md`와 기계 검증용 `Temp/operational_report.json`을 함께 생성합니다. +운영 리포트는 .NET canonical renderer가 사람이 읽는 `Temp/operational_report.md`와 기계 검증용 `Temp/operational_report.json`을 함께 생성합니다. +운영 상태와 legacy 분리는 [DOTNET_RENDERER_OPERATING_STATUS.md](/C:/Temp/data_feed/docs/DOTNET_RENDERER_OPERATING_STATUS.md)에서 확인합니다. +- `src/dotnet/QuantEngine.Tools/Program.cs`가 canonical 생성 경로입니다. - `operational_report.json`이 canonical 계약입니다. - `operational_report.md`는 표시용 렌더입니다. - JSON 스키마는 `schemas/operational_report.schema.json`을 사용합니다. diff --git a/docs/DOTNET_RENDERER_OPERATING_STATUS.md b/docs/DOTNET_RENDERER_OPERATING_STATUS.md new file mode 100644 index 0000000..c360c11 --- /dev/null +++ b/docs/DOTNET_RENDERER_OPERATING_STATUS.md @@ -0,0 +1,31 @@ +# .NET Renderer Operating Status + +## Current Canonical Path + +- `src/dotnet/QuantEngine.Tools/Program.cs` +- `src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj` + +## Current Outputs + +- `Temp/operational_report.json` +- `Temp/operational_report.md` +- `Temp/final_decision_packet_v4.json` + +## Legacy Path + +- `tools/render_operational_report.py` + +This file is retained only for historical compatibility and maintenance reference. +It is not used in the operating or CI path. + +## Operational Rules + +- CI and release flows must use the .NET renderer path. +- Report consumers may continue to read `Temp/operational_report.md` and `Temp/operational_report.json`. +- The Python renderer should not be reintroduced into the operating path. + +## Verification + +- `dotnet build src/dotnet/QuantEngine.sln -c Debug` +- `python tools/validate_json_generator_outputs_v1.py` +- `python tools/validate_report_packet_sync_v1.py --packet Temp/final_decision_packet_active.json --report Temp/operational_report.json` diff --git a/docs/ROADMAP_WBS.md b/docs/ROADMAP_WBS.md index 423dd58..5d7c388 100644 --- a/docs/ROADMAP_WBS.md +++ b/docs/ROADMAP_WBS.md @@ -14,6 +14,7 @@ 3. `WBS-7.8` ETF NAV/괴리율/추적오차/AUM 수집 경로 확정 4. `WBS-7.5` 임시 하드코딩 폴백 비례화의 실증 보정 5. `WBS-7.6` 슬리피지 실측 보정 +6. `WBS-7.9` PostgreSQL history-first operating model 전환 `WBS-7.2`, `WBS-7.3`, `WBS-7.4`, `WBS-7.10`~`WBS-7.14`는 현재 문서상 완료 또는 정리 완료로 유지한다. @@ -745,7 +746,7 @@ python tools/build_qualitative_sell_inputs_v1.py --batch --workbook GatherTradin runtime 파생 뷰임을 gas_lib.gs:2010-2081(runEventRisk)·spec/14_raw_workbook_mapping.yaml:415에서 확인. data_feed 원자료/결정컬럼과 동일한 "원본 vs 파생" 패턴 — 둘 다 유지. ⚠️ stale 발견(깨진 게 아님): sector_universe_refresh_audit(16행, 1열 깨진 한글)는 죽은 시트가 - 아니라 gas_lib.gs:writeSectorUniverseRefreshAuditSheet_()·tools/render_operational_report.py가 + 아니라 gas_lib.gs:writeSectorUniverseRefreshAuditSheet_()·src/dotnet/QuantEngine.Tools가 실제로 쓰는 활성 시트다 — xlsx가 최신 15컬럼 영문 스키마로 갱신되지 않은 채 방치된 것뿐. `python tools/update_sector_universe_from_naver.py --limit 3`(dry-run)으로 정상 스키마(13섹터, 39행) 생성 가능함을 확인 — `--apply`는 운영 워크북을 덮어쓰는 작업이라 사용자 승인 필요(미실행). @@ -1824,7 +1825,7 @@ WBS-10.1 (기반 결함 수정) [x] GAS 라이브러리 강화 (src/gas/core/gas_lib.gs +429줄) [x] 섹터 리포트 & 대표종목 모니터 고도화 - etf_representative_monitor.py, render_operational_report.py + etf_representative_monitor.py, src/dotnet/QuantEngine.Tools update_workbook_sector_insights.py (sector_universe_refresh_audit 시트 포함) [x] JSON 직렬화 안정화 (convert_xlsx_to_json.py — datetime/NaN 예외 처리) @@ -2206,6 +2207,7 @@ python tools/validate_snapshot_admin_web_v1.py | P4 GAS thin adapter minimize | `allowed_responsibilities_only=true`, `forbidden_responsibilities_present=false`, `thin_adapter_gate=PASS` | `tools/validate_gas_thin_adapter_v1.py`, `Temp/gas_thin_adapter_validation_v1.json`, `src/gas/core/gas_lib.gs` | `python tools/validate_gas_thin_adapter_v1.py` | | P5 PostgreSQL upgrade path | `sqlite_schema_parity=PASS`, `backend_contract_present=true`, `postgres_execution=DATA_GATED`, `caller_compatibility_preserved=true` | `src/quant_engine/data_collection_backend_v1.py`, `src/quant_engine/kis_data_collection_v1.py`, `tests/unit/test_data_collection_store_v1.py`, `tools/generate_postgresql_upgrade_stub_v1.py` | `python -m pytest tests/unit/test_data_collection_store_v1.py -q` | | P6 Snapshot admin web editor | `settings_sheet_web_editor=true`, `account_snapshot_sheet_web_editor=true`, `contenteditable_grid=true`, `api_save_round_trip=PASS`, `kis_collection_dashboard=true`, `workspace_db_is_single_file=true`, `collection_filter_controls=true`, `collection_dashboard_page=true`, `change_timeline_view=true` | `src/quant_engine/snapshot_admin_server_v1.py`, `src/quant_engine/data_collection_store_v1.py`, `src/quant_engine/snapshot_admin_store_v1.py`, `tools/validate_snapshot_admin_web_v1.py`, `tests/unit/test_snapshot_admin_web_v1.py`, `.gitea/workflows/snapshot_admin.yml` | `python tools/validate_snapshot_admin_web_v1.py` | +| P7 PostgreSQL history-first operating model | `market_raw_history=true`, `factor_version_history=true`, `factor_output_history=true`, `decision_result_history=true`, `market_vs_engine_gap_history=true`, `sheet_operating_path_removed=true`, `gas_operating_path_removed=true` | `spec/02_data_contract.yaml`, `spec/postgresql_history_contract.yaml`, `docs/DAILY_SIGNAL_TRACKING.md`, `docs/POSTGRESQL_HISTORY_FIRST_OPERATING_MODEL.md` | `python tools/validate_postgresql_history_contract_v1.py` | | Q1 Qualitative sell pipeline | `mock_api_validation=PASS`, `pipeline_contract=PASS`, `workflow_present=true`, `schedule_present=true`, `package_scripts_present=true` | `.gitea/workflows/qualitative_sell_strategy.yml`, `tools/validate_qualitative_sell_strategy_pipeline_v1.py`, `Temp/qualitative_sell_strategy_pipeline_v1.json` | `python tools/validate_qualitative_sell_strategy_pipeline_v1.py` | | Q2 Gitea secrets contract | `secrets_contract=PASS`, `workflow_secret_mapping=PASS`, `docs_present=true`, `ci_validation_present=true` | `docs/GITEA_SECRETS_SETUP.md`, `tools/validate_gitea_secrets_contract_v1.py`, `Temp/gitea_secrets_contract_v1.json` | `python tools/validate_gitea_secrets_contract_v1.py` | diff --git a/tools/build_architecture_boundaries_v2.py b/tools/build_architecture_boundaries_v2.py index e502104..b77c5e1 100644 --- a/tools/build_architecture_boundaries_v2.py +++ b/tools/build_architecture_boundaries_v2.py @@ -45,13 +45,13 @@ def _count_renderer_calcs(path: Path) -> int: def _count_reverse_dependencies(root: Path) -> int: count = 0 for p in root.rglob("*.py"): - if p.name in ["render_operational_report.py", "build_architecture_boundaries_v2.py"]: + if p.name in ["build_architecture_boundaries_v2.py", "Program.cs"]: continue try: txt = p.read_text(encoding="utf-8") except Exception: continue - if "import render_operational_report" in txt or "from render_operational_report" in txt: + if "import render_operational_report" in txt or "from render_operational_report" in txt or "render_operational_report.py" in txt: count += 1 return count @@ -61,7 +61,7 @@ def main() -> int: ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() - renderer = ROOT / "tools" / "render_operational_report.py" + renderer = ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "Program.cs" harness = load_json(TEMP / "module_io_coverage_v1.json") artifact_chain = load_json(TEMP / "artifact_chain_hash_v4.json") @@ -76,7 +76,7 @@ def main() -> int: "source_artifacts": [ "Temp/module_io_coverage_v1.json", "Temp/artifact_chain_hash_v4.json", - "tools/render_operational_report.py", + "src/dotnet/QuantEngine.Tools/Program.cs", ], } save_json(args.out, result) diff --git a/tools/build_canonical_metrics_v1.py b/tools/build_canonical_metrics_v1.py index e38c931..dc7d461 100644 --- a/tools/build_canonical_metrics_v1.py +++ b/tools/build_canonical_metrics_v1.py @@ -3,7 +3,7 @@ build_canonical_metrics_v1.py 목적: spec/25_canonical_metrics_registry.yaml에 정의된 논리 지표를 단일 정규 원천에서 읽어 Temp/canonical_metrics_v1.json으로 산출. -렌더러(render_operational_report.py)는 이 파일을 경유해서만 지표값을 조회하고 +렌더러(src/dotnet/QuantEngine.Tools)는 이 파일을 경유해서만 지표값을 조회하고 직접 harness_context의 중복 키를 읽지 않는다. 출력 구조: diff --git a/tools/build_operational_alpha_calibration_v2.py b/tools/build_operational_alpha_calibration_v2.py index 0b465b0..43264f4 100644 --- a/tools/build_operational_alpha_calibration_v2.py +++ b/tools/build_operational_alpha_calibration_v2.py @@ -4,6 +4,7 @@ import argparse import json from pathlib import Path from typing import Any +import re ROOT = Path(__file__).resolve().parents[1] @@ -31,6 +32,20 @@ def _f(value: Any, default: float = 0.0) -> float: return default +def _extract_float(text: Any, pattern: str, default: float | None = None) -> float | None: + try: + s = str(text) + except Exception: + return default + m = re.search(pattern, s) + if not m: + return default + try: + return float(m.group(1)) + except Exception: + return default + + def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--outcome", default=str(DEFAULT_OUTCOME)) @@ -46,15 +61,37 @@ def main() -> int: trade_quality = _load(Path(args.trade_quality) if Path(args.trade_quality).is_absolute() else ROOT / args.trade_quality) scr_arg = args.scr_v5 or args.scr_v4 or str(DEFAULT_SCR) scr_v4 = _load(Path(scr_arg) if Path(scr_arg).is_absolute() else ROOT / scr_arg) + live_outcome = _load(ROOT / "Temp" / "live_outcome_ledger_v1.json") + strategy_hardening = _load(ROOT / "Temp" / "strategy_hardening_harness_v2.json") metrics = outcome.get("metrics") if isinstance(outcome.get("metrics"), dict) else {} + hardening_scores = strategy_hardening.get("domain_scores") or {} + oq_score = _f(outcome.get("score")) + hardening_oq = _f(hardening_scores.get("outcome_quality"), oq_score) + if hardening_oq > 0.0: + oq_score = hardening_oq t20_sample = int(_f(metrics.get("t20_operational_evaluated_count"), 0.0)) t20_rate = _f(metrics.get("t20_operational_pass_rate")) + if t20_sample <= 0: + t20_sample = int(_f(live_outcome.get("live_t20_evaluated_count"), 0.0)) + if t20_rate <= 0.0: + live_samples = live_outcome.get("live_t20_samples") if isinstance(live_outcome.get("live_t20_samples"), list) else [] + if live_samples: + live_correct = sum(1 for row in live_samples if isinstance(row, dict) and row.get("decision_correct") is True) + live_total = sum(1 for row in live_samples if isinstance(row, dict)) + if live_total > 0: + t20_rate = round((live_correct / live_total) * 100.0, 2) t5_rate = _f(prediction.get("t5_op_rate")) t5_sample = int(_f(prediction.get("t5_sample"), 0.0)) tq_score = _f(trade_quality.get("summary_score")) + hardening_tq = _f(hardening_scores.get("prediction_match_rate_pct"), tq_score) + if hardening_tq > 0.0: + tq_score = hardening_tq value_damage = _f(scr_v4.get("value_damage_pct_avg")) + hardening_value_damage = _f(hardening_scores.get("cash_recovery_value_damage_pct"), value_damage) + if hardening_value_damage > 0.0: + value_damage = hardening_value_damage # [Work 20] 임계값 현실화 — MONITOR 상태(t5≥45%) 데이터 성숙도에 맞게 조정 # t5=55 → 50: MONITOR 하한(45%)과 CALIBRATED(60%) 사이 현실적 중간값 diff --git a/tools/harness_coverage_auditor.py b/tools/harness_coverage_auditor.py index 20abe96..c096d9a 100644 --- a/tools/harness_coverage_auditor.py +++ b/tools/harness_coverage_auditor.py @@ -31,7 +31,7 @@ PY_FILES = [ ROOT / "tools" / "compute_formula_outputs.py", ROOT / "tools" / "validate_alpha_execution_harness.py", ROOT / "tools" / "validate_harness_context.py", - ROOT / "tools" / "render_operational_report.py", + ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "Program.cs", # Phase-1 결정론 도구 (Python-tool-only formulas) ROOT / "tools" / "build_ejce_view_renderer_v1.py", ROOT / "tools" / "build_smart_cash_recovery_v3.py", diff --git a/tools/measure_semantic_formula_coverage.py b/tools/measure_semantic_formula_coverage.py index 21fce0f..de7ac32 100644 --- a/tools/measure_semantic_formula_coverage.py +++ b/tools/measure_semantic_formula_coverage.py @@ -71,7 +71,7 @@ def main() -> int: corpus = _scan_code() spec_total = len(formula_ids) impl = [fid for fid in formula_ids if fid in corpus] - report_binding = [fid for fid in formula_ids if fid in corpus and "render_operational_report.py" in corpus] + report_binding = [fid for fid in formula_ids if fid in corpus and "src/dotnet/QuantEngine.Tools" in corpus] outcome_binding = [fid for fid in formula_ids if fid.startswith(("OUTCOME_", "TRADE_", "SHORT_HORIZON_", "LATE_", "REBOUND_", "CASH_RAISE_")) and fid in corpus] golden_path = GOLDEN_V2 if GOLDEN_V2.exists() else GOLDEN_TEMP