379 lines
13 KiB
JavaScript
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};
|
|
}
|