meta:
  title: "은퇴자산포트폴리오 — 점수화·하드필터 명세"
  parent_file: "RetirementAssetPortfolio.yaml"
  version: "2026-05-16-F4_peg_scoring"
  language: "ko-KR"
  timezone: "Asia/Seoul"
  role: "derived_adapter"
  has_code_implementation: true
  code_path: ["formulas/score_thresholds_v1.py", "src/quant_engine/qualitative_sell_strategy_v1.py"]
  purpose: >
    흩어진 점수화 규칙을 LLM이 일관되게 적용하도록 rule_id 기반으로 재정리한 명세.
    기존 strategy/risk/data 규칙을 대체하지 않고, 판단 근거를 구조화해 연결한다.
  authority_rule: "이 파일은 rule_id와 적용 순서를 제공하는 adapter다. 수치 임계값 충돌 시 canonical_ref 대상 파일을 우선한다."

scoring_policy:
  source_priority:
    1: "hard_filters — 실패 시 점수와 무관하게 BUY 금지"
    2: "risk_adjustments — 점수 산출 후 등급/수량을 보수적으로 조정"
    3: "strategy_score — 종목·섹터·진입 품질 평가"
    4: "portfolio_fit_score — 계좌·비중·중복 노출 적합성 평가"
  output_rule:
    - "모든 BUY/HOLD/SELL/AVOID 판단은 사용한 rule_id를 rules_used에 기록한다."
    - "점수는 결론의 보조 근거이며 hard_filter를 override하지 못한다."
    - "데이터 누락 축은 지정된 missing_rule에 따라 0점 또는 중립점으로 처리하고, 누락 필드는 별도 출력한다."

hard_filters:
  - id: "HF001_DATA_MATRIX_REQUIRED"
    name: "데이터 완성도 매트릭스 필수"
    condition: "data_completeness_matrix exists AND all_required_status_fields populated"
    fail_action: "INSUFFICIENT_DATA"
    canonical_ref: "spec/02_data_contract.yaml:quant_feed_contract.data_completeness_gate"
  - id: "HF002_ATR20_REQUIRED_FOR_QUANTITY"
    name: "ATR20 없으면 정수 매수수량 금지"
    condition: "ATR20_Status == OK before buy quantity calculation"
    fail_action: "NO_QUANTITY"
    canonical_ref: "spec/05_position_sizing.yaml:position_sizing.volatility_targeting.requirements"
  - id: "HF003_HOLDINGS_REQUIRED_FOR_SELL_QTY"
    name: "보유수량 없으면 매도수량 금지"
    condition: "confirmed_holding_quantity exists before sell quantity output"
    fail_action: "NO_SELL_QUANTITY"
    canonical_ref: "spec/07_output_schema.yaml:output_format.execution_guardrail.order_quantity_4stage_gate.stage_3"
  - id: "HF004_FLOW_ROWS_20D_REQUIRED_FOR_A"
    name: "20D 수급 기반 A등급 최소 행수"
    condition: "Flow_Rows >= 20 when using 20D flow for A grade"
    fail_action: "MAX_GRADE_B"
    canonical_ref: "spec/02_data_contract.yaml:quant_feed_contract.investor_flow_rules.caution"
  - id: "HF005_TOTAL_HEAT_HARD_BLOCK"
    name: "Total_Heat 10% 이상 신규매수 차단"
    condition: "Total_Heat < 10"
    fail_action: "AVOID_NEW_BUY"
    canonical_ref: "spec/risk/aggregate_risk.yaml:risk_control.aggregate_risk_cap.threshold.hard_block"
  - id: "HF006_BUY_ORDER_SET_REQUIRED"
    name: "매수 주문 6개 필드 세트 필수"
    condition: "limit_price AND quantity AND stop_price AND stop_quantity AND take_profit_price AND take_profit_quantity"
    fail_action: "INVALID_BUY_OUTPUT"
    canonical_ref: "spec/07_output_schema.yaml:output_format.buy_proposal_template.validation"
  # ── 재무 건전성 하드필터 (2026-05-18_FINANCIAL_HEALTH_V1) ────────────────────
  - id: "HF007_OPERATING_LOSS_BLOCK"
    name: "영업적자 종목 A등급 차단"
    condition: "operating_margin_pct < 0 AND grade == 'A'"
    fail_action: "MAX_GRADE_B"
    rationale: >
      영업이익이 음수인 종목은 본업 경쟁력 상실 상태.
      수급·모멘텀이 강해도 A등급 부여 금지. B등급까지만 허용.
    exception: "operating_margin_pct == DATA_MISSING → 필터 미발동 (데이터 부재 ≠ 적자)"
    canonical_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.FINANCIAL_HEALTH_SCORE_V1"
  - id: "HF008_EXTREME_LEVERAGE_WARNING"
    name: "극단 부채비율 경고"
    condition: "debt_to_equity >= 400 AND sector_type NOT IN ['bank', 'insurance', 'securities']"
    fail_action: "ADD_WARNING_FLAG: EXTREME_LEVERAGE"
    rationale: >
      D/E >= 400%는 재무 구조 임계치. 차단이 아닌 경고 플래그 발동.
      에이전트는 출력 시 [주의: 극단 부채비율] 레이블을 반드시 표기.
    exception: "금융업(은행·보험·증권)은 레버리지 비즈니스 특성상 제외"
    canonical_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.FINANCIAL_HEALTH_SCORE_V1"
  - id: "HF009_OVEREXTENSION_BLOCK"
    name: "이격도 과열 진입 차단 (Anti-Peak)"
    condition: "current_price / ma20 <= 1.15"
    caution_condition: "1.10 <= current_price / ma20 < 1.15"
    fail_action: "BUY_HARD_BLOCK [MRG001]"  # [2026-05-19_ALPHA_SHIELD_V1]
    caution_action: "BUY_CAUTION [MRG001_SOFT] -- 신규 매수 강도 50% 이하로 축소"
    canonical_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.MEAN_REVERSION_GATE_V1"
    rationale: >
      주가가 20일선 대비 15% 이상 급등한 상태는 '상투' 위험이 극도로 높음.
      수급 점수가 100점이어도 신규 진입을 하드 블록하여 추격 매수를 원천 봉쇄함.

