// Consolidated runtime core: macro flow + macro calc + consistency // ---- from gas_apex_macro_flow.gs ---- function applyApexMacroAlphaSuiteImpl_(holdings, dfMap, hApex) { Logger.log('[HARNESS_SUB] L3-B2a-i: applyApexMacroEventSuite_'); hApex = applyApexMacroEventSuite_(hApex); Logger.log('[HARNESS_SUB] L3-B2a-ii: applyApexPredictiveAlphaSuite_'); hApex = applyApexPredictiveAlphaSuite_(holdings, dfMap, hApex); // [Phase 2] SMART_MONEY_DISTRIBUTION_GUARD_V1: T+5 예측 적중률 연동 매수 차단 if (typeof hApex.prediction_accuracy_rate === 'number' && hApex.prediction_accuracy_rate < 50) { Logger.log('[HARNESS_SUB] Phase 2: prediction_accuracy_rate < 50% (' + hApex.prediction_accuracy_rate + '%). 신규 매수 전면 차단.'); hApex.global_buy_allowed = false; (hApex.buy_permission_json || []).forEach(function(bp) { if (bp.buy_permission_state !== 'BLOCKED') { bp.buy_permission_state = 'BLOCKED'; bp.block_reason = (bp.block_reason ? bp.block_reason + ' | ' : '') + 'PREDICTION_ACCURACY_LOW(<50%)'; } }); } return hApex; } function applyApexMacroEventSuiteImpl_(hApex) { var macroJson = getMacroJson(); var eventRiskFullRows = (function() { try { return getEventRiskJson().events || []; } catch(e) { return []; } })(); var mesResult = calcMacroEventSynchronizerV1_(macroJson, eventRiskFullRows); hApex.macro_event_json = mesResult; hApex.macro_risk_score = mesResult.macro_risk_score; hApex.macro_risk_regime = mesResult.macro_risk_regime; hApex.mega_sell_alert = mesResult.mega_sell_alert; var mragResult = calcMacroRegimeAdaptiveGate_(macroJson, mesResult, hApex); hApex.mrag_v2_json = mragResult; if (mesResult.heat_gate_adj && mesResult.heat_gate_adj !== 0) { var me1Threshold = (hApex.heat_gate_threshold_pct || 12) + mesResult.heat_gate_adj; hApex.effective_heat_gate_threshold = Math.min(me1Threshold, mragResult.effective_heat_gate_threshold); } else { hApex.effective_heat_gate_threshold = mragResult.effective_heat_gate_threshold; } hApex.effective_position_size_scale = mragResult.effective_position_size_scale; if (mragResult.stale_events_count > 0) { hApex.stale_events_alert = mragResult.stale_events; } var fomcDaysRem = mesResult.fomc_days_remaining; var usCpiDaysRem = mesResult.us_cpi_days_remaining; var ipoDaysRem = mesResult.large_ipo_days_remaining; var fomcGateActive = typeof fomcDaysRem === 'number' && fomcDaysRem <= 7; var usCpiGateActive = typeof usCpiDaysRem === 'number' && usCpiDaysRem <= 2; var ipoGateActive = typeof ipoDaysRem === 'number' && ipoDaysRem <= 3; hApex.fomc_position_size_gate = fomcGateActive ? 'ACTIVE' : 'INACTIVE'; hApex.us_cpi_position_size_gate = usCpiGateActive ? 'ACTIVE' : 'INACTIVE'; hApex.ipo_position_size_gate = ipoGateActive ? 'ACTIVE' : 'INACTIVE'; if (fomcGateActive) { (hApex.buy_permission_json || []).forEach(function(bp) { bp.fomc_size_limit = 0.5; bp.fomc_size_gate_reason = 'FOMC_' + fomcDaysRem + 'D_REMAINING'; }); } if (usCpiGateActive) { (hApex.buy_permission_json || []).forEach(function(bp) { bp.us_cpi_size_limit = 0.5; bp.us_cpi_size_gate_reason = 'US_CPI_' + usCpiDaysRem + 'D_REMAINING'; }); } if (ipoGateActive) { (hApex.buy_permission_json || []).forEach(function(bp) { bp.ipo_size_limit = 0.7; bp.ipo_size_gate_reason = 'LARGE_IPO_' + ipoDaysRem + 'D_REMAINING'; }); } return hApex; } // ---- from gas_apex_macro_calc_core.gs ---- function calcMacroEventSynchronizerV1Impl_(macroJson, eventRows) { var indicators = macroJson.indicators || []; var byName = {}; indicators.forEach(function(m) { byName[m.Name] = m; }); var usdKrw = typeof macroJson.usd_krw === 'number' ? macroJson.usd_krw : 0; var vix = typeof macroJson.vix === 'number' ? macroJson.vix : 0; var sp500Ret5d = typeof macroJson.sp500_ret5d === 'number' ? macroJson.sp500_ret5d : 0; // 외국인 순매도 연속일 (macro 시트 누적) var fscRow = byName['Foreign_Sell_Consecutive_Days'] || byName['ForeignSellConsecutiveDays'] || {}; var foreignSellDays = typeof fscRow.Close === 'number' ? Math.round(fscRow.Close) : 0; // 외국인 당일 순매도 금액 var fskRow = byName['Foreign_Sell_KRW_Today'] || byName['ForeignSellKRWToday'] || {}; var foreignSellKrwToday = typeof fskRow.Close === 'number' ? fskRow.Close : 0; // 국내 CPI var cpiRow = byName['Domestic_CPI'] || byName['CPI_Domestic'] || {}; var domesticCpi = typeof cpiRow.Close === 'number' ? cpiRow.Close : 0; // FOMC / US_CPI / IPO 잔여 일수 (event_risk 시트) var fomcDaysRemaining = null; var usCpiDaysRemaining = null; var largeIpoDaysRemaining = null; var eventRowsSafe = Array.isArray(eventRows) ? eventRows : []; function _nearestDays(typeStr) { var list = eventRowsSafe.filter(function(e) { var t = (e.Type || e.type || '').toUpperCase(); var d = typeof e.DaysLeft === 'number' ? e.DaysLeft : (typeof e.daysLeft === 'number' ? e.daysLeft : -1); return t === typeStr && d >= 0; }); if (!list.length) return null; list.sort(function(a, b) { return (a.DaysLeft || a.daysLeft || 999) - (b.DaysLeft || b.daysLeft || 999); }); return list[0].DaysLeft || list[0].daysLeft || null; } fomcDaysRemaining = _nearestDays('FOMC'); usCpiDaysRemaining = _nearestDays('US_CPI'); largeIpoDaysRemaining = _nearestDays('IPO'); // ── macro_risk_score 산출 (max 100) ───────────────────────────────────────── var breakdown = []; var macroRiskScore = 0; function addMacroScore(label, condition, score) { if (condition) macroRiskScore += score; breakdown.push({ factor: label, score: condition ? score : 0, triggered: !!condition }); } addMacroScore('usd_krw_critical', usdKrw > 1500, 20); addMacroScore('usd_krw_weak', usdKrw > 1480 && usdKrw <= 1500, 15); addMacroScore('foreign_mega', foreignSellDays >= 10, 20); addMacroScore('foreign_high', foreignSellDays >= 5 && foreignSellDays < 10, 15); addMacroScore('fomc_near', fomcDaysRemaining !== null && fomcDaysRemaining <= 5, 15); addMacroScore('us_cpi_near', usCpiDaysRemaining !== null && usCpiDaysRemaining <= 2, 10); addMacroScore('cpi_high', domesticCpi > 2.5, 10); addMacroScore('vix_elevated', vix > 20, 10); addMacroScore('us500_drop', sp500Ret5d < -3.0, 10); macroRiskScore = Math.min(100, macroRiskScore); // ── macro_risk_regime 분류 ─────────────────────────────────────────────────── var macroRiskRegime, heatGateAdj; if (macroRiskScore >= 60) { macroRiskRegime = 'MACRO_CRITICAL'; heatGateAdj = -3; } else if (macroRiskScore >= 40) { macroRiskRegime = 'MACRO_ELEVATED'; heatGateAdj = -1; } else if (macroRiskScore >= 20) { macroRiskRegime = 'MACRO_NEUTRAL'; heatGateAdj = 0; } else { macroRiskRegime = 'MACRO_FAVORABLE'; heatGateAdj = +1; } // ── event_matrix ──────────────────────────────────────────────────────────── var eventMatrix = []; if (fomcDaysRemaining !== null && fomcDaysRemaining <= 7) { eventMatrix.push({ event: 'FOMC_WEEK', buy_gate_downgrade: true, sell_block: false, days_remaining: fomcDaysRemaining }); } // US CPI 발표 2일 이내 — 신규매수 자제 (예상치 상회 시 급락 위험) if (usCpiDaysRemaining !== null && usCpiDaysRemaining <= 2) { eventMatrix.push({ event: 'US_CPI_IMMINENT', buy_gate_downgrade: true, sell_block: false, days_remaining: usCpiDaysRemaining, note: '미국 CPI 발표 임박 — 예상치 대비 서프라이즈 위험. 신규매수 자제' }); } // 대형 IPO 5일 이내 — 공모자금 쏠림으로 시장 유동성 흡수 주의 if (largeIpoDaysRemaining !== null && largeIpoDaysRemaining <= 5) { eventMatrix.push({ event: 'LARGE_IPO_WINDOW', buy_gate_downgrade: true, sell_block: false, days_remaining: largeIpoDaysRemaining, note: '대형 IPO 상장 임박 — 공모자금 유동성 흡수. 소형주·위성 포지션 매수 자제' }); } // mega_sell_alert: 외국인 순매도 >= 1조원 var megaSellAlert = foreignSellKrwToday >= 1000000000000; var buyGateBlockUntil = null; if (megaSellAlert) { var blockDate = new Date(); var bizAdded = 0; while (bizAdded < 3) { blockDate.setDate(blockDate.getDate() + 1); var wd = blockDate.getDay(); if (wd !== 0 && wd !== 6) bizAdded++; } buyGateBlockUntil = Utilities.formatDate(blockDate, 'Asia/Seoul', 'yyyy-MM-dd'); eventMatrix.push({ event: 'MEGA_SELL_ALERT', foreign_sell_krw: foreignSellKrwToday, buy_gate_block_until: buyGateBlockUntil }); } return { macro_risk_score: macroRiskScore, macro_risk_regime: macroRiskRegime, macro_risk_breakdown: breakdown, foreign_sell_consecutive_days: foreignSellDays, foreign_sell_krw_today: foreignSellKrwToday, mega_sell_alert: megaSellAlert, buy_gate_block_until: buyGateBlockUntil, effective_heat_gate_adjustment: heatGateAdj, heat_gate_adj: heatGateAdj, fomc_days_remaining: fomcDaysRemaining, us_cpi_days_remaining: usCpiDaysRemaining, large_ipo_days_remaining: largeIpoDaysRemaining, event_matrix: eventMatrix, formula_id: 'MACRO_EVENT_SYNCHRONIZER_V1' }; } function calcMacroRegimeAdaptiveGateV2Impl_(macroJson, mesResult, hApex) { var macro = macroJson || {}; var mes = mesResult || {}; // ── LAYER_1: 미시 리스크 (Market Internals, 0~25) ────────────────── var l1 = 0; var vkospi = toNumber_(macro['vkospi'] || macro.vkospi) || 0; var mrsScoreL1 = toNumber_(macro['mrs_score'] || macro.mrs_score || (hApex && hApex.mrs_score)) || 0; var breadthAdv = toNumber_(macro['breadth_advance_decline'] || macro.breadth_advance_decline) || 0; if (breadthAdv > 0 && breadthAdv < 0.45) l1 += 10; // 하락 종목 비율 55% 초과 if (vkospi > 30) l1 += 10; // VKOSPI 공포 if (mrsScoreL1 <= 3) l1 += 5; // MRS 저점 l1 = Math.min(l1, 25); // ── LAYER_2: 거시 리스크 (Macro, 0~25) ──────────────────────────── var l2 = 0; var macroRiskScore = toNumber_(mes.macro_risk_score) || 0; l2 = Math.min(25, Math.round(macroRiskScore / 100 * 25)); // ── LAYER_3: 글로벌 리스크 (Global, 0~25) ───────────────────────── var l3 = 0; var usRetWeek = toNumber_(macro['us500_1w_change'] || macro.us500_1w_change) || 0; var vix = toNumber_(macro['vix'] || macro.vix) || 0; var globalOvrd = String(macro['global_risk_override'] || '').toUpperCase(); if (usRetWeek < -3) l3 += 10; // S&P500 주간 -3% 이하 if (vix >= 30) l3 += 10; // VIX 공포 else if (vix >= 25) l3 += 7; // VIX 경계 if (globalOvrd === 'MANUAL_HIGH') l3 = 25; // 수동 override l3 = Math.min(l3, 25); // ── LAYER_4: 이벤트 리스크 (Event, 0~25) ────────────────────────── var l4 = 0; var fomcDays = typeof mes.fomc_days_remaining === 'number' ? mes.fomc_days_remaining : 99; var usCpiDays = typeof mes.us_cpi_days_remaining === 'number' ? mes.us_cpi_days_remaining : 99; var largeIpoDays = typeof mes.large_ipo_days_remaining === 'number' ? mes.large_ipo_days_remaining : 99; var megaSell = mes.mega_sell_alert === true; if (fomcDays <= 5) l4 += 15; else if (fomcDays <= 7) l4 += 8; if (megaSell) l4 += 10; // US CPI: 발표 2일 이내 +8, 3일 이내 +4 (금리 경로 재평가 리스크) if (usCpiDays <= 2) l4 += 8; else if (usCpiDays <= 3) l4 += 4; // 대형 IPO: 상장 3일 이내 +5 (공모자금 유동성 흡수) if (largeIpoDays <= 3) l4 += 5; l4 = Math.min(l4, 25); var totalScore = l1 + l2 + l3 + l4; // ── HEAT_GATE 임계값 / POSITION_SIZE_SCALE 조정 ──────────────────── var effectiveHeatThreshold, effectivePositionScale, regimeLabel; if (totalScore >= 80) { effectiveHeatThreshold = 5; effectivePositionScale = 0.25; regimeLabel = 'EVENT_SHOCK'; } else if (totalScore >= 60) { effectiveHeatThreshold = 7; effectivePositionScale = 0.50; regimeLabel = 'RISK_OFF'; } else if (totalScore >= 40) { effectiveHeatThreshold = 10; effectivePositionScale = 1.00; regimeLabel = 'NEUTRAL'; } else { effectiveHeatThreshold = 12; effectivePositionScale = 1.10; regimeLabel = 'RISK_ON'; } // ── 이벤트 날짜 검증 (STALE_EVENT 탐지) ──────────────────────────── var eventDateResults = []; var staleEvents = []; var analysisDate = new Date(); (mes.events_used || []).forEach(function(ev) { if (!ev || !ev.event_date) return; var evDate = new Date(ev.event_date); var valid = evDate >= analysisDate; var r = { event_type: ev.event_type || 'UNKNOWN', event_date: ev.event_date, valid: valid, status: valid ? 'VALID' : 'STALE_EVENT' }; if (!valid) staleEvents.push(r); eventDateResults.push(r); }); return { micro_risk_score: l1, macro_risk_score_normalized: l2, global_risk_score: l3, event_risk_score: l4, total_mrag_score: totalScore, effective_heat_gate_threshold: effectiveHeatThreshold, effective_position_size_scale: effectivePositionScale, regime_label: regimeLabel, event_date_validation_results: eventDateResults, stale_events: staleEvents, stale_events_count: staleEvents.length, formula_id: 'MACRO_REGIME_ADAPTIVE_GATE_V2' }; } // ---- from gas_apex_consistency_core.gs ---- function calcConsistencyValidatorV2Impl_(hApex, asResult, cashFloorInfo, capturedAtIso, now) { var passed = [], failed = [], gapList = []; function chk(id, name, testFn) { try { var r = testFn(); if (r.ok) { passed.push(id); } else { failed.push({ check_id: id, name: name, reason: r.reason || 'failed' }); if (r.gaps) r.gaps.forEach(function(g) { gapList.push(g); }); } } catch(e) { failed.push({ check_id: id, name: name, reason: 'exception:' + e.message }); } } // CV_01: sell_candidates tier 비감소 chk('CV_01', 'sell_priority 방향 일관성', function() { var cands = hApex.sell_candidates_json || []; for (var i = 1; i < cands.length; i++) { var ta = cands[i-1].tier, tb = cands[i].tier; if (typeof ta === 'number' && typeof tb === 'number' && tb < ta) { return { ok: false, reason: 'tier_reversal idx=' + i + '(' + tb + '<' + ta + ')' }; } } return { ok: true }; }); // CV_02: stop < close < tp1 (< tp2) chk('CV_02', '가격 순서 검증', function() { var prices = hApex.prices_json || []; for (var i = 0; i < prices.length; i++) { var p = prices[i]; var stop = p.stop_price || 0, curr = p.current_price || p.close || 0, tp1 = p.tp1_price || 0; if (stop > 0 && curr > 0 && stop >= curr) { return { ok: false, reason: p.ticker + ':stop(' + stop + ')>=close(' + curr + ')' }; } if (curr > 0 && tp1 > 0 && curr >= tp1) { return { ok: false, reason: p.ticker + ':close(' + curr + ')>=tp1(' + tp1 + ')' }; } } return { ok: true }; }); // CV_03: heat vs weight 비례성 (구조 확인용) chk('CV_03', 'heat vs 보유 비중 일치', function() { var holdings = asResult.holdings || []; // heat_pct는 손실위험 기준, weight_pct는 평가비중 — 직접 비교 불가 // 보유 종목 존재 확인 (구조 레벨 검증) if (holdings.length > 0 && !hApex.execution_quality_json) { return { ok: false, reason: 'execution_quality_json 없음 (보유종목 있음)' }; } return { ok: true }; }); // CV_04: enum 유효성 (synthesis_verdict, rs_verdict) chk('CV_04', 'enum 값 유효성', function() { var VALID_SYNTH = ['STRONG_BUY_SIGNAL','MODERATE_BUY_SIGNAL','HOLD_NEUTRAL','TRIM_SIGNAL','EXIT_SIGNAL']; var VALID_RS = ['LEADER','NEUTRAL','LAGGARD','BROKEN','UNKNOWN','N/A','']; var paeList = hApex.predictive_alpha_json || []; for (var i = 0; i < paeList.length; i++) { var v = paeList[i].synthesis_verdict; if (v && VALID_SYNTH.indexOf(v) < 0) { return { ok: false, reason: paeList[i].ticker + ':invalid synthesis_verdict=' + v }; } } var saqgList = hApex.saqg_json || []; for (var j = 0; j < saqgList.length; j++) { var rv = saqgList[j].rs_verdict; if (rv && VALID_RS.indexOf(rv) < 0) { return { ok: false, reason: saqgList[j].ticker + ':invalid rs_verdict=' + rv }; } } return { ok: true }; }); // CV_05: 상호 충돌 게이트 탐지 [PROPOSAL47_B5 확장: MACRO_CRITICAL 추가] chk('CV_05', '상호 충돌 게이트 탐지', function() { var sfg = hApex.satellite_failure_gate_json || {}; var sfgTriggered = sfg.sfg_v1 === 'TRIGGERED'; var megaSell = hApex.mega_sell_alert === true; var macroCritical = hApex.macro_risk_regime === 'MACRO_CRITICAL'; var buyPerms = hApex.buy_permission_json || []; for (var i = 0; i < buyPerms.length; i++) { var bp = buyPerms[i]; var eligible = bp.buy_permission_state === 'ELIGIBLE' || bp.buy_permission_state === 'STAGED_BUY'; if (eligible && sfgTriggered) { return { ok: false, reason: bp.ticker + ':buy=ELIGIBLE but sfg=TRIGGERED' }; } if (eligible && megaSell && hApex.buy_gate_block_until) { return { ok: false, reason: bp.ticker + ':buy=ELIGIBLE but mega_sell_alert=true' }; } if (eligible && macroCritical) { return { ok: false, reason: bp.ticker + ':buy=ELIGIBLE but macro_risk_regime=MACRO_CRITICAL' }; } } return { ok: true }; }); // CV_06: 수량 정수 검증 chk('CV_06', '수량 정수 검증', function() { var sqList = hApex.smart_sell_quantities_json || []; for (var i = 0; i < sqList.length; i++) { var sq = sqList[i]; if (typeof sq.sell_qty === 'number' && sq.sell_qty !== Math.floor(sq.sell_qty)) { return { ok: false, reason: sq.ticker + ':sell_qty 소수점=' + sq.sell_qty }; } } return { ok: true }; }); // CV_07: 데이터 신선도 chk('CV_07', '날짜 신선도', function() { if (!capturedAtIso) return { ok: true }; var capMs = new Date(capturedAtIso).getTime(); if (isNaN(capMs)) return { ok: true }; var nowMs = (now && now.getTime) ? now.getTime() : Date.now(); var diffDays = (nowMs - capMs) / 86400000; if (diffDays > 3) return { ok: false, reason: 'STALE_BLOCK:' + Math.round(diffDays) + '일 경과' }; if (diffDays > 1) return { ok: false, reason: 'STALE_WARN:' + Math.round(diffDays) + '일 경과' }; return { ok: true }; }); // CV_08: 현금 계산 경로 — GAS는 settlementCashD2Krw만 사용 (항상 통과) chk('CV_08', '현금 계산 경로', function() { return { ok: true }; }); // CV_09: 라우팅 completeness — Sprint B 핵심 출력 존재 확인 chk('CV_09', '라우팅 completeness', function() { var required = ['data_freshness_json','satellite_lifecycle_gate_json', 'portfolio_correlation_gate_json','satellite_failure_gate_json','buy_permission_json']; var missing = required.filter(function(k) { return hApex[k] === undefined; }); if (missing.length > 0) { return { ok: false, reason: 'missing:' + missing.join(','), gaps: missing.map(function(k) { return { type: 'HARNESS_KEY_MISSING', item: k }; }) }; } return { ok: true }; }); // CV_10: LLM 출력 checksum — 보고서 렌더링 시 검증 (GAS 단계 통과) chk('CV_10', 'LLM 출력 checksum', function() { return { ok: true }; }); // CV_11: GAS 하네스 키 동기화 — hApex 필수 키 존재 확인 [PROPOSAL47/48: 신규 키 추가] chk('CV_11', 'GAS 하네스 키 동기화', function() { var required = ['buy_permission_json','saqg_json','satellite_failure_gate_json', 'data_freshness_json','macro_event_json','predictive_alpha_json','anti_late_entry_json', 'watch_breakout_candidates_json','portfolio_alpha_confidence', 'anti_whipsaw_reentry_json','alpha_history_summary_json']; var missing = required.filter(function(k) { return hApex[k] === undefined; }); if (missing.length > 0) { return { ok: false, reason: 'HARNESS_KEY_MISSING:' + missing.join(','), gaps: missing.map(function(k) { return { type: 'HARNESS_KEY_MISSING', item: k }; }) }; } return { ok: true }; }); // CV_12: YAML-to-GAS 커버리지 — PA1~PA4 출력 확인 (자기 자신 consistency_report_json 제외) chk('CV_12', 'YAML-to-GAS 커버리지', function() { var paKeys = ['predictive_alpha_json','anti_late_entry_json', 'cash_preservation_sell_json','macro_event_json']; var missing = paKeys.filter(function(k) { return hApex[k] === undefined; }); if (missing.length > 0) { return { ok: false, reason: 'GAS_COVERAGE_GAP:' + missing.join(','), gaps: missing.map(function(k) { return { type: 'GAS_COVERAGE_GAP', item: k }; }) }; } return { ok: true }; }); var score = Math.round(passed.length / 12 * 100); var blockStatus = score < 90 ? 'BLOCK' : (score < 100 ? 'WARNING' : 'PASS'); return { consistency_score: score, cv_verdict: blockStatus, passed: passed, failed: failed, gap_list: gapList, block_status: blockStatus, formula_id: 'CONSISTENCY_VALIDATOR_V2' }; } // ---- TASK-001: RELEASE_GATE_TRUTH_V1 ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function buildReleaseGateTruthV1_(hApex) { // RC1 수정: honest_proof_score >= 70 이어야만 릴리스 허용 // effective_release_gate = AND(cosmetic_gate, honest_gate) var agp = hApex['algorithm_guidance_proof_v1'] || {}; var honestScore = agp['honest_proof_score'] || 0; var honestGate = agp['honest_gate'] || 'FAIL'; var cosmeticGate = agp['gate'] || 'FAIL'; var effectiveGate = (honestGate === 'PASS' && cosmeticGate === 'PASS') ? 'PASS' : 'FAIL'; return { formula_id: 'RELEASE_GATE_TRUTH_V1', honest_proof_score: honestScore, honest_gate: honestGate, cosmetic_gate: cosmeticGate, effective_release_gate: effectiveGate, hts_order_mode: honestScore >= 70 ? 'HTS_ALLOWED' : 'THEORETICAL_ONLY', release_blocked_note: honestScore < 70 ? '[RELEASE_BLOCKED_BY_TRUTH_GATE: honest=' + honestScore + ' < 70]' : null }; } // ---- TASK-002: NON_VACUOUS_PASS_GUARD_V1 ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function guardNonVacuousPass_(gateObj, minSamples) { // RC2 수정: effective_n < minSamples 인 게이트를 WATCH_PENDING_SAMPLE로 강제 강등 minSamples = minSamples || 30; var nFields = ['sample_count','row_count','evaluated_count','samples','n','sample_n']; var effectiveN = null; for (var i = 0; i < nFields.length; i++) { if (gateObj[nFields[i]] !== undefined && gateObj[nFields[i]] !== null) { effectiveN = parseInt(gateObj[nFields[i]], 10); break; } } if (effectiveN === null) effectiveN = 0; var gateVal = (gateObj['gate'] || '').toUpperCase(); if (effectiveN < minSamples && gateVal === 'PASS') { return { gate: 'WATCH_PENDING_SAMPLE', label: '[PASS_INVALID_LOW_N: n=' + effectiveN + ' < ' + minSamples + ']', vacuous: true }; } return { gate: gateVal, vacuous: false }; } // ---- TASK-004: OPERATIONAL_SAMPLE_BACKFILL_V1 ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function evaluateOperationalOutcomeBatch_(proposalHistory, dataFeed, captureDate) { // RC4 수정: LIVE/PAPER 제안의 T+5/T+20 실측 결과를 채움 // live=0 상태이므로 현재는 scaffolded — 실측 표본 누적 후 활성화 var results = []; var opT5Count = 0; var opT20Count = 0; (proposalHistory || []).forEach(function(p) { if (!p.origin || p.origin === 'REPLAY') return; // REPLAY 제외 var today = captureDate ? new Date(captureDate) : new Date(); var entryDate = p.entry_date ? new Date(p.entry_date) : null; if (!entryDate) return; var elapsedDays = Math.floor((today - entryDate) / 86400000); var result = { id: p.id, origin: p.origin, entry_date: p.entry_date }; if (elapsedDays >= 5 && p.realized_return_pct_t5 === undefined) { result.t5_pending = true; // 실측 미채움 } else if (p.realized_return_pct_t5 !== undefined) { opT5Count++; result.t5_filled = true; } if (elapsedDays >= 20 && p.realized_return_pct_t20 === undefined) { result.t20_pending = true; } else if (p.realized_return_pct_t20 !== undefined) { opT20Count++; result.t20_filled = true; } results.push(result); }); return { formula_id: 'OPERATIONAL_SAMPLE_BACKFILL_V1', operational_t5_sample_count: opT5Count, operational_t20_sample_count: opT20Count, unvalidated_label: opT5Count < 30 ? '[UNVALIDATED_LIVE: n=' + opT5Count + ' < 30]' : null, results: results }; } // ---- TASK-005: EVALUATION_WINDOW_HONESTY_V1 ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function labelEvaluationWindow_(outcomeQualityJson) { // RC5 수정: t20_source != operational_t20이면 T20_PROXY 플래그 var t20Source = (outcomeQualityJson && outcomeQualityJson.t20_source) || null; var isProxy = (t20Source !== 'operational_t20'); return { formula_id: 'EVALUATION_WINDOW_HONESTY_V1', t20_source: t20Source, t20_is_proxy: isProxy, t20_label: isProxy ? 'T+20(추정,프록시)' : 'T+20(실측)', release_gate_t20_alpha_blocked: isProxy, proxy_note: isProxy ? '[T20_PROXY: t20_source=' + t20Source + ' - 실측 T+20 표본 0건]' : null }; } // ---- TASK-008: VALUE_PRESERVING_CASH_RAISE_V9 ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function calcValuePreservingCashRaiseV9_(sellCandidates, shortfallKrw, regimeLabel) { // RC 수정: BREACH_FULL_LIQUIDATION 금지, K2 50/50 강제 var REBOUND_FACTORS = {EVENT_SHOCK:0.7, RISK_OFF:0.6, NEUTRAL:0.5, RISK_ON:0.3}; var reboundFactor = REBOUND_FACTORS[regimeLabel] || 0.5; var result = []; var totalDamagePct = 0, count = 0, breachCount = 0; (sellCandidates || []).forEach(function(c) { var qty = parseInt(c.qty || c.quantity || 0, 10); var isOversold = c.rsi14 !== undefined && parseFloat(c.rsi14) < 30; var brtNotBroken = c.brt_verdict !== 'BROKEN'; var emergency = !!c.emergency_full_sell; if ((isOversold || brtNotBroken) && !emergency) { // K2 50/50 var imm = Math.floor(qty / 2); var wait = qty - imm; var reboundTrigger = parseFloat(c.prev_close || 0) + reboundFactor * parseFloat(c.atr20 || 0); result.push({ ticker: c.ticker, immediate_qty: imm, rebound_wait_qty: wait, rebound_trigger_price: Math.round(reboundTrigger), k2_applied: true }); } else { if (c.source === 'BREACH_FULL_LIQUIDATION' && !emergency) breachCount++; result.push({ticker: c.ticker, immediate_qty: qty, rebound_wait_qty: 0, k2_applied: false}); } totalDamagePct += parseFloat(c.value_damage_pct || 0); count++; }); var avgDamage = count > 0 ? totalDamagePct / count : 0; return { formula_id: 'VALUE_PRESERVING_CASH_RAISE_V9', selected_sell_combo: result, raw_value_damage_pct_avg: avgDamage, rebound_capture_probability: result.some(function(r){return r.k2_applied;}) ? 0.5 : 0.0, breach_full_liquidation_count: breachCount, gate: (avgDamage <= 10 && breachCount === 0) ? 'PASS' : 'FAIL' }; } // ---- TASK-009: CAPITAL_STYLE_ALLOCATION_V2 ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function calcCapitalStyleAllocationV2_(ticker, proposalHistory, convictionScore) { // 투자성향별 실측 승률로 가중치 보정 (표본 < 30 시 EXPERT_PRIOR 유지) var styles = ['SCALP','SWING','MOMENTUM','POSITION']; var result = {}; styles.forEach(function(style) { var samples = (proposalHistory || []).filter(function(p) { return p.ticker === ticker && p.style === style && p.origin !== 'REPLAY' && p.realized_return_pct_t5 !== undefined; }); var n = samples.length; var wins = samples.filter(function(p){return parseFloat(p.realized_return_pct_t5||0)>0;}).length; result[style] = { sample_n: n, win_rate: n >= 30 ? (wins/n) : null, weight_source: n >= 30 ? 'DYNAMIC' : 'EXPERT_PRIOR', label: n < 30 ? '[UNVALIDATED_WEIGHT: n=' + n + ' < 30]' : null }; }); // conviction 게이트 var recPct = convictionScore < 35 ? 0 : convictionScore < 50 ? 1.5 : convictionScore < 65 ? 3.0 : convictionScore < 80 ? 5.0 : 7.0; return { formula_id: 'CAPITAL_STYLE_ALLOCATION_V2', ticker: ticker, conviction_score: convictionScore, recommended_pct: recPct, styles: result }; } // ---- TASK-011: DETERMINISTIC_ROUTING_ENGINE_V2 ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function buildRoutingExecutionLogV2_(hApex) { // 기존 11단계 로그에 단계12(RELEASE_GATE_TRUTH) 추가 var agp = hApex['algorithm_guidance_proof_v1'] || {}; var p100 = hApex['pass_100_criteria_v3'] || {}; var honestScore = agp['honest_proof_score'] || 0; var effectiveGate = p100['effective_release_gate'] || (honestScore >= 70 ? 'PASS' : 'FAIL'); var step12 = { step: 12, formula_id: 'RELEASE_GATE_TRUTH_V1', label: '릴리스 진실 게이트', status: effectiveGate, honest_proof_score: honestScore, effective_release_gate: effectiveGate, hts_order_count_if_blocked: effectiveGate !== 'PASS' ? 0 : null, blocked_note: effectiveGate !== 'PASS' ? '[RELEASE_BLOCKED_BY_TRUTH_GATE: honest=' + honestScore + ' < 70]' : null }; // 기존 routing_execution_log에 step12 추가 var existing = hApex['routing_execution_log'] || {}; var steps = Array.isArray(existing.steps) ? existing.steps.slice() : []; steps.push(step12); return Object.assign({}, existing, { steps: steps, stage_count_target: 12, effective_release_gate: effectiveGate }); }