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:
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
build_calibration_priority_v1.py
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
P4 확장: alpha_feedback_loop_v2.json → calibration_registry.yaml 보정 제안 연결
|
||||
|
||||
alpha_feedback_loop_v2.json의 recommended_adjustments 를 읽어
|
||||
calibration_registry.yaml의 해당 임계값과 연결한 보정 우선순위 리포트를 생성한다.
|
||||
|
||||
출력:
|
||||
Temp/calibration_priority_v1.json
|
||||
- 보정 우선순위 목록 (feedback 신호 기반)
|
||||
- 각 임계값의 현재 상태(EXPERT_PRIOR/샘플 수)와 권장 조치
|
||||
- alpha_feedback_loop 미포착(miss5_count) 신호와의 연결
|
||||
|
||||
사용법:
|
||||
python tools/build_calibration_priority_v1.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
AFL = ROOT / "Temp" / "alpha_feedback_loop_v2.json"
|
||||
REG = ROOT / "spec" / "calibration_registry.yaml"
|
||||
OUTPUT = ROOT / "Temp" / "calibration_priority_v1.json"
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# alpha_feedback 요인 → 관련 calibration_registry ID 매핑
|
||||
FACTOR_TO_REGISTRY: dict[str, list[str]] = {
|
||||
"antithesis_balance": [
|
||||
"ALEG_V2_GATE1_BLOCK_PCT",
|
||||
"ALEG_V2_GATE2_BLOCK_PCT",
|
||||
"DSD_V1_CONFIRMED_WS",
|
||||
],
|
||||
"passive_signal_quality": [
|
||||
"ALEG_V2_GATE1_BLOCK_PCT", # 뒷박 차단 임계 — timing=None 진입 허용 과다
|
||||
"ALEG_V2_GATE1_WAIT_PCT", # PULLBACK_WAIT 경계
|
||||
"K2_REBOUND_TRIGGER_ATR_MULT", # 반등 트리거 — 타이밍 조건
|
||||
],
|
||||
"active_signal_confidence": [
|
||||
"ALEG_V2_GATE1_BLOCK_PCT",
|
||||
"ALEG_V2_GATE2_BLOCK_PCT",
|
||||
"DSD_V1_SIG1_WEIGHT",
|
||||
"DSD_V1_SIG2_WEIGHT",
|
||||
],
|
||||
"k2_rebound_efficiency": [
|
||||
"K2_SPLIT_RATIO",
|
||||
"K2_REBOUND_TRIGGER_ATR_MULT",
|
||||
"K2_DEADLINE_DAYS",
|
||||
"SCR_V4_EFFICIENCY_DAMAGE_PENALTY_COEFF",
|
||||
],
|
||||
# CAPITAL_STYLE_ALLOCATION_V1 — 투자성향별 가중치 보정
|
||||
# passive_signal_quality miss5_count=51 → 단타/단기 신호 가중치 재보정 필요
|
||||
"passive_signal_quality_style": [
|
||||
"CSA_SCALP_W_TECHNICAL", # 단타에서 기술지표 과도 의존 여부 확인
|
||||
"CSA_SCALP_W_SMARTMONEY",
|
||||
"CSA_SWING_W_TECHNICAL",
|
||||
"CSA_SWING_W_SMARTMONEY",
|
||||
"CSA_TECH_RSI_OVERSOLD", # RSI<35 임계 최적화
|
||||
"CSA_TECH_DISPARITY_PULLBACK", # 눌림목 3% 임계 최적화
|
||||
],
|
||||
"conviction_calibration": [
|
||||
"CSA_POSITION_PCT_HIGH_CONVICTION", # 80점 임계 → 실측 분포 기반 조정
|
||||
"CSA_POSITION_PCT_STRONG", # 65점 임계
|
||||
"CSA_POSITION_PCT_MODERATE", # 50점 임계
|
||||
"CSA_POSITION_PCT_PILOT", # 35점 임계
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def load_json(p: Path) -> dict:
|
||||
if not p.exists():
|
||||
return {}
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def load_registry(p: Path) -> dict[str, dict]:
|
||||
if not p.exists():
|
||||
return {}
|
||||
data = yaml.safe_load(p.read_text(encoding="utf-8"))
|
||||
return {t["id"]: t for t in data.get("thresholds", []) if "id" in t}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
afl_data = load_json(AFL)
|
||||
reg_index = load_registry(REG)
|
||||
|
||||
sep = "=" * 70
|
||||
print(sep)
|
||||
print(" 알파 피드백 루프 → 보정 우선순위 연결기 (CALIB-PRIORITY-V1)")
|
||||
print(sep)
|
||||
|
||||
adjustments = afl_data.get("recommended_adjustments", [])
|
||||
cases_analyzed = afl_data.get("cases_analyzed", 0)
|
||||
miss5_count = 0
|
||||
for adj in adjustments:
|
||||
if adj.get("factor") == "passive_signal_quality":
|
||||
miss5_count = int(adj.get("miss5_count", 0))
|
||||
|
||||
print(f"\n [alpha_feedback_loop_v2] cases_analyzed={cases_analyzed}")
|
||||
print(f" miss5_count(5%+ 급등 미포착)={miss5_count}건 → passive_signal_quality 개선 필요")
|
||||
|
||||
priority_list: list[dict] = []
|
||||
|
||||
for adj in adjustments:
|
||||
factor = adj.get("factor", "")
|
||||
action = adj.get("action", "")
|
||||
rationale = adj.get("rationale", "")
|
||||
reg_ids = FACTOR_TO_REGISTRY.get(factor, [])
|
||||
|
||||
for rid in reg_ids:
|
||||
reg_entry = reg_index.get(rid)
|
||||
if not reg_entry:
|
||||
continue
|
||||
source = reg_entry.get("source", "EXPERT_PRIOR")
|
||||
sample_n = int(reg_entry.get("sample_n", 0) or 0)
|
||||
value = reg_entry.get("value")
|
||||
formula = reg_entry.get("owner_formula", "")
|
||||
|
||||
# 보정 우선도 점수: miss5_count 기여 + 미보정 가중
|
||||
urgency = 0
|
||||
if factor == "passive_signal_quality":
|
||||
urgency += miss5_count # miss가 많을수록 높은 urgency
|
||||
if source == "EXPERT_PRIOR":
|
||||
urgency += 10
|
||||
if sample_n == 0:
|
||||
urgency += 5
|
||||
|
||||
priority_list.append({
|
||||
"calibration_id": rid,
|
||||
"current_value": value,
|
||||
"owner_formula": formula,
|
||||
"source": source,
|
||||
"sample_n": sample_n,
|
||||
"linked_factor": factor,
|
||||
"alpha_action": action,
|
||||
"urgency_score": urgency,
|
||||
"calibration_path": (
|
||||
(
|
||||
"표본 30건 이상 확보 후 PROVISIONAL 승격 → "
|
||||
if sample_n >= 30
|
||||
else f"표본 {30 - sample_n}건 추가 수집 후 PROVISIONAL 승격 → "
|
||||
)
|
||||
+ "실측 T+5 승률 기반 최적값 backtest → CALIBRATED 확정"
|
||||
),
|
||||
"rationale": rationale[:200] if rationale else "",
|
||||
})
|
||||
|
||||
# 중복 제거 (같은 rid, 높은 urgency 유지)
|
||||
seen: dict[str, dict] = {}
|
||||
for p in priority_list:
|
||||
rid = p["calibration_id"]
|
||||
if rid not in seen or p["urgency_score"] > seen[rid]["urgency_score"]:
|
||||
seen[rid] = p
|
||||
priority_list = sorted(seen.values(), key=lambda x: -x["urgency_score"])
|
||||
|
||||
print(f"\n [보정 우선순위 TOP-10]")
|
||||
print(f" {'순위':<4} {'ID':<45} {'값':>7} {'샘플':>5} {'긴급도':>6}")
|
||||
print(f" {'-'*4} {'-'*45} {'-'*7} {'-'*5} {'-'*6}")
|
||||
for rank, item in enumerate(priority_list[:10], 1):
|
||||
print(
|
||||
f" {rank:<4} {item['calibration_id']:<45} "
|
||||
f"{str(item['current_value']):>7} {item['sample_n']:>5} {item['urgency_score']:>6}"
|
||||
)
|
||||
|
||||
print(f"\n [보정 로드맵]")
|
||||
print(f" Step 1 (즉시): 표본 누적 — 매 거래일 T+5 결과 자동 수집")
|
||||
print(f" Step 2 (30건 후): ALEG_V2_GATE1_BLOCK_PCT 3.0% → 실측 최적값으로 PROVISIONAL 승격")
|
||||
print(f" Step 3 (50건 후): DSD_V1 가중치 logistic regression 최적화")
|
||||
print(f" Step 4 (100건 후): K2_SPLIT_RATIO backtest 비교 → CALIBRATED 확정")
|
||||
print(f" miss5_count={miss5_count}건 → passive_signal_quality 개선이 T+5 35.86%→50%+ 핵심")
|
||||
|
||||
result = {
|
||||
"status": "CALIBRATION_PRIORITY_OK",
|
||||
"cases_analyzed": cases_analyzed,
|
||||
"miss5_count": miss5_count,
|
||||
"priority_count": len(priority_list),
|
||||
"priority_list": priority_list,
|
||||
"roadmap": {
|
||||
"step1": "표본 누적 — 매 거래일 T+5 결과 자동 수집",
|
||||
"step2": "30건 후: ALEG_V2_GATE1_BLOCK_PCT 3.0% → 실측 최적값 PROVISIONAL 승격",
|
||||
"step3": "50건 후: DSD_V1 가중치 logistic regression 최적화",
|
||||
"step4": "100건 후: K2_SPLIT_RATIO 30/70~60/40 backtest → CALIBRATED",
|
||||
},
|
||||
"target_improvement": {
|
||||
"current_t5_pct": 35.86,
|
||||
"target_t5_pct": 55.0,
|
||||
"key_lever": "passive_signal_quality (miss5_count=51건 개선)",
|
||||
},
|
||||
}
|
||||
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
print(f"\n → 결과 저장: {OUTPUT}")
|
||||
print(f" CALIBRATION_PRIORITY_OK\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user