ee3e799de1
주요 변경: - 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>
136 lines
8.1 KiB
Python
136 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_TRUTH = ROOT / "Temp" / "operational_truth_score_v1.json"
|
|
DEFAULT_DECISION = ROOT / "Temp" / "final_execution_decision_v1.json"
|
|
DEFAULT_MATRIX = ROOT / "Temp" / "execution_readiness_matrix_v1.json"
|
|
DEFAULT_DQ = ROOT / "Temp" / "data_quality_reconciliation_v1.json"
|
|
DEFAULT_SCR = ROOT / "Temp" / "smart_cash_recovery_v5.json"
|
|
DEFAULT_HARDENING = ROOT / "Temp" / "strategy_hardening_harness_v2.json"
|
|
DEFAULT_OUT = 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 _as_float(value: Any, default: float = 0.0) -> float:
|
|
try:
|
|
if value is None or value == "":
|
|
return default
|
|
return float(value)
|
|
except Exception:
|
|
return default
|
|
|
|
|
|
def _criterion(criterion_id: str, actual: Any, target: str, passed: bool, source_json: str, formula_id: str, remediation: str) -> dict[str, Any]:
|
|
return {
|
|
"criterion_id": criterion_id,
|
|
"actual": actual,
|
|
"target": target,
|
|
"passed": bool(passed),
|
|
"source_json": source_json,
|
|
"formula_id": formula_id,
|
|
"remediation": "NONE" if passed else remediation,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Build PASS_100_CRITERIA_V1.")
|
|
ap.add_argument("--truth", default=str(DEFAULT_TRUTH))
|
|
ap.add_argument("--decision", default=str(DEFAULT_DECISION))
|
|
ap.add_argument("--matrix", default=str(DEFAULT_MATRIX))
|
|
ap.add_argument("--dq", default=str(DEFAULT_DQ))
|
|
ap.add_argument("--scr", default=str(DEFAULT_SCR))
|
|
ap.add_argument("--hardening", default=str(DEFAULT_HARDENING))
|
|
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
|
|
|
|
truth = _load(_rp(args.truth))
|
|
decision = _load(_rp(args.decision))
|
|
matrix = _load(_rp(args.matrix))
|
|
dq = _load(_rp(args.dq))
|
|
scr = _load(_rp(args.scr))
|
|
hardening = _load(_rp(args.hardening))
|
|
|
|
domain_scores = hardening.get("domain_scores") if isinstance(hardening.get("domain_scores"), dict) else {}
|
|
meta_scores = hardening.get("meta_scores") if isinstance(hardening.get("meta_scores"), dict) else {}
|
|
basis = truth.get("metric_basis") if isinstance(truth.get("metric_basis"), dict) else {}
|
|
decision_basis = decision.get("decision_basis") if isinstance(decision.get("decision_basis"), dict) else {}
|
|
|
|
schema_score = _as_float(dq.get("schema_presence_score", basis.get("schema_presence_score")))
|
|
formula_coverage = _as_float(domain_scores.get("formula_runtime_coverage_pct", basis.get("formula_runtime_coverage_pct", 100.0)))
|
|
cap_basis = _as_float(dq.get("confidence_cap_basis_score", basis.get("confidence_cap_basis_score")))
|
|
truth_score = _as_float(truth.get("score_0_100"))
|
|
execution_score = _as_float(truth.get("execution_truth_score"))
|
|
performance_score = _as_float(truth.get("performance_readiness_score"))
|
|
scr_damage = _as_float(scr.get("value_damage_pct_avg"))
|
|
hts_count = int(_as_float(decision.get("hts_order_count")))
|
|
export_status = str(decision_basis.get("export_status") or basis.get("export_status") or "UNKNOWN")
|
|
export_allowed = decision_basis.get("export_allowed")
|
|
readiness_gate = str(meta_scores.get("readiness_gate") or decision_basis.get("readiness_gate") or "MISSING")
|
|
matrix_gate = str(matrix.get("gate") or "MISSING")
|
|
matrix_min = _as_float(matrix.get("min_axis_score"))
|
|
|
|
criteria = [
|
|
_criterion("SCHEMA_PRESENCE_100", schema_score, "== 100", schema_score == 100.0, "data_quality_reconciliation_v1.json", "DATA_QUALITY_RECONCILIATION_V1", "restore missing required JSON collections/fields"),
|
|
_criterion("FORMULA_RUNTIME_COVERAGE_100", formula_coverage, "== 100", formula_coverage == 100.0, "strategy_hardening_harness_v2.json", "STRATEGY_HARDENING_HARNESS_V2", "map every YAML formula to runtime code and validator"),
|
|
_criterion("CONFIDENCE_CAP_BASIS_GE_90", cap_basis, ">= 90", cap_basis >= 90.0, "data_quality_reconciliation_v1.json", "DATA_QUALITY_RECONCILIATION_V1", "fill actual investment-quality evidence; do not use schema score as substitute"),
|
|
_criterion("OPERATIONAL_TRUTH_SCORE_100", truth_score, "== 100 and gate PASS_100", truth_score == 100.0 and str(truth.get("gate") or "") == "PASS_100", "operational_truth_score_v1.json", "OPERATIONAL_TRUTH_SCORE_V1", "remove all truth blockers and rerun truth score"),
|
|
_criterion("EXECUTION_TRUTH_SCORE_100", execution_score, "== 100", execution_score == 100.0, "operational_truth_score_v1.json", "OPERATIONAL_TRUTH_SCORE_V1", "resolve export/order validation blockers"),
|
|
_criterion("PERFORMANCE_READINESS_GE_90", performance_score, ">= 90 and readiness_gate PERFORMANCE_READY", performance_score >= 90.0 and readiness_gate == "PERFORMANCE_READY", "operational_truth_score_v1.json", "OPERATIONAL_TRUTH_SCORE_V1", "accumulate operating T+20 samples or complete performance replay audit"),
|
|
_criterion("SMART_CASH_VALUE_DAMAGE_LE_10", scr_damage, "<= 10", scr_damage <= 10.0, "smart_cash_recovery_v5.json", "SMART_CASH_RECOVERY_V5", "rebuild rebound-preserving sell plan with value damage <= 10%"),
|
|
_criterion("SMART_CASH_EXECUTION_ALLOWED", scr.get("execution_allowed"), "is true and status PASS", bool(scr.get("execution_allowed")) and str(scr.get("status") or "") == "PASS", "smart_cash_recovery_v5.json", "SMART_CASH_RECOVERY_V5", "clear smart cash recovery hard blocks"),
|
|
_criterion("EXPORT_GATE_READY", {"status": export_status, "allowed": export_allowed}, "EXPORT_READY and allowed true", export_status == "EXPORT_READY" and export_allowed is True, "final_execution_decision_v1.json", "FINAL_EXECUTION_DECISION_V1", "rerun GAS export until export_gate_json.hts_entry_allowed=true"),
|
|
_criterion("FINAL_EXECUTION_HTS_READY", decision.get("global_execution_gate"), "HTS_READY", str(decision.get("global_execution_gate") or "") == "HTS_READY", "final_execution_decision_v1.json", "FINAL_EXECUTION_DECISION_V1", "remove truth/export/readiness blockers before HTS order generation"),
|
|
_criterion("HTS_ORDER_COUNT_GT_0", hts_count, "> 0", hts_count > 0, "final_execution_decision_v1.json", "FINAL_EXECUTION_DECISION_V1", "order_blueprint_json must contain PASS validation rows"),
|
|
_criterion("EXECUTION_READINESS_MATRIX_PASS_100", {"gate": matrix_gate, "min_axis_score": matrix_min}, "gate PASS_100 and min_axis_score 100", matrix_gate == "PASS_100" and matrix_min == 100.0, "execution_readiness_matrix_v1.json", "EXECUTION_READINESS_MATRIX_V1", "raise every readiness axis to 100 or keep EXPLAIN_ONLY"),
|
|
]
|
|
|
|
failed = [row for row in criteria if not row["passed"]]
|
|
score = round((len(criteria) - len(failed)) / len(criteria) * 100.0, 2) if criteria else 0.0
|
|
pass_allowed = len(failed) == 0
|
|
gate = "PASS_100" if pass_allowed else "BLOCK_EXECUTION"
|
|
|
|
result = {
|
|
"formula_id": "PASS_100_CRITERIA_V1",
|
|
"gate": gate,
|
|
"pass_100_allowed": pass_allowed,
|
|
"score_0_100": score,
|
|
"passed_count": len(criteria) - len(failed),
|
|
"failed_count": len(failed),
|
|
"failed_criteria": [str(row["criterion_id"]) for row in failed],
|
|
"criteria": criteria,
|
|
"targets": {
|
|
"completion_definition": "PASS_100 only when every criterion passed",
|
|
"hts_execution_definition": "HTS execution remains forbidden unless FINAL_EXECUTION_HTS_READY and HTS_ORDER_COUNT_GT_0 pass",
|
|
"llm_role": "copy criteria and render ledgers only; no ad-hoc numeric overrides",
|
|
},
|
|
}
|
|
|
|
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
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|