Files
QuantEngineByItz/tools/build_ratchet_trailing_general_v1.py
T
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

136 lines
4.6 KiB
Python
Raw 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" / "ratchet_trailing_general_v1.json"
def _load(path: Path) -> dict[str, Any]:
try:
d = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
return d if isinstance(d, dict) else {}
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):
try:
return _rows(json.loads(v))
except Exception:
return []
return []
def _f(v: Any) -> float:
try:
return float(v)
except Exception:
return 0.0
def _breakeven_ratchet_price(avg_cost: float) -> int | None:
if avg_cost <= 0:
return None
# BREAKEVEN_RATCHET_V1: 세후/호가 반올림 이전의 보수적 손익분기 래칫 기준선
return int(round(avg_cost * 1.005, 0))
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()
jp = Path(args.json)
op = Path(args.out)
if not jp.is_absolute():
jp = ROOT / jp
if not op.is_absolute():
op = ROOT / op
payload = _load(jp)
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
h = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else {}
# [NF5] PROFIT_GIVEBACK_RATCHET_FACTOR_V1: 수익률 국면별 k 계수 (calibration_registry.yaml)
# trail_stop = max(prev_trail≈avg, high_since_entry≈cur k × ATR20)
NF5_K = {
"APEX_SUPER": 1.0, # NF5_K_APEX_SUPER: profit >= 50%
"APEX_TRAILING": 1.5, # NF5_K_APEX_TRAILING: profit 40~50%
"PROFIT_LOCK_30": 2.0, # NF5_K_PROFIT_LOCK_30: profit 20~40%
"NEUTRAL": 2.5, # NF5_K_NEUTRAL: profit < 20%
}
def _nf5_k(profit_pct: float) -> tuple[float, str]:
if profit_pct >= 50.0:
return NF5_K["APEX_SUPER"], "APEX_SUPER"
if profit_pct >= 40.0:
return NF5_K["APEX_TRAILING"], "APEX_TRAILING"
if profit_pct >= 20.0:
return NF5_K["PROFIT_LOCK_30"], "PROFIT_LOCK_30"
return NF5_K["NEUTRAL"], "NEUTRAL"
prices = _rows(h.get("prices_json"))
coverage_denom = 0
coverage_num = 0
rows = []
summary_breakeven = None
summary_source_ticker = None
summary_source_name = None
for p in prices:
cur = _f(p.get("current_price") or p.get("current_price_krw"))
avg = _f(
p.get("avg_price")
or p.get("average_cost")
or p.get("avg_cost_krw")
or p.get("avg_cost")
)
atr = _f(p.get("atr20") or p.get("ATR20"))
profit = cur > 0 and avg > 0 and cur > avg
trailing = None
profit_stage = None
k_used = None
breakeven = _breakeven_ratchet_price(avg)
if summary_breakeven is None and breakeven is not None:
summary_breakeven = breakeven
summary_source_ticker = p.get("ticker")
summary_source_name = p.get("name")
if profit:
coverage_denom += 1
profit_pct = (cur - avg) / avg * 100.0
k_used, profit_stage = _nf5_k(profit_pct)
trailing = round(max(avg, cur - atr * k_used), 0)
coverage_num += 1
rows.append({
"ticker": p.get("ticker"),
"name": p.get("name"),
"auto_trailing_stop": trailing,
"breakeven_stop_price": breakeven,
"profit_stage": profit_stage,
"nf5_k": k_used,
})
coverage = 100.0 if coverage_denom == 0 else round((coverage_num / coverage_denom) * 100.0, 2)
out = {
"formula_id": "RATCHET_TRAILING_GENERAL_V1",
"gate": "PASS" if coverage >= 99.0 else "CAUTION",
"coverage_pct": coverage,
"rows": rows,
"breakeven_formula_id": "BREAKEVEN_RATCHET_V1",
"breakeven_stop_price": summary_breakeven,
"breakeven_source_ticker": summary_source_ticker,
"breakeven_source_name": summary_source_name,
}
op.parent.mkdir(parents=True, exist_ok=True)
op.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({"formula_id": out["formula_id"], "coverage_pct": out["coverage_pct"], "gate": out["gate"]}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())