chore: 프로젝트 루트의 파편화된 .gs 파일들을 src/gas_adapter_parts/로 이동 격리
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 4s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m17s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped

This commit is contained in:
2026-06-26 11:33:46 +09:00
parent 3e7ea1d007
commit 6051338367
129 changed files with 979 additions and 114 deletions
@@ -0,0 +1,378 @@
/**
* 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};
}
@@ -0,0 +1,705 @@
// 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
});
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// gas_event_calendar.gs — compatibility stub
//
// 이벤트 캘린더 seed / risk 로직은 gas_lib.gs 에 구현되어 있다.
// 이 파일은 upload ZIP / GAS 프로젝트 배포에서 명시적 엔트리포인트를 유지하기 위한 호환 스텁이다.
//
// 실제 동작 함수:
// seedEventCalendar_()
// runEventRisk()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+446
View File
@@ -0,0 +1,446 @@
// gas_report.gs - Report & template generation
// getDailyBrief, getSummaryJson, getTradeTemplate
// Changes only when report format changes. Rarely touched during engine work.
// GAS global scope: functions in gas_lib.gs / gas_data_feed.gs callable directly
// ── E1: 일일 의사결정 브리핑 ─────────────────────────────────────────────────
// 시장 상태·포트폴리오 건강·액션 목록·주의 종목·7일 이벤트를 한 JSON으로 통합.
// doGet(?view=brief) 또는 cacheAllViews()에서 매일 1회 생성.
function getDailyBrief(sellPriorityViewInput) {
const macro = getMacroJson();
const settings = readSettingsTab_();
const port = getPortfolioJson();
const events = getEventRiskJson();
const today = Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd");
const holdings = port.holdings ?? [];
// ── 액션 분류: Final_Action canonical 기준 (A-1/B-1 — Allowed_Action 기반 제거) ──
// Final_Action이 canonical output field. Allowed_Action은 중간 계산값.
const BUY_FINALS_ = new Set(["BUY_STAGE1_READY","BUY_BREAKOUT_PILOT_ONLY","BUY_PULLBACK_WAIT"]);
const SELL_FINALS_ = new Set(["SELL_READY"]);
const EXIT_FINALS_ = new Set(["EXIT_SIGNAL","EXIT_REVIEW"]);
const sellList = holdings.filter(h => SELL_FINALS_.has(h.Final_Action));
const exitList = holdings.filter(h => EXIT_FINALS_.has(h.Final_Action));
const buyList = holdings.filter(h => BUY_FINALS_.has(h.Final_Action));
const watchList = holdings.filter(h => h.Final_Action === "WATCH_TIMING_SETUP");
const holdList = holdings.filter(h =>
!SELL_FINALS_.has(h.Final_Action) && !EXIT_FINALS_.has(h.Final_Action) &&
!BUY_FINALS_.has(h.Final_Action) && h.Final_Action !== "WATCH_TIMING_SETUP"
);
// 주의 종목
const stage2Pass = holdings.filter(h => h.Stage2_Gate === "PASS");
const timeStopNear= holdings.filter(h => Number.isFinite(+h.Days_To_Time_Stop)
&& +h.Days_To_Time_Stop >= 0
&& +h.Days_To_Time_Stop <= 7);
const overweight = holdings.filter(h => h.Band_Status === "OVERWEIGHT");
const tp1Near = holdings.filter(h => Number.isFinite(+h.Profit_Pct) && +h.Profit_Pct >= 10);
// 포트폴리오 건강 판단
const heatVal = parseFloat(macro.total_heat_pct);
const fcVal = parseFloat(macro.fc_budget_pct);
const heatOk = Number.isFinite(heatVal) && heatVal < 10;
const heatCautionB= Number.isFinite(heatVal) && heatVal >= 7 && heatVal < 10;
const heatBlockB = Number.isFinite(heatVal) && heatVal >= 10;
const fcOk = Number.isFinite(fcVal) && fcVal < 100;
const regimeStr = String(macro.market_regime ?? "");
const isRiskOffB = regimeStr === "RISK_OFF" || regimeStr === "RISK_OFF_CANDIDATE";
const nrf = macro.net_return_feedback;
const orbitAdj= parseInt(macro.orbit_slot_adj) || 0;
// account_snapshot freshness 체크
const acctFresh = checkAccountSnapshotFreshness_();
// 텍스트 브리핑 (ChatGPT 직접 복붙용)
const L = [];
const hardBlockWarn = String(settings["cash_floor_hard_block_warning"] ?? "").trim();
const accountConfirmWarn = String(settings["account_snapshot_confirmation_warning"] ?? "").trim();
const cashLedgerWarn = String(settings["cash_ledger_warning"] ?? "").trim();
if (hardBlockWarn) L.push(`[긴급 경고] ${hardBlockWarn}`);
if (accountConfirmWarn) L.push(`[운영 경고] ${accountConfirmWarn}`);
if (cashLedgerWarn) L.push(`[운영 경고] ${cashLedgerWarn}`);
L.push(`[시장] ${macro.market_regime} / MRS ${macro.mrs_score}/10 / VIX ${macro.vix} / KOSPI ${macro.kospi} / USD/KRW ${macro.usd_krw}`);
const heatTag = heatBlockB ? "⚠HF005:BLOCK" : heatCautionB ? "⚠CAUTION:수량50%감액" : "OK";
L.push(`[포트폴리오] HEAT ${macro.total_heat_pct}%(${heatTag}) / FC ${macro.fc_budget_pct}%(${fcOk?"OK":"⚠EXHAUSTED"}) / ${nrf} / BUCKET ${macro.bucket_status}`);
if (isRiskOffB) L.push(`[⚠ 레짐 차단] ${regimeStr} — 신규 매수 전면 차단, 보유 종목 50% 단계 축소 검토`);
const bayesSourceTag = macro.bayesian_data_source === "actual" ? "실제거래기반" : "기본값(거래이력없음)";
L.push(`[Bayesian] ${macro.bayesian_label} (${macro.bayesian_multiplier}×) — ${bayesSourceTag}`);
if (acctFresh.fresh === false) L.push(`[⚠ account_snapshot STALE] ${acctFresh.reason} — 손절가·수량 재확인 필요`);
else if (acctFresh.fresh === null) L.push(`[⚠ account_snapshot] ${acctFresh.reason}`);
// 데이터 신선도 경고 — PRICE_STALE / PRICE_QUOTE_ONLY / FLOW_STALE
const priceStaleList_ = holdings.filter(h => h.Price_Status === "PRICE_STALE");
const quoteOnlyList_ = holdings.filter(h => h.Price_Status === "PRICE_QUOTE_ONLY");
const flowStaleList_ = holdings.filter(h => String(h.Missing_Fields ?? "").includes("FLOW_STALE"));
if (priceStaleList_.length)
L.push(`[⚠ 가격 스테일] ${priceStaleList_.map(h => h.Name).join(", ")} — OHLC 날짜 오래됨, runDataFeed 재실행 권장`);
if (quoteOnlyList_.length)
L.push(`[⚠ 호가전용] ${quoteOnlyList_.map(h => h.Name).join(", ")} — OHLC 수집 실패, MA/ATR 결측 → OBSERVE_ONLY 처리`);
if (flowStaleList_.length)
L.push(`[⚠ 수급 스테일] ${flowStaleList_.map(h => h.Name).join(", ")} — 외국인/기관 수급 날짜 오래됨`);
if (orbitAdj !== 0)
L.push(`[Orbit] ${macro.orbit_state} → 공격슬롯 ${orbitAdj>0?"+":""}${orbitAdj}개 / 현금조정 ${macro.orbit_cash_adj}%p`);
// ── C-1: Final_Action 기준 단일 우선순위 목록 ─────────────────────────────
// 우선순위 순서: SELL_READY > EXIT_* > BUY > WATCH > HOLD
// 같은 그룹 내에서는 Final_Rank(Priority_Score) 오름차순
const byRank = (arr) => [...arr].sort((a, b) => (+a.Final_Rank || 999) - (+b.Final_Rank || 999));
L.push("─".repeat(44));
L.push(`[오늘 액션] — ${today} (Final_Action 기준, 우선순위 정렬)`);
if (sellList.length) {
L.push(" ▶ SELL_READY (즉시 HTS 주문 가능)");
byRank(sellList).forEach((h, i) => {
const r = h.Action_Reason || `${h.Sell_Action} ${h.Sell_Qty}주 @${h.Sell_Limit_Price}`;
const p = h.Action_Params ? `\n ${h.Action_Params}` : "";
L.push(` ${i+1}. ${h.Name} → ${r}${p}`);
});
}
if (exitList.length) {
L.push(" ▶ EXIT_SIGNAL / REVIEW (캡처 → ChatGPT 수량 계산 후 매도)");
byRank(exitList).forEach((h, i) => {
const r = h.Action_Reason || `${h.Final_Action}(RW${h.RW_Partial})`;
const p = h.Action_Params ? ` | ${h.Action_Params}` : "";
L.push(` ${sellList.length+i+1}. ${h.Name}[${h.Final_Action}] → ${r}${p}`);
});
}
if (buyList.length) {
L.push(" ▶ BUY (진입 조건 충족)");
byRank(buyList).forEach((h, i) => {
const constr = h.Pos_Size_Constraint || "미계산*";
const rank_ = sellList.length + exitList.length + i + 1;
L.push(` ${rank_}. ${h.Name}[${h.Final_Action}] → ${h.Action_Reason || ""}`);
const params_ = h.Action_Params || `목표 ${h.Pos_Size_Qty}주[${constr}]`;
L.push(` ${params_}`);
});
}
if (watchList.length) {
L.push(" ▶ WATCH (타이밍 대기)");
byRank(watchList).forEach((h, i) => {
const rank_ = sellList.length + exitList.length + buyList.length + i + 1;
L.push(` ${rank_}. ${h.Name} → ${h.Action_Reason || `SS001:${h.SS001_Grade} 타이밍미충족`}`);
});
}
if (holdList.length) {
L.push(" ▶ HOLD / BLOCK");
byRank(holdList).forEach((h, i) => {
const rank_ = sellList.length + exitList.length + buyList.length + watchList.length + i + 1;
L.push(` ${rank_}. ${h.Name}[${h.Allowed_Action}] → ${h.Action_Reason || h.Allowed_Action}`);
});
}
if (!sellList.length && !exitList.length && !buyList.length && !watchList.length)
L.push(" HOLD — 오늘 액션 없음");
// 단일 진실원천: sell_priority는 반드시 runSellPriority() 결과만 사용
const sellPriorityView_ = sellPriorityViewInput || runSellPriority();
const _cashRaiseCands_ = Array.isArray(sellPriorityView_.sell_priority_table)
? sellPriorityView_.sell_priority_table
: [];
const _cashBelowTgt_ = isRiskOffB || (() => {
const cp = parseFloat(macro.immediate_cash_pct ?? macro.cash_pct ?? "");
const tp = parseFloat(macro.target_cash_pct ?? settings["weekly_target_cash_pct"] ?? "10");
return Number.isFinite(cp) && Number.isFinite(tp) && cp < tp;
})();
if (_cashBelowTgt_ && _cashRaiseCands_.length) {
L.push("─".repeat(44));
const gapReason = isRiskOffB
? `REGIME_TRIM_50 발동(${regimeStr})`
: `현금 부족 → sell_priority_engine`;
L.push(`[현금확보 매도우선순위] — ${gapReason}`);
L.push(" spec: ①하드스탑>②매도신호>③중복ETF>④손실위성>⑥익절>⑨코어주도주(마지막)");
L.push(" ⚠ 매도수량은 HTS 캡처 제공 후 결정 — 수량 미제공 시 수량 산출 금지(P1규칙)");
_cashRaiseCands_.slice(0, 8).forEach((c, i) => {
const pStr = (c.profit_pct !== "" && c.profit_pct !== null)
? ` (${Number(c.profit_pct) >= 0 ? "+" : ""}${Number(c.profit_pct).toFixed(1)}%)`
: "";
const etfTag = c.is_etf ? "[ETF]" : "";
const clTag = c.is_core_leader ? "[주도주⛔매도금지]" : "";
L.push(` ${i+1}. ${c.tier_label} ${c.name}${etfTag}${clTag} W:${c.weight_pct}%${pStr} RW:${c.rw_partial} Score:${c.sell_priority_score}`);
if (c.trim_style || c.rebound_holdback_score)
L.push(` └ trim=${c.trim_style || "N/A"} rebound_holdback=${c.rebound_holdback_score ?? 0}${c.rebound_holdback_reason ? ` | ${c.rebound_holdback_reason}` : ""}`);
if (c.action_params) L.push(` └ ${c.action_params}`);
if (c.hold_reason) L.push(` └ ⚠ ${c.hold_reason}`);
});
}
// 주의 종목 섹션
if (stage2Pass.length || timeStopNear.length || overweight.length || tp1Near.length) {
L.push("[주의]");
stage2Pass.forEach(h => L.push(` ${h.Name} Stage2_Gate=PASS → 2단계 진입 검토 (진입가 ${h.Limit_Price_Est ?? "N/A"})`));
timeStopNear.forEach(h => L.push(` ${h.Name} Time_Stop ${h.Days_To_Time_Stop}일 남음 (${h.Time_Stop_Date})`));
overweight.forEach(h => L.push(` ${h.Name} OVERWEIGHT ${h.Weight_Pct}% (상한 7%)`));
tp1Near.forEach(h => L.push(` ${h.Name} +${h.Profit_Pct}% → TP1(${h.TP1_Price}원) 근접`));
}
if (events.upcoming_7d?.length) {
L.push("[7일 이벤트]");
events.upcoming_7d.forEach(ev => L.push(` ${ev.Date}(D+${ev.DaysLeft}) ${ev.Event} [${ev.Impact}]`));
}
// brief_ — holdings row → JSON 요약 (API 소비자용)
const brief_ = (h) => ({
ticker: h.Ticker, name: h.Name,
final_action: h.Final_Action, // canonical output field
action_reason: h.Action_Reason, // 왜 이 액션인가
action_params: h.Action_Params, // 실행 파라미터 압축 (C-3)
final_rank: h.Final_Rank,
allowed_action: h.Allowed_Action,
ss001_grade: h.SS001_Grade, ss001_norm_score: h.SS001_Norm_Score,
rw_partial: h.RW_Partial,
weight_pct: h.Weight_Pct, profit_pct: h.Profit_Pct,
stage2_gate: h.Stage2_Gate, band_status: h.Band_Status,
limit_price_est: h.Limit_Price_Est,
stop_price_est: h.Stop_Price_Est, stop_price_source: h.Stop_Price_Source,
pos_size_qty: h.Pos_Size_Qty, pos_size_constraint: h.Pos_Size_Constraint,
tp1_price: h.TP1_Price, tp1_qty: h.TP1_Qty,
tp2_price: h.TP2_Price, tp2_qty: h.TP2_Qty,
entry_mode: h.Entry_Mode, entry_mode_gate: h.Entry_Mode_Gate,
entry_mode_reason: h.Entry_Mode_Reason,
timing_score_entry: h.Timing_Score_Entry,
timing_score_exit: h.Timing_Score_Exit,
timing_action: h.Timing_Action,
timing_block_reason: h.Timing_Block_Reason,
sell_action: h.Sell_Action,
sell_ratio_pct: h.Sell_Ratio_Pct,
sell_limit_price: h.Sell_Limit_Price,
sell_reason: h.Sell_Reason,
sell_validation: h.Sell_Validation,
cash_preserve_style: h.Cash_Preserve_Style || "",
cash_preserve_ratio: h.Cash_Preserve_Ratio || "",
cash_preserve_reason: h.Cash_Preserve_Reason || "",
rsi14: h.RSI14, disparity: h.Disparity, ma20_slope: h.MA20_Slope,
exit_signal_detail: h.Exit_Signal_Detail,
});
return {
date: today,
brief_text: L.join("\n"),
market: {
regime: macro.market_regime, mrs_score: macro.mrs_score,
vix: macro.vix, kospi: macro.kospi, usd_krw: macro.usd_krw,
sp500_ret5d: macro.sp500_ret5d,
},
portfolio_health: {
heat_pct: macro.total_heat_pct, heat_ok: heatOk,
heat_tag: heatTag,
heat_block: heatBlockB, heat_caution: heatCautionB,
fc_budget_pct: macro.fc_budget_pct, fc_ok: fcOk,
net_return_feedback: nrf,
bucket_status: macro.bucket_status,
regime_buy_blocked: isRiskOffB,
bayesian_label: macro.bayesian_label,
bayesian_multiplier: macro.bayesian_multiplier,
},
orbit: {
gap_pct: macro.orbit_gap_pct, state: macro.orbit_state,
slot_adjustment: orbitAdj, cash_adjustment: macro.orbit_cash_adj,
},
// Final_Action canonical 분류 (A-1/B-1)
actions: {
sell_ready: sellList.map(brief_),
exit_signals: exitList.map(brief_),
buy_signals: buyList.map(brief_),
watch_signals: watchList.map(brief_),
hold_signals: holdList.map(brief_),
},
alerts: {
stage2_ready: stage2Pass.map(h=>({ticker:h.Ticker,name:h.Name,profit_pct:h.Profit_Pct,limit_price_est:h.Limit_Price_Est})),
time_stop_near: timeStopNear.map(h=>({ticker:h.Ticker,name:h.Name,days_left:h.Days_To_Time_Stop,stop_date:h.Time_Stop_Date})),
overweight: overweight.map(h=>({ticker:h.Ticker,name:h.Name,weight_pct:h.Weight_Pct})),
tp1_near: tp1Near.map(h=>({ticker:h.Ticker,name:h.Name,profit_pct:h.Profit_Pct,tp1_price:h.TP1_Price,tp2_price:h.TP2_Price})),
},
upcoming_events: events.upcoming_7d,
account_snapshot_freshness: acctFresh,
data_quality: {
price_stale: priceStaleList_.map(h=>({ticker:h.Ticker,name:h.Name,price_date:h.Price_Date})),
quote_only: quoteOnlyList_.map(h=>({ticker:h.Ticker,name:h.Name})),
flow_stale: flowStaleList_.map(h=>({ticker:h.Ticker,name:h.Name,missing_fields:h.Missing_Fields})),
},
// sell_priority_engine 출력 (spec: portfolio_exposure.yaml:sell_priority_engine)
// 활성화: REGIME_TRIM_50 또는 현금 부족. ETF→손실위성→코어주도주 순서로 정렬.
cash_raise: _cashBelowTgt_ ? {
active: true,
reason: isRiskOffB ? `REGIME_TRIM_50(${regimeStr})` : "cash_below_target",
prohibition: "매도수량은 HTS 캡처 제공 후 결정. 수량 미제공 시 수량 기재 금지(spec:P1규칙).",
sell_priority_table: _cashRaiseCands_,
sector_exposure_summary: sellPriorityView_.sector_exposure ?? sellPriorityView_.sector_exposure_summary ?? {},
} : { active: false },
};
}
// ── E3: 거래 진입 템플릿 생성 ────────────────────────────────────────────────
// BUY_CANDIDATE/WATCH_CANDIDATE 종목에 대해 performance 탭 입력 행 + 진입 체크리스트 반환.
// doGet(?view=trade_template&ticker=064350)
function getTradeTemplate(ticker) {
if (!ticker) return { error: "ticker 파라미터 필요 (?view=trade_template&ticker=XXXXXX)" };
const allData = sheetToJson("data_feed");
const row = allData.find(r => String(r.Ticker) === String(ticker) || r.Name === ticker);
if (!row) return { error: `ticker ${ticker} not found in data_feed` };
const macro = getMacroJson();
const today = Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd");
const sector = TICKER_SECTOR_MAP[ticker] ?? "N/A";
// 진입 체크리스트 — 각 항목 true/false
const checklist = {
data_quality: row.Price_Status === "PRICE_OK",
no_dart_risk: !row.DART_Risk || row.DART_Risk === "" || row.DART_Risk === "N",
liquidity_ok: row.Liquidity_Status === "OK",
timing_ready: ["BUY_STAGE1_READY","BUY_PULLBACK_WAIT","BUY_BREAKOUT_PILOT_ONLY"].includes(row.Timing_Action),
leader_gate: ["PASS","EXPLORE_CANDIDATE","WATCH_ONLY"].includes(row.Leader_Gate),
ac_gate: row.AC_Gate === "CLEAR",
flow_credit_ok: parseFloat(row.Flow_Credit) >= 0.4,
regime_ok: ["RISK_ON","SECULAR_LEADER_RISK_ON","LEADER_CONCENTRATION"].includes(macro.market_regime),
heat_ok: Number.isFinite(parseFloat(macro.total_heat_pct)) && parseFloat(macro.total_heat_pct) < 10,
fc_budget_ok: Number.isFinite(parseFloat(macro.fc_budget_pct)) && parseFloat(macro.fc_budget_pct) < 100,
nr_feedback_ok: macro.net_return_feedback !== "REDUCED",
ee_positive: parseFloat(row.EE_Est) > 0,
ss001_grade_ok: ["A","B"].includes(row.SS001_Grade),
};
const passCount = Object.values(checklist).filter(Boolean).length;
const totalCheck = Object.keys(checklist).length;
const gateStatus = passCount === totalCheck ? "ALL_PASS"
: passCount >= totalCheck - 2 ? "MINOR_ISSUES"
: "BLOCK";
return {
ticker,
name: row.Name,
sector,
generated_at: today,
gate_status: gateStatus,
gate_score: `${passCount}/${totalCheck}`,
checklist,
// performance 탭에 바로 붙여넣을 수 있는 행 템플릿
performance_tab_template: {
trade_id: `${today.replace(/-/g,"")}${ticker}`,
ticker,
sector,
entry_date: today,
entry_price: row.Limit_Price_Est ?? "",
entry_stage: "stage_1",
quantity: row.Pos_Size_Qty ?? "",
stop_price_at_entry: row.Stop_Price_Est ?? "",
target_price_at_entry: row.Target_Price ?? "",
exit_date: "",
exit_price: "",
exit_reason: "",
pnl_pct: "",
holding_days: "",
entry_c1_score: row.C1_Price ?? "",
entry_c2_score: row.C2_RelStr ?? "",
entry_c3_score: row.C3_VolSurge ?? "",
entry_c4_score: row.C4_Flow ?? "",
entry_c5_score: row.C5_Sector ?? "",
entry_mode: row.Entry_Mode ?? "",
entry_gate: row.Entry_Mode_Gate ?? "",
timing_action: row.Timing_Action ?? "",
timing_score_entry: row.Timing_Score_Entry ?? "",
timing_score_exit: row.Timing_Score_Exit ?? "",
anti_climax_gate: row.AC_Gate ?? "",
flow_credit: row.Flow_Credit ?? "",
entry_mrs_score: macro.mrs_score ?? "",
fc_bucket: "",
},
current_state: {
close: row.Close,
allowed_action: row.Allowed_Action,
timing_action: row.Timing_Action,
timing_score_entry: row.Timing_Score_Entry,
timing_score_exit: row.Timing_Score_Exit,
timing_block_reason: row.Timing_Block_Reason,
sell_action: row.Sell_Action,
sell_ratio_pct: row.Sell_Ratio_Pct,
sell_qty: row.Sell_Qty,
sell_limit_price: row.Sell_Limit_Price,
sell_price_source: row.Sell_Price_Source,
sell_reason: row.Sell_Reason,
sell_validation: row.Sell_Validation,
ss001_grade: row.SS001_Grade,
ss001_total: row.SS001_Total,
flow_credit: row.Flow_Credit,
rw_partial: row.RW_Partial,
limit_price_est: row.Limit_Price_Est,
stop_price_est: row.Stop_Price_Est,
stop_price_source: row.Stop_Price_Source,
ee_est: row.EE_Est,
pos_size_qty: row.Pos_Size_Qty,
upside_pct: row.Upside_Pct,
atr20: row.ATR20,
tp1_price: row.TP1_Price,
tp1_qty: row.TP1_Qty,
tp2_price: row.TP2_Price,
tp2_qty: row.TP2_Qty,
dart_risk: row.DART_Risk,
days_to_earnings: row.Days_To_Earnings,
},
};
}
function getSummaryJson() {
// ChatGPT 포트폴리오 분석에 최적화된 통합 뷰
const sectors = getSectorFlowJson();
const port = getPortfolioJson();
const macro = getMacroJson();
const events = getEventRiskJson();
// 포트폴리오 전체 수급 요약
const holdings = port.holdings;
const totalFrg5 = holdings.reduce((s,h) => s + (parseFloat(h.Frg_5D) || 0), 0);
const totalInst5 = holdings.reduce((s,h) => s + (parseFloat(h.Inst_5D) || 0), 0);
const flowOkCount = holdings.filter(h => h.Flow_OK === "Y").length;
// SS001 등급 분포 및 Allowed_Action 집계
const ss001Dist = { A: 0, B: 0, C: 0, D: 0 };
const actionDist = {};
holdings.forEach(h => {
const g = h["SS001_Grade"];
if (g in ss001Dist) ss001Dist[g]++;
const a = h["Allowed_Action"] || "UNKNOWN";
actionDist[a] = (actionDist[a] ?? 0) + 1;
});
return {
portfolio_flow_summary: {
total_holdings: holdings.length,
data_ok_count: flowOkCount,
portfolio_frg_5d_total: totalFrg5,
portfolio_inst_5d_total: totalInst5,
portfolio_indiv_5d_total: -(totalFrg5 + totalInst5),
},
ss001_grade_distribution: ss001Dist,
action_distribution: actionDist,
sector_summary: {
total_sectors: sectors.count,
top_inflow_sectors: sectors.top_inflow,
outflow_warning_sectors: sectors.outflow_warning,
strong_smart_money_sectors: sectors.strong_smart_money,
},
macro_snapshot: {
vix: macro.vix,
usd_krw: macro.usd_krw,
kospi: macro.kospi,
sp500_5d_ret: macro.sp500_ret5d,
market_regime: macro.market_regime,
mrs_score: macro.mrs_score,
bayesian_multiplier: macro.bayesian_multiplier,
total_heat_pct: macro.total_heat_pct,
fc_budget_pct: macro.fc_budget_pct,
net_return_feedback: macro.net_return_feedback,
orbit_gap_pct: macro.orbit_gap_pct,
orbit_state: macro.orbit_state,
orbit_slot_adj: macro.orbit_slot_adj,
bucket_status: macro.bucket_status,
bucket_detail: macro.bucket_detail,
},
event_alerts: events.upcoming_7d,
holdings_detail: holdings,
sector_detail: sectors.sectors,
macro_detail: macro.indicators,
macro_computed: macro.computed_summary,
};
}