5166750b53
2026-06-21 비판적 리뷰에서 spec/governance YAML이 코드 상태와 어긋난 채로 방치되던 3개 구체적 사례를 발견하고 정정했다. 근본 원인(동기화를 보장하는 장치 없음)에 대응하는 신규 CI 게이트도 함께 추가한다. - spec/aliases.yaml: deprecated alias 17건 제거(활성 참조 0건 확인 후, 2026-06-30 데드라인 전). role: deprecated_redirect인 spec/03_risk_policy.yaml, spec/04_strategy_rules.yaml 2개만 실삭제 — spec/06_exit_policy.yaml은 role: compatibility_index(영구유지 설계)였음을 재확인해 보존 - governance/gas_logic_migration_ledger_v1.yaml: 존재하지 않는 파일을 canonical 구현으로 인용하던 오류 2건 발견·정정, parity 테스트 부재로 GAS 코드 삭제 보류(F12/F13/F14) - spec/13_formula_registry.yaml: OVERHANG_PRESSURE_V1의 "-500000" 절대값 폴백을 avg_volume_5d 비례식으로 교체(EXPERT_PRIOR 등록) - tools/validate_specs.py: validate_spec_code_sync() 신규 — has_code_implementation/ code_path 필드가 있는 spec만 검사(점진적 롤아웃, 기존 PASS 상태 비파괴), 12개 파일 1차 태깅
343 lines
19 KiB
YAML
343 lines
19 KiB
YAML
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"
|
||
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: "양도소득세 적용 여부 확인 후 활성화. 현재는 세전 수익률만 사용."
|