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:
@@ -0,0 +1,117 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user