meta:
  title: "계좌 이미지 캡처 — Account Snapshot Contract"
  parent_file: "RetirementAssetPortfolio.yaml"
  version: "2026-05-16-F15_account_snapshot_contract"
  language: "ko-KR"
  timezone: "Asia/Seoul"
  role: "canonical"
  has_code_implementation: true
  code_path:
    - "src/quant_engine/snapshot_admin_store_v1.py"
    - "tools/validate_account_snapshot_contract_v1.py"
    - "tools/validate_snapshot_admin_web_v1.py"
  purpose: >
    이미지 캡처로 제공되는 계좌·잔고·현금 데이터를 구조화하는 계약.
    HTS 입력 가능 주문수량은 이 계약을 통과한 account_snapshot 없이는 산출 금지.

account_snapshot_contract:
  source_type: "image_capture"
  priority: "highest_for_account_holdings_cash"
  relationship_to_market_raw:
    market_raw_json: "GatherTradingData.json"
    market_raw_workbook_source: "GatherTradingData.xlsx"
    rule: "market raw JSON/xlsx는 시장 데이터, account_snapshot은 계좌 데이터. 서로 대체하지 않는다."

  required_capture_groups:
    holdings_screen:
      purpose: "보유수량·평단·평가금액 확인"
      required_fields:
        - "account"
        - "account_type"
        - "ticker_or_name"
        - "holding_quantity"
        - "average_cost"
        - "current_price"
        - "market_value"
    cash_screen:
      purpose: "즉시현금·D+2·주문가능금액 확인"
      required_fields:
        - "account"
        - "immediate_cash"
        - "settlement_cash_d2"
        - "available_cash"
    open_orders_screen:
      purpose: "미체결 주문·예약 주문금액 확인"
      required_fields:
        - "account"
        - "ticker_or_name"
        - "open_order_quantity"
        - "open_order_amount"
        - "order_side"
    contribution_limit_screen:
      purpose: "ISA/연금저축 납입 가능액·사용액 확인"
      required_fields:
        - "account"
        - "account_type"
        - "monthly_contribution_limit"
        - "monthly_contribution_used"
        - "remaining_contribution_capacity"

  canonical_fields:
    captured_at: {type: "datetime", timezone: "Asia/Seoul", required: true, column_position: 1}
    account: {type: "string", required: true}
    account_type: {type: "enum", allowed: ["일반계좌", "ISA", "연금저축"], required: true}
    ticker: {type: "string", required_for: ["position_match"], missing_action: "match_by_name_then_warn"}
    name: {type: "string", required_for: ["position_match"]}
    holding_quantity: {type: "integer", unit: "shares", required_for: ["sell_quantity", "total_heat", "take_profit"], note: "국내주식 정수 필수. 해외주식(foreign_equity_flag=true)은 AS002A 예외 적용, 소수 4자리까지 허용."}
    available_quantity: {type: "integer", unit: "shares", note: "HTS 매도가능수량. 미표시 시 빈칸."}
    foreign_equity_flag: {type: "boolean", optional: true, default: false, note: "해외주식 여부. true이면 AS002A 예외 적용."}
    foreign_currency: {type: "string", optional: true, note: "해외주식 원화환산 전 통화 코드. 예: USD, HKD."}
    fx_rate_at_capture: {type: "number", optional: true, unit: "KRW_per_foreign", note: "캡처 시점 환율. 원화평가액 = foreign_quantity × foreign_price × fx_rate."}
    krw_estimated_value: {type: "number", optional: true, unit: "KRW", note: "환율 적용 원화 평가금액. market_value와 교차검증."}
    estimated_withholding_tax_rate_pct: {type: "number", optional: true, unit: "percent", note: "해외주식 배당 원천징수세율. 예: 15.0 (미국 기본세율)."}
    country_code: {type: "string", optional: true, note: "종목 상장 국가코드. ISO 3166-1 alpha-2. 예: US, HK, JP."}
    average_cost: {type: "number", unit: "KRW_per_share", required_for: ["profit_pct", "take_profit"]}
    total_cost: {type: "number", unit: "KRW", note: "매입금액 = holding_quantity × average_cost. HTS 직접 값 우선."}
    current_price: {type: "number", unit: "KRW_per_share", required_for: ["position_value_cross_check"]}
    market_value: {type: "number", unit: "KRW", required_for: ["cross_check"]}
    profit_loss: {type: "number", unit: "KRW", note: "평가손익. HTS 화면 값."}
    return_pct: {type: "number", unit: "percent", note: "수익률%. 예: 7.5 (% 기호 제외)."}
    immediate_cash: {type: "number", unit: "KRW", required_for: ["cash_floor"], note: "일반계좌 기준 포트폴리오 현금 원장. ISA/연금저축 행은 참고잔액일 수 있으나 포트폴리오 cash_floor/buy_power 합산 금지."}
    settlement_cash_d2: {type: "number", unit: "KRW", required_for: ["buy_power"], note: "일반계좌 기준 D+2 원장. ISA/연금저축 행은 일반계좌 매수재원으로 합산 금지."}
    available_cash: {type: "number", unit: "KRW", required_for: ["position_sizing"], note: "계좌별 주문 가능 금액. 일반계좌 외 계좌는 동일 계좌 내부 의사결정 참고용으로만 사용."}
    open_order_amount: {type: "number", unit: "KRW", default: 0}
    monthly_contribution_limit: {type: "number", unit: "KRW", required_for: ["ISA", "연금저축"]}
    monthly_contribution_used: {type: "number", unit: "KRW", required_for: ["ISA", "연금저축"]}
    parse_status: {type: "enum", allowed: ["CAPTURE_READ_OK", "CAPTURE_READ_FAILED", "CAPTURE_PROVIDED_BUT_NOT_HOLDINGS", "NOT_PROVIDED"], required: true}
    stop_price: {type: "number", unit: "KRW_per_share", optional: true, note: "선택 손절가. 미입력 시 ATR 기반 추정."}
    highest_price_since_entry: {type: "number", unit: "KRW_per_share", optional: true}
    entry_date: {type: "date_ISO8601", optional: true}
    entry_stage: {type: "string", optional: true, allowed: ["stage_1", "stage_2", "stage_3", "mixed"]}
    position_type: {type: "string", optional: true, allowed: ["core", "satellite"], default: "satellite"}
    last_updated: {type: "date_ISO8601", optional: true}

  capture_priority_rules:
    principle: >
      캡처 이미지로 제공된 데이터는 JSON data.account_snapshot의
      기존 값보다 항상 우선한다. 충돌 시 캡처값이 정답이며 GAS값은 무효.
    conflict_detection:
      trigger: "GatherTradingData.json 또는 account_snapshot 데이터가 같은 대화에 업로드된 경우"
      compare_fields: ["holding_quantity", "average_cost"]
      output: "capture_parse_prompt.md STEP 2b 충돌 감지 표"
    override_fields:
      - holding_quantity: "캡처값 → Sell_Qty 재산출 기준"
      - average_cost:     "캡처값 → Profit_Pct·Unrealized_PnL 재산출 기준"
      - market_value:     "캡처값 → Weight_Pct 재산출 기준"
    recalculate_on_override:
      - "Profit_Pct = (Close - 캡처평단) / 캡처평단 × 100"
      - "Unrealized_PnL = (Close - 캡처평단) × 캡처수량"
      - "Weight_Pct = (Close × 캡처수량) / 총자산 × 100"
      - "Sell_Qty = 캡처수량 × Sell_Ratio_Pct / 100 (반올림, available_quantity 상한)"
    no_recalculate:
      - "SS001_Grade, RW_Partial, Flow_Credit, ATR20, MA20, Final_Action, Sell_Ratio_Pct"
      - "이 항목들은 시장데이터 기반 — JSON data.data_feed 값 그대로 사용"
    display:
      tag: "(캡처재산출)"
      rule: "재산출된 값 옆에 태그를 붙여 GAS값과 구분"
    isa_pension_scope:
      rule: >
        ISA·연금저축 계좌 캡처의 금액은 일반계좌 현금이 아니라
        이미 매입·운용이 진행 중인 계좌의 잔액/평가 reference로 취급한다.
        따라서 해당 행의 immediate_cash, settlement_cash_d2, available_cash, open_order_amount는
        포트폴리오 레벨 cash_floor, immediate_cash_pct, buy_power_krw 합산에 포함하지 않는다.
        개별 종목 보유수량·평단이 account_snapshot에 포함되지 않으면
        해당 계좌 종목의 Sell_Qty 산출 불가 → "캡처확인후기재".
      routing_rule:
        portfolio_cash_ledger: "일반계좌 only"
        restricted_account_types: ["ISA", "연금저축"]
        restricted_account_treatment:
          - "포트폴리오 cash_floor 계산 제외"
          - "포트폴리오 buy_power 계산 제외"
          - "일반계좌 신규매수 재원으로 전용 금지"
          - "계좌 유형 식별·한도 추적·보유평가 참고용으로만 유지"

  validation_rules:
    - id: "AS001_LABEL_FIRST"
      rule: "숫자보다 라벨을 먼저 판독한다. 라벨 없는 숫자는 사용 금지."
    - id: "AS002_QUANTITY_INTEGER"
      rule: "holding_quantity와 open_order_quantity는 정수. 소수면 CAPTURE_READ_FAILED."
    - id: "AS002A_FOREIGN_EQUITY_DECIMAL"
      rule: "foreign_equity_flag=true인 종목은 holding_quantity가 소수 가능. decimal_precision: 4. CAPTURE_READ_OK 처리. open_order_quantity도 동일 예외 적용."
      condition: "foreign_equity_flag == true"
      decimal_precision: 4
      note: "해외주식 소수주(미국 증권사 fractional share, ISA 해외ETF 등) 대응. 국내주식에는 이 예외 미적용."
    - id: "AS003_MARKET_VALUE_CROSS_CHECK"
      rule: "abs(market_value - holding_quantity * current_price) / max(market_value, 1) <= 0.01 이어야 한다."
      fail_action: "DATA_CONFLICT"
    - id: "AS004_CASH_NON_NEGATIVE"
      rule: "현금 관련 필드는 0 이상이어야 한다."
      fail_action: "CAPTURE_READ_FAILED"
    - id: "AS005_ACCOUNT_SCOPE"
      rule: "계좌별 주문수량은 같은 계좌의 보유수량·현금만 사용한다. 계좌 간 현금 합산 금지."
    - id: "AS005A_RESTRICTED_ACCOUNT_CASH_EXCLUSION"
      rule: "account_type in [ISA, 연금저축] 행의 현금성 숫자는 포트폴리오 immediate_cash, settlement_cash_d2, buy_power 집계에서 제외한다."
      fail_action: "HARNESS_CASH_LEDGER_CONFLICT"
    - id: "AS006_AUTO_INVEST_SCREEN_EXCLUSION"
      rule: "자동투자/적립식 설정 화면은 보유수량·평단·현금 원장으로 사용 금지."

  output_required:
    table_name: "account_snapshot"
    total_columns: 27
    column_order_locked: true
    columns:
      - "captured_at"
      - "account"
      - "account_type"
      - "ticker"
      - "name"
      - "holding_quantity"
      - "available_quantity"
      - "average_cost"
      - "total_cost"
      - "current_price"
      - "market_value"
      - "profit_loss"
      - "return_pct"
      - "immediate_cash"
      - "settlement_cash_d2"
      - "available_cash"
      - "open_order_amount"
      - "monthly_contribution_limit"
      - "monthly_contribution_used"
      - "parse_status"
      - "user_confirmed"
      - "stop_price"
      - "highest_price_since_entry"
      - "entry_date"
      - "entry_stage"
      - "position_type"
      - "last_updated"

  excel_paste_output:
    purpose: >
      캡처 판독 후 분석보다 먼저 엑셀 붙여넣기용 TSV 표를 출력한다.
      사용자가 account_snapshot 탭에 복사·붙여넣기만으로 원장을 갱신할 수 있게 한다.
    trigger: "HTS 캡처 이미지가 대화에 첨부된 모든 경우"
    prompt_file: "prompts/capture_parse_prompt.md"
    output_sequence:
      - step: 1
        output: "화면 종류 판별 + 검증 결과 (AS001~AS006)"
      - step: 2
        output: "account_snapshot 탭 붙여넣기용 TSV (헤더 없이, A3 셀 기준)"
        format: "TSV (탭구분), 코드블록으로 감싸기"
        paste_target: "account_snapshot 탭 A3 셀"
        column_order_locked: true
        total_columns: 27
        column_sequence: "captured_at|account|account_type|ticker|name|holding_quantity|available_quantity|average_cost|total_cost|current_price|market_value|profit_loss|return_pct|immediate_cash|settlement_cash_d2|available_cash|open_order_amount|monthly_contribution_limit|monthly_contribution_used|parse_status|user_confirmed|stop_price|highest_price_since_entry|entry_date|entry_stage|position_type|last_updated"
        user_confirmed_value: "Y"
      - step: 3
        output: "account_snapshot 선택 포지션 상태 갱신 표 (ticker | stop_price | highest_price_since_entry | entry_stage | position_type | last_updated)"
        note: "보유수량·평단은 account_snapshot 캡처 TSV가 담당하며 positions 탭은 사용하지 않음"
      - step: 4
        output: "현금 요약 1줄 + settings 탭 settlement_cash_d2_krw 입력 안내"
      - step: 5
        output: "다음 단계 안내 (account_snapshot 붙여넣기 → GAS 재실행)"
    prohibition:
      - "캡처 파싱 표 생략 후 분석부터 시작 금지"
      - "TSV 대신 마크다운 표만 출력 금지 (마크다운 표는 Excel 붙여넣기 불가)"
      - "캡처 데이터를 분석에만 사용하고 원장 갱신 표를 누락하는 행위 금지"

  hard_stops:
    - "parse_status != CAPTURE_READ_OK인 계좌는 주문수량 산출 금지"
    - "holding_quantity 미확인 종목은 매도수량 산출 금지"
    - "available_cash 미확인 계좌는 매수수량 산출 금지"
    - "open_order_amount 미확인 시 중복주문 위험을 표시하고 validation_status=REVIEW_REQUIRED"

