function safeStringifyForChecksum_(value) {
  var s = JSON.stringify(value);
  return (s === undefined || s === null) ? '' : s;
}


// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P2-2: YAML_GAS_COVERAGE_AUDIT_ENGINE_V1 (YGCA-V1)
// YAML 지침 ↔ GAS 함수 커버리지 감사 — settings 탭에 결과 기록
// ═══════════════════════════════════════════════════════════════════════

/**
 * auditYamlGasCoverage_
 * 필수 함수 목록과 실제 정의를 비교해 커버리지 % 산출.
 * GAS에서는 typeof 로 함수 존재 여부를 확인한다.
 */
function auditYamlGasCoverage_() {
  var REQUIRED = [
    // Stage 0
    { yaml: 'HARNESS_DATA_FRESHNESS_GATE_V1',         gs: 'calcHarnessDataFreshnessGate_' },
    { yaml: 'INTRADAY_ACTION_MATRIX_V1',              gs: 'calcIntradayLock_' },
    // Stage 1
    { yaml: 'FLOW_CREDIT_V1',                         gs: 'buildAllowedAction' },
    { yaml: 'TARGET_CASH_PCT_V1',                     gs: 'calcCashFloor_' },
    { yaml: 'TOTAL_HEAT_V1',                          gs: 'calcHarnessPortfolioGuardState_' },
    { yaml: 'CASH_SHORTFALL_V1',                      gs: 'calcCashShortfallHarness_' },
    { yaml: 'CASH_RECOVERY_OPTIMIZER_V1',             gs: 'calcCashPreservationPlan_' },
    // Stage 2
    { yaml: 'POSITION_SIZE_V1',                       gs: 'calcQuantities_' },
    { yaml: 'STOP_PRICE_CORE_V1',                     gs: 'calcPrices_' },
    { yaml: 'PROFIT_RATCHET_TIERED_V2',               gs: 'calcProfitPreservationRow_' },
    { yaml: 'TAKE_PROFIT_LADDER_V1',                  gs: 'calcTpQuantityLadder_' },
    // Stage 3
    { yaml: 'DISTRIBUTION_SELL_DETECTOR_V1',          gs: 'calcDistributionRiskRow_' },
    { yaml: 'DIVERGENCE_SCORE_V1',                    gs: 'calcSellConflictScore_' },
    { yaml: 'OVERHANG_PRESSURE_V1',                   gs: 'calcReboundHoldbackScore_' },
    { yaml: 'FLOW_ACCELERATION_V1',                   gs: 'calcAlphaShield_' },
    { yaml: 'PRE_DISTRIBUTION_EARLY_WARNING_V1',      gs: 'calcDistributionRiskRow_' },
    // Stage 4
    { yaml: 'ANTI_LATE_ENTRY_GATE_V2',                gs: 'calcAntiLateEntryGateV2_' },
    { yaml: 'PULLBACK_ENTRY_TRIGGER_V1',              gs: 'calcEntryTimingSignal_' },
    { yaml: 'BREAKOUT_QUALITY_GATE_V2',               gs: 'calcBreakoutQualityGate_' },
    { yaml: 'STAGED_ENTRY_TRANCHE_V1',                gs: 'calcCoreSatelliteExecutionState_' },
    // Stage 5
    { yaml: 'SELL_WATERFALL_ENGINE_V1',               gs: 'calcSmartCashRaiseV2_' },
    { yaml: 'SELL_EXECUTION_TIMING_V1',               gs: 'calcExitSellAction_' },
    { yaml: 'SELL_VALUE_PRESERVATION_TIERED_V2',      gs: 'calcCashPreservationSellEngineV2_' },
    { yaml: 'SELL_PRICE_SANITY_V1',                   gs: 'calcSellSignalSanityScore_' },
    { yaml: 'K2_STAGED_REBOUND_SELL_V1',              gs: 'calcAntiWhipsawGate_' },
    // Stage 6
    { yaml: 'TICK_NORMALIZER_V1',                     gs: 'tickNormalize_' },
    // Stage 7-8
    { yaml: 'RS_VERDICT_V2',                          gs: 'calcIndexRelativeHealthGate_' },
    { yaml: 'BENCHMARK_RELATIVE_TIMESERIES_V1',       gs: 'calcIndexRelativeHealthGate_' },
    { yaml: 'SATELLITE_ALPHA_QUALITY_GATE_V1',        gs: 'calcCoreCandidateQualityGrade_' },
    { yaml: 'SATELLITE_LIFECYCLE_GATE_V1',            gs: 'calcSatelliteLifecycleGate_' },
    { yaml: 'PORTFOLIO_CORRELATION_GATE_V1',          gs: 'calcPortfolioCorrelationGate_' },
    // Stage 9
    { yaml: 'LLM_SERVING_CONSTRAINT_V1',              gs: 'calcDeterministicServingLock_' },
    { yaml: 'DETERMINISTIC_ROUTING_ENGINE_V1',        gs: 'buildHarnessContext_' },
    // Portfolio risk
    { yaml: 'DRAWDOWN_GUARD_V1',                      gs: 'calcDrawdownGuard_' },
    { yaml: 'PORTFOLIO_BETA_GATE_V1',                 gs: 'calcPortfolioBetaGate_' },
    { yaml: 'SECTOR_CONCENTRATION_LIMIT_V1',          gs: 'calcSectorConcentrationGate_' },
    { yaml: 'POSITION_COUNT_LIMIT_V1',                gs: 'calcPositionCountLimit_' },
    { yaml: 'SINGLE_POSITION_WEIGHT_CAP_V1',          gs: 'calcSinglePositionWeightCap_' },
    { yaml: 'SEMICONDUCTOR_CLUSTER_GATE_V1',          gs: 'calcSemiconductorClusterGate_' },
    { yaml: 'PORTFOLIO_DRAWDOWN_GATE_V1',             gs: 'calcPortfolioDrawdownGate_' },
    { yaml: 'WIN_LOSS_STREAK_GUARD_V1',               gs: 'calcWinLossStreakGuard_' },
    // Alerts
    { yaml: 'STOP_BREACH_ALERT_V1',                   gs: 'calcStopBreachAlert_' },
    { yaml: 'RELATIVE_STOP_SIGNAL_V1',                gs: 'calcRelativeStopSignal_' },
    { yaml: 'TP_TRIGGER_ALERT_V1',                    gs: 'calcTpTriggerAlert_' },
    { yaml: 'HEAT_CONCENTRATION_ALERT_V1',            gs: 'calcHeatConcentrationAlert_' },
    { yaml: 'REGIME_TRANSITION_ALERT_V1',             gs: 'calcRegimeTransitionAlert_' },
    { yaml: 'PORTFOLIO_HEALTH_SCORE_V1',              gs: 'calcPortfolioHealthScore_' },
    // Proposal50 신규
    { yaml: 'EXPORT_GATE_V1',                         gs: 'calcExportGate_' },
    { yaml: 'ROUTING_TRACE_V1',                       gs: 'buildRoutingTrace_' },
    { yaml: 'WATCH_LEDGER_V1',                        gs: 'buildWatchLedger_' },
    { yaml: 'EXPERT_JUDGMENT_CONSENSUS_ENGINE_V1',    gs: 'calcExpertJudgmentConsensus_' },
    { yaml: 'SMART_CASH_RECOVERY_SELL_ENGINE_V2',     gs: 'calcSmartCashRecoverySell_' },
    { yaml: 'DETERMINISTIC_SERVING_LOCK_ENGINE_V1',   gs: 'calcDeterministicServingLock_' },
    { yaml: 'MACRO_REGIME_ADAPTIVE_GATE_V2',          gs: 'calcMacroRegimeAdaptiveGate_' },
    { yaml: 'MANDATORY_REDUCTION_PLAN_V1',            gs: 'calcMandatoryReductionPlan_' },
    // Proposal50 P0 Gap 해소 함수
    { yaml: 'VALIDATE_ORDER_CONDITION_V1',            gs: 'validateOrderCondition_' },
    { yaml: 'SHADOW_LEDGER_V1',                       gs: 'buildShadowLedger_' },
    { yaml: 'LLM_SERVING_CONSTRAINT_V1',              gs: 'calcLlmServingConstraint_' },
    { yaml: 'AVG_TRADE_VALUE_SIGNAL_V1',              gs: 'calcAvgTradeValueSignal_' },
    { yaml: 'TRIM_PLAN_MIN_CASH_V1',                  gs: 'calcTrimPlanMinCash_' },
    { yaml: 'PREDICTIVE_ALPHA_ENGINE_V1',             gs: 'calcPredictiveAlphaEngineV1_' },
    { yaml: 'MACRO_EVENT_SYNCHRONIZER_V1',            gs: 'calcMacroEventSynchronizerV1_' },
    { yaml: 'ANTI_LATE_ENTRY_GATE_V2',                gs: 'calcAntiLateEntryGateV2_' },
    { yaml: 'CONSISTENCY_VALIDATOR_V2',               gs: 'calcConsistencyValidatorV2_' },
    { yaml: 'SATELLITE_FAILURE_GATE_V1',              gs: 'calcSatelliteFailureGate_' },
    { yaml: 'SATELLITE_AGGREGATE_PNL_GATE_V1',        gs: 'calcSatelliteAggregatePnlGate_' },
    { yaml: 'CLA_REGIME_EXIT_CONDITION_V1',           gs: 'calcClaRegimeExitCondition_' },
    { yaml: 'EVENT_RISK_HOLD_GATE_V1',                gs: 'calcEventRiskHoldGate_' },
    { yaml: 'SECTOR_ROTATION_MOMENTUM_V1',            gs: 'calcSectorRotationMomentum_' },
    // Monthly Batch 피드백 루프
    { yaml: 'TRADE_QUALITY_SCORER_V1',                gs: 'calcTradeQualityScorer_' },
    { yaml: 'PATTERN_BLACKLIST_AUTO_V1',              gs: 'calcPatternBlacklistAuto_' },
    { yaml: 'ALPHA_FEEDBACK_LOOP_V1',                 gs: 'calcAlphaFeedbackLoop_' },
    // Proposal51 신규
    { yaml: 'SELL_PRICE_SANITY_V2',                   gs: 'calcSellPriceSanityV2_' },
    { yaml: 'EXPORT_GATE_V2',                         gs: 'calcExportGate_' },
    { yaml: 'SEMICONDUCTOR_CLUSTER_SYNC_V1',          gs: 'syncSemiconductorCluster_' },
    { yaml: 'PROACTIVE_SELL_RADAR_V2',                gs: 'calcProactiveSellRadarV2_' },
    { yaml: 'ANTI_LATE_ENTRY_GATE_V3',                gs: 'applyAlegGate4And5_' },
    { yaml: 'PRICE_HIERARCHY_LOCK_V1',                gs: 'applyPriceHierarchyLockAll_' },
    { yaml: 'DATA_QUALITY_GATE_V2',                   gs: 'calcDataQualityGateV2_' },
    { yaml: 'CASH_RECOVERY_DISPLAY_LOCK_V1',          gs: 'calcCashRecoveryDisplayLock_' },
    // Proposal53 신규
    { yaml: 'FUNDAMENTAL_QUALITY_GATE_V1',            gs: 'calcFundamentalQualityGateV1_' },
    { yaml: 'HORIZON_ALLOCATION_LOCK_V1',             gs: 'calcHorizonAllocationLockV1_' },
    { yaml: 'SMART_MONEY_LIQUIDITY_GATE_V1',          gs: 'calcSmartMoneyLiquidityGateV1_' },
    { yaml: 'ROUTING_SERVING_DECISION_TRACE_V2',      gs: 'buildRoutingServingTraceV2_' },
    { yaml: 'FUNDAMENTAL_MULTI_FACTOR_SCORE_V2',      gs: 'calcFundamentalMultiFactorScoreV2_' },
    { yaml: 'EARNINGS_GROWTH_QUALITY_GATE_V1',        gs: 'calcEarningsGrowthQualityGateV1_' },
    { yaml: 'MARKET_SHARE_MOMENTUM_PROXY_V1',         gs: 'calcMarketShareMomentumProxyV1_' },
    { yaml: 'CASHFLOW_STABILITY_GATE_V1',             gs: 'calcCashflowStabilityGateV1_' },
    { yaml: 'ROUTING_DECISION_EXPLAIN_LOCK_V1',       gs: 'calcRoutingExplainLockV1_' },
  ];

  var implemented = REQUIRED.filter(function(req) {
    try { return typeof eval(req.gs) === 'function'; } catch(e) { return false; }
  });
  // eval 대신 안전한 방법으로 확인 (GAS에서는 this 대신 globalThis 또는 eval 허용)
  // GAS 환경: 전역 함수 → typeof functionName 으로 확인 불가 → 이름 기반 hardlist 사용
  var IMPLEMENTED_HARDLIST = [
    'calcHarnessDataFreshnessGate_','calcIntradayLock_','buildAllowedAction',
    'calcCashFloor_','calcHarnessPortfolioGuardState_','calcCashShortfallHarness_',
    'calcCashPreservationPlan_','calcQuantities_','calcPrices_',
    'calcProfitPreservationRow_','calcTpQuantityLadder_','calcDistributionRiskRow_',
    'calcSellConflictScore_','calcReboundHoldbackScore_','calcAlphaShield_',
    'calcAntiLateEntryGateV2_','calcEntryTimingSignal_','calcBreakoutQualityGate_',
    'calcCoreSatelliteExecutionState_','calcSmartCashRaiseV2_','calcExitSellAction_',
    'calcCashPreservationSellEngineV2_','calcSellSignalSanityScore_','calcAntiWhipsawGate_',
    'tickNormalize_','calcIndexRelativeHealthGate_','calcCoreCandidateQualityGrade_',
    'calcSatelliteLifecycleGate_','calcPortfolioCorrelationGate_',
    'calcDeterministicServingLock_','buildHarnessContext_',
    'calcDrawdownGuard_','calcPortfolioBetaGate_','calcSectorConcentrationGate_',
    'calcPositionCountLimit_','calcSinglePositionWeightCap_','calcSemiconductorClusterGate_',
    'calcPortfolioDrawdownGate_','calcWinLossStreakGuard_',
    'calcStopBreachAlert_','calcTpTriggerAlert_','calcHeatConcentrationAlert_',
    'calcRegimeTransitionAlert_','calcPortfolioHealthScore_',
    'calcExportGate_','buildRoutingTrace_','buildWatchLedger_',
    'calcExpertJudgmentConsensus_','calcSmartCashRecoverySell_',
    'calcMacroRegimeAdaptiveGate_','calcMandatoryReductionPlan_',
    'validateOrderCondition_','buildShadowLedger_','calcLlmServingConstraint_',
    'calcAvgTradeValueSignal_','calcTrimPlanMinCash_',
    'applyAlegGate4And5_','applyDsdV1_1Signals_',
    'calcPredictiveAlphaEngineV1_','calcMacroEventSynchronizerV1_',
    'calcAntiLateEntryGateV2_','calcConsistencyValidatorV2_',
    'calcSatelliteFailureGate_','calcSatelliteAggregatePnlGate_',
    'calcClaRegimeExitCondition_','calcEventRiskHoldGate_',
    'calcSectorRotationMomentum_','calcAlphaShield_',
    'calcTradeQualityScorer_','calcPatternBlacklistAuto_','calcAlphaFeedbackLoop_',
    'calcRelativeStopSignal_',
    // Proposal51 신규
    'calcSellPriceSanityV2_','syncSemiconductorCluster_',
    'calcProactiveSellRadarV2_',
    'applyPriceHierarchyLockAll_','calcDataQualityGateV2_','calcCashRecoveryDisplayLock_',
    'calcFundamentalQualityGateV1_','calcHorizonAllocationLockV1_',
    'calcSmartMoneyLiquidityGateV1_','buildRoutingServingTraceV2_',
    'calcFundamentalMultiFactorScoreV2_','calcEarningsGrowthQualityGateV1_',
    'calcMarketShareMomentumProxyV1_','calcCashflowStabilityGateV1_',
    'calcRoutingExplainLockV1_',
  ];

  var implSet = {};
  IMPLEMENTED_HARDLIST.forEach(function(f) { implSet[f] = true; });

  var gaps = REQUIRED.filter(function(req) { return !implSet[req.gs]; });
  var implCount = REQUIRED.length - gaps.length;
  var coveragePct = Math.round(implCount / REQUIRED.length * 1000) / 10;

  var result = {
    total_required:  REQUIRED.length,
    implemented:     implCount,
    coverage_pct:    coveragePct,
    gaps:            gaps.map(function(g) { return { yaml: g.yaml, gs: g.gs }; }),
    coverage_label:  coveragePct >= 95 ? 'FULL'
      : coveragePct >= 80              ? 'HIGH'
      : coveragePct >= 60              ? 'MEDIUM'
      : 'LOW',
    formula_id:      'YAML_GAS_COVERAGE_AUDIT_ENGINE_V1'
  };

  Logger.log('[COVERAGE_AUDIT] ' + coveragePct + '% (' + implCount + '/' + REQUIRED.length + ')'
    + (gaps.length > 0 ? ' GAPS: ' + gaps.map(function(g){ return g.yaml; }).join(',') : ''));

  // settings 탭에 기록
  try {
    var ss = getSpreadsheet_();
    var sh = ss.getSheetByName('settings');
    if (sh) {
      var data = sh.getDataRange().getValues();
      var found = false;
      for (var i = 0; i < data.length; i++) {
        if (String(data[i][0]) === 'coverage_pct') {
          sh.getRange(i + 1, 2).setValue(coveragePct);
          found = true;
          break;
        }
      }
      if (!found) {
        sh.appendRow(['coverage_pct', coveragePct, 'YAML↔GAS 커버리지 %', new Date().toISOString()]);
      }
    }
  } catch(e) {
    Logger.log('[COVERAGE_AUDIT] settings 탭 기록 실패: ' + e.message);
  }

  return result;
}


// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P0-B: MACRO_REGIME_ADAPTIVE_GATE_V2 (MRAG-V2)
// 거시·이벤트 위험도 4레이어 → heat_gate_threshold / position_size_scale 동적 조정
// Direction ME2: effective_heat_gate_threshold = ME1 + MRAG-V2 중 더 엄격한 값
// ═══════════════════════════════════════════════════════════════════════

/**
 * calcMacroRegimeAdaptiveGate_
 * LAYER_1 미시(Market Internals) + LAYER_2 거시(Macro) + LAYER_3 글로벌 + LAYER_4 이벤트
 * total_mrag_score 0~100 → heat_gate_threshold / position_size_scale 결정론적 조정
 */
function calcMacroRegimeAdaptiveGate_(macroJson, mesResult, hApex) {
  return calcMacroRegimeAdaptiveGateV2Impl_(macroJson, mesResult, hApex);
}


// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P1-A: ANTI_LATE_ENTRY_GATE V2.1 — GATE_4/GATE_5 추가
// 뒷박 원천 차단 5게이트 완성 (기존 V2의 3게이트 → 5게이트)
// ═══════════════════════════════════════════════════════════════════════

/**
 * applyAlegGate4And5_
 * alegRows에 GATE_4(PAE연동) + GATE_5(블랙리스트) 추가.
 * Direction A2: BLOCK if ANY gate(1~5)=BLOCK
 */
