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로 미화 금지. 차단 사유를 명시한다."
