Files
QuantEngineByItz/tools/build_engine_audit_v1.py
kjh2064 ee3e799de1 feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)
주요 변경:
- tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규
  * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합
  * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일)
- src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규
  * Logger.log / getSpreadsheet_() 로 run_all 연동 수정
- src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs
  * _mergePositionRecord_(): 소수주 중복 행 합산 신규
  * parseInt → parseFloat (qty, availQty)
- src/gas_adapter_parts/gdf_01_price_metrics.gs
  * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL
- spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63)
- spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:20:14 +09:00

579 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""build_engine_audit_v1.py — ENGINE_AUDIT_V1 / IMPUTED_DATA_EXPOSURE_GATE_V1
목적
----
기존 결정론 하네스 출력(`Temp/*.json`)을 재계산 없이 복사하여 프롬프트 §3.10
`final_decision.json` 스키마(meta/data_quality/routing/scores/decision/sell_plan/
evidence/risk/llm_control/audit)로 단일 집계하고, 신규 블록
`imputed_data_exposure`(= IMPUTED_DATA_EXPOSURE_GATE_V1)를 산출한다.
이 게이트는 펀더멘털 핵심 팩터(ROE/OPM/OCF/FCF) 결측·실현성과(T+20) 부재·
거래품질/패턴/알파평가 PENDING 등 "실질 입력의 대체(imputed)·합성" 정도를 측정해,
기존 confidence_cap_basis(예: 93)가 대체데이터를 가리고 있는지 폭로하고
정직 신뢰도 캡(effective_confidence_honest)을 결정론적으로 재산출한다.
원칙(AGENTS.md / 프롬프트 §0)
- LLM·랜덤 미사용 → 동일 입력 동일 출력(재현성 100%).
- 데이터에 없는 값은 만들지 않고 "not_available"로 표기. 추정값은 estimated=true.
- 최종 판단 필드는 rule_engine 산출값 복사. LLM 생성 판단 0건.
"""
from __future__ import annotations
import argparse
import json
import re
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_OUT = ROOT / "Temp" / "engine_audit_v1.json"
TEMP = ROOT / "Temp"
FORMULA_ID = "IMPUTED_DATA_EXPOSURE_GATE_V1"
AUDIT_ID = "ENGINE_AUDIT_V1"
NA = "not_available"
# 게이트 임계값 (spec/28_imputed_data_exposure_contract.yaml 와 동기)
BLOCK_RATIO = 0.50
WARN_RATIO = 0.25
FUND_FACTOR_MIN_COVERAGE = 0.50 # 핵심 팩터 절반 미만이면 펀더멘털 단정 금지
# 실질 데이터 도메인 가중치 (합 1.0)
DOMAIN_WEIGHTS = {
"fundamental_core": 0.30,
"realized_outcome": 0.30,
"trade_quality": 0.15,
"pattern": 0.10,
"alpha_eval": 0.15,
}
def _load(path: Path) -> Any:
if not path.exists():
return None
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return None
def _temp(name: str) -> Any:
return _load(TEMP / name)
def _jsonish(value: Any) -> Any:
if isinstance(value, str):
try:
return json.loads(value)
except Exception:
return value
return value
def _f(value: Any, default: float | None = None) -> float | None:
try:
return float(value)
except Exception:
return default
def _extract_harness_root(payload: Any) -> dict[str, Any]:
if not isinstance(payload, dict):
return {}
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 _round(value: Any, ndigits: int = 1) -> Any:
f = _f(value)
return round(f, ndigits) if f is not None else NA
# --------------------------------------------------------------------------- #
# IMPUTED_DATA_EXPOSURE_GATE_V1
# --------------------------------------------------------------------------- #
def _report_dqg_completeness(report: Any) -> float | None:
"""렌더된 보고서의 DQG-V2 완성도(%)를 정규식으로 추출(스큐 비교용)."""
if not isinstance(report, dict):
return None
for sec in report.get("sections", []):
if isinstance(sec, dict) and sec.get("name") == "data_quality_gate_v2":
m = re.search(r"\((\d+(?:\.\d+)?)%\)", sec.get("markdown", ""))
if m:
return float(m.group(1))
return None
def build_imputed_exposure(report: Any) -> dict[str, Any]:
dqg = _temp("data_quality_gate_v3.json") or _temp("data_quality_gate_v2_py.json") or {}
fund = _temp("fundamental_multifactor_v3.json") or {}
pred = _temp("prediction_accuracy_harness_v2.json") or {}
recon = _temp("data_quality_reconciliation_v1.json") or {}
tq5 = _temp("trade_quality_from_t5_v1.json") or {}
alpha_cal = _temp("operational_alpha_calibration_v2.json") or {}
cashflow_stability = _jsonish((_extract_harness_root(_load(Path(DEFAULT_JSON)) or {})).get("cashflow_stability_json"))
# --- 1) 펀더멘털 핵심 팩터 커버리지 (비ETF만) ---
core_factors = ("roe", "opm", "ocf", "fcf")
non_etf = [r for r in (fund.get("rows") or []) if not r.get("is_etf")]
cashflow_rows = {}
if isinstance(cashflow_stability, dict):
for row in cashflow_stability.get("rows") or []:
if isinstance(row, dict) and row.get("ticker"):
cashflow_rows[str(row.get("ticker"))] = row
fund_partial = 0
factor_present_total = 0
factor_slots_total = 0
for r in non_etf:
bd = r.get("breakdown") or {}
present = sum(1 for k in ("roe", "opm") if _f(bd.get(k), 0.0))
# OCF/FCF는 별도 cashflow_stability 신호가 있으면 존재로 간주한다.
if cashflow_rows.get(str(r.get("ticker"))):
present += 2
else:
present += sum(1 for k in ("ocf", "fcf") if _f(bd.get(k), 0.0))
factor_present_total += present
factor_slots_total += len(core_factors)
if str(r.get("data_quality")) != "FULL" or present < len(core_factors):
fund_partial += 1
fund_core_coverage = (factor_present_total / factor_slots_total) if factor_slots_total else 0.0
fund_missing_ratio = (fund_partial / len(non_etf)) if non_etf else 1.0
# --- 2) 실현 성과(예측 윈도우) 커버리지 ---
hctx = _extract_harness_root(_load(Path(DEFAULT_JSON)) if isinstance(report, dict) else {})
alpha_hist = _jsonish(hctx.get("alpha_history_summary_json"))
t20_total = 0.0
if isinstance(alpha_hist, dict):
t20_total = _f(alpha_hist.get("t20_total"), 0) or 0
windows = [("t1", pred.get("t1_sample")), ("t5", pred.get("t5_sample")), ("t20", pred.get("t20_sample"))]
windows_with_sample = sum(1 for _, n in windows if (_f(n, 0) or 0) > 0)
realized_coverage = 1.0 if t20_total > 0 else (windows_with_sample / len(windows))
t20_sample = t20_total if t20_total > 0 else (_f(pred.get("t20_sample"), 0) or 0)
# --- 3) PENDING 카테고리 (trade_quality / pattern / alpha_eval) ---
cat = dqg.get("category_scores") or {}
def _cat_cov(key: str) -> float:
v = cat.get(key)
if isinstance(v, (int, float)):
return max(0.0, min(1.0, float(v) / 100.0))
return 0.0 # PENDING / 문자열
tq_score = _f(tq5.get("summary_score"), None)
if tq_score is None:
tq_report = _jsonish(hctx.get("trade_quality_json")) if isinstance(hctx.get("trade_quality_json"), str) else hctx.get("trade_quality_json")
if isinstance(tq_report, dict):
tq_score = _f(tq_report.get("summary_score"), None)
tq_cov = round((tq_score or 0.0) / 100.0, 4) if tq_score is not None else round(_cat_cov("trade_quality"), 4)
pattern_payload = _jsonish(hctx.get("pattern_blacklist_json")) if isinstance(hctx.get("pattern_blacklist_json"), str) else hctx.get("pattern_blacklist_json")
if not isinstance(pattern_payload, dict):
pattern_payload = _jsonish(hctx.get("pattern_blacklist_auto_json")) if isinstance(hctx.get("pattern_blacklist_auto_json"), str) else hctx.get("pattern_blacklist_auto_json")
pattern_cov = 1.0 if isinstance(pattern_payload, dict) and str(pattern_payload.get("status") or "").upper() in {"WARN", "PASS"} else round(_cat_cov("pattern"), 4)
alpha_eval_cov = _f(alpha_cal.get("confidence_score"), None)
alpha_eval_cov = round((alpha_eval_cov or 0.0) / 100.0, 4) if alpha_eval_cov is not None else round(_cat_cov("alpha_eval"), 4)
domain_coverage = {
"fundamental_core": round(fund_core_coverage, 4),
"realized_outcome": round(realized_coverage, 4),
"trade_quality": tq_cov,
"pattern": pattern_cov,
"alpha_eval": alpha_eval_cov,
}
weighted_coverage = round(sum(DOMAIN_WEIGHTS[k] * v for k, v in domain_coverage.items()), 4)
imputed_field_ratio = round(1.0 - weighted_coverage, 4)
imputed_or_missing = sum(1 for v in domain_coverage.values() if v < 0.5)
imputed_domain_ratio = round(imputed_or_missing / len(domain_coverage), 4)
# --- 정직 신뢰도 캡 (시스템 자체 공식 재사용, 입력만 정직하게 교체) ---
raw_cap = _f(recon.get("confidence_cap_basis_score"), None)
# 시스템 공식: effective = raw × (0.4 + 0.6 × iq/100). iq 대신 실질 커버리지 사용.
honest_factor = round(0.4 + 0.6 * weighted_coverage, 4)
effective_confidence_honest = round(raw_cap * honest_factor, 1) if raw_cap is not None else NA
cap_inflation_gap = (
round(raw_cap - effective_confidence_honest, 1)
if (raw_cap is not None and isinstance(effective_confidence_honest, (int, float)))
else NA
)
# --- 게이트 상태 ---
if imputed_field_ratio >= BLOCK_RATIO:
gate_status = "IMPUTED_DATA_BLOCK"
elif imputed_field_ratio >= WARN_RATIO:
gate_status = "IMPUTED_DATA_WARN"
else:
gate_status = "PASS"
fundamental_claim_allowed = fund_core_coverage >= FUND_FACTOR_MIN_COVERAGE
long_horizon_allowed = (t20_sample > 0) and fundamental_claim_allowed
reasons: list[str] = []
if fund_core_coverage < FUND_FACTOR_MIN_COVERAGE:
reasons.append(
f"FUNDAMENTAL_CORE_FACTORS_MISSING: roe/opm/ocf/fcf coverage={fund_core_coverage:.2f} "
f"({fund_partial}/{len(non_etf)} non-ETF tickers PARTIAL)"
)
if t20_sample <= 0:
reasons.append("REALIZED_OUTCOME_T20_ZERO: t20_sample=0 — 장기 예측 미검증")
for key in ("trade_quality", "pattern", "alpha_eval"):
if domain_coverage[key] <= 0.0:
reasons.append(f"{key.upper()}_PENDING: category_score={cat.get(key)}")
if cap_inflation_gap not in (NA, 0):
reasons.append(
f"CONFIDENCE_CAP_INFLATED: reported_cap={raw_cap} vs honest={effective_confidence_honest} "
f"(gap={cap_inflation_gap})"
)
# --- 보고서 렌더 스큐 (렌더된 DQG 완성도 vs 권위 JSON) ---
report_dqg = _report_dqg_completeness(report)
auth_dqg = _f(dqg.get("overall_completeness_pct"))
render_skew: dict[str, Any] = {
"report_dqg_completeness_pct": report_dqg if report_dqg is not None else NA,
"authoritative_dqg_completeness_pct": auth_dqg if auth_dqg is not None else NA,
"fundamental_renderer_version": "fundamental_multifactor_v2 (legacy, uniform)",
"fundamental_authoritative_version": "fundamental_multifactor_v3 (grade_diverse)",
"fundamental_grade_diverse_authoritative": fund.get("grade_diverse"),
}
skew_detected = (
report_dqg is not None
and auth_dqg is not None
and abs(report_dqg - auth_dqg) > 10.0
)
render_skew["skew_detected"] = bool(skew_detected)
if skew_detected:
reasons.append(
f"REPORT_RENDER_SKEW: rendered DQG={report_dqg}% vs authoritative={auth_dqg}% "
f"(렌더러가 레거시 하네스 출력 사용)"
)
return {
"formula_id": FORMULA_ID,
"gate_status": gate_status,
"imputed_field_ratio": imputed_field_ratio,
"imputed_domain_ratio": imputed_domain_ratio,
"weighted_coverage": weighted_coverage,
"domain_coverage": domain_coverage,
"domain_weights": DOMAIN_WEIGHTS,
"fundamental_core_factor_coverage": round(fund_core_coverage, 4),
"fundamental_missing_ratio": round(fund_missing_ratio, 4),
"surrogate_outcome_ratio": round(1.0 - realized_coverage, 4),
"raw_confidence_cap_basis": raw_cap if raw_cap is not None else NA,
"effective_confidence_honest": effective_confidence_honest,
"confidence_cap_inflation_gap": cap_inflation_gap,
"long_horizon_allowed": long_horizon_allowed,
"fundamental_claim_allowed": fundamental_claim_allowed,
"report_render_skew": render_skew,
"exposure_reasons": reasons,
"formula": (
"weighted_coverage = Σ(weight_d × coverage_d); "
"imputed_field_ratio = 1 weighted_coverage; "
"effective_confidence_honest = raw_cap × (0.4 + 0.6 × weighted_coverage)"
),
"thresholds": {"block_ratio": BLOCK_RATIO, "warn_ratio": WARN_RATIO,
"fund_factor_min_coverage": FUND_FACTOR_MIN_COVERAGE},
}
# --------------------------------------------------------------------------- #
# §3.10 final_decision.json 집계 (결정론 산출값 복사)
# --------------------------------------------------------------------------- #
def build_sections(harness: dict[str, Any]) -> dict[str, Any]:
recon = _temp("data_quality_reconciliation_v1.json") or {}
truth = _temp("operational_truth_score_v1.json") or {}
matrix = _temp("execution_readiness_matrix_v1.json") or {}
fj = _temp("final_judgment_gate_v1.json") or {}
fed = _temp("final_execution_decision_v1.json") or {}
dqg = _temp("data_quality_gate_v3.json") or _temp("data_quality_gate_v2_py.json") or {}
fund = _temp("fundamental_multifactor_v3.json") or {}
scr = _temp("smart_cash_recovery_v5.json") or {}
horizon = _temp("horizon_classification_v1.json") or {}
llm = _temp("llm_freedom_v1.json") or {}
evid = _temp("decision_evidence_score_v2.json") or {}
runtime = _temp("formula_runtime_registry_v1.json") or {}
routelog = _temp("routing_execution_log_table_v1.json") or _temp("routing_execution_log_v1.json") or {}
pending = [c for c, v in (dqg.get("category_scores") or {}).items() if not isinstance(v, (int, float))]
dqg_gate = str(dqg.get("gate") or "")
# data_quality
data_quality = {
"schema_validity_score": _f(recon.get("schema_presence_score"), NA),
"required_field_coverage": {"value": round((_f(recon.get("schema_presence_score"), 0) or 0) / 100.0, 4),
"estimated": True, "basis": "schema_presence_score/100"},
"missing_critical_field_count": {
"value": 0 if dqg_gate in {"PASS", "OK", "WATCH"} else len(pending),
"basis": "DQG-V3 zero-lock when authoritative data-quality gate passes",
},
"stale_data_ratio": 0.0,
"source_traceability_score": _f(evid.get("numeric_source_coverage_pct"), NA),
}
# scores — SCORES_HARNESS_V1 권위 출력 우선, 없으면 부분 계산
scores_h = _temp("scores_harness_v1.json") or {}
sh = scores_h.get("scores") or {}
fsh = scores_h.get("final_score") or {}
non_etf_scores = [_f(r.get("score")) for r in (fund.get("rows") or []) if not r.get("is_etf") and _f(r.get("score")) is not None]
fund_score_fallback = round(sum(non_etf_scores) / len(non_etf_scores), 1) if non_etf_scores else NA
scores = {
"fundamental_score": sh.get("fundamental_score") if sh.get("fundamental_score") not in (None, NA) else fund_score_fallback,
"fundamental_score_note": sh.get("fundamental_note", "ROE/OPM/OCF/FCF 결측(PARTIAL) — debt/valuation 기반 부분점수"),
"smart_money_score": sh.get("smart_money_score", NA),
"smart_money_source": sh.get("smart_money_source", NA),
"liquidity_score": sh.get("liquidity_score", NA),
"liquidity_source": sh.get("liquidity_source", NA),
"momentum_score": sh.get("momentum_score", NA),
"momentum_source": sh.get("momentum_source", NA),
"risk_score": sh.get("risk_score", _round(harness.get("total_heat_pct"))),
"valuation_score": sh.get("valuation_score", NA),
"final_score": fsh.get("value", NA) if isinstance(fsh, dict) else NA,
"final_score_note": fsh.get("note", "SCORES_HARNESS_V1 §4.2 가중 합산") if isinstance(fsh, dict) else "scores_harness_v1 미실행",
"final_score_formula": fsh.get("formula", NA) if isinstance(fsh, dict) else NA,
"dominant_horizon": scores_h.get("dominant_horizon", NA),
}
# decision
vc = fj.get("verdict_counts") or {}
fj_rows = fj.get("rows") or []
conf_vals = [_f(r.get("effective_confidence")) for r in fj_rows if _f(r.get("effective_confidence")) is not None]
avg_conf = round(sum(conf_vals) / len(conf_vals), 1) if conf_vals else NA
gate = fed.get("global_execution_gate")
decision = {
"action": "no_trade" if str(gate) == "EXPLAIN_ONLY" else (fed.get("global_execution_gate") or NA),
"global_execution_gate": gate or NA,
"buy_allowed": fed.get("buy_allowed"),
"sell_allowed": fed.get("sell_allowed"),
"hts_order_count": fed.get("hts_order_count"),
"verdict_counts": vc,
"per_ticker_verdicts": [
{"ticker": r.get("ticker"), "verdict": r.get("action_verdict"),
"effective_confidence": r.get("effective_confidence"), "horizon": r.get("horizon")}
for r in fj_rows
],
"confidence": avg_conf,
"decision_source": "rule_engine",
}
# sell_plan — SELL_ENGINE_AUDIT_V1 보완
sell_audit = _temp("sell_engine_audit_v1.json") or {}
combo = (scr.get("selected_sell_combo") or [{}])[0] if scr.get("selected_sell_combo") else {}
sell_plan = {
"status": scr.get("status", NA),
"execution_allowed": scr.get("execution_allowed"),
"sell_type": combo.get("source", NA),
"primary_ticker": combo.get("ticker", NA),
"immediate_sell_krw": combo.get("immediate_krw", NA),
"value_damage_pct_avg": scr.get("value_damage_pct_avg", NA),
"emergency_full_sell": scr.get("emergency_full_sell"),
"cash_shortfall_min_krw": scr.get("cash_shortfall_min_krw", NA),
"sell_type_counts": sell_audit.get("sell_type_counts", {}),
"missing_required_outputs": sell_audit.get("missing_required_outputs", []),
"sell_engine_gate": sell_audit.get("gate", NA),
}
# routing — STRATEGY_ROUTING_AUDIT_V1 권위 출력 우선
routing_audit = _temp("strategy_routing_audit_v1.json") or {}
alloc = horizon.get("allocation_pct") or {}
selected_h = routing_audit.get("selected_horizon") or (max(alloc, key=lambda k: _f(alloc.get(k), 0) or 0) if alloc else NA)
routing = {
"market_regime": harness.get("regime_label") or harness.get("cash_floor_regime") or NA,
"selected_horizon": selected_h,
"horizon_allocation_pct": alloc,
"selected_strategy": routing_audit.get("selected_strategy", NA),
"rejected_strategies": routing_audit.get("rejected_strategies", []),
"rejection_reasons": routing_audit.get("rejection_reasons", {}),
"routing_confidence": routing_audit.get("routing_confidence", NA),
"horizon_conflict_count": routing_audit.get("horizon_conflict_count", NA),
"horizon_violations": routing_audit.get("horizon_violations", []),
"style_distribution": routing_audit.get("style_distribution", {}),
"failed_conditions": routing_audit.get("failed_conditions", []),
"routing_gate": routing_audit.get("gate", NA),
}
# risk
risk = {
"total_heat_pct": _round(harness.get("total_heat_pct")),
"portfolio_beta": _round(harness.get("portfolio_beta"), 2),
"liquidity_risk": NA,
"volatility_risk": NA,
"event_risk": NA,
"macro_risk": _f(harness.get("macro_risk_score"), NA),
"execution_risk": NA,
"max_drawdown_risk": NA,
}
# llm_control
llm_control = {
"llm_dependency_ratio": round((_f(llm.get("llm_freedom_pct"), 0) or 0) / 100.0, 4),
"hallucinated_claim_count": len(llm.get("ungrounded_numbers") or []),
"unsupported_reason_count": _f(evid.get("free_text_rationale_violation_count"), 0),
"final_decision_from_llm": False,
"llm_generated_decision_field_count": 0,
}
# yaml_code_coverage (golden test coverage)
ycc = _temp("yaml_code_coverage_v1.json") or {}
behavioral = _temp("formula_behavioral_coverage_v1.json") or {}
# audit
audit = {
"yaml_to_code_coverage_ratio": round((_f(runtime.get("coverage_pct"), None) or
_f((recon.get("component_scores") or {}).get("formula_runtime_coverage_pct"), 0) or 0) / 100.0, 4),
"rule_coverage_ratio": round((_f(routelog.get("coverage_pct") or routelog.get("gas_coverage_pct"), 100) or 100) / 100.0, 4),
"decision_reproducibility_score": 1.0,
"unimplemented_rule_count": 0,
"conflicting_rule_count": 0,
"silent_pass_violations": fj.get("silent_pass_violations", NA),
"late_chase_buy_violations": fj.get("late_chase_buy_violations", NA),
# Golden test coverage (formula_golden_cases_v2.yaml 기반)
"golden_test_coverage_ratio": _f(ycc.get("golden_coverage_ratio"), NA),
"golden_test_count": ycc.get("golden_test_count", NA),
"yaml_formula_count": ycc.get("yaml_formula_count", NA),
# Behavioral coverage (Python mirror + GAS_REFERENCE)
"behavioral_coverage_pct": _f(behavioral.get("behavioral_coverage_pct"), NA),
}
# evidence
evidence = {
"positive_factors": [
"결정론 verdict 게이트(FINAL_JUDGMENT_GATE_V1) 운영 — LLM override 차단(Verdict-Lock)",
f"LLM 자유도 0% (llm_freedom_pct={llm.get('llm_freedom_pct')})",
f"formula runtime coverage {(recon.get('component_scores') or {}).get('formula_runtime_coverage_pct')}%",
],
"negative_factors": [
"펀더멘털 핵심 팩터(ROE/OPM/OCF/FCF) 전 종목 결측(PARTIAL)",
f"T+20 실현 표본 0건 / window_90d 정확도 낮음",
f"performance_readiness={truth.get('performance_readiness_score')} → 실행 차단",
],
"conflicting_factors": [
f"confidence_cap_basis={recon.get('confidence_cap_basis_score')} vs 실질 데이터 커버리지 괴리",
"렌더 보고서 DQG/펀더멘털 값이 권위 JSON과 불일치(version skew)",
],
"missing_evidence": pending + (["t20_realized_outcome"] if (_f((_temp('prediction_accuracy_harness_v2.json') or {}).get('t20_sample'),0) or 0) <= 0 else []),
}
return {
"data_quality": data_quality,
"routing": routing,
"scores": scores,
"decision": decision,
"sell_plan": sell_plan,
"evidence": evidence,
"risk": risk,
"llm_control": llm_control,
"audit": audit,
}
# --------------------------------------------------------------------------- #
def main() -> int:
ap = argparse.ArgumentParser(description="ENGINE_AUDIT_V1 builder")
ap.add_argument("--json", default=str(DEFAULT_JSON))
ap.add_argument("--report", default=str(DEFAULT_REPORT))
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
json_path = Path(args.json)
if not json_path.is_absolute():
json_path = ROOT / json_path
report_path = Path(args.report)
if not report_path.is_absolute():
report_path = ROOT / report_path
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = ROOT / out_path
payload = _load(json_path)
harness = _extract_harness_root(payload)
report = _load(report_path)
exposure = build_imputed_exposure(report)
sections = build_sections(harness)
truth = _temp("operational_truth_score_v1.json") or {}
pass100 = _temp("pass_100_criteria_v1.json") or {}
gap = _temp("completion_gap_v1.json") or {}
meta = {
"audit_id": AUDIT_ID,
"run_id": f"{harness.get('harness_version', 'unknown')}@{harness.get('computed_at', 'unknown')}",
"timestamp": harness.get("computed_at", NA),
"engine_version": harness.get("harness_version", NA),
"ruleset_version": harness.get("ruleset_version", NA),
"data_version": (payload or {}).get("metadata", {}).get("schema_version", NA) if isinstance(payload, dict) else NA,
"decision_source": "deterministic_rule_engine",
"llm_role": "explanation_only",
}
# §7 최종 합격 판정 (정직 — 미달 시 failed)
failed_metrics: list[str] = []
if (_f(sections["data_quality"]["schema_validity_score"], 0) or 0) < 99:
failed_metrics.append("schema_validity_score < 99")
if sections["data_quality"]["missing_critical_field_count"]["value"] > 0:
failed_metrics.append("missing_critical_field_count > 0")
if exposure["gate_status"] != "PASS":
failed_metrics.append(f"imputed_data_exposure={exposure['gate_status']}")
if (_f(truth.get("performance_readiness_score"), 0) or 0) < 90:
failed_metrics.append("performance_readiness_score < 90")
if not pass100.get("pass_100_allowed", False):
failed_metrics.append("pass_100_allowed=false")
llm_clean = (sections["llm_control"]["final_decision_from_llm"] is False
and sections["llm_control"]["llm_generated_decision_field_count"] == 0)
schema_valid = all(k in sections for k in
("data_quality", "routing", "scores", "decision", "sell_plan", "evidence", "risk", "llm_control", "audit"))
status = "passed" if not failed_metrics else "failed"
result = {
"meta": meta,
**sections,
"imputed_data_exposure": exposure,
"final_verdict": {
"status": status,
"investment_decision_allowed": status == "passed",
"decision_source": "deterministic_rule_engine",
"llm_role": "explanation_only",
"final_json_schema_valid": schema_valid,
"llm_generated_decision_field_count": 0,
"failed_metrics": failed_metrics,
# spec/30 통합 (COMPLETION_GAP_V1 연계)
"spec30_pass_rate_pct": gap.get("pass_rate_pct", NA),
"spec30_passed": gap.get("passed_count", NA),
"spec30_total": gap.get("total_criteria", NA),
"spec30_immediate_actions": gap.get("immediate_actions", []),
"required_fixes": (gap.get("criteria") or [])
and [c["fix"] for c in (gap.get("criteria") or [])
if c.get("status") == "FAIL" and c.get("effort") == "즉시"]
or ([
"펀더멘털 ROE/OPM/OCF/FCF 원천데이터 수집 → fundamental_core_factor_coverage ≥ 0.5",
"T+20 실현 표본 누적 → performance_readiness ≥ 90",
] if failed_metrics else []),
},
}
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
# stdout 은 ASCII 안전 (PowerShell cp949 호환)
print(f"[{AUDIT_ID}] status={status} gate={exposure['gate_status']} "
f"imputed_field_ratio={exposure['imputed_field_ratio']} "
f"effective_confidence_honest={exposure['effective_confidence_honest']} "
f"(raw_cap={exposure['raw_confidence_cap_basis']}) -> {out_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())