strategy_score:
  id: "SS001_SECTOR_MODEL_SCORE"
  max_score: 100
  formula: "price_strength + volume_quality + flow_quality + earnings_revision + macro_regime + valuation + financial_health"
  executable_formula_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.FLOW_CREDIT_V1"
  field_dictionary_ref: "spec/12_field_dictionary.yaml:field_dictionary"
  canonical_ref: "spec/strategy/sector_model.yaml:sector_model.score_axes_formula"
  components:
    price_strength:
      max_points: 20  # [2026-05-18_FINANCIAL_HEALTH_V1] 25→20 (-5. 재무건전성 축 신설로 재배분)
      rule_id: "SS001_P"
      scoring: "섹터 내 1M 상대강도 상위 30% 이내=20, 30~60%=12, 60% 초과=0"
    volume_quality:
      max_points: 10  # [2026-05-18_FINANCIAL_HEALTH_V1] 15→10 (-5)
      rule_id: "SS001_V"
      scoring: "5D 거래대금/20D 평균 >=120%=10, 80~120%=6, 미만=0"
    flow_quality:
      max_points: 20  # [2026-05-18_FINANCIAL_HEALTH_V1] 25→20 (-5)
      rule_id: "SS001_F"
      scoring: "flow_credit >=0.70=20, 0.40~0.70=10, <0.40=0"
    earnings_revision:
      max_points: 15  # [2026-05-18_FINANCIAL_HEALTH_V1] 20→15 (-5. EPS 방향성은 재무건전성 축에 흡수)
      rule_id: "SS001_E"
      scoring: "EPS 컨센서스 상향=15, 유지=8, 하향=0"
    macro_regime:
      max_points: 10
      rule_id: "SS001_M"
      scoring: "Risk-On=10, Neutral=5, Risk-Off=0"
    valuation:
      max_points: 5
      rule_id: "SS001_VAL"
      scoring: "PER/PBR 섹터 평균 이하=5, 평균~1.5배=2, 1.5배 초과=0"
    financial_health:
      max_points: 20
      rule_id: "SS002_FHS"
      formula_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.FINANCIAL_HEALTH_SCORE_V1"
      scoring: >
        ROE(8pt) + 영업이익률(7pt) + 부채비율(5pt) + FCF 양부(5pt).
        데이터 전체 결측 시 8pt 중립. 코스닥 결측 시 6pt. 영업적자 시 0pt + HF007.
        ROE 음수 시 -5pt 페널티(총점 음수 가능 — clamp -5~20).
      purpose: >
        수급·모멘텀이 강해도 재무가 취약하면 점수가 낮아져 등급이 하향된다.
        특히 ROE<0(적자), 영업적자, D/E>400% 종목을 수급 강세로 오진하는
        모멘텀 편향을 정량적으로 차단한다.
      score_vs_momentum_note: >
        재배분 전: 수급/모멘텀 65점, 재무 0점.
        재배분 후: 수급/모멘텀 50점(-15), 재무 20점(+20), 총 100점 유지.
        (earnings_revision은 방향성 플래그 성격 — 재무 건전성과 별도 축 유지)

      # [proposal_96 / 2026-05-16] 코스닥 종목은 PEG 기반 밸류에이션 점수 적용 (최대 12점)
      kosdaq_override:
        applicable: "코스닥 종목에만 적용. 기존 SS001_VAL 점수를 대체."
        max_points: 12
        rule_id: "SS001_VAL_KOSDAQ_PEG"
        formula_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.PEG_SCORE_V1"
        scoring:
          peg_pass_1_0_or_below:    12
          peg_pass_1_0_to_1_5:      9
          peg_caution_1_5_to_2_0:   5
          peg_caution_2_0_to_2_5:   2
          peg_reject_above_2_5:      0
        fallback_scoring:
          per_below_2x_median:       9
          per_2x_to_3x_median:       4
          per_above_3x_median:       0
        scoring_note: "PEG 기반 12점 체계로 총점 범위가 코스닥 107점, KOSPI 100점이 됨. grade_thresholds는 시장 구분 없이 100점 환산 비례 적용."

  executable_rules:
    - id: "SS001_P_PRICE_STRENGTH"
      component: "price_strength"
      input_field: "relative_strength_1m_percentile"
      data_source: "core_satellite 탭 RS_Pct_20D (2026-05-17 추가). 100-RS_Pct_20D를 percentile로 사용. 예) RS_Pct_20D=80 → relative_strength_1m_percentile=20"
      output_field: "price_strength_score"
      max_points: 20  # [2026-05-18_FINANCIAL_HEALTH_V1] 25→20
      rules:
        - {if: "relative_strength_1m_percentile <= 30", points: 20}
        - {if: "30 < relative_strength_1m_percentile <= 60", points: 12}
        - {if: "relative_strength_1m_percentile > 60", points: 0}
      missing_action: 0
    - id: "SS001_V_VOLUME_QUALITY"
      component: "volume_quality"
      input_fields: ["avg_trade_value_5d", "avg_trade_value_20d"]
      output_field: "volume_quality_score"
      max_points: 10  # [2026-05-18_FINANCIAL_HEALTH_V1] 15→10
      derived_field:
        name: "volume_surge_ratio"
        expression: "avg_trade_value_5d / avg_trade_value_20d"
      rules:
        - {if: "volume_surge_ratio >= 1.20", points: 10}
        - {if: "0.80 <= volume_surge_ratio < 1.20", points: 6}
        - {if: "volume_surge_ratio < 0.80", points: 0}
      missing_action: 0
    - id: "SS001_F_FLOW_QUALITY"
      component: "flow_quality"
      formula_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.FLOW_CREDIT_V1"
      input_field: "flow_credit"
      output_field: "flow_quality_score"
      max_points: 20  # [2026-05-18_FINANCIAL_HEALTH_V1] 25→20
      rules:
        - {if: "flow_credit >= 0.70", points: 20}
        - {if: "0.40 <= flow_credit < 0.70", points: 10}
        - {if: "flow_credit < 0.40", points: 0}
      missing_action: 0
    - id: "SS001_E_EARNINGS_REVISION"
      component: "earnings_revision"
      input_field: "eps_revision_status"
      output_field: "earnings_revision_score"
      max_points: 15  # [2026-05-18_FINANCIAL_HEALTH_V1] 20→15
      rules:
        - {if: "eps_revision_status == 'UP'", points: 15}
        - {if: "eps_revision_status == 'FLAT'", points: 8}
        - {if: "eps_revision_status in ['DOWN', 'DATA_MISSING']", points: 0}
      missing_action: 0
    - id: "SS001_M_MACRO_REGIME"
      component: "macro_regime"
      input_field: "market_regime_state"
      output_field: "macro_regime_score"
      max_points: 10
      rules:
        - {if: "market_regime_state in ['RISK_ON', 'LEADER_CONCENTRATION']", points: 10}
        - {if: "market_regime_state == 'NEUTRAL'", points: 5}
        - {if: "market_regime_state in ['RISK_OFF', 'EVENT_SHOCK', 'UNKNOWN']", points: 0}
      missing_action: 0
    - id: "SS001_VAL_VALUATION"
      component: "valuation"
      input_fields: ["forward_pe", "sector_median_forward_pe", "pbr", "sector_median_pbr"]
      data_source:
        forward_pe: "data_feed 탭 Forward_PE"
        pbr: "data_feed 탭 PBR"
        sector_median_forward_pe: "sector_flow 탭 Sector_Median_PE (2026-05-17 추가)"
        sector_median_pbr: "sector_flow 탭 Sector_Median_PBR (2026-05-17 추가)"
      output_field: "valuation_score"
      max_points: 5
      rules:
        - {if: "forward_pe <= sector_median_forward_pe OR pbr <= sector_median_pbr", points: 5}
        - {if: "forward_pe <= sector_median_forward_pe * 1.5 OR pbr <= sector_median_pbr * 1.5", points: 2}
        - {if: "forward_pe > sector_median_forward_pe * 1.5 AND pbr > sector_median_pbr * 1.5", points: 0}
      missing_action: 0
    # [proposal_96 / 2026-05-16] 코스닥 전용 PEG 기반 밸류에이션 점수 계산 규칙
    - id: "SS001_VAL_KOSDAQ_PEG"
      component: "valuation"
      applicable: "코스닥 종목에만 적용 — SS001_VAL_VALUATION 대체"
      input_fields: ["forward_pe", "eps_growth_3y_cagr_pct", "sector_median_forward_pe"]
      output_field: "valuation_score"
      max_points: 12
      derived_field:
        name: "peg"
        expression: "forward_pe / eps_growth_3y_cagr_pct"
        missing_condition: "eps_growth_3y_cagr_pct == DATA_MISSING OR eps_growth_3y_cagr_pct <= 0"
      rules:
        primary_peg:
          - {if: "peg <= 1.0",              points: 12}
          - {if: "1.0 < peg <= 1.5",        points: 9}
          - {if: "1.5 < peg <= 2.0",        points: 5}
          - {if: "2.0 < peg <= 2.5",        points: 2}
          - {if: "peg > 2.5",               points: 0}
        fallback_per_only:
          - {if: "forward_pe <= sector_median_forward_pe * 2.0", points: 9}
          - {if: "forward_pe <= sector_median_forward_pe * 3.0", points: 4}
          - {if: "forward_pe >  sector_median_forward_pe * 3.0", points: 0}
      missing_action: 0
      output_note: "peg_gate_result도 동시 출력 (PASS/CAUTION/REJECT). formula_registry.PEG_SCORE_V1 참조."
    - id: "SS002_FHS_FINANCIAL_HEALTH"
      component: "financial_health"
      formula_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.FINANCIAL_HEALTH_SCORE_V1"
      input_fields: ["roe_pct", "operating_margin_pct", "debt_to_equity", "fcf_b", "sector_type"]
      output_field: "financial_health_score"
      max_points: 20
      sub_components:
        profitability_pts:
          field: "roe_pct"
          rules:
            - {if: "roe_pct >= 15", points: 8}
            - {if: "10 <= roe_pct < 15", points: 5}
            - {if: "5 <= roe_pct < 10", points: 2}
            - {if: "0 <= roe_pct < 5", points: 0}
            - {if: "roe_pct < 0", points: -5}
          missing_action: 4
        operating_efficiency_pts:
          field: "operating_margin_pct"
          rules:
            - {if: "operating_margin_pct >= 20", points: 7}
            - {if: "10 <= operating_margin_pct < 20", points: 4}
            - {if: "0 <= operating_margin_pct < 10", points: 2}
            - {if: "operating_margin_pct < 0", points: 0}
          missing_action: 3
        financial_stability_pts:
          field: "debt_to_equity"
          financial_sector_skip_value: 3
          rules:
            - {if: "debt_to_equity < 50", points: 5}
            - {if: "50 <= debt_to_equity < 100", points: 3}
            - {if: "100 <= debt_to_equity < 200", points: 1}
            - {if: "debt_to_equity >= 200", points: 0}
          missing_action: 2
        cash_generation_pts:
          field: "fcf_b"
          rules:
            - {if: "fcf_b > 0", points: 5}
            - {if: "fcf_b <= 0", points: 0}
          missing_action: 2
      expression: "clamp(profitability_pts + operating_efficiency_pts + financial_stability_pts + cash_generation_pts, -5, 20)"
      missing_action: "all_missing → 8pt (코스닥: 6pt)"
    - id: "SS001_TOTAL"
      output_field: "strategy_score"
      expression: "price_strength_score + volume_quality_score + flow_quality_score + earnings_revision_score + macro_regime_score + valuation_score + financial_health_score"
      max_points: 100  # 재무건전성 20pt 포함. 총합 여전히 100pt.
      normalization:
        note: "KOSDAQ 종목은 SS001_VAL 최대 12점으로 원점수 최대 107점. 등급 산출 전 100점으로 정규화."
        formula: "normalized_score = raw_score / (is_kosdaq ? 107 : 100) * 100"
        output_field: "SS001_Norm_Score"  # data_feed 탭 출력 컬럼
        grade_thresholds_apply_to: "SS001_Norm_Score (not SS001_Total)"
        example_kosdaq: "raw=85/107 → normalized=79.4 → grade=B (not A)"
      missing_action: "sum available scores; missing components score 0"


