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>
156 lines
6.0 KiB
Python
156 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from hashlib import sha256
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_JSON = ROOT / "GatherTradingData.json"
|
|
DEFAULT_OUT = ROOT / "Temp" / "cash_recovery_optimizer_v4.json"
|
|
FORMULA_ID = "CASH_RECOVERY_OPTIMIZER_V4"
|
|
|
|
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
|
|
sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1)
|
|
|
|
|
|
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 _extract_harness_root(payload: dict[str, Any]) -> dict[str, Any]:
|
|
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 _canonical(obj: Any) -> str:
|
|
return json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
|
|
|
|
def _hash_text(text: str) -> str:
|
|
return sha256(text.encode("utf-8")).hexdigest()[:16]
|
|
|
|
|
|
def _rows(v: Any) -> list[dict[str, Any]]:
|
|
if isinstance(v, list):
|
|
return [x for x in v if isinstance(x, dict)]
|
|
if isinstance(v, str) and v.strip():
|
|
try:
|
|
return _rows(json.loads(v))
|
|
except Exception:
|
|
return []
|
|
if isinstance(v, dict):
|
|
for key in ("rows", "data", "tickers"):
|
|
candidate = v.get(key)
|
|
if isinstance(candidate, list):
|
|
return [x for x in candidate if isinstance(x, dict)]
|
|
return []
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Build deterministic cash recovery optimizer v4.")
|
|
ap.add_argument("--json", default=str(DEFAULT_JSON))
|
|
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
|
args = ap.parse_args()
|
|
|
|
json_path = Path(args.json)
|
|
out_path = Path(args.out)
|
|
if not json_path.is_absolute():
|
|
json_path = ROOT / json_path
|
|
if not out_path.is_absolute():
|
|
out_path = ROOT / out_path
|
|
|
|
payload = _load(json_path)
|
|
hctx = _extract_harness_root(payload)
|
|
|
|
scr7_path = ROOT / "Temp" / "smart_cash_recovery_v7_authoritative.json"
|
|
scr6_path = ROOT / "Temp" / "smart_cash_recovery_v6.json"
|
|
sell_audit_path = ROOT / "Temp" / "sell_engine_audit_v1.json"
|
|
scr7 = _load(scr7_path)
|
|
scr6 = _load(scr6_path)
|
|
sell_audit = _load(sell_audit_path)
|
|
|
|
selected_combo = _rows(scr7.get("selected_sell_combo")) or _rows(scr6.get("selected_sell_combo"))
|
|
selected_sell_combo_source = "SMART_CASH_RECOVERY_V7_AUTH" if _rows(scr7.get("selected_sell_combo")) else "SMART_CASH_RECOVERY_V6"
|
|
# v7 authoritative artifact already contains the K2 50/50 redesign with raw damage <= 10.
|
|
value_damage_pct_avg = (
|
|
scr7.get("optimized_value_damage_pct_avg")
|
|
if scr7.get("optimized_value_damage_pct_avg") is not None
|
|
else scr7.get("raw_value_damage_pct_avg")
|
|
if scr7.get("raw_value_damage_pct_avg") is not None
|
|
else scr6.get("value_damage_pct_avg")
|
|
)
|
|
cash_shortfall_min_krw = scr7.get("cash_shortfall_min_krw") or scr6.get("cash_shortfall_min_krw")
|
|
cash_shortfall_covered = bool(scr7.get("cash_shortfall_covered") if scr7 else scr6.get("cash_shortfall_covered"))
|
|
execution_allowed = bool(scr7.get("execution_allowed") if scr7 else scr6.get("execution_allowed"))
|
|
raw_damage_pct = (
|
|
float(selected_combo[0].get("raw_value_damage_pct") or selected_combo[0].get("adjusted_value_damage_pct") or 0.0)
|
|
if selected_combo
|
|
else float(scr6.get("value_damage_pct_avg") or 0.0)
|
|
)
|
|
adjusted_damage_pct = (
|
|
float(selected_combo[0].get("adjusted_value_damage_pct") or selected_combo[0].get("raw_value_damage_pct") or 0.0)
|
|
if selected_combo
|
|
else raw_damage_pct
|
|
)
|
|
|
|
sell_authority_conflict_count = 0
|
|
if isinstance(sell_audit.get("scr_plan"), dict):
|
|
audit_combo_count = int(sell_audit["scr_plan"].get("combo_count") or 0)
|
|
if audit_combo_count != len(selected_combo):
|
|
sell_authority_conflict_count += 1
|
|
if any(not isinstance(row, dict) for row in selected_combo):
|
|
sell_authority_conflict_count += 1
|
|
|
|
result = {
|
|
"formula_id": FORMULA_ID,
|
|
"status": "PASS" if sell_authority_conflict_count == 0 else "WARN",
|
|
"execution_allowed": execution_allowed,
|
|
"selected_sell_combo_source": selected_sell_combo_source,
|
|
"sell_authority_conflict_count": sell_authority_conflict_count,
|
|
"selected_sell_combo": selected_combo,
|
|
"cash_shortfall_min_krw": cash_shortfall_min_krw,
|
|
"cash_shortfall_covered": cash_shortfall_covered,
|
|
"value_damage_pct_avg": value_damage_pct_avg,
|
|
"value_damage_raw_pct": round(raw_damage_pct, 2),
|
|
"value_damage_adjusted_pct": round(adjusted_damage_pct, 2),
|
|
"value_damage_pct_avg_max": 10.0,
|
|
"sell_engine_audit_gate": sell_audit.get("gate", "MISSING"),
|
|
"optimizer_scr_divergence_count": sell_authority_conflict_count,
|
|
"source": {
|
|
"source_json": str(json_path),
|
|
"smart_cash_recovery_v7_authoritative_json": str(scr7_path),
|
|
"smart_cash_recovery_v6_json": str(scr6_path),
|
|
"sell_engine_audit_v1_json": str(sell_audit_path),
|
|
"generated_by_llm": False,
|
|
},
|
|
"input_hash": _hash_text(_canonical(payload) + _canonical(scr6) + _canonical(sell_audit)),
|
|
}
|
|
|
|
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())
|