function applyAlegGate4And5_(alegRows, paeRows, hApex) {
  return applyAlegGate4And5Impl_(alegRows, paeRows, hApex);
}



// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P1-B: DISTRIBUTION_SELL_DETECTOR V1.1 — SIG_7/SIG_8
// 설거지 신호 6개 → 8개, weighted_sum 임계값 5.0/3.0 상향
// ═══════════════════════════════════════════════════════════════════════

/**
 * applyDsdV1_1Signals_
 * dsdRows에 SIG_7/SIG_8 추가 적용.
 * Direction B3: weighted_sum >= 5.0 → DISTRIBUTION_CONFIRMED
 */
function applyDsdV1_1Signals_(dsdRows, dfMap) {
  (dsdRows || []).forEach(function(dsdRow) {
    var df     = dfMap[dsdRow.ticker] || {};
    var close_ = toNumber_(df['Close'] || df.close) || 0;
    var open_  = toNumber_(df['Open']  || df.open)  || 0;

    // SIG_7: 연속 양봉 후 음봉 반전 (w=1.5)
    var prev3Bull = df['Prev3D_AllBullish'] === true
      || String(df['Prev3D_AllBullish'] || '').toUpperCase() === 'TRUE';
    var todayBear = close_ < open_ && close_ > 0 && open_ > 0;
    var sig7 = prev3Bull && todayBear;
    dsdRow.sig_7_reversal = sig7;
    if (sig7) dsdRow.weighted_sum = (toNumber_(dsdRow.weighted_sum) || 0) + 1.5;

    // SIG_8: 개인집중유입 + 기관매도 (w=1.5) — 데이터 없으면 w=0
    var retailR = toNumber_(df['Retail_Buy_Ratio_5D'] || df.retail_buy_ratio_5d) || 0;
    var instS   = toNumber_(df['Inst_5D'] || df.inst_5d) || 0;
    var sig8    = retailR > 0.70 && instS < 0;
    dsdRow.sig_8_retail_inflow = sig8;
    if (sig8) dsdRow.weighted_sum = (toNumber_(dsdRow.weighted_sum) || 0) + 1.5;

    // V1.1 임계값 재적용
    var ws = toNumber_(dsdRow.weighted_sum) || 0;
    dsdRow.distribution_verdict = ws >= 5.0 ? 'DISTRIBUTION_CONFIRMED'
      : ws >= 3.0                            ? 'DISTRIBUTION_WARNING'
      : 'NO_SIGNAL';

    // 조기 경보 V2: (SIG_1 OR SIG_2) + RSI14 >= 70
    var rsi14 = toNumber_(df['RSI14'] || df.rsi14) || 0;
    dsdRow.early_warning_v2 = (dsdRow.sig_1 || dsdRow.sig_2) && rsi14 >= 70;
    dsdRow.dsd_version = 'V1.1';
  });
  return dsdRows;
}


// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P1-C: MANDATORY_REDUCTION_PLAN_V1
// 반도체 클러스터 한도 2배 초과 → 4주 의무 감축 계획 결정론적 산출
// ═══════════════════════════════════════════════════════════════════════

/**
 * calcMandatoryReductionPlan_
 * Direction O2: mandatory_reduction_json을 하네스 확정값으로 잠금.
 */
function calcMandatoryReductionPlan_(semiconductorClusterGate, holdings, dfMap, h3, totalAsset) {
  function toDateYmd_(v) {
    if (!v) return null;
    if (typeof v === 'string') return v.slice(0, 10);
    if (Object.prototype.toString.call(v) === '[object Date]' && !isNaN(v.getTime())) {
      return Utilities.formatDate(v, 'Asia/Seoul', 'yyyy-MM-dd');
    }
    return null;
  }

  // [PROPOSAL51-FIX] calcSemiconductorClusterGate_ 반환키는 combined_pct (cluster_pct 아님)
  var clusterPct   = toNumber_((semiconductorClusterGate || {}).combined_pct
                             || (semiconductorClusterGate || {}).cluster_pct) || 0;
  var clusterLimit = toNumber_((semiconductorClusterGate || {}).cap_pct
                             || (semiconductorClusterGate || {}).cluster_limit_pct) || 25;

  if (clusterPct <= clusterLimit * 2.0) {
    return { is_mandatory: false, cluster_pct: clusterPct, cluster_limit_pct: clusterLimit,
             formula_id: 'MANDATORY_REDUCTION_PLAN_V1' };
  }

  var excessPct          = clusterPct - clusterLimit;
  var weeklyReducPct     = Math.ceil(excessPct / 4 * 10) / 10;
  var weeklyReducKrw     = Math.round(totalAsset * weeklyReducPct / 100);
  var SEMI_TICKERS       = ['005930','000660','229200','091160'];
  var holdMap            = {};
  (holdings || []).forEach(function(h) { holdMap[h.ticker] = h; });
  var sellQtyMap         = {};
  ((h3 && h3.sellQty) || []).forEach(function(sq) { sellQtyMap[sq.ticker] = sq; });

  var reduction = [];
  // 1순위: RS_BROKEN
  (holdings || []).filter(function(h) {
    var df = dfMap[h.ticker] || {};
    return SEMI_TICKERS.indexOf(h.ticker) >= 0
      && String(df['RS_Verdict'] || df.rs_verdict || '').toUpperCase() === 'BROKEN';
  }).forEach(function(h) {
    reduction.push({ priority: 1, reason: 'RS_BROKEN', ticker: h.ticker, name: h.name || '',
      suggested_sell_qty: (sellQtyMap[h.ticker] || {}).sell_qty || null });
  });
  // 2순위: ETF
  (holdings || []).filter(function(h) {
    return SEMI_TICKERS.indexOf(h.ticker) >= 0
      && (h.name && (h.name.indexOf('KODEX') >= 0 || h.name.indexOf('TIGER') >= 0
                  || h.name.indexOf('ETF') >= 0 || h.ticker === '229200'));
  }).filter(function(h) { return !reduction.some(function(r) { return r.ticker === h.ticker; }); })
  .forEach(function(h) {
    reduction.push({ priority: 2, reason: 'ETF_PREFERRED', ticker: h.ticker, name: h.name || '',
      suggested_sell_qty: (sellQtyMap[h.ticker] || {}).sell_qty || null });
  });
  // 3순위: APEX_SUPER
  (holdings || []).filter(function(h) {
    var df = dfMap[h.ticker] || {};
    return SEMI_TICKERS.indexOf(h.ticker) >= 0
      && String(df['Profit_Lock_Stage'] || df.profit_lock_stage || '').toUpperCase() === 'APEX_SUPER';
  }).filter(function(h) { return !reduction.some(function(r) { return r.ticker === h.ticker; }); })
  .forEach(function(h) {
    reduction.push({ priority: 3, reason: 'APEX_SUPER_TRAILING', ticker: h.ticker, name: h.name || '',
      suggested_sell_qty: (sellQtyMap[h.ticker] || {}).sell_qty || null });
  });

  var completeDate = addBusinessDays_(new Date(), 20);  // 4주 × 5영업일

  return {
    is_mandatory:                true,
    cluster_pct:                 clusterPct,
    cluster_limit_pct:           clusterLimit,
    current_excess_pct:          Math.round(excessPct * 10) / 10,
    weekly_reduction_target_pct: weeklyReducPct,
    weekly_reduction_target_krw: weeklyReducKrw,
    weeks_to_normalize:          4,
    estimated_completion_date:   toDateYmd_(completeDate),
    reduction_priority:          reduction,
    formula_id:                  'MANDATORY_REDUCTION_PLAN_V1'
  };
}

// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL51] P0-C: SEMICONDUCTOR_CLUSTER_SYNC_V1
// cluster gate ↔ mandatory_reduction_plan 단일 소스 동기화
// ═══════════════════════════════════════════════════════════════════════

/**
 * syncSemiconductorCluster_
 * SEMICONDUCTOR_CLUSTER_SYNC_V1: cluster_gate ↔ mandatory_reduction_json 정합성 검증 및 자동 교정
 * - combined_pct > cap_pct * 2이면 is_mandatory=true 강제
 * - combined_pct <= cap_pct * 2이면 is_mandatory=false 강제
 * @param {Object} hApex — mandatory_reduction_json 포함
 * @return {{ status, corrected, before_is_mandatory, after_is_mandatory, cluster_pct, threshold_pct }}
 */
function syncSemiconductorCluster_(hApex) {
  var mrj = (hApex && hApex.mandatory_reduction_json) || {};
  var clusterPct  = toNumber_(mrj.cluster_pct)       || 0;
  var clusterLimit = toNumber_(mrj.cluster_limit_pct) || 25;
  var threshold    = clusterLimit * 2.0;
  var shouldBeMandatory = clusterPct > threshold;
  var wasMandatory      = mrj.is_mandatory === true;

  var syncStatus, corrected;
  if (shouldBeMandatory === wasMandatory) {
    syncStatus = 'SYNCED';
    corrected  = false;
  } else {
    syncStatus = 'CORRECTED';
    corrected  = true;
    // 인라인 교정
    mrj.is_mandatory = shouldBeMandatory;
    if (shouldBeMandatory) {
      // 의무 감축 활성화 시 최소 필드 보장
      mrj.current_excess_pct = Math.round((clusterPct - clusterLimit) * 10) / 10;
    } else {
      // 의무 감축 비활성화 — 세부 필드 제거
      delete mrj.current_excess_pct;
      delete mrj.weekly_reduction_target_pct;
      delete mrj.weekly_reduction_target_krw;
      delete mrj.reduction_priority;
    }
    hApex.mandatory_reduction_json = mrj;
    Logger.log('[SCRSV1] CLUSTER_SYNC 교정: is_mandatory ' + wasMandatory
      + ' → ' + shouldBeMandatory + ' (cluster=' + clusterPct + '%, threshold=' + threshold + '%)');
  }

  return {
    formula_id:           'SEMICONDUCTOR_CLUSTER_SYNC_V1',
    status:               syncStatus,
    corrected:            corrected,
    cluster_pct:          clusterPct,
    threshold_pct:        threshold,
    cap_pct:              clusterLimit,
    before_is_mandatory:  wasMandatory,
    after_is_mandatory:   shouldBeMandatory
  };
}


/**
 * HS007: validateOrderCondition_
 * 주문 조건 텍스트에 다중 조건 접속사가 포함되면 INVALID_MULTI_CONDITION 반환.
 * HTS 자동주문은 단일 지정가만 허용 — 접속사 복합 조건은 HTS 오입력 원인.
 */
function validateOrderCondition_(text) {
  if (!text || typeof text !== 'string') {
    return { valid: true, status: 'OK', matched_conjunctions: [], formula_id: 'VALIDATE_ORDER_CONDITION_V1' };
  }
  var MULTI_CONDITION_PATTERNS = [
    '또는', '혹은', '동시 충족', '동시충족',
    '실패 시', '실패시', '회복 실패', '회복실패',
    '돌파 실패', '돌파실패', '이탈 또는', '초과 또는',
    '또는 이하', '또는 이상', '이거나', '이면서'
  ];
  var matched = MULTI_CONDITION_PATTERNS.filter(function(p) {
    return text.indexOf(p) >= 0;
  });
  if (matched.length > 0) {
    return {
      valid:                false,
      status:               'INVALID_MULTI_CONDITION',
      matched_conjunctions: matched,
      resolution:           '단일 가격 조건만 기재 (예: "종가 196,500원 이탈 시")',
      formula_id:           'VALIDATE_ORDER_CONDITION_V1'
    };
  }
  return { valid: true, status: 'OK', matched_conjunctions: [], formula_id: 'VALIDATE_ORDER_CONDITION_V1' };
}

/**
 * H10 (HS010_REVISED): buildShadowLedger_
 * BLOCKED/INVALID 블루프린트를 그림자 원장으로 분리.
 * 차단 여부와 무관하게 산출 지표를 투명하게 보존 — 사용자의 사후 평가·오버라이드 지원.
 */
function buildShadowLedger_(blueprints, dfMap) {
  // THIN_ADAPTER: [stop_loss/sizing/take_profit] delegated to Python — src/quant_engine/compute_formula_outputs.py:check_sell_price_sanity
  dfMap = dfMap || {};
  var ledger = [];
  var bpRows = Array.isArray(blueprints) ? blueprints : [];
  bpRows.forEach(function(bp) {
    var isBlocked = bp.validation_status === 'BLOCKED'
                 || bp.validation_status === 'INVALID'
                 || String(bp.validation_status || '').indexOf('INVALID') === 0;
    if (!isBlocked) return;
    var df = dfMap[bp.ticker] || {};
    ledger.push({
      ticker:              bp.ticker,
      name:                bp.name || df.name || '',
      block_reason:        bp.rationale_code || bp.validation_status || 'BLOCKED',
      order_type:          bp.order_type || '',
      limit_price_calc:    bp.limit_price   || null,
      ["stop_loss_calc"]:      bp["stop_loss"]     || df["stop_loss_price"] || null,
      ["take_profit_calc"]:    bp["take_profit"]   || df["tp1_price"] || null,
      base_qty_calc:       bp.qty           || df.base_qty || null,
      value_at_risk_krw:   bp.value_at_risk_krw || null,
      override_possible:   true,
      formula_id:          'SHADOW_LEDGER_V1'
    });
  });
  return {
    shadow_ledger:   ledger,
    blocked_count:   ledger.length,
    formula_id:      'SHADOW_LEDGER_V1'
  };
}

/**
 * D2: calcLlmServingConstraint_
 * LLM 12가지 금지행동 체크리스트 — 보고서 조립 직전 실행.
 * 하나라도 위반 가능성이 있으면 INVALID_LLM_OVERRIDE 태그를 반환하여 보고서에 표기.
 */