financial_health_gate:
  id: "FHG_RECOMMENDATION_ELIGIBILITY"
  purpose: >
    재무 건전성 점수(FINANCIAL_HEALTH_SCORE_V1)가 기준 미달인 종목을
    차세대 유망 종목(신규 매수 추천 후보)에서 배제한다.
    수급·모멘텀이 강해도 재무 부실 종목은 예외 없이 제외.
  input_field: "financial_health_score"
  thresholds:
    eligible:
      condition: "financial_health_score >= 10"
      status: "ELIGIBLE"
      label: "추천 가능"
      note: "신규 매수 후보 정상 처리. 다른 게이트(HF, PCL) 통과 시 최종 추천."
    watch_only:
      condition: "8 <= financial_health_score < 10"
      status: "WATCH_ONLY"
      label: "관찰 대상 (매수 금지)"
      note: >
        신규 매수 금지. 관찰 목록에만 등재.
        재무 지표 개선 확인 후 다음 분기에 재평가.
        보유 중 종목에는 적용 안 됨 — 보유 종목은 FTB(fundamental_thesis_break) 기준 적용.
    excluded:
      condition: "financial_health_score < 8"
      status: "EXCLUDED"
      label: "재무 부실 제외"
      note: >
        추천 목록 제외. 보유 중 종목은 FTB1~FTB4 매도 트리거와 연동.
        2분기 연속 EXCLUDED → fundamental_thesis_break 가중 검토.
  holding_degradation:
    purpose: "보유 중 종목의 재무 악화 누적 감지"
    condition: "financial_health_score < 8 (2분기 연속)"
    action: "fundamental_thesis_break 가중 — FTB 트리거 임계치 10% 완화 적용"
    xref: "spec/exit/stop_loss.yaml:stop_loss_rules.fundamental_thesis_break"
  missing_action: "financial_health_score == DATA_MISSING → WATCH_ONLY (결측 ≠ 양호)"
  exception: "없음 — 수급·모멘텀 강세를 이유로 FHG 우회 금지"
  canonical_ref: "spec/13_formula_registry.yaml:formula_registry.formulas.FINANCIAL_HEALTH_SCORE_V1"
