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: "양도소득세 적용 여부 확인 후 활성화. 현재는 세전 수익률만 사용."