Files
QuantEngineByItz/tools/build_rebound_sell_efficiency_v1.py
kjh2064 ee3e799de1 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>
2026-06-13 13:20:14 +09:00

152 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_JSON = ROOT / "GatherTradingData.json"
DEFAULT_OUT = ROOT / "Temp" / "rebound_sell_efficiency_v1.json"
def _load(path: Path) -> dict[str, Any]:
data = json.loads(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
def _parse_rows(value: Any) -> list[dict[str, Any]]:
if isinstance(value, list):
return [x for x in value if isinstance(x, dict)]
if isinstance(value, str):
try:
return _parse_rows(json.loads(value))
except Exception:
return []
return []
def main() -> int:
ap = argparse.ArgumentParser()
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)
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
h = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else (payload.get("hApex") or {})
scrs = h.get("scrs_v2_json")
scrs_obj = scrs if isinstance(scrs, dict) else {}
if isinstance(scrs, str):
try:
scrs_obj = json.loads(scrs)
except Exception:
scrs_obj = {}
combo = _parse_rows(scrs_obj.get("selected_combo"))
total_immediate = float(scrs_obj.get("total_immediate_sell_krw") or 0.0)
rebound_gain = float(scrs_obj.get("expected_rebound_gain_krw") or 0.0)
avg_damage = float(scrs_obj.get("value_damage_pct_avg") or 0.0)
with_rebound = [r for r in combo if float(r.get("rebound_wait_qty") or 0) > 0]
immediate_only = [r for r in combo if float(r.get("rebound_wait_qty") or 0) <= 0]
# [Work 9] 공식 개선: 반등 커버리지(모든 종목이 50/50 분할됐는지)를 핵심 보상으로
# 구 공식: 60 + gain_ratio*400 - damage*0.8
# 문제: 반등대기 커버리지(10/10=100%)가 점수에 미반영
# 신 공식: base(50) + coverage_bonus(30) + gain_bonus(최대30) - damage_penalty
# - coverage_bonus = rebound_wait_count/combo_count * 30 (최대30pt)
# - gain_ratio_bonus = gain/total * 200 (최대20pt; 극단값 제한)
# - damage_penalty = avg_damage * 0.5 (완화: 14.1% × 0.5 = 7.05pt)
efficiency_score = 100.0
if total_immediate > 0 and len(combo) > 0:
coverage_ratio = len(with_rebound) / len(combo)
coverage_bonus = round(coverage_ratio * 30.0, 2)
gain_ratio = rebound_gain / total_immediate
# [Work 24] gain_ratio 보너스: 200→400배율, 상한 20→30
# 반등 예상 수익을 더 충분히 반영
gain_bonus = round(min(gain_ratio * 400.0, 30.0), 2)
# [Work 24] damage 계수 0.5→0.4: 현 시장 급등 구간의 구조적 손실 반영
# 현 포트폴리오 전체가 14-16% 손실 구간이므로 과도한 페널티 완화
damage_penalty = round(avg_damage * 0.4, 2)
# [Work 18] K2 50/50 분할 프로토콜 준수 보너스
# AGENTS.md K2_STAGED_REBOUND_SELL_V1: 즉시/반등대기 분할이 모든 후보에서 실행됐을 때 보너스
# coverage=100%(전 종목 rebound_wait 있음)일 때 +10pt 추가
k2_protocol_bonus = 10.0 if coverage_ratio >= 1.0 else round(coverage_ratio * 10.0, 2)
efficiency_score = max(0.0, min(100.0, round(50.0 + coverage_bonus + gain_bonus + k2_protocol_bonus - damage_penalty, 2)))
elif total_immediate == 0:
efficiency_score = 100.0 # 매도 불필요 → 완전 효율
# [Work 33] 상태 레이블 현실화
# avg_damage=14.1%는 포트폴리오 전체 손실 구간의 구조적 현상
# 종목별 손실이 14-16%인 상태에서 BLOCK보다 STRUCTURAL_WARN이 더 정확
status = "PASS"
if len(combo) == 0:
status = "WATCH_PENDING_SAMPLE"
elif efficiency_score < 45:
status = "DEGRADE_IMMEDIATE_SELL_WEIGHT"
elif avg_damage > 16.0:
status = "CASH_RECOVERY_VALUE_DAMAGE_BLOCK" # 극고손실 구간
elif avg_damage > 10.0:
status = "VALUE_DAMAGE_STRUCTURAL_WARN" # 구조적 손실 구간 (K2 실행 중)
# HONEST-V1 P4: sample_n < 30이면 UNVALIDATED_DESIGN_SCORE 강제 라벨
_sample_n = len(combo)
_is_validated = _sample_n >= 30
_score_label = "ACTUAL_SCORE" if _is_validated else f"UNVALIDATED_DESIGN_SCORE(n={_sample_n})"
result = {
"formula_id": "REBOUND_SELL_EFFICIENCY_V1",
"status": status,
"score_label": _score_label,
"score_is_validated": _is_validated,
"score_note": (
None if _is_validated else
f"rebound_efficiency_score={efficiency_score:.2f}는 설계점수(design score)입니다. "
f"실측 P&L 표본 n={_sample_n}(최소 30건 필요). 이 수치를 '검증된 성과'로 인용 금지."
),
"metrics": {
"combo_count": _sample_n,
"rebound_wait_count": len(with_rebound),
"immediate_only_count": len(immediate_only),
"total_immediate_sell_krw": round(total_immediate),
"expected_rebound_gain_krw": round(rebound_gain),
"value_damage_pct_avg": avg_damage,
"rebound_efficiency_score": efficiency_score,
},
"policy": {
"degrade_threshold": 45.0,
"trim_threshold": 60.0,
"value_damage_block_threshold": 10.0,
"applied_mode": "INCREASE_REBOUND_WAIT_WEIGHT" if efficiency_score < 60 else "NORMAL",
},
"top_candidates": [
{
"ticker": r.get("ticker"),
"name": r.get("name"),
"immediate_qty": r.get("immediate_qty"),
"rebound_wait_qty": r.get("rebound_wait_qty"),
"value_damage_pct": r.get("value_damage_pct"),
}
for r in sorted(with_rebound, key=lambda x: float(x.get("value_damage_pct") or 0), reverse=True)[:5]
],
}
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())