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
+201
View File
@@ -0,0 +1,201 @@
meta:
title: "performance 탭 계약서 — Bayesian Multiplier 자동 계산"
parent_file: "RetirementAssetPortfolio.yaml"
version: "2026-05-17-initial"
language: "ko-KR"
timezone: "Asia/Seoul"
role: "canonical"
purpose: >
Google Sheets 'performance' 탭의 구조·입력 규칙을 정의하고,
GAS가 이 데이터를 읽어 Bayesian multiplier를 자동 계산하는 계약을 명시한다.
S1_trades_performance_sheet(spec/16_data_gaps_roadmap.yaml) 구현 사양.
# ─────────────────────────────────────────────────────────────────────────────
# 시트 구조
# ─────────────────────────────────────────────────────────────────────────────
sheet:
name: "performance"
header_row: 2 # row1=updated 메타, row2=헤더
data_start_row: 3
sort_order: "entry_date 내림차순 (최신 거래 위)"
max_lookback_trades: 30 # Bayesian 계산에 사용하는 최근 거래 수
required_columns:
- name: "trade_id"
type: "string"
format: "T-YYYYMMDD-NNN (예: T-20260517-001)"
note: "중복 방지용 고유 ID. 수동 입력."
- name: "ticker"
type: "string"
note: "종목코드 6자리."
- name: "name"
type: "string"
- name: "sector"
type: "string"
note: "TICKER_SECTOR_MAP 기준 섹터명."
- name: "account"
type: "string"
allowed_values: ["일반계좌", "ISA", "연금저축"]
- name: "entry_date"
type: "date_ISO8601"
example: "2026-05-17"
- name: "entry_price"
type: "number"
unit: "KRW_per_share"
- name: "entry_stage"
type: "string"
allowed_values: ["stage_1", "stage_2", "stage_3"]
note: "staged_entry_v2 기준 진입 단계."
- name: "quantity"
type: "integer"
unit: "shares"
- name: "stop_price_at_entry"
type: "number"
unit: "KRW_per_share"
note: "진입 당시 설정한 손절가. ATR 기반 또는 HTS 실제 설정값."
- name: "target_price_at_entry"
type: "number"
unit: "KRW_per_share"
note: "진입 당시 컨센서스 목표주가."
- name: "exit_date"
type: "date_ISO8601"
note: "미청산 포지션은 공백 — 진행 중 거래 제외 후 계산."
- name: "exit_price"
type: "number"
unit: "KRW_per_share"
note: "미청산 시 공백."
- name: "exit_reason"
type: "string"
allowed_values: ["stop_loss", "take_profit", "time_stop", "rw_exit", "manual", "partial_exit"]
note: "exit_date 공백이면 이 필드도 공백."
- name: "pnl_pct"
type: "number"
unit: "percent"
expression: "(exit_price / entry_price - 1) * 100"
note: "수동 입력 또는 시트 수식. 미청산 시 공백."
- name: "holding_days"
type: "integer"
expression: "exit_date - entry_date (영업일 기준 권장, 달력일도 허용)"
note: "미청산 시 공백."
- name: "entry_c1_score"
type: "number"
note: "진입 당시 C1 값 (0 or 1). data_feed 탭에서 복사."
- name: "entry_c2_score"
type: "number"
- name: "entry_c3_score"
type: "number"
- name: "entry_c4_score"
type: "number"
- name: "entry_c5_score"
type: "number"
- name: "entry_mrs_score"
type: "number"
note: "진입 당시 MRS 점수 (0~10). macro 탭에서 복사."
- name: "entry_leader_scan_total"
type: "number"
note: "진입 당시 Leader_Scan_Total. 자동 계산 = sum(C1~C5)."
- name: "fc_bucket"
type: "string"
allowed_values: ["Y", "N"]
note: "Y=stage_1 탐색 손실 → explore_loss_budget 귀속. N=본계좌 PnL."
# ─────────────────────────────────────────────────────────────────────────────
# Bayesian Multiplier 계산 규칙
# ─────────────────────────────────────────────────────────────────────────────
bayesian_multiplier:
formula_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.RISK_BUDGET_CASCADE_V1"
lookback: 30 # 최근 N건 청산 완료 거래만 사용 (exit_date 공백 제외)
minimum_trades: 5 # 5건 미만 시 not_enough_data → medium_confidence(0.5×) 기본
derived_metrics:
win_rate:
expression: "count(pnl_pct > 0) / count(pnl_pct is not null)"
note: "청산 완료 거래 중 수익 비율."
avg_win_pct:
expression: "mean(pnl_pct where pnl_pct > 0)"
avg_loss_pct:
expression: "mean(abs(pnl_pct) where pnl_pct <= 0)"
net_expectancy:
expression: "(win_rate * avg_win_pct) - ((1 - win_rate) * avg_loss_pct)"
note: "양수=시스템 양기대치. 음수=시스템 개선 필요."
multiplier_rules:
high_confidence:
condition: "win_rate >= 0.60 AND net_expectancy >= 3.0"
multiplier: 1.0
label: "high_bet"
medium_confidence:
condition: "win_rate >= 0.45 AND net_expectancy >= 0"
multiplier: 0.5
label: "medium_bet"
low_confidence:
condition: "win_rate < 0.45 OR net_expectancy < 0"
multiplier: 0.25
label: "low_bet"
no_bet:
condition: "연속 5회 손절 (최근 5건 모두 pnl_pct <= 0)"
multiplier: 0.0
label: "no_bet"
note: "시스템 재검토 기간. performance_brake와 연동."
not_enough_data:
condition: "청산 완료 거래 < minimum_trades"
multiplier: 0.5
label: "medium_confidence (데이터 부족 기본값)"
output_fields:
bayesian_multiplier:
type: "number"
values: [0.0, 0.25, 0.5, 1.0]
bayesian_label:
type: "string"
values: ["high_bet", "medium_bet", "low_bet", "no_bet", "medium_confidence"]
win_rate_30:
type: "number"
note: "최근 30건 승률."
net_expectancy_30:
type: "number"
note: "최근 30건 기대수익률(%)."
consecutive_losses:
type: "integer"
note: "최근 연속 손절 횟수."
trades_used:
type: "integer"
note: "계산에 사용된 거래 수."
# ─────────────────────────────────────────────────────────────────────────────
# GAS 통합 계획
# ─────────────────────────────────────────────────────────────────────────────
gas_integration:
function_name: "readPerformanceSheet_"
return_type: "object"
return_fields:
- "bayesian_multiplier: number (0.0/0.25/0.5/1.0)"
- "bayesian_label: string"
- "win_rate_30: number | null"
- "net_expectancy_30: number | null"
- "consecutive_losses: integer"
- "trades_used: integer"
fallback:
condition: "performance 탭 없음 OR 청산 완료 거래 < 5건"
return: "{ bayesian_multiplier: 0.5, bayesian_label: 'medium_confidence', trades_used: 0 }"
integration_point:
sheet: "data_feed"
field: "EE_Est"
current_formula: "(target-entry)/(entry-stop) × 0.5 - 0.003"
updated_formula: "(target-entry)/(entry-stop) × bayesian_multiplier - 0.003"
note: "runDataFeed 시작 시 1회 호출 → 루프 전체에 동일 multiplier 적용."
additional_output:
sheet: "macro"
row: "BAYESIAN_COMPUTED"
fields: ["Close=multiplier", "Status=label + win_rate + net_expectancy"]
note: "runMacro 또는 runDataFeed 마지막에 macro 탭에 Bayesian 상태 추가 행으로 기록."
# ─────────────────────────────────────────────────────────────────────────────
# 운영 지침
# ─────────────────────────────────────────────────────────────────────────────
operational_rules:
- "포지션 청산 당일 performance 탭에 수동 기록한다."
- "entry_c1~c5는 진입 당일 data_feed 탭에서 복사 — 사후 재계산 금지."
- "entry_mrs_score는 진입 당일 macro 탭 MRS_COMPUTED 행의 Close 값."
- "fc_bucket=Y인 거래는 explore_loss_budget 누적에 포함. 월말 집계."
- "연속 5회 손절(no_bet) 발동 시 runDataFeed에서 EE_Est=0으로 출력 — 신규 진입 자동 억제."