Files
QuantEngineByItz/gas_apex_alpha_watch.gs

379 lines
13 KiB
JavaScript

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