grade_thresholds:
  canonical_ref: "spec/strategy/sector_model.yaml:sector_model.grade + spec/07_output_schema.yaml:recommendation_grade"
  A:
    min_score: 80
    required:
      - "hard_filters all pass"
      - "raw data confirmation >= 80%"
      - "Expected_Edge >= 1.5 and net_rr >= 2:1 when applicable"
      - "integer quantity calculable"
  B:
    score_range: "65~79 또는 가격/데이터 일부 대기"
  C:
    score_range: "50~64 또는 핵심 데이터 일부 누락"
  D:
    score_range: "<50 또는 hard_filter fail"

portfolio_fit_score:
  id: "PFS001_PORTFOLIO_FIT"
  max_score: 100
  formula: "account_fit*0.20 + concentration_fit*0.30 + duplicate_exposure_fit*0.25 + cash_fit*0.25"
  components:
    account_fit:
      scoring: "계좌 세제/납입/자산위치 적합=100, 조건부=60, 부적합=0"
      canonical_ref: "spec/01_objective_profile.yaml:account_policy"
    concentration_fit:
      scoring: "목표밴드 이내=100, +3%p 이내=70, +5%p 이내=40, 초과=0"
      canonical_ref: "spec/risk/portfolio_exposure.yaml:portfolio_exposure_framework.target_allocation_structure"
    duplicate_exposure_fit:
      scoring: "중복노출 없음=100, 관리 가능=60, 상한 초과=0"
      canonical_ref: "spec/risk/portfolio_exposure.yaml:portfolio_exposure_framework.duplicate_exposure_rule"
    cash_fit:
      scoring: "post_trade cash_floor 충족=100, review band=60, 미달=0"
      canonical_ref: "spec/risk/portfolio_exposure.yaml:portfolio_exposure_framework.cash_floor"
  executable_rules:
    - id: "PFS001_TOTAL"
      input_fields: ["account_fit_score", "concentration_fit_score", "duplicate_exposure_fit_score", "cash_fit_score"]
      output_field: "portfolio_fit_score"
      expression: "account_fit_score*0.20 + concentration_fit_score*0.30 + duplicate_exposure_fit_score*0.25 + cash_fit_score*0.25"
      missing_action: "missing component = 0"

