function calcProfitPreservationRow_(h, df, priceRow, distributionRow) { var close = df.close || h.close || 0; var avgCost = h.avgCost || 0; var profitPct = close > 0 && avgCost > 0 ? (close - avgCost) / avgCost * 100 : 0; var state = 'NORMAL'; var preserveScore = 100; if (profitPct >= 30) state = 'PROFIT_LOCK_30'; else if (profitPct >= 20) state = 'PROFIT_LOCK_20'; else if (profitPct >= 10) state = 'PROFIT_LOCK_10'; else if (profitPct >= 8 || (df.atr20 > 0 && close >= avgCost + df.atr20)) state = 'BREAKEVEN_RATCHET'; if (state === 'PROFIT_LOCK_30') preserveScore = 20; else if (state === 'PROFIT_LOCK_20') preserveScore = 40; else if (state === 'PROFIT_LOCK_10') preserveScore = 60; else if (state === 'BREAKEVEN_RATCHET') preserveScore = 80; if (state === 'PROFIT_LOCK_30' && distributionRow && distributionRow.anti_distribution_state === 'PASS') { state = 'APEX_TRAILING'; } if (distributionRow && distributionRow.anti_distribution_state === 'BLOCK_BUY') { preserveScore = Math.max(0, preserveScore - 15); } // L2: RATCHET_TRAILING_AUTO_V1 — ATR 기반 자동 트레일링 손절 계산 var atr20 = typeof df.atr20 === 'number' && df.atr20 > 0 ? df.atr20 : 0; var ratchetStop = priceRow && typeof priceRow.stop_price === 'number' ? priceRow.stop_price : 0; var highestClose = priceRow && typeof priceRow.highest_price_since_entry === 'number' ? priceRow.highest_price_since_entry : close; var autoTrailingStop = null; var autoTrailingNote = null; if (atr20 > 0 && (state === 'PROFIT_LOCK_30' || state === 'APEX_TRAILING')) { var raw = Math.max(ratchetStop, highestClose - 2.0 * atr20); autoTrailingStop = tickNormalize_(raw); autoTrailingNote = 'max(ratchet,' + highestClose + '-2.0×ATR)'; } else if (atr20 > 0 && state === 'PROFIT_LOCK_20') { var raw = Math.max(ratchetStop, highestClose - 1.5 * atr20); autoTrailingStop = tickNormalize_(raw); autoTrailingNote = 'max(ratchet,' + highestClose + '-1.5×ATR)'; } return { ticker: h.ticker, name: h.name || df.name || '', profit_pct: round2_(profitPct), profit_preservation_state: state, rebound_preservation_score: Math.min(100, Math.max(0, Math.round(preserveScore))), protected_stop_price: priceRow ? priceRow.stop_price : null, ratchet_partial_qty: priceRow ? priceRow.ratchet_partial_qty : 0, auto_trailing_stop: autoTrailingStop, auto_trailing_note: autoTrailingNote, formula_id: 'PROFIT_PRESERVATION_STATE_V1' }; } function calcExecutionQualityRow_(ticker, orderRow, df) { var amount = orderRow && orderRow.order_amount_krw ? orderRow.order_amount_krw : 0; var advKrw = 0; if (typeof df.avgTradeVal5d === 'number') { // AvgTradeValue_5D_M is usually million KRW in sheet label. advKrw = df.avgTradeVal5d * 1000000; } var status = 'PASS'; var splitCount = 1; var reasons = []; if (amount > 0 && advKrw > 0 && amount > advKrw * 0.03) { status = 'BLOCKED_ADV_3PCT'; reasons.push('order_amount_gt_3pct_adv'); } else if (amount > 0 && advKrw > 0 && amount > advKrw * 0.01) { status = 'SPLIT_REQUIRED'; splitCount = 2; reasons.push('order_amount_gt_1pct_adv'); } if (df.spreadStatus && String(df.spreadStatus).indexOf('WIDE') >= 0) { status = 'BLOCKED_SPREAD'; reasons.push('wide_spread'); } return { ticker: ticker, execution_quality_status: status, split_count: splitCount, child_order_amount_krw: splitCount > 1 ? Math.round(amount / splitCount) : amount, hts_allowed: status === 'PASS', reason_codes: reasons, formula_id: 'EXECUTION_QUALITY_GUARD_V1' }; } // ── [2026-05-20_HARNESS_V5] H6: 뒷박 차단 — BREAKOUT_QUALITY_GATE_V2 ───────── function calcBreakoutQualityGate_(h, df, alphaRow, distRow) { var close = df.close || h.close || 0; var prevClose = df.prevClose || close; var ma20 = df.ma20 || 0; var rsi14 = typeof df.rsi14 === 'number' ? df.rsi14 : null; var volume = typeof df.volume === 'number' ? df.volume : null; var avgVol5d = typeof df.avgVolume5d === 'number' ? df.avgVolume5d : null; var ret1d = (close > 0 && prevClose > 0) ? (close - prevClose) / prevClose * 100 : null; var ret3d = typeof df.ret5d === 'number' ? df.ret5d * 0.6 : null; // ret5d 프록시 var disparity = (close > 0 && ma20 > 0) ? (close / ma20 - 1) * 100 : null; var timingScoreExit = alphaRow && typeof alphaRow.timing_score_exit === 'number' ? alphaRow.timing_score_exit : 0; var distributionRiskScore = distRow && typeof distRow["distribution_risk_score"] === 'number' ? distRow["distribution_risk_score"] : 0; var lateChaseRiskScore = alphaRow && typeof alphaRow["late_chase_risk_score"] === 'number' ? alphaRow["late_chase_risk_score"] : 0; var score = 50; var reasons = []; if (ret3d !== null && ret3d >= 7) { score -= 30; reasons.push('ret3d_gte7'); } if (disparity !== null && disparity > 10) { score -= 25; reasons.push('disparity_gt10'); } if (ret1d !== null && ret1d >= 4 && volume !== null && avgVol5d !== null && avgVol5d > 0 && volume < avgVol5d * 0.9) { score -= 40; reasons.push('surge_day_low_vol'); } if (rsi14 !== null && rsi14 > 75) { score -= 20; reasons.push('rsi14_gt75'); } if (timingScoreExit >= 50) { score -= 50; reasons.push('timing_exit_gte50'); } if (distributionRiskScore >= 70) { score -= 35; reasons.push('distribution_gte70'); } if (lateChaseRiskScore >= 70) { score -= 30; reasons.push('late_chase_gte70'); } if (volume !== null && avgVol5d !== null && avgVol5d > 0 && volume >= avgVol5d * 1.5 && ret1d !== null && ret1d >= 2 && ret3d !== null && ret3d < 5) { score += 25; reasons.push('quality_breakout_vol'); } if (disparity !== null && disparity >= 0 && disparity < 6) { score += 15; reasons.push('disparity_healthy'); } if (rsi14 !== null && rsi14 >= 45 && rsi14 <= 65) { score += 10; reasons.push('rsi14_healthy'); } score = Math.max(0, Math.min(100, Math.round(score))); var gate = score < 10 ? 'BLOCKED_LATE_CHASE' : score < 40 ? 'WATCH_COOLING_OFF' : 'PILOT_ALLOWED'; return { ticker: h.ticker, name: h.name || df.name || '', breakout_quality_score: score, breakout_quality_gate: gate, reason_codes: reasons, formula_id: 'BREAKOUT_QUALITY_GATE_V2', version: '2026-05-20_HARNESS_V5' }; } // ── [2026-05-20_HARNESS_V5] H7: 가짜 매도 차단 — ANTI_WHIPSAW_HOLD_GATE_V1 ─── function calcAntiWhipsawGate_(h, df, kospiRet5d) { var inst5d = typeof df.inst5d === 'number' ? df.inst5d : null; var frg5d = typeof df.frg5d === 'number' ? df.frg5d : null; var volSurge = typeof df.valSurgePct === 'number' ? df.valSurgePct : null; var consecutiveSell5d = typeof df.consecutiveSellSignals5d === 'number' ? df.consecutiveSellSignals5d : 0; var sectorRS5d = null; if (typeof df.ret5d === 'number' && typeof kospiRet5d === 'number') { var stockFactor = 1 + df.ret5d / 100; var kospiFactor = 1 + kospiRet5d / 100; sectorRS5d = kospiFactor > 0 ? stockFactor / kospiFactor * 100 : null; } var score = 0; var reasons = []; if (consecutiveSell5d >= 5) { score += 20; reasons.push('consecutive_sell_5d_gte5'); } if (inst5d !== null && inst5d > 0) { score += 30; reasons.push('inst_net_buy'); } if (frg5d !== null && frg5d > 0) { score += 20; reasons.push('frg_net_buy'); } if (sectorRS5d !== null && sectorRS5d > 100) { score += 15; reasons.push('sector_outperforming'); } if (volSurge !== null && volSurge >= 50) { score -= 25; reasons.push('vol_surge_50pct'); } if (volSurge !== null && volSurge >= 100) { score -= 20; reasons.push('vol_surge_100pct'); } score = Math.max(-50, Math.min(100, Math.round(score))); // [V1.1] 자동 해제 조건 3개 — 충족 수에 따라 hold_days 결정 var wClose = h.close || df.close || 0; var wMa20 = typeof df.ma20 === 'number' ? df.ma20 : 0; var clearCnt = 0; var clearList = []; if (inst5d !== null && inst5d > 0) { clearCnt++; clearList.push('inst_net_buy'); } if (frg5d !== null && frg5d > 0) { clearCnt++; clearList.push('frg_net_buy'); } if (wMa20 > 0 && wClose > 0 && wClose > wMa20) { clearCnt++; clearList.push('price_above_ma20'); } var gate, holdDays; if (score >= 30) { if (clearCnt >= 3) { gate = 'WHIPSAW_AUTO_RELEASED'; holdDays = 0; } else if (clearCnt >= 2) { gate = 'WHIPSAW_WEAKENING'; holdDays = 1; } else { gate = 'WHIPSAW_CONFIRMED'; holdDays = 3; } } else if (score >= 10) { gate = 'INCONCLUSIVE'; holdDays = 0; } else { gate = 'CONFIRMED_SELL'; holdDays = 0; } return { ticker: h.ticker, name: h.name || df.name || '', anti_whipsaw_score: score, anti_whipsaw_gate: gate, anti_whipsaw_hold_days: holdDays, clear_conditions_count: clearCnt, clear_conditions: clearList, reason_codes: reasons, formula_id: 'ANTI_WHIPSAW_HOLD_GATE_V1', version: '2026-05-24_V1.1' }; } // ── [2026-05-20_HARNESS_V5] H8: 4경로 결정론적 현금확보 라우터 ───────────────── function calcSmartCashRaiseV2_(h, df, profitRow, priceRow, cashShortfallInfo) { var posClass = String(h.positionClass || df.positionClass || '').toUpperCase(); var rsi14 = typeof df.rsi14 === 'number' ? df.rsi14 : 50; var profitStage = priceRow && priceRow.profit_lock_stage ? String(priceRow.profit_lock_stage) : (profitRow ? String(profitRow.profit_preservation_state || 'NORMAL') : 'NORMAL'); var secularPass = priceRow && priceRow.secular_leader_gate_active === false; // PASS = not active restriction var emergencyFull = !!(cashShortfallInfo && cashShortfallInfo.emergency_full_sell); var stopPrice = priceRow && typeof priceRow.stop_price === 'number' ? priceRow.stop_price : 0; var close = df.close || h.close || 0; var breachImmediate = stopPrice > 0 && close > 0 && close < stopPrice; var stopBreachGate = breachImmediate ? 'BREACH' : 'PASS'; var route, routeLabel, rationale; if (emergencyFull || breachImmediate) { route = 'ROUTE_D'; routeLabel = '긴급 전량매도'; rationale = emergencyFull ? 'emergency_full_sell=true' : 'close= 0 && rsi14 >= 35) { route = 'ROUTE_A'; routeLabel = '위성 비중 트림'; rationale = 'SATELLITE+RSI14(' + rsi14 + ')>=35'; } else if (rsi14 < 35) { route = 'ROUTE_B'; routeLabel = '과매도 분할 매도'; rationale = 'RSI14(' + rsi14 + ')<35→K2_50/50'; } else if (posClass.indexOf('CORE') >= 0 && (profitStage === 'PROFIT_LOCK_STAGE_20' || profitStage === 'PROFIT_LOCK_STAGE_30' || profitStage === 'PROFIT_LOCK_20' || profitStage === 'PROFIT_LOCK_30') && secularPass) { route = 'ROUTE_C'; routeLabel = '코어 익절 잠금'; rationale = 'CORE+' + profitStage + '+secular_PASS'; } else { route = 'NO_ACTION'; routeLabel = '현금확보 비대상'; rationale = 'no_condition_met'; } return { ticker: h.ticker, name: h.name || df.name || '', smart_cash_raise_route: route, route_label: routeLabel, rationale: rationale, profit_lock_stage: profitStage, stop_breach_gate: stopBreachGate, emergency_full_sell: emergencyFull, rebound_wait_pct: route === 'ROUTE_B' ? 50 : 0, formula_id: 'SMART_CASH_RAISE_V2', version: '2026-05-20_HARNESS_V5' }; } // ── [2026-05-20_HARNESS_V5] Gate 4b: O'Neil Follow-Through Day — FOLLOW_THROUGH_DAY_CONFIRM_V1 // 돌파 당일(Day 0)에 즉시 매수 금지. Day 2~7 사이에 수익률+거래량 조건 충족 시만 BUY_PILOT_ALLOWED. // daysSinceBreakout / retSinceBreakout / volumeBreakoutDay 이 df에 없으면 프록시 계산으로 후퇴. function calcFollowThroughDayConfirm_(h, df) { var ticker = h.ticker; var name = h.name || df.name || ''; // ── 입력 수집 (실제 필드 우선, 프록시 fallback) ────────────────────────── var daysSince = typeof df.daysSinceBreakout === 'number' ? df.daysSinceBreakout : null; var retSince = typeof df.retSinceBreakout === 'number' ? df.retSinceBreakout : null; var volToday = typeof df.volume === 'number' ? df.volume : null; var volBreakout = typeof df.volumeBreakoutDay === 'number' ? df.volumeBreakoutDay : null; // 프록시: daysSinceBreakout — close vs MA20 돌파여부로 추정 // MA20 이하에서 위로 올라온 직후이면 daysSince=0, 그 이전이면 null if (daysSince === null) { var close = df.close || h.close || 0; var ma20 = df.ma20 || 0; var prevClose = df.prevClose || close; // 오늘 ma20 상향 돌파면 Day 0 if (close > 0 && ma20 > 0 && close > ma20 && prevClose <= ma20) { daysSince = 0; } // 이미 ma20 위에 있고 ret5d 존재 → days를 ret5d로 추정(보수적 5일 상한) else if (close > 0 && ma20 > 0 && close > ma20 && typeof df.ret5d === 'number') { // 5일 기준 프록시: 상승률이 클수록 이미 많이 경과했다고 가정 daysSince = df.ret5d >= 7 ? 8 : df.ret5d >= 3 ? 4 : 2; } } // 프록시: retSinceBreakout — ret5d 사용 if (retSince === null && typeof df.ret5d === 'number') { retSince = df.ret5d; } // 프록시: volBreakoutDay — avgVolume5d 사용 if (volBreakout === null && typeof df.avgVolume5d === 'number') { volBreakout = df.avgVolume5d; } // ── 상태 분류 ────────────────────────────────────────────────────────────── var state, result, reasons = []; if (daysSince === null) { state = 'PENDING_DATA'; result = 'WATCH_NO_BREAKOUT_TRACKED'; reasons.push('days_since_breakout_null'); } else if (daysSince === 0) { state = 'BREAKOUT_DAY_1'; result = 'WATCH_FOLLOW_THROUGH_PENDING'; reasons.push('day0_no_immediate_buy'); } else if (daysSince > 7) { state = 'EXTENDED_FOLLOW'; result = 'WATCH_TOO_LATE'; reasons.push('days_since_gt7'); } else { // daysSince 2~7 범위 var volOk = (volToday !== null && volBreakout !== null && volBreakout > 0) ? (volToday >= volBreakout * 0.9) : true; // 데이터 없으면 통과 var retOk = (retSince !== null) ? (retSince >= 1.5) : false; if (retOk && volOk) { state = 'FOLLOW_THROUGH_OK'; result = 'BUY_PILOT_ALLOWED'; reasons.push('days_' + daysSince + '_ret_' + (retSince !== null ? retSince.toFixed(1) : 'N/A')); if (volOk) reasons.push('vol_confirmed'); } else { state = 'FOLLOW_THROUGH_FAIL'; result = 'WATCH_RESET_REQUIRED'; if (!retOk) reasons.push('ret_since_lt1.5pct'); if (!volOk) reasons.push('vol_lt90pct_breakout_day'); } } return { ticker: ticker, name: name, days_since_breakout: daysSince, ret_since_breakout: retSince, vol_ratio_vs_breakout_day: (volToday !== null && volBreakout !== null && volBreakout > 0) ? Math.round(volToday / volBreakout * 100) / 100 : null, follow_through_state: state, follow_through_result: result, reason_codes: reasons, formula_id: 'FOLLOW_THROUGH_DAY_CONFIRM_V1', version: '2026-05-20_HARNESS_V5' }; } function calcApexExecutionHarness_(holdings, dfMap, sectorFlowData, kospiRet5d, h1, h2, h3, h4, orderBlueprint, cashShortfallInfo, marketRegime) { var alphaLead = []; var followThrough = []; var distribution = []; var profitPreservation = []; var entryFreshness = []; var cashRaisePlan = []; var reboundTriggers = []; var smartSellQty = []; var sellValuePreservation = []; var executionQuality = []; var buyPermission = []; var limitPolicy = []; var benchmarkRelativeRows = []; var indexRelativeHealthRows = []; var saqgRows = []; var cashCreationLockRows = []; // ── [2026-05-20_HARNESS_V5] 신규 V5 게이트 결과 배열 var breakoutQualityGate = []; var antiWhipsawGate = []; var smartCashRaiseV2 = []; var followThroughConfirm = []; var blockCount = 0; var regime = marketRegime || 'UNKNOWN'; var priceMap = {}; (h4.prices || []).forEach(function(p) { priceMap[p.ticker] = p; }); var sellQtyMap = {}; (h3.sellQty || []).forEach(function(s) { sellQtyMap[s.ticker] = s; }); holdings.forEach(function(h) { var df = dfMap[h.ticker] || {}; var distRow = calcDistributionRiskRow_(h, df, kospiRet5d, sectorFlowData); // [PROPOSAL50] P1-B: DSD V1.1 — SIG_7/SIG_8 추가, weighted_sum 5.0/3.0 상향 applyDsdV1_1Signals_([distRow], dfMap); var alphaRow = calcAlphaLeadRow_(h, df, sectorFlowData, distRow); var ftRow = calcFollowThroughRow_(h, df); var priceRow = priceMap[h.ticker] || {}; var profitRow = calcProfitPreservationRow_(h, df, priceRow, distRow); var orderRow = findOrderBlueprintRow_(orderBlueprint, h.ticker) || {}; var eqRow = calcExecutionQualityRow_(h.ticker, orderRow, df); var saqgState = df.saqg_v1 || (h.position_type === 'core' ? 'EXEMPT' : 'WATCHLIST_ONLY'); var cand = findCandidateByTicker_(h2.candidates, h.ticker) || {}; var sq = sellQtyMap[h.ticker] || {}; var tradePlan = calcApexTradePlan_( h, df, h1, alphaRow, ftRow, distRow, priceRow, orderRow, sq, profitRow, cashShortfallInfo, saqgState ); var buyState = tradePlan.buyState; var buyReasons = tradePlan.buyReasons; if (buyState === 'BLOCKED') blockCount++; var style = tradePlan.style; var immediateQty = tradePlan.immediateQty; var reboundQty = tradePlan.reboundQty; var k2Emergency = tradePlan.k2Emergency; var tranchePhase = tradePlan.tranchePhase; var currentTrancheAllowedPct = tradePlan.currentTrancheAllowedPct; var nextTrancheCondition = tradePlan.nextTrancheCondition; var normalizedSellPrice = tradePlan.normalizedSellPrice; var normalizedBuyPrice = tradePlan.normalizedBuyPrice; var htsLimitPrice = tradePlan.htsLimitPrice; var close = h.close || df.close || 0; var atr20 = df.atr20 || 0; var holdingQty = h.holdingQty || 0; var prevClose = df.prevClose || close; // ── [2026-05-20_HARNESS_V5] V5 게이트 산출 ────────────────────────────── var bqRow = calcBreakoutQualityGate_(h, df, alphaRow, distRow); var awRow = calcAntiWhipsawGate_(h, df, kospiRet5d); var scrV2 = calcSmartCashRaiseV2_(h, df, profitRow, priceRow, cashShortfallInfo); var ftdRow = calcFollowThroughDayConfirm_(h, df); // H6: 뒷박 차단 — BUY 상태 override if (bqRow.breakout_quality_gate === 'BLOCKED_LATE_CHASE') { if (buyState !== 'BLOCKED') { buyState = 'BLOCKED'; } buyReasons.push('breakout_quality_BLOCKED_LATE_CHASE'); blockCount++; } // Gate 4b: FTD 미확인 — BUY 차단 (돌파 당일 즉시 매수 금지, 데이터 부재 시 WATCH로 후퇴) if (ftdRow.follow_through_result === 'WATCH_FOLLOW_THROUGH_PENDING' || ftdRow.follow_through_result === 'WATCH_RESET_REQUIRED') { if (buyState === 'ALLOW_PILOT') { buyState = 'WATCH'; // PILOT → WATCH (BLOCKED 아님 — 관찰 유지) buyReasons.push('ftd_' + ftdRow.follow_through_result); } } else if (ftdRow.follow_through_result === 'WATCH_TOO_LATE') { if (buyState === 'ALLOW_PILOT') { buyState = 'WATCH'; buyReasons.push('ftd_WATCH_TOO_LATE'); } } // H7: 가짜 매도 차단 — V1.1: CONFIRMED/WEAKENING만 보류 표기 (AUTO_RELEASED 제외) if (awRow.anti_whipsaw_gate === 'WHIPSAW_CONFIRMED' || awRow.anti_whipsaw_gate === 'WHIPSAW_WEAKENING') { buyReasons.push('whipsaw_hold_' + (awRow.anti_whipsaw_hold_days || 1) + 'd'); } distribution.push(distRow); alphaLead.push(alphaRow); followThrough.push(ftRow); profitPreservation.push(profitRow); benchmarkRelativeRows.push({ ticker: h.ticker, name: h.name || df.name || '', stock_drawdown_from_high_pct: typeof df.stock_drawdown_from_high_pct === 'number' ? df.stock_drawdown_from_high_pct : null, excess_drawdown_pctp: typeof df.excess_drawdown_pctp === 'number' ? df.excess_drawdown_pctp : null, recovery_ratio_5d: typeof df.recovery_ratio_5d === 'number' ? df.recovery_ratio_5d : null, recovery_ratio_20d: typeof df.recovery_ratio_20d === 'number' ? df.recovery_ratio_20d : null, downside_beta: typeof df.downside_beta === 'number' ? df.downside_beta : null, rs_line_20d_slope: typeof df.rs_line_20d_slope === 'number' ? df.rs_line_20d_slope : null, rs_line_60d_slope: typeof df.rs_line_60d_slope === 'number' ? df.rs_line_60d_slope : null, brt_verdict: df.brt_verdict || 'UNKNOWN', brt_method: df.brt_method || 'DATA_MISSING', formula_id: 'BENCHMARK_RELATIVE_TIMESERIES_V1' }); var indexRelRow = calcIndexRelativeHealthGate_(h, df, kospiRet5d); indexRelativeHealthRows.push(indexRelRow); saqgRows.push({ ticker: h.ticker, name: h.name || df.name || '', position_type: h.position_type || 'unknown', saqg_v1: saqgState, saqg_penalty: typeof df.saqg_penalty === 'number' ? df.saqg_penalty : null, saqg_failed_filters: df.saqg_failed_filters || '', hts_allowed: saqgState === 'ELIGIBLE' || saqgState === 'EXEMPT', formula_id: 'SATELLITE_ALPHA_QUALITY_GATE_V1' }); breakoutQualityGate.push(bqRow); antiWhipsawGate.push(awRow); smartCashRaiseV2.push(scrV2); followThroughConfirm.push(ftdRow); executionQuality.push(eqRow); // ── 진입 신선도 게이트 (ENTRY_FRESHNESS_GATE_V1) ─────────────────────── var freshnessState = 'FRESH_PILOT'; var freshnessReasons = []; if (bqRow.breakout_quality_gate === 'BLOCKED_LATE_CHASE' || alphaRow["late_chase_risk_score"] >= 70) { freshnessState = 'BLOCK_LATE_CHASE'; freshnessReasons.push('late_chase'); } else if (ftRow.follow_through_state === 'WAIT_PULLBACK' || ftdRow.follow_through_result === 'WATCH_TOO_LATE' || ftdRow.follow_through_result === 'WATCH_RESET_REQUIRED') { freshnessState = 'PULLBACK_WAIT'; freshnessReasons.push('follow_through_wait'); } else if (distRow.pre_distribution_warning === 'EARLY_WARNING') { freshnessState = 'STALE_REVIEW'; freshnessReasons.push('pre_distribution_warning'); } else if (buyState === 'WATCH' || buyState === 'BLOCKED') { freshnessState = 'WATCH_FRESHNESS'; freshnessReasons.push('buy_state_' + buyState.toLowerCase()); } if (indexRelRow.relative_health_state === 'DECOUPLED' || indexRelRow.relative_health_state === 'OVER_EXTENDED') { freshnessState = freshnessState === 'FRESH_PILOT' ? 'WATCH_FRESHNESS' : freshnessState; freshnessReasons.push('index_relative_' + String(indexRelRow.relative_health_state).toLowerCase()); if (buyState === 'ALLOW_PILOT' || buyState === 'ALLOW_ADD_ON') { buyState = 'WATCH'; buyReasons.push('index_relative_' + String(indexRelRow.relative_health_state).toLowerCase()); } } else if (indexRelRow.relative_health_state === 'UNDERPERFORMING') { if (buyState === 'ALLOW_PILOT' || buyState === 'ALLOW_ADD_ON') { buyState = 'WATCH'; } freshnessReasons.push('index_relative_underperforming'); } entryFreshness.push({ ticker: h.ticker, name: h.name || df.name || '', alpha_lead_score: alphaRow.alpha_lead_score != null ? alphaRow.alpha_lead_score : null, ["late_chase_risk_score"]: alphaRow["late_chase_risk_score"] != null ? alphaRow["late_chase_risk_score"] : null, follow_through_state: ftRow.follow_through_state || null, breakout_quality_gate: bqRow.breakout_quality_gate || null, pre_distribution_warning: distRow.pre_distribution_warning || 'NONE', t20_alpha_gate: null, freshness_state: freshnessState, reason_codes: freshnessReasons, formula_id: 'ENTRY_FRESHNESS_GATE_V1' }); // ── 회복 보존 매도 게이트 (SELL_VALUE_PRESERVATION_GATE_V1) ───────────── var sellPreserveState = 'HOLD'; var sellPreserveReasons = []; if (scrV2.smart_cash_raise_route === 'ROUTE_D' || k2Emergency || scrV2.stop_breach_gate === 'BREACH') { sellPreserveState = 'EMERGENCY_EXIT'; sellPreserveReasons.push('route_d_or_breach'); } else if (awRow.anti_whipsaw_gate === 'WHIPSAW_CONFIRMED' || awRow.anti_whipsaw_gate === 'WHIPSAW_WEAKENING') { sellPreserveState = 'REBOUND_CONFIRM_HOLD'; sellPreserveReasons.push('whipsaw_hold_' + (awRow.anti_whipsaw_hold_days || 1) + 'd'); } else if (style === 'OVERSOLD_REBOUND_SELL' && reboundQty > 0) { sellPreserveState = 'STAGED_REBOUND'; sellPreserveReasons.push('rebound_wait_qty'); } else if (profitRow.profit_preservation_state === 'PROFIT_LOCK_10' || profitRow.profit_preservation_state === 'PROFIT_LOCK_20' || profitRow.profit_preservation_state === 'PROFIT_LOCK_30' || profitRow.profit_preservation_state === 'APEX_TRAILING') { sellPreserveState = 'PRESERVE_TIERED'; sellPreserveReasons.push('profit_lock'); } else if (distRow.anti_distribution_state === 'BLOCK_BUY') { sellPreserveState = 'TRIM_ONLY'; sellPreserveReasons.push('distribution_exit'); } else if (indexRelRow.relative_health_state === 'OVER_EXTENDED' || indexRelRow.relative_health_state === 'DECOUPLED') { if (style !== 'OVERSOLD_REBOUND_SELL') { sellPreserveState = 'TRIM_ONLY'; } sellPreserveReasons.push('index_relative_' + String(indexRelRow.relative_health_state).toLowerCase()); } sellValuePreservation.push({ ticker: h.ticker, name: h.name || df.name || '', profit_preservation_state: profitRow.profit_preservation_state || 'NORMAL', cash_raise_group: style, anti_whipsaw_gate: awRow.anti_whipsaw_gate || null, immediate_qty: immediateQty > 0 ? immediateQty : null, rebound_wait_qty: reboundQty > 0 ? reboundQty : null, auto_trailing_stop: profitRow.auto_trailing_stop || null, sell_value_preservation_state: sellPreserveState, reason_codes: sellPreserveReasons, formula_id: 'SELL_VALUE_PRESERVATION_GATE_V1' }); // K1: 트랜치 엔진 결과 포함 buy_permission_json buyPermission.push({ ticker: h.ticker, name: h.name || df.name || '', buy_permission_state: buyState, max_tranche_pct: buyState === 'ALLOW_PILOT' ? 30 : buyState === 'ALLOW_ADD_ON' ? 60 : 0, tranche_phase: tranchePhase, current_tranche_allowed_pct: currentTrancheAllowedPct, next_tranche_condition: nextTrancheCondition, blocked_reason_codes: buyReasons, position_type: h.position_type || 'unknown', brt_verdict: df.brt_verdict || null, saqg_v1: saqgState, rs_verdict: df.rs_verdict || null, composite_verdict: df.composite_verdict || null, rag_v1: df.rag_v1 || null, formula_id: 'BUY_PERMISSION_MATRIX_V1+STAGED_ENTRY_TRANCHE_V1' }); // K2: 반등 대기 분할 매도 결과 포함 cash_raise_plan_json cashRaisePlan.push({ ticker: h.ticker, name: h.name || df.name || '', rank: cand.rank || null, execution_style: style, immediate_qty: immediateQty > 0 ? immediateQty : null, rebound_wait_qty: reboundQty > 0 ? reboundQty : null, emergency_full_sell: k2Emergency, max_daily_qty: Math.floor(holdingQty * 0.50), expected_immediate_krw: immediateQty > 0 ? Math.round(immediateQty * close) : 0, cash_shortfall_min_krw: (cashShortfallInfo && cashShortfallInfo.cash_shortfall_min_krw) || 0, formula_id: 'SMART_CASH_RAISE_PLAN_V1+K2_STAGED_REBOUND_SELL' }); // K2: 반등 트리거 조건부 잔여 수량 var reboundTriggerPrice = null; if (style === 'OVERSOLD_REBOUND_SELL' && reboundQty > 0) { // 반등 트리거: prevClose + 0.5×ATR 또는 단순 close + 0.3×ATR reboundTriggerPrice = atr20 > 0 ? tickNormalize_((prevClose > 0 ? prevClose : close) + atr20 * 0.5) : null; } reboundTriggers.push({ ticker: h.ticker, rebound_trigger_state: (style === 'OVERSOLD_REBOUND_SELL' && reboundQty > 0) ? 'WAIT_REBOUND_TRIGGER' : 'NOT_APPLICABLE', trigger_price: reboundTriggerPrice, rebound_sell_qty: reboundQty > 0 ? reboundQty : null, emergency_override: k2Emergency, formula_id: 'REBOUND_SELL_TRIGGER_V1' }); smartSellQty.push({ ticker: h.ticker, immediate_sell_qty: immediateQty > 0 ? immediateQty : null, staged_total_qty: (typeof sq.sell_qty === 'number' && sq.sell_qty > 0) ? sq.sell_qty : null, rebound_wait_qty: reboundQty > 0 ? reboundQty : null, emergency_full_sell: k2Emergency, expected_cash_recovered_krw: immediateQty > 0 ? Math.round(immediateQty * close) : 0, formula_id: 'SELL_QUANTITY_ALLOCATOR_V1+K2_STAGED_REBOUND_SELL' }); // J5: 스타일별 실제 지정가 산출 결과 포함 limit_price_policy_json limitPolicy.push({ ticker: h.ticker, execution_style: style, sell_limit_price: normalizedSellPrice, buy_limit_price: normalizedBuyPrice, hts_limit_price: htsLimitPrice, tick_status: htsLimitPrice ? 'TICK_OK' : 'NO_EXECUTION_PRICE', sell_price_basis: style === 'URGENT_LIQUIDITY_TRIM' ? 'min(close,prevClose×0.998)' : style === 'OVERSOLD_REBOUND_SELL' ? 'close_no_undercut' : style === 'DISTRIBUTION_EXIT' ? 'close-0.25×ATR20' : style === 'PROFIT_PROTECT_TRIM' ? 'ratchet_stop_or_close×0.999' : 'close', formula_id: 'LIMIT_PRICE_POLICY_V1' }); }); // K3: 국면·섹터 연계 H2 동적 우선순위 var regimeAdjPriority = calcRegimeAdjustedSellPriority_( h2.candidates, regime, dfMap, kospiRet5d ); // ── [2026-05-21_CLA_HARNESS_V1] SATELLITE_FAILURE_GATE_V1 ──────────────────── var satelliteRowsForSFG = []; holdings.forEach(function(h) { if (h.position_type !== 'core') { var df = dfMap[h.ticker] || {}; satelliteRowsForSFG.push({ composite_verdict: df.composite_verdict || null, rs_verdict: df.rs_verdict || null, ret20d: typeof df.ret20d === 'number' ? df.ret20d : null, excess_ret_10d: typeof df.excess_ret_10d === 'number' ? df.excess_ret_10d : null }); } }); var sfgResult = calcSatelliteFailureGate_(satelliteRowsForSFG); var sapgResult = calcSatelliteAggregatePnlGate_(holdings); holdings.forEach(function(h) { var df = dfMap[h.ticker] || {}; cashCreationLockRows.push(calcCashCreationPurposeLockRow_(h, df, sfgResult)); }); // ── [2026-05-21_AEW_V1] ALPHA_EVALUATION_WINDOW_V1 ────────────────────────── var aewRows = calcAlphaEvaluationWindow_(holdings, dfMap); // SFG-1: TRIGGERED 시 위성 BUY 전면 차단 (post-processing) if (sfgResult.sfg_v1 === 'TRIGGERED' || sapgResult.sapg_status === 'SAPG_CRITICAL') { buyPermission.forEach(function(bp) { var h = holdings.find(function(x) { return x.ticker === bp.ticker; }); if (h && h.position_type !== 'core') { if (bp.buy_permission_state !== 'BLOCKED') { bp.buy_permission_state = 'BLOCKED'; bp.blocked_reason_codes = (bp.blocked_reason_codes || []).concat([ sfgResult.sfg_v1 === 'TRIGGERED' ? 'sfg_v1_TRIGGERED' : 'sapg_CRITICAL' ]); } } }); } // ── [QEH010] WHIPSAW V1.1 → order_blueprint validation_status 소급 차단 ── // V1.1: WHIPSAW_CONFIRMED(hold_3d) + WHIPSAW_WEAKENING(hold_1d) 차단 // WHIPSAW_AUTO_RELEASED(hold_0d)은 자동 해제 — 차단 안 함 var whipsawTickers_ = {}; antiWhipsawGate.forEach(function(aw) { if (aw.anti_whipsaw_gate === 'WHIPSAW_CONFIRMED' || aw.anti_whipsaw_gate === 'WHIPSAW_WEAKENING') { whipsawTickers_[aw.ticker] = aw.anti_whipsaw_hold_days || 1; } }); var SELL_ORDER_TYPES_ = { SELL: 1, TRIM: 1, EXIT_100: 1, EXIT_FULL: 1 }; orderBlueprint.forEach(function(bp) { var wHoldDays = whipsawTickers_[bp.ticker]; if (wHoldDays && SELL_ORDER_TYPES_[bp.order_type] && bp.validation_status === 'PASS') { bp.validation_status = 'BLOCKED'; bp.rationale_code = 'WHIPSAW_V1_1:hold_' + wHoldDays + 'd'; } }); // ── [2026-05-20_HARNESS_V5] V5 포트폴리오 레벨 집계 var smartCashRaiseRoute = 'NO_ACTION'; for (var sci = 0; sci < smartCashRaiseV2.length; sci++) { if (smartCashRaiseV2[sci].smart_cash_raise_route !== 'NO_ACTION') { smartCashRaiseRoute = smartCashRaiseV2[sci].smart_cash_raise_route; break; // 첫 번째 실제 경로를 포트폴리오 레벨 대표 경로로 설정 } } return { alpha_lead_json: alphaLead, follow_through_json: followThrough, distribution_risk_json: distribution, profit_preservation_json: profitPreservation, entry_freshness_json: entryFreshness, cash_raise_plan_json: cashRaisePlan, rebound_sell_trigger_json: reboundTriggers, smart_sell_quantities_json: smartSellQty, sell_value_preservation_json: sellValuePreservation, execution_quality_json: executionQuality, buy_permission_json: buyPermission, limit_price_policy_json: limitPolicy, regime_adjusted_sell_priority_json: regimeAdjPriority, benchmark_relative_timeseries_json: benchmarkRelativeRows, index_relative_health_json: indexRelativeHealthRows, saqg_json: saqgRows, cash_creation_purpose_lock_json: cashCreationLockRows, // ── [2026-05-20_HARNESS_V5] 신규 V5 출력 ────────────────────────────── breakout_quality_gate_json: breakoutQualityGate, anti_whipsaw_gate_json: antiWhipsawGate, smart_cash_raise_json: smartCashRaiseV2, smart_cash_raise_route: smartCashRaiseRoute, follow_through_confirm_json: followThroughConfirm, breakout_quality_gate_lock: true, anti_whipsaw_gate_lock: true, follow_through_lock: true, follow_through_confirm_lock: true, apex_block_count: blockCount, // ── [2026-05-21_CLA_HARNESS_V1] 신규 하네스 출력 ────────────────────────── satellite_failure_gate_json: sfgResult, sapg_json: sapgResult, // ── [2026-05-21_AEW_V1] ───────────────────────────────────────────────────── alpha_evaluation_window_json: aewRows, sfg_v1_lock: true }; } // ═══════════════════════════════════════════════════════════════════════════════ // [2026-05-23_PROPOSAL46] PA1~PA5 신규 하네스 calc 함수 // spec/13b_harness_formulas.yaml: PA1 PREDICTIVE_ALPHA_ENGINE_V1 // PA2 ANTI_LATE_ENTRY_GATE_V2 // PA3 CASH_PRESERVATION_SELL_ENGINE_V2 // PA4 MACRO_EVENT_SYNCHRONIZER_V1 // PA5 CONSISTENCY_VALIDATOR_V2 // ═══════════════════════════════════════════════════════════════════════════════ /** * [PROPOSAL47_B6 / PROPOSAL48_B6_FALLBACK] prediction_accuracy_rate 읽기. * 우선순위: ① monthly_history.prediction_accuracy_rate * ② settings.prediction_accuracy_rate * ③ 상수 기본값 48.48 (운영 중 실측값으로 교체 예정) * 값이 0~1 범위면 *100 변환, 0~100 범위면 그대로 사용. */ var PREDICTION_ACCURACY_RATE_DEFAULT_ = 48.48; // 2026-05-23 실측, 매월 갱신 function getPredictionAccuracyRate_() { function parseAccuracy_(val) { if (val === '' || val === null || val === undefined) return null; var num = typeof val === 'number' ? val : parseFloat(String(val)); if (isNaN(num)) return null; return num <= 1 ? Math.round(num * 1000) / 10 : num; } try { var ss = getSpreadsheet_(); // ① monthly_history 시트 var sh = ss.getSheetByName('monthly_history'); if (sh) { var mhData = sh.getDataRange().getValues(); if (mhData && mhData.length >= 2) { var header = mhData[0] || []; var colIdx = -1; for (var i = 0; i < header.length; i++) { if (String(header[i]).trim().toLowerCase() === 'prediction_accuracy_rate') { colIdx = i; break; } } if (colIdx >= 0) { for (var r = mhData.length - 1; r >= 1; r--) { var parsed = parseAccuracy_(mhData[r][colIdx]); if (parsed !== null) return parsed; } } } } // ② settings 시트 (Key-Value 구조) var settingsSh = ss.getSheetByName('settings'); if (settingsSh) { var sData = settingsSh.getDataRange().getValues(); for (var si = 0; si < sData.length; si++) { var key = String(sData[si][0] || '').trim().toLowerCase(); if (key === 'prediction_accuracy_rate') { var parsed2 = parseAccuracy_(sData[si][1]); if (parsed2 !== null) return parsed2; } } } } catch(e) { /* fallback to default */ } // ③ 상수 기본값 return PREDICTION_ACCURACY_RATE_DEFAULT_; } /** * [PA1 V1.2] 팩터 가중치 오버라이드 읽�� * settings 시트의 pa1_w_ 키-값을 읽어 기본값과 병합. * 오버라이드가 존재하면 _source='DYNAMIC', 없으면 'STATIC'. */ function getPa1WeightOverrides_() { var defaults = { pullback_entry: 20, flow_strong: 20, rs_leader: 15, volume_confirm: 15, rsi_healthy: 15, brt_leader: 15, chase_risk: 25, distribution: 20, rsi_overbought: 20, foreign_sell: 15, usd_krw_weak: 10, stale_position: 10, _source: 'STATIC' }; try { var ss = getSpreadsheet_(); var sh = ss.getSheetByName('settings'); if (!sh) return defaults; var data = sh.getDataRange().getValues(); var overrides = {}; for (var i = 0; i < data.length; i++) { var key = String(data[i][0] || '').trim(); if (key.indexOf('pa1_w_') !== 0) continue; var factorName = key.slice(6); // 'pa1_w_' = 6자 var val = parseFloat(String(data[i][1] || '')); if (!isNaN(val) && val >= 0 && val <= 50) overrides[factorName] = val; } if (Object.keys(overrides).length === 0) return defaults; var merged = {}; for (var k in defaults) merged[k] = defaults[k]; for (var k in overrides) merged[k] = overrides[k]; merged._source = 'DYNAMIC'; return merged; } catch(e) { return defaults; } } /** * [PA1 V1.3] T+5 피드백 기록 * STRONG_BUY_SIGNAL / EXIT_SIGNAL / TRIM_SIGNAL 예측 → pa1_feedback 시트 기록. * V1.3: TRIM_SIGNAL 추가, signal_type 컬럼 추가 (BUY/SELL 분리 정확도 추적) * evaluatePa1FeedbackBatch_() 주간 배치에서 결과를 평가. */ function recordPa1FeedbackEntry_(paeRows, dfMap) { if (!paeRows || !paeRows.length) return; // [V1.3] TRIM_SIGNAL 추가 var RECORD_VERDICTS = { STRONG_BUY_SIGNAL: 1, EXIT_SIGNAL: 1, TRIM_SIGNAL: 1 }; var toRecord = paeRows.filter(function(pa) { return !!RECORD_VERDICTS[pa.synthesis_verdict]; }); if (!toRecord.length) return; try { var ss = getSpreadsheet_(); var sh = ss.getSheetByName('pa1_feedback'); if (!sh) { sh = ss.insertSheet('pa1_feedback'); sh.appendRow(['date','ticker','synthesis_verdict','direction_confidence', 'close_at_record','signal_type','t5_evaluated','t5_return_pct','t5_correct']); } else { // [V1.3] signal_type 컬럼 없으면 헤더 확인 — 없어도 appendRow는 동작함 } var today = Utilities.formatDate(new Date(), 'Asia/Seoul', 'yyyy-MM-dd'); toRecord.forEach(function(pa) { var df = dfMap[pa.ticker] || {}; var closeNow = df.close || 0; var signalType = (pa.synthesis_verdict === 'STRONG_BUY_SIGNAL') ? 'BUY' : 'SELL'; sh.appendRow([today, pa.ticker, pa.synthesis_verdict, pa.direction_confidence, closeNow, signalType, false, '', '']); }); } catch(e) { Logger.log('[PA1_FEEDBACK] recordPa1FeedbackEntry_ error: ' + e.message); } } /** * [PA1 V1.3] 매도 PASS 정확도 조회 * pa1_feedback 시트에서 signal_type=SELL + t5_evaluated=true 행의 정확도 산출. * @return {number|null} sell_pass_accuracy_rate (0~100) or null if insufficient data */ function getSellPassAccuracyRate_() { try { var ss = getSpreadsheet_(); var fbSh = ss.getSheetByName('pa1_feedback'); if (!fbSh) return null; var data = fbSh.getDataRange().getValues(); if (data.length < 2) return null; var header = data[0]; var COL = {}; header.forEach(function(h, i) { COL[String(h)] = i; }); if (COL['signal_type'] == null || COL['t5_evaluated'] == null || COL['t5_correct'] == null) return null; var sellRows = data.slice(1).filter(function(row) { return String(row[COL['signal_type']] || '').toUpperCase() === 'SELL' && (row[COL['t5_evaluated']] === true || String(row[COL['t5_evaluated']]).toUpperCase() === 'TRUE'); }); if (sellRows.length < 5) return null; var correct = sellRows.filter(function(row) { return row[COL['t5_correct']] === true || String(row[COL['t5_correct']]).toUpperCase() === 'TRUE'; }).length; return Math.round(correct / sellRows.length * 1000) / 10; } catch(e) { Logger.log('[PA1_V1.3] getSellPassAccuracyRate_ error: ' + e.message); return null; } } /** * [PA1 V1.2] 주간 배치 — T+5(7캘린더일) 결과 평가 + prediction_accuracy_rate 갱신 * GAS 트리거에 주 1회 등록해 사용 (매주 월요일 권장). */ function evaluatePa1FeedbackBatch_() { try { var ss = getSpreadsheet_(); var fbSh = ss.getSheetByName('pa1_feedback'); if (!fbSh) { Logger.log('[PA1_V1.2] pa1_feedback 시트 없음'); return; } var data = fbSh.getDataRange().getValues(); if (data.length < 2) return; var header = data[0]; var COL = {}; header.forEach(function(h, i) { COL[String(h)] = i; }); var reqCols = ['date','ticker','synthesis_verdict','close_at_record','t5_evaluated','t5_return_pct','t5_correct']; for (var ci = 0; ci < reqCols.length; ci++) { if (COL[reqCols[ci]] == null) { Logger.log('[PA1_V1.2] 컬럼 누락: ' + reqCols[ci]); return; } } // 현재 종가 맵 (data_feed 시트) 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 ri = 1; ri < dfData.length; ri++) { var t = String(dfData[ri][tCol] || '').trim(); var c = parseFloat(String(dfData[ri][cCol] || '')); if (t && !isNaN(c) && c > 0) priceMap[t] = c; } } } } var todayMs = new Date().getTime(); var evalThisRun = 0; for (var i = 1; i < data.length; i++) { var row = data[i]; var evaled = row[COL['t5_evaluated']]; if (evaled === true || String(evaled).toUpperCase() === 'TRUE') continue; var daysDiff = (todayMs - new Date(row[COL['date']]).getTime()) / 86400000; if (daysDiff < 7) continue; var ticker = String(row[COL['ticker']] || ''); var verdict = String(row[COL['synthesis_verdict']] || ''); var closeAt = parseFloat(String(row[COL['close_at_record']] || '')); var closeNow = priceMap[ticker] || 0; if (closeAt <= 0 || closeNow <= 0) continue; var t5Ret = Math.round((closeNow - closeAt) / closeAt * 10000) / 100; var isCorrect = (verdict === 'STRONG_BUY_SIGNAL') ? (t5Ret > 0) : (t5Ret < 0); fbSh.getRange(i + 1, COL['t5_evaluated'] + 1).setValue(true); fbSh.getRange(i + 1, COL['t5_return_pct'] + 1).setValue(t5Ret); fbSh.getRange(i + 1, COL['t5_correct'] + 1).setValue(isCorrect ? 'CORRECT' : 'WRONG'); evalThisRun++; } // prediction_accuracy_rate 갱신 (최소 10건 평가 완료 후) var freshData = fbSh.getDataRange().getValues(); var allEval = 0, allCorrect = 0; for (var j = 1; j < freshData.length; j++) { var ev = freshData[j][COL['t5_evaluated']]; if (ev !== true && String(ev).toUpperCase() !== 'TRUE') continue; allEval++; if (String(freshData[j][COL['t5_correct']] || '') === 'CORRECT') allCorrect++; } if (allEval >= 10) { var newRate = Math.round(allCorrect / allEval * 1000) / 10; var settingSh = ss.getSheetByName('settings'); if (settingSh) { var sData = settingSh.getDataRange().getValues(); var updated = false; for (var si = 0; si < sData.length; si++) { if (String(sData[si][0] || '').trim().toLowerCase() === 'prediction_accuracy_rate') { settingSh.getRange(si + 1, 2).setValue(newRate); updated = true; break; } } if (!updated) settingSh.appendRow(['prediction_accuracy_rate', newRate]); Logger.log('[PA1_V1.2] prediction_accuracy_rate=' + newRate + '% (' + allCorrect + '/' + allEval + ')'); } } Logger.log('[PA1_V1.2] evaluatePa1FeedbackBatch_ 완료: 이번 평가=' + evalThisRun + '건'); // [PA1 V1.2] 정확도 기반 가중치 자동 조정 (평가 완료 후) if (allEval >= 10) { var accuracy7d = allCorrect / allEval; adjustPaeWeights_(); } } catch(e) { Logger.log('[PA1_V1.2] evaluatePa1FeedbackBatch_ 오류: ' + e.message); } } /** * [PA1 V1.2] adjustPaeWeights_ * T+5 예측 정확도(7일) 기반으로 thesis/antithesis 가중치 자동 조정. * 조정값을 settings 시트에 pa1_w_ 형태로 기록 → 다음 실행 시 반영. */ function adjustPaeWeights_() { try { // 현재 precision 읽기 var accRate = getPredictionAccuracyRate_(); if (accRate === null) return; // 데이터 부족 시 조정 안 함 var accuracy = accRate / 100; // 0~1 범위로 변환 var ss = getSpreadsheet_(); var settingSh = ss.getSheetByName('settings'); if (!settingSh) return; var sData = settingSh.getDataRange().getValues(); var currentWeights = {}; var rowIndex = {}; sData.forEach(function(row, i) { var key = String(row[0] || '').trim().toLowerCase(); if (key.indexOf('pa1_w_') === 0) { currentWeights[key] = parseFloat(String(row[1] || '')) || null; rowIndex[key] = i + 1; // 1-based } }); // 기본 thesis/antithesis 총합 (12개 팩터 기본 가중치 합) var DEFAULT_THESIS_TOTAL = 100; // 20+20+15+15+15+15 var DEFAULT_ANTI_TOTAL = 100; // 25+20+20+15+10+10 // 조정 방향 결정 var adjustThesis = 0; var adjustAnti = 0; if (accuracy < 0.55) { // 정확도 낮음 → antithesis 강화 (+5% of base) adjustThesis = -5; adjustAnti = +5; } else if (accuracy > 0.75) { // 정확도 높음 → thesis 강화 (+3% of base) adjustThesis = +3; adjustAnti = 0; } else { Logger.log('[PA1_V1.2] adjustPaeWeights_: 정확도 정상범위(' + Math.round(accuracy*100) + '%) — 조정 불필요'); return; } // thesis 팩터 가중치 조정 (각 비례 분배) var thesisFactors = ['pullback_entry','flow_strong','rs_leader','volume_confirm','rsi_healthy','brt_leader']; var thesisDefaults = { pullback_entry: 20, flow_strong: 20, rs_leader: 15, volume_confirm: 15, rsi_healthy: 15, brt_leader: 15 }; thesisFactors.forEach(function(f) { var key = 'pa1_w_' + f; var baseW = thesisDefaults[f] || 0; var currentW = currentWeights[key] != null ? currentWeights[key] : baseW; var delta = Math.round(baseW / DEFAULT_THESIS_TOTAL * adjustThesis); var newW = Math.max(5, Math.min(35, currentW + delta)); if (rowIndex[key]) { settingSh.getRange(rowIndex[key], 2).setValue(newW); } else { settingSh.appendRow([key, newW]); } }); // antithesis 팩터 가중치 조정 var antiFactors = ['chase_risk','distribution','rsi_overbought','foreign_sell','usd_krw_weak','stale_position']; var antiDefaults = { chase_risk: 25, distribution: 20, rsi_overbought: 20, foreign_sell: 15, usd_krw_weak: 10, stale_position: 10 }; antiFactors.forEach(function(f) { var key = 'pa1_w_' + f; var baseW = antiDefaults[f] || 0; var currentW = currentWeights[key] != null ? currentWeights[key] : baseW; var delta = Math.round(baseW / DEFAULT_ANTI_TOTAL * adjustAnti); var newW = Math.max(5, Math.min(40, currentW + delta)); if (rowIndex[key]) { settingSh.getRange(rowIndex[key], 2).setValue(newW); } else { settingSh.appendRow([key, newW]); } }); Logger.log('[PA1_V1.2] adjustPaeWeights_ 완료: accuracy=' + Math.round(accuracy*100) + '% adjustThesis=' + adjustThesis + ' adjustAnti=' + adjustAnti); } catch(e) { Logger.log('[PA1_V1.2] adjustPaeWeights_ 오류: ' + e.message); } } /** * updatePa1WeightsManual_ * PA1 팩터 가중치를 Work-1 승인값으로 settings 시트에 직접 기록. * 근거: 기존 8.0x 획일 비율(thesis=30, anti=240) → 2.6x 차별화(thesis=70, anti=185) * 효과: 모든 종목이 EXIT(-83~-95)로 획일화됐던 synthesis가 종목별 차별화됨 * (예: 000270 기아 +20 BULLISH / 005930 삼성전자 -18 BEARISH 등) * 사용법: GAS 에디터 → updatePa1WeightsManual_ 선택 → 실행 */ function updatePa1WeightsManual_() { try { var ss = SpreadsheetApp.getActiveSpreadsheet(); var settingSh = ss.getSheetByName(SETTINGS_SHEET_NAME); if (!settingSh) { Logger.log('[updatePa1WeightsManual_] settings 시트를 찾을 수 없음'); return; } // Work-1 승인 PA1 가중치 (thesis 70pt, antithesis 185pt, ratio=2.6x) var APPROVED_WEIGHTS = { // Thesis 팩터 (개별종목 차별화 강화): 5→10~15 pa1_w_pullback_entry: 15, // 눌림목 진입 — 핵심 타이밍 pa1_w_flow_strong: 15, // 수급 강세 pa1_w_rs_leader: 10, // 상대강도 선도 pa1_w_volume_confirm: 10, // 거래량 확인 pa1_w_rsi_healthy: 10, // RSI 여력 pa1_w_brt_leader: 10, // BRT 선도 // Antithesis 팩터 (핵심만 유지, 획일화 해소): 일부 완화 pa1_w_chase_risk: 40, // 뒷박 위험 — 유지 pa1_w_distribution: 40, // 분배 신호 — 유지 pa1_w_rsi_overbought: 40, // RSI 과열 — 유지 pa1_w_foreign_sell: 30, // 외인 매도 — 완화 (단기 노이즈) pa1_w_usd_krw_weak: 15, // 환율 약세 — 대폭 완화 (전 종목 동일 페널티 방지) pa1_w_stale_position: 20 // 장기보유 페널티 — 완화 }; // settings 시트에서 기존 pa1_w_* 행 인덱스 수집 var data = settingSh.getDataRange().getValues(); var rowIndex = {}; data.forEach(function(row, i) { var key = String(row[0] || '').trim().toLowerCase(); if (key.indexOf('pa1_w_') === 0) { rowIndex[key] = i + 1; // 1-based } }); // 값 쓰기 (존재하면 업데이트, 없으면 추가) var updated = []; var added = []; Object.keys(APPROVED_WEIGHTS).forEach(function(key) { var val = APPROVED_WEIGHTS[key]; if (rowIndex[key]) { settingSh.getRange(rowIndex[key], 2).setValue(val); updated.push(key + '=' + val); } else { settingSh.appendRow([key, val]); added.push(key + '=' + val); } }); var thesisTotal = 15+15+10+10+10+10; var antiTotal = 40+40+40+30+15+20; Logger.log('[updatePa1WeightsManual_] 완료'); Logger.log(' 업데이트: ' + updated.join(', ')); Logger.log(' 신규 추가: ' + (added.length ? added.join(', ') : '없음')); Logger.log(' thesis합=' + thesisTotal + 'pt antithesis합=' + antiTotal + 'pt ratio=' + (antiTotal/thesisTotal).toFixed(1) + 'x'); SpreadsheetApp.getUi().alert( 'PA1 가중치 업데이트 완료\n' + 'thesis합=' + thesisTotal + 'pt / antithesis합=' + antiTotal + 'pt (ratio=' + (antiTotal/thesisTotal).toFixed(1) + 'x)\n' + '업데이트: ' + updated.length + '개 / 추가: ' + added.length + '개\n\n' + '다음 runDataFeed 실행 시 새 가중치가 PA1 계산에 반영됩니다.' ); } catch(e) { Logger.log('[updatePa1WeightsManual_] 오류: ' + e.message); SpreadsheetApp.getUi().alert('오류: ' + e.message); } } /** * PA4 — MACRO_EVENT_SYNCHRONIZER_V1 * 외국인 순매도 연속일·USD/KRW·FOMC·VIX 등 거시 변수를 macro_risk_score로 환산. * heat_gate_adj(-3/-1/0/+1) 및 mega_sell_alert 산출. * @param {Object} macroJson getMacroJson() 반환값 * @param {Array} eventRows getEventRiskJson().events (DaysLeft, Type 컬럼) */ function calcMacroEventSynchronizerV1_(macroJson, eventRows) { return calcMacroEventSynchronizerV1Impl_(macroJson, eventRows); } /** * PA1 — PREDICTIVE_ALPHA_ENGINE_V1 * 正(thesis) + 反(antithesis) = 合(direction_confidence) 3계층 점수. * synthesis_verdict=BEARISH(EXIT/TRIM) → BUY 차단 근거. * @param {Array} holdings * @param {Object} dfMap * @param {Object} macroJson getMacroJson() 반환값 * @param {Object} mesResult calcMacroEventSynchronizerV1_ 반환값 */ function calcPredictiveAlphaEngineV1_(holdings, dfMap, macroJson, mesResult, weightOverrides) { return calcPredictiveAlphaEngineV1Impl_(holdings, dfMap, macroJson, mesResult, weightOverrides); } /** * PA2 — ANTI_LATE_ENTRY_GATE_V2 * 3중 AND 게이트: velocity_1d / velocity_5d / distribution_weighted_sum. * ANTI_CHASING_VELOCITY_V1을 완전 대체. * @param {Array} holdings * @param {Object} dfMap */ function calcAntiLateEntryGateV2_(holdings, dfMap) { return calcAntiLateEntryGateV2Impl_(holdings, dfMap); } /** * PA3 — CASH_PRESERVATION_SELL_ENGINE_V2 * K2(분할) + C1(폭포수) + C2(타이밍)를 통합. 매도 스타일 결정 + value_preservation_score. * h3.sellQty에 수량이 있는 종목만 처리. * @param {Array} holdings * @param {Object} dfMap * @param {Object} cashShortfallInfo calcCashShortfallHarness_ 반환값 * @param {Object} h3 calcQuantities_ 반환값 (.sellQty 배열) */ function calcCashPreservationSellEngineV2_(holdings, dfMap, cashShortfallInfo, h3) { var shortfallKrw = (cashShortfallInfo && cashShortfallInfo.cash_shortfall_min_krw) || 0; var sellQtyMap = {}; ((h3 && h3.sellQty) || []).forEach(function(sq) { if (typeof sq.sell_qty === 'number' && sq.sell_qty > 0) { sellQtyMap[sq.ticker] = Math.floor(sq.sell_qty); } }); var rows = []; holdings.forEach(function(h) { var df = dfMap[h.ticker] || {}; var baseQty = sellQtyMap[h.ticker] || 0; if (baseQty <= 0 && shortfallKrw <= 0) return; var close = h.close || df.close || 0; var prevClose = df.prevClose || close; var rsi14 = typeof df.rsi14 === 'number' ? df.rsi14 : 50; var atr20 = typeof df.atr20 === 'number' ? df.atr20 : (close * 0.02); var stopPrice = h.stopPrice || 0; var frg5d = typeof df.frg5d === 'number' ? df.frg5d : 0; var inst5d = typeof df.inst5d === 'number' ? df.inst5d : 0; var volume = typeof df.volume === 'number' ? df.volume : 0; var avgVol5d = typeof df.avgVolume5d === 'number' ? df.avgVolume5d : 0; // 현금 부족 시 baseQty 추정 (h3 미포함 종목) if (baseQty <= 0 && shortfallKrw > 0 && close > 0) { baseQty = Math.min(Math.floor(shortfallKrw / close), h.holdingQty || 0); } if (baseQty <= 0) return; // distribution weighted_sum (inline) var distWS = 0; if (frg5d < 0) distWS += 2.0; if (inst5d < 0) distWS += 2.0; if (avgVol5d > 0 && volume > avgVol5d * 1.3) distWS += 1.5; if (prevClose > 0 && close < prevClose) distWS += 1.5; if (rsi14 > 70) distWS += 1.0; if (df.acGate === 'BLOCK') distWS += 1.0; var emergencyFullSell = h.stopBreach === true; // ── execution_style 결정 ───────────────────────────────────────────────── var execStyle; if (emergencyFullSell) execStyle = 'EMERGENCY_FULL_EXIT'; else if (rsi14 < 30) execStyle = 'OVERSOLD_REBOUND_SELL'; else execStyle = 'STAGED_WATERFALL'; // ── 수량 산출 ──────────────────────────────────────────────────────────── var immediateQty = 0, reboundWaitQty = 0, reboundTriggerPrice = 0, reboundDeadlineDays = 0; if (execStyle === 'OVERSOLD_REBOUND_SELL') { immediateQty = Math.floor(baseQty * 0.50); reboundWaitQty = baseQty - immediateQty; // TICK_NORMALIZER_V1 간소화: 10원 단위 반올림 reboundTriggerPrice = Math.round((prevClose + 0.5 * atr20) / 10) * 10; reboundDeadlineDays = 3; } else if (execStyle === 'EMERGENCY_FULL_EXIT') { immediateQty = baseQty; reboundWaitQty = 0; reboundTriggerPrice = 0; reboundDeadlineDays = 0; } else { immediateQty = Math.floor(baseQty * 0.50); reboundWaitQty = baseQty - immediateQty; reboundTriggerPrice = prevClose > 0 ? prevClose : close; reboundDeadlineDays = 5; } // ── rebound_scenario ───────────────────────────────────────────────────── var limitPrice = prevClose > 0 ? prevClose : close; var immediateKrw = immediateQty * limitPrice; var reboundUpsideKrw = reboundWaitQty * (reboundTriggerPrice > 0 ? reboundTriggerPrice : limitPrice); var downsideRiskKrw = reboundWaitQty * (stopPrice > 0 ? stopPrice : close * 0.92); var rrNum = reboundUpsideKrw - immediateKrw; var rrDen = Math.max(1, immediateKrw - downsideRiskKrw); var riskRewardRatio = round2_(rrNum / rrDen); // ── value_preservation_score ───────────────────────────────────────────── var vpScore = 100; if (immediateQty >= baseQty && rsi14 < 30) vpScore -= 30; // full_sell_oversold if (distWS >= 3.0) vpScore -= 15; // distribution_high if (reboundWaitQty > 0) vpScore += 15; // rebound_wait_exists if (reboundTriggerPrice > 0 && limitPrice > 0 && reboundTriggerPrice <= limitPrice * 1.03) vpScore += 10; // tight_trigger vpScore = Math.max(0, Math.min(100, Math.round(vpScore))); rows.push({ ticker: h.ticker, name: h.name || df.name || '', execution_style: execStyle, base_qty: baseQty, immediate_qty: immediateQty, rebound_wait_qty: reboundWaitQty, rebound_trigger_price: reboundTriggerPrice, rebound_deadline_days: reboundDeadlineDays, risk_reward_ratio: riskRewardRatio, value_preservation_score: vpScore, immediate_sell_krw: Math.round(immediateKrw), rebound_upside_krw: Math.round(reboundUpsideKrw), emergency_full_sell_flag: emergencyFullSell, sell_value_damage_warning: vpScore < 50, dist_weighted_sum: Math.round(distWS * 10) / 10, formula_id: 'CASH_PRESERVATION_SELL_ENGINE_V2' }); }); return rows; } /** * PA5 — CONSISTENCY_VALIDATOR_V2 * 12개 논리 검증 항목으로 hApex 일관성 점검. score < 90 → cv_verdict=BLOCK. * Sprint C 마지막에 실행 — 이전 PA1~PA4 결과까지 모두 포함한 hApex 검증. * @param {Object} hApex * @param {Object} asResult * @param {Object} cashFloorInfo * @param {string} capturedAtIso * @param {Date} now */ function calcConsistencyValidatorV2_(hApex, asResult, cashFloorInfo, capturedAtIso, now) { return calcConsistencyValidatorV2Impl_(hApex, asResult, cashFloorInfo, capturedAtIso, now); } /** * [PROPOSAL47_A1] WATCH_BREAKOUT_REALTIME_GATE_V1 * REVIEW / EXIT 라이프사이클 단계의 보유 종목 중 velocity_1d >= 2.0% 급등 탐지. * 감시 중 급등 누락(49건 근본 원인) 해결 — 당일 급등 감지 시 후보 승격 검토 신호 생성. * anti_late_entry_grade가 F(BLOCK)인 경우 승격 제외 (추격 매수 방지). * * @param {Array} holdings asResult.holdings * @param {Object} dfMap 종목별 데이터 피드 * @param {Array} slgRows satellite_lifecycle_gate_json (lifecycle_stage 포함) * @param {Array} aleRows anti_late_entry_json (entry_grade 포함, F면 제외) * @returns {Array} watch_breakout_candidates_json */ function calcWatchBreakoutRealtimeGateV1_(holdings, dfMap, slgRows, aleRows) { var VELOCITY_THRESHOLD = 2.0; var REVIEW_STAGES = ['REVIEW', 'EXIT']; var slgMap = {}; (slgRows || []).forEach(function(r) { slgMap[String(r.ticker || '')] = String(r.lifecycle_stage || ''); }); var aleMap = {}; (aleRows || []).forEach(function(r) { aleMap[String(r.ticker || '')] = r; }); var results = []; (holdings || []).forEach(function(h) { var ticker = String(h.ticker || ''); var stage = slgMap[ticker] || ''; if (REVIEW_STAGES.indexOf(stage) < 0) return; var df = dfMap[ticker] || {}; var close = Number(df.close || h.close || 0); var prevClose = Number(df.prevClose || 0); if (close <= 0 || prevClose <= 0) return; var velocity1d = Math.round((close - prevClose) / prevClose * 10000) / 100; if (velocity1d < VELOCITY_THRESHOLD) return; var aleEntry = aleMap[ticker] || {}; var aleGrade = aleEntry.entry_grade || 'B'; if (aleGrade === 'F') return; // 추격매수 방지: anti_late_entry_grade F 제외 results.push({ ticker: ticker, name: h.name || df.name || '', lifecycle_stage: stage, velocity_1d: velocity1d, promotion_signal: 'WATCH_BREAKOUT', anti_late_entry_grade: aleGrade, formula_id: 'WATCH_BREAKOUT_REALTIME_GATE_V1' }); }); return results; } /** * [PROPOSAL48_A3] ANTI_WHIPSAW_REENTRY_GATE_V1 * 매도 압박(tier=1/2) 종목이 당일 +3% 이상 급반등 시 REENTRY_CANDIDATE 마킹. * 9건 "매도 신호 후 반등" 패턴 처리. 매도 실행 전 재검토 신호 제공. * * @param {Array} sellCandidates hApex.sell_candidates_json (tier, ticker, action 포함) * @param {Object} dfMap 종목별 데이터 피드 * @param {Array} holdings asResult.holdings * @returns {Array} anti_whipsaw_reentry_json */ function calcAntiWhipsawReentryGateV1_(sellCandidates, dfMap, holdings) { var REENTRY_VELOCITY_THRESHOLD = 3.0; // 재진입 급반등 기준: +3% var WHIPSAW_TIERS = [1, 2]; // 즉시·단계 매도 압박 대상 var results = []; (sellCandidates || []).forEach(function(cand) { var tier = typeof cand.tier === 'number' ? cand.tier : parseInt(cand.tier) || 99; if (WHIPSAW_TIERS.indexOf(tier) < 0) return; var ticker = cand.ticker; var df = dfMap[ticker] || {}; var h = (holdings || []).find(function(x) { return x.ticker === ticker; }) || {}; var close = h.close || df.close || 0; var prevClose = df.prevClose || 0; if (close <= 0 || prevClose <= 0) return; var velocity1d = Math.round((close - prevClose) / prevClose * 10000) / 100; if (velocity1d < REENTRY_VELOCITY_THRESHOLD) return; var profitPct = h.avgCost > 0 ? Math.round((close - h.avgCost) / h.avgCost * 1000) / 10 : null; var rsi14 = typeof df.rsi14 === 'number' ? df.rsi14 : null; // 재진입 등급: A(rsi<50 + rs_leader), B(rsi<60), C(기본) var reentryGrade = 'C'; if (rsi14 !== null && rsi14 < 50 && df.rs_verdict === 'LEADER') reentryGrade = 'A'; else if (rsi14 !== null && rsi14 < 60) reentryGrade = 'B'; results.push({ ticker: ticker, name: h.name || df.name || '', sell_tier: tier, sell_action: cand.action || '', velocity_1d: velocity1d, close: close, prev_close: prevClose, rsi14: rsi14, rs_verdict: df.rs_verdict || '', profit_pct: profitPct, reentry_grade: reentryGrade, reentry_signal: 'REENTRY_CANDIDATE', whipsaw_warning: '매도 압박 중 반등 — 실행 전 재검토 권고', formula_id: 'ANTI_WHIPSAW_REENTRY_GATE_V1' }); }); return results; } /** * [PROPOSAL48_C7] getAlphaHistorySummary_ * alpha_history 시트의 T20/T60 alpha gate 결과를 집계. * 위성 종목의 장기 알파 생성 능력 추적 — T+5 피드백 루프 대용 지표. * DATA_INSUFFICIENT 상태에서도 구조를 갖춰 LLM 참조 가능하게 유지. */ function getAlphaHistorySummary_() { try { var ss = getSpreadsheet_(); var sh = ss.getSheetByName('alpha_history'); if (!sh) return { status: 'NO_SHEET', formula_id: 'ALPHA_HISTORY_SUMMARY_V1' }; var rows = sh.getDataRange().getValues(); if (!rows || rows.length < 2) return { status: 'EMPTY', formula_id: 'ALPHA_HISTORY_SUMMARY_V1' }; var header = rows[0].map(function(h) { return String(h).trim(); }); var idx = {}; ['Ticker','T20_Alpha_Gate','T60_Alpha_Gate','T20_Vs_Core_Pctp','T60_Vs_Core_Pctp','SAQG_Grade_At_Entry'].forEach(function(k) { idx[k] = header.indexOf(k); }); var t20 = { total: 0, pass: 0, fail: 0, missing: 0 }; var t60 = { total: 0, pass: 0, fail: 0, missing: 0 }; var gradeCount = {}; for (var r = 1; r < rows.length; r++) { var row = rows[r]; var g20 = idx['T20_Alpha_Gate'] >= 0 ? String(row[idx['T20_Alpha_Gate']] || '') : ''; var g60 = idx['T60_Alpha_Gate'] >= 0 ? String(row[idx['T60_Alpha_Gate']] || '') : ''; var grade = idx['SAQG_Grade_At_Entry'] >= 0 ? String(row[idx['SAQG_Grade_At_Entry']] || '') : ''; if (g20 && g20 !== 'PENDING') { t20.total++; if (g20 === 'PASS') t20.pass++; else if (g20 === 'FAIL') t20.fail++; else t20.missing++; } if (g60 && g60 !== 'PENDING') { t60.total++; if (g60 === 'PASS') t60.pass++; else if (g60 === 'FAIL') t60.fail++; else t60.missing++; } if (grade) gradeCount[grade] = (gradeCount[grade] || 0) + 1; } var t20Rate = t20.total > 0 ? Math.round(t20.pass / t20.total * 1000) / 10 : null; var t60Rate = t60.total > 0 ? Math.round(t60.pass / t60.total * 1000) / 10 : null; return { status: (t20.total > 0 || t60.total > 0) ? 'OK' : 'DATA_INSUFFICIENT', t20_total: t20.total, t20_pass_rate: t20Rate, t20_pass: t20.pass, t20_fail: t20.fail, t60_total: t60.total, t60_pass_rate: t60Rate, t60_pass: t60.pass, t60_fail: t60.fail, grade_count: gradeCount, total_rows: rows.length - 1, formula_id: 'ALPHA_HISTORY_SUMMARY_V1' }; } catch(e) { return { status: 'ERROR', error: e.message, formula_id: 'ALPHA_HISTORY_SUMMARY_V1' }; } } // ═══════════════════════════════════════════════════════════════════════ // [PROPOSAL50] P0-1: EXPORT_GATE_V1 — PENDING_EXPORT 원인 자동 진단 // Direction G5: PENDING_EXPORT 원인 진단 의무 // ═══════════════════════════════════════════════════════════════════════ /** * calcExportGate_ * 5개 체크리스트 자동 평가 → EXPORT_READY / PENDING_EXPORT * PASS 전 HTS 입력 금지 조건을 결정론적으로 산출. */ function calcExportGate_(hApex, asResult, cashFloorInfo) { var checks = []; // CHECK_1: account_snapshot 캡처 완료 여부 var captureRequired = !(asResult && asResult.holdings && asResult.holdings.length > 0 && asResult.settlementCashD2Krw > 0); checks.push({ check_id: 'CHECK_1_SNAPSHOT_CAPTURED', status: captureRequired ? 'FAIL' : 'PASS', message: captureRequired ? 'account_snapshot 미캡처 — HTS 화면 캡처 후 재실행 필요' : 'account_snapshot OK' }); // CHECK_2: 데이터 완성도 (buy_permission_json 기준 전 종목 존재) var bpJson = (hApex && hApex.buy_permission_json) || []; var holdingCount = (asResult && asResult.holdings) ? asResult.holdings.length : 0; var dataOk = holdingCount > 0 && bpJson.length >= holdingCount; checks.push({ check_id: 'CHECK_2_DATA_COMPLETENESS', status: dataOk ? 'PASS' : 'FAIL', message: dataOk ? 'data_feed 완성도 OK (' + bpJson.length + '/' + holdingCount + ')' : 'data_feed 누락 — npm run convert-data-json 후 재실행' }); // CHECK_3: 하네스 무결성 체크섬 (consistency_score 기준) var cvScore = (hApex && typeof hApex.consistency_score === 'number') ? hApex.consistency_score : null; var cvOk = cvScore !== null && cvScore >= 70; checks.push({ check_id: 'CHECK_3_HARNESS_INTEGRITY', status: cvOk ? 'PASS' : 'FAIL', message: cvOk ? 'consistency_score=' + cvScore + ' 무결성 OK' : 'consistency_score=' + (cvScore !== null ? cvScore : 'null') + ' — 70 미만 또는 미산출' }); // CHECK_4: SELL_PRICE_SANITY — INVALID 주문 없음 var blueprint = (hApex && hApex.order_blueprint_json) || []; var invalidPrices = blueprint.filter(function(b) { return String(b.validation_status || '').indexOf('INVALID') >= 0; }); checks.push({ check_id: 'CHECK_4_NO_INVALID_PRICES', status: invalidPrices.length === 0 ? 'PASS' : 'FAIL', message: invalidPrices.length === 0 ? 'SELL_PRICE_SANITY 이상 없음' : 'INVALID 가격 ' + invalidPrices.length + '건: ' + invalidPrices.map(function(b) { return b.ticker; }).join(',') }); // CHECK_5: cashFloor 블록 상태 확인 (HARD_BLOCK 시 현금 부족 경보) var cashStatus = (cashFloorInfo && cashFloorInfo.status) || 'UNKNOWN'; var cashOk = cashStatus !== 'UNKNOWN'; checks.push({ check_id: 'CHECK_5_CASH_LEDGER', status: cashOk ? 'PASS' : 'WARN', message: cashOk ? 'cash_floor_status=' + cashStatus + ' (기록됨)' : 'cash_floor_status=UNKNOWN — settlement_cash_d2_krw 확인 필요' }); // [PROPOSAL51] P1-A: CHECK_6 — SCRS_RENDER 검증 (immediate_sell_qty 유효값 필수) var scrsV2 = (hApex && hApex.scrs_v2_json) || {}; // [PROPOSAL51-FIX] GAS는 immediate_qty 반환 (calcSmartCashRecoverySell_ 확인) var scrsRows = scrsV2.selected_combo || scrsV2.candidates || scrsV2.rows || []; var scrsRenderOk = scrsRows.length === 0 || scrsRows.every(function(r) { var qty = r.immediate_qty !== undefined ? r.immediate_qty : r.immediate_sell_qty; return qty !== null && qty !== undefined && qty !== '-' && qty !== ''; }); checks.push({ check_id: 'CHECK_6_SCRS_RENDER', status: scrsRenderOk ? 'PASS' : 'WARN', message: scrsRenderOk ? 'SCRS-V2 immediate_sell_qty 렌더링 OK' : 'SCRS-V2 immediate_sell_qty 누락 — render_operational_report 키 불일치 확인 필요' }); // [PROPOSAL51] P1-A: CHECK_7 — PORTFOLIO_HEALTH_SCORE 타입 (Boolean 금지) var healthScore = hApex && hApex.portfolio_health_score; var healthTypeOk = (typeof healthScore === 'number' && !isNaN(healthScore)); checks.push({ check_id: 'CHECK_7_HEALTH_SCORE_TYPE', status: healthTypeOk ? 'PASS' : 'WARN', message: healthTypeOk ? 'portfolio_health_score=' + healthScore + ' (숫자 OK)' : 'portfolio_health_score=' + JSON.stringify(healthScore) + ' — 숫자여야 함 (Boolean/null 금지)' }); // [PROPOSAL51] P1-A: CHECK_8 — CLUSTER_SYNC 교정 없음 확인 var clusterSync = (hApex && hApex.cluster_sync_result_json) || {}; var clusterSyncOk = clusterSync.status === 'SYNCED' || !clusterSync.status; checks.push({ check_id: 'CHECK_8_CLUSTER_SYNC', status: clusterSyncOk ? 'PASS' : 'WARN', message: clusterSyncOk ? 'SEMICONDUCTOR_CLUSTER_SYNC: 정합성 OK' : 'CLUSTER_SYNC 교정 발생 (cluster_pct=' + (clusterSync.cluster_pct || '?') + '%, threshold=' + (clusterSync.threshold_pct || '?') + '%)' }); var failChecks = checks.filter(function(c) { return c.status === 'FAIL'; }); var warnChecks = checks.filter(function(c) { return c.status === 'WARN'; }); var exportStatus; if (failChecks.length > 0) exportStatus = 'PENDING_EXPORT'; else if (warnChecks.length > 0) exportStatus = 'REVIEW_ONLY'; else exportStatus = 'EXPORT_READY'; var htsAllowed = exportStatus === 'EXPORT_READY'; var nonPassChecks = checks.filter(function(c) { return c.status !== 'PASS'; }); var resolutionGuide = nonPassChecks.map(function(c) { return '[' + c.check_id + '] ' + c.message; }); return { json_validation_status: exportStatus, export_gate_status: exportStatus, all_checks_passed: failChecks.length === 0 && warnChecks.length === 0, checks: checks, failed_checks: failChecks.map(function(c) { return c.check_id; }), warn_checks: warnChecks.map(function(c) { return c.check_id; }), resolution_guide: resolutionGuide, hts_entry_allowed: htsAllowed, formula_id: 'EXPORT_GATE_V2' }; } // ═══════════════════════════════════════════════════════════════════════ // [PROPOSAL50] P0-2: ROUTING_TRACE_V1 — 라우팅 Trace 필수 출력 (Direction G4) // ═══════════════════════════════════════════════════════════════════════ /** * buildRoutingTrace_ * 모든 보고서 선행 출력 의무 — request_route, bundle, prompt, 검증 상태 etc. * 누락 시 보고서 전체 INCOMPLETE_REPORT. */ function buildRoutingTrace_(intradayLock, cashFloorInfo, hApex, capturedAtIso) { var scope = intradayLock ? 'TRIM_ONLY' : 'FULL_ANALYSIS'; var bundleSelected = (function() { var cv = (hApex && hApex.consistency_score); if (cv === null || cv === undefined) return 'retirement_portfolio_ultra_compact'; if (cv < 70) return 'retirement_portfolio_ultra_compact'; return 'retirement_portfolio_compact'; })(); var exportGate = (hApex && hApex.export_gate_json) || {}; var jsonValStatus = exportGate.json_validation_status || 'PENDING_EXPORT'; var captureRequired = exportGate.checks ? !exportGate.checks.some(function(c) { return c.check_id === 'CHECK_1_SNAPSHOT_CAPTURED' && c.status === 'PASS'; }) : true; var cashLedgerBasis = 'D2_ONLY'; var snapshotExecGate = (cashFloorInfo && cashFloorInfo.status === 'PASS') ? 'FULL_EXECUTION' : 'REVIEW_ONLY'; return { request_route: 'PIPELINE_EOD_BATCH', bundle_selected: bundleSelected, prompt_entrypoint: 'prompts/analysis_prompt.md', json_validation_status: jsonValStatus, capture_required: captureRequired, intraday_scope: scope, snapshot_execution_gate: snapshotExecGate, price_basis: capturedAtIso || 'UNKNOWN', cash_ledger_basis: cashLedgerBasis, routing_trace_complete: true, formula_id: 'ROUTING_TRACE_V1' }; } // ═══════════════════════════════════════════════════════════════════════ // [PROPOSAL50] P0-3: WATCH_LEDGER_V1 — WATCH 감시 원장 (Direction I4) // HTS 입력 금지 컬럼명만 허용 — 주문표와 물리적 분리 // ═══════════════════════════════════════════════════════════════════════ /** * buildWatchLedger_ * order_blueprint_json에서 validation_status != PASS 행을 분리. * 허용 컬럼: ticker/name, reference_stop_price, reference_tp_state, hts_allowed, reason_code * 금지 컬럼: 지정가, 손절가, 익절가, 주문가, 주문수량 등 (INVALID_COLUMN) */ function buildWatchLedger_(orderBlueprint, h4) { var priceMap = {}; ((h4 && h4.prices) || []).forEach(function(p) { priceMap[p.ticker] = p; }); var blueprintRows = Array.isArray(orderBlueprint) ? orderBlueprint : []; var watchRows = blueprintRows.filter(function(b) { return b.validation_status !== 'PASS'; }); return watchRows.map(function(b) { var p = priceMap[b.ticker] || {}; var tpState = (function() { if (!p.tp1_price) return 'INVALID_TP_STALE'; if (p.tp_state === 'TP1_ALREADY_TRIGGERED') return 'TP1_ALREADY_TRIGGERED'; return 'PENDING'; })(); return { ticker: b.ticker, name: b.name || '', reference_stop_price: p.stop_price || null, reference_tp_state: tpState, hts_allowed: false, reason_code: b.validation_status || 'NO_EXECUTION:WATCH', note: '주문 아님. HTS 입력 금지.' }; }); } // ═══════════════════════════════════════════════════════════════════════ // [PROPOSAL50] P1-1: EXPERT_JUDGMENT_CONSENSUS_ENGINE_V1 (EJCE-V1) // 30년 전문가 수준 3관점(애널리스트·트레이더·퀀트) 합의 게이트 // Direction EJ1: consensus_result=NO_BUY 시 BUY 절대 금지 // ═══════════════════════════════════════════════════════════════════════ /** * calcExpertJudgmentConsensus_ * 3관점 독립 채점 → majority_rule → final_allowed_action 고착화 * LLM "분위기 좋으니까" 판단을 결정론적 합의로 대체. */ function calcExpertJudgmentConsensus_(ticker, df, paeRow, h1, hApex, dfMap) { df = df || {}; paeRow = paeRow || {}; // ── ANALYST_VIEW: 펀더멘털·밸류에이션 ───────────────────────────────────── var compositeScore = toNumber_(df['SS001_Score'] || df['composite_score']) || 0; var pegScore = toNumber_(df['PEG_Score'] || df['peg_score']) || 0; var upsidePct = toNumber_(df['Upside_Pct'] || df['upside_pct']) || 0; var epsMiss = toNumber_(df['EPS_Revision_Status'] === 'MISS' ? 1 : 0); var dartRisk = String(df['DART_Risk'] || '').toUpperCase() === 'Y'; var analystScore = 0; if (compositeScore >= 70) analystScore += 30; else if (compositeScore >= 50) analystScore += 15; if (pegScore >= 8 || upsidePct > 15) analystScore += 20; if (upsidePct > 15) analystScore += 5; if (epsMiss >= 2) analystScore -= 30; if (dartRisk) analystScore -= 20; var analystVerdict = analystScore >= 30 ? 'BULLISH' : analystScore >= -10 ? 'NEUTRAL' : 'BEARISH'; // ── TRADER_VIEW: 타이밍·수급·추세 ───────────────────────────────────────── var flowCredit = toNumber_(df['Flow_Credit'] || df['flow_credit']) || 0; var rsVerdict = String(df['RS_Verdict'] || df['rs_verdict'] || '').toUpperCase(); var velocity1d = toNumber_(df['Ret5D'] != null ? df['Close'] / (df['Close'] / (1 + toNumber_(df['Ret5D']) / 100)) - 1 : 0) * 100; // 더 단순하게: Ret5D/5 근사 var ret5d = toNumber_(df['Ret5D'] || df['ret5d']) || 0; var vel1d_approx = ret5d / 5; var paeAnti = toNumber_(paeRow.antithesis_score) || 0; var distCount = toNumber_(df['Dist_Signals'] || df['distribution_signals_count']) || 0; var ma20 = toNumber_(df['MA20']) || 0; var close = toNumber_(df['Close'] || df['close']) || 0; var atr20 = toNumber_(df['ATR20']) || 0; var inPullback = (ma20 > 0 && close > 0) ? close <= ma20 * 1.03 : false; var traderScore = 0; if (flowCredit >= 0.55 && rsVerdict === 'LEADER') traderScore += 25; if (inPullback) traderScore += 20; if (vel1d_approx < 1.5 && ret5d > 0) traderScore += 20; if (vel1d_approx >= 3.0) traderScore -= 30; // 뒷박 강한 패널티 if (paeAnti >= 50) traderScore -= 25; // 설거지 경보 if (distCount >= 2) traderScore -= 25; var traderVerdict = traderScore >= 20 ? 'ENTRY_OK' : traderScore >= -10 ? 'WAIT' : 'BLOCK_ENTRY'; // ── QUANT_VIEW: 통계·팩터·리스크예산 ───────────────────────────────────── var pacVal = toNumber_((hApex && hApex.portfolio_alpha_confidence)) || 0; var heatGate = String((hApex && hApex.heat_gate_status) || '').toUpperCase(); var ddGuard = String((hApex && hApex.drawdown_guard_state) || '').toUpperCase(); var expectedEdge = toNumber_(df['Expected_Edge'] || df['expected_edge']) || 0; var atrAvail = atr20 > 0; var quantScore = 0; if (expectedEdge > 0 && atrAvail) quantScore += 25; if (atrAvail) quantScore += 10; if (pacVal > 20) quantScore += 20; if (pacVal < -20) quantScore -= 30; // 전체 알파 신뢰도 BLOCK if (heatGate === 'BLOCK_NEW_BUY') quantScore -= 20; if (ddGuard === 'NO_BUY') quantScore -= 15; var quantVerdict = quantScore >= 20 ? 'APPROVED' : quantScore >= -10 ? 'REDUCED' : 'REJECTED'; // ── CONSENSUS_MATRIX: 2/3 이상 BLOCK → NO_BUY ─────────────────────────── var blockCount = 0; if (analystVerdict === 'BEARISH') blockCount++; if (traderVerdict === 'BLOCK_ENTRY') blockCount++; if (quantVerdict === 'REJECTED') blockCount++; var consensusResult, finalAllowedAction; if (blockCount >= 2) { consensusResult = 'NO_BUY'; finalAllowedAction = 'HOLD'; } else if (analystVerdict === 'BULLISH' && traderVerdict === 'ENTRY_OK' && quantVerdict === 'APPROVED') { consensusResult = 'STRONG_BUY'; finalAllowedAction = 'BUY'; } else if (analystVerdict === 'BULLISH' && traderVerdict === 'ENTRY_OK') { consensusResult = 'BUY_HALF'; finalAllowedAction = 'BUY_HALF'; } else if (analystVerdict === 'BULLISH' && traderVerdict === 'WAIT') { consensusResult = 'BUY_PULLBACK'; finalAllowedAction = 'WAIT_PULLBACK'; } else if (analystVerdict === 'NEUTRAL' && traderVerdict === 'ENTRY_OK') { consensusResult = 'BUY_PILOT'; finalAllowedAction = 'PILOT'; } else { consensusResult = 'HOLD_WATCH'; finalAllowedAction = 'WATCH'; } var blockReasons = []; if (analystVerdict === 'BEARISH') blockReasons.push('ANALYST_BEARISH'); if (traderVerdict === 'BLOCK_ENTRY') blockReasons.push('TRADER_BLOCK_ENTRY_vel=' + vel1d_approx.toFixed(1) + '%'); if (quantVerdict === 'REJECTED') blockReasons.push('QUANT_REJECTED_pac=' + pacVal.toFixed(1)); return { ticker: ticker, analyst_score: analystScore, analyst_verdict: analystVerdict, trader_score: traderScore, trader_verdict: traderVerdict, quant_score: quantScore, quant_verdict: quantVerdict, block_count: blockCount, consensus_result: consensusResult, final_allowed_action: finalAllowedAction, block_reasons: blockReasons, override_required: blockCount >= 2, formula_id: 'EXPERT_JUDGMENT_CONSENSUS_ENGINE_V1' }; } // ═══════════════════════════════════════════════════════════════════════ // [PROPOSAL50] P1-2: SMART_CASH_RECOVERY_SELL_ENGINE_V2 (SCRS-V2) // 세련된 현금확보 매도 — 주식가치 보호 + 반등 포착 통합 엔진 // Direction C3: SCRS-V2 selected_combo만 HTS 주문표 기재 허용 // ═══════════════════════════════════════════════════════════════════════ /** * calcSmartCashRecoverySell_ * 현금 부족액을 최소 주식가치 훼손으로 회수. * 반등 기대 수익(expected_rebound_gain_krw) 사전 산출. * "현금 급함" 이유로 Stage_2 우회 원천 차단. */ function calcSmartCashRecoverySell_(holdings, dfMap, cashShortfallInfo, h2, hApex) { var shortfall = toNumber_((cashShortfallInfo && cashShortfallInfo.cash_shortfall_min_krw)) || 0; var totalAsset = toNumber_((hApex && hApex.total_asset_krw) || (cashShortfallInfo && cashShortfallInfo.total_asset_krw)) || 1; var emergencyScore = shortfall / totalAsset * 100; var level = emergencyScore >= 15 ? 'EMERGENCY' : emergencyScore >= 8 ? 'URGENT' : emergencyScore >= 3 ? 'NORMAL' : 'TRIM_ONLY'; var holdMap = {}; (holdings || []).forEach(function(h) { holdMap[h.ticker] = h; }); var sellQtyMap = {}; ((hApex && hApex.sell_quantities_json) || []).forEach(function(sq) { sellQtyMap[sq.ticker] = sq; }); var candidates = ((h2 && h2.candidates) || []).slice(); // [Phase 3] SMART_CASH_RECOVERY_V6: value_damage_score(가치 훼손 점수) 기준 오름차순 정렬 candidates.forEach(function(c) { var h = holdMap[c.ticker] || {}; var df = dfMap[c.ticker] || {}; var close = toNumber_(h.close || df['Close'] || df.close) || 0; var atr20 = toNumber_(df['ATR20'] || df.atr20) || (close * 0.02); // 가치 훼손 점수: 슬리피지 및 낙폭 리스크를 수치화 (낮을수록 매도 유리) c.value_damage_score = close > 0 ? ((atr20 * 0.3) / close) * 100 : 100; }); candidates.sort(function(a, b) { return (a.value_damage_score || 0) - (b.value_damage_score || 0); }); var cumulative = 0; var combo = []; for (var i = 0; i < candidates.length; i++) { if (shortfall > 0 && cumulative >= shortfall) break; var c = candidates[i]; var h = holdMap[c.ticker] || {}; var df = dfMap[c.ticker] || {}; var close = toNumber_(h.close || df['Close'] || df.close) || 0; var atr20 = toNumber_(df['ATR20'] || df.atr20) || (close * 0.02); var holding = toNumber_(h.holdingQty || h.holding_qty) || 0; var sqRow = sellQtyMap[c.ticker] || {}; var baseQty = toNumber_(sqRow.sell_qty) || Math.floor(holding * 0.33); if (close <= 0 || baseQty <= 0) continue; var currentValue = holding * close; var immediateQty = Math.floor(baseQty * 0.50); var reboundWaitQty = baseQty - immediateQty; var slippage = atr20 * 0.3; var immediateKrw = immediateQty * Math.max(0, close - slippage); var damagePct = currentValue > 0 ? immediateKrw / currentValue * 100 : 100; if (damagePct > 30 && level !== 'EMERGENCY') continue; var reboundTrigger = tickNormalize_(close + atr20 * 0.5, close); var expectedReboundKrw = reboundWaitQty * Math.max(0, reboundTrigger - close); // [Phase 3] 유동성 기준 exec_mode 강제 지정 var avgTradeValue = toNumber_(df['AvgTradeValue_20D_M'] || df.avgTradeVal20d) || 10000000000; var execMode = 'LIMIT_NEAR_BID'; if (avgTradeValue < 5000000000) { execMode = 'TWAP_5_SPLIT'; } else if (avgTradeValue > 50000000000) { execMode = 'MARKET'; } cumulative += immediateKrw; combo.push({ rank: c.rank, ticker: c.ticker, name: c.name || (h.name || ''), exec_mode: execMode, value_damage_score: Math.round(c.value_damage_score * 10) / 10, immediate_qty: immediateQty, rebound_wait_qty: reboundWaitQty, immediate_krw: Math.round(immediateKrw), rebound_trigger_price: reboundTrigger, expected_rebound_krw: Math.round(expectedReboundKrw), value_damage_pct: Math.round(damagePct * 10) / 10, rebound_deadline_date: addBusinessDays_(new Date(), 3) }); } var totalReboundGain = combo.reduce(function(s, c) { return s + c.expected_rebound_krw; }, 0); var avgDamage = combo.length > 0 ? combo.reduce(function(s, c) { return s + c.value_damage_pct; }, 0) / combo.length : 0; var emergencyFullSell = combo.length > 0 && combo[0].immediate_krw * 2 < shortfall && level === 'EMERGENCY'; return { emergency_level: level, shortfall_krw: Math.round(shortfall), selected_combo: combo, total_immediate_sell_krw: Math.round(cumulative), expected_rebound_gain_krw: Math.round(totalReboundGain), value_damage_pct_avg: Math.round(avgDamage * 10) / 10, emergency_full_sell: emergencyFullSell, shortfall_covered: shortfall <= 0 || cumulative >= shortfall, formula_id: 'SMART_CASH_RECOVERY_SELL_ENGINE_V6' }; } // ═══════════════════════════════════════════════════════════════════════ // [PROPOSAL51] P1-C: CASH_RECOVERY_DISPLAY_LOCK_V1 (CRDL-V1) // 현금회복 금액 3분리 표시 잠금 — 207억 과대표시 차단 // min_required / optimal_combo / reference_total (주문 아님) // ═══════════════════════════════════════════════════════════════════════ /** * calcCashRecoveryDisplayLock_ * 현금회복 금액을 3분리(최소필요/최적조합/전체후보) 표시 잠금. * reference_total_krw는 "주문 아님" 레이블 필수. */ function calcCashRecoveryDisplayLock_(scrsJson, trimPlanJson, cashInfo) { function normalizeRows_(v) { if (Array.isArray(v)) return v; if (!v) return []; if (typeof v === 'string') { try { return normalizeRows_(JSON.parse(v)); } catch (e) { return []; } } if (typeof v === 'object') { var vals = []; for (var k in v) if (Object.prototype.hasOwnProperty.call(v, k)) vals.push(v[k]); return vals; } return []; } var scrs = scrsJson || {}; if (typeof scrs === 'string') { try { scrs = JSON.parse(scrs); } catch (e0) { scrs = {}; } } var trim = normalizeRows_(trimPlanJson); var cash = cashInfo || {}; var minRequired = toNumber_(cash.cash_shortfall_min_krw) || 0; var combo = normalizeRows_(scrs.selected_combo); var optimalCombo = combo.reduce(function(s, r) { return s + (toNumber_(r.immediate_krw) || 0); }, 0); var refTotal = trim.reduce(function(s, r) { return s + (toNumber_(r.sell_amount_krw || r.trim_amount_krw || r.trimming_krw) || 0); }, 0); var coverageStatus; if (minRequired <= 0) coverageStatus = 'NO_SHORTFALL'; else if (optimalCombo < minRequired) coverageStatus = 'UNCOVERED'; else if (optimalCombo > minRequired * 2) coverageStatus = 'OVER_SELL'; else coverageStatus = 'COVERED'; return { formula_id: 'CASH_RECOVERY_DISPLAY_LOCK_V1', min_required_krw: Math.round(minRequired), optimal_combo_krw: Math.round(optimalCombo), reference_total_krw: Math.round(refTotal), coverage_status: coverageStatus, display_mode: 'SHOW_MIN_OPTIMAL', reference_label: '참고용 전체 후보 누적 — 주문 아님', over_sell_warning: coverageStatus === 'OVER_SELL' ? 'OVER_SELL_WARNING: 최적조합(' + Math.round(optimalCombo/10000) + '만원)이 최소필요(' + Math.round(minRequired/10000) + '만원)의 2배 초과' : null, shortfall_uncovered: coverageStatus === 'UNCOVERED' ? 'CASH_SHORTFALL_UNCOVERED: SCRS-V2 재실행 필요' : null }; } // ═══════════════════════════════════════════════════════════════════════ // [PROPOSAL51] P1-B: DATA_QUALITY_GATE_V2 (DQG-V2) // 데이터 완성도 필드충족률 기반 게이트 — 행수 카운트 폐기 // COMPLETE(≥90%) / PARTIAL(≥60%) / INSUFFICIENT(<60%) // ═══════════════════════════════════════════════════════════════════════ /** * calcDataQualityGateV2_ * 핵심 필드 충족률로 데이터 완성도 등급 산출. * T+20=0건, trade_quality=0건 시 특수 경고 발동. */ function calcDataQualityGateV2_(hApex) { var h = hApex || {}; var pa1 = ((h.alpha_lead_json || [])[0]) || {}; var tradeQualRecords = ((h.trade_quality_report_json || {}).records || []); var tqFirst = tradeQualRecords[0] || {}; var alphaHist = (h.alpha_history_summary_json) || {}; var scrsV2 = (h.scrs_v2_json) || {}; var combo = scrsV2.selected_combo || []; var cluster = (h.semiconductor_cluster_json) || {}; var alphaEval = (h.alpha_evaluation_window_json || []); var firstAlpha = alphaEval[0] || {}; var pp0 = ((h.profit_preservation_json) || [])[0] || {}; var isValid = function(v) { return v !== null && v !== undefined && v !== '-' && v !== 'PENDING' && v !== ''; }; // [R2-1c] 필드경로 버그 수정: 실재 데이터를 0으로 깔던 false-negative 제거. // prediction: alpha_lead_json[0] → pa1_report_json(PA1 진짜 필드). // cash: cash_shortfall_json.cash_shortfall_min_krw(None) → 직접키 h.cash_shortfall_min_krw. // cluster: h.semiconductor_cluster_json → h.semiconductor_cluster_gate_json 또는 직접 필드. // stop_loss: final_stop_price/stop_price(없는 키) → protected_stop_price/auto_trailing_stop. // trade_quality/alpha_eval/pattern: 표본 필요 → PENDING 값으로 명시(분모 제외). var pa1Report = h.pa1_report_json || {}; if (typeof pa1Report === 'string') { try { pa1Report = JSON.parse(pa1Report); } catch(e) { pa1Report = {}; } } var pa1Rows = Array.isArray(pa1Report) ? pa1Report : (pa1Report.rows || []); var pa1Row0 = pa1Rows[0] || {}; var clusterDirect = h.semiconductor_cluster_json || {}; if (typeof clusterDirect === 'string') { try { clusterDirect = JSON.parse(clusterDirect); } catch(e) { clusterDirect = {}; } } var CATEGORIES = { prediction: [pa1Row0.direction_confidence, pa1Row0.synthesis_verdict, pa1Row0.thesis_score, pa1Row0.antithesis_score], trade_quality: [tqFirst.grade || 'PENDING', tqFirst.feedback_tag || 'PENDING', tqFirst.t5_return_pct, tqFirst.t20_vs_core_pct], pattern: [(h.pattern_blacklist_auto_json || {}).status || 'PENDING', (h.pattern_blacklist_auto_json || {}).accumulated_poor_count], ["stop_loss"]: [pp0.auto_trailing_stop, pp0.protected_stop_price, pp0.profit_preservation_state], cash: [h.settlement_cash_d2_krw, h.cash_floor_status, h.cash_shortfall_min_krw], sell_engine: [scrsV2.emergency_level, (combo[0] || {}).immediate_qty, (combo[0] || {}).rebound_wait_qty], cluster: [clusterDirect.cluster_state, clusterDirect.combined_pct], alpha_eval: [firstAlpha.alpha_gate_verdict || 'PENDING', alphaHist.prediction_accuracy_rate] }; var categoryScores = {}; Object.keys(CATEGORIES).forEach(function(cat) { var fields = CATEGORIES[cat]; var filled = fields.filter(isValid).length; categoryScores[cat] = Math.round(filled / fields.length * 100); }); var catVals = Object.keys(categoryScores).map(function(k) { return categoryScores[k]; }); var overallPct = catVals.length > 0 ? Math.round(catVals.reduce(function(s, v) { return s + v; }, 0) / catVals.length) : 0; var grade = overallPct >= 90 ? 'COMPLETE' : overallPct >= 60 ? 'PARTIAL' : 'INSUFFICIENT'; var warnings = []; var t20Count = toNumber_((alphaHist).t20_evaluation_count) || 0; var tqCount = tradeQualRecords.length; var accRate = alphaHist.prediction_accuracy_rate; var t5Count = toNumber_(alphaHist.t5_match_count) || 0; if (t20Count === 0) warnings.push('warn_t20_zero: T+20 평가 0건 — 장기 예측 신뢰도 미검증'); if (tqCount === 0) warnings.push('warn_quality_unverified: 거래 품질 기록 0건'); if (!isValid(accRate)) warnings.push('warn_accuracy_unknown: 예측 정확도 미산출(PENDING)'); if (t5Count < 5) warnings.push('warn_insufficient_samples: T+5 표본 ' + t5Count + '건(최소 5건 미달)'); return { formula_id: 'DATA_QUALITY_GATE_V2', overall_completeness_pct: overallPct, completeness_grade: grade, category_scores: categoryScores, special_warnings: warnings, t20_evaluation_count: t20Count, trade_quality_record_count: tqCount, prediction_accuracy_rate: accRate || null, confidence_ceiling: grade === 'INSUFFICIENT' ? 'BUY_SELL_CONFIDENCE_LIMITED: 핵심 데이터 부족 — 신호 신뢰도 상한 경고' : null }; } /** * addBusinessDays_: 영업일 기준 날짜 계산 (토·일 제외) */ function addBusinessDays_(startDate, days) { var d = new Date(startDate.getTime()); var added = 0; while (added < days) { d.setDate(d.getDate() + 1); var dow = d.getDay(); if (dow !== 0 && dow !== 6) added++; } return Utilities.formatDate(d, 'Asia/Seoul', 'yyyy-MM-dd'); } // ═══════════════════════════════════════════════════════════════════════ // [PROPOSAL50] P2-1: DETERMINISTIC_SERVING_LOCK_ENGINE_V1 (DSLE-V1) // 11단계 stage_token 잠금 + LLM 수치 생성 = 0 강제 // Direction D3: LLM 서빙 수치 생성 절대 금지 // ═══════════════════════════════════════════════════════════════════════ /** * calcDeterministicServingLock_ * 11단계 파이프라인 각 단계의 status·checksum을 토큰으로 기록. * integrity_checksum 불일치 시 INVALID_SERVING_OVERRIDE 자동 표시. */ function calcDeterministicServingLock_(hApex, capturedAtIso, now) { var stages = [ { id: 'Stage_01_freshness', key: 'data_freshness_status' }, { id: 'Stage_02_intraday', key: 'intraday_scope' }, { id: 'Stage_03_portfolio', key: 'cash_floor_status' }, { id: 'Stage_04_macro', key: 'macro_risk_score' }, { id: 'Stage_05_sell_radar', key: 'distribution_sell_detector_json' }, { id: 'Stage_06_buy_gate', key: 'anti_late_entry_json' }, { id: 'Stage_07_sell_priority', key: 'sell_candidates_json' }, { id: 'Stage_08_cash_recovery', key: 'scrs_v2_json' }, { id: 'Stage_09_rs_quality', key: 'rs_verdict' }, { id: 'Stage_10_tick_norm', key: 'tick_normalized_prices_json' }, { id: 'Stage_11_serving', key: 'order_blueprint_json' }, ]; var tokens = []; var blockDetected = false; var blockReason = null; for (var i = 0; i < stages.length; i++) { var s = stages[i]; var value = hApex ? hApex[s.key] : null; var status = (value !== null && value !== undefined) ? 'OK' : 'MISSING'; if (status === 'MISSING' && i < 4) { blockDetected = true; blockReason = blockReason || (s.id + '_MISSING'); } tokens.push({ stage_id: s.id, key: s.key, status: status, checksum: computeStringChecksum_(safeStringifyForChecksum_(value)) }); } var tokenChecksum = computeStringChecksum_(safeStringifyForChecksum_(tokens)); return { route_lock_status: blockDetected ? 'PARTIALLY_LOCKED' : 'FULLY_LOCKED', stage_tokens: tokens, integrity_checksum: tokenChecksum, llm_serving_budget: { max_tokens: 1000, numeric_generation_allowed: 0, constraint: 'LLM_SERVING_CONSTRAINT_V1' }, block_reason: blockReason, captured_at: capturedAtIso || null, generated_at: now ? Utilities.formatDate(now, 'Asia/Seoul', 'yyyy-MM-dd HH:mm') : null, formula_id: 'DETERMINISTIC_SERVING_LOCK_ENGINE_V1' }; }