/** * gas_apex_alpha_watch.gs * ──────────────────────────────────────────────────────────────────────────── * APEX 행위기반 커버리지 하네스 — 핵심 계산 엔진 (Impl) * [2026-05-30] BCH-V1 대응을 위해 분리된 순수 함수들 */ /** * PA2: ANTI_LATE_ENTRY_GATE_V2 * [Python py_anti_late_entry_gate_v2 미러와 100% 동일 로직] * * @param {Array} holdings asResult.holdings * @param {Object} dfMap 종목별 데이터 피드 * @return {Array} anti_late_entry_json */ function calcAntiLateEntryGateV2Impl_(holdings, dfMap) { var results = []; for (var i = 0; i < holdings.length; i++) { var h = holdings[i]; var ticker = h.ticker || ''; var df = dfMap[ticker] || {}; var close = Number(h.close || df.close || 0); var prevClose = Number(df.prevClose || 0); var ma20 = Number(df.ma20 || 0); var rsi14 = Number(df.rsi14 != null ? df.rsi14 : 50); var flowCredit = Number(df.flowCredit != null ? df.flowCredit : 0); var volume = Number(df.volume || 0); var avgVol5d = Number(df.avgVolume5d || 0); var frg5d = Number(df.frg5d || 0); var inst5d = Number(df.inst5d || 0); var ret5d = Number(df.ret5d || 0); var acGate = String(df.acGate || ''); var v1d = prevClose > 0 ? (close - prevClose) / prevClose * 100 : 0.0; var v5d = ret5d; var distWs = 0.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 (acGate === 'BLOCK') distWs += 1.0; var gate1 = 'PASS'; if (v1d >= 3.0) gate1 = 'BLOCK_CHASE'; else if (v1d >= 1.5) gate1 = 'PULLBACK_WAIT'; var gate2 = 'PASS'; if (v5d >= 8.0) gate2 = 'BLOCK_CHASE_5D'; else if (v5d >= 5.0) gate2 = 'PULLBACK_WAIT_5D'; var gate3 = 'PASS'; if (distWs >= 3.0) gate3 = 'BLOCK_DISTRIBUTION'; else if (distWs >= 2.0) gate3 = 'PULLBACK_WAIT_DIST'; var hasBlock = (gate1 === 'BLOCK_CHASE' || gate2 === 'BLOCK_CHASE_5D' || gate3 === 'BLOCK_DISTRIBUTION'); var hasPullback = (gate1 === 'PULLBACK_WAIT' || gate2 === 'PULLBACK_WAIT_5D' || gate3 === 'PULLBACK_WAIT_DIST'); var finalGate = 'PASS'; if (hasBlock) finalGate = 'BLOCK'; else if (hasPullback) finalGate = 'PULLBACK_WAIT'; var grade = 'B'; if (finalGate === 'BLOCK') { grade = 'F'; } else if (v1d < 0.5 && ma20 > 0 && close >= ma20 && close <= ma20 * 1.02 && flowCredit >= 0.55) { grade = 'A'; } else if (v1d < 1.5 && ma20 > 0 && Math.abs(close - ma20) / ma20 <= 0.05) { grade = 'B'; } else if (finalGate === 'PULLBACK_WAIT') { grade = 'C'; } else if (v5d > 5.0) { grade = 'D'; } results.push({ ticker: ticker, gate1_status: gate1, gate2_status: gate2, gate3_status: gate3, final_gate_status: finalGate, anti_late_entry_status: finalGate, entry_grade: grade, velocity_1d: Math.round(v1d * 100) / 100, velocity_5d: Math.round(v5d * 100) / 100, dist_weighted_sum: Math.round(distWs * 10) / 10 }); } return results; } /** * PA5: CONSISTENCY_VALIDATOR_V2 * [P0 GAP 해소 - 데이터 정합성 검증] */ function calcConsistencyValidatorV2Impl_(hApex, asResult, cashFloorInfo, capturedAtIso, now) { var checks = []; var passed = []; var failed = []; var gapList = []; // CV_01: sell_priority 방향 일관성 var sellCandidates = hApex.sell_candidates_json || []; var tierOk = true; for (var i = 1; i < sellCandidates.length; i++) { if (sellCandidates[i].tier < sellCandidates[i-1].tier) { tierOk = false; break; } } if (tierOk) passed.push('CV_01'); else failed.push({check_id: 'CV_01', reason: 'tier_reversal'}); // CV_02: 가격 순서 검증 var prices = hApex.prices_json || []; var priceOk = true; for (var i = 0; i < prices.length; i++) { var p = prices[i]; if (p.stop_price && p.current_price && p.stop_price >= p.current_price) priceOk = false; } if (priceOk) passed.push('CV_02'); else failed.push({check_id: 'CV_02', reason: 'price_hierarchy_violation'}); // CV_06: 수량 정수 검증 var qtyOk = true; var bqi = hApex.buy_qty_inputs_json || []; for (var i = 0; i < bqi.length; i++) { if (bqi[i].final_qty && bqi[i].final_qty % 1 !== 0) qtyOk = false; } if (qtyOk) passed.push('CV_06'); else failed.push({check_id: 'CV_06', reason: 'float_quantity'}); // CV_08: 현금 계산 경로 if (hApex.cash_ledger_basis === 'D2_ONLY') passed.push('CV_08'); else failed.push({check_id: 'CV_08', reason: 'invalid_cash_basis'}); // Score 계산 var score = Math.floor((passed.length / 12) * 100); var status = score >= 90 ? (score === 100 ? 'PASS' : 'WARNING') : 'BLOCK'; return { formula_id: 'CONSISTENCY_VALIDATOR_V2', consistency_score: score, cv_verdict: status === 'BLOCK' ? 'ABORT' : 'PASS', block_status: status, passed: passed, failed: failed, gap_list: gapList, consistency_report_json: { score: score, passed: passed, failed: failed } }; } /** * PA4: MACRO_EVENT_SYNCHRONIZER_V1 */ function calcMacroEventSynchronizerV1Impl_(macroJson, eventRows) { var usdKrw = Number(macroJson.usd_krw || 0); var foreignSellDays = Number(macroJson.foreign_sell_consecutive_days || 0); var score = 0; if (usdKrw > 1500) score += 20; else if (usdKrw > 1480) score += 15; if (foreignSellDays >= 10) score += 20; else if (foreignSellDays >= 5) score += 15; var regime = 'MACRO_NEUTRAL'; var heatAdj = 0; if (score >= 60) { regime = 'MACRO_CRITICAL'; heatAdj = -3; } else if (score >= 40) { regime = 'MACRO_ELEVATED'; heatAdj = -1; } else if (score < 20) { regime = 'MACRO_FAVORABLE'; heatAdj = 1; } return { formula_id: 'MACRO_EVENT_SYNCHRONIZER_V1', macro_risk_score: score, macro_risk_regime: regime, effective_heat_gate_adjustment: heatAdj, mega_sell_alert: false, macro_event_json: { score: score, regime: regime, heat_gate_adj: heatAdj } }; } /** * PA1: PREDICTIVE_ALPHA_ENGINE_V1 */ function calcPredictiveAlphaEngineV1Impl_(holdings, dfMap, macroJson, mesResult, weightOverrides) { var results = []; for (var i = 0; i < holdings.length; i++) { var h = holdings[i]; var ticker = h.ticker; var df = dfMap[ticker] || {}; var thesis = 0; if (df.close > df.ma20 && df.close < df.ma20 * 1.03) thesis += 20; if (df.flowCredit >= 0.55) thesis += 20; var antithesis = 0; var v1d = df.prevClose > 0 ? (df.close - df.prevClose) / df.prevClose * 100 : 0; if (v1d >= 3.0) antithesis += 25; var confidence = thesis - antithesis; var verdict = 'HOLD_NEUTRAL'; if (confidence >= 40) verdict = 'STRONG_BUY_SIGNAL'; else if (confidence >= 20) verdict = 'MODERATE_BUY_SIGNAL'; else if (confidence < -30) verdict = 'EXIT_SIGNAL'; else if (confidence < -10) verdict = 'TRIM_SIGNAL'; results.push({ ticker: ticker, direction_confidence: confidence, thesis_score: thesis, antithesis_score: antithesis, synthesis_verdict: verdict, predictive_alpha_json: { confidence: confidence, verdict: verdict } }); } return results; } /** * MACRO_REGIME_ADAPTIVE_GATE_V2 */ function calcMacroRegimeAdaptiveGateV2Impl_(macroJson, mesResult, hApex) { var totalScore = mesResult.macro_risk_score || 0; var regime = 'MODERATE_RISK'; var heatThreshold = 10.0; var sizeScale = 1.0; if (totalScore >= 75) { regime = 'EXTREME_RISK'; heatThreshold = 5.0; sizeScale = 0.25; } else if (totalScore >= 50) { regime = 'HIGH_RISK'; heatThreshold = 7.0; sizeScale = 0.50; } else if (totalScore < 25) { regime = 'LOW_RISK'; heatThreshold = 12.0; sizeScale = 1.10; } return { formula_id: 'MACRO_REGIME_ADAPTIVE_GATE_V2', total_mrag_score: totalScore, regime_label: regime, effective_heat_gate_threshold: heatThreshold, effective_position_size_scale: sizeScale, mrag_v2_json: { score: totalScore, regime: regime } }; } /** * applyAlegGate4And5Impl_ */ function applyAlegGate4And5Impl_(alegRows, paeRows, hApex) { var results = []; var paeMap = {}; for (var i = 0; i < paeRows.length; i++) paeMap[paeRows[i].ticker] = paeRows[i]; for (var i = 0; i < alegRows.length; i++) { var row = alegRows[i]; var pae = paeMap[row.ticker] || {}; if (pae.synthesis_verdict === 'EXIT_SIGNAL' || pae.synthesis_verdict === 'TRIM_SIGNAL') { row.gate4_status = 'BLOCK_PAE'; row.final_gate_status = 'BLOCK'; row.anti_late_entry_status = 'BLOCK'; } else { row.gate4_status = 'PASS'; } results.push(row); } return results; } /** * Suite Aggregators */ function applyApexMacroAlphaSuiteImpl_(holdings, dfMap, hApex) { // Placeholder for macro alpha suite return hApex; } function applyApexMacroEventSuiteImpl_(hApex) { // Placeholder for macro event suite return hApex; } function applyApexPredictiveAlphaSuiteImpl_(holdings, dfMap, hApex) { var macroJson = hApex.macro_event_json || {}; var mesResult = hApex.macro_event_json || {}; var paeRows = calcPredictiveAlphaEngineV1Impl_(holdings, dfMap, macroJson, mesResult, null); hApex.predictive_alpha_json = paeRows; // portfolio_alpha_confidence: mean direction_confidence across all holdings var sum = 0, n = 0; (paeRows || []).forEach(function(r) { if (typeof r.direction_confidence === 'number') { sum += r.direction_confidence; n++; } }); hApex.portfolio_alpha_confidence = n > 0 ? Math.round(sum / n * 100) / 100 : 0; return hApex; } function applyApexWatchBreakoutSuiteImpl_(holdings, dfMap, hApex) { var slgRows = hApex.satellite_lifecycle_gate_json || []; var aleRows = hApex.anti_late_entry_json || []; hApex.watch_breakout_candidates_json = calcWatchBreakoutRealtimeGateV1_(holdings, dfMap, slgRows, aleRows); return hApex; } // ---- TASK-006: ANTI_LATE_ENTRY_GATE_V2_CALIBRATED ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function calibrateAntiLateEntryV2_(proposalHistory, captureDate) { // RC 수정: velocity 버킷별 T+5 승률 계산 (실측 표본 >= 30 충족 후 활성화) var buckets = { LOW: {n:0,wins:0}, MID: {n:0,wins:0}, HIGH: {n:0,wins:0} }; var totalBuys = 0, chaseBuys = 0; (proposalHistory || []).forEach(function(p) { if (p.origin === 'REPLAY' || p.action !== 'BUY') return; if (p.realized_return_pct_t5 === undefined) return; // 미채움 제외 totalBuys++; var v = parseFloat(p.velocity_1d || 0); var win = parseFloat(p.realized_return_pct_t5 || 0) > 0; var bucket = v < 1.0 ? 'LOW' : v < 3.0 ? 'MID' : 'HIGH'; buckets[bucket].n++; if (win) buckets[bucket].wins++; if (v >= 3.0) chaseBuys++; }); var minSamples = 30; var validated = Object.keys(buckets).every(function(k) { return buckets[k].n >= minSamples; }); return { formula_id: 'ANTI_LATE_ENTRY_GATE_V2_CALIBRATED', validated: validated, unvalidated_label: validated ? null : '[UNVALIDATED_LIVE: n<30 per bucket]', chase_entry_rate_pct: totalBuys > 0 ? (chaseBuys / totalBuys * 100).toFixed(1) : null, buckets: buckets, threshold_source: validated ? 'DYNAMIC' : 'EXPERT_PRIOR', velocity_1d_block_pct: 3.0 }; } // ---- TASK-007: DISTRIBUTION_BLOCK_EFFECTIVENESS_V1 ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function trackDistributionBlockEffectiveness_(proposalHistory) { var blocked = (proposalHistory || []).filter(function(p) { return p.blocked_reason === 'DISTRIBUTION_CONFIRMED' || p.blocked_reason === 'DISTRIBUTION_BLOCK'; }); var avoidedLoss = blocked.filter(function(p) { return p.t5_return_if_not_blocked !== undefined && parseFloat(p.t5_return_if_not_blocked) < 0; }); var blockedN = blocked.length; var avoidedLossRate = blockedN > 0 ? (avoidedLoss.length / blockedN) : null; return { formula_id: 'DISTRIBUTION_BLOCK_EFFECTIVENESS_V1', blocked_sample_count: blockedN, avoided_loss_rate: avoidedLossRate, target_avoided_loss_rate: 0.60, effectiveness_label: blockedN < 30 ? '[UNVALIDATED_LOW_N: n=' + blockedN + ' < 30]' : (avoidedLossRate >= 0.60 ? 'EFFECTIVE' : 'REVIEW_THRESHOLD') }; } // ---- TASK-010: SMART_MONEY_LIQUIDITY_OUTCOME_LINK_V1 ---- // [GAS_STUB_ONLY: requires Google Sheets deployment] function linkSmartMoneyOutcome_(proposalHistory) { var buckets = {}; (proposalHistory || []).forEach(function(p) { if (p.origin === 'REPLAY' || !p.liquidity_label) return; var lbl = p.liquidity_label; if (!buckets[lbl]) buckets[lbl] = {returns:[], slippages:[]}; if (p.realized_return_pct_t5 !== undefined) buckets[lbl].returns.push(parseFloat(p.realized_return_pct_t5)); if (p.slippage_pct !== undefined) buckets[lbl].slippages.push(parseFloat(p.slippage_pct)); }); var table = Object.keys(buckets).map(function(lbl) { var d = buckets[lbl]; var n = d.returns.length; var wins = d.returns.filter(function(r){return r>0;}).length; return { liquidity_label: lbl, sample_count: n, t5_avg_return_pct: n > 0 ? d.returns.reduce(function(a,b){return a+b;},0)/n : null, t5_win_rate: n > 0 ? wins/n : null, label: n < 30 ? '[UNVALIDATED: n=' + n + ' < 30]' : 'VALIDATED' }; }); return {formula_id: 'SMART_MONEY_LIQUIDITY_OUTCOME_LINK_V1', table: table}; }