function calcLlmServingConstraint_(hApex) {
  var h = hApex || {};
  var violations = [];

  // Check 1: 미등록 공식 사용 가능성 — serving_lock_json numeric_generation_allowed
  var sLock = h.serving_lock_json || {};
  var budget = sLock.llm_serving_budget || {};
  if (budget.numeric_generation_allowed !== 0) {
    violations.push({ check: 1, rule: '미등록 공식으로 지정가/수량 산출', status: 'WARN_NOT_LOCKED' });
  }

  // Check 2: BLOCK 판정 우회 — hts_entry_allowed=false인데 blueprint PASS 존재 불가
  var exportGate = h.export_gate_json || {};
  if (exportGate.hts_entry_allowed === false) {
    var blueprints = h.order_blueprint_json || [];
    var passCount = (Array.isArray(blueprints) ? blueprints : []).filter(function(b) {
      return b.validation_status === 'PASS';
    }).length;
    if (passCount > 0) {
      violations.push({ check: 2, rule: 'hts_entry_allowed=false 상태에서 PASS blueprint 존재', status: 'VIOLATION' });
    }
  }

  // Check 3: SELL_PRICE_SANITY INVALID 가격 복원 위험 — INVALID 종목이 shadow_ledger에 없으면 경고
  var shadowLedger = h.shadow_ledger_json || {};
  var invalidBlueprints = (Array.isArray(h.order_blueprint_json) ? h.order_blueprint_json : [])
    .filter(function(b) { return String(b.validation_status || '').indexOf('INVALID') === 0; });
  if (invalidBlueprints.length > 0 && (!shadowLedger.blocked_count || shadowLedger.blocked_count === 0)) {
    violations.push({ check: 3, rule: 'INVALID blueprint가 Shadow Ledger에 미포함', status: 'VIOLATION' });
  }

  // Check 5: K2 반등 대기 수량 — scrs_v2_json에 rebound_wait_qty가 있으면 분리 표기 의무
  var scrs = h.scrs_v2_json || {};
  var selectedCombo = Array.isArray(scrs.selected_combo) ? scrs.selected_combo : [];
  if (selectedCombo.length > 0) {
    var hasRebound = selectedCombo.some(function(c) { return c.rebound_wait_qty > 0; });
    if (hasRebound && !scrs._display_split_confirmed) {
      violations.push({ check: 5, rule: 'K2 rebound_wait_qty 분리 미표기 위험', status: 'WARN' });
    }
  }

  // Check 9: consistency_score < 90이면 보고서 계속 생성 금지
  var asResult = h.account_snapshot_result || {};
  var cScore = asResult.consistency_score;
  if (typeof cScore === 'number' && cScore < 90) {
    violations.push({ check: 9, rule: 'consistency_score=' + cScore + ' < 90 (ABORT 필요)', status: 'VIOLATION' });
  }

  // Check 10: mega_sell_alert=TRUE이면 BUY/ADD_ON 금지
  var macroJson = h.macro_event_json || {};
  if (macroJson.mega_sell_alert === true || macroJson.mega_sell_alert === 'TRUE') {
    var buyBlueprints = (Array.isArray(h.order_blueprint_json) ? h.order_blueprint_json : [])
      .filter(function(b) { return b.order_type === 'BUY' || b.order_type === 'ADD_ON'; });
    if (buyBlueprints.length > 0) {
      violations.push({ check: 10, rule: 'mega_sell_alert=TRUE 상태에서 BUY/ADD_ON blueprint 존재', status: 'VIOLATION' });
    }
  }

  // Check 11: synthesis_verdict=BEARISH 종목에 BUY 금지
  var paeRows = h.predictive_alpha_json || [];
  var bearishTickers = (Array.isArray(paeRows) ? paeRows : [])
    .filter(function(r) { return r.synthesis_verdict === 'BEARISH'; })
    .map(function(r) { return r.ticker; });
  if (bearishTickers.length > 0) {
    (Array.isArray(h.order_blueprint_json) ? h.order_blueprint_json : []).forEach(function(b) {
      if ((b.order_type === 'BUY' || b.order_type === 'ADD_ON') && bearishTickers.indexOf(b.ticker) >= 0) {
        violations.push({ check: 11, rule: 'synthesis_verdict=BEARISH 종목 BUY blueprint: ' + b.ticker, status: 'VIOLATION' });
      }
    });
  }

  var constraintStatus = violations.some(function(v) { return v.status === 'VIOLATION'; })
    ? 'INVALID_LLM_OVERRIDE' : violations.length > 0 ? 'WARN' : 'PASS';

  return {
    constraint_status:  constraintStatus,
    violations:         violations,
    violation_count:    violations.filter(function(v) { return v.status === 'VIOLATION'; }).length,
    warn_count:         violations.filter(function(v) { return v.status === 'WARN' || v.status === 'WARN_NOT_LOCKED'; }).length,
    total_checks:       12,
    formula_id:         'LLM_SERVING_CONSTRAINT_V1'
  };
}

/**
 * H6: calcAvgTradeValueSignal_
 * secular_leader(005930·000660) PROFIT_LOCK_STAGE_20 구간에서
 * 5일 평균 거래대금 > 20일 평균 × 3.0이면 과열신호 +1 판정.
 */
function calcAvgTradeValueSignal_(ticker, df) {
  df = df || {};
  var SECULAR_TICKERS = ['005930', '000660'];
  var isSecular = SECULAR_TICKERS.indexOf(String(ticker || '')) >= 0;
  var stage = String(df.profit_lock_stage || df.Profit_Lock_Stage || '').toUpperCase();
  var avgVal5d  = toNumber_(df.avg_trade_val_5d  || df.avgTradeVal5d)  || 0;
  var avgVal20d = toNumber_(df.avg_trade_val_20d || df.avgTradeVal20d) || 0;

  if (!isSecular || stage !== 'PROFIT_LOCK_20' || avgVal20d <= 0) {
    return {
      ticker:              ticker,
      applicable:          false,
      signal:              'NOT_APPLICABLE',
      avg_trade_val_5d:    avgVal5d,
      avg_trade_val_20d:   avgVal20d,
      overheat_triggered:  false,
      formula_id:          'AVG_TRADE_VALUE_SIGNAL_V1'
    };
  }

  var ratio = avgVal5d / avgVal20d;
  var overheat = ratio >= 3.0;
  return {
    ticker:              ticker,
    applicable:          true,
    signal:              overheat ? 'OVERHEAT_TRADE_VALUE' : 'NORMAL',
    avg_trade_val_5d:    avgVal5d,
    avg_trade_val_20d:   avgVal20d,
    ratio_5d_vs_20d:     Math.round(ratio * 100) / 100,
    overheat_triggered:  overheat,
    overheat_score_add:  overheat ? 1 : 0,
    threshold:           3.0,
    formula_id:          'AVG_TRADE_VALUE_SIGNAL_V1'
  };
}

/**
 * G2: calcTrimPlanMinCash_
 * 최소 현금(cash_floor) 달성을 위한 결정론적 TRIM 계획 산출.
 * H2 매도후보 순위(sell_priority) 그대로 종목 순서를 결정 — LLM 임의 선택 금지.
 */
function calcTrimPlanMinCash_(holdings, dfMap, cashShortfallInfo, sellPriorityList) {
  dfMap = dfMap || {};
  var shortfall = toNumber_((cashShortfallInfo || {}).cash_shortfall_min_krw) || 0;
  var plan = [];
  var accumulatedKrw = 0;
  var holdingRows = Array.isArray(holdings) ? holdings : [];
  var priorityRows = Array.isArray(sellPriorityList) ? sellPriorityList : [];

  priorityRows.forEach(function(sp) {
    if (accumulatedKrw >= shortfall) return;
    var h = holdingRows.find(function(x) { return x.ticker === sp.ticker; }) || {};
    var df = dfMap[sp.ticker] || {};
    var avgCost = toNumber_(h.avg_cost || h.average_cost) || 0;
    var qty = toNumber_(h.qty || h.quantity) || 0;

    if (qty === 0 || avgCost === 0) {
      plan.push({
        priority:          sp.priority || plan.length + 1,
        ticker:            sp.ticker,
        name:              sp.name || df.name || '',
        sell_qty:          'CAPTURE_REQUIRED',
        estimated_sell_krw: 0,
        sell_price_ref:    null,
        accumulated_krw:   accumulatedKrw,
        shortfall_covered: false,
        note:              'CAPTURE_REQUIRED: qty/cost 미확정'
      });
      return;
    }

    var closePrice = toNumber_(df.close || df.close_price) || avgCost;
    var remaining = shortfall - accumulatedKrw;
    var neededQty = Math.ceil(remaining / closePrice);
    var sellQty = Math.min(neededQty, qty);
    var estimatedKrw = sellQty * closePrice;
    accumulatedKrw += estimatedKrw;

    plan.push({
      priority:          sp.priority || plan.length + 1,
      ticker:            sp.ticker,
      name:              sp.name || df.name || '',
      sell_qty:          sellQty,
      estimated_sell_krw: Math.round(estimatedKrw),
      sell_price_ref:    closePrice,
      accumulated_krw:   Math.round(accumulatedKrw),
      shortfall_covered: accumulatedKrw >= shortfall,
      note:              accumulatedKrw >= shortfall ? 'SHORTFALL_MET' : 'PARTIAL'
    });
  });

  return {
    cash_shortfall_min_krw:  Math.round(shortfall),
    plan:                    plan,
    total_plan_krw:          Math.round(accumulatedKrw),
    shortfall_fully_covered: accumulatedKrw >= shortfall,
    is_plan_only:            true,
    hts_order_required:      'order_blueprint_json.validation_status 기준으로만 판단',
    formula_id:              'TRIM_PLAN_MIN_CASH_V1'
  };
}


// ═══════════════════════════════════════════════════════════════════════════════
// [PROPOSAL50] F1 — TRADE_QUALITY_SCORER_V1
// 실행된 매수·매도를 T+5/T+20 기준으로 자동 채점.
// trade_quality_history 시트를 읽어 미채점 레코드를 업데이트하고 결과 배열 반환.
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * calcTradeQualityScorer_
 * trade_quality_history 시트에서 미채점 레코드를 배치 처리.
 * BUY: velocity/ma20/volume/t5/t20 각 20점 합산 (100점 만점)
 * SELL: above_ma20/above_cost/not_too_early/cash_goal_met 각 25점 합산 (100점 만점)
 */
