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:
2026-06-13 13:20:14 +09:00
commit ee3e799de1
1474 changed files with 176087 additions and 0 deletions
+178
View File
@@ -0,0 +1,178 @@
from __future__ import annotations
import argparse
import json
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" / "execution_method_ladder_v1.json"
DEFAULT_TIMING = ROOT / "Temp" / "sell_execution_timing_lock_v2.json"
DEFAULT_WATERFALL = ROOT / "Temp" / "sell_waterfall_engine_v2.json"
DEFAULT_SCR = ROOT / "Temp" / "smart_cash_recovery_v7.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 _hash_text(text: str) -> str:
return sha256(text.encode("utf-8")).hexdigest()[:16]
def _rows(value: Any) -> list[dict[str, Any]]:
if isinstance(value, list):
return [x for x in value if isinstance(x, dict)]
return []
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 _method_row(method: str, order_type: str, split_count: int, trigger_rule: str, emergency_full_sell: bool) -> dict[str, Any]:
return {
"method": method,
"order_type": order_type,
"split_count": split_count,
"trigger_rule": trigger_rule,
"emergency_full_sell": emergency_full_sell,
"market_order_default": order_type.upper() == "MARKET",
}
def main() -> int:
ap = argparse.ArgumentParser(description="Build execution method ladder contract.")
ap.add_argument("--json", default=str(DEFAULT_JSON))
ap.add_argument("--out", default=str(DEFAULT_OUT))
ap.add_argument("--timing", default=str(DEFAULT_TIMING))
ap.add_argument("--waterfall", default=str(DEFAULT_WATERFALL))
ap.add_argument("--scr", default=str(DEFAULT_SCR))
args = ap.parse_args()
def _rp(path_str: str) -> Path:
path = Path(path_str)
return path if path.is_absolute() else ROOT / path
json_path = _rp(args.json)
timing_path = _rp(args.timing)
waterfall_path = _rp(args.waterfall)
scr_path = _rp(args.scr)
out_path = _rp(args.out)
payload = _load(json_path)
hctx = _extract_harness_root(payload)
timing = _load(timing_path)
waterfall = _load(waterfall_path)
scr = _load(scr_path)
sell_timing_verdict = str(timing.get("sell_timing_verdict") or hctx.get("sell_timing_verdict") or "DATA_MISSING")
waterfall_gate = str(waterfall.get("gate") or "MISSING")
scr_gate = str(scr.get("status") or "MISSING")
methods = [
_method_row(
"NORMAL_LIQUIDITY",
"LIMIT_NEAR_BID_OR_MID",
3,
"avg_trade_value_20d_m >= 50 and not emergency_full_sell",
False,
),
_method_row(
"HIGH_LIQUIDITY_BREACH",
"TWAP_5_SPLIT",
5,
"avg_trade_value_20d_m >= 500 and stop_breach_gate == BREACH",
False,
),
_method_row(
"OVERSOLD_REBOUND",
"K2_50_50",
2,
"execution_style == OVERSOLD_REBOUND_SELL and rebound_trigger_price defined",
False,
),
_method_row(
"EMERGENCY",
"EXIT_100",
1,
"emergency_full_sell == true",
True,
),
]
market_order_default_count = sum(1 for row in methods if row["market_order_default"])
emergency_full_sell_without_flag_count = sum(
1 for row in methods if row["method"] == "EMERGENCY" and row["emergency_full_sell"] is not True
)
gate = "PASS"
reasons: list[str] = []
if sell_timing_verdict == "DATA_MISSING":
gate = "WARN"
reasons.append("SELL_TIMING_DATA_MISSING")
if waterfall_gate not in {"PASS", "WARN"}:
gate = "FAIL"
reasons.append(f"WATERFALL_GATE={waterfall_gate}")
if scr_gate not in {"PASS", "WARN"}:
gate = "FAIL"
reasons.append(f"SMART_CASH_RECOVERY_GATE={scr_gate}")
if market_order_default_count != 0:
gate = "FAIL"
reasons.append("MARKET_ORDER_DEFAULT_NOT_ZERO")
if emergency_full_sell_without_flag_count != 0:
gate = "FAIL"
reasons.append("EMERGENCY_FULL_SELL_FLAG_MISSING")
result = {
"formula_id": "EXECUTION_METHOD_LADDER_V1",
"gate": gate,
"sell_timing_verdict": sell_timing_verdict,
"waterfall_gate": waterfall_gate,
"smart_cash_recovery_gate": scr_gate,
"market_order_default_count": market_order_default_count,
"emergency_full_sell_without_flag_count": emergency_full_sell_without_flag_count,
"methods": methods,
"source": {
"json_path": str(json_path),
"sell_execution_timing_lock_v2": str(timing_path),
"sell_waterfall_engine_v2": str(waterfall_path),
"smart_cash_recovery_v7": str(scr_path),
"generated_by_llm": False,
},
"input_hash": _hash_text(json.dumps(payload, ensure_ascii=False, sort_keys=True)),
"validation": {
"all_methods_defined": len(methods) == 4,
"no_market_order_default": market_order_default_count == 0,
"emergency_flag_locked": emergency_full_sell_without_flag_count == 0,
"reasons": reasons,
},
}
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())