meta: title: "은퇴자산포트폴리오 — 의사결정 상태 머신" parent_file: "RetirementAssetPortfolio.yaml" version: "2026-05-15-F3_decision_flow" language: "ko-KR" timezone: "Asia/Seoul" purpose: > LLM이 투자 판단을 임의 순서로 수행하지 않도록 상태 머신으로 절차를 고정한다. 각 상태는 통과 조건, 실패 시 행동, 참조 파일을 가진다. decision_flow: initial_state: "INPUT_VALIDATION" terminal_states: ["FINAL_DECISION", "INSUFFICIENT_DATA", "BLOCKED"] deterministic_execution_control: purpose: "텍스트 해석 차이로 매번 다른 결론이 나오는 것을 줄이기 위한 결정 추적·동률 처리·first-match 규칙." rule: "모든 상태는 ordered_checks를 위에서 아래로 평가하고, 첫 번째 BLOCK/PASS/SELECT 결과를 decision_trace에 기록한다." no_freeform_override: - "상태 머신 밖의 산문 판단으로 final_action, grade, quantity, sell_priority를 변경 금지" - "동일 입력·동일 기준시각이면 동일 decision_trace와 동일 final_action이 나와야 한다." - "규칙 충돌 시 자연어로 절충하지 말고 tie_breaker 또는 INSUFFICIENT_DATA/BLOCKED로 종료한다." trace_required_fields: - "state" - "check_id" - "rule_ref" - "inputs_used" - "result" - "selected_action" - "blocked_actions" - "missing_inputs" - "tie_breaker_applied" tie_breaker_order: 1: "source_of_truth_order 상위 파일" 2: "risk hard stop 또는 master_prohibitions" 3: "데이터 완성도 OK > PARTIAL > DATA_MISSING" 4: "계산 가능한 공식 ID가 있는 규칙" 5: "보수적 행동: BLOCKED/INSUFFICIENT_DATA/WATCH" null_propagation_rule: "필수 입력이 null이면 해당 계산은 null로 유지하고 prohibited_calculations에 사유를 남긴다. null을 0으로 대체 금지." output_requirement: "OUTPUT_VALIDATION에서 decision_trace 누락 시 schema_validation_status=FAIL." states: INPUT_VALIDATION: purpose: "요청, 기준일, 계좌, 보유수량, 가격/수급/ATR 입력 존재 여부 확인" required_refs: - "spec/01_objective_profile.yaml:input_required" - "spec/12_field_dictionary.yaml:field_dictionary" - "spec/02_data_contract.yaml:quant_feed_contract" required_inputs: ["request_scope", "analysis_date", "account_scope"] computed_outputs: ["normalized_request", "field_alias_resolution_log"] pass_condition: "minimum request context exists" fail_state: "INSUFFICIENT_DATA" DATA_COMPLETENESS_CHECK: purpose: "데이터 상태 OK/PARTIAL/DATA_MISSING/DATA_CONFLICT/DATA_STALE 판정" required_refs: - "spec/02_data_contract.yaml:data_rule" - "spec/02_data_contract.yaml:quant_feed_contract.data_completeness_gate" - "spec/12_field_dictionary.yaml:field_dictionary.policy" required_inputs: ["normalized_request", "raw_data_sources"] computed_outputs: ["data_completeness_matrix", "missing_fields", "field_unit_conflicts"] pass_condition: "data_completeness_matrix produced" fail_state: "INSUFFICIENT_DATA" HARD_FILTER_CHECK: purpose: "하드 필터를 점수보다 먼저 적용" required_refs: - "spec/08_scoring_rules.yaml:hard_filters" - "spec/risk/aggregate_risk.yaml:risk_control.aggregate_risk_cap" - "spec/risk/circuit_breakers.yaml:risk_control.weekly_circuit_breaker" - "spec/13_formula_registry.yaml:formula_registry.formulas.TOTAL_HEAT_V1" required_inputs: ["data_completeness_matrix", "account_snapshot", "total_asset"] computed_outputs: ["hard_filter_results", "total_heat_pct", "blocked_actions"] pass_condition: "no blocking hard filter for requested action" fail_state: "BLOCKED" MARKET_REGIME_CHECK: purpose: "Risk-On/Neutral/Risk-Off 및 현금 목표 확인" required_refs: - "spec/risk/market_risk_cash.yaml:risk_control.market_risk_score_based_cash" - "spec/strategy/entry_core.yaml:entry_timing_guardrails.regime_based_entry" - "spec/13_formula_registry.yaml:formula_registry.formulas.MARKET_RISK_SCORE_V1" - "spec/13_formula_registry.yaml:formula_registry.formulas.TARGET_CASH_PCT_V1" required_inputs: ["vix_close", "kospi_close", "kospi_ma20", "usd_krw", "usd_jpy_2d_change_pct", "credit_stress_status"] computed_outputs: ["market_risk_score", "target_cash_pct", "market_regime_state"] pass_condition: "market regime classified or marked UNKNOWN" fail_state: "INSUFFICIENT_DATA" STRATEGY_SCORING: purpose: "섹터/종목/수급/유동성/실적/밸류 점수 산출" required_refs: - "spec/08_scoring_rules.yaml:strategy_score" - "spec/strategy/sector_model.yaml:sector_model" - "spec/13_formula_registry.yaml:formula_registry.formulas.FLOW_CREDIT_V1" required_inputs: ["close_price", "ma20", "flow_ok", "flow_rows", "frg_5d_sh", "inst_5d_sh", "avg_trade_value_5d"] computed_outputs: ["flow_credit", "strategy_score", "component_scores", "grade_candidate"] pass_condition: "strategy_score calculated or missing fields listed" fail_state: "INSUFFICIENT_DATA" PORTFOLIO_CONSTRAINT_CHECK: purpose: "계좌·비중·중복노출·현금·납입한도 확인" required_refs: - "spec/10_portfolio_rules.yaml" - "spec/risk/portfolio_exposure.yaml:portfolio_exposure_framework" required_inputs: ["target_cash_pct", "available_cash", "total_asset", "current_exposures", "account_scope"] computed_outputs: ["portfolio_fit_score", "cash_floor_status", "exposure_limit_amounts"] pass_condition: "portfolio constraints pass or downgrade action selected" fail_state: "BLOCKED" cash_shortfall_resolution: # [2026-05-19_HARNESS_AUDIT_V1] H2 rule_id: "CSR001" purpose: > 현금 < target_cash_pct 확인 시 BUY를 결정론적으로 차단하고 SELL_PRIORITY 엔진으로 TRIM 수량을 자동 배정한다. LLM 임의 배정 금지. trigger_condition: "CASH_RATIOS_V1.current_cash_pct < TARGET_CASH_PCT_V1.target_cash_pct" steps: step_1_block_buy: action: "모든 BUY 신호 → IGNORE 전환. 이유 불문, LLM 재해석 금지." output_key: "buy_gate_status = CASH_BLOCKED" step_2_calc_shortfall: formula: "cash_shortfall_krw = max(0, (target_cash_pct/100 - current_cash_pct/100) × total_asset)" source: "CASH_RATIOS_V1 결과 기반 — LLM 직접 계산 금지" output_key: "cash_shortfall_krw" step_3_assign_trim: source: "sell_priority_engine (spec/risk/portfolio_exposure.yaml)" algorithm: > SELL_PRIORITY 1순위 종목부터 계단식 TRIM 수량 배정. 잔여 부족액이 남으면 2순위로 이동하며 반복. formula: "trim_qty = min(floor(remaining_shortfall_krw / current_price), holding_quantity)" cascade: "remaining_shortfall_krw -= trim_qty × current_price → 다음 순위" output_tag: "[CASH_RAISE_AUTO: {종목} {qty}주 TRIM → 예상확보 {proceeds}원]" step_4_output_required: fields: - "cash_shortfall_krw (부족액)" - "trim_assignments: [{종목, qty, expected_proceeds}] (배정 목록)" - "post_trim_cash_pct (TRIM 후 예상 현금비중)" enforcement: - "수동 배정·서사적 배정 금지 — 공식 결과만 기재" - "trim_assignments 없이 현금 부족 해소 주장 금지" - "QEH_AUDIT_BLOCK.SELL_PRIORITY_V1 행에 배정 결과 요약 필수" - "1순위 소진 전 2순위 배정 금지 (sell_priority_engine 순서 준수)" POSITION_SIZING: purpose: "ATR20·현금·목표비중·유동성으로 정수 수량 산출" required_refs: - "spec/05_position_sizing.yaml:position_sizing" - "spec/13_formula_registry.yaml:formula_registry.formulas.RISK_BUDGET_CASCADE_V1" - "spec/13_formula_registry.yaml:formula_registry.formulas.POSITION_SIZE_V1" required_inputs: ["total_asset", "atr20", "entry_price", "available_cash", "exposure_limit_amounts", "bayesian_confidence_multiplier"] computed_outputs: ["final_risk_budget", "atr_quantity", "cash_limit_quantity", "target_weight_limit_quantity", "sector_limit_quantity", "liquidity_limit_quantity", "final_quantity"] pass_condition: "integer quantity calculated, or NO_QUANTITY reason emitted" fail_state: "INSUFFICIENT_DATA" EXIT_POLICY_CHECK: purpose: "손절/익절/trailing/보유주 점검 규칙 적용" required_refs: - "spec/exit/stop_loss.yaml:stop_loss" - "spec/exit/take_profit.yaml:take_profit" - "spec/exit/position_review.yaml:position_review_cycle" - "spec/13_formula_registry.yaml:formula_registry.formulas.EXPECTED_EDGE_V1" required_inputs: ["entry_price", "stop_price", "target_price", "final_quantity"] computed_outputs: ["expected_edge", "stop_order", "take_profit_order", "invalidation_conditions"] pass_condition: "exit or hold policy evaluated" fail_state: "INSUFFICIENT_DATA" proactive_exit_radar_check: # [2026-05-19_PROACTIVE_RADAR_V1] purpose: > 보유 포지션 분석 시 항상 실행. 가격이 멀쩡할 때도 수급·유동성·섹터 신호로 선제 매도 적기를 감지한다. '매도가 예술이다' 원칙 구현. required_refs: - "spec/exit/proactive_exit_radar.yaml" - "spec/13_formula_registry.yaml:DIVERGENCE_SCORE_V1" - "spec/13_formula_registry.yaml:OVERHANG_PRESSURE_V1" - "spec/13_formula_registry.yaml:SECTOR_ROTATION_RADAR_V1" required_inputs: - "frg_5d_sh, inst_5d_sh, flow_credit (W1)" - "frg_5d_sh, frg_20d_sh, volume, avg_volume_5d (W2)" - "sector_flow.SmartMoney_5D_Norm_Score, sector_flow.Rank (W3)" computed_outputs: - "divergence_score + W1_status" - "overhang_score + W2_status" - "W3_status" - "radar_composite_status (PASS/CAUTION/ALERT/CRITICAL_ALERT)" - "PROACTIVE_RADAR_BLOCK (보고서 출력용)" hard_rules: - "ALERT 이상 발화 시 sell_priority_engine 실행 필수" - "CRITICAL_ALERT 시 코어 포지션 포함 전면 재검토 강제" - "LLM이 레이더 결과를 완화하는 서사 출력 금지 (Section B 해설만 허용)" - "RADAR_MISSING(데이터 부족) 시 soft-block: 보유 포지션 수동 점검 권고 문구 출력" OUTPUT_VALIDATION: purpose: "JSON Schema와 HTS 표 필드 검증" required_refs: - "schemas/output_schema.json" - "spec/07_output_schema.yaml" required_inputs: ["final_action", "orders", "scores", "position_sizing", "triggered_rules", "missing_data"] computed_outputs: ["schema_validation_status", "prohibited_calculations"] pass_condition: "all required output fields populated or prohibited_calculations filled" fail_state: "INSUFFICIENT_DATA" FINAL_DECISION: purpose: "BUY/HOLD/SELL/TRIM/ROTATE/AVOID/WATCH/INSUFFICIENT_DATA 중 하나로 결론" required_refs: - "spec/07_output_schema.yaml:recommendation_grade" output_required: - "final_action" - "grade" - "orders or prohibited_calculations" - "rules_used" INSUFFICIENT_DATA: purpose: "데이터 부족으로 산출 불가. 다음 확인 출처를 제시." output_required: - "missing_fields" - "prohibited_calculations" - "next_source_to_check" BLOCKED: purpose: "하드 필터 또는 리스크 정책으로 행동 차단." output_required: - "triggered_rules" - "blocked_action" - "allowed_alternative" transitions: - from: "INPUT_VALIDATION" to: "DATA_COMPLETENESS_CHECK" condition: "minimum request context exists" - from: "INPUT_VALIDATION" to: "INSUFFICIENT_DATA" condition: "account/request context missing and cannot be inferred" - from: "DATA_COMPLETENESS_CHECK" to: "HARD_FILTER_CHECK" condition: "data_completeness_matrix produced" - from: "DATA_COMPLETENESS_CHECK" to: "INSUFFICIENT_DATA" condition: "matrix cannot be produced" - from: "HARD_FILTER_CHECK" to: "BLOCKED" condition: "blocking hard_filter failed" - from: "HARD_FILTER_CHECK" to: "MARKET_REGIME_CHECK" condition: "no blocking hard_filter failed" - from: "MARKET_REGIME_CHECK" to: "STRATEGY_SCORING" condition: "regime classified or UNKNOWN with caution" - from: "STRATEGY_SCORING" to: "PORTFOLIO_CONSTRAINT_CHECK" condition: "strategy score calculated or downgrade reason emitted" - from: "PORTFOLIO_CONSTRAINT_CHECK" to: "BLOCKED" condition: "cash_floor, duplicate exposure, account limit, or Total_Heat blocks requested action" - from: "PORTFOLIO_CONSTRAINT_CHECK" to: "POSITION_SIZING" condition: "requested action requires quantity" - from: "PORTFOLIO_CONSTRAINT_CHECK" to: "EXIT_POLICY_CHECK" condition: "requested action is hold/trim/sell review" - from: "POSITION_SIZING" to: "EXIT_POLICY_CHECK" condition: "quantity calculated or NO_QUANTITY reason emitted" - from: "EXIT_POLICY_CHECK" to: "OUTPUT_VALIDATION" condition: "order/hold/watch decision prepared" - from: "OUTPUT_VALIDATION" to: "FINAL_DECISION" condition: "schema and required output fields valid" - from: "OUTPUT_VALIDATION" to: "INSUFFICIENT_DATA" condition: "required output fields missing without prohibited_calculations reason" global_prohibitions: - "HARD_FILTER_CHECK 이전에 BUY 결론 출력 금지" - "POSITION_SIZING 이전에 정수 주문수량 출력 금지" - "OUTPUT_VALIDATION 실패 상태에서 즉시 실행 플레이북 출력 금지" - "BLOCKED 상태를 WATCH로 미화 금지. 차단 사유를 명시한다."