Files
QuantEngineByItz/gas_apex_runtime_core.gs
T

706 lines
31 KiB
JavaScript

// 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
});
}