schema_version: formula_domain.v1
source: C:\Temp\data_feed\spec\13_formula_registry.yaml
domain: sector
meta:
  note: >
    governance/todo/v8_9_p1_adoption_plan.yaml P1-A.
    source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:sector_graph_engine_v8_9
formulas:
  SECTOR_EXPOSURE_GRAPH_V1:
    purpose: >
      섹터를 단일 텍스트 라벨이 아니라 L1:L2:L3:L4 canonical ID로 분류하고, ETF 구성종목을
      lookthrough하여 직접보유와 합산한 실질노출을 계산하며, AI/반도체/전력 등 테마 간
      중복 베타를 residualize해 과집중 진단의 이중계산을 막는다.
      (governance/todo/v8_9_p1_adoption_plan.yaml P1-A.1,
      source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:sector_graph_engine_v8_9)
    canonical_sector_id_format: 'L1:L2:L3:L4, 예: EQ:TECH:SEMIS:HBM'
    applicable: portfolio_exposure.concentration_caps_v8_9_supplement, PORTFOLIO_TRANSITION_UTILITY_V1.concentration_reduction_benefit_krw 계산 직전.
    inputs:
    - field: direct_weight_pct
      unit: percent
      note: 종목 직접보유 비중
    - field: etf_constituents_json
      unit: json
      note: 'ETF 구성종목 리스트 [{ticker, weight_pct}]. ETF_quality_gate(NAV_asof_valid, constituents_asof_valid) 통과 필요.'
    - field: etf_weight_pct
      unit: percent
      note: 해당 ETF의 포트폴리오 내 비중
    - field: sector_id
      unit: string
      note: canonical_sector_id_format 준수
    - field: peer_sector_betas
      unit: list_of_ratio
      note: 동일 macro_driver(AI_capex, semiconductor 등)를 공유하는 다른 섹터의 베타 목록
    expression:
      lookthrough_etf_weight_pct: "sum(constituent.weight_pct * etf_weight_pct / 100 for constituent in etf_constituents_json if constituent.sector_id == sector_id)"
      sector_family_total_pct: "direct_weight_pct + lookthrough_etf_weight_pct"
      factor_beta_residualized: "factor_beta_raw - sum(shared_variance_with(peer) for peer in peer_sector_betas if peer.macro_driver == self.macro_driver)"
    output:
      field: sector_family_total_pct
      unit: percent
    additional_outputs:
    - lookthrough_etf_weight_pct
    - factor_beta_residualized
    - theme_overlap_pct
    gates:
    - if: sector_family_total_pct > concentration_caps_v8_9_supplement.top3_combined_cap_pct.hard_cap_pct
      action: HARD_CONCENTRATION_BLOCK
    missing_policy:
      etf_constituents_json: ETF_BUY_BLOCKED — constituents_missing (v8.9 V89_016). lookthrough를 0으로 추정 금지.
      peer_sector_betas: factor_beta_residualized를 raw 값 그대로 사용하고 PARTIAL 표기. 0으로 추정 금지.
    canonical_ref: spec/risk/portfolio_exposure.yaml:duplicate_exposure_rule
    implementation: tools/build_sector_exposure_graph_v1.py
    owner: quant_team
    lifecycle_state: shadow
    input_fields:
    - direct_weight_pct
    - etf_constituents_json
    - etf_weight_pct
    - sector_id
    - peer_sector_betas
    output_fields:
    - sector_family_total_pct
    - lookthrough_etf_weight_pct
    - factor_beta_residualized
    golden_cases:
    - V89_044_sector_overlap
    - V89_045_ETF_direct_overlap
    activation_threshold:
      min_t20_sample: 30
    retirement_condition: performance_degradation
  LEADER_LIFECYCLE_GATE_V1:
    purpose: >
      종목의 시장 주도력을 CAPTAIN/CORE_LEADER/ENABLER/CYCLICAL_BETA/LAGGARD/DISTRIBUTION_RISK
      6개 role로 분류하고, 승급·강등 조건을 결정론적으로 평가한다. LLM이 '주도주라서 산다'는
      서사로 role을 임의 부여하는 것을 금지한다.
      (governance/todo/v8_9_p1_adoption_plan.yaml P1-A.2,
      source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:sector_graph_engine_v8_9.leader_lifecycle)
    applicable: SECTOR_EXPOSURE_GRAPH_V1 산출 직후. PORTFOLIO_TRANSITION_UTILITY_V1 candidate_action 생성 전.
    roles: [CAPTAIN, CORE_LEADER, ENABLER, CYCLICAL_BETA, LAGGARD, DISTRIBUTION_RISK]
    inputs:
    - field: relative_strength_leads_sector
      unit: boolean
    - field: volume_quality_confirmed
      unit: boolean
    - field: above_ma60_or_reclaim_confirmed
      unit: boolean
    - field: earnings_revision_status
      unit: 'enum: positive | neutral | negative'
    - field: institutional_flow_status
      unit: 'enum: accumulation | neutral | distribution'
    - field: current_role
      unit: enum
      note: 직전 평가에서 결정된 role. 최초 평가 시 LAGGARD로 시작.
    promotion_requires_all:
    - relative_strength_leads_sector == true
    - volume_quality_confirmed == true
    - above_ma60_or_reclaim_confirmed == true
    - earnings_revision_status != negative
    - institutional_flow_status != distribution
    demotion_triggers_any:
    - break_ma60_with_distribution: "above_ma60_or_reclaim_confirmed == false AND institutional_flow_status == distribution"
    - underperform_sector_20d: relative_strength_leads_sector == false (20거래일 연속)
    - earnings_revision_negative: earnings_revision_status == negative
    - crowded_flow_reversal: institutional_flow_status == distribution AND current_role IN [CAPTAIN, CORE_LEADER]
    role_transition_table:
      promotion_path: [LAGGARD, CYCLICAL_BETA, ENABLER, CORE_LEADER, CAPTAIN]
      demotion_path: [CAPTAIN, CORE_LEADER, ENABLER, CYCLICAL_BETA, LAGGARD, DISTRIBUTION_RISK]
      rule: 승급은 promotion_requires_all 충족 시 promotion_path 다음 단계로 1단계만 이동. 강등은 demotion_triggers_any 발동 시 즉시 DISTRIBUTION_RISK로 직행(단계적 강등 아님 — 보수적 원칙).
    output:
      field: leader_role
      unit: enum
    additional_outputs:
    - role_transition_reason
    - role_changed
    gates:
    - if: leader_role == DISTRIBUTION_RISK
      action: NEW_BUY_BLOCKED
    missing_policy: 입력 필드 중 하나라도 null이면 role 유지(current_role) + role_transition_reason=DATA_MISSING. 임의 승급/강등 금지.
    canonical_ref: spec/strategy/leader_scan.yaml
    implementation: tools/build_sector_exposure_graph_v1.py
    owner: quant_team
    lifecycle_state: shadow
    input_fields:
    - relative_strength_leads_sector
    - volume_quality_confirmed
    - above_ma60_or_reclaim_confirmed
    - earnings_revision_status
    - institutional_flow_status
    - current_role
    output_fields:
    - leader_role
    - role_transition_reason
    golden_cases:
    - V89_046_leader_distribution
    activation_threshold:
      min_t20_sample: 30
    retirement_condition: performance_degradation
