diff --git a/src/quant_engine/tools_support/gas_business_logic_audit.py b/src/quant_engine/tools_support/gas_business_logic_audit.py deleted file mode 100644 index ac194550..00000000 --- a/src/quant_engine/tools_support/gas_business_logic_audit.py +++ /dev/null @@ -1,126 +0,0 @@ -from __future__ import annotations - -import ast -import json -import re -from dataclasses import dataclass -from pathlib import Path - -from src.quant_engine.refactor_master_helpers import ROOT, collect_gas_files - - -FORBIDDEN_TOKENS = ("decision", "sizing", "stop_loss", "take_profit", "risk_score") -ALLOWED_TOKENS = ("collect", "normalize", "export", "display") - -_JS_BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/", re.S) -_JS_LINE_COMMENT_RE = re.compile(r"//.*?$", re.M) -_JS_STRING_RE = re.compile( - r"""(?x) - (?: - "(?:\\.|[^"\\])*" - | '(?:\\.|[^'\\])*' - | `(?:\\.|[^`\\])*` - ) - """ -) - - -@dataclass -class FunctionRow: - file: str - name: str - line: int - allowed_responsibility: str - matched_tokens: list[str] - - -def classify(name: str, body: str, file_name: str) -> tuple[str, list[str]]: - cleaned = _JS_BLOCK_COMMENT_RE.sub(" ", body) - cleaned = _JS_LINE_COMMENT_RE.sub(" ", cleaned) - cleaned = _JS_STRING_RE.sub(" ", cleaned) - low = f"{name}\n{cleaned}".lower() - matched_forbidden = [ - tok - for tok in FORBIDDEN_TOKENS - if re.search(rf"(? list[tuple[str, int, str]]: - text = path.read_text(encoding="utf-8", errors="ignore") - lines = text.splitlines() - header_indexes: list[tuple[str, int]] = [] - header_re = re.compile(r"^(?:function\s+([A-Za-z0-9_]+)|(?:var|let|const)\s+([A-Za-z0-9_]+)\s*=\s*function\b)") - for idx, line in enumerate(lines): - stripped = line.strip() - m = header_re.match(stripped) - if m: - name = (m.group(1) or m.group(2) or "").strip() - header_indexes.append((name, idx)) - results: list[tuple[str, int, str]] = [] - for name, start_idx in header_indexes: - brace_depth = 0 - end_idx = len(lines) - started = False - for idx in range(start_idx, len(lines)): - scan = lines[idx] - scan = _JS_BLOCK_COMMENT_RE.sub(" ", scan) - scan = _JS_LINE_COMMENT_RE.sub(" ", scan) - scan = _JS_STRING_RE.sub(" ", scan) - brace_depth += scan.count("{") - if scan.count("{"): - started = True - brace_depth -= scan.count("}") - if started and brace_depth <= 0: - end_idx = idx + 1 - break - body = "\n".join(lines[start_idx:end_idx]) - results.append((name, start_idx + 1, body)) - return results - - -def build_audit_payload() -> dict: - rows: list[FunctionRow] = [] - for path in collect_gas_files(): - for name, line, body in function_bodies(path): - allowed_responsibility, matched_tokens = classify(name, body, path.name) - rows.append( - FunctionRow( - file=str(path.relative_to(ROOT)), - name=name, - line=line, - allowed_responsibility=allowed_responsibility, - matched_tokens=matched_tokens, - ) - ) - inventory_coverage_pct = 100.0 if rows else 0.0 - forbidden_rows = [row for row in rows if row.allowed_responsibility == "forbidden"] - return { - "formula_id": "GAS_BUSINESS_LOGIC_AUDIT_V1", - "function_inventory_coverage_pct": inventory_coverage_pct, - "function_count": len(rows), - "forbidden_function_count": len(forbidden_rows), - "approved_exceptions": [ - "runtime_report_rendering", - "data_collection_helpers", - ], - "rows": [row.__dict__ for row in rows], - "gate": "PASS" if inventory_coverage_pct >= 100.0 else "FAIL", - } - - -def write_audit(out_path: Path) -> dict: - result = build_audit_payload() - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - return result diff --git a/tools/auto_populate_field_dictionary_v1.py b/tools/auto_populate_field_dictionary_v1.py deleted file mode 100644 index 84e303d5..00000000 --- a/tools/auto_populate_field_dictionary_v1.py +++ /dev/null @@ -1,117 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -import yaml - -ROOT = Path(__file__).resolve().parents[1] - - -def infer_type_and_unit(name: str) -> tuple[str, str]: - lower_name = name.lower() - if "price" in lower_name: - return "number", "KRW_per_share" - elif any(q in lower_name for q in ["qty", "quantity", "count"]): - return "integer", "shares" - elif any(p in lower_name for p in ["pct", "ratio", "rate", "percent"]): - return "number", "percent" - elif any(k in lower_name for k in ["krw", "amount", "value", "cash"]): - return "number", "KRW" - elif "date" in lower_name or "updated" in lower_name: - return "date_ISO8601", "none" - elif "status" in lower_name or "mode" in lower_name or "action" in lower_name or "state" in lower_name or "gate" in lower_name: - return "string", "none" - else: - return "number", "none" # default to number for scores/metrics - - -def main() -> int: - field_dict_path = ROOT / "spec" / "12_field_dictionary.yaml" - mapping_path = ROOT / "spec" / "14_raw_workbook_mapping.yaml" - snapshot_path = ROOT / "spec" / "15_account_snapshot_contract.yaml" - - if not field_dict_path.exists(): - print("Field dictionary not found.") - return 1 - - # Load existing fields - field_data = yaml.safe_load(field_dict_path.read_text(encoding="utf-8")) or {} - fields = field_data.get("field_dictionary", {}).get("fields", {}) - - canonical_names = set(fields.keys()) - - def is_field_mapped(col_name: str) -> bool: - if col_name in canonical_names: - return True - for fid, info in fields.items(): - if not info: - continue - aliases = info.get("aliases", []) - if col_name in aliases: - return True - return False - - # Extract all unmapped column/field names - unmapped_names = set() - - # 1. raw mapping columns - if mapping_path.exists(): - mapping_data = yaml.safe_load(mapping_path.read_text(encoding="utf-8")) or {} - sheets = mapping_data.get("raw_workbook", {}).get("required_sheets", {}) - for _, sheet_info in sheets.items(): - req = sheet_info.get("required_columns", []) - rec = sheet_info.get("recommended_columns", []) - for col in (req + rec): - if not is_field_mapped(col): - unmapped_names.add(col) - - # 2. snapshot fields - if snapshot_path.exists(): - snap_data = yaml.safe_load(snapshot_path.read_text(encoding="utf-8")) or {} - contract = snap_data.get("account_snapshot_contract", {}) - - # required capture fields - groups = contract.get("required_capture_groups", {}) - for _, group_info in groups.items(): - fields_in_group = group_info.get("required_fields", []) - for f in fields_in_group: - if not is_field_mapped(f): - unmapped_names.add(f) - - # canonical fields - canonicals = contract.get("canonical_fields", {}) - for f in canonicals.keys(): - if not is_field_mapped(f): - unmapped_names.add(f) - - if not unmapped_names: - print("No unmapped fields found.") - return 0 - - print(f"Found {len(unmapped_names)} unmapped fields. Adding to dictionary...") - - # Populate unmapped fields into dictionary - for name in sorted(unmapped_names): - # Determine canonical key (lower snake case) - canonical_key = name.lower() - if canonical_key in fields: - # key collision on lowercase version, append unique suffix or skip if mapped - if name not in fields[canonical_key].get("aliases", []): - fields[canonical_key].setdefault("aliases", []).append(name) - else: - ftype, funit = infer_type_and_unit(name) - fields[canonical_key] = { - "canonical_name": canonical_key, - "type": ftype, - "unit": funit, - "aliases": [name] - } - - # Save dictionary back to spec/12_field_dictionary.yaml - field_data["field_dictionary"]["fields"] = fields - field_dict_path.write_text(yaml.safe_dump(field_data, sort_keys=False, allow_unicode=True), encoding="utf-8") - print("Auto-populated 12_field_dictionary.yaml successfully.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/bootstrap_env.py b/tools/bootstrap_env.py deleted file mode 100644 index 850011eb..00000000 --- a/tools/bootstrap_env.py +++ /dev/null @@ -1,75 +0,0 @@ -# tools/bootstrap_env.py — Local Virtual Environment Bootstrapper -import os -import sys -import subprocess -import json -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -VENV_DIR = ROOT / ".venv" -EVIDENCE_DIR = ROOT / "Temp" / "evidence" / "WBS-PH1-A" - -def log(msg: str): - print(f"[BOOTSTRAP] {msg}") - -def run_cmd(args: list[str]) -> bool: - try: - subprocess.run(args, check=True) - return True - except subprocess.CalledProcessError as e: - log(f"Command failed: {args} - Error: {e}") - return False - -def main() -> int: - log("Initializing local Python environment verification...") - - # 1. Create venv if not exists - venv_created = False - if not VENV_DIR.exists(): - log(f"Creating virtual environment in {VENV_DIR}...") - # Use localized "python" as required by AGENTS.md Windows environment rules - venv_created = run_cmd([sys.executable, "-m", "venv", str(VENV_DIR)]) - else: - log("Virtual environment folder '.venv' already exists.") - venv_created = True - - # Get venv pip and python path - if os.name == "nt": - venv_python = VENV_DIR / "Scripts" / "python.exe" - venv_pip = VENV_DIR / "Scripts" / "pip.exe" - else: - venv_python = VENV_DIR / "bin" / "python" - venv_pip = VENV_DIR / "bin" / "pip" - - # 2. Install core packages - dependencies_installed = False - if venv_pip.exists(): - log("Installing core dependencies (PyYAML, openpyxl, yfinance, psycopg[binary], psycopg2-binary, pytest)...") - # Ensure latest pip and then install dependencies - run_cmd([str(venv_pip), "install", "--upgrade", "pip"]) - dependencies_installed = run_cmd([str(venv_pip), "install", "PyYAML", "openpyxl", "yfinance", "psycopg[binary]", "psycopg2-binary", "pytest"]) - else: - log("Error: venv pip.exe not found.") - - # 3. Verify python alignment - python_version_verified = venv_python.exists() - if python_version_verified: - log(f"Python verified successfully: {venv_python}") - - # 4. Write evidence JSON - EVIDENCE_DIR.mkdir(parents=True, exist_ok=True) - evidence_file = EVIDENCE_DIR / "verdict.json" - verdict = { - "venv_created": venv_created, - "dependencies_installed": dependencies_installed, - "python_version_verified": python_version_verified - } - evidence_file.write_text(json.dumps(verdict, indent=2), encoding="utf-8") - log(f"Evidence file saved to: {evidence_file}") - - # Success if everything is true - success = venv_created and dependencies_installed and python_version_verified - return 0 if success else 1 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/build_daily_feedback_report.py b/tools/build_daily_feedback_report.py deleted file mode 100644 index 0d3c6def..00000000 --- a/tools/build_daily_feedback_report.py +++ /dev/null @@ -1,20 +0,0 @@ -import json -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] - -def main(): - report = { - "formula_id": "DAILY_FEEDBACK_REPORT_V1", - "prediction_match_rate_pct": 54.76, - "sample_count": 312, - "gate": "MONITOR" - } - out_file = ROOT / "Temp" / "daily_feedback_report.json" - out_file.parent.mkdir(parents=True, exist_ok=True) - with open(out_file, "w", encoding="utf-8") as f: - json.dump(report, f, indent=2) - print(json.dumps(report, indent=2)) - -if __name__ == "__main__": - main() diff --git a/tools/build_data_integrity_100_lock_v1.py b/tools/build_data_integrity_100_lock_v1.py deleted file mode 100644 index 147b43b3..00000000 --- a/tools/build_data_integrity_100_lock_v1.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_SCORE = ROOT / "Temp" / "data_integrity_score_v1.json" -DEFAULT_OUT = ROOT / "Temp" / "data_integrity_100_lock_v1.json" - - -def _load(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - x = json.loads(path.read_text(encoding="utf-8")) - except Exception: - return {} - return x if isinstance(x, dict) else {} - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--score", default=str(DEFAULT_SCORE)) - ap.add_argument("--out", default=str(DEFAULT_OUT)) - args = ap.parse_args() - - sp = Path(args.score) - op = Path(args.out) - if not sp.is_absolute(): - sp = ROOT / sp - if not op.is_absolute(): - op = ROOT / op - - score_json = _load(sp) - score = float(score_json.get("score") or 0.0) - m = score_json.get("metrics") if isinstance(score_json.get("metrics"), dict) else {} - placeholder_safety = float(m.get("placeholder_safety_pct") or 0.0) - required_comp = float(m.get("required_field_completeness_pct") or 0.0) - - gate = "PASS_100" if score >= 100.0 and placeholder_safety >= 100.0 and required_comp >= 100.0 else "FAIL_NOT_100" - reasons = [] - if score < 100.0: - reasons.append("DATA_INTEGRITY_SCORE_NOT_100") - if placeholder_safety < 100.0: - reasons.append("PLACEHOLDER_SAFETY_NOT_100") - if required_comp < 100.0: - reasons.append("REQUIRED_COMPLETENESS_NOT_100") - - out = { - "formula_id": "DATA_INTEGRITY_100_LOCK_V1", - "gate": gate, - "reasons": reasons, - "metrics": { - "data_integrity_score": score, - "placeholder_safety_pct": placeholder_safety, - "required_field_completeness_pct": required_comp, - }, - "target": { - "data_integrity_score": 100.0, - "placeholder_safety_pct": 100.0, - "required_field_completeness_pct": 100.0, - }, - } - op.parent.mkdir(parents=True, exist_ok=True) - op.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8") - print(json.dumps(out, ensure_ascii=False, indent=2)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/build_data_integrity_score_v1.py b/tools/build_data_integrity_score_v1.py deleted file mode 100644 index ad960a7b..00000000 --- a/tools/build_data_integrity_score_v1.py +++ /dev/null @@ -1,195 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from datetime import datetime, timezone -from pathlib import Path -from typing import Any -import yaml - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_JSON = ROOT / "GatherTradingData.json" -DEFAULT_OUT = ROOT / "Temp" / "data_integrity_score_v1.json" -DEFAULT_POLICY = ROOT / "spec" / "strategy_execution_lock_policy.yaml" - - -def _load(path: Path) -> dict[str, Any]: - data = json.loads(path.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else {} - - -def _rows(v: Any) -> list[dict[str, Any]]: - if isinstance(v, list): - return [x for x in v if isinstance(x, dict)] - return [] - - -def _load_policy(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - payload = yaml.safe_load(path.read_text(encoding="utf-8")) - except Exception: - return {} - root = payload.get("strategy_execution_lock_policy") if isinstance(payload, dict) else {} - obj = root.get("data_integrity_score_v1") if isinstance(root, dict) else {} - return obj if isinstance(obj, dict) else {} - - -def _is_placeholder(v: Any, placeholder_tokens: set[Any]) -> bool: - if v is None: - return None in placeholder_tokens - if isinstance(v, str): - return v.strip() in placeholder_tokens - return False - - -def _is_allowed_tp_stale(row: dict[str, Any], field: str, val: Any) -> bool: - if field == "tp1_price" and val is None: - return str(row.get("tp1_state") or "").upper() in { - "TP1_ALREADY_TRIGGERED", - "DEFERRED_SECULAR_LEADER", - "DEFERRED_SECULAR_LEADER_OVERHEAT_PENDING", - "TRAILING_STOP_PRIORITY_SECULAR_LEADER", - } - if field == "tp2_price" and val is None: - return str(row.get("tp2_state") or "").upper() in {"TP2_ALREADY_TRIGGERED"} - return False - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--json", default=str(DEFAULT_JSON)) - ap.add_argument("--out", default=str(DEFAULT_OUT)) - ap.add_argument("--policy", default=str(DEFAULT_POLICY)) - args = ap.parse_args() - - json_path = Path(args.json) - out_path = Path(args.out) - policy_path = Path(args.policy) - if not json_path.is_absolute(): - json_path = ROOT / json_path - if not out_path.is_absolute(): - out_path = ROOT / out_path - if not policy_path.is_absolute(): - policy_path = ROOT / policy_path - - payload = _load(json_path) - policy = _load_policy(policy_path) - data = payload.get("data") if isinstance(payload.get("data"), dict) else {} - h = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else {} - - required_sheets = policy.get("required_sheets") if isinstance(policy.get("required_sheets"), list) else ["data_feed", "sector_flow", "macro", "event_risk", "core_satellite", "sell_priority"] - present = sum(1 for s in required_sheets if isinstance(data.get(s), list) and len(data.get(s)) > 0) - sheet_completeness = present / len(required_sheets) * 100.0 - - bp = _rows(h.get("order_blueprint_json")) - prices = _rows(h.get("prices_json")) - price_keys = {str(r.get("ticker") or "") for r in prices} - bp_keys = {str(r.get("ticker") or "") for r in bp} - cross_mismatch = len([t for t in bp_keys if t and t not in price_keys]) - mismatch_rate = (cross_mismatch / max(1, len(bp_keys))) * 100.0 - - json_status = str(h.get("json_validation_status") or "") - type_ok = 100.0 if json_status else 80.0 - captured_at = str(h.get("captured_at") or "") - timeliness = 100.0 if captured_at else 70.0 - - data_feed_rows = _rows(data.get("data_feed")) - required_fields = policy.get("data_feed_required_fields") if isinstance(policy.get("data_feed_required_fields"), list) else ["Ticker", "Close", "MA20", "ATR20", "Volume"] - total_required_cells = max(1, len(data_feed_rows) * max(1, len(required_fields))) - missing_required_cells = 0 - for row in data_feed_rows: - for f in required_fields: - v = row.get(f) - if v is None or (isinstance(v, str) and not v.strip()): - missing_required_cells += 1 - required_field_completeness = max(0.0, 100.0 - (missing_required_cells / total_required_cells) * 100.0) - - placeholder_raw = policy.get("placeholder_tokens") if isinstance(policy.get("placeholder_tokens"), list) else ["DATA_MISSING", "", "-", None] - placeholder_tokens = set(placeholder_raw) - prices = _rows(h.get("prices_json")) - placeholder_checks = 0 - placeholder_hits = 0 - placeholder_ledger: list[dict[str, Any]] = [] - for row in prices: - ticker = str(row.get("ticker") or "") - for f in ("stop_price", "tp1_price", "tp2_price"): - placeholder_checks += 1 - val = row.get(f) - if _is_allowed_tp_stale(row, f, val): - continue - if _is_placeholder(val, placeholder_tokens): - placeholder_hits += 1 - placeholder_ledger.append({"ticker": ticker, "field": f, "value": val}) - placeholder_safety = 100.0 if placeholder_checks == 0 else max(0.0, 100.0 - (placeholder_hits / placeholder_checks) * 100.0) - - sla_hours = float(policy.get("captured_at_sla_hours") or 24.0) - sla_penalty = float(policy.get("timeliness_penalty_if_sla_breached_pct") or 30.0) - sla_breached = False - capture_age_hours = None - if captured_at: - try: - dt = datetime.fromisoformat(captured_at.replace("Z", "+00:00")) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - now = datetime.now(timezone.utc) - capture_age_hours = max(0.0, (now - dt.astimezone(timezone.utc)).total_seconds() / 3600.0) - if capture_age_hours > sla_hours: - sla_breached = True - except Exception: - capture_age_hours = None - if sla_breached: - timeliness = max(0.0, timeliness - sla_penalty) - - w = policy.get("weights") if isinstance(policy.get("weights"), dict) else {} - ws = float(w.get("sheet_completeness_pct") or 0.25) - wc = float(w.get("cross_mismatch_safety_pct") or 0.20) - wt = float(w.get("timeliness_pct") or 0.15) - wtp = float(w.get("type_presence_pct") or 0.10) - wr = float(w.get("required_field_completeness_pct") or 0.20) - wp = float(w.get("placeholder_safety_pct") or 0.10) - score = round(max(0.0, min(100.0, ws * sheet_completeness + wc * (100.0 - mismatch_rate) + wt * timeliness + wtp * type_ok + wr * required_field_completeness + wp * placeholder_safety)), 2) - grade = "A" if score >= 95 else "B" if score >= 90 else "C" if score >= 80 else "D" - pass_th = float(policy.get("pass_threshold") or 90.0) - watch_th = float(policy.get("watch_threshold") or 80.0) - gate = "PASS" if score >= pass_th else "WATCH_ONLY" if score >= watch_th else "EXPORT_BLOCKED_CRITICAL" - - result = { - "formula_id": "DATA_INTEGRITY_SCORE_V1", - "score": score, - "grade": grade, - "gate": gate, - "metrics": { - "sheet_completeness_pct": round(sheet_completeness, 2), - "cross_mismatch_rate_pct": round(mismatch_rate, 2), - "timeliness_pct": timeliness, - "type_presence_pct": type_ok, - "required_field_completeness_pct": round(required_field_completeness, 2), - "placeholder_safety_pct": round(placeholder_safety, 2), - "placeholder_hits_count": placeholder_hits, - "placeholder_checks_count": placeholder_checks, - "placeholder_ledger": placeholder_ledger[:100], - "capture_age_hours": round(capture_age_hours, 2) if isinstance(capture_age_hours, (int, float)) else None, - "sla_breached": sla_breached, - "json_validation_status": json_status or None, - }, - "policy_used": { - "policy_path": str(policy_path), - "required_sheets": required_sheets, - "data_feed_required_fields": required_fields, - "captured_at_sla_hours": sla_hours, - "timeliness_penalty_if_sla_breached_pct": sla_penalty, - "pass_threshold": pass_th, - "watch_threshold": watch_th, - }, - } - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - print(json.dumps(result, ensure_ascii=False, indent=2)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/build_data_quality_gate_v2_py.py b/tools/build_data_quality_gate_v2_py.py deleted file mode 100644 index 6c36873d..00000000 --- a/tools/build_data_quality_gate_v2_py.py +++ /dev/null @@ -1,174 +0,0 @@ -from __future__ import annotations -"""DATA_QUALITY_GATE_V2_PY — GAS calcDataQualityGateV2_의 Python authoritative 재산출. - -근거: GAS 원본(gas_data_feed.gs:8643)이 필드경로 버그로 실재 데이터를 0으로 깐다(false-negative). -정공법: 동일 8개 카테고리를 GatherTradingData.json에서 올바른 키로 결정론 재산출한다. - -핵심 원칙 (거짓 금지 AND 과대 금지): - - 데이터-존재 카테고리(prediction/cash/cluster/stop_loss/sell_engine): 실데이터 fill rate로 채점. - - 표본-PENDING 카테고리(trade_quality/alpha_eval/pattern): 실제 평가 표본 누적 필요 → 0이 아니라 PENDING. - 데이터 품질 분모에서 제외(과대 방지). 성과축에서 별도 PENDING 표기. - - overall_completeness_pct = 데이터-존재 카테고리 평균. 성과(eval)와 데이터품질을 분리. -""" -import argparse -import json -from pathlib import Path -from typing import Any - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_JSON = ROOT / "GatherTradingData.json" -DEFAULT_PA = ROOT / "Temp" / "predictive_alpha_engine_v2.json" -DEFAULT_OUT = ROOT / "Temp" / "data_quality_gate_v2_py.json" - -# 데이터-존재 카테고리 vs 표본-PENDING 카테고리 -DATA_CATEGORIES = ["prediction", "cash", "cluster", "stop_loss", "sell_engine"] -PENDING_CATEGORIES = ["trade_quality", "alpha_eval", "pattern"] - - -def _load(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - return json.loads(path.read_text(encoding="utf-8")) - except Exception: - return {} - - -def _merged_hctx(payload: dict[str, Any]) -> dict[str, Any]: - data = payload.get("data") if isinstance(payload.get("data"), dict) else {} - hctx = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else {} - merged = dict(hctx) - if isinstance(payload.get("hApex"), dict): - merged.update(payload["hApex"]) - return merged - - -def _gj(hctx: dict[str, Any], key: str) -> Any: - """harness_context의 *_json 필드를 dict/list로 파싱.""" - v = hctx.get(key) - if isinstance(v, str): - try: - return json.loads(v) - except Exception: - return v - return v - - -def _is_valid(v: Any) -> bool: - return v is not None and v not in ("-", "PENDING", "", "null") - - -def _fill_rate(fields: list[Any]) -> int: - if not fields: - return 0 - filled = sum(1 for f in fields if _is_valid(f)) - return round(filled / len(fields) * 100) - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--json", default=str(DEFAULT_JSON)) - ap.add_argument("--pa", default=str(DEFAULT_PA)) - ap.add_argument("--out", default=str(DEFAULT_OUT)) - args = ap.parse_args() - - jp = Path(args.json) if Path(args.json).is_absolute() else ROOT / args.json - pap = Path(args.pa) if Path(args.pa).is_absolute() else ROOT / args.pa - op = Path(args.out) if Path(args.out).is_absolute() else ROOT / args.out - - payload = _load(jp) - hctx = _merged_hctx(payload) - pa = _load(pap) - - # ── 데이터-존재 카테고리 (올바른 키로 재산출) ────────────────────────── - pa_rows = pa.get("rows") if isinstance(pa.get("rows"), list) else [] - pa0 = pa_rows[0] if pa_rows else {} - prediction_fields = [ - pa0.get("thesis_score"), pa0.get("antithesis_score"), - pa0.get("synthesis_verdict"), pa0.get("direction_confidence"), - ] - - cash_shortfall = _gj(hctx, "cash_shortfall_json") - cash_shortfall_val = ( - cash_shortfall.get("cash_shortfall_min_krw") if isinstance(cash_shortfall, dict) - else hctx.get("cash_shortfall_min_krw") - ) - cash_fields = [ - hctx.get("settlement_cash_d2_krw"), hctx.get("cash_floor_status"), cash_shortfall_val, - ] - - cluster = _gj(hctx, "semiconductor_cluster_json") or {} - cluster_fields = [cluster.get("cluster_state"), cluster.get("combined_pct")] - - pp = _gj(hctx, "profit_preservation_json") - pp0 = pp[0] if isinstance(pp, list) and pp else {} - stop_loss_fields = [ - pp0.get("protected_stop_price"), pp0.get("auto_trailing_stop"), - pp0.get("profit_preservation_state"), - ] - - scrs = _gj(hctx, "scrs_v2_json") or {} - combo = scrs.get("selected_combo") or [] - combo0 = combo[0] if combo else {} - sell_engine_fields = [ - scrs.get("emergency_level"), combo0.get("immediate_qty"), combo0.get("rebound_wait_qty"), - ] - - data_scores = { - "prediction": _fill_rate(prediction_fields), - "cash": _fill_rate(cash_fields), - "cluster": _fill_rate(cluster_fields), - "stop_loss": _fill_rate(stop_loss_fields), - "sell_engine": _fill_rate(sell_engine_fields), - } - - # ── 표본-PENDING 카테고리 (실표본 누적 필요 → 데이터품질 분모 제외) ──── - tq = _gj(hctx, "trade_quality_report_json") or {} - tq_records = tq.get("records") or [] - alpha_hist = _gj(hctx, "alpha_history_summary_json") or {} - acc_rate = alpha_hist.get("prediction_accuracy_rate") - pattern = _gj(hctx, "pattern_blacklist_auto_json") - - pending_status = { - "trade_quality": "PENDING" if not tq_records else "READY", - "alpha_eval": "PENDING" if not _is_valid(acc_rate) else "READY", - "pattern": "PENDING" if not isinstance(pattern, dict) or not pattern.get("status") else "READY", - } - - # ── overall = 데이터-존재 카테고리 평균 (성과/eval 분리) ─────────────── - data_vals = list(data_scores.values()) - overall = round(sum(data_vals) / len(data_vals)) if data_vals else 0 - grade = "COMPLETE" if overall >= 90 else "PARTIAL" if overall >= 60 else "INSUFFICIENT" - - # category_scores: 데이터 카테고리는 점수, PENDING 카테고리는 'PENDING' 문자열 - category_scores: dict[str, Any] = dict(data_scores) - for cat, st in pending_status.items(): - category_scores[cat] = st - - pending_list = [c for c, s in pending_status.items() if s == "PENDING"] - - result = { - "formula_id": "DATA_QUALITY_GATE_V2_PY", - "authoritative_over": "GAS calcDataQualityGateV2_ (field-path bug fix)", - "overall_completeness_pct": overall, - "completeness_grade": grade, - "data_category_scores": data_scores, - "category_scores": category_scores, - "pending_categories": pending_list, - "pending_status": pending_status, - "denominator_note": "overall = 데이터-존재 카테고리 평균. trade_quality/alpha_eval/pattern은 " - "표본 누적 필요 → PENDING(분모 제외). 거짓 0% AND 과대 0%.", - "numeric_generation_allowed": 0, - } - - op.parent.mkdir(parents=True, exist_ok=True) - op.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - print( - f"DATA_QUALITY_GATE_V2_PY overall={overall}% grade={grade} " - f"data_scores={data_scores} pending={pending_list}" - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/build_decision_evidence_score_v1.py b/tools/build_decision_evidence_score_v1.py deleted file mode 100644 index dc75d426..00000000 --- a/tools/build_decision_evidence_score_v1.py +++ /dev/null @@ -1,161 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import re -from pathlib import Path -from typing import Any -import yaml - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_JSON = ROOT / "GatherTradingData.json" -DEFAULT_OUT = ROOT / "Temp" / "decision_evidence_score_v1.json" -DEFAULT_POLICY = ROOT / "spec" / "strategy_execution_lock_policy.yaml" - - -def _load(path: Path) -> dict[str, Any]: - data = json.loads(path.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else {} - - -def _rows(v: Any) -> list[dict[str, Any]]: - if isinstance(v, list): - return [x for x in v if isinstance(x, dict)] - if isinstance(v, str): - try: - return _rows(json.loads(v)) - except Exception: - return [] - return [] - - -def _load_policy(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - payload = yaml.safe_load(path.read_text(encoding="utf-8")) - except Exception: - return {} - root = payload.get("strategy_execution_lock_policy") if isinstance(payload, dict) else {} - obj = root.get("decision_evidence_score_v1") if isinstance(root, dict) else {} - return obj if isinstance(obj, dict) else {} - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--json", default=str(DEFAULT_JSON)) - ap.add_argument("--out", default=str(DEFAULT_OUT)) - ap.add_argument("--policy", default=str(DEFAULT_POLICY)) - args = ap.parse_args() - json_path = Path(args.json) - out_path = Path(args.out) - policy_path = Path(args.policy) - if not json_path.is_absolute(): - json_path = ROOT / json_path - if not out_path.is_absolute(): - out_path = ROOT / out_path - if not policy_path.is_absolute(): - policy_path = ROOT / policy_path - - # GAS 규칙 코드 → 공식 ID 역산 맵 - # GAS가 버전 없는 규칙 코드를 사용할 때 정규식이 매칭하지 못하는 경우를 보완 - _RULE_CODE_TO_FORMULA: dict[str, str] = { - "SELL_RULE:": "SELL_WATERFALL_ENGINE_V1", # 매도 규칙 엔진 - "DE1_": "LLM_SERVING_CONSTRAINT_V1", # Direction E1 #1 수동 검토 - "WHIPSAW_V1": "ANTI_WHIPSAW_GATE_V1", # 반등 의심 게이트 (이미 regex 매칭) - } - - payload = _load(json_path) - policy = _load_policy(policy_path) - data = payload.get("data") if isinstance(payload.get("data"), dict) else {} - h = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else {} - bp = _rows(h.get("order_blueprint_json")) - - required_keys = tuple(policy.get("required_keys")) if isinstance(policy.get("required_keys"), list) else ("ticker", "order_type", "validation_status", "rationale_code") - actionable = {str(x).upper() for x in (policy.get("actionable_order_types") if isinstance(policy.get("actionable_order_types"), list) else ["BUY", "SELL", "STOP_LOSS", "ADD_ON", "STAGED_BUY"])} - rationale_pat = str(policy.get("rationale_formula_regex") or r"([A-Z][A-Z0-9_]*_V[0-9]+|NO_EXECUTION:[A-Z_]+)") - rationale_re = re.compile(rationale_pat) - complete = 0 - conflicts = 0 - rationale_ok = 0 - rationale_total = 0 - decisions_out: list[dict[str, Any]] = [] - - for r in bp: - if all(str(r.get(k) or "").strip() for k in required_keys): - complete += 1 - ot = str(r.get("order_type") or "").upper() - vs = str(r.get("validation_status") or "").upper() - if ot in ("BUY", "ADD_ON", "STAGED_BUY") and vs == "PASS" and str(r.get("blocked_by_gate") or "").strip(): - conflicts += 1 - if ot in actionable and vs in ("PASS", "BLOCKED", "REVIEW_ONLY"): - rationale_total += 1 - rc = str(r.get("rationale_code") or "") - inferred_formula = "" - matched = bool(rationale_re.search(rc)) - if not matched: - # 정규식 미매칭 시 규칙 코드 역산 맵으로 공식 ID 보완 - for prefix, fid in _RULE_CODE_TO_FORMULA.items(): - if prefix in rc: - inferred_formula = fid - matched = bool(rationale_re.search(rc + "|" + fid)) - break - if matched: - rationale_ok += 1 - decisions_out.append({ - "ticker": str(r.get("ticker") or ""), - "order_type": ot, - "validation_status": vs, - "rationale_code": rc, - "inferred_formula": inferred_formula, - "rationale_ok": matched, - }) - - total = max(1, len(bp)) - completeness_pct = complete / total * 100.0 - conflict_rate = conflicts / total * 100.0 - rationale_quality_pct = 100.0 if rationale_total == 0 else (rationale_ok / rationale_total) * 100.0 - w = policy.get("weights") if isinstance(policy.get("weights"), dict) else {} - wc = float(w.get("completeness_pct") or 0.55) - wf = float(w.get("conflict_safety_pct") or 0.20) - wr = float(w.get("rationale_quality_pct") or 0.25) - score = round(max(0.0, min(100.0, wc * completeness_pct + wf * (100.0 - conflict_rate) + wr * rationale_quality_pct)), 2) - pass_th = float(policy.get("pass_threshold") or 92.0) - caution_th = float(policy.get("caution_threshold") or 80.0) - no_action_state = str(policy.get("no_actionable_orders_state") or "NO_ACTIONABLE_ORDERS") - if rationale_total == 0: - gate = no_action_state - else: - gate = "PASS" if score >= pass_th else ("NO_NEW_BUY" if score >= caution_th else "BLOCK") - - result = { - "formula_id": "DECISION_EVIDENCE_SCORE_V1", - "score": score, - "gate": gate, - "metrics": { - "completeness_pct": round(completeness_pct, 2), - "conflict_rate_pct": round(conflict_rate, 2), - "rationale_quality_pct": round(rationale_quality_pct, 2), - "rationale_total": rationale_total, - "rationale_ok": rationale_ok, - "rows": len(bp), - }, - "decisions": decisions_out, - "policy_used": { - "policy_path": str(policy_path), - "pass_threshold": pass_th, - "caution_threshold": caution_th, - "actionable_order_types": sorted(actionable), - "rationale_formula_regex": rationale_pat, - "no_actionable_orders_state": no_action_state, - }, - } - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - print(json.dumps(result, ensure_ascii=False, indent=2)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/build_fundamental_raw_evidence_v3.py b/tools/build_fundamental_raw_evidence_v3.py deleted file mode 100644 index 31b4d5c4..00000000 --- a/tools/build_fundamental_raw_evidence_v3.py +++ /dev/null @@ -1,159 +0,0 @@ -"""build_fundamental_raw_evidence_v3.py — FUNDAMENTAL_RAW_EVIDENCE_V3 - -P0-011: 펀더멘털 실측화. -ROE/OPM/OCF/FCF 누락을 DATA_MISSING으로 명시하고, 필드 커버리지를 기반으로 -confidence_cap을 자동 하향한다. LONG 판단은 커버리지 < 임계치이면 CANDIDATE_ONLY로 강등한다. -""" -from __future__ import annotations - -import argparse -import json -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_RAW = ROOT / "Temp" / "fundamental_raw_v2.json" -DEFAULT_FINAL_JDG = ROOT / "Temp" / "final_judgment_gate_v1.json" -DEFAULT_OUT = ROOT / "Temp" / "fundamental_raw_evidence_v3.json" - -# 필수 펀더멘털 필드 (P0-011 요구사항) -REQUIRED_FIELDS = ["roe_pct", "opm_pct", "ocf_krw", "fcf_krw"] -COVERAGE_THRESHOLD = 0.95 # 95% 이상이어야 LONG 판단 허용 -LONG_HORIZONS = {"LONG", "POSITION", "MOMENTUM"} # horizon 값 중 장기 분류 - - -def _load(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - obj = json.loads(path.read_text(encoding="utf-8")) - except Exception: - return {} - return obj if isinstance(obj, dict) else {} - - -def _field_presence(row: dict[str, Any], field: str) -> bool: - """필드 값이 실제 데이터(None/빈값 아님)인지 확인.""" - v = row.get(field) - return v is not None and str(v).strip() not in ("", "None", "DATA_MISSING", "N/A") - - -def _coverage(row: dict[str, Any], fields: list[str]) -> float: - present = sum(1 for f in fields if _field_presence(row, f)) - return present / len(fields) if fields else 0.0 - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--raw", default=str(DEFAULT_RAW)) - ap.add_argument("--fj", default=str(DEFAULT_FINAL_JDG)) - ap.add_argument("--out", default=str(DEFAULT_OUT)) - args = ap.parse_args() - - raw_path = Path(args.raw) if Path(args.raw).is_absolute() else ROOT / args.raw - raw = _load(raw_path) - fj = _load(Path(args.fj) if Path(args.fj).is_absolute() else ROOT / args.fj) - - # data_feed의 OCF_B/FCF_B를 보완 소스로 활용 - gtd = _load(ROOT / "GatherTradingData.json") - df_list = (gtd.get("data") or {}).get("data_feed") or [] - if not isinstance(df_list, list): - df_list = [] - df_by_ticker: dict[str, dict[str, Any]] = {str(r.get("Ticker") or ""): r for r in df_list} - - raw_rows = raw.get("rows", []) - non_etf = [r for r in raw_rows if not r.get("is_etf")] - - # verdict/horizon lookup from final judgment - horizon_by_ticker: dict[str, str] = {} - for row in fj.get("rows", []) if isinstance(fj.get("rows"), list) else []: - t = str(row.get("ticker") or "") - h = str(row.get("best_horizon") or row.get("horizon") or "") - if t: - horizon_by_ticker[t] = h - - evidence_rows = [] - total_field_slots = 0 - filled_field_slots = 0 - - for row in non_etf: - ticker = str(row.get("ticker") or "") - df_row = df_by_ticker.get(ticker, {}) - field_status: dict[str, str] = {} - - # OCF/FCF는 raw_v2의 ocf_krw/fcf_krw 우선, 없으면 data_feed의 OCF_B/FCF_B 사용 - if not _field_presence(row, "ocf_krw") and _field_presence(df_row, "OCF_B"): - row = dict(row); row["ocf_krw"] = df_row["OCF_B"] - if not _field_presence(row, "fcf_krw") and _field_presence(df_row, "FCF_B"): - row = dict(row); row["fcf_krw"] = df_row["FCF_B"] - - for field in REQUIRED_FIELDS: - if _field_presence(row, field): - field_status[field] = str(row[field]) - filled_field_slots += 1 - else: - field_status[field] = "DATA_MISSING" - total_field_slots += 1 - - field_coverage = _coverage(row, REQUIRED_FIELDS) - horizon = horizon_by_ticker.get(ticker, "UNKNOWN") - is_long_horizon = any(lh in horizon.upper() for lh in LONG_HORIZONS) - long_buy_downgraded = is_long_horizon and field_coverage < COVERAGE_THRESHOLD - - evidence_rows.append({ - "ticker": ticker, - "name": row.get("name", ""), - "source": row.get("source", ""), - "as_of_date": row.get("as_of_date", ""), - "field_coverage_pct": round(field_coverage * 100, 2), - "horizon": horizon, - "is_long_horizon": is_long_horizon, - "long_buy_downgraded_to_candidate_only": long_buy_downgraded, - "downgrade_reason": f"fundamental_coverage={field_coverage*100:.0f}% < {COVERAGE_THRESHOLD*100:.0f}%" if long_buy_downgraded else None, - "fields": field_status, - "source_path": str(raw_path.relative_to(ROOT)), - "formula_id": "FUNDAMENTAL_RAW_EVIDENCE_V3", - }) - - overall_coverage = (filled_field_slots / total_field_slots * 100.0) if total_field_slots > 0 else 0.0 - roe_opm_ocf_fcf_missing_count = sum( - 1 for r in evidence_rows - for field in REQUIRED_FIELDS - if r["fields"].get(field) == "DATA_MISSING" - ) - long_buy_with_missing = [r for r in evidence_rows if r["long_buy_downgraded_to_candidate_only"]] - - # gate 판정 - if overall_coverage >= 95.0 and len(long_buy_with_missing) == 0: - gate = "PASS" - elif overall_coverage >= 50.0: - gate = "CAUTION" - else: - gate = "FAIL" - - result = { - "formula_id": "FUNDAMENTAL_RAW_EVIDENCE_V3", - "gate": gate, - "fundamental_source_field_coverage_pct": round(overall_coverage, 2), - "roe_opm_ocf_fcf_missing_count": roe_opm_ocf_fcf_missing_count, - "long_horizon_buy_with_missing_fundamental_count": len(long_buy_with_missing), - "long_buy_downgraded_tickers": [r["ticker"] for r in long_buy_with_missing], - "coverage_threshold_pct": COVERAGE_THRESHOLD * 100, - "non_etf_ticker_count": len(non_etf), - "rows": evidence_rows, - "generated_at": datetime.now(timezone.utc).isoformat(), - "source_path": "Temp/fundamental_raw_evidence_v3.json", - } - - out_path = Path(args.out) if Path(args.out).is_absolute() else ROOT / args.out - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - summary = {k: v for k, v in result.items() if k != "rows"} - print(json.dumps(summary, indent=2, ensure_ascii=False)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/build_honest_performance_guard_v2.py b/tools/build_honest_performance_guard_v2.py deleted file mode 100644 index 9c359e34..00000000 --- a/tools/build_honest_performance_guard_v2.py +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env python3 -""" -build_honest_performance_guard_v2.py -──────────────────────────────────────────────────────────────────────── -정직 성과증빙 하네스 V2 (P0_01 단계) - -P0_01: design vs validated 분리를 엄격하게 - -모든 *_score 필드에 score_kind ∈ {DESIGN, VALIDATED} 라벨을 강제하고, -VALIDATED는 live_sample_n >= 30일 때만 허용한다. -보고서에 노출되는 점수는 VALIDATED만 허용. - -출력: - - Temp/honest_performance_guard_v2.json - - Temp/p0_01_strictness_report.json -""" - -from __future__ import annotations - -import json -import sys -from pathlib import Path -from datetime import datetime -from typing import Any - -ROOT = Path(__file__).resolve().parent.parent - -# 입력 파일 -OP_REPORT = ROOT / "Temp" / "operational_report.json" -REBOUND_EFF = ROOT / "Temp" / "rebound_sell_efficiency_v1.json" -LATE_CHASE = ROOT / "Temp" / "late_chase_attribution_v1.json" -PREDICTION_ACC = ROOT / "Temp" / "prediction_accuracy_harness_v2.json" - -# 출력 파일 -OUTPUT_V2 = ROOT / "Temp" / "honest_performance_guard_v2.json" -REPORT_P001 = ROOT / "Temp" / "p0_01_strictness_report.json" - -SAMPLE_THRESHOLD = 30 -ACCEPTED_SCORE_KINDS = {"DESIGN", "VALIDATED"} - -if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): - sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1) - - -def load_json(p: Path) -> dict | list: - if not p.exists(): - return {} - try: - return json.loads(p.read_text(encoding="utf-8")) - except Exception as e: - print(f"[WARN] Failed to load {p.name}: {e}") - return {} - - -def check_all_scores_have_kind_and_sample_n(obj: Any, path: str = "") -> list[dict]: - """모든 *_score 필드가 score_kind와 sample_n을 가지는지 검사.""" - violations = [] - - if isinstance(obj, dict): - for key, value in obj.items(): - current_path = f"{path}.{key}" if path else key - - # *_score 필드 검사 - if key.endswith("_score"): - if not isinstance(value, dict): - violations.append({ - "path": current_path, - "issue": "SCORE_NOT_DICT", - "value": value, - "detail": f"점수가 dict가 아님. 값={value}" - }) - else: - # score_kind 검사 - score_kind = value.get("score_kind") - sample_n = value.get("sample_n") - score_value = value.get("value") - - if score_kind is None: - violations.append({ - "path": current_path, - "issue": "MISSING_SCORE_KIND", - "detail": "score_kind 필드 누락" - }) - elif score_kind not in ACCEPTED_SCORE_KINDS: - violations.append({ - "path": current_path, - "issue": "INVALID_SCORE_KIND", - "value": score_kind, - "detail": f"허용되지 않는 값: {score_kind}" - }) - - if sample_n is None: - violations.append({ - "path": current_path, - "issue": "MISSING_SAMPLE_N", - "detail": "sample_n 필드 누락" - }) - - # VALIDATED인데 sample_n < 30 검사 - if score_kind == "VALIDATED" and isinstance(sample_n, int): - if sample_n < SAMPLE_THRESHOLD: - violations.append({ - "path": current_path, - "issue": "INVALID_VALIDATED_LABEL", - "sample_n": sample_n, - "detail": f"VALIDATED 라벨인데 sample_n={sample_n} < {SAMPLE_THRESHOLD}" - }) - - # 재귀 검사 - elif isinstance(value, (dict, list)): - violations.extend(check_all_scores_have_kind_and_sample_n(value, current_path)) - - elif isinstance(obj, list): - for i, item in enumerate(obj): - current_path = f"{path}[{i}]" - violations.extend(check_all_scores_have_kind_and_sample_n(item, current_path)) - - return violations - - -def build_strictness_report(rebound: dict, chase: dict, pred_acc: dict) -> dict: - """P0_01 엄격성 검사 보고서 작성.""" - report = { - "phase": "P0_01_DESIGN_VS_VALIDATED_SEPARATION", - "generated_at": datetime.now().isoformat(), - "threshold_sample_min": SAMPLE_THRESHOLD, - "findings": { - "rebound_efficiency": {}, - "late_chase_attribution": {}, - "prediction_accuracy": {} - }, - "violations": [], - "corrections_required": [] - } - - # 1. rebound_efficiency 검사 - rb_metrics = rebound.get("metrics", {}) - rb_combo = rb_metrics.get("combo_count", 0) - rb_score = rb_metrics.get("rebound_efficiency_score", 0) - - report["findings"]["rebound_efficiency"] = { - "metric_name": "rebound_efficiency_score", - "current_value": rb_score, - "sample_n": rb_combo, - "meets_validated_threshold": rb_combo >= SAMPLE_THRESHOLD, - "required_score_kind": "VALIDATED" if rb_combo >= SAMPLE_THRESHOLD else "DESIGN", - "annotation_suffix": f" [설계점수, n={rb_combo}]" if rb_combo < SAMPLE_THRESHOLD else "" - } - - if rb_combo < SAMPLE_THRESHOLD: - report["corrections_required"].append({ - "metric": "rebound_efficiency_score", - "action": "ANNOTATE_DESIGN", - "new_structure": { - "score_kind": "DESIGN", - "value": rb_score, - "sample_n": rb_combo, - "annotation": f"n={rb_combo} < {SAMPLE_THRESHOLD}. 실측 미검증." - } - }) - - # 2. late_chase_attribution 검사 - chase_metrics = chase.get("metrics", {}) - chase_sample = chase_metrics.get("sample_n", 0) - chase_rate = chase_metrics.get("chase_entry_rate_pct", 0) - - report["findings"]["late_chase_attribution"] = { - "metric_name": "late_chase_attribution", - "current_value": chase_rate, - "sample_n": chase_sample, - "meets_validated_threshold": chase_sample >= SAMPLE_THRESHOLD, - "required_score_kind": "VALIDATED" if chase_sample >= SAMPLE_THRESHOLD else "DESIGN" - } - - if chase_sample < SAMPLE_THRESHOLD: - report["corrections_required"].append({ - "metric": "late_chase_attribution", - "action": "ANNOTATE_DESIGN", - "new_structure": { - "score_kind": "DESIGN", - "value": chase_rate, - "sample_n": chase_sample, - "annotation": f"뒷박 차단 효과 미검증 (n={chase_sample})" - } - }) - - # 3. prediction_accuracy 검사 - t5_sample = pred_acc.get("t5_sample", 0) - t5_rate = pred_acc.get("t5_op_rate", 0) - - report["findings"]["prediction_accuracy"] = { - "metric_name": "t5_match_rate_pct", - "current_value": t5_rate, - "sample_n": t5_sample, - "meets_validated_threshold": t5_sample >= SAMPLE_THRESHOLD, - "required_score_kind": "VALIDATED" if t5_sample >= SAMPLE_THRESHOLD else "DESIGN" - } - - if t5_sample < SAMPLE_THRESHOLD: - report["corrections_required"].append({ - "metric": "t5_match_rate_pct", - "action": "ANNOTATE_DESIGN", - "new_structure": { - "score_kind": "DESIGN", - "value": t5_rate, - "sample_n": t5_sample, - "annotation": f"실측 미검증 (n={t5_sample})" - } - }) - - # 최종 verdict - report["verdict"] = { - "all_scores_properly_labeled": len(report["corrections_required"]) == 0, - "required_corrections_count": len(report["corrections_required"]), - "status": "PASS" if len(report["corrections_required"]) == 0 else "FAIL_CORRECTION_REQUIRED" - } - - return report - - -def main() -> int: - print("=" * 80) - print(" P0_01: Design vs Validated 엄격한 분리") - print("=" * 80) - - # 입력 로드 - rebound = load_json(REBOUND_EFF) - chase = load_json(LATE_CHASE) - pred_acc = load_json(PREDICTION_ACC) - - # P0_01 보고서 생성 - p001_report = build_strictness_report(rebound, chase, pred_acc) - - print(f"\n[1] 재정렬 효율 (rebound_efficiency_score)") - rb_find = p001_report["findings"]["rebound_efficiency"] - print(f" 현재값: {rb_find['current_value']}") - print(f" 표본 수: {rb_find['sample_n']} / {SAMPLE_THRESHOLD}") - print(f" 필수 라벨: {rb_find['required_score_kind']}") - - print(f"\n[2] 뒷박 매수 (late_chase_attribution)") - chase_find = p001_report["findings"]["late_chase_attribution"] - print(f" 현재값: {chase_find['current_value']}") - print(f" 표본 수: {chase_find['sample_n']} / {SAMPLE_THRESHOLD}") - print(f" 필수 라벨: {chase_find['required_score_kind']}") - - print(f"\n[3] 예측 정확도 (T+5 일치율)") - pred_find = p001_report["findings"]["prediction_accuracy"] - print(f" 현재값: {pred_find['current_value']}%") - print(f" 표본 수: {pred_find['sample_n']} / {SAMPLE_THRESHOLD}") - print(f" 필수 라벨: {pred_find['required_score_kind']}") - - print(f"\n[결과]") - print(f" 필요한 수정: {p001_report['verdict']['required_corrections_count']}") - print(f" 상태: {p001_report['verdict']['status']}") - - # 보고서 저장 - REPORT_P001.write_text( - json.dumps(p001_report, ensure_ascii=False, indent=2), - encoding="utf-8" - ) - print(f"\n✓ P0_01 보고서 저장: {REPORT_P001.name}") - - # V2 가드 생성 - guard_v2 = { - "schema_version": "honest_performance_guard_v2", - "generated_at": datetime.now().isoformat(), - "p0_01_strictness": p001_report["verdict"], - "required_corrections": p001_report["corrections_required"], - "action_plan": [ - { - "step": 1, - "title": "모든 *_score 필드를 dict 구조로 변환", - "fields": ["score_kind", "value", "sample_n", "annotation"] - }, - { - "step": 2, - "title": "각 필드에 score_kind ∈ {DESIGN, VALIDATED} 할당", - "rule": "sample_n >= 30 → VALIDATED, else → DESIGN" - }, - { - "step": 3, - "title": "보고서 노출 규칙 적용", - "rule": "DESIGN 점수는 보고서 요약에 단독 노출 금지. (설계, n=N) 접미사 필수" - } - ] - } - - OUTPUT_V2.write_text( - json.dumps(guard_v2, ensure_ascii=False, indent=2), - encoding="utf-8" - ) - print(f"✓ P0_01 가드 저장: {OUTPUT_V2.name}") - - return 0 if p001_report['verdict']['status'] == "PASS" else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/build_operational_truth_score_v1.py b/tools/build_operational_truth_score_v1.py deleted file mode 100644 index 5c6dc33f..00000000 --- a/tools/build_operational_truth_score_v1.py +++ /dev/null @@ -1,374 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_JSON = ROOT / "GatherTradingData.json" -DEFAULT_REPORT = ROOT / "Temp" / "operational_report.json" -DEFAULT_DQR = ROOT / "Temp" / "data_quality_reconciliation_v1.json" -DEFAULT_FJ = ROOT / "Temp" / "final_judgment_gate_v1.json" -DEFAULT_SCR = ROOT / "Temp" / "smart_cash_recovery_v5.json" -DEFAULT_HARDENING = ROOT / "Temp" / "strategy_hardening_harness_v2.json" -DEFAULT_OUTCOME = ROOT / "Temp" / "operational_outcome_lock_v1.json" -DEFAULT_ALPHA = ROOT / "Temp" / "operational_alpha_calibration_v2.json" -DEFAULT_OUT = ROOT / "Temp" / "operational_truth_score_v1.json" - - -def _load(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - obj = json.loads(path.read_text(encoding="utf-8")) - except Exception: - return {} - return obj if isinstance(obj, dict) else {} - - -def _as_float(value: Any, default: float = 0.0) -> float: - try: - return float(value) - except Exception: - return default - - -def _as_int(value: Any, default: int = 0) -> int: - try: - return int(float(value)) - except Exception: - return default - - -def _as_dict(value: Any) -> dict[str, Any]: - if isinstance(value, dict): - return value - if isinstance(value, str) and value.strip(): - try: - parsed = json.loads(value) - return parsed if isinstance(parsed, dict) else {} - except Exception: - return {} - return {} - - -def _extract_harness_root(payload: dict[str, Any]) -> dict[str, Any]: - h_apex = payload.get("hApex") - data_apex = ((payload.get("data") or {}).get("_harness_context")) if isinstance(payload.get("data"), dict) else None - if isinstance(h_apex, dict) and isinstance(data_apex, dict): - merged = dict(data_apex) - merged.update(h_apex) - return merged - if isinstance(h_apex, dict): - return h_apex - if isinstance(data_apex, dict): - return data_apex - return payload - - -def _score_from_span(primary: float, secondary: float) -> float: - return round(max(0.0, 100.0 - abs(primary - secondary)), 2) - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--json", default=str(DEFAULT_JSON)) - ap.add_argument("--report", default=str(DEFAULT_REPORT)) - ap.add_argument("--dq", default=str(DEFAULT_DQR)) - ap.add_argument("--fj", default=str(DEFAULT_FJ)) - ap.add_argument("--scr", default=str(DEFAULT_SCR)) - ap.add_argument("--hardening", default=str(DEFAULT_HARDENING)) - ap.add_argument("--outcome", default=str(DEFAULT_OUTCOME)) - ap.add_argument("--alpha", default=str(DEFAULT_ALPHA)) - ap.add_argument("--out", default=str(DEFAULT_OUT)) - args = ap.parse_args() - - def _rp(path_str: str) -> Path: - path = Path(path_str) - return path if path.is_absolute() else ROOT / path - - payload = _load(_rp(args.json)) - report = _load(_rp(args.report)) - hctx = _extract_harness_root(payload) - dqr = _load(_rp(args.dq)) - fj = _load(_rp(args.fj)) - scr = _load(_rp(args.scr)) - hardening = _load(_rp(args.hardening)) - outcome = _load(_rp(args.outcome)) - alpha = _load(_rp(args.alpha)) - summary = report.get("summary") if isinstance(report.get("summary"), dict) else {} - sections = report.get("sections") if isinstance(report.get("sections"), list) else [] - section_names = {str(s.get("name") or "") for s in sections if isinstance(s, dict)} - - schema = _as_float(dqr.get("schema_presence_score")) - modern = _as_float(dqr.get("modern_investment_quality_score")) - legacy = _as_float(dqr.get("legacy_investment_quality_score")) - invest_score = _as_float(dqr.get("investment_quality_score")) - cap_basis = _as_float(dqr.get("confidence_cap_basis_score"), min(modern or invest_score, legacy or invest_score)) - quality_gap = max(0.0, modern - cap_basis) - quality_conflict = bool(dqr.get("quality_conflict_flag")) - - fj_gate = str(fj.get("gate") or "MISSING") - fj_coverage = _as_float(fj.get("coverage_pct")) - fj_silent = _as_int(fj.get("silent_pass_violations")) - fj_late = len(fj.get("late_chase_buy_violations") or []) - - export_gate = _as_dict(hctx.get("export_gate_json")) - export_status = str(_first_non_null(export_gate.get("json_validation_status"), hctx.get("json_validation_status"), summary.get("json_validation_status")) or "UNKNOWN") - export_allowed = export_gate.get("hts_entry_allowed") - execution_allowed = bool(scr.get("execution_allowed")) - cash_status = str(scr.get("status") or "UNKNOWN") - cash_damage = _as_float(scr.get("value_damage_pct_avg")) - - hardening_meta = hardening.get("meta_scores") if isinstance(hardening.get("meta_scores"), dict) else {} - hardening_overall = _as_float(hardening_meta.get("overall_hardening_score")) - readiness_gate = str(hardening_meta.get("readiness_gate") or "MISSING") - readiness_reasons = hardening_meta.get("readiness_reasons") if isinstance(hardening_meta.get("readiness_reasons"), list) else [] - - outcome_metrics = outcome.get("metrics") if isinstance(outcome.get("metrics"), dict) else {} - t20_count = _as_float(outcome_metrics.get("operational_t20_count")) - t20_pass = _as_float(outcome_metrics.get("operational_t20_pass_rate")) - expectancy = _as_float(outcome_metrics.get("execution_expectancy_pct")) - win_rate = _as_float(outcome_metrics.get("execution_win_rate_pct")) - - alpha_gate = str(alpha.get("gate") or "MISSING") - alpha_confidence = _as_float(alpha.get("confidence_score")) - - # 누적손익 교차 검사: executive_brief vs pnl_attribution (±10만원 허용) - import re as _re - def _extract_pnl_from_section(name: str) -> float | None: - for sec in sections: - if not isinstance(sec, dict) or sec.get("name") != name: - continue - md = str(sec.get("markdown") or "") - # "누적 평가손익" 텍스트 뒤에 오는 원화 금액만 추출 (총자산 등 오매칭 방지) - m = _re.search(r"누적\s*평가손익[^\n]*?([+\-]\s*[\d,]+)원", md) - if m: - try: - return float(m.group(1).replace(",", "").replace(" ", "")) - except Exception: - pass - return None - - _pnl_brief = _extract_pnl_from_section("executive_brief") - _pnl_attr = _extract_pnl_from_section("pnl_attribution") - _pnl_consistent = ( - _pnl_brief is None or _pnl_attr is None - or abs(_pnl_brief - _pnl_attr) <= 100_000 # 10만원 이내 = 정상 - ) - - report_consistency_checks = [ - bool(report), - "routing_serving_trace" in section_names, - "QEH_AUDIT_BLOCK" in section_names, - "concise_hts_input_sheet" in section_names, - "reference_price_ledger" in section_names, - bool(summary.get("canonical_order_ok")), - export_status in {"EXPORT_READY", "REVIEW_ONLY", "PENDING_EXPORT", "EXPORT_BLOCKED_CRITICAL"}, - _pnl_consistent, # 누적손익 섹션 간 일치 (±10만원) - ] - report_consistency_score = round(sum(1 for ok in report_consistency_checks if ok) / len(report_consistency_checks) * 100.0, 2) - - data_truth_score = _score_from_span(modern if modern else invest_score, cap_basis if cap_basis else invest_score) - if schema >= 99.0 and data_truth_score > 0: - data_truth_score = round(min(100.0, (schema + data_truth_score) / 2.0), 2) - - decision_truth_score = 100.0 - if fj_gate != "PASS": - decision_truth_score = min(decision_truth_score, 55.0) - if fj_coverage < 100.0: - decision_truth_score = min(decision_truth_score, fj_coverage) - if fj_silent > 0: - decision_truth_score = 0.0 - if fj_late > 0: - decision_truth_score = min(decision_truth_score, 40.0) - - execution_truth_score = 100.0 - if export_status == "EXPORT_BLOCKED_CRITICAL": - execution_truth_score = 0.0 - elif export_status == "EXPORT_READY" and export_allowed is True: - execution_truth_score = 100.0 - elif export_status == "REVIEW_ONLY": - # Partial credit: human review required but not hard-blocked - execution_truth_score = 40.0 - else: - execution_truth_score = 0.0 - if not execution_allowed: - execution_truth_score = min(execution_truth_score, 20.0) - if cash_status != "PASS": - execution_truth_score = min(execution_truth_score, 25.0) - if cash_damage > 10.0: - execution_truth_score = min(execution_truth_score, max(0.0, 100.0 - (cash_damage - 10.0) * 5.0)) - - # replay T+20 보정 — 운영 T+20이 없으면 replay(estimated)로 최소 상향 - _pred_path = ROOT / "Temp" / "prediction_accuracy_harness_v2.json" - _pred_data: dict = {} - try: - import json as _json - _pred_data = _json.loads(_pred_path.read_text(encoding="utf-8")) if _pred_path.exists() else {} - except Exception: - pass - _replay_t20_n = _pred_data.get("t20_replay_sample") or 0 - _replay_calibrated = str(_pred_data.get("replay_calibration_state") or "") == "REPLAY_CALIBRATED" - - performance_readiness_score = hardening_overall if hardening_overall > 0 else 0.0 - if readiness_gate != "PERFORMANCE_READY": - performance_readiness_score = min(performance_readiness_score, 60.0) - # T+20 미달 패널티 — replay 충분 시 30→50으로 완화 (estimated=true 명시) - # 순서: replay 우선 확인 → 미달 캡 결정 - _t20_cap = 30.0 - if _replay_calibrated and _replay_t20_n >= 30: - _t20_cap = 50.0 # replay 510건 확보 → 운영 미달 패널티 완화 - if "OPERATIONAL_T20_SAMPLE_LT_30" in readiness_reasons or t20_count < 30: - performance_readiness_score = min(performance_readiness_score, _t20_cap) - # Guard: only penalise T+20 pass-rate when there is actual T+20 data. - # t20_pass=0 when t20_count=0 is vacuously zero, not a failure signal. - if t20_count >= 10 and t20_pass < 60.0: - performance_readiness_score = min(performance_readiness_score, t20_pass) - # Guard: expectancy/win_rate derived from T+20 evaluations — vacuous when count=0. - if t20_count >= 10 and expectancy <= 0.1: - performance_readiness_score = min(performance_readiness_score, 20.0) - if t20_count >= 10 and win_rate < 45.0: - performance_readiness_score = min(performance_readiness_score, win_rate) - if cash_damage > 10.0: - performance_readiness_score = min(performance_readiness_score, max(0.0, 100.0 - cash_damage * 4.0)) - if alpha_gate != "PERFORMANCE_READY": - performance_readiness_score = min(performance_readiness_score, alpha_confidence) - - weighted_score = round( - (data_truth_score * 0.25) - + (decision_truth_score * 0.20) - + (execution_truth_score * 0.20) - + (performance_readiness_score * 0.20) - + (report_consistency_score * 0.15), - 2, - ) - - blocking_reasons: list[str] = [] - if cap_basis < 50.0: - blocking_reasons.append("DATA_QUALITY_CAP_BASIS_LT_50") - # Gap threshold raised from 20→40 after blended cap_basis fix (V2). - # Gap of 20-40% is expected: modern harness elevates quality from sparse raw fields. - # Gap >40% still indicates genuine data-vs-processing conflict. - if quality_gap >= 40.0: - blocking_reasons.append("LEGACY_MODERN_QUALITY_GAP_WIDE") - if fj_gate != "PASS" or fj_silent > 0: - blocking_reasons.append("DECISION_GATE_NOT_STABLE") - if export_status == "EXPORT_BLOCKED_CRITICAL": - blocking_reasons.append("EXPORT_GATE_NOT_READY") - elif export_status != "EXPORT_READY" and export_status != "REVIEW_ONLY": - blocking_reasons.append("EXPORT_GATE_NOT_READY") - elif export_status == "REVIEW_ONLY": - blocking_reasons.append("EXPORT_GATE_REVIEW_ONLY") # soft — not a hard block - if not execution_allowed or cash_status != "PASS": - blocking_reasons.append("CASH_RECOVERY_EXECUTION_BLOCKED") - if readiness_gate != "PERFORMANCE_READY" or t20_count < 30: - blocking_reasons.append("PERFORMANCE_NOT_READY") - if cash_damage > 10.0: - blocking_reasons.append("VALUE_DAMAGE_GT_10") - if not bool(summary.get("canonical_order_ok")): - blocking_reasons.append("REPORT_CANONICAL_ORDER_INVALID") - - hard_blocking = [r for r in blocking_reasons if r != "EXPORT_GATE_REVIEW_ONLY"] - if not hard_blocking and weighted_score >= 100.0: - gate = "PASS_100" - llm_allowed_actions = ["HTS_READY"] - elif "EXPORT_GATE_NOT_READY" in blocking_reasons or "CASH_RECOVERY_EXECUTION_BLOCKED" in blocking_reasons: - gate = "BLOCK_EXECUTION" - llm_allowed_actions = ["EXPLAIN_ONLY", "RENDER_LEDGER_ONLY"] - elif "DATA_QUALITY_CAP_BASIS_LT_50" in blocking_reasons or "LEGACY_MODERN_QUALITY_GAP_WIDE" in blocking_reasons: - gate = "DATA_CONFLICT" - llm_allowed_actions = ["EXPLAIN_ONLY", "RENDER_LEDGER_ONLY"] - elif "PERFORMANCE_NOT_READY" in blocking_reasons: - gate = "WATCH_PENDING_SAMPLE" - llm_allowed_actions = ["EXPLAIN_ONLY", "RENDER_LEDGER_ONLY"] - elif "EXPORT_GATE_REVIEW_ONLY" in blocking_reasons: - gate = "REVIEW_ONLY_PENDING" - llm_allowed_actions = ["EXPLAIN_ONLY", "RENDER_LEDGER_ONLY"] - else: - gate = "WATCH_PENDING_SAMPLE" - llm_allowed_actions = ["EXPLAIN_ONLY", "RENDER_LEDGER_ONLY"] - - # [R2-2] 히스테리시스: score 변동 ±3 이내면 직전 gate 유지 (경계 밴딩). - # 동일 xlsx 미세 입력변동이 gate를 점프시키는 비결정론을 방지. - _HYSTERESIS_BAND = 3.0 - try: - _prev_path = _rp(args.out) - if _prev_path.exists(): - _prev = json.loads(_prev_path.read_text(encoding="utf-8")) - _prev_score = float(_prev.get("score_0_100") or 0.0) - _prev_gate = str(_prev.get("gate") or "") - _gate_rank = {"PASS_100": 4, "WATCH_PENDING_SAMPLE": 3, "REVIEW_ONLY_PENDING": 3, - "DATA_CONFLICT": 2, "BLOCK_EXECUTION": 1} - _cur_rank = _gate_rank.get(gate, 2) - _prev_rank = _gate_rank.get(_prev_gate, 2) - # 점수 차이가 밴드 이내이고 hard_blocking 상태가 바뀌지 않았으면 이전 gate 유지 - if (abs(weighted_score - _prev_score) <= _HYSTERESIS_BAND - and _prev_gate in _gate_rank - and abs(_cur_rank - _prev_rank) <= 1): - gate = _prev_gate - llm_allowed_actions = _prev.get("llm_allowed_actions") or llm_allowed_actions - except Exception: - pass # 히스테리시스 실패 시 계산된 gate 그대로 사용 - - hard_block_count = len([reason for reason in blocking_reasons if reason in { - "DATA_QUALITY_CAP_BASIS_LT_50", - "EXPORT_GATE_NOT_READY", - "CASH_RECOVERY_EXECUTION_BLOCKED", - "REPORT_CANONICAL_ORDER_INVALID", - # EXPORT_GATE_REVIEW_ONLY is soft — excluded from hard block count - }]) - - result = { - "formula_id": "OPERATIONAL_TRUTH_SCORE_V1", - "score_0_100": weighted_score, - "gate": gate, - "hard_block_count": hard_block_count, - "blocking_reasons": blocking_reasons, - "llm_allowed_actions": llm_allowed_actions, - "data_truth_score": round(data_truth_score, 2), - "decision_truth_score": round(decision_truth_score, 2), - "execution_truth_score": round(execution_truth_score, 2), - "performance_readiness_score": round(performance_readiness_score, 2), - "report_consistency_score": round(report_consistency_score, 2), - "metric_basis": { - "schema_presence_score": schema, - "legacy_investment_quality_score": legacy, - "modern_investment_quality_score": modern, - "investment_quality_score": invest_score, - "confidence_cap_basis_score": cap_basis, - "quality_gap_pct": round(quality_gap, 2), - "quality_conflict_flag": quality_conflict, - "final_judgment_gate": fj_gate, - "final_judgment_coverage_pct": fj_coverage, - "smart_cash_recovery_status": cash_status, - "smart_cash_recovery_execution_allowed": execution_allowed, - "export_status": export_status, - "export_allowed": export_allowed, - "operational_t20_count": t20_count, - "operational_t20_pass_rate": t20_pass, - "execution_expectancy_pct": expectancy, - "execution_win_rate_pct": win_rate, - "alpha_calibration_gate": alpha_gate, - "alpha_calibration_confidence_score": alpha_confidence, - }, - } - - out_path = _rp(args.out) - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - print(json.dumps(result, ensure_ascii=False, indent=2)) - return 0 - - -def _first_non_null(*values: Any) -> Any: - for value in values: - if value is not None: - return value - return None - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/build_p0_02_masking_removal.py b/tools/build_p0_02_masking_removal.py deleted file mode 100644 index 88e35a65..00000000 --- a/tools/build_p0_02_masking_removal.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env python3 -""" -build_p0_02_masking_removal.py -──────────────────────────────────────────────────────────────────────── -P0_02: 값 손상 지표에서 adjusted 마스킹 제거 - -핵심 변경: -1. value_damage_raw_pct는 게이트 입력으로 사용 (항상 raw 값) -2. value_damage_adjusted_pct는 annotation only (참고용) -3. cap_pass=false를 summary에 그대로 전파 -4. 같은 지표가 3개의 다른 값을 가지는 문제 제거 - -출력: - - Temp/p0_02_masking_removal_report.json -""" - -from __future__ import annotations - -import json -import sys -from pathlib import Path -from datetime import datetime - -ROOT = Path(__file__).resolve().parent.parent - -# 입력 파일 -CASH_RECOVERY = ROOT / "Temp" / "cash_recovery_optimizer_v4.json" -SMART_CASH_V7 = ROOT / "Temp" / "smart_cash_recovery_v7_authoritative.json" - -# 출력 파일 -REPORT_P002 = ROOT / "Temp" / "p0_02_masking_removal_report.json" - -if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): - sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1) - - -def load_json(p: Path) -> dict: - if not p.exists(): - return {} - try: - return json.loads(p.read_text(encoding="utf-8")) - except Exception as e: - print(f"[WARN] Failed to load {p.name}: {e}") - return {} - - -def find_masking_violations(cash_rec: dict, smart_v7: dict) -> list[dict]: - """adjusted가 0.0으로 강제되는 부분 찾기.""" - violations = [] - - # 1. cash_recovery_optimizer_v4에서 raw vs adjusted 충돌 검사 - raw_dmg = cash_rec.get("value_damage_raw_pct", 0) - adj_dmg = cash_rec.get("value_damage_adjusted_pct", 0) - - if raw_dmg > 0 and adj_dmg == 0.0: - violations.append({ - "location": "cash_recovery_optimizer_v4.json", - "issue": "ADJUSTED_MASKED_TO_ZERO", - "raw_value": raw_dmg, - "adjusted_value": adj_dmg, - "detail": f"raw={raw_dmg} > adjusted={adj_dmg}. adjusted가 마스킹되었을 가능성.", - "severity": "CRITICAL" - }) - - # 2. smart_cash_recovery_v7에서 마스킹 검사 - raw_v7 = smart_v7.get("raw_value_damage_pct_avg") - opt_v7 = smart_v7.get("optimized_value_damage_pct_avg") - cap_pass = smart_v7.get("cap_pass") - - if raw_v7 is not None and opt_v7 is not None: - if raw_v7 > opt_v7 and cap_pass == False: - violations.append({ - "location": "smart_cash_recovery_v7_authoritative.json", - "issue": "CAP_FAIL_NOT_PROPAGATED", - "raw_value": raw_v7, - "optimized_value": opt_v7, - "cap_pass": cap_pass, - "detail": f"raw={raw_v7} > optimized={opt_v7} AND cap_pass=false. 하지만 summary에 cap_pass 정보가 전파되지 않을 가능성.", - "severity": "HIGH" - }) - - return violations - - -def build_p002_report(cash_rec: dict, smart_v7: dict) -> dict: - """P0_02 마스킹 제거 보고서.""" - violations = find_masking_violations(cash_rec, smart_v7) - - report = { - "phase": "P0_02_NO_ADJUSTED_MASKING", - "generated_at": datetime.now().isoformat(), - "violations_found": len(violations), - "violations": violations, - "required_actions": [ - { - "action": "USE_RAW_AS_GATE_INPUT", - "description": "value_damage_raw_pct를 게이트 입력으로 항상 사용", - "fields": ["value_damage_raw_pct"], - "rule": "gate_input = raw, adjusted는 annotation only" - }, - { - "action": "REMOVE_MASKING_LOGIC", - "description": "adjusted=0.0 강제 로직 제거", - "files": [ - "tools/build_cash_recovery_optimizer_v4.py (line 109-113)", - "tools/build_value_preservation_scorer_v1.py" - ] - }, - { - "action": "PROPAGATE_CAP_PASS", - "description": "cap_pass=false를 summary에 명시적으로 표시", - "example": "raw=15.7 > cap=10.0 → cap_pass=false (summary에 명시)" - } - ], - "metric_structure": { - "before_p002": { - "value_damage_raw_pct": 15.7, - "value_damage_adjusted_pct": 0.0, - "cap_pass": "MISSING_FROM_SUMMARY" - }, - "after_p002": { - "value_damage_raw_pct": 15.7, - "value_damage_adjusted_pct": {"value": 15.7, "annotation": "raw 값 (cap=10.0)"}, - "cap_pass": True, - "gate_input": "value_damage_raw_pct (항상 raw)" - } - } - } - - return report - - -def main() -> int: - print("=" * 80) - print(" P0_02: Adjusted 마스킹 제거 및 Raw 값 복원") - print("=" * 80) - - # 입력 로드 - cash_rec = load_json(CASH_RECOVERY) - smart_v7 = load_json(SMART_CASH_V7) - - # P0_02 보고서 생성 - p002_report = build_p002_report(cash_rec, smart_v7) - - print(f"\n[검사 결과]") - print(f" 마스킹 위반 발견: {p002_report['violations_found']}") - - for i, v in enumerate(p002_report["violations"], 1): - print(f"\n [{i}] {v['issue']}") - print(f" 위치: {v['location']}") - print(f" 심각도: {v['severity']}") - if "raw_value" in v: - print(f" raw: {v['raw_value']}") - if "adjusted_value" in v: - print(f" adjusted: {v['adjusted_value']}") - - print(f"\n[필수 조치]") - for i, action in enumerate(p002_report["required_actions"], 1): - print(f" {i}. {action['action']}") - print(f" → {action['description']}") - - # 보고서 저장 - REPORT_P002.write_text( - json.dumps(p002_report, ensure_ascii=False, indent=2), - encoding="utf-8" - ) - print(f"\n✓ P0_02 보고서 저장: {REPORT_P002.name}") - - return 0 if p002_report['violations_found'] == 0 else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/build_p0_03_unified_coverage.py b/tools/build_p0_03_unified_coverage.py deleted file mode 100644 index 35338112..00000000 --- a/tools/build_p0_03_unified_coverage.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env python3 -""" -build_p0_03_unified_coverage.py -──────────────────────────────────────────────────────────────────────── -P0_03: 커버리지 분모 통일 — 288 vs 204 분모 불일치 해소 - -핵심 변경: -1. spec/13_formula_registry.yaml에서 active=true 공식만 수집 (단일 분모) -2. deprecated/orphan을 active=false로 명시 -3. 골든 커버리지를 단일 분모로만 계산 -4. 장식용 100% 필드 전면 삭제 - -출력: - - Temp/p0_03_unified_coverage.json - - Temp/p0_03_denominator_audit.json -""" - -from __future__ import annotations - -import json -import sys -import re -from pathlib import Path -from datetime import datetime -from typing import Any - -ROOT = Path(__file__).resolve().parent.parent - -# 입력 파일 -FORMULA_REGISTRY = ROOT / "spec" / "13_formula_registry.yaml" -YAML_CODE_COVERAGE = ROOT / "Temp" / "yaml_code_coverage_v1.json" - -# 출력 파일 -OUTPUT_UNIFIED = ROOT / "Temp" / "p0_03_unified_coverage.json" -AUDIT_REPORT = ROOT / "Temp" / "p0_03_denominator_audit.json" - -if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): - sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1) - - -def load_yaml_simple(p: Path) -> dict: - """간단한 YAML 파싱 (설치된 라이브러리 없이).""" - if not p.exists(): - return {} - text = p.read_text(encoding="utf-8") - result = {} - - # execution_order 섹션 찾기 - in_exec_order = False - current_list = [] - - for line in text.split("\n"): - stripped = line.strip() - - if stripped.startswith("execution_order:"): - in_exec_order = True - continue - - if in_exec_order: - if stripped.startswith("- "): - formula_id = stripped[2:].strip() - if formula_id: - current_list.append(formula_id) - elif stripped and not stripped.startswith("-") and not stripped.startswith("#"): - # 다음 섹션 시작 - in_exec_order = False - - result["execution_order"] = current_list - return result - - -def load_json(p: Path) -> dict | list: - if not p.exists(): - return {} if p.suffix == ".json" else {} - try: - return json.loads(p.read_text(encoding="utf-8")) - except Exception as e: - print(f"[WARN] Failed to load {p.name}: {e}") - return {} - - -def build_denominator_audit(formula_registry: dict, yaml_coverage: dict) -> dict: - """분모 감사 보고서.""" - audit = { - "generated_at": datetime.now().isoformat(), - "findings": {} - } - - # 1. execution_order에서 active=true 공식 수집 - exec_order = formula_registry.get("execution_order", []) - active_count = len(exec_order) - - audit["findings"]["execution_order"] = { - "total_in_registry": active_count, - "formula_ids": exec_order[:10] + (["..."] if len(exec_order) > 10 else []) - } - - # 2. yaml_code_coverage와의 비교 - yaml_cov = yaml_coverage.get("coverage_summary", {}) - yaml_formula_count = yaml_cov.get("formula_total", 0) - orphan_count = yaml_cov.get("orphan_code_formula_count", 0) - - audit["findings"]["yaml_code_coverage"] = { - "formula_total": yaml_formula_count, - "orphan_code_formula_count": orphan_count, - "effective_denominator": yaml_formula_count - orphan_count - } - - # 3. 분모 불일치 진단 - expected_denominator = active_count - actual_288 = 288 # 기존 분모 - actual_204 = 204 # 다른 분모 - - audit["findings"]["denominator_collision"] = { - "expected_unified": expected_denominator, - "legacy_288": actual_288, - "legacy_204": actual_204, - "collision_exists": (actual_288 != actual_204), - "recommendation": f"Use execution_order count={expected_denominator} as SINGLE source of truth" - } - - return audit - - -def build_unified_coverage(formula_registry: dict) -> dict: - """통일된 커버리지 계산.""" - exec_order = formula_registry.get("execution_order", []) - active_formula_count = len(exec_order) - - # 현재는 exec_order 개수를 분모로 사용하는 것만 계산 - unified = { - "schema_version": "unified_coverage_v1", - "generated_at": datetime.now().isoformat(), - "denominator_single_source": "spec/13_formula_registry.yaml:execution_order", - "active_formula_count": active_formula_count, - "coverage_calculation_rule": { - "numerator": "GAS implementation + Python harness implementation", - "denominator": "execution_order count (deprecated/orphan excluded)", - "formula": f"coverage_pct = (gs_impl_count + py_impl_count) / {active_formula_count} * 100" - }, - "required_corrections": [ - { - "issue": "DECORATIVE_100_FIELD_REMOVAL", - "description": "'adjusted_coverage_pct (참고용, PASS 미사용)' 같은 필드 제거", - "action": "Delete all '**_adjusted', '**_参考용' fields" - }, - { - "issue": "SINGLE_DENOMINATOR_LOCK", - "description": "모든 커버리지 계산을 execution_order 분모로 통일", - "action": f"Use denominator={active_formula_count} for all coverage metrics" - }, - { - "issue": "GOLDEN_COVERAGE_UNIFICATION", - "description": "골든 커버리지를 64.93 / 90.2 / 67.93 / 100 중 1개로 선택 불가", - "action": "Calculate single golden_coverage_ratio with execution_order denominator" - } - ], - "implementation_checklist": [ - {"step": 1, "task": "measure_yaml_gs_ps_coverage.py 업데이트 (active=true만 수집)"}, - {"step": 2, "task": "deprecated/orphan formula를 spec/13_formula_registry.yaml에서 active=false로 명시"}, - {"step": 3, "task": "모든 *_adjusted, *_참고용 필드 제거"}, - {"step": 4, "task": "validate_golden_coverage_100.py 업데이트 (단일 분모 검증)"} - ] - } - - return unified - - -def main() -> int: - print("=" * 80) - print(" P0_03: 커버리지 분모 통일 (288 vs 204 불일치 해소)") - print("=" * 80) - - # 입력 로드 - formula_reg = load_yaml_simple(FORMULA_REGISTRY) - yaml_cov = load_json(YAML_CODE_COVERAGE) - - # 분모 감사 - denominator_audit = build_denominator_audit(formula_reg, yaml_cov) - - print(f"\n[1] Execution Order 공식 수") - print(f" 총 개수: {denominator_audit['findings']['execution_order']['total_in_registry']}") - - print(f"\n[2] YAML 코드 커버리지") - yaml_find = denominator_audit["findings"]["yaml_code_coverage"] - print(f" 공식 총 수: {yaml_find['formula_total']}") - print(f" 고아 공식: {yaml_find['orphan_code_formula_count']}") - print(f" 유효 분모: {yaml_find['effective_denominator']}") - - print(f"\n[3] 분모 불일치 진단") - denom_find = denominator_audit["findings"]["denominator_collision"] - print(f" 기대값(execution_order): {denom_find['expected_unified']}") - print(f" 기존 분모 1: {denom_find['legacy_288']}") - print(f" 기존 분모 2: {denom_find['legacy_204']}") - print(f" 충돌: {'YES' if denom_find['collision_exists'] else 'NO'}") - - # 통일된 커버리지 계산 - unified_cov = build_unified_coverage(formula_reg) - - print(f"\n[4] 필수 수정사항") - for i, corr in enumerate(unified_cov['required_corrections'], 1): - print(f" {i}. {corr['issue']}") - print(f" → {corr['description']}") - print(f" → {corr['action']}") - - # 보고서 저장 - AUDIT_REPORT.write_text( - json.dumps(denominator_audit, ensure_ascii=False, indent=2), - encoding="utf-8" - ) - print(f"\n✓ P0_03 분모 감사 저장: {AUDIT_REPORT.name}") - - OUTPUT_UNIFIED.write_text( - json.dumps(unified_cov, ensure_ascii=False, indent=2), - encoding="utf-8" - ) - print(f"✓ P0_03 통일 커버리지 저장: {OUTPUT_UNIFIED.name}") - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/build_p3_01_stop_loss_taxonomy.py b/tools/build_p3_01_stop_loss_taxonomy.py deleted file mode 100644 index e0d896b5..00000000 --- a/tools/build_p3_01_stop_loss_taxonomy.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env python3 -""" -build_p3_01_stop_loss_taxonomy.py -──────────────────────────────────────────────────────────────────────── -P3_01: 손절 체계 재정의 — ABSOLUTE_RISK_STOP vs RELATIVE_ALERT 분리 - -문제 진단: - 기존: "시장대비 10% 빠지면 매도" → 목적 불명확 - 실제: (1) 절대 리스크 스탑 + (2) 상대성과 경보 혼합 - -해결: - 1. ABSOLUTE_RISK_STOP_V1: ATR 기반 절대 하방 캡 - 2. RELATIVE_UNDERPERFORMANCE_ALERT_V1: 강도 약화 신호 (로테이션용) - -출력: - - Temp/p3_01_stop_taxonomy_spec.json - - Temp/p3_01_implementation_plan.json -""" - -from __future__ import annotations - -import json -import sys -from pathlib import Path -from datetime import datetime - -ROOT = Path(__file__).resolve().parent.parent - -OUTPUT_SPEC = ROOT / "Temp" / "p3_01_stop_taxonomy_spec.json" -OUTPUT_PLAN = ROOT / "Temp" / "p3_01_implementation_plan.json" - -if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): - sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1) - - -def build_stop_taxonomy() -> dict: - """손절 체계 정의.""" - spec = { - "schema_version": "stop_loss_taxonomy_v1", - "generated_at": datetime.now().isoformat(), - "root_cause": "절대 리스크(자본보호)와 상대강도(기회비용)를 혼합 → 손절 논리 불명확", - "diagnosis": { - "issue_1": "절대 스탑 없이 상대 로테이션만 사용 → 시장 폭락 시 -30% 출혈 가능", - "issue_2": "지정가와 수량 규칙 없음 → HTS 입력 불가능 (HS007)", - "issue_3": "비호가 가격 사용 → TICK_NORMALIZER 통과 실패 (HS008)", - "issue_4": "상대성과만으로 전량 청산 → 기회비용 최악 (회복 불가)" - }, - "taxonomy": { - "ABSOLUTE_RISK_STOP_V1": { - "purpose": "자본 하방 보호 (항상 1순위)", - "target": "손실을 ATR/퍼센트 캡으로 제한", - "formula_core": "max(entry*0.92, entry - ATR20*1.5)", - "formula_core_note": "ATR20_Pct >= 8%면 *2.0으로 확대", - "formula_satellite": "entry - ATR20*2.0", - "formula_satellite_fallback": "entry*0.88", - "order_method": "지정가 (갭하락 시 09:00~09:15 시장가 금지)", - "quantity_rule": "3단계: 50% 즉시 + 50% 나머지", - "sample_prices": { - "entry": 100000, - "atr20": 2000, - "core_stop": 98000, - "core_stop_pct": "-2.0%", - "satellite_stop": 96000, - "satellite_stop_pct": "-4.0%" - }, - "implementation": "spec/exit/stop_loss.yaml:ABSOLUTE_RISK_STOP_V1" - }, - "RELATIVE_UNDERPERFORMANCE_ALERT_V1": { - "purpose": "기회비용 관리 (로테이션) — 손절매 아님", - "target": "강도 약화 신호 포착 → 신규 매수 차단 or 일부 trim", - "formula": "excess_ret_20d <= min(-10, rel_threshold_pct)", - "excess_ret_20d": "ret_stock_20d - beta_adj * ret_market_20d", - "rel_threshold_pct": "-clip(1.5 * sigma20_pct, 6, 18)", - "confirmation": "2영업일 연속 종가 확인 (단발 노이즈 차단)", - "action_ladder": { - "WATCH": "alert만 충족 → 신규매수 금지, 보유 유지", - "TRIM_30": "alert + [수급이탈|섹터순위하락|MA20이탈] 중 1개 → 30% 지정가", - "TRIM_50": "alert + 확인조건 2개↑ OR 절손 <=-20% → 50% 분할매도", - "EXIT_100": "하드스탑|회계위험|거래정지 → 전량 하네스 지정방식" - }, - "sample_trigger": { - "stock_ret_20d": "0%", - "market_ret_20d": "5%", - "excess_ret": "-5% (시장 수익 미달)", - "beta_adj_threshold": "-10% (기준)", - "trigger": "YES" - }, - "implementation": "spec/exit/stop_loss.yaml:RELATIVE_UNDERPERFORMANCE_ALERT_V1" - }, - "FUNDAMENTAL_THESIS_BREAK_V1": { - "purpose": "재무 위험 (ROE 붕괴, FCF 악화 등)", - "independent": "절대/상대 스탑과 독립 평가", - "implementation": "spec/exit/stop_loss.yaml:FUNDAMENTAL_THESIS_BREAK_V1" - } - }, - "mandatory_fields": { - "stop_trigger": "[price, qty, order_method, reason] 4필드 강제", - "price_normalization": "TICK_NORMALIZER_V1 통과 필수 (HS008)", - "multi_condition": "다중조건 접속사 금지: '또는', '실패 시' (HS007)", - "single_relative_exit_100": "상대성과만으로 EXIT_100 금지" - } - } - return spec - - -def build_implementation_plan() -> dict: - """구현 계획.""" - plan = { - "phase": "P3_01_STOP_LOSS_TAXONOMY_FIX", - "priority": "P0", - "files_to_update": [ - "spec/exit/stop_loss.yaml", - "spec/13_formula_registry.yaml", - "gas_data_feed.gs (함수 3개 신규)", - "tools/validate_stop_loss_policy_v1.py (신규)" - ], - "tasks": [ - { - "task_id": "P3_01_A", - "title": "stop_loss.yaml 리팩토링", - "steps": [ - "ABSOLUTE_RISK_STOP_V1 섹션 신규 생성", - "RELATIVE_UNDERPERFORMANCE_ALERT_V1 섹션 신규 생성", - "기존 '시장대비 N%' 문구 → ALERT로 강등", - "모든 트리거에 [price, qty, method, reason] 4필드 명시" - ], - "acceptance": { - "stop_policy_ambiguous_phrase_count": 0, - "stop_action_has_price_qty_method_reason": True, - "relative_only_full_liquidation_count": 0 - } - }, - { - "task_id": "P3_01_B", - "title": "formula_registry에 3개 공식 등록", - "steps": [ - "ABSOLUTE_RISK_STOP_V1 formula_id 등록", - "RELATIVE_UNDERPERFORMANCE_ALERT_V1 formula_id 등록", - "FUNDAMENTAL_THESIS_BREAK_V1 formula_id 등록", - "execution_order에 포함 (active=true)" - ], - "acceptance": { - "formula_count": 3, - "execution_order_included": True - } - }, - { - "task_id": "P3_01_C", - "title": "GAS 함수 구현", - "functions": [ - "calcAbsoluteRiskStopV1_(): entry, atr20, pct → stop_price", - "calcRelativeUnderperfAlertV1_(): ret_stock, ret_market → alert_flag", - "calcStopActionLadderV1_(): alert + conditions → action (WATCH/TRIM/EXIT)" - ], - "file": "gas_data_feed.gs", - "acceptance": { - "function_count": 3, - "gated_to_datasheet": True - } - }, - { - "task_id": "P3_01_D", - "title": "검증 도구 구현", - "file": "tools/validate_stop_loss_policy_v1.py", - "checks": [ - "gap_down 프로토콜: 09:00~09:15 시장가 투매 금지", - "TICK_NORMALIZER_V1: 모든 지정가 통과", - "multi-condition: 접속사 금지", - "single-relative EXIT_100: 금지" - ], - "acceptance": { - "validation_pass": True, - "violations": 0 - } - } - ], - "risk_mitigation": [ - "기존 '시장대비 N%' 로직은 ALERT로만 사용 (전량 청산 금지)", - "ABSOLUTE_RISK_STOP은 매우 항상 1순위 (코어 손절)", - "상대성과는 신규 매수만 차단 (보유 포지션은 허용)" - ], - "rollout": { - "phase_1": "spec/exit/stop_loss.yaml 리팩토링 + formula_registry 등록", - "phase_2": "GAS 함수 구현 + 데이터 전송", - "phase_3": "analysis_prompt.md 업데이트 (LLM 지침)", - "phase_4": "실전 신호 검증 (3건 이상)" - } - } - return plan - - -def main() -> int: - print("=" * 80) - print(" P3_01: 손절 체계 재정의 — ABSOLUTE vs RELATIVE 분리") - print("=" * 80) - - # 스펙 생성 - spec = build_stop_taxonomy() - OUTPUT_SPEC.write_text( - json.dumps(spec, ensure_ascii=False, indent=2), - encoding="utf-8" - ) - print(f"\n✓ 손절 분류 스펙 저장: {OUTPUT_SPEC.name}") - print(f" 분류: {len(spec['taxonomy'])}개 (ABSOLUTE, RELATIVE, FUNDAMENTAL)") - - # 구현 계획 - plan = build_implementation_plan() - OUTPUT_PLAN.write_text( - json.dumps(plan, ensure_ascii=False, indent=2), - encoding="utf-8" - ) - print(f"✓ 구현 계획 저장: {OUTPUT_PLAN.name}") - print(f" 파일 업데이트: {len(plan['files_to_update'])}") - print(f" 태스크: {len(plan['tasks'])}") - - # 진단 요약 - print(f"\n[문제 진단]") - for i, issue in enumerate(spec['diagnosis'].values(), 1): - print(f" {i}. {issue}") - - print(f"\n[3가지 손절 메커니즘]") - for name, detail in spec['taxonomy'].items(): - print(f" • {name}") - print(f" → {detail['purpose']}") - - print(f"\n[다음 액션]") - for task in plan['tasks'][:2]: - print(f" {task['task_id']}: {task['title']}") - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/build_p4_01_unified_routing.py b/tools/build_p4_01_unified_routing.py deleted file mode 100644 index f872084b..00000000 --- a/tools/build_p4_01_unified_routing.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -"""P4_01: 라우팅·서빙·판단 단일화 — SCALP/SWING/MOMENTUM/POSITION 결정론화""" - -from __future__ import annotations -import json, sys -from pathlib import Path -from datetime import datetime - -ROOT = Path(__file__).resolve().parent.parent -OUTPUT = ROOT / "Temp" / "p4_01_routing_packet_spec.json" - -if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): - sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1) - -def build_routing_spec() -> dict: - return { - "schema_version": "unified_route_packet_v1", - "generated_at": datetime.now().isoformat(), - "purpose": "SCALP/SWING/MOMENTUM/POSITION 판단을 결정론적 JSON으로 잠금", - "route_dimensions": ["SCALP", "SWING", "MOMENTUM", "POSITION"], - "style_weights": { - "SCALP": {"technical": 0.50, "smart_money": 0.25, "liquidity": 0.15, "fundamental": 0.10}, - "SWING": {"smart_money": 0.35, "technical": 0.30, "liquidity": 0.20, "fundamental": 0.15}, - "MOMENTUM": {"fundamental": 0.40, "smart_money": 0.30, "technical": 0.20, "liquidity": 0.10}, - "POSITION": {"fundamental": 0.55, "smart_money": 0.20, "liquidity": 0.15, "technical": 0.10} - }, - "conviction_to_pct": { - "<35": "진입 금지", - "35-49": "1.5% (PILOT)", - "50-64": "3%", - "65-79": "5%", - "80+": "7%" - }, - "route_formula": "score = weighted_score × data_quality × regime_scale × anti_chase × liquidity × cash", - "mandatory_output": [ - "ticker별 4스타일 점수(0-100)", - "best_style", - "recommended_pct", - "blocked_reason_codes (if blocked)" - ], - "implementation_files": [ - "spec/xx_routing_contract.yaml", - "gas_data_feed.gs: buildRoutePacket_()", - "tools/validate_capital_style_allocation_v1.py" - ] - } - -def main() -> int: - print("=" * 70) - print(" P4_01: 라우팅·서빙·판단 단일화") - print("=" * 70) - spec = build_routing_spec() - OUTPUT.write_text(json.dumps(spec, ensure_ascii=False, indent=2), encoding="utf-8") - print(f"\n✓ 라우팅 스펙 저장: {OUTPUT.name}") - print(f" 4가지 스타일 권중 정의 완료") - print(f" LLM 자유도 제거: 결정론적 JSON으로 잠금") - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/build_p5_01_anti_late_entry.py b/tools/build_p5_01_anti_late_entry.py deleted file mode 100644 index 2486e332..00000000 --- a/tools/build_p5_01_anti_late_entry.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -"""P5_01: 뒷북 매수·설거지 차단 — alpha_lead + pre_distribution 게이트""" - -from __future__ import annotations -import json, sys -from pathlib import Path -from datetime import datetime - -ROOT = Path(__file__).resolve().parent.parent -OUTPUT = ROOT / "Temp" / "p5_01_anti_chase_spec.json" - -if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): - sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1) - -def build_spec() -> dict: - return { - "schema_version": "anti_late_entry_v1", - "generated_at": datetime.now().isoformat(), - "problem": "late_chase_status=DEGRADE_BUY_PERMISSION 발동중. 뒷북+설거지 차단 필요", - "solution_1_alpha_lead_entry": { - "name": "ALPHA_LEAD_ENTRY_GATE_V1", - "rules": { - "pilot_allowed": "alpha_lead_score >= 75 AND lead_entry_state == PILOT_ALLOWED", - "add_on_allowed": "pilot_pnl >= 0 AND flow_confirmed=true AND breakout_volume_confirmed=true", - "pullback_allowed": "confirmed_add_on=true AND pullback_to_ma20_or_atr_band=true" - }, - "tranche_order": ["T1(30%)", "T2(30%)", "T3(40%)"], - "forbidden": ["CONFIRMED_ADD_ON 없이 T3 진입", "분위기로 PILOT 승격"] - }, - "solution_2_pre_distribution_gate": { - "name": "PRE_DISTRIBUTION_EARLY_WARNING_V1", - "block_buy_if": [ - "distribution_risk_score >= 70", - "price_up_volume_down == true", - "foreign_inst_net_sell_5d == true", - "candle_upper_tail_cluster == true" - ] - }, - "implementation": [ - "spec/exit/pre_distribution_gate.yaml", - "gas_data_feed.gs: calcAlphaLeadV1_(), calcDistributionRiskV1_()", - "tools/validate_alpha_execution_harness.py" - ] - } - -def main() -> int: - print("=" * 70) - print(" P5_01: 뒷북 매수·설거지 차단") - print("=" * 70) - spec = build_spec() - OUTPUT.write_text(json.dumps(spec, ensure_ascii=False, indent=2), encoding="utf-8") - print(f"\n✓ 뒷북 차단 스펙 저장: {OUTPUT.name}") - print(f" 1. Alpha Lead Entry: alpha_lead_score >= 75") - print(f" 2. Pre-Distribution: distribution_risk >= 70 → BUY 블록") - print(f" 3. Tranche 순서: T1(30%) → T2(30%) → T3(40%)") - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/build_p6_01_cash_optimizer.py b/tools/build_p6_01_cash_optimizer.py deleted file mode 100644 index 3fbe01e8..00000000 --- a/tools/build_p6_01_cash_optimizer.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -"""P6_01: 가치보존형 현금확보 — 5,913만원 부족액 최소 훼손으로 조성""" - -from __future__ import annotations -import json, sys -from pathlib import Path -from datetime import datetime - -ROOT = Path(__file__).resolve().parent.parent -OUTPUT = ROOT / "Temp" / "p6_01_cash_optimizer_spec.json" - -if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): - sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1) - -def build_spec() -> dict: - return { - "schema_version": "cash_recovery_optimizer_v1", - "generated_at": datetime.now().isoformat(), - "problem": { - "current_cash_pct": 3.86, - "target_cash_pct": 15.0, - "shortfall_krw": 41342219, - "cash_floor_status": "BELOW_FLOOR", - "market_regime": "BREAKDOWN" - }, - "objective": "현금 부족액 충족 AND 주식가치 훼손 최소 (raw <= 10%)", - "approach": "K2 50/50 분할: immediate_qty + rebound_wait_qty", - "key_rules": { - "rule_1": "K2 즉시 50% / 반등 대기 50% (rebound_trigger_price 전 실행 금지)", - "rule_2": "매도 순서: K3 regime_adjusted_sell_priority 사용 (코어 주도주 마지막)", - "rule_3": "value_damage_raw_pct <= 10% 상한 (cap_pass=false 허용 안함)", - "rule_4": "emergency_full_sell=true 조건: half_expected*2 < shortfall_min 일 때만" - }, - "formulas": { - "rebound_trigger_price": "prevClose + 0.5*ATR20 (tick 정규화)", - "value_damage_raw": "sum(target_sell_krw) / current_portfolio_value * 100" - }, - "implementation": [ - "spec/exit/cash_recovery.yaml", - "gas_data_feed.gs: calcCashRecoveryOptimizerV1_()", - "tools/validate_value_preservation_v1.py (raw <= 10% 검증)" - ], - "sample_case": { - "current_asset": 394191813, - "shortfall": 41342219, - "target_damage": "10% max", - "expected_recovery": 37108765 - } - } - -def main() -> int: - print("=" * 70) - print(" P6_01: 가치보존형 현금확보") - print("=" * 70) - spec = build_spec() - OUTPUT.write_text(json.dumps(spec, ensure_ascii=False, indent=2), encoding="utf-8") - print(f"\n✓ 현금 최적화 스펙 저장: {OUTPUT.name}") - print(f" 문제: 현금 3.86% → 목표 15% (부족액: 4,134만원)") - print(f" 해결: K2 50/50 분할매도 + value_damage <= 10% 유지") - print(f" 순서: K3 우선순위 적용 (코어 주도주 마지막)") - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/build_pass_100_honest_gate_v1.py b/tools/build_pass_100_honest_gate_v1.py deleted file mode 100644 index 02015fd3..00000000 --- a/tools/build_pass_100_honest_gate_v1.py +++ /dev/null @@ -1,179 +0,0 @@ -"""build_pass_100_honest_gate_v1.py — PASS_100_HONEST_GATE_V1 - -P5-T1: 거짓 없는 최종 합격선. -P5의 pass_100_honest_criteria 12개를 체크하고 전부 충족 시에만 HTS_READY 승격 허용. -구조 점수가 아닌 실제 데이터 기반으로만 PASS 가능. -""" -from __future__ import annotations - -import json -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -from v7_hardening_common import ROOT, TEMP, load_json, save_json - -DEFAULT_OUT = TEMP / "pass_100_honest_v1.json" - - -def _f(v: Any, default: float = 0.0) -> float: - try: - return float(v) - except Exception: - return default - - -def _criterion(cid: str, actual: Any, target: str, passed: bool, source: str, note: str = "") -> dict[str, Any]: - return { - "criterion_id": cid, - "actual": actual, - "target": target, - "passed": bool(passed), - "source": source, - "note": note, - } - - -def main() -> int: - trg = load_json(TEMP / "truth_reconciliation_gate_v1.json") - scr = load_json(TEMP / "smart_cash_recovery_v6.json") or load_json(TEMP / "smart_cash_recovery_v5.json") - cov = load_json(TEMP / "yaml_gs_ps_coverage.json") - parity = load_json(TEMP / "formula_gas_parity_v1.json") - golden = load_json(TEMP / "formula_behavioral_coverage_v3.json") - ycc = load_json(TEMP / "yaml_code_coverage_v1.json") - fund = load_json(TEMP / "fundamental_raw_v2.json") - pred = load_json(TEMP / "prediction_accuracy_harness_v2.json") - olock = load_json(TEMP / "operational_outcome_lock_v1.json") - late = load_json(TEMP / "late_chase_attribution_v1.json") - proof = load_json(TEMP / "algorithm_guidance_proof_v1.json") - stl = load_json(TEMP / "single_truth_ledger_v2.json") - - criteria = [ - # C1: 교차파일 정합성 (P0-T3) - _criterion("TRUTH_RECONCILIATION_PASS", - trg.get("gate"), "PASS", - trg.get("gate") == "PASS", - "truth_reconciliation_gate_v1.json", - "동일 지표 파일간 불일치 0건"), - - # C2: 은폐 지표 0 (P0-T1) - _criterion("MASKED_METRIC_COUNT_ZERO", - abs(_f(scr.get("value_damage_pct_avg")) - _f(scr.get("value_damage_pct_avg_raw"))), - "== 0", - abs(_f(scr.get("value_damage_pct_avg")) - _f(scr.get("value_damage_pct_avg_raw"))) < 0.01, - "smart_cash_recovery_v6.json", - "value_damage 표시값=원시값"), - - # C3: adjusted PASS 신호 0 (P0-T4) - _criterion("DENOMINATOR_ADJUSTED_PASS_ZERO", - cov.get("status"), "!= OK (strict 기준)", - cov.get("status") != "OK", - "yaml_gs_ps_coverage.json", - "strict < 100이면 status FAIL이어야 정직"), - - # C4: GAS strict 커버리지 decision_critical 100% (P1-T1) - # 현재 GAS V2 함수들이 의사결정 핵심공식 커버 → 12/12 체크 - _criterion("GS_STRICT_DECISION_CRITICAL_100", - "V2_VARIANTS_COVER_DECISION_CRITICAL", - "decision_critical 12공식 GAS 구현 존재", - True, # calcAntiLateEntryGateV2_, calcDistributionRiskRow_, etc. 존재 확인됨 - "gas_data_feed.gs", - "V2 함수들이 의사결정 핵심공식 커버 (strict 100%는 P1 완료 후)"), - - # C5: GAS↔Python 패리티 (P1-T2) - _criterion("GAS_PYTHON_PARITY_ZERO_MISMATCH", - parity.get("mismatch_count", parity.get("fail_count", 0)), - "== 0", - int(parity.get("mismatch_count", parity.get("fail_count", 0))) == 0, - "formula_gas_parity_v1.json"), - - # C6: Golden test coverage >= 0.98 (P2-T1) - _criterion("GOLDEN_COVERAGE_GE_98", - _f(golden.get("behavioral_coverage_pct")), - ">= 98.0", - _f(golden.get("behavioral_coverage_pct")) >= 98.0, - "formula_behavioral_coverage_v3.json"), - - # C7: 펀더멘털 커버리지 >= 80% (P2 FUNDAMENTAL) - _criterion("FUNDAMENTAL_FIELD_COMPLETENESS_GE_80", - _f(fund.get("coverage_pct")), - ">= 80.0", - _f(fund.get("coverage_pct")) >= 80.0, - "fundamental_raw_v2.json", - "현재 OCF/FCF 미수집 (GAS fetchFundamentalsWithCache_ 보완 필요)"), - - # C8: prediction_match_rate >= 60 (P4-T1, DATA-GATED) - _criterion("PREDICTION_MATCH_RATE_GE_60", - _f(pred.get("t5_ap_combined") or pred.get("prediction_match_rate_pct")), - ">= 60.0", - _f(pred.get("t5_ap_combined") or pred.get("prediction_match_rate_pct")) >= 60.0, - "prediction_accuracy_harness_v2.json", - "DATA-GATED: 표본 누적 필요"), - - # C9: t20_operational >= 30 (P4-T1, DATA-GATED) - _criterion("T20_OPERATIONAL_SAMPLES_GE_30", - int(olock.get("metrics", {}).get("operational_t20_count") or 0), - ">= 30", - int(olock.get("metrics", {}).get("operational_t20_count") or 0) >= 30, - "operational_outcome_lock_v1.json", - "DATA-GATED: 실측 T+20 30건 누적 필요"), - - # C10: late_chase_live_samples >= 30 (P4-T2, DATA-GATED) - _criterion("LATE_CHASE_LIVE_SAMPLES_GE_30", - int(late.get("samples") or 0), - ">= 30", - int(late.get("samples") or 0) >= 30, - "late_chase_attribution_v1.json", - "DATA-GATED: 뒷박 라이브 귀인 표본 30건 필요"), - - # C11: value_damage honest display (P0-T1 완료) - _criterion("VALUE_DAMAGE_DISPLAY_EQUALS_RAW", - {"display": _f(scr.get("value_damage_pct_avg")), "raw": _f(scr.get("value_damage_pct_avg_raw"))}, - "display == raw", - abs(_f(scr.get("value_damage_pct_avg")) - _f(scr.get("value_damage_pct_avg_raw"))) < 0.01, - "smart_cash_recovery_v6.json"), - - # C12: honest_proof_score >= 90 (P0-T5, DATA-GATED until T+20) - _criterion("HONEST_PROOF_SCORE_GE_90", - _f(proof.get("honest_proof_score")), - ">= 90.0", - _f(proof.get("honest_proof_score")) >= 90.0, - "algorithm_guidance_proof_v1.json", - "DATA-GATED: live_validation(T+20 30건) + prediction(60%) 충족 후 달성 가능"), - ] - - failed = [c for c in criteria if not c["passed"]] - passed_count = len(criteria) - len(failed) - gate = "PASS" if not failed else "BLOCK_DEPLOYMENT" - data_gated = [c["criterion_id"] for c in failed if "DATA-GATED" in c.get("note", "")] - code_fixable = [c["criterion_id"] for c in failed if "DATA-GATED" not in c.get("note", "")] - - result = { - "formula_id": "PASS_100_HONEST_GATE_V1", - "gate": gate, - "pass_100_honest_allowed": gate == "PASS", - "passed_count": passed_count, - "total_count": len(criteria), - "failed_count": len(failed), - "failed_criteria": [c["criterion_id"] for c in failed], - "data_gated_criteria": data_gated, - "code_fixable_criteria": code_fixable, - "criteria": criteria, - "honest_note": "이 게이트는 구조 점수가 아닌 실제 데이터 기반으로만 PASS 가능. DATA-GATED 항목은 6월 말~7월 자연 해소.", - "generated_at": datetime.now(timezone.utc).isoformat(), - } - save_json(str(DEFAULT_OUT), result) - summary = {k: v for k, v in result.items() if k != "criteria"} - print(json.dumps(summary, indent=2, ensure_ascii=True)) - if gate == "PASS": - print("PASS_100_HONEST_GATE_V1_PASS") - else: - print(f"PASS_100_HONEST_GATE_V1_BLOCK ({len(failed)} criteria failed)") - for c in failed: - note = f" [{c['note']}]" if c.get("note") else "" - print(f" {c['criterion_id']}: {c['actual']} vs target={c['target']}{note}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/build_refactor_baseline_v1.py b/tools/build_refactor_baseline_v1.py deleted file mode 100644 index 06eed54e..00000000 --- a/tools/build_refactor_baseline_v1.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import hashlib -import json -import os -from pathlib import Path -import yaml - - -ROOT = Path(__file__).resolve().parent.parent - - -def file_sha256(path: Path) -> str: - if not path.exists(): - return "" - h = hashlib.sha256() - try: - with path.open("rb") as f: - for chunk in iter(lambda: f.read(65536), b""): - h.update(chunk) - return h.hexdigest() - except Exception: - return "" - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--out", required=True) - args = parser.parse_args() - - out_path = Path(args.out) - if not out_path.is_absolute(): - out_path = ROOT / out_path - - # Load active artifact manifest - manifest_path = ROOT / "runtime" / "active_artifact_manifest.yaml" - manifest_data = {} - if manifest_path.exists(): - try: - manifest_data = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) - except Exception as e: - print(f"Error reading manifest: {e}") - - # Load canonical metrics from final_decision_packet_active.json - packet_path = ROOT / "Temp" / "final_decision_packet_active.json" - packet_hash = file_sha256(packet_path) - - # Count files - files = [p for p in ROOT.rglob("*") if p.is_file() and not p.parts[len(ROOT.parts):].count("Temp") and not p.parts[len(ROOT.parts):].count(".git")] - - baseline = { - "formula_id": "REFACTOR_BASELINE_MANIFEST_V2", - "total_files": len(files), - "active_manifest_rows": manifest_data.get("manifest_rows", []), - "canonical_metrics_hash": packet_hash, - "lock_temp_edits": True, - "source_zip_sha256": "c8d214d3c880392b176c26947367d832a55fd9f4a107bad69c7f272cd4c6b01e" - } - - # Write baseline manifest - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(yaml.safe_dump(baseline, allow_unicode=True, default_flow_style=False), encoding="utf-8") - print(f"Written baseline manifest to {out_path}") - - # Write rollback manifest v2 - rollback_path = ROOT / "runtime" / "rollback_manifest_v2.yaml" - rollback = { - "formula_id": "REFACTOR_ROLLBACK_MANIFEST_V2", - "previous_active_packet": "Temp/final_decision_packet_active.json", - "previous_manifest": "runtime/active_artifact_manifest.yaml", - "rollback_files": [ - {"path": "runtime/active_artifact_manifest.yaml", "sha256": file_sha256(manifest_path)}, - {"path": "Temp/final_decision_packet_active.json", "sha256": packet_hash} - ] - } - rollback_path.write_text(yaml.safe_dump(rollback, allow_unicode=True, default_flow_style=False), encoding="utf-8") - print(f"Written rollback manifest to {rollback_path}") - - # Ensure lineage events log exists - lineage_log = ROOT / "runtime" / "lineage_events.jsonl" - if not lineage_log.exists(): - lineage_log.touch() - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/build_truth_reconciliation_gate_v1.py b/tools/build_truth_reconciliation_gate_v1.py deleted file mode 100644 index 09085c5b..00000000 --- a/tools/build_truth_reconciliation_gate_v1.py +++ /dev/null @@ -1,149 +0,0 @@ -"""build_truth_reconciliation_gate_v1.py — TRUTH_RECONCILIATION_GATE_V1 - -P0-T3: 동일 지표가 파일마다 다른 값을 가지면 자동 FAIL. -감시 지표: prediction_match_rate_pct, t20_pass_rate, value_damage_pct_avg, - gs_coverage_pct, portfolio_alpha_confidence, performance_readiness_score -허용 오차: 비율 지표 ±0.5%p, 금액 지표 ±1원 -""" -from __future__ import annotations - -import json -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -ROOT = Path(__file__).resolve().parents[1] -TEMP = ROOT / "Temp" -DEFAULT_OUT = TEMP / "truth_reconciliation_gate_v1.json" - -TOLERANCE_RATE = 0.5 # %p -TOLERANCE_KRW = 1.0 # 원 - -# 감시 지표: (정규화된 metric_id, json_pointer_list, 단위, 제외 파일 패턴) -# 위양성 방지: 같은 key명이 다른 개념에 쓰이는 파일은 명시 제외 -MONITORED_METRICS: list[tuple[str, list[str], str, set[str]]] = [ - ("prediction_match_rate_pct", - ["prediction_match_rate_pct", "t5_ap_combined"], - "rate", - # v5 = legacy v5.todo.batch 파일 (builder 없음), v7 = 다른 블렌드 점수 - {"prediction_accuracy_harness_v5", "smart_cash_recovery_v7"}), - ("t20_pass_rate", - ["t20_pass_rate"], # pass_rate_pct는 제외 (completion_gap과 혼동) - "rate", - {"completion_gap", "phase_checks"}), # 완료기준 통과율 파일 제외 - ("value_damage_pct_avg", - ["value_damage_pct_avg"], - "rate", - # 다른 목적함수 + 구버전 아카이브 파일 제외 (현재 파이프라인 외 레거시) - {"dynamic_value_preservation", "cash_raise_value_optimizer", - "cash_raise_value_preservation", "value_preserving_cash_raise_v1", - "hts_sell_blueprint", - "smart_cash_recovery_v7.json"}), # v7 non-authoritative (2026-05-31 legacy) - ("gs_strict_coverage_pct", # gs_coverage_pct 대신 strict 전용 포인터 - ["gs_coverage_pct"], - "rate", - {"gs_native_coverage_lock"}), # native coverage는 다른 개념 - ("portfolio_alpha_confidence", - ["portfolio_alpha_confidence", "alpha_confidence"], - "rate", - set()), - ("performance_readiness_score", - ["performance_readiness_score", "blended_performance_readiness_score"], - "rate", - set()), -] - - -def _load(p: Path) -> dict[str, Any]: - if not p.exists(): - return {} - try: - obj = json.loads(p.read_text(encoding="utf-8")) - return obj if isinstance(obj, dict) else {} - except Exception: - return {} - - -def _extract(d: dict[str, Any], pointers: list[str]) -> float | None: - for ptr in pointers: - v = d.get(ptr) - if v is not None: - try: - f = float(v) - if f != 0.0 or ptr in d: - return f - except (TypeError, ValueError): - pass - return None - - -def main() -> int: - # 모든 Temp JSON 로드 - json_files = list(TEMP.glob("*.json")) - # 제외: 보고서/golden/binary - exclude_patterns = {"formula_golden", "formula_behavioral", "formula_gas_parity", "engine_audit_2026"} - candidates = [f for f in json_files if not any(ex in f.name for ex in exclude_patterns)] - - observations: dict[str, list[dict[str, Any]]] = {m[0]: [] for m in MONITORED_METRICS} - - for f in candidates: - d = _load(f) - if not d: - continue - rel = str(f.relative_to(ROOT)) - for metric_id, pointers, unit, exclude_patterns in MONITORED_METRICS: - # 제외 패턴 파일 스킵 - if any(ep in f.name for ep in exclude_patterns): - continue - val = _extract(d, pointers) - if val is not None: - observations[metric_id].append({"file": rel, "value": val}) - - conflicts: list[dict[str, Any]] = [] - for metric_id, pointers, unit, _ in MONITORED_METRICS: - obs = observations[metric_id] - if len(obs) < 2: - continue - values = [o["value"] for o in obs] - min_v, max_v = min(values), max(values) - tol = TOLERANCE_RATE if unit == "rate" else TOLERANCE_KRW - if (max_v - min_v) > tol: - conflicts.append({ - "metric_id": metric_id, - "min": min_v, - "max": max_v, - "spread": round(max_v - min_v, 4), - "tolerance": tol, - "unit": unit, - "observations": sorted(obs, key=lambda x: x["value"]), - }) - - gate = "PASS" if not conflicts else "FAIL" - result = { - "formula_id": "TRUTH_RECONCILIATION_GATE_V1", - "gate": gate, - "conflict_count": len(conflicts), - "conflicts": conflicts, - "monitored_metrics": [m[0] for m in MONITORED_METRICS], - "excluded_per_metric": {m[0]: list(m[3]) for m in MONITORED_METRICS if m[3]}, - "files_scanned": len(candidates), - "generated_at": datetime.now(timezone.utc).isoformat(), - } - - DEFAULT_OUT.parent.mkdir(parents=True, exist_ok=True) - DEFAULT_OUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - summary = {k: v for k, v in result.items() if k != "conflicts"} - print(json.dumps(summary, indent=2, ensure_ascii=False)) - if gate == "PASS": - print("TRUTH_RECONCILIATION_GATE_V1_PASS") - else: - print(f"TRUTH_RECONCILIATION_GATE_V1_FAIL ({len(conflicts)} conflicts)") - for c in conflicts: - print(f" {c['metric_id']}: spread={c['spread']} (tol={c['tolerance']})") - for o in c["observations"]: - print(f" {o['file']}: {o['value']}") - return 0 if gate == "PASS" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/build_unified_route_packet_v1.py b/tools/build_unified_route_packet_v1.py deleted file mode 100644 index 82b0bfde..00000000 --- a/tools/build_unified_route_packet_v1.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -TEMP = ROOT / "Temp" -DEFAULT_CAPITAL = TEMP / "capital_style_allocation_v1.json" -DEFAULT_HORIZON = TEMP / "horizon_classification_v1.json" -DEFAULT_FUND = TEMP / "fundamental_multifactor_v3.json" -DEFAULT_OUT = TEMP / "unified_route_packet_v1.json" -FORMULA_ID = "UNIFIED_ROUTE_PACKET_V1" -VALID_STYLES = ("SCALP", "SWING", "MOMENTUM", "POSITION") - - -def _load(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - obj = json.loads(path.read_text(encoding="utf-8")) - except Exception: - return {} - return obj if isinstance(obj, dict) else {} - - -def _f(v: Any, default: float = 0.0) -> float: - try: - return float(v) - except Exception: - return default - - -def _best_style(row: dict[str, Any]) -> dict[str, Any]: - styles = row.get("styles") or [] - best = max( - [s for s in styles if isinstance(s, dict)], - key=lambda s: _f(s.get("conviction_score")), - default={}, - ) - return best if isinstance(best, dict) else {} - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--capital", default=str(DEFAULT_CAPITAL)) - ap.add_argument("--horizon", default=str(DEFAULT_HORIZON)) - ap.add_argument("--fund", default=str(DEFAULT_FUND)) - ap.add_argument("--out", default=str(DEFAULT_OUT)) - args = ap.parse_args() - - capital_path = Path(args.capital) - horizon_path = Path(args.horizon) - fund_path = Path(args.fund) - out_path = Path(args.out) - if not capital_path.is_absolute(): - capital_path = ROOT / capital_path - if not horizon_path.is_absolute(): - horizon_path = ROOT / horizon_path - if not fund_path.is_absolute(): - fund_path = ROOT / fund_path - if not out_path.is_absolute(): - out_path = ROOT / out_path - - capital = _load(capital_path) - horizon = _load(horizon_path) - fund = _load(fund_path) - - fund_rows = {str(r.get("ticker") or ""): r for r in (fund.get("rows") or []) if isinstance(r, dict)} - hz_rows = {str(r.get("ticker") or ""): r for r in (horizon.get("rows") or []) if isinstance(r, dict)} - - rows_out: list[dict[str, Any]] = [] - blocked_count = 0 - style_score_range_violations = 0 - every_ticker_has_one_best_style = True - blocked_reason_codes_non_empty_when_blocked = True - - for row in capital.get("rows") or []: - if not isinstance(row, dict): - continue - ticker = str(row.get("ticker") or "") - name = str(row.get("name") or "") - sb = row.get("signal_breakdown") or {} - best = _best_style(row) - best_style = str(best.get("style") or "UNKNOWN") - conviction = _f(best.get("conviction_score")) - recommended_pct = _f(best.get("recommended_pct")) - actual_horizon = str(hz_rows.get(ticker, {}).get("horizon") or "UNKNOWN") - expected_horizon = {"SCALP": "SHORT", "SWING": "SHORT", "MOMENTUM": "MID", "POSITION": "LONG"}.get(best_style, "UNKNOWN") - buy_allowed = bool((fund_rows.get(ticker) or {}).get("buy_allowed")) - liquidity_label = str(sb.get("liquidity_label") or "UNKNOWN") - macro_gate = str(sb.get("macro_gate") or "UNKNOWN") - blocked_reason_codes: list[str] = [] - - if not best_style or best_style not in VALID_STYLES: - every_ticker_has_one_best_style = False - blocked_reason_codes.append("BEST_STYLE_MISSING") - if not (0.0 <= conviction <= 100.0): - style_score_range_violations += 1 - blocked_reason_codes.append("CONVICTION_RANGE") - if liquidity_label == "FROZEN": - blocked_reason_codes.append("LIQUIDITY_FROZEN") - if macro_gate == "AVOID_NEW_BUY": - blocked_reason_codes.append("MACRO_AVOID_NEW_BUY") - if not buy_allowed: - blocked_reason_codes.append("FUNDAMENTAL_BUY_BLOCK") - if expected_horizon != "UNKNOWN" and actual_horizon not in ("UNKNOWN", "ETF") and expected_horizon != actual_horizon: - blocked_reason_codes.append("STYLE_HORIZON_MISMATCH") - if conviction < 35.0: - blocked_reason_codes.append("CONVICTION_LT_35") - - blocked = len(blocked_reason_codes) > 0 - if blocked: - blocked_count += 1 - if not blocked_reason_codes: - blocked_reason_codes_non_empty_when_blocked = False - - rows_out.append({ - "ticker": ticker, - "name": name, - "best_style": best_style, - "best_style_conviction_score": round(conviction, 2), - "recommended_pct": recommended_pct, - "expected_horizon": expected_horizon, - "actual_horizon": actual_horizon, - "blocked": blocked, - "blocked_reason_codes": blocked_reason_codes, - "signal_breakdown": sb, - "formula_id": FORMULA_ID, - }) - - result = { - "formula_id": FORMULA_ID, - "gate": "PASS" if every_ticker_has_one_best_style and style_score_range_violations == 0 and blocked_reason_codes_non_empty_when_blocked else "FAIL", - "ticker_count": len(rows_out), - "blocked_count": blocked_count, - "every_ticker_has_one_best_style": every_ticker_has_one_best_style, - "every_style_score_range_0_100": style_score_range_violations == 0, - "blocked_reason_codes_non_empty_when_blocked": blocked_reason_codes_non_empty_when_blocked, - "rows": rows_out, - "source": { - "capital_style_allocation_v1_json": str(capital_path), - "horizon_classification_v1_json": str(horizon_path), - "fundamental_multifactor_v3_json": str(fund_path), - }, - } - - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - print(json.dumps({k: v for k, v in result.items() if k != "rows"}, ensure_ascii=False, indent=2)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/check_sector_flow_reliability_ready_v1.py b/tools/check_sector_flow_reliability_ready_v1.py deleted file mode 100644 index 78e940da..00000000 --- a/tools/check_sector_flow_reliability_ready_v1.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -"""Check whether WBS-9.5 can be promoted from DATA_GATED to DONE.""" -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from tools.build_sector_flow_history_progress_v1 import DEFAULT_JSON, FORMULA_ID, main as build_progress_main # type: ignore - - -def _load(path: Path) -> dict: - if not path.exists(): - return {} - try: - payload = json.loads(path.read_text(encoding="utf-8")) - return payload if isinstance(payload, dict) else {} - except Exception: - return {} - - -def main() -> int: - ap = argparse.ArgumentParser(description="Check WBS-9.5 readiness.") - ap.add_argument("--input", default=str(ROOT / "Temp" / "sector_flow_history_progress_v1.json")) - ap.add_argument("--json", action="store_true") - args = ap.parse_args() - - input_path = Path(args.input) - input_path = input_path if input_path.is_absolute() else ROOT / input_path - if not input_path.exists(): - build_progress_main() - - payload = _load(input_path) - current = int(payload.get("current_dates") or 0) - target = int(payload.get("target_dates") or 30) - ready = current >= target and payload.get("status") == "DONE" - - result = { - "formula_id": "WBS_9_5_RELIABILITY_READY_V1", - "source": str(input_path), - "current_dates": current, - "target_dates": target, - "ready": ready, - "status": "DONE" if ready else "DATA_GATED", - "message": "WBS-9.5 can be promoted" if ready else "WBS-9.5 remains DATA_GATED", - } - - print(json.dumps(result, ensure_ascii=False, indent=2) if args.json else f"{result['status']}: {result['message']} ({current}/{target})") - return 0 if ready else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) - diff --git a/tools/gas_thin_adapter_phase2_extract.py b/tools/gas_thin_adapter_phase2_extract.py deleted file mode 100644 index c259c394..00000000 --- a/tools/gas_thin_adapter_phase2_extract.py +++ /dev/null @@ -1,277 +0,0 @@ -""" -GAS_THIN_ADAPTER_POLICY_V1 — Phase 2: Extract -spec/39_gas_thin_adapter_policy.yaml의 extract 단계. - -audit_gas_thin_adapter_v1.py 결과(forbidden 23개)를 읽어 -각 GAS 함수의 Python canonical 대응을 매핑한다. -결과를 Temp/gas_python_migration_map_v1.json에 저장. -""" -from __future__ import annotations -import json -import sys -from datetime import datetime -from pathlib import Path - -ROOT = Path(__file__).parent.parent - -# ── GAS forbidden 함수 → Python canonical 매핑 ──────────────────────────── -# status: -# MAPPED — Python에 동등 구현 존재, GAS 버전 제거 가능 -# NEEDS_STUB — Python 등가 미존재, stub 신규 작성 필요 -# PARTIAL — 부분 구현 존재, 확장 필요 -MIGRATION_MAP: list[dict] = [ - { - "gas_file": "gdc_01_fetch_fundamentals.gs", - "gas_function": "_mergePositionRecord_", - "responsibility": ["stop_loss"], - "status": "MAPPED", - "python_module": "src/quant_engine/convert_xlsx_to_json.py", - "python_function": "normalize_backdata_harness_payload", - "note": "포지션 레코드 병합은 convert_xlsx_to_json의 backdata 정규화 로직이 담당", - }, - { - "gas_file": "gdc_02_account_satellite.gs", - "gas_function": "_addTickerRoute_", - "responsibility": ["unknown"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "calc_semiconductor_cluster", - "note": "ticker 라우팅 집계는 inject_computed_harness의 cluster/route 집계 로직이 담당", - }, - { - "gas_file": "gdf_01_price_metrics.gs", - "gas_function": "calcApexTradePlan_", - "responsibility": ["sizing", "normalize"], - "status": "MAPPED", - "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_position_size", - "note": "포지션 사이징 계획은 compute_position_size (POSITION_SIZE_V1) 담당", - }, - { - "gas_file": "gdf_02_harness_assembly.gs", - "gas_function": "assembleHarnessCoreLayers_", - "responsibility": ["sizing"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "main", - "note": "하네스 코어 레이어 조립은 inject_computed_harness.main() 전체 파이프라인이 담당", - }, - { - "gas_file": "gdf_02_harness_assembly.gs", - "gas_function": "applyApexCashPreservationSuite_", - "responsibility": ["unknown"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "cash_recovery", - "note": "현금 보존 스위트는 cash_recovery + compute_cash_recovery_optimizer 담당", - }, - { - "gas_file": "gdf_02_harness_assembly.gs", - "gas_function": "applyApexFeedbackSignalSuite_", - "responsibility": ["decision"], - "status": "MAPPED", - "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_final_decision", - "note": "피드백 신호 스위트는 compute_final_decision + compute_timing_decision 담당", - }, - { - "gas_file": "gdf_02_harness_assembly.gs", - "gas_function": "applyProposal54BuyBlockLocks_", - "responsibility": ["decision"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "main", - "note": "Proposal-54 매수 차단 잠금은 inject_computed_harness의 buy_permission 로직이 담당", - }, - { - "gas_file": "gdf_02_harness_assembly.gs", - "gas_function": "calcStopBreachAlert_", - "responsibility": ["stop_loss"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "calc_stop_breach_alerts", - "note": "직접 대응: calc_stop_breach_alerts (STOP_BREACH_ALERT_V1)", - }, - { - "gas_file": "gdf_02_harness_assembly.gs", - "gas_function": "calcAbsoluteRiskStopV1_", - "responsibility": ["stop_loss"], - "status": "MAPPED", - "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_stop_price_core", - "note": "절대 리스크 스탑은 compute_stop_price_core (STOP_PRICE_CORE_V1) 담당", - }, - { - "gas_file": "gdf_02_harness_assembly.gs", - "gas_function": "calcTpTriggerAlert_", - "responsibility": ["take_profit"], - "status": "MAPPED", - "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_tp_validity", - "note": "TP 트리거 알람은 compute_tp_validity (TP_TRIGGER_ALERT_V1) 담당", - }, - { - "gas_file": "gdf_03_portfolio_gates.gs", - "gas_function": "calcTpQuantityLadder_", - "responsibility": ["sizing", "take_profit"], - "status": "MAPPED", - "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_position_size", - "note": "TP 수량 사다리는 compute_position_size + TP 비율 적용으로 담당", - }, - { - "gas_file": "gdf_03_portfolio_gates.gs", - "gas_function": "scoreSellCandidate_", - "responsibility": ["unknown"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "check_sanity", - "note": "매도 후보 채점은 check_sanity (SELL_PRICE_SANITY_V1) + cash_recovery 순위 담당", - }, - { - "gas_file": "gdf_03_portfolio_gates.gs", - "gas_function": "calcPrices_", - "responsibility": ["stop_loss", "take_profit"], - "status": "MAPPED", - "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_stop_price_core", - "note": "지정가/스탑/TP 가격 계산은 compute_stop_price_core + compute_tp_validity 담당", - }, - { - "gas_file": "gdf_03_portfolio_gates.gs", - "gas_function": "runRouteFlow_", - "responsibility": ["stop_loss"], - "status": "NEEDS_STUB", - "python_module": "tools/gas_thin_adapter_stubs_v1.py", - "python_function": "stub_run_route_flow", - "note": "라우트 플로우 실행 — Python harness에 직접 대응 없음. stub 생성 필요.", - }, - { - "gas_file": "gdf_03_portfolio_gates.gs", - "gas_function": "buildOrderBlueprint_", - "responsibility": ["stop_loss", "take_profit"], - "status": "MAPPED", - "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "main (order_blueprint_json 조립)", - "note": "주문 청사진 조립은 compute_formula_outputs 의 blueprint_rows 생성 로직이 담당", - }, - { - "gas_file": "gdf_03_portfolio_gates.gs", - "gas_function": "calcDistributionRiskRow_", - "responsibility": ["risk_score"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "calc_distribution_detector_per_ticker", - "note": "설거지 위험 점수는 calc_distribution_detector_per_ticker (DISTRIBUTION_SELL_DETECTOR_V1) 담당", - }, - { - "gas_file": "gdf_04_execution_quality.gs", - "gas_function": "calcProfitPreservationRow_", - "responsibility": ["stop_loss"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "trailing_stop_v2", - "note": "이익 보존 래칫은 trailing_stop_v2 (PROFIT_RATCHET_TIERED_V2) 담당", - }, - { - "gas_file": "gdf_04_execution_quality.gs", - "gas_function": "calcSmartCashRaiseV2_", - "responsibility": ["stop_loss"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "cash_recovery", - "note": "스마트 현금 조달은 cash_recovery + K2 rebound logic 담당", - }, - { - "gas_file": "gdf_04_execution_quality.gs", - "gas_function": "calcApexExecutionHarness_", - "responsibility": ["sizing", "normalize", "decision"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "main", - "note": "Apex 실행 하네스 전체는 inject_computed_harness.main() 파이프라인이 담당", - }, - { - "gas_file": "gdf_04_execution_quality.gs", - "gas_function": "calcCashPreservationSellEngineV2_", - "responsibility": ["sizing"], - "status": "MAPPED", - "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "cash_recovery", - "note": "현금 보존 매도 엔진은 cash_recovery (CASH_RECOVERY_OPTIMIZER_V1) 담당", - }, - { - "gas_file": "gdf_04_execution_quality.gs", - "gas_function": "calcExportGate_", - "responsibility": ["unknown"], - "status": "NEEDS_STUB", - "python_module": "tools/gas_thin_adapter_stubs_v1.py", - "python_function": "stub_calc_export_gate", - "note": "export gate 계산 — Python harness에 직접 대응 없음. stub 생성 필요.", - }, - { - "gas_file": "gdf_04_execution_quality.gs", - "gas_function": "buildWatchLedger_", - "responsibility": ["stop_loss", "take_profit"], - "status": "NEEDS_STUB", - "python_module": "tools/gas_thin_adapter_stubs_v1.py", - "python_function": "stub_build_watch_ledger", - "note": "관찰 원장 조립 — Python harness에 직접 대응 없음. stub 생성 필요.", - }, - { - "gas_file": "gdf_05_alpha_engines.gs", - "gas_function": "buildShadowLedger_", - "responsibility": ["stop_loss", "sizing", "take_profit"], - "status": "MAPPED", - "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "check_sell_price_sanity (shadow_ledger 필드 포함)", - "note": "그림자 원장(BLOCKED blueprint 분리)은 check_sell_price_sanity의 shadow_ledger 필드 담당", - }, -] - - -def main() -> int: - mapped = [m for m in MIGRATION_MAP if m["status"] == "MAPPED"] - stubs = [m for m in MIGRATION_MAP if m["status"] == "NEEDS_STUB"] - partial = [m for m in MIGRATION_MAP if m["status"] == "PARTIAL"] - - output = { - "policy_id": "GAS_THIN_ADAPTER_POLICY_V1", - "phase": "extract", - "generated_at": datetime.now().isoformat(), - "summary": { - "total_forbidden": len(MIGRATION_MAP), - "mapped_count": len(mapped), - "needs_stub_count": len(stubs), - "partial_count": len(partial), - "extraction_readiness_pct": round(len(mapped) / len(MIGRATION_MAP) * 100, 1), - }, - "mapping": MIGRATION_MAP, - "needs_stub": [ - {"gas_function": m["gas_function"], "python_function": m["python_function"], - "responsibility": m["responsibility"], "note": m["note"]} - for m in stubs - ], - } - - out = ROOT / "Temp" / "gas_python_migration_map_v1.json" - out.parent.mkdir(exist_ok=True) - out.write_text(json.dumps(output, ensure_ascii=False, indent=2), encoding="utf-8") - - print("=== GAS_THIN_ADAPTER_POLICY_V1 Phase-2: Extract ===") - print(f"총 forbidden 함수 : {len(MIGRATION_MAP)}") - print(f" MAPPED : {len(mapped)}개 (Python 대응 확인)") - print(f" NEEDS_STUB : {len(stubs)}개 (stub 신규 작성 필요)") - print(f" PARTIAL : {len(partial)}개") - print(f"extraction_readiness: {output['summary']['extraction_readiness_pct']}%") - print() - print("── NEEDS_STUB 목록 ──") - for s in stubs: - print(f" {s['gas_file']:45} {s['gas_function']}") - print() - print(f"출력: {out}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/gas_thin_adapter_phase3_annotate.py b/tools/gas_thin_adapter_phase3_annotate.py deleted file mode 100644 index 81a7e62d..00000000 --- a/tools/gas_thin_adapter_phase3_annotate.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -GAS_THIN_ADAPTER_POLICY_V1 — Phase 3: thin_adapter annotation -spec/39_gas_thin_adapter_policy.yaml 참조. - -각 GAS forbidden 함수의 첫 번째 실행 라인 직전에 - // THIN_ADAPTER: delegated to Python — : -한 줄 주석을 삽입한다. 기능 코드는 변경하지 않는다 (additive-only). - -이 주석은 Phase 4 검증 도구가 "이 함수는 이전 대상으로 등록됨"을 확인하는 마커가 된다. -""" -from __future__ import annotations - -import json -import re -import sys -from pathlib import Path - -ROOT = Path(__file__).parent.parent -GAS_DIR = ROOT / "src" / "gas_adapter_parts" - -# GAS forbidden 함수 → Python 대응 매핑 (phase2_extract.py에서 가져온 데이터) -THIN_ADAPTER_MAP: list[dict] = [ - {"gas_file": "gdc_01_fetch_fundamentals.gs", "gas_function": "_mergePositionRecord_", - "responsibility": "stop_loss", "python_module": "src/quant_engine/convert_xlsx_to_json.py", - "python_function": "normalize_backdata_harness_payload"}, - {"gas_file": "gdc_02_account_satellite.gs", "gas_function": "_addTickerRoute_", - "responsibility": "unknown", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "calc_semiconductor_cluster"}, - {"gas_file": "gdf_01_price_metrics.gs", "gas_function": "calcApexTradePlan_", - "responsibility": "sizing/normalize", "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_position_size"}, - {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "assembleHarnessCoreLayers_", - "responsibility": "sizing", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "main"}, - {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "applyApexCashPreservationSuite_", - "responsibility": "decision", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "cash_recovery"}, - {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "applyApexFeedbackSignalSuite_", - "responsibility": "decision", "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_final_decision"}, - {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "applyProposal54BuyBlockLocks_", - "responsibility": "decision", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "main"}, - {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "calcStopBreachAlert_", - "responsibility": "stop_loss", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "calc_stop_breach_alerts"}, - {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "calcAbsoluteRiskStopV1_", - "responsibility": "stop_loss", "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_stop_price_core"}, - {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "calcTpTriggerAlert_", - "responsibility": "take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_tp_validity"}, - {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "calcTpQuantityLadder_", - "responsibility": "sizing/take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_position_size"}, - {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "scoreSellCandidate_", - "responsibility": "decision", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "check_sanity"}, - {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "calcPrices_", - "responsibility": "stop_loss/take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "compute_stop_price_core"}, - {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "runRouteFlow_", - "responsibility": "stop_loss", "python_module": "tools/gas_thin_adapter_stubs_v1.py", - "python_function": "stub_run_route_flow"}, - {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "buildOrderBlueprint_", - "responsibility": "stop_loss/take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "main (order_blueprint_json)"}, - {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "calcDistributionRiskRow_", - "responsibility": "risk_score", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "calc_distribution_detector_per_ticker"}, - {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcProfitPreservationRow_", - "responsibility": "stop_loss", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "trailing_stop_v2"}, - {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcSmartCashRaiseV2_", - "responsibility": "stop_loss", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "cash_recovery"}, - {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcApexExecutionHarness_", - "responsibility": "sizing/decision", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "main"}, - {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcCashPreservationSellEngineV2_", - "responsibility": "sizing", "python_module": "src/quant_engine/inject_computed_harness.py", - "python_function": "cash_recovery"}, - {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcExportGate_", - "responsibility": "unknown", "python_module": "tools/gas_thin_adapter_stubs_v1.py", - "python_function": "stub_calc_export_gate"}, - {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "buildWatchLedger_", - "responsibility": "stop_loss/take_profit", "python_module": "tools/gas_thin_adapter_stubs_v1.py", - "python_function": "stub_build_watch_ledger"}, - {"gas_file": "gdf_05_alpha_engines.gs", "gas_function": "buildShadowLedger_", - "responsibility": "stop_loss/sizing/take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", - "python_function": "check_sell_price_sanity"}, -] - -MARKER_PREFIX = "// THIN_ADAPTER:" - - -def _build_annotation(entry: dict) -> str: - return ( - f" {MARKER_PREFIX} [{entry['responsibility']}] delegated to Python " - f"— {entry['python_module']}:{entry['python_function']}" - ) - - -def _find_function_body_start(lines: list[str], func_name: str) -> int | None: - """함수 선언 다음 줄 ({이 시작되는 줄 이후 첫 번째 실행 코드 라인 인덱스)를 반환한다.""" - # function 선언 패턴: function funcName(... { - pattern = re.compile( - r"^(?:function\s+)" + re.escape(func_name) + r"\s*\(" - ) - for i, line in enumerate(lines): - if pattern.search(line): - # 선언 라인부터 { 를 찾아 함수 본문 시작 위치를 결정 - for j in range(i, min(i + 10, len(lines))): - if "{" in lines[j]: - return j # { 가 있는 줄 인덱스 반환 (다음 줄에 주석 삽입) - return None - - -def annotate_file(gs_path: Path, entries: list[dict], dry_run: bool = False) -> dict: - original = gs_path.read_text(encoding="utf-8") - lines = original.splitlines(keepends=True) - - annotated: list[tuple[int, str]] = [] # (insert-after-line-index, annotation) - already_annotated = 0 - not_found = [] - - for entry in entries: - func_name = entry["gas_function"] - annotation = _build_annotation(entry) - - body_start = _find_function_body_start(lines, func_name) - if body_start is None: - not_found.append(func_name) - continue - - # 이미 마커가 있으면 건너뜀 - next_few = "".join(lines[body_start : body_start + 3]) - if MARKER_PREFIX in next_few: - already_annotated += 1 - continue - - annotated.append((body_start, annotation + "\n")) - - if annotated and not dry_run: - # 역순 삽입 (라인 인덱스 밀림 방지) - for insert_after, text in sorted(annotated, reverse=True): - lines.insert(insert_after + 1, text) - gs_path.write_text("".join(lines), encoding="utf-8") - - return { - "file": gs_path.name, - "annotated": len(annotated), - "already_annotated": already_annotated, - "not_found": not_found, - "modified": len(annotated) > 0 and not dry_run, - } - - -def main(dry_run: bool = False) -> int: - # 파일별로 그룹화 - from collections import defaultdict - by_file: dict[str, list[dict]] = defaultdict(list) - for entry in THIN_ADAPTER_MAP: - by_file[entry["gas_file"]].append(entry) - - total_annotated = 0 - results = [] - for fname, entries in by_file.items(): - gs_path = GAS_DIR / fname - if not gs_path.exists(): - print(f" SKIP (not found): {fname}") - continue - result = annotate_file(gs_path, entries, dry_run=dry_run) - results.append(result) - total_annotated += result["annotated"] - status = "DRY" if dry_run else ("MODIFIED" if result["modified"] else "SKIP") - print(f" [{status}] {fname}: +{result['annotated']} 주석, skip={result['already_annotated']}, not_found={result['not_found']}") - - print() - print(f"=== Phase 3 thin_adapter annotation {'(dry-run)' if dry_run else '완료'} ===") - print(f"총 THIN_ADAPTER 주석 삽입: {total_annotated} / 23") - - # 결과를 Temp에 기록 - out = ROOT / "Temp" / "gas_thin_adapter_phase3_result.json" - out.parent.mkdir(exist_ok=True) - out.write_text(json.dumps({ - "phase": "thin_adapter", - "dry_run": dry_run, - "total_annotated": total_annotated, - "results": results, - }, ensure_ascii=False, indent=2), encoding="utf-8") - print(f"결과 저장: {out}") - return 0 - - -if __name__ == "__main__": - dry = "--dry-run" in sys.argv - sys.exit(main(dry_run=dry)) diff --git a/tools/generate_postgresql_history_schema_v1.py b/tools/generate_postgresql_history_schema_v1.py deleted file mode 100644 index c72af7e6..00000000 --- a/tools/generate_postgresql_history_schema_v1.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path - -import yaml - -ROOT = Path(__file__).resolve().parents[1] -CONTRACT = ROOT / "spec" / "postgresql_history_contract.yaml" -DEFAULT_SQL = ROOT / "Temp" / "postgresql_history_schema_v1.sql" -DEFAULT_JSON = ROOT / "Temp" / "postgresql_history_schema_v1.json" - - -def _columns(domain: dict) -> list[str]: - cols = domain.get("key_fields") or [] - out: list[str] = [] - for col in cols: - name = str(col) - if name in {"provenance"}: - continue - out.append(name) - return out - - -def _table_name(domain_name: str) -> str: - return domain_name - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--contract", default=str(CONTRACT)) - ap.add_argument("--sql-out", default=str(DEFAULT_SQL)) - ap.add_argument("--json-out", default=str(DEFAULT_JSON)) - args = ap.parse_args() - - contract_path = Path(args.contract) - data = yaml.safe_load(contract_path.read_text(encoding="utf-8")) - domains = data.get("domains") or {} - - sql_lines = [ - "-- PostgreSQL history-first schema", - "-- generated from spec/postgresql_history_contract.yaml", - "", - "CREATE SCHEMA IF NOT EXISTS engine_history;", - "" - ] - table_defs: dict[str, dict[str, object]] = {} - - for domain_name, domain in domains.items(): - if not isinstance(domain, dict): - continue - cols = _columns(domain) - table_name = _table_name(domain_name) - sql_lines.append(f"CREATE TABLE IF NOT EXISTS engine_history.{table_name} (") - sql_lines.append(" id BIGSERIAL PRIMARY KEY,") - for col in cols: - sql_lines.append(f" {col} TEXT NOT NULL,") - sql_lines.append(" provenance JSONB NOT NULL DEFAULT '{}'::jsonb,") - sql_lines.append(" created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()") - sql_lines.append(");") - sql_lines.append("") - sql_lines.append(f"CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at ON engine_history.{table_name} (created_at DESC);") - sql_lines.append("") - table_defs[table_name] = {"columns": cols, "description": domain.get("description", "")} - - sql_text = "\n".join(sql_lines).rstrip() + "\n" - sql_out = Path(args.sql_out) - json_out = Path(args.json_out) - sql_out.parent.mkdir(parents=True, exist_ok=True) - sql_out.write_text(sql_text, encoding="utf-8") - json_out.write_text( - yaml.safe_dump( - { - "formula_id": "POSTGRESQL_HISTORY_SCHEMA_V1", - "gate": "PASS", - "contract_path": str(contract_path.relative_to(ROOT)), - "tables": table_defs, - "sql_out": str(sql_out.relative_to(ROOT)), - }, - allow_unicode=True, - sort_keys=False, - ), - encoding="utf-8", - ) - print(f"POSTGRESQL_HISTORY_SCHEMA_V1 gate=PASS tables={len(table_defs)}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/inspect_gitea_actions_run_v2.py b/tools/inspect_gitea_actions_run_v2.py deleted file mode 100644 index 701d3052..00000000 --- a/tools/inspect_gitea_actions_run_v2.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python3 -""" -Gitea Actions Run Detail Inspector v2 -AGENTS.md 지침 준수: Gitea API를 하네스로 조회. 직접 서버 접근 금지. -formula_id: GITEA_ACTIONS_RUN_DETAIL_V2 - -토큰 우선순위 (높은 것부터): - 1. --token CLI 인자 - 2. GITEA_TOKEN_BAIK (사용자 지정 별칭) - 3. GITEA_TOKEN_TAXBAIK (기존 표준) - 4. GITEA_TOKEN (일반 fallback) - 5. GITEA_TOKEN_HOME (레거시) -""" -from __future__ import annotations - -import argparse -import json -import os -import urllib.error -import urllib.request -from pathlib import Path -from typing import Any - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_BASE_URL = "https://gitea.taxbaik.com" -DEFAULT_OWNER = "kjh2064" -DEFAULT_REPO = "QuantEngineByItz" - - -def _get_token(cli_token: str = "") -> str: - """토큰 우선순위: CLI → GITEA_TOKEN_BAIK → GITEA_TOKEN_TAXBAIK → GITEA_TOKEN → GITEA_TOKEN_HOME""" - if cli_token and cli_token.strip(): - return cli_token.strip() - for env_key in ("GITEA_TOKEN_BAIK", "GITEA_TOKEN_TAXBAIK", "GITEA_TOKEN", "GITEA_TOKEN_HOME"): - val = os.environ.get(env_key, "").strip() - if val: - return val - return "" - - -def _request_json(url: str, token: str = "") -> tuple[int, str, Any]: - headers = { - "Accept": "application/json", - "User-Agent": "QuantEngine-Harness/2.0", - } - if token: - headers["Authorization"] = f"token {token}" - req = urllib.request.Request(url, headers=headers, method="GET") - try: - with urllib.request.urlopen(req, timeout=30) as resp: - raw = resp.read().decode("utf-8", errors="replace") - payload = json.loads(raw) if raw else None - return resp.status, resp.reason, payload - except urllib.error.HTTPError as exc: - raw = exc.read().decode("utf-8", errors="replace") - try: - payload = json.loads(raw) - except Exception: - payload = raw - return exc.code, exc.reason or "", payload - - -def _summary_run(run: dict) -> dict: - return { - "id": run.get("id"), - "run_number": run.get("run_number"), - "display_title": run.get("display_title"), - "workflow_path": run.get("path"), - "event": run.get("event"), - "status": run.get("status"), - "conclusion": run.get("conclusion"), - "head_branch": run.get("head_branch"), - "head_sha": run.get("head_sha"), - "started_at": run.get("started_at"), - "completed_at": run.get("completed_at"), - "actor": (run.get("actor") or {}).get("login"), - } - - -def main() -> int: - ap = argparse.ArgumentParser(description="Gitea Actions Run Detail Inspector v2") - ap.add_argument("--base-url", default=DEFAULT_BASE_URL) - ap.add_argument("--owner", default=DEFAULT_OWNER) - ap.add_argument("--repo", default=DEFAULT_REPO) - ap.add_argument("--run-id", type=int, default=2545, help="Gitea Actions run ID") - ap.add_argument("--token", default="", help="Gitea API Personal Access Token") - ap.add_argument("--all-runs", action="store_true", help="List recent N runs") - ap.add_argument("--limit", type=int, default=10, help="Number of recent runs to list when --all-runs") - args = ap.parse_args() - - token = _get_token(args.token) - base = f"{args.base_url.rstrip('/')}/api/v1/repos/{args.owner}/{args.repo}" - errors: list[str] = [] - evidence: dict[str, Any] = { - "owner": args.owner, - "repo": args.repo, - "target_run_id": args.run_id, - "token_provided": bool(token), - } - - # 1. Target run details - run_url = f"{base}/actions/runs/{args.run_id}" - s1, _, run_payload = _request_json(run_url, token=token) - evidence["target_run_http_status"] = s1 - - if s1 != 200: - errors.append(f"Run #{args.run_id} fetch failed: HTTP {s1}") - target_run_summary = None - jobs_data = None - else: - target_run_summary = _summary_run(run_payload) - - # 2. Fetch jobs for this run - jobs_url = f"{base}/actions/runs/{args.run_id}/jobs" - s2, _, jobs_payload = _request_json(jobs_url, token=token) - evidence["jobs_http_status"] = s2 - - if s2 == 200 and isinstance(jobs_payload, dict): - raw_jobs = jobs_payload.get("workflow_jobs") or [] - jobs_data = [] - for job in raw_jobs: - steps = job.get("steps") or [] - failed_steps = [ - { - "step_number": st.get("number"), - "name": st.get("name"), - "conclusion": st.get("conclusion"), - "started_at": st.get("started_at"), - "completed_at": st.get("completed_at"), - } - for st in steps - if st.get("conclusion") not in ("success", "skipped", None) - ] - jobs_data.append({ - "job_id": job.get("id"), - "job_name": job.get("name"), - "status": job.get("status"), - "conclusion": job.get("conclusion"), - "started_at": job.get("started_at"), - "completed_at": job.get("completed_at"), - "runner_name": job.get("runner_name"), - "total_steps": len(steps), - "failed_steps": failed_steps, - }) - else: - jobs_data = None - if s2 != 200: - errors.append(f"Jobs fetch failed: HTTP {s2}") - - # 3. Recent runs list (optional) - recent_runs = None - if args.all_runs: - runs_url = f"{base}/actions/runs?limit={args.limit}" - s3, _, runs_payload = _request_json(runs_url, token=token) - evidence["runs_list_http_status"] = s3 - if s3 == 200 and isinstance(runs_payload, dict): - raw_runs = runs_payload.get("workflow_runs") or [] - recent_runs = [_summary_run(r) for r in raw_runs] - else: - errors.append(f"Runs list fetch failed: HTTP {s3}") - - result = { - "formula_id": "GITEA_ACTIONS_RUN_DETAIL_V2", - "gate": "PASS" if not errors else "FAIL", - "errors": errors, - "evidence": evidence, - "run_summary": target_run_summary, - "jobs": jobs_data, - "recent_runs": recent_runs, - } - - out = ROOT / "Temp" / f"gitea_actions_run_{args.run_id}_detail_v2.json" - out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - print(json.dumps(result, ensure_ascii=False, indent=2)) - return 0 if not errors else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/llm_radar_phase1_phase2_v1.py b/tools/llm_radar_phase1_phase2_v1.py deleted file mode 100644 index 22356fca..00000000 --- a/tools/llm_radar_phase1_phase2_v1.py +++ /dev/null @@ -1,326 +0,0 @@ -#!/usr/bin/env python3 -""" -WBS-9.6 Phase 1 & 2: LLM Radar Trust Tier + Loading Order - -Phase 1: Trust 라벨 시스템 정의 -Phase 2: 5-tier 로딩 순서 구현 -""" - -import yaml -import json -from pathlib import Path -from typing import Dict, List, Tuple -from datetime import datetime -import sys - -class LLMRadarPhase12: - """LLM Radar Trust Tier System (Phase 1 & 2)""" - - def __init__(self): - self.trust_tier_spec = Path("spec/llm_radar_trust_tiers_v1.yaml") - self.context_cache = {} - self.load_order_sequence = [] - self.results = { - "timestamp": datetime.now().isoformat(), - "phase_1": {}, - "phase_2": {}, - "summary": {} - } - - def load_trust_tier_spec(self) -> Dict: - """Trust tier 스펙 로드""" - if not self.trust_tier_spec.exists(): - print(f"[ERROR] Trust tier spec not found: {self.trust_tier_spec}") - return {} - - with open(self.trust_tier_spec, encoding='utf-8') as f: - spec = yaml.safe_load(f) - - return spec - - def phase_1_build_trust_labels(self) -> Dict: - """Phase 1: 모든 문서에 trust 라벨 지정""" - spec = self.load_trust_tier_spec() - - if not spec: - return {"status": "FAILED", "error": "Spec not loaded"} - - # Trust tier 정보 추출 - trust_tiers = spec.get("trust_tier_system", {}) - doc_classification = spec.get("document_classification", {}) - - trust_labels = {} - - # 각 분류에서 문서 추출 - for category, config in doc_classification.items(): - tier = config.get("tier") - documents = config.get("documents", []) - - if tier not in trust_tiers: - print(f"[WARNING] Unknown tier: {tier} for category {category}") - continue - - tier_info = trust_tiers[tier] - trust_level = tier_info.get("trust_level", 0) - - for doc in documents: - trust_labels[doc] = { - "tier": tier, - "trust_level": trust_level, - "category": category, - "priority": tier_info.get("loading_priority", 0) - } - - self.results["phase_1"]["total_documents"] = len(trust_labels) - self.results["phase_1"]["tier_distribution"] = self._count_by_tier(trust_labels) - self.results["phase_1"]["trust_labels"] = trust_labels - - print(f"[Phase 1] Trust labels created for {len(trust_labels)} documents") - return { - "status": "SUCCESS", - "documents_labeled": len(trust_labels), - "tiers": list(set(label["tier"] for label in trust_labels.values())) - } - - def _count_by_tier(self, labels: Dict) -> Dict: - """Tier별 문서 개수 계산""" - counts = {} - for label in labels.values(): - tier = label["tier"] - counts[tier] = counts.get(tier, 0) + 1 - return counts - - def phase_2_build_loading_order(self) -> Dict: - """Phase 2: 5-tier 로딩 순서 정의""" - spec = self.load_trust_tier_spec() - - if not spec: - return {"status": "FAILED", "error": "Spec not loaded"} - - loading_strategy = spec.get("loading_strategy", {}) - trust_tiers = spec.get("trust_tier_system", {}) - - # 로딩 순서 정의 - load_sequence = [] - - # Phase 1: Canonical (trust_level=100) - canonical_docs = [ - "spec/12_field_dictionary.yaml", - "spec/14_raw_workbook_mapping.yaml", - "spec/11_market_regime.yaml" - ] - load_sequence.append({ - "phase": 1, - "name": "Canonical References", - "tier": "canonical", - "trust_threshold": 100, - "documents": canonical_docs, - "action": "Always load" - }) - - # Phase 2: Adapter (trust_level=80) - adapter_docs = [ - "spec/09_decision_flow.yaml", - "spec/13_formula_registry.yaml" - ] - load_sequence.append({ - "phase": 2, - "name": "Adapter Bridges", - "tier": "adapter", - "trust_threshold": 80, - "documents": adapter_docs, - "action": "Load if no conflict with canonical" - }) - - # Phase 3: Reference (trust_level=60) - reference_docs = [ - "docs/WBS_9_1_F14_MIGRATION_COMPLETE_2026_06_22.md", - "docs/WBS_9_4_INCIDENT_RESPONSE_PLAYBOOK_2026_06_22.md" - ] - load_sequence.append({ - "phase": 3, - "name": "Reference Context", - "tier": "reference", - "trust_threshold": 60, - "documents": reference_docs, - "action": "Load as secondary context" - }) - - # Phase 4: Search-Based - load_sequence.append({ - "phase": 4, - "name": "Search-Based Context", - "tier": "search", - "trust_threshold": 50, - "documents": [], # Dynamic - determined by query - "action": "Retrieve by relevance + tier" - }) - - # Phase 5: Fallback - load_sequence.append({ - "phase": 5, - "name": "LLM Knowledge Fallback", - "tier": "fallback", - "trust_threshold": 0, - "documents": [], # LLM internal - "action": "Use LLM training data only" - }) - - self.load_order_sequence = load_sequence - self.results["phase_2"]["loading_phases"] = load_sequence - self.results["phase_2"]["total_phases"] = len(load_sequence) - - print(f"[Phase 2] Loading order defined for {len(load_sequence)} phases") - return { - "status": "SUCCESS", - "phases": len(load_sequence), - "total_documents_to_load": sum(len(p.get("documents", [])) for p in load_sequence) - } - - def generate_llm_context_builder_pseudo_code(self) -> str: - """LLM context builder를 위한 의사 코드 생성""" - pseudo_code = """ -// LLM Radar Phase 1 & 2 Context Builder -function buildContextWithTrustTiers(query, userContext) { - context = [] - loadedDocs = set() - - // Phase 1: Load Canonical (100% trust) - for (doc in canonicalDocuments) { - if (doc.exists()) { - content = loadDocument(doc) - context.append({ - tier: "canonical", - trust_level: 100, - content: content, - loaded_at: phase_1 - }) - loadedDocs.add(doc) - } - } - - // Phase 2: Load Adapter (80% trust) - for (doc in adapterDocuments) { - if (doc.exists() && doc not in loadedDocs) { - content = loadDocument(doc) - if (!conflictsWithCanonical(content, context)) { - context.append({ - tier: "adapter", - trust_level: 80, - content: content, - loaded_at: phase_2 - }) - loadedDocs.add(doc) - } - } - } - - // Phase 3: Load Reference (60% trust) - for (doc in referenceDocuments) { - if (doc.exists() && doc not in loadedDocs) { - content = loadDocument(doc) - context.append({ - tier: "reference", - trust_level: 60, - content: content, - loaded_at: phase_3 - }) - loadedDocs.add(doc) - } - } - - // Phase 4: Search-Based (50% trust) - relevantDocs = searchDocuments(query, threshold=50) - for (doc in relevantDocs) { - if (doc not in loadedDocs) { - content = loadDocument(doc) - context.append({ - tier: "search", - trust_level: 50, - content: content, - relevance_score: doc.score, - loaded_at: phase_4 - }) - loadedDocs.add(doc) - } - } - - // Phase 5: Fallback (0% trust - use LLM knowledge) - if (context.isEmpty()) { - context.append({ - tier: "fallback", - trust_level: 0, - source: "llm_training_data", - loaded_at: phase_5 - }) - } - - return context -} - -// Conflict detection -function conflictsWithCanonical(adapterDoc, canonicalContext) { - for (canonical in canonicalContext) { - if (contradicts(adapterDoc, canonical)) { - logWarning("Adapter contradicts canonical") - return true - } - } - return false -} -""" - return pseudo_code - - def generate_report(self) -> Dict: - """전체 리포트 생성""" - print("\n" + "="*80) - print("WBS-9.6 Phase 1 & 2: LLM Radar Trust Tier System") - print("="*80) - - # Phase 1 - phase1_result = self.phase_1_build_trust_labels() - print(f"\n[Phase 1 Result] {phase1_result}") - - # Phase 2 - phase2_result = self.phase_2_build_loading_order() - print(f"[Phase 2 Result] {phase2_result}") - - # Summary - self.results["summary"] = { - "phase_1_status": phase1_result.get("status"), - "phase_2_status": phase2_result.get("status"), - "total_trust_labels": self.results["phase_1"].get("total_documents", 0), - "loading_phases_defined": self.results["phase_2"].get("total_phases", 0), - "next_phase": "Phase 3: Dependency Graph", - "target_completion": "2026-08-15", - "error_rate_target": "50% reduction from baseline" - } - - print("\n[Summary]") - print(f" Phase 1 (Trust Labels): {self.results['phase_1'].get('total_documents', 0)} docs labeled") - print(f" Phase 2 (Load Order): {self.results['phase_2'].get('total_phases', 0)} phases defined") - print(f" Tiers: {', '.join(self.results['phase_1'].get('tier_distribution', {}).keys())}") - - return self.results - - def save_report(self, output_file: str = None) -> None: - """리포트 저장""" - if not output_file: - output_file = f"Temp/llm_radar_phase12_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - - Path(output_file).parent.mkdir(parents=True, exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(self.results, f, indent=2, ensure_ascii=False) - - print(f"\n[Save] Report saved: {output_file}") - -if __name__ == "__main__": - radar = LLMRadarPhase12() - radar.generate_report() - radar.save_report() - - # 의사 코드 출력 - print("\n" + "="*80) - print("Pseudo Code: LLM Context Builder with Trust Tiers") - print("="*80) - print(radar.generate_llm_context_builder_pseudo_code()) diff --git a/tools/resolve_field_aliases_collision_v1.py b/tools/resolve_field_aliases_collision_v1.py deleted file mode 100644 index f15562d1..00000000 --- a/tools/resolve_field_aliases_collision_v1.py +++ /dev/null @@ -1,96 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -import yaml - -ROOT = Path(__file__).resolve().parents[1] - - -def main() -> int: - field_dict_path = ROOT / "spec" / "12_field_dictionary.yaml" - if not field_dict_path.exists(): - print("Field dictionary not found.") - return 1 - - field_data = yaml.safe_load(field_dict_path.read_text(encoding="utf-8")) or {} - fields = field_data.get("field_dictionary", {}).get("fields", {}) - - # Identify all collisions - alias_to_canonicals: dict[str, list[str]] = {} - for fid, info in fields.items(): - if not info: - continue - canonical_name = info.get("canonical_name", fid) - aliases = info.get("aliases", []) - - all_names = [canonical_name] + aliases - for name in all_names: - alias_to_canonicals.setdefault(name, []).append(fid) - - collisions = {name: sorted(list(set(clist))) for name, clist in alias_to_canonicals.items() if len(set(clist)) > 1} - - if not collisions: - print("No collisions to resolve.") - return 0 - - print(f"Resolving {len(collisions)} alias collisions...") - - # We iterate and apply resolution rules - for name, clist in collisions.items(): - # Rule 1: If name matches one of the canonical names exactly, keep it only there - exact_match = None - for fid in clist: - if fields[fid].get("canonical_name") == name: - exact_match = fid - break - - if exact_match is not None: - # Remove from all other fields' aliases - for fid in clist: - if fid != exact_match: - aliases = fields[fid].get("aliases", []) - if name in aliases: - aliases.remove(name) - fields[fid]["aliases"] = aliases - continue - - # Rule 2: Case-insensitive or close matching - # Assign to the field whose canonical name is closest to lowercase of the name - target_fid = None - lower_name = name.lower() - - # Check if lowercase maps to a canonical name - for fid in clist: - if fields[fid].get("canonical_name") == lower_name: - target_fid = fid - break - - # Suffix/prefix matching heuristic - if target_fid is None: - for fid in clist: - cname = fields[fid].get("canonical_name", "") - if cname in lower_name or lower_name in cname: - target_fid = fid - break - - # Fallback: just pick the first one - if target_fid is None: - target_fid = clist[0] - - # Keep alias in target_fid, remove from others - for fid in clist: - if fid != target_fid: - aliases = fields[fid].get("aliases", []) - if name in aliases: - aliases.remove(name) - fields[fid]["aliases"] = aliases - - # Save cleaned fields back - field_data["field_dictionary"]["fields"] = fields - field_dict_path.write_text(yaml.safe_dump(field_data, sort_keys=False, allow_unicode=True), encoding="utf-8") - print("Resolved field alias collisions successfully.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/setup_wbs81_monitoring.py b/tools/setup_wbs81_monitoring.py deleted file mode 100644 index 8e180bce..00000000 --- a/tools/setup_wbs81_monitoring.py +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env python3 -""" -WBS-8.1 모니터링 준비 - -T+20 레저 30건 목표 달성을 위한 모니터링 시스템 설정 -""" - -from datetime import datetime, timedelta -from pathlib import Path -import json - -class WBS81MonitoringSetup: - """WBS-8.1 모니터링 준비""" - - def __init__(self): - self.today = datetime.now().date() - self.target_date = datetime(2026, 7, 15).date() - self.days_until_target = (self.target_date - self.today).days - self.results = { - "timestamp": datetime.now().isoformat(), - "monitoring_setup": {} - } - - def calculate_milestones(self) -> dict: - """마일스톤 계산""" - milestones = { - "phase": "WBS-8.1: T+20 레저 30건", - "target_date": str(self.target_date), - "days_remaining": self.days_until_target, - "current_progress": 0, - "target_trades": 30, - - "timeline": { - "week_1": { - "date": str(self.today + timedelta(days=7)), - "target_accumulation": 4, - "note": "매일 ~0.5건 수집 추정" - }, - "week_2": { - "date": str(self.today + timedelta(days=14)), - "target_accumulation": 7, - "note": "누적 진행률 23%" - }, - "week_3": { - "date": str(self.today + timedelta(days=21)), - "target_accumulation": 11, - "note": "누적 진행률 37%" - }, - "week_4": { - "date": str(self.today + timedelta(days=28)), - "target_accumulation": 15, - "note": "누적 진행률 50%, 중간점" - }, - "target": { - "date": str(self.target_date), - "target_accumulation": 30, - "note": "최종 목표 달성" - } - } - } - - return milestones - - def define_monitoring_metrics(self) -> dict: - """모니터링 메트릭 정의""" - metrics = { - "daily_collection": { - "metric": "entries_added_per_day", - "target": 0.5, - "unit": "entries", - "tracking": "kis_data_collection.db row count" - }, - "t20_milestone": { - "metric": "trades_reaching_t20_date", - "target": 30, - "unit": "trades", - "tracking": "performance.t20_milestone IS NOT NULL", - "formula": "entry_date + 20 days <= today" - }, - "data_quality": { - "metric": "completeness_score", - "target": 95, - "unit": "percent", - "tracking": "NULL values in critical columns" - }, - "accuracy": { - "metric": "price_match_vs_kis_api", - "target": 100, - "unit": "percent", - "tracking": "kis_data_collection.close_price vs live KIS" - } - } - - return metrics - - def define_monitoring_tools(self) -> list: - """모니터링 도구 정의""" - tools = [ - { - "name": "auto_collect_t20_ledger_v1.py", - "frequency": "daily", - "schedule": "00:00 UTC", - "purpose": "T+20 경과 거래 자동 감지 및 기록" - }, - { - "name": "monitor_wbs_progress_v1.py", - "frequency": "hourly", - "schedule": "*/1 * * * *", - "purpose": "WBS-8 진행률 모니터링" - }, - { - "name": "validate_data_collection_v1.py", - "frequency": "daily", - "schedule": "12:00 UTC", - "purpose": "데이터 무결성 검증" - }, - { - "name": "benchmark_snapshot_admin_performance_v1.py", - "frequency": "weekly", - "schedule": "sun 00:00 UTC", - "purpose": "성능 벤치마크 (WBS-9.2와 통합)" - } - ] - - return tools - - def define_risk_factors(self) -> list: - """리스크 팩터""" - risks = [ - { - "risk": "낮은 수집 속도", - "current_rate": 0.5, - "required_rate": 0.5, - "threshold": "< 0.3 entries/day", - "mitigation": "KIS API 대역폭 확대 또는 추가 계정 활용" - }, - { - "risk": "API 다운타임", - "impact": "데이터 수집 중단", - "mitigation": "Fallback to cached data (CACHED_ONLY mode)", - "recovery_time": "< 2 minutes" - }, - { - "risk": "데이터 품질 저하", - "impact": "T+20 계산 부정확", - "mitigation": "NULL policy enforcement + CI gates", - "detection": "daily validation" - }, - { - "risk": "거래 정체", - "impact": "30건 목표 미달성", - "threshold": "< 4 entries/week", - "mitigation": "거래 전략 검토 및 조정" - } - ] - - return risks - - def setup_alerting(self) -> dict: - """알림 규칙""" - alerts = { - "critical": { - "daily_collection_failed": { - "condition": "entries_added_per_day = 0", - "action": "Immediate: Check KIS API status + logs", - "escalation": "1 hour grace period, then escalate" - }, - "no_t20_records_for_7_days": { - "condition": "No new t20_milestone for 7 days", - "action": "Review trade entry date distribution", - "escalation": "Check if T+20 threshold calculation is correct" - } - }, - "warning": { - "collection_below_target": { - "condition": "entries_added_per_day < 0.3", - "action": "Warning: Below target collection rate", - "threshold": 3, - "unit": "consecutive days" - }, - "progress_behind_schedule": { - "condition": "cumulative < (days_elapsed / total_days) * 30", - "action": "Warning: Progress behind linear schedule", - "recovery_plan": "Increase daily collection rate" - } - } - } - - return alerts - - def generate_report(self) -> dict: - """모니터링 설정 리포트""" - print("\n" + "="*80) - print("WBS-8.1 모니터링 시스템 설정") - print("="*80) - - # 마일스톤 - milestones = self.calculate_milestones() - print(f"\n[목표]") - print(f" Phase: {milestones['phase']}") - print(f" Target Date: {milestones['target_date']}") - print(f" Days Remaining: {milestones['days_remaining']}") - print(f" Target Trades: {milestones['target_trades']} entries") - - # 메트릭 - metrics = self.define_monitoring_metrics() - print(f"\n[메트릭]") - for name, metric in metrics.items(): - print(f" {name}:") - print(f" └─ Target: {metric['target']} {metric['unit']}") - print(f" └─ Tracking: {metric['tracking']}") - - # 도구 - tools = self.define_monitoring_tools() - print(f"\n[모니터링 도구]") - for tool in tools: - print(f" {tool['name']}") - print(f" └─ Schedule: {tool['frequency']} ({tool['schedule']})") - - # 리스크 - risks = self.define_risk_factors() - print(f"\n[리스크 팩터]") - for risk in risks: - print(f" {risk['risk']}") - print(f" └─ Mitigation: {risk.get('mitigation', 'TBD')}") - - # 결과 저장 - self.results["monitoring_setup"] = { - "milestones": milestones, - "metrics": metrics, - "tools": tools, - "risks": risks, - "alerts": self.setup_alerting() - } - - return self.results - -if __name__ == "__main__": - setup = WBS81MonitoringSetup() - setup.generate_report() - - # 설정 저장 - config_file = Path("Temp/wbs81_monitoring_config.json") - config_file.parent.mkdir(parents=True, exist_ok=True) - with open(config_file, 'w', encoding='utf-8') as f: - import json - json.dump(setup.results, f, indent=2, ensure_ascii=False) - - print(f"\n[저장] 모니터링 설정: {config_file}") - print("[준비 완료] 2026-07-15 T+20 레저 30건 목표 달성을 위한 모니터링 시스템 준비됨") diff --git a/tools/tag_spec_code_implementation.py b/tools/tag_spec_code_implementation.py deleted file mode 100644 index e18e98c0..00000000 --- a/tools/tag_spec_code_implementation.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 -""" -WBS-8.7: spec-코드 동기화 확장 (has_code_implementation 태그 자동화) -""" - -import yaml -from pathlib import Path -from typing import Dict, List - -SPEC_DIR = Path("spec") - -def get_code_reference_patterns() -> Dict[str, str]: - """각 spec 파일이 참조하는 코드 패턴""" - return { - # Formula references - "formula_registry": "src/quant_engine", - "decision_flow": "tools/build_final_execution_decision", - "routing": "tools/validate_order_grammar", - "market_regime": "spec/11_market_regime", - "field_dictionary": "spec/14_raw_workbook_mapping", - - # Performance/Data - "performance_contract": "tools/benchmark_snapshot_admin_performance", - "data_gaps": "tools/auto_fill", - "low_capability_llm": "tools/build_final_context_for_llm", - - # Gas/KIS - "gas_adapter": "tools/validate_gitea_secrets_contract", - "kis": "tools/validate_gitea_secrets_contract", - - # Storage - "release_dag": ".gitea/workflows", - - # Strategy - "anti_late_entry": "tools/validate_anti_late_entry_gate", - "pre_distribution": "tools/validate_pre_distribution_early_warning", - "smart_money": "tools/validate_smart_money_liquidity_gate", - "cash_floor": "tools/validate_cash_floor_policy", - } - -def should_have_code_reference(filename: str) -> bool: - """파일이 코드 참조를 가져야 하는지 판단""" - patterns = get_code_reference_patterns() - filename_lower = filename.lower() - - for pattern in patterns.keys(): - if pattern in filename_lower: - return True - - return False - -def tag_spec_file(file_path: Path) -> bool: - """spec 파일에 has_code_implementation 태그 추가""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = yaml.safe_load(f) - - if content is None: - content = {} - - # meta 섹션이 없으면 생성 - if 'meta' not in content: - content['meta'] = {} - - meta = content['meta'] - - # 이미 태그되어 있으면 스킵 - if 'has_code_implementation' in meta: - return None # 이미 처리됨 - - # 코드 참조 여부 판단 - has_reference = should_have_code_reference(file_path.stem) - - # 태그 추가 - if has_reference: - meta['has_code_implementation'] = True - meta['code_path'] = "tools/ or spec/ or .gitea/workflows" - else: - meta['has_code_implementation'] = False - - # 파일 저장 - with open(file_path, 'w', encoding='utf-8') as f: - yaml.dump(content, f, allow_unicode=True, default_flow_style=False) - - return has_reference - - except Exception as e: - print(f"Error processing {file_path}: {e}") - return None - -def main(): - """메인 함수""" - print("WBS-8.7: spec-코드 동기화 확장") - print("="*80) - - # 모든 spec 파일 찾기 - spec_files = sorted(SPEC_DIR.glob("*.yaml")) - - tagged_count = 0 - with_code = 0 - without_code = 0 - already_tagged = 0 - - for spec_file in spec_files: - result = tag_spec_file(spec_file) - - if result is None: - already_tagged += 1 - elif result: - with_code += 1 - tagged_count += 1 - print(f"[+] {spec_file.name}: has_code_implementation=true") - else: - without_code += 1 - tagged_count += 1 - print(f"[-] {spec_file.name}: has_code_implementation=false") - - print("\n" + "="*80) - print(f"[결과]") - print(f" 새로 태그된 파일: {tagged_count}") - print(f" 코드 참조 있음: {with_code}") - print(f" 코드 참조 없음: {without_code}") - print(f" 이미 태그됨: {already_tagged}") - print(f" 총 파일 수: {len(spec_files)}") - - coverage = ((tagged_count + already_tagged) / len(spec_files)) * 100 - print(f"\n[진행률] {coverage:.1f}% 완료") - - if coverage >= 90: - print("[OK] WBS-8.7 목표 달성 (>90%)") - else: - print(f"[진행 중] 목표까지 {90-coverage:.1f}% 남음") - -if __name__ == "__main__": - main() diff --git a/tools/validate_db_first_pipeline_v1.py b/tools/validate_db_first_pipeline_v1.py deleted file mode 100644 index df0da6a7..00000000 --- a/tools/validate_db_first_pipeline_v1.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import json -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - - -def main() -> int: - errors: list[str] = [] - - spec_path = ROOT / "spec" / "02_data_contract.yaml" - server_path = ROOT / "src" / "quant_engine" / "snapshot_admin_server_v1.py" - collector_path = ROOT / "src" / "quant_engine" / "kis_data_collection_v1.py" - - # Check if files exist before reading - spec_exists = spec_path.exists() - server_exists = server_path.exists() - collector_exists = collector_path.exists() - - spec_text = spec_path.read_text(encoding="utf-8") if spec_exists else "" - server_text = server_path.read_text(encoding="utf-8") if server_exists else "" - collector_text = collector_path.read_text(encoding="utf-8") if collector_exists else "" - - if not spec_exists: - print(f"Warning: {spec_path} not found, skipping validation") - if not server_exists: - print(f"Warning: {server_path} not found, skipping server validation") - if not collector_exists: - print(f"Warning: {collector_path} not found, skipping collector validation") - - # Only check markers in files that exist - marker_checks = { - "spec/db-first": (spec_text, "DB 기반 수집 결과를 바탕으로 생성된 파생 보고서 증빙", spec_exists), - "spec/db-first-xlsx": (spec_text, "xlsx는 HTS 잔고·거래내역 판독 또는 DB 반영 이전의 보조 감사 소스", spec_exists), - "server/json-role": (server_text, "derived_report_evidence", server_exists), - "server/json-evidence": (server_text, "Derived JSON Evidence Preview", server_exists), - "server/collection-trend": (server_text, "collectionTrendChart", server_exists), - "collector/db-canonical": (collector_text, "SQLite as the canonical persistence layer", collector_exists), - } - - for name, (haystack, marker, should_check) in marker_checks.items(): - if should_check and marker not in haystack: - errors.append(f"missing marker: {name}") - - # If no files exist, pass with warning - if not (spec_exists or server_exists or collector_exists): - print(json.dumps({"gate": "PASS", "errors": [], "note": "Legacy Python files not found, skipping DB pipeline validation"}, ensure_ascii=False, indent=2)) - return 0 - - if errors: - print(json.dumps({"gate": "FAIL", "errors": errors}, ensure_ascii=False, indent=2)) - return 1 - print(json.dumps({"gate": "PASS", "errors": []}, ensure_ascii=False, indent=2)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/validate_execution_readiness_matrix_v1.py b/tools/validate_execution_readiness_matrix_v1.py deleted file mode 100644 index 6fcaede2..00000000 --- a/tools/validate_execution_readiness_matrix_v1.py +++ /dev/null @@ -1,130 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_PATH = ROOT / "Temp" / "execution_readiness_matrix_v1.json" -REQUIRED_AXES = { - "data_integrity", - "routing_serving", - "serving_output_lock", - "decision_governance", - "final_judgment_lock", - "fundamental_basis", - "horizon_policy", - "smart_money_liquidity", - "cash_recovery_execution", - "execution_availability", - "performance_readiness", - "report_consistency", -} - - -def _load(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - obj = json.loads(path.read_text(encoding="utf-8")) - except Exception: - return {} - return obj if isinstance(obj, dict) else {} - - -def main() -> int: - ap = argparse.ArgumentParser(description="Validate EXECUTION_READINESS_MATRIX_V1.") - ap.add_argument("--json", default=str(DEFAULT_PATH)) - args = ap.parse_args() - - path = Path(args.json) - if not path.is_absolute(): - path = ROOT / path - - payload = _load(path) - errors: list[str] = [] - - if str(payload.get("formula_id") or "") != "EXECUTION_READINESS_MATRIX_V1": - errors.append("formula_id mismatch") - - gate = str(payload.get("gate") or "") - if gate not in {"PASS_100", "WATCH_PENDING_SAMPLE", "BLOCK_EXECUTION"}: - errors.append(f"gate={gate}") - - axes = payload.get("axes") - if not isinstance(axes, list) or not axes: - errors.append("axes must be non-empty list") - axes = [] - - seen: set[str] = set() - scores: list[float] = [] - block_count = 0 - for idx, row in enumerate(axes): - if not isinstance(row, dict): - errors.append(f"axes[{idx}] must be object") - continue - axis = str(row.get("axis") or "") - seen.add(axis) - score = row.get("score_0_100") - if not isinstance(score, (int, float)): - errors.append(f"axes[{idx}].score_0_100 must be numeric") - continue - score_f = float(score) - if score_f < 0 or score_f > 100: - errors.append(f"axes[{idx}].score_0_100 out of range") - scores.append(score_f) - row_gate = str(row.get("gate") or "") - if row_gate not in {"PASS_100", "WATCH", "BLOCK"}: - errors.append(f"axes[{idx}].gate={row_gate}") - if row_gate == "BLOCK": - block_count += 1 - if row_gate != "PASS_100" and not str(row.get("blocking_reason") or "").strip(): - errors.append(f"axes[{idx}].blocking_reason missing") - if not str(row.get("source_json") or "").strip(): - errors.append(f"axes[{idx}].source_json missing") - if not str(row.get("formula_id") or "").strip(): - errors.append(f"axes[{idx}].formula_id missing") - - missing_axes = sorted(REQUIRED_AXES - seen) - if missing_axes: - errors.append(f"missing_axes={missing_axes}") - - if scores: - expected_min = round(min(scores), 2) - actual_min = payload.get("min_axis_score") - if not isinstance(actual_min, (int, float)) or round(float(actual_min), 2) != expected_min: - errors.append(f"min_axis_score mismatch expected={expected_min} actual={actual_min}") - expected_avg = round(sum(scores) / len(scores), 2) - actual_avg = payload.get("average_axis_score") - if not isinstance(actual_avg, (int, float)) or round(float(actual_avg), 2) != expected_avg: - errors.append(f"average_axis_score mismatch expected={expected_avg} actual={actual_avg}") - - actual_blocks = payload.get("hard_block_count") - if not isinstance(actual_blocks, int) or actual_blocks != block_count: - errors.append(f"hard_block_count mismatch expected={block_count} actual={actual_blocks}") - - if gate == "PASS_100": - if block_count != 0: - errors.append("PASS_100 cannot have BLOCK axes") - if scores and min(scores) < 100.0: - errors.append("PASS_100 requires all axes score_0_100=100") - if gate == "BLOCK_EXECUTION" and block_count == 0: - errors.append("BLOCK_EXECUTION requires at least one BLOCK axis") - - if errors: - print("EXECUTION_READINESS_MATRIX_V1_FAIL") - for err in errors: - print(f" {err}") - return 1 - - print("EXECUTION_READINESS_MATRIX_V1_OK") - print(f" gate: {gate}") - print(f" min_axis_score: {float(payload.get('min_axis_score')):.2f}") - print(f" hard_block_count: {block_count}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/validate_operational_report_json.py b/tools/validate_operational_report_json.py deleted file mode 100644 index 417c8def..00000000 --- a/tools/validate_operational_report_json.py +++ /dev/null @@ -1,180 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Any - -from operational_report_contract import REPORT_SECTION_ORDER - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_PATH = ROOT / "Temp" / "operational_report.json" -SCHEMA_PATH = ROOT / "schemas" / "operational_report.schema.json" - -REQUIRED_TOP_LEVEL_KEYS = {"schema_version", "source_json", "section_count", "sections", "summary"} -REQUIRED_SECTIONS = [ - "today_decision_summary_card", - "routing_serving_trace", - "QEH_AUDIT_BLOCK", - "investment_quality_headline", - "final_judgment_table", - "operational_truth_score", - "execution_readiness_matrix", - "pass_100_criteria", - "final_execution_decision", - "concise_hts_input_sheet", - "reference_price_ledger", - "watch_breakout_gate", - "anti_whipsaw_reentry_gate", -] - - -def safe_print(message: str) -> None: - try: - print(message) - except UnicodeEncodeError: - fallback = message.encode("cp949", errors="backslashreplace").decode("cp949", errors="ignore") - print(fallback) - - -def load_json(path: Path) -> dict[str, Any]: - payload = json.loads(path.read_text(encoding="utf-8")) - return payload if isinstance(payload, dict) else {} - - -def main() -> int: - parser = argparse.ArgumentParser(description="Validate operational report JSON structure.") - parser.add_argument("--json", default=str(DEFAULT_PATH)) - args = parser.parse_args() - - path = Path(args.json) - if not path.is_absolute(): - path = ROOT / path - if not path.exists(): - print("OPERATIONAL_REPORT_JSON_FAIL: missing file") - return 1 - - payload = load_json(path) - errors: list[str] = [] - - if not SCHEMA_PATH.exists(): - print("OPERATIONAL_REPORT_JSON_FAIL: missing schema file") - return 1 - - schema = load_json(SCHEMA_PATH) - if not isinstance(schema, dict): - print("OPERATIONAL_REPORT_JSON_FAIL: invalid schema file") - return 1 - - if payload.get("schema_version") != schema.get("properties", {}).get("schema_version", {}).get("const"): - errors.append("schema_version const mismatch") - if payload.get("source_json") != schema.get("properties", {}).get("source_json", {}).get("const"): - errors.append("source_json const mismatch") - - sections = payload.get("sections") - if not isinstance(sections, list): - errors.append("sections: must be array") - sections = [] - else: - for idx, section in enumerate(sections): - if not isinstance(section, dict): - errors.append(f"sections[{idx}]: must be object") - continue - if not isinstance(section.get("name"), str) or not section.get("name").strip(): - errors.append(f"sections[{idx}]: missing name") - if not isinstance(section.get("title"), str) or not section.get("title").strip(): - errors.append(f"sections[{idx}]: missing title") - if not isinstance(section.get("markdown"), str) or not section.get("markdown").startswith(f"## {section.get('title')}"): - errors.append(f"sections[{idx}]: markdown/title mismatch") - - missing_top = REQUIRED_TOP_LEVEL_KEYS - set(payload) - if missing_top: - errors.append(f"missing_top_level_keys={sorted(missing_top)}") - - sections = payload.get("sections") - if not isinstance(sections, list) or not sections: - errors.append("sections: must be a non-empty list") - sections = [] - - if payload.get("section_count") != len(sections): - errors.append(f"section_count mismatch: stored={payload.get('section_count')} actual={len(sections)}") - - names: list[str] = [] - for idx, section in enumerate(sections): - if not isinstance(section, dict): - errors.append(f"sections[{idx}]: must be object") - continue - name = str(section.get("name") or "").strip() - title = str(section.get("title") or "").strip() - markdown = str(section.get("markdown") or "").strip() - if not name: - errors.append(f"sections[{idx}]: missing name") - if not title: - errors.append(f"sections[{idx}]: missing title") - if not markdown.startswith(f"## {title}"): - errors.append(f"sections[{idx}]: markdown/title mismatch") - names.append(name) - - if len(names) != len(set(names)): - errors.append("sections: duplicate section names detected") - - if names: - missing_canonical = [name for name in REPORT_SECTION_ORDER if name not in names] - if missing_canonical: - errors.append(f"sections: missing canonical sections={missing_canonical[:5]}") - - for required in REQUIRED_SECTIONS: - if required not in names: - errors.append(f"missing_section={required}") - - routing_idx = names.index("routing_serving_trace") if "routing_serving_trace" in names else -1 - qeh_idx = names.index("QEH_AUDIT_BLOCK") if "QEH_AUDIT_BLOCK" in names else -1 - if routing_idx < 0 or qeh_idx < 0 or routing_idx > qeh_idx: - errors.append("section_order_invalid:routing_serving_trace_before_QEH_AUDIT_BLOCK") - - summary = payload.get("summary") - if not isinstance(summary, dict): - errors.append("summary: must be object") - else: - if not isinstance(summary.get("found_settlement"), bool): - errors.append("summary.found_settlement must be bool") - if not isinstance(summary.get("found_heat"), bool): - errors.append("summary.found_heat must be bool") - if not isinstance(summary.get("found_routing"), bool): - errors.append("summary.found_routing must be bool") - if not isinstance(summary.get("found_qeh"), bool): - errors.append("summary.found_qeh must be bool") - if not isinstance(summary.get("found_concise_hts_input_sheet"), bool): - errors.append("summary.found_concise_hts_input_sheet must be bool") - if not isinstance(summary.get("found_reference_price_ledger"), bool): - errors.append("summary.found_reference_price_ledger must be bool") - if not isinstance(summary.get("canonical_order_ok"), bool): - errors.append("summary.canonical_order_ok must be bool") - if summary.get("json_validation_status") is not None and not isinstance(summary.get("json_validation_status"), str): - errors.append("summary.json_validation_status must be string or null") - if summary.get("found_outcome_eval_window") is not None and not isinstance(summary.get("found_outcome_eval_window"), bool): - errors.append("summary.found_outcome_eval_window must be bool or null") - if summary.get("outcome_eval_gate") is not None and not isinstance(summary.get("outcome_eval_gate"), str): - errors.append("summary.outcome_eval_gate must be string or null") - if summary.get("outcome_root_cause_flags") is not None and not isinstance(summary.get("outcome_root_cause_flags"), list): - errors.append("summary.outcome_root_cause_flags must be list or null") - if summary.get("found_algorithm_guidance_proof") is not None and not isinstance(summary.get("found_algorithm_guidance_proof"), bool): - errors.append("summary.found_algorithm_guidance_proof must be bool or null") - if summary.get("algorithm_guidance_proof_score") is not None and not isinstance(summary.get("algorithm_guidance_proof_score"), (int, float)): - errors.append("summary.algorithm_guidance_proof_score must be number or null") - if summary.get("algorithm_guidance_proof_gate") is not None and not isinstance(summary.get("algorithm_guidance_proof_gate"), str): - errors.append("summary.algorithm_guidance_proof_gate must be string or null") - - if errors: - for error in errors: - safe_print(error) - safe_print("OPERATIONAL_REPORT_JSON_FAIL") - return 1 - - safe_print("OPERATIONAL_REPORT_JSON_OK") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/validate_pass_100_authority_lock_v1.py b/tools/validate_pass_100_authority_lock_v1.py deleted file mode 100644 index 652a4aad..00000000 --- a/tools/validate_pass_100_authority_lock_v1.py +++ /dev/null @@ -1,69 +0,0 @@ -"""validate_pass_100_authority_lock_v1.py — P0-002 수용 검증기 - -active PASS_100 산출물이 정확히 1개이고 v3가 그것임을 검증한다. -legacy v1/v2에는 legacy_reference_only 마킹이 있어야 한다. -""" -from __future__ import annotations - -import json -from pathlib import Path - -from v7_hardening_common import ROOT, TEMP, load_json, save_json - -DEFAULT_OUT = TEMP / "pass_100_authority_lock_v1.json" - - -def main() -> int: - v3 = load_json(TEMP / "pass_100_criteria_v3.json") - v2 = load_json(TEMP / "pass_100_criteria_v2.json") - v1 = load_json(TEMP / "pass_100_criteria_v1.json") - - errors: list[str] = [] - - # v3 must be active - if not v3: - errors.append("pass_100_criteria_v3.json not found — run build_pass_100_criteria_v3.py first") - elif not v3.get("is_active"): - errors.append("pass_100_criteria_v3.json.is_active != true") - - # v3 must be the only active artifact - if v2 and v2.get("is_active"): - errors.append("pass_100_criteria_v2.json still has is_active=true — must be legacy_reference_only") - if v1 and v1.get("is_active"): - errors.append("pass_100_criteria_v1.json still has is_active=true — must be legacy_reference_only") - - # active PASS_100 not achieving PASS_100 when failed_count>0 is correct behaviour - # but authority lock must exist - active_artifact_count = sum([ - 1 if (v3 and v3.get("is_active")) else 0, - 1 if (v2 and v2.get("is_active")) else 0, - 1 if (v1 and v1.get("is_active")) else 0, - ]) - - if active_artifact_count != 1: - errors.append(f"active_artifact_count={active_artifact_count}, expected exactly 1 (pass_100_criteria_v3.json)") - - status = "PASS" if not errors else "FAIL" - result = { - "formula_id": "PASS_100_AUTHORITY_LOCK_V1", - "status": status, - "active_artifact": "pass_100_criteria_v3.json", - "active_artifact_count": active_artifact_count, - "errors": errors, - "v3_gate": v3.get("gate") if v3 else "MISSING", - "v3_score_0_100": v3.get("score_0_100") if v3 else None, - "v3_failed_count": v3.get("failed_count") if v3 else None, - } - save_json(str(DEFAULT_OUT), result) - print(json.dumps(result, ensure_ascii=False, indent=2)) - if status == "PASS": - print("PASS_100_AUTHORITY_LOCK_V1_OK") - else: - print("PASS_100_AUTHORITY_LOCK_V1_FAIL") - for e in errors: - print(f" ERROR: {e}") - return 0 if status == "PASS" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/validate_pass_100_criteria_v1.py b/tools/validate_pass_100_criteria_v1.py deleted file mode 100644 index 815d220f..00000000 --- a/tools/validate_pass_100_criteria_v1.py +++ /dev/null @@ -1,109 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_PATH = ROOT / "Temp" / "pass_100_criteria_v1.json" - - -def _load(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - obj = json.loads(path.read_text(encoding="utf-8")) - except Exception: - return {} - return obj if isinstance(obj, dict) else {} - - -def main() -> int: - ap = argparse.ArgumentParser(description="Validate PASS_100_CRITERIA_V1.") - ap.add_argument("--json", default=str(DEFAULT_PATH)) - args = ap.parse_args() - - path = Path(args.json) - if not path.is_absolute(): - path = ROOT / path - - payload = _load(path) - errors: list[str] = [] - - _VALID_IDS = {"PASS_100_CRITERIA_V1", "PASS_100_CRITERIA_V3_ALIAS_V1"} - if str(payload.get("formula_id") or "") not in _VALID_IDS: - errors.append("formula_id mismatch") - - gate = str(payload.get("gate") or "") - if gate not in {"PASS_100", "BLOCK_EXECUTION"}: - errors.append(f"gate={gate}") - if not isinstance(payload.get("pass_100_allowed"), bool): - errors.append("pass_100_allowed must be bool") - - criteria = payload.get("criteria") - if not isinstance(criteria, list) or not criteria: - errors.append("criteria must be non-empty list") - criteria = [] - - passed_count = 0 - failed: list[str] = [] - for idx, row in enumerate(criteria): - if not isinstance(row, dict): - errors.append(f"criteria[{idx}] must be object") - continue - if not str(row.get("criterion_id") or "").strip(): - errors.append(f"criteria[{idx}].criterion_id missing") - if not isinstance(row.get("passed"), bool): - errors.append(f"criteria[{idx}].passed must be bool") - continue - if row["passed"]: - passed_count += 1 - else: - failed.append(str(row.get("criterion_id") or f"criteria[{idx}]")) - if not str(row.get("remediation") or "").strip() or str(row.get("remediation")) == "NONE": - errors.append(f"criteria[{idx}].remediation missing") - if not str(row.get("source_json") or "").strip(): - errors.append(f"criteria[{idx}].source_json missing") - if not str(row.get("formula_id") or "").strip(): - errors.append(f"criteria[{idx}].formula_id missing") - - expected_score = round(passed_count / len(criteria) * 100.0, 2) if criteria else 0.0 - actual_score = payload.get("score_0_100") - if not isinstance(actual_score, (int, float)) or round(float(actual_score), 2) != expected_score: - errors.append(f"score_0_100 mismatch expected={expected_score} actual={actual_score}") - if payload.get("passed_count") != passed_count: - errors.append(f"passed_count mismatch expected={passed_count} actual={payload.get('passed_count')}") - if payload.get("failed_count") != len(failed): - errors.append(f"failed_count mismatch expected={len(failed)} actual={payload.get('failed_count')}") - if payload.get("failed_criteria") != failed: - errors.append("failed_criteria mismatch") - - pass_allowed = bool(payload.get("pass_100_allowed")) - if pass_allowed and failed: - errors.append("pass_100_allowed cannot be true with failed criteria") - if gate == "PASS_100" and failed: - errors.append("PASS_100 cannot have failed criteria") - if gate == "BLOCK_EXECUTION" and not failed: - errors.append("BLOCK_EXECUTION requires failed criteria") - if gate == "PASS_100" and not pass_allowed: - errors.append("PASS_100 requires pass_100_allowed=true") - if gate == "BLOCK_EXECUTION" and pass_allowed: - errors.append("BLOCK_EXECUTION requires pass_100_allowed=false") - - if errors: - print("PASS_100_CRITERIA_V1_FAIL") - for err in errors: - print(f" {err}") - return 1 - - print("PASS_100_CRITERIA_V1_OK") - print(f" gate: {gate}") - print(f" score_0_100: {float(payload.get('score_0_100')):.2f}") - print(f" failed_count: {len(failed)}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/validate_truthful_decision_ledger_v2.py b/tools/validate_truthful_decision_ledger_v2.py deleted file mode 100644 index e1c32ad9..00000000 --- a/tools/validate_truthful_decision_ledger_v2.py +++ /dev/null @@ -1,145 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import hashlib -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_INPUT = ROOT / "Temp" / "truthful_decision_ledger_v2.json" -DEFAULT_REPORT = ROOT / "Temp" / "operational_report.json" - - -def _load_json(path: Path) -> dict[str, Any]: - try: - data = json.loads(path.read_text(encoding="utf-8")) - except Exception: - return {} - return data if isinstance(data, dict) else {} - - -def _is_number(v: Any) -> bool: - return isinstance(v, (int, float)) and not isinstance(v, bool) - - -def _canonical(obj: Any) -> str: - return json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":")) - - -def _sha256_text(text: str) -> str: - return hashlib.sha256(text.encode("utf-8")).hexdigest() - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--input", default=str(DEFAULT_INPUT)) - ap.add_argument("--report", default=str(DEFAULT_REPORT)) - args = ap.parse_args() - - input_path = Path(args.input) - if not input_path.is_absolute(): - input_path = ROOT / input_path - report_path = Path(args.report) - if not report_path.is_absolute(): - report_path = ROOT / report_path - - payload = _load_json(input_path) - if not payload: - print("TRUTHFUL_DECISION_LEDGER_V2_FAIL") - print("- invalid input json") - return 1 - - errors: list[str] = [] - - if str(payload.get("formula_id") or "") != "TRUTHFUL_DECISION_LEDGER_V2": - errors.append("formula_id mismatch") - if payload.get("llm_numeric_generated_flag") is not False: - errors.append("llm_numeric_generated_flag must be false") - if not str(payload.get("input_hash") or "").strip(): - errors.append("input_hash missing") - if not str(payload.get("route_id") or "").strip(): - errors.append("route_id missing") - if not str(payload.get("report_hash") or "").strip(): - errors.append("report_hash missing") - elif report_path.exists(): - try: - report_payload = json.loads(report_path.read_text(encoding="utf-8")) - except Exception: - report_payload = None - if report_payload is None: - errors.append("report_json invalid") - else: - expected_report_hash = _sha256_text(_canonical(report_payload)) - if payload.get("report_hash") != expected_report_hash: - errors.append("report_hash mismatch") - - ledger_rows = payload.get("ledger_rows") - if not isinstance(ledger_rows, list): - errors.append("ledger_rows must be a list") - ledger_rows = [] - elif not ledger_rows: - print("WARNING: ledger_rows is empty (no active proposals)") - # Do not append to errors to allow gate to pass - - for i, row in enumerate(ledger_rows): - if not isinstance(row, dict): - errors.append(f"ledger_rows[{i}] must be object") - continue - required = [ - "decision_id", - "proposal_id", - "ticker", - "action", - "state", - "formula_id", - "input_hash", - "output_hash", - "source_fields", - "source_snapshot_hash", - "price_basis", - "qty_basis", - "reason_codes", - "gate_stack", - "export_gate", - "renderer_section", - "llm_numeric_generated_flag", - "outcome_binding_id", - "record_type", - ] - for key in required: - if key not in row: - errors.append(f"ledger_rows[{i}].{key} missing") - if row.get("llm_numeric_generated_flag") is not False: - errors.append(f"ledger_rows[{i}].llm_numeric_generated_flag must be false") - if not isinstance(row.get("source_fields"), list) or not row.get("source_fields"): - errors.append(f"ledger_rows[{i}].source_fields must be non-empty list") - if not isinstance(row.get("reason_codes"), list) or not row.get("reason_codes"): - errors.append(f"ledger_rows[{i}].reason_codes must be non-empty list") - if not isinstance(row.get("gate_stack"), list) or not row.get("gate_stack"): - errors.append(f"ledger_rows[{i}].gate_stack must be non-empty list") - if row.get("record_type") not in {"order_blueprint", "shadow_ledger"}: - errors.append(f"ledger_rows[{i}].record_type invalid") - if not str(row.get("output_hash") or "").strip(): - errors.append(f"ledger_rows[{i}].output_hash missing") - else: - recompute = dict(row) - recompute.pop("output_hash", None) - expected = _sha256_text(_canonical(recompute)) - if row.get("output_hash") != expected: - errors.append(f"ledger_rows[{i}].output_hash mismatch") - - if errors: - print("TRUTHFUL_DECISION_LEDGER_V2_FAIL") - for e in errors: - print(f"- {e}") - return 1 - - print("TRUTHFUL_DECISION_LEDGER_V2_OK") - print(f"rows={len(ledger_rows)}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/wbs92_performance_optimization_v1.py b/tools/wbs92_performance_optimization_v1.py deleted file mode 100644 index ebc8ab78..00000000 --- a/tools/wbs92_performance_optimization_v1.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python3 -""" -WBS-9.2: snapshot_admin 성능 최적화 - -목표: P99 < 2초 달성 -""" - -import time -import json -from pathlib import Path -from datetime import datetime -from typing import Dict, List - -class PerformanceOptimizer: - """snapshot_admin 성능 최적화""" - - def __init__(self): - self.results = { - "timestamp": datetime.now().isoformat(), - "optimizations": [], - "summary": {} - } - - def identify_optimization_opportunities(self) -> List[Dict]: - """최적화 기회 식별""" - opportunities = [ - { - "name": "Query Indexing", - "current_impact": "테이블 스캔으로 인한 지연", - "optimization": "기존 인덱스 확인 및 추가", - "tables": ["data_feed", "performance", "positions"], - "expected_improvement": "3-5배 빠르기", - "complexity": "LOW", - "status": "IDENTIFIED" - }, - { - "name": "Connection Pooling", - "current_impact": "매 요청마다 새로운 DB 연결", - "optimization": "SQLite 커넥션 풀 또는 WAL 모드", - "expected_improvement": "2-3배 빠르기", - "complexity": "MEDIUM", - "status": "IDENTIFIED" - }, - { - "name": "Query Caching", - "current_impact": "반복적인 동일 쿼리 실행", - "optimization": "Redis 또는 메모리 캐시 추가", - "expected_improvement": "10-100배 빠르기 (캐시 히트)", - "complexity": "MEDIUM", - "status": "IDENTIFIED" - }, - { - "name": "Table Partitioning", - "current_impact": "큰 테이블에서 느린 조회", - "optimization": "성능 메트릭별 파티셔닝", - "expected_improvement": "5-10배 빠르기", - "complexity": "HIGH", - "status": "IDENTIFIED" - }, - { - "name": "WAL Mode", - "current_impact": "동시 접근 시 락 경합", - "optimization": "SQLite PRAGMA journal_mode=WAL", - "expected_improvement": "2-3배 빠르기 + 동시성 향상", - "complexity": "LOW", - "status": "READY_TO_IMPLEMENT" - }, - { - "name": "PRAGMA Optimization", - "current_impact": "기본 설정으로 인한 오버헤드", - "optimization": "cache_size, synchronous, temp_store 조정", - "expected_improvement": "1.5-2배 빠르기", - "complexity": "LOW", - "status": "READY_TO_IMPLEMENT" - } - ] - - return opportunities - - def implement_wal_mode(self) -> Dict: - """WAL 모드 적용""" - import sqlite3 - - optimizations = [] - - for db_name, db_path in [ - ("kis_data_collection", "src/quant_engine/kis_data_collection.db"), - ("snapshot_admin", "src/quant_engine/snapshot_admin.db") - ]: - try: - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # WAL 모드 활성화 - cursor.execute("PRAGMA journal_mode=WAL") - mode = cursor.fetchone()[0] - - # 추가 최적화 - cursor.execute("PRAGMA synchronous=NORMAL") - cursor.execute("PRAGMA cache_size=10000") - cursor.execute("PRAGMA temp_store=MEMORY") - - conn.commit() - conn.close() - - optimizations.append({ - "database": db_name, - "optimization": "WAL mode enabled", - "journal_mode": mode, - "status": "SUCCESS" - }) - - print(f"[OK] {db_name}: WAL mode = {mode}") - except Exception as e: - optimizations.append({ - "database": db_name, - "error": str(e), - "status": "FAILED" - }) - print(f"[FAIL] {db_name}: {e}") - - return { - "optimization": "WAL Mode", - "results": optimizations - } - - def add_performance_indexes(self) -> Dict: - """성능 인덱스 추가""" - import sqlite3 - - indexes = [ - ("kis_data_collection", "data_feed", "entry_date"), - ("kis_data_collection", "data_feed", "ticker"), - ("snapshot_admin", "performance", "entry_date"), - ("snapshot_admin", "performance", "ticker"), - ("snapshot_admin", "positions", "ticker"), - ] - - results = [] - for db_name, table, column in indexes: - db_path = "src/quant_engine/" + db_name + ".db" - try: - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # 인덱스 생성 - index_name = f"idx_{table}_{column}" - cursor.execute(f"CREATE INDEX IF NOT EXISTS {index_name} ON {table}({column})") - - conn.commit() - conn.close() - - results.append({ - "database": db_name, - "table": table, - "column": column, - "index_name": index_name, - "status": "SUCCESS" - }) - - print(f"[OK] {db_name}.{table}.{column} indexed") - except Exception as e: - results.append({ - "database": db_name, - "table": table, - "column": column, - "error": str(e), - "status": "FAILED" - }) - print(f"[FAIL] {db_name}.{table}.{column}: {e}") - - return { - "optimization": "Performance Indexes", - "results": results - } - - def generate_report(self) -> Dict: - """최적화 리포트""" - print("\n" + "="*80) - print("WBS-9.2: snapshot_admin 성능 최적화") - print("="*80) - - opportunities = self.identify_optimization_opportunities() - - print("\n[식별된 최적화 기회]") - for opp in opportunities: - status = "[READY]" if opp["status"] == "READY_TO_IMPLEMENT" else "[FUTURE]" - print(f" {status} {opp['name']}") - print(f" └─ 개선: {opp['expected_improvement']}") - - # 즉시 적용 가능한 최적화 - print("\n[즉시 적용 가능한 최적화]") - - wal_result = self.implement_wal_mode() - self.results["optimizations"].append(wal_result) - - index_result = self.add_performance_indexes() - self.results["optimizations"].append(index_result) - - # 성능 목표 - print("\n[성능 목표]") - print(" P99 < 2000ms (2초)") - print(" 동시 접근 10개 테이블") - print(" 데이터 무결성: 100%") - - print("\n[예상 효과]") - print(" 1. WAL 모드: 2-3배 빠르기 + 동시성 향상") - print(" 2. 인덱싱: 3-5배 빠르기 (entry_date, ticker 조회)") - print(" 3. PRAGMA: 1.5-2배 빠르기 (캐시 최적화)") - print(" 4. 누적 효과: 5-10배 성능 개선 예상") - - self.results["summary"] = { - "target_p99_ms": 2000, - "optimizations_applied": 2, - "opportunities_identified": len(opportunities), - "expected_improvement_factor": "5-10x", - "status": "IN_PROGRESS" - } - - return self.results - -if __name__ == "__main__": - optimizer = PerformanceOptimizer() - result = optimizer.generate_report() - - # 결과 저장 - output_file = Path("Temp/wbs92_optimization_report.json") - output_file.parent.mkdir(parents=True, exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(result, f, indent=2, ensure_ascii=False) - - print(f"\n[저장] 최적화 리포트: {output_file}") - print("[완료] WBS-9.2 성능 최적화 적용 완료") diff --git a/tools/wbs93_null_policy_enforcement.py b/tools/wbs93_null_policy_enforcement.py deleted file mode 100644 index 8a795b8a..00000000 --- a/tools/wbs93_null_policy_enforcement.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env python3 -""" -WBS-9.3: NULL Policy Enforcement - -목표: 모든 DB 테이블에서 각 컬럼의 NULL 정책 강제 -- Phase 1: NULL 정책 정의 (각 테이블별 컬럼) -- Phase 2: 제약조건 검증 (NOT NULL 강제) -- Phase 3: CI 게이트 (입력 데이터 검증) -- Phase 4: 자동 복구 (NULL 값 처리) -""" - -import sqlite3 -from pathlib import Path -from datetime import datetime -import json - -class NullPolicyEnforcement: - """NULL 정책 강제""" - - def __init__(self): - self.kis_db = Path('src/quant_engine/kis_data_collection.db') - self.snapshot_db = Path('src/quant_engine/snapshot_admin.db') - self.results = { - "timestamp": datetime.now().isoformat(), - "phases": {} - } - - def phase_1_define_null_policy(self) -> dict: - """Phase 1: NULL 정책 정의""" - print("\n[Phase 1] NULL 정책 정의") - - null_policy = { - "kis_data_collection": { - "data_feed": { - "NOT_NULL": ["ticker", "entry_price", "entry_date"], - "ALLOW_NULL": ["stop_price", "target_price", "ma20", "ma60", "rsi14"] - } - }, - "snapshot_admin": { - "settings": { - "NOT_NULL": ["ordinal", "key"], - "ALLOW_NULL": ["value", "note"] - }, - "account_snapshot": { - "NOT_NULL": ["captured_at", "account", "account_type"], - "ALLOW_NULL": ["stop_price", "highest_price_since_entry", "entry_date"] - }, - "alpha_history": { - "NOT_NULL": ["entry_date", "ticker", "entry_price"], - "ALLOW_NULL": ["stop_price", "pnl_pct", "mae_pct"] - }, - "event_calendar": { - "NOT_NULL": ["event_date", "event_name"], - "ALLOW_NULL": ["event_description", "impact_level"] - }, - "core_satellite": { - "NOT_NULL": ["ticker", "name"], - "ALLOW_NULL": ["allocation_pct", "risk_score"] - } - } - } - - print(f" 정의된 테이블: {sum(len(v) for v in null_policy.values())}개") - for db, tables in null_policy.items(): - for table, policy in tables.items(): - print(f" {db}.{table}") - print(f" NOT_NULL: {len(policy['NOT_NULL'])}개 컬럼") - print(f" ALLOW_NULL: {len(policy['ALLOW_NULL'])}개 컬럼") - - return null_policy - - def phase_2_validate_constraints(self, null_policy: dict) -> dict: - """Phase 2: 제약조건 검증""" - print("\n[Phase 2] 제약조건 검증") - - validation_results = {} - - # kis_data_collection 검증 - conn = sqlite3.connect(self.kis_db) - cursor = conn.cursor() - - for table, policy in null_policy["kis_data_collection"].items(): - cursor.execute(f"PRAGMA table_info({table})") - columns = {col[1]: col[3] for col in cursor.fetchall()} - - violations = [] - for col in policy["NOT_NULL"]: - if col in columns and columns[col] == 0: - violations.append(f"{col} should be NOT NULL but is nullable") - - status = "OK" if not violations else "VIOLATION" - validation_results[f"kis.{table}"] = { - "status": status, - "violations": violations - } - print(f" kis.{table}: {status}") - if violations: - for v in violations: - print(f" [!] {v}") - - conn.close() - - # snapshot_admin 검증 - conn = sqlite3.connect(self.snapshot_db) - cursor = conn.cursor() - - for table, policy in null_policy["snapshot_admin"].items(): - if table not in ["settings", "account_snapshot", "alpha_history", "event_calendar", "core_satellite"]: - continue - - try: - cursor.execute(f"PRAGMA table_info({table})") - columns = {col[1]: col[3] for col in cursor.fetchall()} - - violations = [] - for col in policy["NOT_NULL"]: - if col in columns and columns[col] == 0: - violations.append(f"{col} should be NOT NULL but is nullable") - - status = "OK" if not violations else "VIOLATION" - validation_results[f"snapshot.{table}"] = { - "status": status, - "violations": violations - } - print(f" snapshot.{table}: {status}") - if violations: - for v in violations: - print(f" [!] {v}") - except sqlite3.OperationalError: - print(f" snapshot.{table}: SKIP (table not found)") - - conn.close() - - return validation_results - - def phase_3_ci_gates(self, null_policy: dict) -> dict: - """Phase 3: CI 게이트 (데이터 입력 검증)""" - print("\n[Phase 3] CI 게이트 (데이터 입력 검증)") - - gates = { - "pre_insert_validation": { - "description": "INSERT/UPDATE 전 NULL 검증", - "check_required_columns": True, - "check_data_types": True, - "fail_on_violation": True - }, - "post_insert_validation": { - "description": "INSERT/UPDATE 후 NULL 검증", - "check_row_count": True, - "check_integrity": True, - "fail_on_violation": True - }, - "daily_audit": { - "description": "일일 NULL 값 감시", - "schedule": "00:00 UTC", - "alert_on_violation": True - } - } - - print(f" CI 게이트: {len(gates)}개") - for gate, config in gates.items(): - print(f" {gate}: {config['description']}") - - return gates - - def phase_4_auto_recovery(self, null_policy: dict) -> dict: - """Phase 4: 자동 복구 (NULL 값 처리)""" - print("\n[Phase 4] 자동 복구") - - recovery_rules = { - "default_values": { - "ticker": "UNKNOWN", - "entry_date": "1900-01-01", - "account": "DEFAULT", - "event_date": "1900-01-01" - }, - "fallback_strategies": { - "entry_price": "use_previous_value_or_fail", - "stop_price": "use_default_or_null", - "target_price": "calculate_from_entry" - }, - "validation_levels": { - "CRITICAL": "fail_immediately", - "HIGH": "log_and_continue", - "MEDIUM": "auto_fix_and_log" - } - } - - print(f" 기본값 규칙: {len(recovery_rules['default_values'])}개") - print(f" 폴백 전략: {len(recovery_rules['fallback_strategies'])}개") - print(f" 검증 레벨: {len(recovery_rules['validation_levels'])}개") - - return recovery_rules - - def run(self) -> dict: - """전체 실행""" - print("="*80) - print("WBS-9.3: NULL Policy Enforcement") - print("="*80) - - # Phase 1: 정책 정의 - null_policy = self.phase_1_define_null_policy() - self.results["phases"]["phase_1"] = null_policy - - # Phase 2: 검증 - validation = self.phase_2_validate_constraints(null_policy) - self.results["phases"]["phase_2"] = validation - - # Phase 3: CI 게이트 - ci_gates = self.phase_3_ci_gates(null_policy) - self.results["phases"]["phase_3"] = ci_gates - - # Phase 4: 자동 복구 - recovery = self.phase_4_auto_recovery(null_policy) - self.results["phases"]["phase_4"] = recovery - - # 요약 - print("\n" + "="*80) - print("[결과 요약]") - violations_count = sum(1 for v in validation.values() if v["status"] == "VIOLATION") - print(f" 검증 결과: {len(validation) - violations_count}/{len(validation)} PASS") - print(f" CI 게이트: {len(ci_gates)}개 구현") - print(f" 자동 복구: {len(recovery['default_values'])}개 규칙") - - self.results["summary"] = { - "phase_1_status": "COMPLETE", - "phase_2_status": "VALIDATED", - "phase_3_status": "IMPLEMENTED", - "phase_4_status": "CONFIGURED", - "violations": violations_count, - "overall_status": "100%" if violations_count == 0 else "90% (violations to fix)" - } - - return self.results - -if __name__ == "__main__": - enforcer = NullPolicyEnforcement() - result = enforcer.run() - - # 결과 저장 - output_file = Path("Temp/wbs93_null_policy.json") - output_file.parent.mkdir(parents=True, exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(result, f, indent=2, ensure_ascii=False) - - print(f"\n[저장] {output_file}") - print("[완료] WBS-9.3 NULL Policy Enforcement 구현 완료") diff --git a/tools/wbs95_sector_flow_reliability.py b/tools/wbs95_sector_flow_reliability.py deleted file mode 100644 index ebdba7d9..00000000 --- a/tools/wbs95_sector_flow_reliability.py +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env python3 -""" -WBS-9.5: Sector Flow Reliability Measurement - -섹터 흐름 신뢰도 측정 -- 데이터 커버리지: 섹터별 데이터 가용도 -- 신선도: 최신 데이터의 타이밍 -- 일관성: 데이터 품질 및 이상치 감지 -""" - -import sqlite3 -from pathlib import Path -from datetime import datetime, timedelta -import json - -class SectorFlowReliability: - """섹터 흐름 신뢰도 측정""" - - def __init__(self): - self.snapshot_db = Path('src/quant_engine/snapshot_admin.db') - self.results = { - "timestamp": datetime.now().isoformat(), - "measurements": {} - } - - def measure_data_coverage(self) -> dict: - """데이터 커버리지 측정""" - print("\n[1. 데이터 커버리지]") - - conn = sqlite3.connect(self.snapshot_db) - cursor = conn.cursor() - - # 테이블 확인 - cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sector_flow_history'") - if not cursor.fetchone(): - print(" [!] sector_flow_history 테이블이 없음") - return {} - - # 총 행 수 - cursor.execute("SELECT COUNT(*) FROM sector_flow_history") - total_rows = cursor.fetchone()[0] - - # 섹터별 행 수 - cursor.execute(""" - SELECT Sector, COUNT(*) as count - FROM sector_flow_history - GROUP BY Sector - ORDER BY count DESC - """) - sector_counts = cursor.fetchall() - - coverage = { - "total_rows": total_rows, - "sectors": len(sector_counts), - "sector_distribution": {} - } - - print(f" 총 행: {total_rows}") - print(f" 섹터 수: {len(sector_counts)}") - print(f" 섹터별 분포:") - - for sector, count in sector_counts[:10]: - pct = (count / total_rows * 100) if total_rows > 0 else 0 - coverage["sector_distribution"][sector] = { - "count": count, - "percentage": round(pct, 1) - } - print(f" {sector}: {count} ({pct:.1f}%)") - - conn.close() - return coverage - - def measure_data_freshness(self) -> dict: - """데이터 신선도 측정""" - print("\n[2. 데이터 신선도]") - - conn = sqlite3.connect(self.snapshot_db) - cursor = conn.cursor() - - # 최신 날짜 확인 - cursor.execute(""" - SELECT MIN(Snapshot_Date) as earliest, MAX(Snapshot_Date) as latest - FROM sector_flow_history - """) - earliest, latest = cursor.fetchone() - - freshness = { - "earliest_date": earliest, - "latest_date": latest, - "age_days": 0 - } - - if latest: - try: - latest_dt = datetime.fromisoformat(latest) - age = (datetime.now() - latest_dt).days - freshness["age_days"] = age - print(f" 최신 데이터: {latest} ({age}일 전)") - except: - print(f" 최신 데이터: {latest}") - - if earliest: - print(f" 가장 오래된 데이터: {earliest}") - - # 시간대별 분포 - cursor.execute(""" - SELECT - DATE(Snapshot_Date) as date, - COUNT(*) as count - FROM sector_flow_history - GROUP BY DATE(Snapshot_Date) - ORDER BY date DESC - LIMIT 10 - """) - - date_dist = cursor.fetchall() - print(f" 최근 10일 분포:") - for date, count in date_dist: - print(f" {date}: {count}") - - conn.close() - return freshness - - def measure_data_consistency(self) -> dict: - """데이터 일관성 측정""" - print("\n[3. 데이터 일관성]") - - conn = sqlite3.connect(self.snapshot_db) - cursor = conn.cursor() - - consistency = { - "null_values": 0, - "outliers": 0, - "warnings": [] - } - - # NULL 값 확인 - cursor.execute(""" - SELECT - COUNT(*) as null_count, - COUNT(DISTINCT Sector) as sectors_with_nulls - FROM sector_flow_history - WHERE - Sector IS NULL - OR Snapshot_Date IS NULL - OR Sector_Score IS NULL - """) - - null_count, sectors_null = cursor.fetchone() - consistency["null_values"] = null_count - if null_count > 0: - consistency["warnings"].append(f"NULL 값 발견: {null_count}개") - print(f" [!] NULL 값: {null_count}") - - # 이상치 감지 (극단값) - cursor.execute(""" - SELECT - Sector, - MIN(Sector_Score) as min_val, - MAX(Sector_Score) as max_val, - AVG(Sector_Score) as avg_val - FROM sector_flow_history - WHERE Sector_Score IS NOT NULL - GROUP BY Sector - """) - - anomalies = 0 - for sector, min_val, max_val, avg_val in cursor.fetchall(): - if min_val is None or max_val is None: - continue - - # 극단값 감지 (평균의 5배 이상) - if avg_val and max_val > avg_val * 5: - anomalies += 1 - consistency["warnings"].append(f"{sector}: 극단값 감지 ({max_val})") - - consistency["outliers"] = anomalies - if anomalies > 0: - print(f" [!] 이상치: {anomalies}개 섹터") - - # 데이터 완정성 (중요 컬럼) - cursor.execute(""" - SELECT - (COUNT(*) - COUNT(Sector)) as missing_sectors, - (COUNT(*) - COUNT(Snapshot_Date)) as missing_dates, - (COUNT(*) - COUNT(Sector_Score)) as missing_values - FROM sector_flow_history - """) - - missing_sectors, missing_dates, missing_values = cursor.fetchone() - print(f" 누락 데이터: sector={missing_sectors}, date={missing_dates}, value={missing_values}") - - conn.close() - return consistency - - def calculate_reliability_score(self, coverage: dict, freshness: dict, consistency: dict) -> float: - """종합 신뢰도 점수 계산""" - print("\n[4. 종합 신뢰도]") - - scores = { - "coverage_score": 0, - "freshness_score": 0, - "consistency_score": 0 - } - - # 커버리지 점수 (0-100) - if coverage.get("total_rows", 0) > 0: - sector_count = coverage.get("sectors", 0) - # 10개 이상 섹터: 100점 - # 1개 미만: 0점 - scores["coverage_score"] = min(100, sector_count * 10) - print(f" 커버리지: {scores['coverage_score']:.1f}/100 ({coverage.get('sectors', 0)} 섹터)") - - # 신선도 점수 (0-100) - age_days = freshness.get("age_days", 9999) - if age_days <= 1: - scores["freshness_score"] = 100 # 1일 이내 - elif age_days <= 7: - scores["freshness_score"] = 80 # 1주일 이내 - elif age_days <= 30: - scores["freshness_score"] = 50 # 1개월 이내 - else: - scores["freshness_score"] = 20 # 오래됨 - print(f" 신선도: {scores['freshness_score']:.1f}/100 ({age_days}일 전)") - - # 일관성 점수 (0-100) - null_violations = consistency.get("null_values", 0) - outlier_count = consistency.get("outliers", 0) - warnings = len(consistency.get("warnings", [])) - - consistency_score = 100 - if null_violations > 0: - consistency_score -= min(20, null_violations / 10) - if outlier_count > 0: - consistency_score -= min(30, outlier_count * 3) - consistency_score = max(0, consistency_score) - scores["consistency_score"] = consistency_score - print(f" 일관성: {scores['consistency_score']:.1f}/100 ({warnings} 경고)") - - # 종합 점수 (가중 평균) - # 커버리지 30%, 신선도 40%, 일관성 30% - overall = ( - scores["coverage_score"] * 0.3 + - scores["freshness_score"] * 0.4 + - scores["consistency_score"] * 0.3 - ) - - print(f"\n 종합 신뢰도: {overall:.1f}/100") - - return overall - - def run(self) -> dict: - """전체 실행""" - print("="*80) - print("WBS-9.5: Sector Flow Reliability Measurement") - print("="*80) - - # 측정 - coverage = self.measure_data_coverage() - freshness = self.measure_data_freshness() - consistency = self.measure_data_consistency() - - # 신뢰도 점수 - reliability_score = self.calculate_reliability_score(coverage, freshness, consistency) - - # 결과 저장 - self.results["measurements"] = { - "coverage": coverage, - "freshness": freshness, - "consistency": consistency, - "reliability_score": reliability_score - } - - # 신뢰도 판정 - if reliability_score >= 80: - status = "HIGH (신뢰 가능)" - elif reliability_score >= 60: - status = "MEDIUM (주의 필요)" - else: - status = "LOW (개선 필요)" - - print(f"\n[판정]") - print(f" 신뢰도 상태: {status}") - print(f" 권장사항: {'데이터 사용 가능' if reliability_score >= 60 else '데이터 보완 필요'}") - - self.results["summary"] = { - "status": status, - "reliability_score": reliability_score, - "measurement_date": datetime.now().isoformat() - } - - return self.results - -if __name__ == "__main__": - measurer = SectorFlowReliability() - result = measurer.run() - - # 결과 저장 - output_file = Path("Temp/wbs95_sector_flow_reliability.json") - output_file.parent.mkdir(parents=True, exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(result, f, indent=2, ensure_ascii=False) - - print(f"\n[저장] {output_file}") - print("[완료] WBS-9.5 Sector Flow Reliability Measurement 완료") diff --git a/tools/wbs96_phase3_to_5_v1.py b/tools/wbs96_phase3_to_5_v1.py deleted file mode 100644 index 5d827bf3..00000000 --- a/tools/wbs96_phase3_to_5_v1.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env python3 -""" -WBS-9.6: LLM Radar Phase 3-5 구현 - -Phase 3: Dependency Graph -Phase 4: Terminology Registry -Phase 5: Error Validation -""" - -import json -from pathlib import Path -from datetime import datetime - -class LLMRadarPhases3to5: - """Phase 3-5 구현""" - - def __init__(self): - self.results = { - "timestamp": datetime.now().isoformat(), - "phases": {} - } - - def phase_3_dependency_graph(self) -> dict: - """Phase 3: 개념 간 의존성 그래프""" - graph = { - "phase": 3, - "name": "Dependency Graph", - "purpose": "문서 간 개념 의존성 정의", - "nodes": { - "field_dictionary": { - "tier": "canonical", - "provides": ["canonical_name", "aliases", "types"], - "depends_on": [] - }, - "market_regime": { - "tier": "canonical", - "provides": ["regime_states", "transitions"], - "depends_on": ["field_dictionary"] - }, - "decision_flow": { - "tier": "adapter", - "provides": ["routing_rules", "gates"], - "depends_on": ["field_dictionary", "market_regime"] - }, - "formula_registry": { - "tier": "adapter", - "provides": ["formula_definitions", "formulas"], - "depends_on": ["field_dictionary"] - }, - "migration_reports": { - "tier": "reference", - "provides": ["implementation_evidence"], - "depends_on": ["formula_registry", "decision_flow"] - }, - "incident_playbooks": { - "tier": "reference", - "provides": ["recovery_procedures"], - "depends_on": ["field_dictionary"] - } - }, - "edges": [ - ("decision_flow", "field_dictionary", "uses"), - ("decision_flow", "market_regime", "depends"), - ("formula_registry", "field_dictionary", "uses"), - ("migration_reports", "formula_registry", "references"), - ("migration_reports", "decision_flow", "references"), - ], - "conflict_resolution": { - "circular_dependency": "BLOCK - hierarchy enforced by tier system", - "missing_dependency": "WARN - reference without implementation", - "stale_dependency": "WARN - outdated tier reference" - } - } - - return graph - - def phase_4_terminology_registry(self) -> dict: - """Phase 4: 용어 통일 레지스트리""" - terminology = { - "phase": 4, - "name": "Terminology Registry", - "purpose": "개념 이름 충돌 제거", - "canonical_terms": { - "trade_entry": { - "canonical": "entry_date", - "aliases": ["trade_entry_date", "entry", "거래개시일"], - "definition": "거래 진입 시점 (ISO 8601)", - "context": ["data_feed", "performance"] - }, - "position_size": { - "canonical": "quantity", - "aliases": ["size", "qty", "거래수량", "수량"], - "definition": "보유 주식 수량 (주식 단위)", - "context": ["positions", "data_feed"] - }, - "exit_condition": { - "canonical": "stop_price", - "aliases": ["stop", "손절가", "exit_trigger"], - "definition": "손절매 기준 가격", - "context": ["decision_flow", "performance"] - }, - "performance_metric": { - "canonical": "pnl_pct", - "aliases": ["return", "수익률", "profit_loss_percent"], - "definition": "손익 백분율 ((close-entry)/entry*100)", - "context": ["performance"] - } - }, - "conflict_resolution": { - "method": "Canonical-first lookup", - "fallback": "Alias matching with warning", - "error": "Unknown term → DATA_MISSING" - } - } - - return terminology - - def phase_5_error_validation(self) -> dict: - """Phase 5: 에러 검증 게이트""" - validation = { - "phase": 5, - "name": "Error Validation", - "purpose": "개념 혼동 자동 감지", - "validation_rules": [ - { - "rule": "Canonical contradiction", - "description": "Canonical과 Reference가 모순", - "detection": "semantic diff check", - "action": "BLOCK - reject Reference", - "severity": "CRITICAL" - }, - { - "rule": "Missing canonical", - "description": "Canonical 문서 부재", - "detection": "tier lookup failure", - "action": "WARN - fallback to adapter", - "severity": "HIGH" - }, - { - "rule": "Stale alias", - "description": "사용 중단된 별칭 사용", - "detection": "alias version mismatch", - "action": "WARN - suggest canonical", - "severity": "MEDIUM" - }, - { - "rule": "Circular definition", - "description": "개념이 자신을 정의", - "detection": "dependency graph cycle", - "action": "BLOCK - fix definition", - "severity": "CRITICAL" - } - ], - "validation_gates": { - "pre_llm_invocation": { - "checks": ["canonical_available", "no_contradictions", "no_cycles"], - "threshold": "ALL PASS required", - "on_fail": "Use fallback tier" - }, - "post_llm_output": { - "checks": ["output_matches_canonical", "no_new_aliases"], - "threshold": "95%+ match", - "on_fail": "Log discrepancy, suggest rerun" - } - } - } - - return validation - - def generate_implementation_plan(self) -> str: - """구현 계획""" - plan = """ -## WBS-9.6 Phase 3-5 구현 계획 - -### Phase 3: Dependency Graph -- 6개 핵심 문서 그래프화 -- 3계층 (canonical → adapter → reference) -- 순환 의존성 감지 - -### Phase 4: Terminology Registry -- 4개 핵심 개념 정의 -- 각 개념별 5-10개 별칭 -- Canonical-first 룩업 - -### Phase 5: Error Validation -- 4개 검증 규칙 -- Pre-LLM + Post-LLM 검증 -- 자동 감지 및 복구 - -### 예상 효과 -- 개념 혼동 감지율: 95%+ -- 거짓 긍정율: <5% -- 오류율 감소: 50% (Phase 1&2 대비 추가 25%) -""" - return plan - - def run(self) -> dict: - """전체 실행""" - print("\n" + "="*80) - print("WBS-9.6: LLM Radar Phase 3-5 구현") - print("="*80) - - # Phase 3 - phase3 = self.phase_3_dependency_graph() - self.results["phases"]["phase_3"] = phase3 - print(f"\n[Phase 3] Dependency Graph") - print(f" 노드: {len(phase3['nodes'])}개") - print(f" 엣지: {len(phase3['edges'])}개") - - # Phase 4 - phase4 = self.phase_4_terminology_registry() - self.results["phases"]["phase_4"] = phase4 - print(f"\n[Phase 4] Terminology Registry") - print(f" 표준 용어: {len(phase4['canonical_terms'])}개") - for term, info in phase4['canonical_terms'].items(): - aliases = len(info['aliases']) - print(f" - {term}: {aliases}개 별칭") - - # Phase 5 - phase5 = self.phase_5_error_validation() - self.results["phases"]["phase_5"] = phase5 - print(f"\n[Phase 5] Error Validation") - print(f" 검증 규칙: {len(phase5['validation_rules'])}개") - print(f" 검증 게이트: {len(phase5['validation_gates'])}개") - - # 계획 - plan = self.generate_implementation_plan() - print(plan) - - self.results["summary"] = { - "total_phases_implemented": 5, - "error_rate_reduction_target": "50%", - "false_positive_rate_target": "<5%", - "confidence_threshold": "95%+", - "status": "COMPLETE" - } - - return self.results - -if __name__ == "__main__": - phases = LLMRadarPhases3to5() - result = phases.run() - - # 저장 - output_file = Path("Temp/wbs96_phase3to5.json") - output_file.parent.mkdir(parents=True, exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(result, f, indent=2, ensure_ascii=False) - - print(f"\n[저장] Phase 3-5 구현: {output_file}") - print("[완료] WBS-9.6 모든 Phase 구현 완료 (1-5)")