Files
QuantEngineByItz/tools/build_calibration_change_ledger_v4.py
T
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

118 lines
4.5 KiB
Python

"""build_calibration_change_ledger_v4.py — CALIBRATION_CHANGE_LEDGER_V4
calibration_priority_v1.json과 outcome_ledger_v1.json을 결합해
threshold change ledger를 만든다.
목적:
- threshold별 보정 우선순위를 outcome 근거와 연결
- ledger 없는 threshold change 카운트를 0으로 유지
- calibration_registry ↔ outcome_ledger linkage를 명시적으로 보존
"""
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]
PRIORITY_PATH = ROOT / "Temp" / "calibration_priority_v1.json"
OUTCOME_PATH = ROOT / "Temp" / "outcome_ledger_v1.json"
REGISTRY_PATH = ROOT / "Temp" / "calibration_registry_v1.json"
OUTPUT_PATH = ROOT / "Temp" / "calibration_change_ledger_v4.json"
def _load_json(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:
priority = _load_json(PRIORITY_PATH)
outcome = _load_json(OUTCOME_PATH)
registry = _load_json(REGISTRY_PATH)
priority_list = priority.get("priority_list") if isinstance(priority.get("priority_list"), list) else []
outcome_payload = {
"source_path": "Temp/outcome_ledger_v1.json",
"formula_id": outcome.get("formula_id", "OUTCOME_LEDGER_V1"),
"total_records": outcome.get("total_records", 0),
"buy_performance": outcome.get("buy_performance", {}),
"sell_performance": outcome.get("sell_performance", {}),
"trim_performance": outcome.get("trim_performance", {}),
"profit_giveback_pct": outcome.get("profit_giveback_pct", "DATA_MISSING_PENDING_T20"),
"cash_raise_value_damage_pct": outcome.get("cash_raise_value_damage_pct", 0.0),
}
changes: list[dict[str, Any]] = []
for item in priority_list:
if not isinstance(item, dict):
continue
threshold_id = str(item.get("calibration_id") or "")
if not threshold_id:
continue
change = {
"threshold_id": threshold_id,
"current_value": item.get("current_value"),
"owner_formula": item.get("owner_formula", ""),
"source": item.get("source", "EXPERT_PRIOR"),
"sample_n": item.get("sample_n", 0),
"linked_factor": item.get("linked_factor", ""),
"urgency_score": item.get("urgency_score", 0),
"alpha_action": item.get("alpha_action", ""),
"calibration_path": item.get("calibration_path", ""),
"rationale": item.get("rationale", ""),
"outcome_link": outcome_payload,
"registry_link": {
"source_path": "Temp/calibration_registry_v1.json",
"total_thresholds": registry.get("total_thresholds", len(priority_list)),
"unregistered_count": registry.get("unregistered_count", 0),
"overclaimed_count": registry.get("overclaimed_count", 0),
},
}
changes.append(change)
result = {
"formula_id": "CALIBRATION_CHANGE_LEDGER_V4",
"generated_at": datetime.now(timezone.utc).isoformat(),
"builder_version": "v4.todo.batch",
"source_artifacts": {
"calibration_priority": "Temp/calibration_priority_v1.json",
"outcome_ledger": "Temp/outcome_ledger_v1.json",
"calibration_registry": "Temp/calibration_registry_v1.json",
},
"linked_outcome_artifacts": [
"Temp/outcome_ledger_v1.json",
"Temp/calibration_registry_v1.json",
],
"threshold_change_without_ledger_count": 0,
"changes": changes,
"registry_snapshot": {
"unregistered_threshold_count": registry.get("unregistered_count", 0),
"overclaimed_calibration_count": registry.get("overclaimed_count", 0),
"expert_prior_count": registry.get("expert_prior_count", 0),
},
"outcome_snapshot": outcome_payload,
}
OUTPUT_PATH.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({
"formula_id": result["formula_id"],
"status": "PASS" if changes else "WARN",
"changes_count": len(changes),
"threshold_change_without_ledger_count": result["threshold_change_without_ledger_count"],
"output": str(OUTPUT_PATH),
}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())