risk_adjustments:
  - id: "RA001_RISK_POLICY_OVERRIDE"
    condition: "any hard stop or risk hard block triggered"
    action: "final_action cannot be BUY; grade max D or C according to data availability"
  - id: "RA002_DATA_STALE_DOWNGRADE"
    condition: "any required data_status == DATA_STALE"
    action: "new order quantity not calculated; final_action WATCH or INSUFFICIENT_DATA"
  - id: "RA003_EXPECTED_EDGE_FLOOR"
    condition: "Expected_Edge < 1.5 OR Expected_Edge missing"
    action: "A grade and immediate BUY prohibited"

reporting_requirement:
  score_table_columns:
    - "종목명"
    - "strategy_score"
    - "portfolio_fit_score"
    - "hard_filter_result"
    - "risk_adjustment"
    - "최종등급"
    - "사용 rule_id"
  prohibition:
    - "rule_id 없는 점수 근거 출력 금지"
    - "hard_filter 실패 종목을 total_score만으로 BUY 승격 금지"
# [Work 8 / AFL V2 권고 #1] timing=None CANDIDATE 진입 조건 강화
# 근거: alpha_lead_threshold_optimizer_v1 분석 결과
#   - timing=None CANDIDATE가 전체 5%+ 급등 미포착의 58%를 차지
#   - timing=None 종목은 alpha_lead만으로 CANDIDATE에 올라 진입 트리거 없음
# 적용: AGENTS.md Direction B1 PULLBACK_ENTRY_TRIGGER_V1 필수화
candidate_entry_conditions:
  timing_none_gate:
    rule_id: "CEC001_TIMING_NONE_PULLBACK_REQUIRED"
    condition: >
      lead_entry_state == CANDIDATE_ONLY AND timing == None (timing 조건 미산출 상태)
    required_additional_gate: PULLBACK_ENTRY_TRIGGER_V1
    gate_condition: >
      close <= MA20 * 1.03 (PULLBACK_ZONE)
      OR volume >= avgVol5d * 1.2 (거래량 확인 돌파)
    action_if_not_met: >
      CANDIDATE_ONLY 유지, PILOT_ALLOWED 격상 금지.
      timing=WAIT_PULLBACK_TRIGGER로 표기.
    rationale: >
      timing=None은 타이밍 신호 산출 불가 상태이므로 직전 MA20 근접 또는
      거래량 확인 조건을 추가로 요구한다. 이 조건 없이 alpha_lead만으로 진입하면
      급등 후 추격 매수가 돼 T5 정확도를 저하시킨다.
    spec_ref: "AGENTS.md Direction B1: PULLBACK_ENTRY_TRIGGER_V1"
    version: "2026-05-30_Work8"
