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>
118 lines
4.5 KiB
Python
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())
|