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
+173
View File
@@ -0,0 +1,173 @@
meta:
title: "은퇴자산포트폴리오 — Final_Action 결정 매트릭스"
version: "2026-05-17-action_matrix_v1"
language: "ko-KR"
role: "canonical"
purpose: >
gas_data_feed.gs의 Allowed_Action(매수 게이트) + calcSellDecision_(매도) +
calcFinalDecision_(통합)이 어떤 조건에서 어떤 Final_Action을 출력하는지
단일 진실 소스로 문서화한다.
"왜 지금 매수/매도인가"의 패턴을 Action_Reason 컬럼과 함께 읽으면 된다.
# ─────────────────────────────────────────────────────────────────────────────
# canonical_fields
# ─────────────────────────────────────────────────────────────────────────────
canonical_fields:
Final_Action:
role: "외부 소비 canonical field — getDailyBrief, API, ChatGPT 모두 이 값을 참조"
allowed_values:
SELL_READY: "Sell_Validation=PASS — 즉시 HTS 주문 가능"
SELL_CHECK_QTY: "Sell_Action 있으나 보유수량 미확인 — 사용자가 확인 후 주문"
EXIT_SIGNAL: "Allowed_Action=EXIT_SIGNAL (RW>=3 또는 STOP_OR_TIME_EXIT_READY)"
EXIT_REVIEW: "Allowed_Action=REVIEW_EXIT (RW=2 또는 EXIT_REVIEW)"
BUY_STAGE1_READY: "SS001_Grade A + Entry_Mode_Gate=PASS + Timing=BUY_STAGE1_READY"
BUY_BREAKOUT_PILOT_ONLY: "SS001_Grade A + 돌파 파일럿 진입 조건 충족"
BUY_PULLBACK_WAIT: "SS001_Grade A/B + 눌림목 대기 (진입 타이밍 준비)"
WATCH_TIMING_SETUP: "SS001_Grade A/B이지만 타이밍 미충족 — Allowed_Action=WATCH_CANDIDATE"
NO_BUY_OVERHEATED: "과열 지표 발동 (AC_Gate=BLOCK 또는 Val_Surge 과도)"
HOLD: "그 외 전체 — Allowed_Action(NO_ADD/HOLD/HOLD_NO_ADD/OBSERVE_ONLY)으로 세분"
Action_Params:
role: "실행 파라미터 압축 — 외부 소비자(getDailyBrief·API·ChatGPT)가 즉시 사용 가능한 실행 정보"
format:
SELL_READY: "{ratio}% {qty}주 @{price}원 | {executionWindow} | {orderType}"
SELL_CHECK_QTY: "{sellAction} | 보유수량 미확인"
EXIT_SIGNAL/EXIT_REVIEW: "보유수량 미확인 — HTS 확인 필요"
BUY_*: "목표 {posSizeQty}주 | 손절 {stopPriceEst}원 | TP1 {tp1Price}원({tp1Qty}주)"
WATCH_TIMING_SETUP: "대기 — {timingBlockReason}"
HOLD/etc: "" # 빈 문자열
Action_Reason:
role: "왜 이 Final_Action인가를 한 문자열로 요약 — 사람이 읽는 컬럼"
format:
SELL_READY: "{sellDetailLabel} {qty}주 @{limitPrice}원 [{sellReason}]"
EXIT_SIGNAL/EXIT_REVIEW: "RW{n}({RW1+RW2...}) {exitSignalDetail}"
BUY_*: "SS001:{grade}{normScore}점 RSI{rsi} 이격{disp}% FC{fc}"
WATCH_TIMING_SETUP: "SS001:{grade}{normScore}점 타이밍미충족({timingBlockReason})"
HOLD: "HeatBlock({heatPct}%) | {regime} | SS001:{grade}"
NO_ADD: "수급이탈 | 거래대금{억}억 | 스프레드{pct}% | {regime}"
HOLD_NO_ADD: "DART:{risk} | 과열({acGate})"
OBSERVE_ONLY: "PRICE_MISSING({priceStatus})"
Allowed_Action:
role: "내부 계산 중간값 — 매수 게이트 판정. 외부 소비 시 Final_Action 우선."
allowed_values:
OBSERVE_ONLY: "가격 데이터 없음 — 모든 계산 불가"
HOLD: "HF005 BLOCK, 레짐 차단, 또는 SS001_Grade C"
NO_ADD: "수급이탈 / 거래대금 부족 / 스프레드 과도 / 레짐 차단(미보유)"
HOLD_NO_ADD: "DART 리스크 또는 과열 게이트"
EXIT_SIGNAL: "RW_Partial >= 3 또는 Timing_Action=STOP_OR_TIME_EXIT_READY"
REVIEW_EXIT: "RW_Partial >= 2 또는 Timing_Action=EXIT_REVIEW"
WATCH_CANDIDATE: "SS001_Grade A/B이지만 타이밍 미충족"
BUY_STAGE1_READY: "SS001_Grade A + 타이밍 충족"
BUY_BREAKOUT_PILOT_ONLY: "SS001_Grade A + 돌파 파일럿"
BUY_PULLBACK_WAIT: "SS001_Grade A/B + 눌림목 대기"
# ─────────────────────────────────────────────────────────────────────────────
# 매수 패턴 매트릭스
# ─────────────────────────────────────────────────────────────────────────────
buy_action_matrix:
purpose: "SS001_Grade × Timing_Action × 제약 → Final_Action 결정 규칙"
BUY_STAGE1_READY:
required_all:
- SS001_Grade: "A"
- Timing_Action: "BUY_STAGE1_READY"
- Entry_Mode_Gate: "PASS"
blocked_if_any:
- heatBlock: true # globalHeatPct >= 10%
- isRiskOffRegime: true # RISK_OFF or RISK_OFF_CANDIDATE
- dartRisk: true
- liquidityFail: true # flow.ok=F or avgTV5D<50 or spread>0.8%
caution_if:
- heatCaution: true # 7~10% → Pos_Size_Qty × 0.5 & Action_Reason에 표기
action_reason_template: "SS001:A{score}점 RSI{rsi} 이격{disp}% FC{fc}"
BUY_BREAKOUT_PILOT_ONLY:
required_all:
- SS001_Grade: "A"
- Timing_Action: "BUY_BREAKOUT_PILOT_ONLY"
- Entry_Mode_Gate: "PASS"
blocked_if_any: [heatBlock, isRiskOffRegime, dartRisk, liquidityFail]
note: "돌파 파일럿 — 전체 수량의 30~50%만 진입. Entry_Mode=BREAKOUT."
BUY_PULLBACK_WAIT:
required_any:
- {SS001_Grade: "A", Timing_Action: "BUY_PULLBACK_WAIT"}
- {SS001_Grade: "B", Timing_Action: "BUY_PULLBACK_WAIT"}
blocked_if_any: [heatBlock, isRiskOffRegime, dartRisk, liquidityFail]
note: "눌림목 대기 — 진입 타이밍 준비 중. 지정가 주문 미리 설정 권장."
WATCH_TIMING_SETUP:
required_all:
- SS001_Grade: ["A", "B"]
timing_condition: "Timing_Action not in [BUY_STAGE1_READY, BUY_BREAKOUT_PILOT_ONLY, BUY_PULLBACK_WAIT]"
note: "등급은 되지만 타이밍 미충족. Action_Reason에 구체적 미충족 이유 표기."
# ─────────────────────────────────────────────────────────────────────────────
# 매도 패턴 매트릭스
# ─────────────────────────────────────────────────────────────────────────────
sell_action_matrix:
purpose: "매도 신호 발생 시 Final_Action 결정 규칙. spec/exit/stop_loss.yaml sell_signal_priority와 연동."
SELL_READY:
trigger: "calcSellDecision_()의 Sell_Validation = PASS"
substates:
EXIT_100: "손절전량 — STOP_OR_TIME_EXIT_READY 또는 RW_Partial >= 4"
REGIME_TRIM_50: "레짐 50% 축소 — getDailyBrief 포트폴리오 경고로 이동 (방향 A: 개별 종목 신호 아님)"
TRIM_70: "RW청산 70% — RW_Partial >= 3 또는 Timing_Exit_Score >= 75"
TRAILING_STOP_BREACH: "트레일링이탈 70% — close <= trailing_stop_price 직접 체크"
TRIM_50: "RW부분 50% — RW_Partial >= 2 OR (RW_Partial >= 1 AND Timing_Exit_Score >= 50). RW_Partial=0 단독 기술지표로는 TRIM_50 불가."
PROFIT_TRIM_50/35/25: "익절 사다리 — Profit_Pct >= 50/30/20"
TAKE_PROFIT_TIER1: "TP1 익절 25% — Profit_Pct >= 10"
TIME_EXIT_100: "타임스탑 전량 — Days_To_Time_Stop <= 0 (spec priority 6)"
TIME_TRIM_50: "타임스탑 50% — Days_To_Time_Stop <= 7 (spec priority 6)"
canonical_price_field: Sell_Limit_Price
canonical_qty_field: Sell_Qty
action_reason_template: "{label} {qty}주 @{price}원 [{reason}]"
note: >
복수 조건 동시 발동 시 SL003_PRIORITY_MATRIX 적용:
Sell_Limit_Price = max(모든 발동 조건의 후보가격). priceSource=PRIORITY_MATRIX_MAX.
EXIT_SIGNAL:
trigger: "Sell_Validation != PASS AND (RW_Partial >= 3 OR Timing_Action=STOP_OR_TIME_EXIT_READY)"
action_reason_template: "RW{n}({items}) {exitSignalDetail}"
note: "보유수량 미확인 상태. 사용자가 HTS에서 보유수량 확인 후 주문."
EXIT_REVIEW:
trigger: "RW_Partial >= 2 OR Timing_Action=EXIT_REVIEW"
action_reason_template: "RW{n}({items}) 검토"
note: "매도 검토 단계. 다음 영업일 재확인 권장."
# ─────────────────────────────────────────────────────────────────────────────
# Action_Priority 우선순위 숫자 (calcFinalDecision_ 기준)
# ─────────────────────────────────────────────────────────────────────────────
action_priority_table:
10: SELL_READY
20: SELL_CHECK_QTY
28: EXIT_SIGNAL
32: EXIT_REVIEW
50: NO_BUY_OVERHEATED
60: BUY_STAGE1_READY
70: BUY_BREAKOUT_PILOT_ONLY
80: BUY_PULLBACK_WAIT
90: WATCH_TIMING_SETUP
99: HOLD
note: "낮을수록 우선순위 높음. Final_Rank는 Priority_Score 기준 내림차순 정렬 후 부여."
# ─────────────────────────────────────────────────────────────────────────────
# 브리핑 출력 형식 (getDailyBrief — C-1 재구조화)
# ─────────────────────────────────────────────────────────────────────────────
brief_format:
canonical_source: "Final_Action (not Allowed_Action)"
sort_within_group: "Final_Rank 오름차순 (Priority_Score 기반)"
sections_order:
1: "SELL_READY — 즉시 HTS 주문 가능"
2: "EXIT_SIGNAL / EXIT_REVIEW — 보유수량 확인 후 매도"
3: "BUY — 진입 조건 충족 (BUY_STAGE1_READY / BUY_BREAKOUT_PILOT_ONLY / BUY_PULLBACK_WAIT)"
4: "WATCH — 타이밍 대기 (WATCH_TIMING_SETUP)"
5: "HOLD / BLOCK — Allowed_Action으로 세분 표시"
dedup_rule: >
같은 종목이 SELL_READY이면서 EXIT_SIGNAL도 발생할 수 있다.
Final_Action=SELL_READY가 최우선 — SELL_READY 섹션에만 출력.
action_reason_display: "각 종목 한 줄에 Action_Reason 출력 → '왜'를 즉시 파악 가능"