# ─────────────────────────────────────────────────────────────────────────────
# account_snapshot 포지션 상태 계약 (S2_stop_price_tracking — 2026-05-18)
# ─────────────────────────────────────────────────────────────────────────────
account_snapshot_position_state:
  sheet_name: "account_snapshot"
  status: "canonical"
  replaces: "positions"
  purpose: >
    보유수량·평단·현금 원장과 선택 포지션 상태(stop_price, highest_price_since_entry,
    entry_stage, position_type)를 account_snapshot에 통합한다.
    positions 탭은 deprecated이며 신규 분석·GAS 계산 입력으로 사용하지 않는다.
  optional_state_columns:
    - "stop_price"
    - "highest_price_since_entry"
    - "entry_date"
    - "entry_stage"
    - "position_type"
    - "last_updated"
  validation_rules:
    - id: "AS007_STOP_BELOW_AVERAGE_COST"
      rule: "stop_price가 있으면 stop_price < average_cost 이어야 한다. 위반 시 GAS가 경고 로그 출력."
    - id: "AS008_TOTAL_HEAT_CONFIRMED_ROWS_ONLY"
      rule: "TOTAL_HEAT_V1은 parse_status=CAPTURE_READ_OK AND user_confirmed=Y인 account_snapshot 보유행만 사용한다."
  gas_integration:
    function_name: "readAccountSnapshotHeat_"
    purpose: "account_snapshot을 읽어 TOTAL_HEAT_V1 계산."
    formula: >
      For each confirmed holding where holding_quantity > 0:
        heat_i = (average_cost - stop_price_or_atr_estimate) * holding_quantity
      total_heat_krw = sum(heat_i)
      total_heat_pct = total_heat_krw / total_asset_krw * 100
    fallback:
      condition: "stop_price 미입력 포지션 존재"
      return: >
        ATR 기반 추정: stop_price_est = average_cost - ATR20 * 1.5 (data_feed에서 ATR20 조회).
        ATR20도 없으면 average_cost * 0.92 보수 추정.
        추정값 사용 시 hf005_status에 "(ATR추정)" 표기.
  freshness_policy:
    update_frequency: "매일 장마감 직후 1회 (16:30~17:00)"
    required_fields_to_update: ["holding_quantity", "average_cost", "stop_price", "highest_price_since_entry", "last_updated"]
    staleness_threshold_days: 1
    staleness_action: >
      getDailyBrief()가 account_snapshot last_updated/captured_at 기준으로 경과일을 계산해
      1일 초과 시 brief_text에 'account_snapshot STALE' 경고를 출력한다.
    gas_check_function: "checkAccountSnapshotFreshness_()"

