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>
This commit is contained in:
2026-06-13 13:20:14 +09:00
commit ee3e799de1
1474 changed files with 176087 additions and 0 deletions
+578
View File
@@ -0,0 +1,578 @@
"""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())