chore: remove 40 more orphaned one-off scripts from tools/
Second pass over the 47 lower-confidence orphan candidates held back from the earlier 34-file cleanup. This time checked two signals, not one: (1) is the script's own filename referenced anywhere, and (2) does the script's own Temp/*.json output get read by any other file (a script can be "orphaned" by name but still load-bearing if something else consumes what it produces). 7 of the 47 failed check (2) - their outputs (outcome_ledger_v1.json, pre_distribution_early_warning_v3.json, shadow_ledger_v1.json, calibration_registry_v1.json, final_execution_decision_v4.json, live_outcome_ledger_v1.json, final_decision_packet_v2.json) are read by other tools even though the producing script's name never appears elsewhere - kept those 7 in place. The remaining 40 (WBS-ticket-tagged one-offs: wbs81/92/93/95/96_*, build_p0-p6_*; and misc build_*/validate_* diagnostics with no external reference by name or by output) were deleted. Re-ran the full-repo reference search (zero hits) and quick-mode release DAG + WBS validator afterward - failure set is unchanged (same 10 tasks already known blocked on missing live trading data / a running web app + Playwright, unrelated to this deletion). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"(?<![A-Za-z0-9_]){re.escape(tok)}(?![A-Za-z0-9_])", low)
|
|
||||||
]
|
|
||||||
matched_allowed = [
|
|
||||||
tok
|
|
||||||
for tok in ALLOWED_TOKENS
|
|
||||||
if re.search(rf"(?<![A-Za-z0-9_]){re.escape(tok)}(?![A-Za-z0-9_])", low)
|
|
||||||
]
|
|
||||||
if matched_forbidden:
|
|
||||||
return "forbidden", matched_forbidden
|
|
||||||
if matched_allowed or file_name == "gas_harness_rows.gs":
|
|
||||||
return "allowed", matched_allowed or ["harness_rows"]
|
|
||||||
return "helper", []
|
|
||||||
|
|
||||||
|
|
||||||
def function_bodies(path: Path) -> 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
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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()
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
|
|
||||||
@@ -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())
|
|
||||||
@@ -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: <responsibility> delegated to Python — <python_module>:<python_function>
|
|
||||||
한 줄 주석을 삽입한다. 기능 코드는 변경하지 않는다 (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))
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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건 목표 달성을 위한 모니터링 시스템 준비됨")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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 성능 최적화 적용 완료")
|
|
||||||
@@ -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 구현 완료")
|
|
||||||
@@ -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 완료")
|
|
||||||
@@ -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)")
|
|
||||||
Reference in New Issue
Block a user