meta:
  title: "performance 탭 계약서 — Bayesian Multiplier 자동 계산"
  parent_file: "RetirementAssetPortfolio.yaml"
  version: "2026-05-17-initial"
  language: "ko-KR"
  timezone: "Asia/Seoul"
  role: "canonical"
  has_code_implementation: true
  code_path: ["tools/build_honest_performance_guard_v1.py", "src/quant_engine/qualitative_sell_strategy_v1.py"]
  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으로 출력 — 신규 진입 자동 억제."

# ─────────────────────────────────────────────────────────────────────────────
# 팩터별 성과 피드백 및 정직 성과증빙 규칙 (P6-T04)
# ─────────────────────────────────────────────────────────────────────────────
honest_performance_guard:
  formula_id: HONEST_PERFORMANCE_GUARD_V1
  rules:
    - rule_id: HP001
      desc: "Live 표본 수가 30건 미만인 지표는 active 승격 근거로 사용 금지 (calibration_state=INSUFFICIENT_SAMPLES 강제)"
      condition: "live_sample_count < 30"
      action: "LOCK_CALIBRATION"
    - rule_id: HP002
      desc: "Replay 데이터와 Live 데이터를 혼합하여 성과 지표를 산출하는 행위 금지 (replay_in_live_stats == 0)"
      condition: "replay_in_live_stats > 0"
      action: "INVALIDATE_METRICS"
    - rule_id: HP003
      desc: "팩터별 성과(T+5/T+20/T+60) 결과를 horizon별로 분리해서 추적 및 저장한다."
      required_fields:
        - "ticker"
        - "action"
        - "horizon"
        - "factor_set"
        - "outcome"
  acceptance_criteria:
    factor_outcome_join_rate_pct: 95.0
    live_sample_under_30_unlock_count: 0
    replay_live_mixed_metric_count: 0

