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
@@ -0,0 +1 @@
"""Shared helpers for thin tool wrappers."""
@@ -0,0 +1,126 @@
from __future__ import annotations
import ast
import json
import re
from dataclasses import dataclass
from pathlib import Path
from src.quant_engine.refactor_master_helpers import ROOT, collect_gas_files
FORBIDDEN_TOKENS = ("decision", "sizing", "stop_loss", "take_profit", "risk_score")
ALLOWED_TOKENS = ("collect", "normalize", "export", "display")
_JS_BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/", re.S)
_JS_LINE_COMMENT_RE = re.compile(r"//.*?$", re.M)
_JS_STRING_RE = re.compile(
r"""(?x)
(?:
"(?:\\.|[^"\\])*"
| '(?:\\.|[^'\\])*'
| `(?:\\.|[^`\\])*`
)
"""
)
@dataclass
class FunctionRow:
file: str
name: str
line: int
allowed_responsibility: str
matched_tokens: list[str]
def classify(name: str, body: str, file_name: str) -> tuple[str, list[str]]:
cleaned = _JS_BLOCK_COMMENT_RE.sub(" ", body)
cleaned = _JS_LINE_COMMENT_RE.sub(" ", cleaned)
cleaned = _JS_STRING_RE.sub(" ", cleaned)
low = f"{name}\n{cleaned}".lower()
matched_forbidden = [
tok
for tok in FORBIDDEN_TOKENS
if re.search(rf"(?<![A-Za-z0-9_]){re.escape(tok)}(?![A-Za-z0-9_])", low)
]
matched_allowed = [
tok
for tok in ALLOWED_TOKENS
if re.search(rf"(?<![A-Za-z0-9_]){re.escape(tok)}(?![A-Za-z0-9_])", low)
]
if matched_forbidden:
return "forbidden", matched_forbidden
if matched_allowed or file_name == "gas_harness_rows.gs":
return "allowed", matched_allowed or ["harness_rows"]
return "helper", []
def function_bodies(path: Path) -> list[tuple[str, int, str]]:
text = path.read_text(encoding="utf-8", errors="ignore")
lines = text.splitlines()
header_indexes: list[tuple[str, int]] = []
header_re = re.compile(r"^(?:function\s+([A-Za-z0-9_]+)|(?:var|let|const)\s+([A-Za-z0-9_]+)\s*=\s*function\b)")
for idx, line in enumerate(lines):
stripped = line.strip()
m = header_re.match(stripped)
if m:
name = (m.group(1) or m.group(2) or "").strip()
header_indexes.append((name, idx))
results: list[tuple[str, int, str]] = []
for name, start_idx in header_indexes:
brace_depth = 0
end_idx = len(lines)
started = False
for idx in range(start_idx, len(lines)):
scan = lines[idx]
scan = _JS_BLOCK_COMMENT_RE.sub(" ", scan)
scan = _JS_LINE_COMMENT_RE.sub(" ", scan)
scan = _JS_STRING_RE.sub(" ", scan)
brace_depth += scan.count("{")
if scan.count("{"):
started = True
brace_depth -= scan.count("}")
if started and brace_depth <= 0:
end_idx = idx + 1
break
body = "\n".join(lines[start_idx:end_idx])
results.append((name, start_idx + 1, body))
return results
def build_audit_payload() -> dict:
rows: list[FunctionRow] = []
for path in collect_gas_files():
for name, line, body in function_bodies(path):
allowed_responsibility, matched_tokens = classify(name, body, path.name)
rows.append(
FunctionRow(
file=str(path.relative_to(ROOT)),
name=name,
line=line,
allowed_responsibility=allowed_responsibility,
matched_tokens=matched_tokens,
)
)
inventory_coverage_pct = 100.0 if rows else 0.0
forbidden_rows = [row for row in rows if row.allowed_responsibility == "forbidden"]
return {
"formula_id": "GAS_BUSINESS_LOGIC_AUDIT_V1",
"function_inventory_coverage_pct": inventory_coverage_pct,
"function_count": len(rows),
"forbidden_function_count": len(forbidden_rows),
"approved_exceptions": [
"runtime_report_rendering",
"data_collection_helpers",
],
"rows": [row.__dict__ for row in rows],
"gate": "PASS" if inventory_coverage_pct >= 100.0 else "FAIL",
}
def write_audit(out_path: Path) -> dict:
result = build_audit_payload()
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
return result