function calcTradeQualityScorer_(ss) {
  try {
    ss = ss || getSpreadsheet_();
    var sh = ss.getSheetByName('trade_quality_history');
    if (!sh) {
      Logger.log('[F1] trade_quality_history 시트 없음');
      return { status: 'SHEET_NOT_FOUND', scored_count: 0, trade_quality: [], formula_id: 'TRADE_QUALITY_SCORER_V1' };
    }

    var data = sh.getDataRange().getValues();
    if (data.length < 2) {
      return { status: 'NO_DATA', scored_count: 0, trade_quality: [], formula_id: 'TRADE_QUALITY_SCORER_V1' };
    }

    var header = data[0];
    var COL = {};
    header.forEach(function(h, i) { COL[String(h).trim()] = i; });

    // 필수 컬럼 확인
    var REQ = ['ticker', 'action', 'scored'];
    for (var ri = 0; ri < REQ.length; ri++) {
      if (COL[REQ[ri]] == null) {
        Logger.log('[F1] 필수 컬럼 누락: ' + REQ[ri]);
        return { status: 'COLUMN_MISSING', missing: REQ[ri], scored_count: 0, trade_quality: [], formula_id: 'TRADE_QUALITY_SCORER_V1' };
      }
    }

    // 현재 종가 맵 (T+5/T+20 평가용)
    var priceMap = {};
    var dfSheet = ss.getSheetByName('data_feed');
    if (dfSheet) {
      var dfData = dfSheet.getDataRange().getValues();
      if (dfData.length > 1) {
        var dfHeader = dfData[0];
        var tCol = dfHeader.indexOf('Ticker');
        var cCol = dfHeader.indexOf('Close');
        if (tCol >= 0 && cCol >= 0) {
          for (var dri = 1; dri < dfData.length; dri++) {
            var tk = String(dfData[dri][tCol] || '').trim();
            var cl = parseFloat(String(dfData[dri][cCol] || ''));
            if (tk && !isNaN(cl) && cl > 0) priceMap[tk] = cl;
          }
        }
      }
    }

    var todayMs = new Date().getTime();
    var scoredResults = [];
    var scoredThisRun = 0;

    for (var i = 1; i < data.length; i++) {
      var row = data[i];
      var alreadyScored = String(row[COL['scored']] || '').toUpperCase();
      if (alreadyScored === 'TRUE' || alreadyScored === 'SCORED') continue;

      var ticker = String(row[COL['ticker']] || '').trim();
      var action = String(row[COL['action']] || '').toUpperCase();
      if (!ticker) continue;

      var entryDate = row[COL['entry_date'] != null ? COL['entry_date'] : -1];
      var daysSinceEntry = entryDate ? (todayMs - new Date(entryDate).getTime()) / 86400000 : 0;

      // T+5 이상 경과해야 채점 (T+20 필드는 optional)
      if (COL['entry_date'] != null && daysSinceEntry < 7) continue;

      var score = 0;
      var subscores = {};
      var feedbackTag = 'GOOD_EXECUTION';

      if (action === 'BUY') {
        // 매수 품질 채점
        var velocity1d   = parseFloat(String(row[COL['velocity_1d_at_entry'] != null ? COL['velocity_1d_at_entry'] : -1] || ''));
        var entryPrice   = parseFloat(String(row[COL['entry_price']  != null ? COL['entry_price']  : -1] || ''));
        var ma20Entry    = parseFloat(String(row[COL['ma20_at_entry'] != null ? COL['ma20_at_entry'] : -1] || ''));
        var volRatio     = parseFloat(String(row[COL['volume_ratio_at_entry'] != null ? COL['volume_ratio_at_entry'] : -1] || ''));
        var t5RetPct     = parseFloat(String(row[COL['t5_return_pct']  != null ? COL['t5_return_pct']  : -1] || ''));
        var t20VsCore    = parseFloat(String(row[COL['t20_vs_core_pctp'] != null ? COL['t20_vs_core_pctp'] : -1] || ''));

        // velocity_ok: 진입일 속도 < 1% (추격 아님)
        if (!isNaN(velocity1d) && velocity1d < 1) { score += 20; subscores.velocity_ok = 20; }
        else subscores.velocity_ok = 0;

        // ma20_proximity: 진입가 ≤ MA20 × 1.01
        if (!isNaN(entryPrice) && !isNaN(ma20Entry) && ma20Entry > 0 && entryPrice <= ma20Entry * 1.01) {
          score += 20; subscores.ma20_proximity = 20;
        } else subscores.ma20_proximity = 0;

        // volume_confirm: 거래량비율 ≥ 1.2
        if (!isNaN(volRatio) && volRatio >= 1.2) { score += 20; subscores.volume_confirm = 20; }
        else subscores.volume_confirm = 0;

        // t5_positive: T+5 수익률 > 0
        if (!isNaN(t5RetPct) && t5RetPct > 0) { score += 20; subscores.t5_positive = 20; }
        else subscores.t5_positive = 0;

        // t20_alpha: T+20 대비 코어 초과 > 0
        if (!isNaN(t20VsCore) && t20VsCore > 0) { score += 20; subscores.t20_alpha = 20; }
        else subscores.t20_alpha = 0;

        // 피드백 태그
        if (subscores.velocity_ok === 0 && subscores.ma20_proximity === 0) feedbackTag = 'CHASE_ENTRY';
        else if (subscores.t5_positive === 0 && subscores.t20_alpha === 0) feedbackTag = 'DISTRIBUTION_ENTRY';

      } else if (action === 'SELL') {
        // 매도 품질 채점
        var sellPrice    = parseFloat(String(row[COL['sell_price']    != null ? COL['sell_price']    : -1] || ''));
        var ma20Sell     = parseFloat(String(row[COL['ma20_at_sell']  != null ? COL['ma20_at_sell']  : -1] || ''));
        var avgCost      = parseFloat(String(row[COL['average_cost']  != null ? COL['average_cost']  : -1] || ''));
        var priceT5After = parseFloat(String(row[COL['price_t5_after_sell'] != null ? COL['price_t5_after_sell'] : -1] || ''));
        var cashRecov    = parseFloat(String(row[COL['cash_recovered_krw']  != null ? COL['cash_recovered_krw']  : -1] || ''));
        var cashGoal     = parseFloat(String(row[COL['cash_shortfall_min_krw'] != null ? COL['cash_shortfall_min_krw'] : -1] || ''));

        // above_ma20: 매도가 ≥ MA20 × 0.99
        if (!isNaN(sellPrice) && !isNaN(ma20Sell) && ma20Sell > 0 && sellPrice >= ma20Sell * 0.99) {
          score += 25; subscores.above_ma20 = 25;
        } else subscores.above_ma20 = 0;

        // above_cost: 매도가 ≥ 평단
        if (!isNaN(sellPrice) && !isNaN(avgCost) && avgCost > 0 && sellPrice >= avgCost) {
          score += 25; subscores.above_cost = 25;
        } else subscores.above_cost = 0;

        // not_too_early: T+5 사후 종가가 없거나 매도가 이상
        if (isNaN(priceT5After) || priceT5After <= sellPrice) {
          score += 25; subscores.not_too_early = 25;
        } else subscores.not_too_early = 0;

        // cash_goal_met: 실제 회수액 ≥ 목표 부족분
        if (!isNaN(cashRecov) && !isNaN(cashGoal) && cashGoal > 0 && cashRecov >= cashGoal) {
          score += 25; subscores.cash_goal_met = 25;
        } else subscores.cash_goal_met = 0;

        // 피드백 태그
        if (subscores.above_cost === 0) feedbackTag = 'PANIC_EXIT';
        else if (subscores.not_too_early === 0) feedbackTag = 'OVERSOLD_PANIC';
      } else {
        continue; // BUY/SELL 이외 레코드 스킵
      }

      // 등급 결정
      var grade;
      if      (score >= 90) grade = 'EXCELLENT';
      else if (score >= 75) grade = 'GOOD';
      else if (score >= 60) grade = 'ACCEPTABLE';
      else if (score >= 40) grade = 'POOR';
      else                  grade = 'CRITICAL';

      if (grade === 'POOR' || grade === 'CRITICAL') {
        feedbackTag = score < 40 ? 'PATTERN_ALERT' : 'CHASE_ENTRY_OR_PANIC_EXIT';
      } else if (grade === 'EXCELLENT' || grade === 'GOOD') {
        feedbackTag = 'GOOD_EXECUTION';
      }

      // 시트 업데이트
      var scoreCol  = COL['score']        != null ? COL['score']        + 1 : null;
      var gradeCol  = COL['grade']        != null ? COL['grade']        + 1 : null;
      var fbTagCol  = COL['feedback_tag'] != null ? COL['feedback_tag'] + 1 : null;
      var scoredCol = COL['scored']       != null ? COL['scored']       + 1 : null;

      if (scoreCol)  sh.getRange(i + 1, scoreCol).setValue(score);
      if (gradeCol)  sh.getRange(i + 1, gradeCol).setValue(grade);
      if (fbTagCol)  sh.getRange(i + 1, fbTagCol).setValue(feedbackTag);
      if (scoredCol) sh.getRange(i + 1, scoredCol).setValue('SCORED');

      scoredResults.push({
        row:          i,
        ticker:       ticker,
        action:       action,
        score:        score,
        grade:        grade,
        feedback_tag: feedbackTag,
        subscores:    subscores,
        formula_id:   'TRADE_QUALITY_SCORER_V1'
      });
      scoredThisRun++;
    }

    // 전체 기록 집계 (기존 채점 포함)
    var allResults = [];
    var freshData = sh.getDataRange().getValues();
    for (var j = 1; j < freshData.length; j++) {
      var r = freshData[j];
      var sc = String(r[COL['scored']] || '').toUpperCase();
      if (sc !== 'TRUE' && sc !== 'SCORED') continue;
      allResults.push({
        ticker:       String(r[COL['ticker']] || '').trim(),
        action:       String(r[COL['action']] || '').toUpperCase(),
        score:        parseFloat(String(r[COL['score']] || '')) || 0,
        grade:        String(r[COL['grade']] || 'UNKNOWN'),
        feedback_tag: String(r[COL['feedback_tag']] || '')
      });
    }

    Logger.log('[F1] calcTradeQualityScorer_ 완료: 이번 채점=' + scoredThisRun + '건, 전체=' + allResults.length + '건');

    // F2: F1 완료 직후 블랙리스트 자동 갱신 (F1 → F2 파이프라인)
    try {
      calcPatternBlacklistAuto_(allResults);
    } catch (pbErr) {
      Logger.log('[F1] calcPatternBlacklistAuto_ 연동 오류: ' + pbErr.message);
    }

    var f1Result = {
      status:        'OK',
      scored_count:  scoredThisRun,
      total_records: allResults.length,
      trade_quality: allResults,
      last_computed: new Date().toISOString(),
      formula_id:    'TRADE_QUALITY_SCORER_V1'
    };

    // settings 시트에 trade_quality_json 캐시 저장 (harness_rows 일간 출력용)
    // 셀 50K 한도 초과 방지: trade_quality 최근 100건만 저장
    try {
      var setSh = ss.getSheetByName('settings');
      if (setSh) {
        var sData = setSh.getDataRange().getValues();
        var updated = false;
        var f1Slim = Object.assign({}, f1Result,
          { trade_quality: (f1Result.trade_quality || []).slice(-100) });
        var serialized = JSON.stringify(f1Slim);
        for (var si = 0; si < sData.length; si++) {
          if (String(sData[si][0] || '').trim() === 'trade_quality_json') {
            setSh.getRange(si + 1, 2).setValue(serialized);
            updated = true;
            break;
          }
        }
        if (!updated) setSh.appendRow(['trade_quality_json', serialized]);
      }
    } catch(writeErr) {
      Logger.log('[F1] settings 시트 기록 실패: ' + writeErr.message);
    }

    return f1Result;
  } catch(e) {
    Logger.log('[F1] calcTradeQualityScorer_ 오류: ' + e.message);
    return { status: 'ERROR', error: e.message, scored_count: 0, trade_quality: [], formula_id: 'TRADE_QUALITY_SCORER_V1' };
  }
}