positions_tab:
  status: "deprecated"
  prohibition:
    - "신규 분석, TOTAL_HEAT, stop_price, 수량, 평단 입력 원장으로 사용 금지"
    - "사용자에게 positions 탭 수동 입력을 요구 금지"

# ─────────────────────────────────────────────────────────────────────────────
# 해외주식 스냅샷 계약 확장 (foreign_holdings_snapshot_extension — 2026-06-10)
# ─────────────────────────────────────────────────────────────────────────────
foreign_holdings_snapshot_extension:
  status: "active"
  applies_to: "account_snapshot rows where foreign_equity_flag=true"
  purpose: >
    해외주식(미국·홍콩·일본 등) 포지션의 원화환산·환율·세금 정보를 구조화.
    국내주식 account_snapshot 계약(AS001~AS008)을 기반으로 해외 특화 필드를 추가 정의.

  required_additional_fields:
    - name: "foreign_equity_flag"
      type: "boolean"
      value: true
      note: "해외주식 임을 명시. AS002A 소수점 예외 및 이 계약 적용 트리거."
    - name: "foreign_currency"
      type: "string"
      examples: ["USD", "HKD", "JPY"]
      note: "종목 원본 통화. HTS 화면에서 확인. 미확인 시 CAPTURE_READ_FAILED."
    - name: "fx_rate_at_capture"
      type: "number"
      unit: "KRW_per_foreign"
      note: "캡처 시점 매매기준율. 예: 1380.5 (1달러=1380.5원). 동일 통화는 동일 환율 일관성 필수."

  optional_additional_fields:
    - name: "krw_estimated_value"
      type: "number"
      unit: "KRW"
      formula: "holding_quantity × foreign_price × fx_rate_at_capture"
      note: "엔진 내부 원화 환산 평가액. market_value와 1% 이내 일치 확인(AS003 확장)."
    - name: "estimated_withholding_tax_rate_pct"
      type: "number"
      unit: "percent"
      defaults: {"US": 15.0, "HK": 0.0, "JP": 15.315}
      note: "배당금 원천징수세율. 시세차익세금과 별도. 운용 알파 계산 시 세후 수익률 보정에 사용."
    - name: "capital_gains_tax_applicable"
      type: "boolean"
      note: "해외주식 양도소득세(250만원 공제 후 22%) 적용 여부. 일반계좌=true, ISA 내=false."
    - name: "country_code"
      type: "string"
      format: "ISO 3166-1 alpha-2"
      examples: ["US", "HK", "JP"]
      note: "상장 국가코드. 세율·환율 룩업 키로 사용."

  validation_rules:
    - id: "ASFE001_FX_RATE_POSITIVE"
      rule: "fx_rate_at_capture > 0 이어야 한다. 0 또는 미입력 시 CAPTURE_READ_FAILED."
    - id: "ASFE002_CURRENCY_CONSISTENT"
      rule: "동일 통화 포지션은 동일 캡처 세션에서 같은 fx_rate_at_capture 사용. 다르면 DATA_CONFLICT."
    - id: "ASFE003_KRW_VALUE_CROSS_CHECK"
      rule: "krw_estimated_value 입력 시 |krw_estimated_value - holding_quantity × foreign_price × fx_rate| / krw_estimated_value <= 0.02. 초과 시 DATA_CONFLICT."

  gas_integration:
    note: >
      GAS parseAccountSnapshot_은 ticker 형식으로 국내/해외를 구분:
      국내 티커 패턴 = 6자리 숫자(\\d{6}), 해외 티커 패턴 = 영문 1~5자(\\b[A-Z]{1,5}\\b).
      foreign_equity_flag=true 행은 원화 환산 후 total_asset에 포함.
      fx_rate_at_capture 미입력 시 GAS가 settings 탭의 fx_rate_default 사용, 경고 로그 출력.
    function_name: "parseAccountSnapshot_"
    required_settings_key: "fx_rate_default"

  data_gated_features:
    - item: "해외주식 환율 손익 분리 표시"
      status: "DATA_GATED"
      note: "캡처 시점 환율 이력 누적 필요. 최소 20세션 이상 축적 후 활성화."
    - item: "세후 수익률 보정"
      status: "DATA_GATED"
      note: "양도소득세 적용 여부 확인 후 활성화. 현재는 세전 수익률만 사용."
