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,307 @@
#!/usr/bin/env python3
"""
measure_harness_coverage.py
───────────────────────────────────────────────────────────────────────────────
하네스 커버리지 측정기
"YAML 스펙을 작성해도 GAS가 실제로 계산하지 않으면 LLM이 매번 다른 숫자를 만든다."
이 도구는 현재 harness_context에서 GAS가 실제 채운 수치 필드 vs
LLM이 추정해야 하는 공백 필드를 정량 측정한다.
출력:
- 전체 커버리지 % (GAS 산출 / 전체 필수 필드)
- 공식별 커버리지 표
- LLM 자유도 점수 (낮을수록 결정론적)
- 재현성 위험 필드 목록 (LLM이 계산해야 하는 필드 = 랜덤성 원천)
사용법:
python tools/measure_harness_coverage.py [GatherTradingData.json]
python tools/measure_harness_coverage.py [GatherTradingData.json] --strict-100
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# ── 공식별 필수 출력 필드 정의 ──────────────────────────────────────────────
# (field_name, description, data_type)
FORMULA_OUTPUT_FIELDS: dict[str, list[tuple[str, str, str]]] = {
# ── STAGE 0 ──────────────────────────────────────────────────────────────
"HARNESS_DATA_FRESHNESS_GATE_V1": [
("data_freshness_status", "데이터 신선도 상태", "enum"),
],
"INTRADAY_ACTION_MATRIX_V1": [
("intraday_scope", "장중/장전 허용 액션 범위", "enum"),
("intraday_lock", "장중 잠금 여부", "bool"),
],
# ── STAGE 1 ──────────────────────────────────────────────────────────────
"CASH_RATIOS_V1": [
("settlement_cash_d2_krw", "D+2 정산현금(원)", "numeric"),
("settlement_cash_pct", "D+2 현금 비율(%)", "numeric"),
("cash_floor_min_pct", "최소 현금 바닥(%)", "numeric"),
("cash_shortfall_min_krw", "현금 부족분(원)", "numeric"),
],
"TOTAL_HEAT_V1": [
("total_heat_pct", "포트폴리오 총 Heat(%)", "numeric"),
("heat_gate_status", "Heat 게이트 상태", "enum"),
],
# ── STAGE 2 ──────────────────────────────────────────────────────────────
"PROFIT_LOCK_RATCHET_V1": [
("profit_lock_stage", "수익 잠금 단계", "enum"),
("auto_trailing_stop", "ATR 기반 자동 트레일링", "numeric"),
],
"PROFIT_RATCHET_TIERED_V2": [
("auto_trailing_stop_v2", "3RD — APEX_SUPER 래칫", "numeric"),
("ratchet_stage_v2", "래칫 단계 v2", "enum"),
],
# ── STAGE 3 ──────────────────────────────────────────────────────────────
"FLOW_ACCELERATION_V1": [
("flow_acceleration_status", "수급 에너지 소진 상태", "enum"),
],
"DISTRIBUTION_SELL_DETECTOR_V1": [
("distribution_sell_detector_status", "설거지 감지 상태 (6신호)", "enum"),
("signals_count", "트리거된 신호 수", "numeric"),
],
# ── STAGE 4 ──────────────────────────────────────────────────────────────
"BREAKOUT_QUALITY_GATE_V2": [
("breakout_quality_score", "돌파 품질 점수", "numeric"),
],
"ANTI_CHASING_VELOCITY_V1": [
("anti_chasing_verdict", "뒷박 추격 차단 판정", "enum"),
("anti_chasing_velocity_status", "속도 차단 상태", "enum"),
],
"PULLBACK_ENTRY_TRIGGER_V1": [
("pullback_entry_verdict", "눌림목 진입 판정", "enum"),
("pullback_entry_trigger_price", "허용 진입 기준가(원)", "numeric"),
],
# ── STAGE 5 ──────────────────────────────────────────────────────────────
"CASH_RECOVERY_OPTIMIZER_V1": [
("cash_recovery_plan_json", "현금회복 최적 매도조합 JSON", "json"),
],
"SELL_WATERFALL_ENGINE_V1": [
("waterfall_plan_json", "폭포수 매도 계획 JSON", "json"),
],
"SELL_EXECUTION_TIMING_V1": [
("sell_timing_verdict", "매도 실행 타이밍 판정", "enum"),
("sell_execution_window", "실행 허용 시간대", "enum"),
],
"SELL_VALUE_PRESERVATION_TIERED_V2": [
("preservation_verdict", "주식가치 보호 매도 판정", "enum"),
],
# ── STAGE 6 ──────────────────────────────────────────────────────────────
"TICK_NORMALIZER_V1": [
("tick_normalized_price", "호가 정규화 완료 표시", "bool"),
],
"SELL_PRICE_SANITY_V1": [
("sell_price_sanity_status", "매도가 역전/비현실가 검증", "enum"),
],
# ── STAGE 7 ──────────────────────────────────────────────────────────────
"BENCHMARK_RELATIVE_TIMESERIES_V1": [
("brt_verdict", "BRT 상대강도 판정", "enum"),
("brt_rs_slope", "RS 기울기", "numeric"),
],
"RS_VERDICT_V2": [
("rs_verdict", "최종 RS 판정", "enum"),
],
# ── STAGE 8 ──────────────────────────────────────────────────────────────
"SATELLITE_ALPHA_QUALITY_GATE_V1": [
("saqg_verdict", "위성 품질 게이트", "enum"),
],
"SATELLITE_AGGREGATE_PNL_GATE_V1": [
("sapg_verdict", "위성 합산 손익 게이트", "enum"),
],
# ── STAGE 9 ──────────────────────────────────────────────────────────────
"LLM_SERVING_CONSTRAINT_V1": [
("serving_constraint_check", "LLM 제약 검사 결과", "enum"),
],
"DETERMINISTIC_ROUTING_ENGINE_V1": [
("routing_execution_log", "9단계 라우팅 실행 로그", "json"),
],
# ── MONTHLY BATCH ─────────────────────────────────────────────────────────
"TRADE_QUALITY_SCORER_V1": [
("trade_quality_json", "거래 품질 채점 결과 JSON", "json"),
],
"PATTERN_BLACKLIST_AUTO_V1": [
("pattern_blacklist_status", "반복 패턴 블랙리스트 상태", "enum"),
],
# ── 기존 필수 필드 ─────────────────────────────────────────────────────────
"POSITION_SIZE_V1": [
("buy_power_krw", "매수 가용 현금(원)", "numeric"),
("total_asset_krw", "총 자산(원)", "numeric"),
],
"prices_lock": [
("prices_json", "가격 잠금 JSON (stop/tp/current)", "json"),
],
"quantities_lock": [
("sell_quantities_json", "매도 수량 잠금 JSON", "json"),
("buy_qty_inputs_json", "매수 수량 잠금 JSON", "json"),
("order_blueprint_json", "HTS 주문 청사진 JSON", "json"),
],
}
SEP = "=" * 70
SEP2 = "-" * 70
def load_harness_context(json_path: Path) -> dict:
raw = json.loads(json_path.read_text(encoding="utf-8"))
hc = None
try:
hc = raw["data"]["_harness_context"]
except (KeyError, TypeError):
pass
if hc is None:
for key in ["_harness_context", "harness_context"]:
if key in raw and isinstance(raw[key], dict):
hc = raw[key]
break
if hc is None:
print("[ERROR] harness_context를 찾을 수 없음")
sys.exit(1)
return hc
def is_field_present(hc: dict, field: str) -> bool:
val = hc.get(field)
if val is None:
return False
if isinstance(val, str) and val.strip() == "":
return False
return True
def field_is_numeric(hc: dict, field: str) -> bool:
val = hc.get(field)
return isinstance(val, (int, float)) and not isinstance(val, bool)
def compute_coverage(hc: dict) -> dict[str, object]:
total_fields = 0
covered_fields = 0
missing_fields: list[tuple[str, str, str]] = []
covered_list: list[tuple[str, str]] = []
formula_results: list[dict[str, object]] = []
for formula_id, fields in FORMULA_OUTPUT_FIELDS.items():
f_total = len(fields)
f_covered = 0
f_missing: list[str] = []
for field_name, _description, dtype in fields:
total_fields += 1
if is_field_present(hc, field_name):
covered_fields += 1
f_covered += 1
covered_list.append((formula_id, field_name))
else:
f_missing.append(field_name)
missing_fields.append((formula_id, field_name, dtype))
pct = f_covered / f_total * 100 if f_total > 0 else 0
formula_results.append({
"formula_id": formula_id,
"total": f_total,
"covered": f_covered,
"pct": pct,
"missing": f_missing,
})
overall_pct = covered_fields / total_fields * 100 if total_fields > 0 else 0
return {
"total_fields": total_fields,
"covered_fields": covered_fields,
"overall_pct": overall_pct,
"llm_freedom_score": 100 - overall_pct,
"missing_fields": missing_fields,
"covered_list": covered_list,
"formula_results": formula_results,
}
def ensure_utf8_stdio() -> None:
# Windows cp949 터미널 호환
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)
if sys.stderr.encoding and sys.stderr.encoding.lower() not in ("utf-8", "utf8"):
sys.stderr = open(sys.stderr.fileno(), mode="w", encoding="utf-8", buffering=1)
def main() -> int:
ensure_utf8_stdio()
strict_100 = "--strict-100" in sys.argv
argv = [arg for arg in sys.argv[1:] if arg != "--strict-100"]
json_path = Path(argv[0]) if argv else ROOT / "GatherTradingData.json"
if not json_path.exists():
print(f"[ERROR] {json_path} not found")
return 1
hc = load_harness_context(json_path)
coverage = compute_coverage(hc)
print(SEP)
print(" 하네스 커버리지 측정기 — Harness Coverage Report")
print(f" 파일: {json_path.name}")
print(f" harness_version: {hc.get('harness_version', '(missing)')}")
print(f" computed_at: {hc.get('computed_at', '(missing)')}")
print(SEP)
# ── 공식별 커버리지 표 ──────────────────────────────────────────────────
print("\n[공식별 커버리지]")
print(f" {'공식 ID':<45} {'커버':<6} {'전체':<6} {'%':<7} 상태")
print(" " + "-" * 65)
for r in coverage["formula_results"]:
bar = "" * r["covered"] + "" * (r["total"] - r["covered"])
status = "✔ FULL" if r["pct"] == 100 else ("△ PARTIAL" if r["pct"] > 0 else "✗ MISSING")
print(f" {r['formula_id']:<45} {r['covered']:<6} {r['total']:<6} {r['pct']:>5.0f}% {status} {bar}")
# ── 전체 커버리지 요약 ──────────────────────────────────────────────────
overall_pct = coverage["overall_pct"]
llm_freedom_score = coverage["llm_freedom_score"] # 높을수록 LLM이 더 많이 추정
print()
print(SEP)
print(f" 전체 커버리지 : {coverage['covered_fields']}/{coverage['total_fields']} 필드 = {overall_pct:.1f}%")
print(f" LLM 자유도 점수 : {llm_freedom_score:.1f}% ← 낮을수록 결정론적 (목표: 0%)")
print(SEP)
if llm_freedom_score == 0:
print("\n ✔ 완전 결정론적 — LLM이 임의 계산해야 할 필드 없음")
else:
# ── 재현성 위험 필드 목록 ─────────────────────────────────────────
missing_fields = coverage["missing_fields"]
print(f"\n[재현성 위험 필드 — GAS 미계산 = LLM 추정 = 랜덤성 원천] ({len(missing_fields)}개)")
print(" 이 필드들은 LLM 호출마다 다른 값이 나올 수 있습니다.\n")
print(f" {'공식 ID':<45} {'필드명':<40} 타입")
print(" " + "-" * 95)
for formula_id, field_name, dtype in missing_fields:
print(f" {formula_id:<45} {field_name:<40} {dtype}")
# ── 수치 필드 실제 값 확인 (GAS 계산 완료된 필드) ──────────────────────
covered_list = coverage["covered_list"]
formula_results = coverage["formula_results"]
print(f"\n[GAS 계산 완료 수치 필드] ({len(covered_list)}개)")
numeric_present = [
(fid, fn, hc[fn])
for fid, fn in covered_list
if field_is_numeric(hc, fn)
]
for fid, fn, val in numeric_present[:20]:
print(f" {fn:<45} = {val:>15,.0f}" if isinstance(val, (int, float)) else f" {fn:<45} = {val}")
# ── GAS 구현 우선순위 권고 ──────────────────────────────────────────────
print(f"\n[GAS 구현 우선순위 — 커버리지 0% 공식부터]")
zero_coverage = [r for r in formula_results if r["pct"] == 0]
for r in zero_coverage:
print(f" !!! {r['formula_id']} — 출력 필드 {r['total']}개 전부 미계산")
print()
threshold = 100.0 if strict_100 else 80.0
return 0 if overall_pct >= threshold else 1
if __name__ == "__main__":
raise SystemExit(main())