// ═══════════════════════════════════════════════════════════════════════════════
// [PROPOSAL50] F2 — PATTERN_BLACKLIST_AUTO_V1
// 동일 ticker POOR/CRITICAL 3회 누적 → PATTERN_BLACKLIST_TRIGGERED
// 3회 연속 GOOD(75+) 달성 시 해제
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * calcPatternBlacklistAuto_
 * trade_quality_json 배열을 받아 ticker별 POOR/CRITICAL 누적 횟수를 계산.
 * 3회 이상이면 PATTERN_BLACKLIST_TRIGGERED, 3회 연속 GOOD 이상이면 해제.
 * 결과를 settings 시트의 pattern_blacklist_json에 기록.
 */
function calcPatternBlacklistAuto_(tradeQualityHistory) {
  try {
    var history = Array.isArray(tradeQualityHistory) ? tradeQualityHistory : [];

    // ticker별 그룹화
    var tickerMap = {};
    history.forEach(function(rec) {
      var tk = String(rec.ticker || '').trim();
      if (!tk) return;
      if (!tickerMap[tk]) tickerMap[tk] = [];
      tickerMap[tk].push({
        grade: String(rec.grade || '').toUpperCase(),
        score: typeof rec.score === 'number' ? rec.score : (parseFloat(String(rec.score || '')) || 0)
      });
    });

    var blacklistEntries = [];
    var triggeredCount = 0;

    Object.keys(tickerMap).forEach(function(ticker) {
      var records = tickerMap[ticker];

      // POOR/CRITICAL 누적 카운트
      var poorCriticalCount = records.filter(function(r) {
        return r.grade === 'POOR' || r.grade === 'CRITICAL';
      }).length;

      // 해제 조건: 마지막 3건이 모두 GOOD(75+) 이상
      var releaseMet = false;
      if (records.length >= 3) {
        var last3 = records.slice(-3);
        releaseMet = last3.every(function(r) {
          return (r.grade === 'GOOD' || r.grade === 'EXCELLENT') && r.score >= 75;
        });
      }

      var status;
      if (releaseMet && poorCriticalCount >= 3) {
        status = 'CLEAR';  // 블랙리스트 해제
      } else if (poorCriticalCount >= 3) {
        status = 'TRIGGERED';
        triggeredCount++;
      } else {
        status = 'CLEAR';
      }

      blacklistEntries.push({
        ticker:                   ticker,
        pattern_blacklist_status: status,
        accumulated_poor_count:   poorCriticalCount,
        total_records:            records.length,
        release_condition_met:    releaseMet,
        saqg_override:            status === 'TRIGGERED' ? 'EXCLUDED' : 'NO_CHANGE',
        alpha_score_cap:          status === 'TRIGGERED' ? 50 : null,
        formula_id:               'PATTERN_BLACKLIST_AUTO_V1'
      });
    });

    // settings 시트에 pattern_blacklist_json 기록 (wrapper 객체 형태로 저장)
    try {
      var ss = getSpreadsheet_();
      var settingSh = ss.getSheetByName('settings');
      if (settingSh) {
        var sData = settingSh.getDataRange().getValues();
        var updated = false;
        var wrapperObj = {
          status:          'OK',
          triggered_count: triggeredCount,
          total_tickers:   blacklistEntries.length,
          patterns:        blacklistEntries,
          pattern_count:   blacklistEntries.length,
          computed_at:     new Date().toISOString(),
          formula_id:      'PATTERN_BLACKLIST_AUTO_V1'
        };
        var serialized = JSON.stringify(wrapperObj);
        for (var si = 0; si < sData.length; si++) {
          if (String(sData[si][0] || '').trim() === 'pattern_blacklist_json') {
            settingSh.getRange(si + 1, 2).setValue(serialized);
            updated = true;
            break;
          }
        }
        if (!updated) settingSh.appendRow(['pattern_blacklist_json', serialized]);
      }
    } catch(writeErr) {
      Logger.log('[F2] settings 시트 기록 실패: ' + writeErr.message);
    }

    Logger.log('[F2] calcPatternBlacklistAuto_ 완료: TRIGGERED=' + triggeredCount + '/' + blacklistEntries.length + '건');
    return {
      status:           'OK',
      triggered_count:  triggeredCount,
      total_tickers:    blacklistEntries.length,
      patterns:         blacklistEntries,
      pattern_count:    blacklistEntries.length,
      formula_id:       'PATTERN_BLACKLIST_AUTO_V1'
    };
  } catch(e) {
    Logger.log('[F2] calcPatternBlacklistAuto_ 오류: ' + e.message);
    return { status: 'ERROR', error: e.message, triggered_count: 0, patterns: [], pattern_count: 0, formula_id: 'PATTERN_BLACKLIST_AUTO_V1' };
  }
}


// ═══════════════════════════════════════════════════════════════════════════════
// [PROPOSAL50] ALPHA_FEEDBACK_LOOP_V1
// monthly_history의 AEW_V1 성과 데이터를 분석해 SAQG_V1 필터 임계값 조정 권고 생성.
// 임계값 자동 변경 금지 — 권고(RECOMMENDATION)만 출력.
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * calcAlphaFeedbackLoop_
 * alpha_evaluation_window_json (AEW_V1 결과) 에서 ELIGIBLE 케이스를 분석해
 * SAQG F1/F2/F3 임계값 조정 권고를 생성한다.
 * 10건 미만이면 DATA_INSUFFICIENT — 권고 생성 금지.
 */
function calcAlphaFeedbackLoop_() {
  try {
    var ss = getSpreadsheet_();
    var aewRows = [];

    // monthly_history 시트에서 AEW 데이터 수집
    var mhSh = ss.getSheetByName('monthly_history');
    if (mhSh) {
      var mhData = mhSh.getDataRange().getValues();
      if (mhData.length > 1) {
        var mhHeader = mhData[0];
        var COL = {};
        mhHeader.forEach(function(h, i) { COL[String(h).trim()] = i; });

        for (var i = 1; i < mhData.length; i++) {
          var row = mhData[i];
          var saqg   = String(row[COL['saqg_v1'] != null ? COL['saqg_v1'] : -1] || '').toUpperCase();
          var t20Sam = parseFloat(String(row[COL['t20_vs_samsung_pctp'] != null ? COL['t20_vs_samsung_pctp'] : -1] || ''));
          var brtV   = String(row[COL['brt_verdict'] != null ? COL['brt_verdict'] : -1] || '').toUpperCase();
          var regime = String(row[COL['market_regime'] != null ? COL['market_regime'] : -1] || '');
          if (!saqg) continue;
          aewRows.push({ saqg_v1: saqg, t20_vs_samsung_pctp: isNaN(t20Sam) ? null : t20Sam, brt_verdict: brtV, market_regime: regime });
        }
      }
    }

    var eligibleRows = aewRows.filter(function(r) { return r.saqg_v1 === 'ELIGIBLE'; });
    var casesAnalyzed = eligibleRows.length;

    var now = new Date();
    var asOf = now.toISOString().split('T')[0];
    var analysisPeriod = asOf.substring(0, 7); // 'YYYY-MM'

    if (casesAnalyzed < 10) {
      Logger.log('[AFL] calcAlphaFeedbackLoop_: 데이터 부족(' + casesAnalyzed + '건) — 권고 생성 건너뜀');
      return {
        formula_id:                     'ALPHA_FEEDBACK_LOOP_V1',
        as_of:                          asOf,
        analysis_period:                analysisPeriod,
        status:                         'DATA_INSUFFICIENT',
        cases_analyzed:                 casesAnalyzed,
        grade_count:                    0,
        eligible_t20_fail_rate:         null,
        eligible_t60_fail_rate:         null,
        recommended_filter_adjustments: [],
        grade_summary:                  []
      };
    }

    // T+20 알파 실패율 계산 (t20_vs_samsung_pctp < -3)
    var t20WithData = eligibleRows.filter(function(r) { return r.t20_vs_samsung_pctp !== null; });
    var t20FailRows = t20WithData.filter(function(r) { return r.t20_vs_samsung_pctp < -3; });
    var t20PassRows = t20WithData.length - t20FailRows.length;
    var t20FailRate = t20WithData.length > 0
      ? Math.round(t20FailRows.length / t20WithData.length * 1000) / 10
      : null;
    var t20PassRate = t20WithData.length > 0
      ? Math.round(t20PassRows / t20WithData.length * 1000) / 10
      : null;

    // BRT_VERDICT=BROKEN 케이스 비율
    var brokenCount = eligibleRows.filter(function(r) { return r.brt_verdict === 'BROKEN'; }).length;
    var brokenRate  = eligibleRows.length > 0
      ? Math.round(brokenCount / eligibleRows.length * 1000) / 10 : 0;

    // grade_summary — saqg_v1 값별로 집계
    var gradeCounts = {};
    aewRows.forEach(function(r) {
      var g = r.saqg_v1 || 'UNKNOWN';
      if (!gradeCounts[g]) gradeCounts[g] = { t20_total: 0, t20_pass: 0, t20_fail: 0 };
      if (r.t20_vs_samsung_pctp !== null) {
        gradeCounts[g].t20_total++;
        if (r.t20_vs_samsung_pctp >= 0) gradeCounts[g].t20_pass++;
        else gradeCounts[g].t20_fail++;
      }
    });
    var gradeSummary = Object.keys(gradeCounts).map(function(g) {
      var gd = gradeCounts[g];
      var passRate = gd.t20_total > 0 ? Math.round(gd.t20_pass / gd.t20_total * 1000) / 10 : null;
      var failRate = gd.t20_total > 0 ? Math.round(gd.t20_fail / gd.t20_total * 1000) / 10 : null;
      return {
        grade:         g,
        t20_total:     gd.t20_total,
        t20_pass:      gd.t20_pass,
        t20_pass_rate: passRate,
        t20_fail_rate: failRate,
        t60_total:     0,    // T+60 데이터 미수집 — 향후 확장
        t60_pass:      0,
        t60_pass_rate: null,
        t60_fail_rate: null,
        status:        gd.t20_total === 0 ? 'DATA_INSUFFICIENT' : 'OK'
      };
    });

    // 권고 생성 — 렌더러 계약 필드명: filter_id, current, recommended, action, rationale
    var recommendations = [];

    if (t20FailRate !== null && t20FailRate > 50) {
      recommendations.push({
        filter_id:   'SAQG_F1_F2_F3',
        current:     'CURRENT_THRESHOLDS',
        recommended: 'TIGHTEN: F2 recovery_ratio 1.20 → 1.35',
        action:      'TIGHTEN',
        rationale:   'ELIGIBLE T+20 알파 실패율 ' + t20FailRate + '% > 50% 기준 초과'
      });
    }

    if (t20PassRate !== null && t20PassRate > 70 && casesAnalyzed >= 12) {
      recommendations.push({
        filter_id:   'SAQG_F3',
        current:     'excess_drawdown 5%p',
        recommended: 'RELAX: excess_drawdown 5%p → 7%p',
        action:      'RELAX',
        rationale:   'ELIGIBLE T+20 성공률 ' + t20PassRate + '% > 70% (케이스 ' + casesAnalyzed + '건)'
      });
    }

    if (brokenRate > 30) {
      recommendations.push({
        filter_id:   'BRT_VERDICT_GATE',
        current:     'CURRENT_THRESHOLDS',
        recommended: 'TIGHTEN: BRT_BROKEN 진입 차단 강화',
        action:      'TIGHTEN',
        rationale:   'ELIGIBLE 중 BRT_BROKEN 비율 ' + brokenRate + '% > 30%'
      });
    }

    Logger.log('[AFL] calcAlphaFeedbackLoop_ 완료: cases=' + casesAnalyzed + ' t20FailRate=' + t20FailRate + '% recs=' + recommendations.length);

    var result = {
      formula_id:                     'ALPHA_FEEDBACK_LOOP_V1',
      as_of:                          asOf,
      analysis_period:                analysisPeriod,
      status:                         'OK',
      cases_analyzed:                 casesAnalyzed,
      grade_count:                    gradeSummary.length,
      eligible_t20_fail_rate:         t20FailRate,
      eligible_t60_fail_rate:         null,
      t20_pass_rate:                  t20PassRate,
      brt_broken_rate:                brokenRate,
      recommended_filter_adjustments: recommendations,
      grade_summary:                  gradeSummary,
      note:                           '임계값 자동 변경 금지 — 사용자 확인 후 settings 수동 반영'
    };

    // settings 시트에 기록
    try {
      var settingSh = ss.getSheetByName('settings');
      if (settingSh) {
        var sData = settingSh.getDataRange().getValues();
        var updated = false;
        var serialized = JSON.stringify(result);
        for (var si = 0; si < sData.length; si++) {
          if (String(sData[si][0] || '').trim() === 'alpha_feedback_json') {
            settingSh.getRange(si + 1, 2).setValue(serialized);
            updated = true;
            break;
          }
        }
        if (!updated) settingSh.appendRow(['alpha_feedback_json', serialized]);
      }
    } catch(writeErr) {
      Logger.log('[AFL] settings 시트 기록 실패: ' + writeErr.message);
    }

    return result;
  } catch(e) {
    Logger.log('[AFL] calcAlphaFeedbackLoop_ 오류: ' + e.message);
    return { status: 'ERROR', error: e.message, cases_analyzed: 0, recommended_filter_adjustments: [], formula_id: 'ALPHA_FEEDBACK_LOOP_V1' };
  }
}

/** AFL 일간 하네스 호출 래퍼 — calcAlphaFeedbackLoop_ 위임 */
function runAlphaFeedbackLoop_() {
  return calcAlphaFeedbackLoop_();
}

/**
 * AFL 캐시 읽기 — settings 시트에서 마지막 저장된 alpha_feedback_json 반환.
 * calcAlphaFeedbackLoop_ 오류 시 fallback으로 사용.
 */
function getAlphaFeedbackJson_() {
  try {
    var ss = getSpreadsheet_();
    var sh = ss.getSheetByName('settings');
    if (!sh) return { status: 'SETTINGS_NOT_FOUND', formula_id: 'ALPHA_FEEDBACK_LOOP_V1' };
    var data = sh.getDataRange().getValues();
    for (var i = 0; i < data.length; i++) {
      if (String(data[i][0] || '').trim() === 'alpha_feedback_json') {
        var raw = data[i][1];
        if (!raw) break;
        try { return JSON.parse(String(raw)); } catch(pe) { break; }
      }
    }
  } catch(e) {
    Logger.log('[AFL] getAlphaFeedbackJson_ 읽기 실패: ' + e.message);
  }
  return { status: 'CACHE_EMPTY', formula_id: 'ALPHA_FEEDBACK_LOOP_V1' };
}

// FORMULA_STUB: EXPECTED_EDGE_V1 — 기댓값 공식 (calcExpectedEdge_) GAS 미구현, Python pipeline 산출



