Files
QuantEngineByItz/src/gas_adapter_parts/gdf_04_execution_quality.gs
T
kjh2064 ee3e799de1 feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)
주요 변경:
- tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규
  * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합
  * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일)
- src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규
  * Logger.log / getSpreadsheet_() 로 run_all 연동 수정
- src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs
  * _mergePositionRecord_(): 소수주 중복 행 합산 신규
  * parseInt → parseFloat (qty, availQty)
- src/gas_adapter_parts/gdf_01_price_metrics.gs
  * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL
- spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63)
- spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:20:14 +09:00

2256 lines
102 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function calcProfitPreservationRow_(h, df, priceRow, distributionRow) {
var close = df.close || h.close || 0;
var avgCost = h.avgCost || 0;
var profitPct = close > 0 && avgCost > 0 ? (close - avgCost) / avgCost * 100 : 0;
var state = 'NORMAL';
var preserveScore = 100;
if (profitPct >= 30) state = 'PROFIT_LOCK_30';
else if (profitPct >= 20) state = 'PROFIT_LOCK_20';
else if (profitPct >= 10) state = 'PROFIT_LOCK_10';
else if (profitPct >= 8 || (df.atr20 > 0 && close >= avgCost + df.atr20)) state = 'BREAKEVEN_RATCHET';
if (state === 'PROFIT_LOCK_30') preserveScore = 20;
else if (state === 'PROFIT_LOCK_20') preserveScore = 40;
else if (state === 'PROFIT_LOCK_10') preserveScore = 60;
else if (state === 'BREAKEVEN_RATCHET') preserveScore = 80;
if (state === 'PROFIT_LOCK_30' && distributionRow && distributionRow.anti_distribution_state === 'PASS') {
state = 'APEX_TRAILING';
}
if (distributionRow && distributionRow.anti_distribution_state === 'BLOCK_BUY') {
preserveScore = Math.max(0, preserveScore - 15);
}
// L2: RATCHET_TRAILING_AUTO_V1 — ATR 기반 자동 트레일링 손절 계산
var atr20 = typeof df.atr20 === 'number' && df.atr20 > 0 ? df.atr20 : 0;
var ratchetStop = priceRow && typeof priceRow.stop_price === 'number' ? priceRow.stop_price : 0;
var highestClose = priceRow && typeof priceRow.highest_price_since_entry === 'number'
? priceRow.highest_price_since_entry : close;
var autoTrailingStop = null;
var autoTrailingNote = null;
if (atr20 > 0 && (state === 'PROFIT_LOCK_30' || state === 'APEX_TRAILING')) {
var raw = Math.max(ratchetStop, highestClose - 2.0 * atr20);
autoTrailingStop = tickNormalize_(raw);
autoTrailingNote = 'max(ratchet,' + highestClose + '-2.0×ATR)';
} else if (atr20 > 0 && state === 'PROFIT_LOCK_20') {
var raw = Math.max(ratchetStop, highestClose - 1.5 * atr20);
autoTrailingStop = tickNormalize_(raw);
autoTrailingNote = 'max(ratchet,' + highestClose + '-1.5×ATR)';
}
return {
ticker: h.ticker,
name: h.name || df.name || '',
profit_pct: round2_(profitPct),
profit_preservation_state: state,
rebound_preservation_score: Math.min(100, Math.max(0, Math.round(preserveScore))),
protected_stop_price: priceRow ? priceRow.stop_price : null,
ratchet_partial_qty: priceRow ? priceRow.ratchet_partial_qty : 0,
auto_trailing_stop: autoTrailingStop,
auto_trailing_note: autoTrailingNote,
formula_id: 'PROFIT_PRESERVATION_STATE_V1'
};
}
function calcExecutionQualityRow_(ticker, orderRow, df) {
var amount = orderRow && orderRow.order_amount_krw ? orderRow.order_amount_krw : 0;
var advKrw = 0;
if (typeof df.avgTradeVal5d === 'number') {
// AvgTradeValue_5D_M is usually million KRW in sheet label.
advKrw = df.avgTradeVal5d * 1000000;
}
var status = 'PASS';
var splitCount = 1;
var reasons = [];
if (amount > 0 && advKrw > 0 && amount > advKrw * 0.03) {
status = 'BLOCKED_ADV_3PCT'; reasons.push('order_amount_gt_3pct_adv');
} else if (amount > 0 && advKrw > 0 && amount > advKrw * 0.01) {
status = 'SPLIT_REQUIRED'; splitCount = 2; reasons.push('order_amount_gt_1pct_adv');
}
if (df.spreadStatus && String(df.spreadStatus).indexOf('WIDE') >= 0) {
status = 'BLOCKED_SPREAD'; reasons.push('wide_spread');
}
return {
ticker: ticker,
execution_quality_status: status,
split_count: splitCount,
child_order_amount_krw: splitCount > 1 ? Math.round(amount / splitCount) : amount,
hts_allowed: status === 'PASS',
reason_codes: reasons,
formula_id: 'EXECUTION_QUALITY_GUARD_V1'
};
}
// ── [2026-05-20_HARNESS_V5] H6: 뒷박 차단 — BREAKOUT_QUALITY_GATE_V2 ─────────
function calcBreakoutQualityGate_(h, df, alphaRow, distRow) {
var close = df.close || h.close || 0;
var prevClose = df.prevClose || close;
var ma20 = df.ma20 || 0;
var rsi14 = typeof df.rsi14 === 'number' ? df.rsi14 : null;
var volume = typeof df.volume === 'number' ? df.volume : null;
var avgVol5d = typeof df.avgVolume5d === 'number' ? df.avgVolume5d : null;
var ret1d = (close > 0 && prevClose > 0) ? (close - prevClose) / prevClose * 100 : null;
var ret3d = typeof df.ret5d === 'number' ? df.ret5d * 0.6 : null; // ret5d 프록시
var disparity = (close > 0 && ma20 > 0) ? (close / ma20 - 1) * 100 : null;
var timingScoreExit = alphaRow && typeof alphaRow.timing_score_exit === 'number' ? alphaRow.timing_score_exit : 0;
var distributionRiskScore = distRow && typeof distRow["distribution_risk_score"] === 'number' ? distRow["distribution_risk_score"] : 0;
var lateChaseRiskScore = alphaRow && typeof alphaRow["late_chase_risk_score"] === 'number' ? alphaRow["late_chase_risk_score"] : 0;
var score = 50;
var reasons = [];
if (ret3d !== null && ret3d >= 7) { score -= 30; reasons.push('ret3d_gte7'); }
if (disparity !== null && disparity > 10) { score -= 25; reasons.push('disparity_gt10'); }
if (ret1d !== null && ret1d >= 4 && volume !== null && avgVol5d !== null
&& avgVol5d > 0 && volume < avgVol5d * 0.9) { score -= 40; reasons.push('surge_day_low_vol'); }
if (rsi14 !== null && rsi14 > 75) { score -= 20; reasons.push('rsi14_gt75'); }
if (timingScoreExit >= 50) { score -= 50; reasons.push('timing_exit_gte50'); }
if (distributionRiskScore >= 70) { score -= 35; reasons.push('distribution_gte70'); }
if (lateChaseRiskScore >= 70) { score -= 30; reasons.push('late_chase_gte70'); }
if (volume !== null && avgVol5d !== null && avgVol5d > 0
&& volume >= avgVol5d * 1.5 && ret1d !== null && ret1d >= 2
&& ret3d !== null && ret3d < 5) { score += 25; reasons.push('quality_breakout_vol'); }
if (disparity !== null && disparity >= 0 && disparity < 6) { score += 15; reasons.push('disparity_healthy'); }
if (rsi14 !== null && rsi14 >= 45 && rsi14 <= 65) { score += 10; reasons.push('rsi14_healthy'); }
score = Math.max(0, Math.min(100, Math.round(score)));
var gate = score < 10 ? 'BLOCKED_LATE_CHASE' : score < 40 ? 'WATCH_COOLING_OFF' : 'PILOT_ALLOWED';
return {
ticker: h.ticker,
name: h.name || df.name || '',
breakout_quality_score: score,
breakout_quality_gate: gate,
reason_codes: reasons,
formula_id: 'BREAKOUT_QUALITY_GATE_V2',
version: '2026-05-20_HARNESS_V5'
};
}
// ── [2026-05-20_HARNESS_V5] H7: 가짜 매도 차단 — ANTI_WHIPSAW_HOLD_GATE_V1 ───
function calcAntiWhipsawGate_(h, df, kospiRet5d) {
var inst5d = typeof df.inst5d === 'number' ? df.inst5d : null;
var frg5d = typeof df.frg5d === 'number' ? df.frg5d : null;
var volSurge = typeof df.valSurgePct === 'number' ? df.valSurgePct : null;
var consecutiveSell5d = typeof df.consecutiveSellSignals5d === 'number'
? df.consecutiveSellSignals5d : 0;
var sectorRS5d = null;
if (typeof df.ret5d === 'number' && typeof kospiRet5d === 'number') {
var stockFactor = 1 + df.ret5d / 100;
var kospiFactor = 1 + kospiRet5d / 100;
sectorRS5d = kospiFactor > 0 ? stockFactor / kospiFactor * 100 : null;
}
var score = 0;
var reasons = [];
if (consecutiveSell5d >= 5) { score += 20; reasons.push('consecutive_sell_5d_gte5'); }
if (inst5d !== null && inst5d > 0) { score += 30; reasons.push('inst_net_buy'); }
if (frg5d !== null && frg5d > 0) { score += 20; reasons.push('frg_net_buy'); }
if (sectorRS5d !== null && sectorRS5d > 100) { score += 15; reasons.push('sector_outperforming'); }
if (volSurge !== null && volSurge >= 50) { score -= 25; reasons.push('vol_surge_50pct'); }
if (volSurge !== null && volSurge >= 100) { score -= 20; reasons.push('vol_surge_100pct'); }
score = Math.max(-50, Math.min(100, Math.round(score)));
// [V1.1] 자동 해제 조건 3개 — 충족 수에 따라 hold_days 결정
var wClose = h.close || df.close || 0;
var wMa20 = typeof df.ma20 === 'number' ? df.ma20 : 0;
var clearCnt = 0;
var clearList = [];
if (inst5d !== null && inst5d > 0) { clearCnt++; clearList.push('inst_net_buy'); }
if (frg5d !== null && frg5d > 0) { clearCnt++; clearList.push('frg_net_buy'); }
if (wMa20 > 0 && wClose > 0 && wClose > wMa20) { clearCnt++; clearList.push('price_above_ma20'); }
var gate, holdDays;
if (score >= 30) {
if (clearCnt >= 3) { gate = 'WHIPSAW_AUTO_RELEASED'; holdDays = 0; }
else if (clearCnt >= 2) { gate = 'WHIPSAW_WEAKENING'; holdDays = 1; }
else { gate = 'WHIPSAW_CONFIRMED'; holdDays = 3; }
} else if (score >= 10) {
gate = 'INCONCLUSIVE'; holdDays = 0;
} else {
gate = 'CONFIRMED_SELL'; holdDays = 0;
}
return {
ticker: h.ticker,
name: h.name || df.name || '',
anti_whipsaw_score: score,
anti_whipsaw_gate: gate,
anti_whipsaw_hold_days: holdDays,
clear_conditions_count: clearCnt,
clear_conditions: clearList,
reason_codes: reasons,
formula_id: 'ANTI_WHIPSAW_HOLD_GATE_V1',
version: '2026-05-24_V1.1'
};
}
// ── [2026-05-20_HARNESS_V5] H8: 4경로 결정론적 현금확보 라우터 ─────────────────
function calcSmartCashRaiseV2_(h, df, profitRow, priceRow, cashShortfallInfo) {
var posClass = String(h.positionClass || df.positionClass || '').toUpperCase();
var rsi14 = typeof df.rsi14 === 'number' ? df.rsi14 : 50;
var profitStage = priceRow && priceRow.profit_lock_stage
? String(priceRow.profit_lock_stage)
: (profitRow ? String(profitRow.profit_preservation_state || 'NORMAL') : 'NORMAL');
var secularPass = priceRow && priceRow.secular_leader_gate_active === false; // PASS = not active restriction
var emergencyFull = !!(cashShortfallInfo && cashShortfallInfo.emergency_full_sell);
var stopPrice = priceRow && typeof priceRow.stop_price === 'number' ? priceRow.stop_price : 0;
var close = df.close || h.close || 0;
var breachImmediate = stopPrice > 0 && close > 0 && close < stopPrice;
var stopBreachGate = breachImmediate ? 'BREACH' : 'PASS';
var route, routeLabel, rationale;
if (emergencyFull || breachImmediate) {
route = 'ROUTE_D';
routeLabel = '긴급 전량매도';
rationale = emergencyFull ? 'emergency_full_sell=true' : 'close<stop_price(stop_breach_gate=BREACH)';
} else if (posClass.indexOf('SATELLITE') >= 0 && rsi14 >= 35) {
route = 'ROUTE_A';
routeLabel = '위성 비중 트림';
rationale = 'SATELLITE+RSI14(' + rsi14 + ')>=35';
} else if (rsi14 < 35) {
route = 'ROUTE_B';
routeLabel = '과매도 분할 매도';
rationale = 'RSI14(' + rsi14 + ')<35→K2_50/50';
} else if (posClass.indexOf('CORE') >= 0
&& (profitStage === 'PROFIT_LOCK_STAGE_20'
|| profitStage === 'PROFIT_LOCK_STAGE_30'
|| profitStage === 'PROFIT_LOCK_20'
|| profitStage === 'PROFIT_LOCK_30')
&& secularPass) {
route = 'ROUTE_C';
routeLabel = '코어 익절 잠금';
rationale = 'CORE+' + profitStage + '+secular_PASS';
} else {
route = 'NO_ACTION';
routeLabel = '현금확보 비대상';
rationale = 'no_condition_met';
}
return {
ticker: h.ticker,
name: h.name || df.name || '',
smart_cash_raise_route: route,
route_label: routeLabel,
rationale: rationale,
profit_lock_stage: profitStage,
stop_breach_gate: stopBreachGate,
emergency_full_sell: emergencyFull,
rebound_wait_pct: route === 'ROUTE_B' ? 50 : 0,
formula_id: 'SMART_CASH_RAISE_V2',
version: '2026-05-20_HARNESS_V5'
};
}
// ── [2026-05-20_HARNESS_V5] Gate 4b: O'Neil Follow-Through Day — FOLLOW_THROUGH_DAY_CONFIRM_V1
// 돌파 당일(Day 0)에 즉시 매수 금지. Day 2~7 사이에 수익률+거래량 조건 충족 시만 BUY_PILOT_ALLOWED.
// daysSinceBreakout / retSinceBreakout / volumeBreakoutDay 이 df에 없으면 프록시 계산으로 후퇴.
function calcFollowThroughDayConfirm_(h, df) {
var ticker = h.ticker;
var name = h.name || df.name || '';
// ── 입력 수집 (실제 필드 우선, 프록시 fallback) ──────────────────────────
var daysSince = typeof df.daysSinceBreakout === 'number' ? df.daysSinceBreakout : null;
var retSince = typeof df.retSinceBreakout === 'number' ? df.retSinceBreakout : null;
var volToday = typeof df.volume === 'number' ? df.volume : null;
var volBreakout = typeof df.volumeBreakoutDay === 'number' ? df.volumeBreakoutDay : null;
// 프록시: daysSinceBreakout — close vs MA20 돌파여부로 추정
// MA20 이하에서 위로 올라온 직후이면 daysSince=0, 그 이전이면 null
if (daysSince === null) {
var close = df.close || h.close || 0;
var ma20 = df.ma20 || 0;
var prevClose = df.prevClose || close;
// 오늘 ma20 상향 돌파면 Day 0
if (close > 0 && ma20 > 0 && close > ma20 && prevClose <= ma20) {
daysSince = 0;
}
// 이미 ma20 위에 있고 ret5d 존재 → days를 ret5d로 추정(보수적 5일 상한)
else if (close > 0 && ma20 > 0 && close > ma20 && typeof df.ret5d === 'number') {
// 5일 기준 프록시: 상승률이 클수록 이미 많이 경과했다고 가정
daysSince = df.ret5d >= 7 ? 8 : df.ret5d >= 3 ? 4 : 2;
}
}
// 프록시: retSinceBreakout — ret5d 사용
if (retSince === null && typeof df.ret5d === 'number') {
retSince = df.ret5d;
}
// 프록시: volBreakoutDay — avgVolume5d 사용
if (volBreakout === null && typeof df.avgVolume5d === 'number') {
volBreakout = df.avgVolume5d;
}
// ── 상태 분류 ──────────────────────────────────────────────────────────────
var state, result, reasons = [];
if (daysSince === null) {
state = 'PENDING_DATA';
result = 'WATCH_NO_BREAKOUT_TRACKED';
reasons.push('days_since_breakout_null');
} else if (daysSince === 0) {
state = 'BREAKOUT_DAY_1';
result = 'WATCH_FOLLOW_THROUGH_PENDING';
reasons.push('day0_no_immediate_buy');
} else if (daysSince > 7) {
state = 'EXTENDED_FOLLOW';
result = 'WATCH_TOO_LATE';
reasons.push('days_since_gt7');
} else {
// daysSince 2~7 범위
var volOk = (volToday !== null && volBreakout !== null && volBreakout > 0)
? (volToday >= volBreakout * 0.9) : true; // 데이터 없으면 통과
var retOk = (retSince !== null) ? (retSince >= 1.5) : false;
if (retOk && volOk) {
state = 'FOLLOW_THROUGH_OK';
result = 'BUY_PILOT_ALLOWED';
reasons.push('days_' + daysSince + '_ret_' + (retSince !== null ? retSince.toFixed(1) : 'N/A'));
if (volOk) reasons.push('vol_confirmed');
} else {
state = 'FOLLOW_THROUGH_FAIL';
result = 'WATCH_RESET_REQUIRED';
if (!retOk) reasons.push('ret_since_lt1.5pct');
if (!volOk) reasons.push('vol_lt90pct_breakout_day');
}
}
return {
ticker: ticker,
name: name,
days_since_breakout: daysSince,
ret_since_breakout: retSince,
vol_ratio_vs_breakout_day: (volToday !== null && volBreakout !== null && volBreakout > 0)
? Math.round(volToday / volBreakout * 100) / 100 : null,
follow_through_state: state,
follow_through_result: result,
reason_codes: reasons,
formula_id: 'FOLLOW_THROUGH_DAY_CONFIRM_V1',
version: '2026-05-20_HARNESS_V5'
};
}
function calcApexExecutionHarness_(holdings, dfMap, sectorFlowData, kospiRet5d, h1, h2, h3, h4, orderBlueprint, cashShortfallInfo, marketRegime) {
var alphaLead = [];
var followThrough = [];
var distribution = [];
var profitPreservation = [];
var entryFreshness = [];
var cashRaisePlan = [];
var reboundTriggers = [];
var smartSellQty = [];
var sellValuePreservation = [];
var executionQuality = [];
var buyPermission = [];
var limitPolicy = [];
var benchmarkRelativeRows = [];
var indexRelativeHealthRows = [];
var saqgRows = [];
var cashCreationLockRows = [];
// ── [2026-05-20_HARNESS_V5] 신규 V5 게이트 결과 배열
var breakoutQualityGate = [];
var antiWhipsawGate = [];
var smartCashRaiseV2 = [];
var followThroughConfirm = [];
var blockCount = 0;
var regime = marketRegime || 'UNKNOWN';
var priceMap = {};
(h4.prices || []).forEach(function(p) { priceMap[p.ticker] = p; });
var sellQtyMap = {};
(h3.sellQty || []).forEach(function(s) { sellQtyMap[s.ticker] = s; });
holdings.forEach(function(h) {
var df = dfMap[h.ticker] || {};
var distRow = calcDistributionRiskRow_(h, df, kospiRet5d, sectorFlowData);
// [PROPOSAL50] P1-B: DSD V1.1 — SIG_7/SIG_8 추가, weighted_sum 5.0/3.0 상향
applyDsdV1_1Signals_([distRow], dfMap);
var alphaRow = calcAlphaLeadRow_(h, df, sectorFlowData, distRow);
var ftRow = calcFollowThroughRow_(h, df);
var priceRow = priceMap[h.ticker] || {};
var profitRow = calcProfitPreservationRow_(h, df, priceRow, distRow);
var orderRow = findOrderBlueprintRow_(orderBlueprint, h.ticker) || {};
var eqRow = calcExecutionQualityRow_(h.ticker, orderRow, df);
var saqgState = df.saqg_v1 || (h.position_type === 'core' ? 'EXEMPT' : 'WATCHLIST_ONLY');
var cand = findCandidateByTicker_(h2.candidates, h.ticker) || {};
var sq = sellQtyMap[h.ticker] || {};
var tradePlan = calcApexTradePlan_(
h, df, h1, alphaRow, ftRow, distRow, priceRow, orderRow, sq, profitRow, cashShortfallInfo, saqgState
);
var buyState = tradePlan.buyState;
var buyReasons = tradePlan.buyReasons;
if (buyState === 'BLOCKED') blockCount++;
var style = tradePlan.style;
var immediateQty = tradePlan.immediateQty;
var reboundQty = tradePlan.reboundQty;
var k2Emergency = tradePlan.k2Emergency;
var tranchePhase = tradePlan.tranchePhase;
var currentTrancheAllowedPct = tradePlan.currentTrancheAllowedPct;
var nextTrancheCondition = tradePlan.nextTrancheCondition;
var normalizedSellPrice = tradePlan.normalizedSellPrice;
var normalizedBuyPrice = tradePlan.normalizedBuyPrice;
var htsLimitPrice = tradePlan.htsLimitPrice;
var close = h.close || df.close || 0;
var atr20 = df.atr20 || 0;
var holdingQty = h.holdingQty || 0;
var prevClose = df.prevClose || close;
// ── [2026-05-20_HARNESS_V5] V5 게이트 산출 ──────────────────────────────
var bqRow = calcBreakoutQualityGate_(h, df, alphaRow, distRow);
var awRow = calcAntiWhipsawGate_(h, df, kospiRet5d);
var scrV2 = calcSmartCashRaiseV2_(h, df, profitRow, priceRow, cashShortfallInfo);
var ftdRow = calcFollowThroughDayConfirm_(h, df);
// H6: 뒷박 차단 — BUY 상태 override
if (bqRow.breakout_quality_gate === 'BLOCKED_LATE_CHASE') {
if (buyState !== 'BLOCKED') { buyState = 'BLOCKED'; }
buyReasons.push('breakout_quality_BLOCKED_LATE_CHASE');
blockCount++;
}
// Gate 4b: FTD 미확인 — BUY 차단 (돌파 당일 즉시 매수 금지, 데이터 부재 시 WATCH로 후퇴)
if (ftdRow.follow_through_result === 'WATCH_FOLLOW_THROUGH_PENDING'
|| ftdRow.follow_through_result === 'WATCH_RESET_REQUIRED') {
if (buyState === 'ALLOW_PILOT') {
buyState = 'WATCH'; // PILOT → WATCH (BLOCKED 아님 — 관찰 유지)
buyReasons.push('ftd_' + ftdRow.follow_through_result);
}
} else if (ftdRow.follow_through_result === 'WATCH_TOO_LATE') {
if (buyState === 'ALLOW_PILOT') {
buyState = 'WATCH';
buyReasons.push('ftd_WATCH_TOO_LATE');
}
}
// H7: 가짜 매도 차단 — V1.1: CONFIRMED/WEAKENING만 보류 표기 (AUTO_RELEASED 제외)
if (awRow.anti_whipsaw_gate === 'WHIPSAW_CONFIRMED' || awRow.anti_whipsaw_gate === 'WHIPSAW_WEAKENING') {
buyReasons.push('whipsaw_hold_' + (awRow.anti_whipsaw_hold_days || 1) + 'd');
}
distribution.push(distRow);
alphaLead.push(alphaRow);
followThrough.push(ftRow);
profitPreservation.push(profitRow);
benchmarkRelativeRows.push({
ticker: h.ticker,
name: h.name || df.name || '',
stock_drawdown_from_high_pct: typeof df.stock_drawdown_from_high_pct === 'number' ? df.stock_drawdown_from_high_pct : null,
excess_drawdown_pctp: typeof df.excess_drawdown_pctp === 'number' ? df.excess_drawdown_pctp : null,
recovery_ratio_5d: typeof df.recovery_ratio_5d === 'number' ? df.recovery_ratio_5d : null,
recovery_ratio_20d: typeof df.recovery_ratio_20d === 'number' ? df.recovery_ratio_20d : null,
downside_beta: typeof df.downside_beta === 'number' ? df.downside_beta : null,
rs_line_20d_slope: typeof df.rs_line_20d_slope === 'number' ? df.rs_line_20d_slope : null,
rs_line_60d_slope: typeof df.rs_line_60d_slope === 'number' ? df.rs_line_60d_slope : null,
brt_verdict: df.brt_verdict || 'UNKNOWN',
brt_method: df.brt_method || 'DATA_MISSING',
formula_id: 'BENCHMARK_RELATIVE_TIMESERIES_V1'
});
var indexRelRow = calcIndexRelativeHealthGate_(h, df, kospiRet5d);
indexRelativeHealthRows.push(indexRelRow);
saqgRows.push({
ticker: h.ticker,
name: h.name || df.name || '',
position_type: h.position_type || 'unknown',
saqg_v1: saqgState,
saqg_penalty: typeof df.saqg_penalty === 'number' ? df.saqg_penalty : null,
saqg_failed_filters: df.saqg_failed_filters || '',
hts_allowed: saqgState === 'ELIGIBLE' || saqgState === 'EXEMPT',
formula_id: 'SATELLITE_ALPHA_QUALITY_GATE_V1'
});
breakoutQualityGate.push(bqRow);
antiWhipsawGate.push(awRow);
smartCashRaiseV2.push(scrV2);
followThroughConfirm.push(ftdRow);
executionQuality.push(eqRow);
// ── 진입 신선도 게이트 (ENTRY_FRESHNESS_GATE_V1) ───────────────────────
var freshnessState = 'FRESH_PILOT';
var freshnessReasons = [];
if (bqRow.breakout_quality_gate === 'BLOCKED_LATE_CHASE' || alphaRow["late_chase_risk_score"] >= 70) {
freshnessState = 'BLOCK_LATE_CHASE';
freshnessReasons.push('late_chase');
} else if (ftRow.follow_through_state === 'WAIT_PULLBACK' || ftdRow.follow_through_result === 'WATCH_TOO_LATE' || ftdRow.follow_through_result === 'WATCH_RESET_REQUIRED') {
freshnessState = 'PULLBACK_WAIT';
freshnessReasons.push('follow_through_wait');
} else if (distRow.pre_distribution_warning === 'EARLY_WARNING') {
freshnessState = 'STALE_REVIEW';
freshnessReasons.push('pre_distribution_warning');
} else if (buyState === 'WATCH' || buyState === 'BLOCKED') {
freshnessState = 'WATCH_FRESHNESS';
freshnessReasons.push('buy_state_' + buyState.toLowerCase());
}
if (indexRelRow.relative_health_state === 'DECOUPLED' || indexRelRow.relative_health_state === 'OVER_EXTENDED') {
freshnessState = freshnessState === 'FRESH_PILOT' ? 'WATCH_FRESHNESS' : freshnessState;
freshnessReasons.push('index_relative_' + String(indexRelRow.relative_health_state).toLowerCase());
if (buyState === 'ALLOW_PILOT' || buyState === 'ALLOW_ADD_ON') {
buyState = 'WATCH';
buyReasons.push('index_relative_' + String(indexRelRow.relative_health_state).toLowerCase());
}
} else if (indexRelRow.relative_health_state === 'UNDERPERFORMING') {
if (buyState === 'ALLOW_PILOT' || buyState === 'ALLOW_ADD_ON') {
buyState = 'WATCH';
}
freshnessReasons.push('index_relative_underperforming');
}
entryFreshness.push({
ticker: h.ticker,
name: h.name || df.name || '',
alpha_lead_score: alphaRow.alpha_lead_score != null ? alphaRow.alpha_lead_score : null,
["late_chase_risk_score"]: alphaRow["late_chase_risk_score"] != null ? alphaRow["late_chase_risk_score"] : null,
follow_through_state: ftRow.follow_through_state || null,
breakout_quality_gate: bqRow.breakout_quality_gate || null,
pre_distribution_warning: distRow.pre_distribution_warning || 'NONE',
t20_alpha_gate: null,
freshness_state: freshnessState,
reason_codes: freshnessReasons,
formula_id: 'ENTRY_FRESHNESS_GATE_V1'
});
// ── 회복 보존 매도 게이트 (SELL_VALUE_PRESERVATION_GATE_V1) ─────────────
var sellPreserveState = 'HOLD';
var sellPreserveReasons = [];
if (scrV2.smart_cash_raise_route === 'ROUTE_D' || k2Emergency || scrV2.stop_breach_gate === 'BREACH') {
sellPreserveState = 'EMERGENCY_EXIT';
sellPreserveReasons.push('route_d_or_breach');
} else if (awRow.anti_whipsaw_gate === 'WHIPSAW_CONFIRMED' || awRow.anti_whipsaw_gate === 'WHIPSAW_WEAKENING') {
sellPreserveState = 'REBOUND_CONFIRM_HOLD';
sellPreserveReasons.push('whipsaw_hold_' + (awRow.anti_whipsaw_hold_days || 1) + 'd');
} else if (style === 'OVERSOLD_REBOUND_SELL' && reboundQty > 0) {
sellPreserveState = 'STAGED_REBOUND';
sellPreserveReasons.push('rebound_wait_qty');
} else if (profitRow.profit_preservation_state === 'PROFIT_LOCK_10'
|| profitRow.profit_preservation_state === 'PROFIT_LOCK_20'
|| profitRow.profit_preservation_state === 'PROFIT_LOCK_30'
|| profitRow.profit_preservation_state === 'APEX_TRAILING') {
sellPreserveState = 'PRESERVE_TIERED';
sellPreserveReasons.push('profit_lock');
} else if (distRow.anti_distribution_state === 'BLOCK_BUY') {
sellPreserveState = 'TRIM_ONLY';
sellPreserveReasons.push('distribution_exit');
} else if (indexRelRow.relative_health_state === 'OVER_EXTENDED' || indexRelRow.relative_health_state === 'DECOUPLED') {
if (style !== 'OVERSOLD_REBOUND_SELL') {
sellPreserveState = 'TRIM_ONLY';
}
sellPreserveReasons.push('index_relative_' + String(indexRelRow.relative_health_state).toLowerCase());
}
sellValuePreservation.push({
ticker: h.ticker,
name: h.name || df.name || '',
profit_preservation_state: profitRow.profit_preservation_state || 'NORMAL',
cash_raise_group: style,
anti_whipsaw_gate: awRow.anti_whipsaw_gate || null,
immediate_qty: immediateQty > 0 ? immediateQty : null,
rebound_wait_qty: reboundQty > 0 ? reboundQty : null,
auto_trailing_stop: profitRow.auto_trailing_stop || null,
sell_value_preservation_state: sellPreserveState,
reason_codes: sellPreserveReasons,
formula_id: 'SELL_VALUE_PRESERVATION_GATE_V1'
});
// K1: 트랜치 엔진 결과 포함 buy_permission_json
buyPermission.push({
ticker: h.ticker,
name: h.name || df.name || '',
buy_permission_state: buyState,
max_tranche_pct: buyState === 'ALLOW_PILOT' ? 30 : buyState === 'ALLOW_ADD_ON' ? 60 : 0,
tranche_phase: tranchePhase,
current_tranche_allowed_pct: currentTrancheAllowedPct,
next_tranche_condition: nextTrancheCondition,
blocked_reason_codes: buyReasons,
position_type: h.position_type || 'unknown',
brt_verdict: df.brt_verdict || null,
saqg_v1: saqgState,
rs_verdict: df.rs_verdict || null,
composite_verdict: df.composite_verdict || null,
rag_v1: df.rag_v1 || null,
formula_id: 'BUY_PERMISSION_MATRIX_V1+STAGED_ENTRY_TRANCHE_V1'
});
// K2: 반등 대기 분할 매도 결과 포함 cash_raise_plan_json
cashRaisePlan.push({
ticker: h.ticker,
name: h.name || df.name || '',
rank: cand.rank || null,
execution_style: style,
immediate_qty: immediateQty > 0 ? immediateQty : null,
rebound_wait_qty: reboundQty > 0 ? reboundQty : null,
emergency_full_sell: k2Emergency,
max_daily_qty: Math.floor(holdingQty * 0.50),
expected_immediate_krw: immediateQty > 0 ? Math.round(immediateQty * close) : 0,
cash_shortfall_min_krw: (cashShortfallInfo && cashShortfallInfo.cash_shortfall_min_krw) || 0,
formula_id: 'SMART_CASH_RAISE_PLAN_V1+K2_STAGED_REBOUND_SELL'
});
// K2: 반등 트리거 조건부 잔여 수량
var reboundTriggerPrice = null;
if (style === 'OVERSOLD_REBOUND_SELL' && reboundQty > 0) {
// 반등 트리거: prevClose + 0.5×ATR 또는 단순 close + 0.3×ATR
reboundTriggerPrice = atr20 > 0
? tickNormalize_((prevClose > 0 ? prevClose : close) + atr20 * 0.5)
: null;
}
reboundTriggers.push({
ticker: h.ticker,
rebound_trigger_state: (style === 'OVERSOLD_REBOUND_SELL' && reboundQty > 0)
? 'WAIT_REBOUND_TRIGGER' : 'NOT_APPLICABLE',
trigger_price: reboundTriggerPrice,
rebound_sell_qty: reboundQty > 0 ? reboundQty : null,
emergency_override: k2Emergency,
formula_id: 'REBOUND_SELL_TRIGGER_V1'
});
smartSellQty.push({
ticker: h.ticker,
immediate_sell_qty: immediateQty > 0 ? immediateQty : null,
staged_total_qty: (typeof sq.sell_qty === 'number' && sq.sell_qty > 0) ? sq.sell_qty : null,
rebound_wait_qty: reboundQty > 0 ? reboundQty : null,
emergency_full_sell: k2Emergency,
expected_cash_recovered_krw: immediateQty > 0 ? Math.round(immediateQty * close) : 0,
formula_id: 'SELL_QUANTITY_ALLOCATOR_V1+K2_STAGED_REBOUND_SELL'
});
// J5: 스타일별 실제 지정가 산출 결과 포함 limit_price_policy_json
limitPolicy.push({
ticker: h.ticker,
execution_style: style,
sell_limit_price: normalizedSellPrice,
buy_limit_price: normalizedBuyPrice,
hts_limit_price: htsLimitPrice,
tick_status: htsLimitPrice ? 'TICK_OK' : 'NO_EXECUTION_PRICE',
sell_price_basis: style === 'URGENT_LIQUIDITY_TRIM' ? 'min(close,prevClose×0.998)'
: style === 'OVERSOLD_REBOUND_SELL' ? 'close_no_undercut'
: style === 'DISTRIBUTION_EXIT' ? 'close-0.25×ATR20'
: style === 'PROFIT_PROTECT_TRIM' ? 'ratchet_stop_or_close×0.999'
: 'close',
formula_id: 'LIMIT_PRICE_POLICY_V1'
});
});
// K3: 국면·섹터 연계 H2 동적 우선순위
var regimeAdjPriority = calcRegimeAdjustedSellPriority_(
h2.candidates, regime, dfMap, kospiRet5d
);
// ── [2026-05-21_CLA_HARNESS_V1] SATELLITE_FAILURE_GATE_V1 ────────────────────
var satelliteRowsForSFG = [];
holdings.forEach(function(h) {
if (h.position_type !== 'core') {
var df = dfMap[h.ticker] || {};
satelliteRowsForSFG.push({
composite_verdict: df.composite_verdict || null,
rs_verdict: df.rs_verdict || null,
ret20d: typeof df.ret20d === 'number' ? df.ret20d : null,
excess_ret_10d: typeof df.excess_ret_10d === 'number' ? df.excess_ret_10d : null
});
}
});
var sfgResult = calcSatelliteFailureGate_(satelliteRowsForSFG);
var sapgResult = calcSatelliteAggregatePnlGate_(holdings);
holdings.forEach(function(h) {
var df = dfMap[h.ticker] || {};
cashCreationLockRows.push(calcCashCreationPurposeLockRow_(h, df, sfgResult));
});
// ── [2026-05-21_AEW_V1] ALPHA_EVALUATION_WINDOW_V1 ──────────────────────────
var aewRows = calcAlphaEvaluationWindow_(holdings, dfMap);
// SFG-1: TRIGGERED 시 위성 BUY 전면 차단 (post-processing)
if (sfgResult.sfg_v1 === 'TRIGGERED' || sapgResult.sapg_status === 'SAPG_CRITICAL') {
buyPermission.forEach(function(bp) {
var h = holdings.find(function(x) { return x.ticker === bp.ticker; });
if (h && h.position_type !== 'core') {
if (bp.buy_permission_state !== 'BLOCKED') {
bp.buy_permission_state = 'BLOCKED';
bp.blocked_reason_codes = (bp.blocked_reason_codes || []).concat([
sfgResult.sfg_v1 === 'TRIGGERED' ? 'sfg_v1_TRIGGERED' : 'sapg_CRITICAL'
]);
}
}
});
}
// ── [QEH010] WHIPSAW V1.1 → order_blueprint validation_status 소급 차단 ──
// V1.1: WHIPSAW_CONFIRMED(hold_3d) + WHIPSAW_WEAKENING(hold_1d) 차단
// WHIPSAW_AUTO_RELEASED(hold_0d)은 자동 해제 — 차단 안 함
var whipsawTickers_ = {};
antiWhipsawGate.forEach(function(aw) {
if (aw.anti_whipsaw_gate === 'WHIPSAW_CONFIRMED' || aw.anti_whipsaw_gate === 'WHIPSAW_WEAKENING') {
whipsawTickers_[aw.ticker] = aw.anti_whipsaw_hold_days || 1;
}
});
var SELL_ORDER_TYPES_ = { SELL: 1, TRIM: 1, EXIT_100: 1, EXIT_FULL: 1 };
orderBlueprint.forEach(function(bp) {
var wHoldDays = whipsawTickers_[bp.ticker];
if (wHoldDays
&& SELL_ORDER_TYPES_[bp.order_type]
&& bp.validation_status === 'PASS') {
bp.validation_status = 'BLOCKED';
bp.rationale_code = 'WHIPSAW_V1_1:hold_' + wHoldDays + 'd';
}
});
// ── [2026-05-20_HARNESS_V5] V5 포트폴리오 레벨 집계
var smartCashRaiseRoute = 'NO_ACTION';
for (var sci = 0; sci < smartCashRaiseV2.length; sci++) {
if (smartCashRaiseV2[sci].smart_cash_raise_route !== 'NO_ACTION') {
smartCashRaiseRoute = smartCashRaiseV2[sci].smart_cash_raise_route;
break; // 첫 번째 실제 경로를 포트폴리오 레벨 대표 경로로 설정
}
}
return {
alpha_lead_json: alphaLead,
follow_through_json: followThrough,
distribution_risk_json: distribution,
profit_preservation_json: profitPreservation,
entry_freshness_json: entryFreshness,
cash_raise_plan_json: cashRaisePlan,
rebound_sell_trigger_json: reboundTriggers,
smart_sell_quantities_json: smartSellQty,
sell_value_preservation_json: sellValuePreservation,
execution_quality_json: executionQuality,
buy_permission_json: buyPermission,
limit_price_policy_json: limitPolicy,
regime_adjusted_sell_priority_json: regimeAdjPriority,
benchmark_relative_timeseries_json: benchmarkRelativeRows,
index_relative_health_json: indexRelativeHealthRows,
saqg_json: saqgRows,
cash_creation_purpose_lock_json: cashCreationLockRows,
// ── [2026-05-20_HARNESS_V5] 신규 V5 출력 ──────────────────────────────
breakout_quality_gate_json: breakoutQualityGate,
anti_whipsaw_gate_json: antiWhipsawGate,
smart_cash_raise_json: smartCashRaiseV2,
smart_cash_raise_route: smartCashRaiseRoute,
follow_through_confirm_json: followThroughConfirm,
breakout_quality_gate_lock: true,
anti_whipsaw_gate_lock: true,
follow_through_lock: true,
follow_through_confirm_lock: true,
apex_block_count: blockCount,
// ── [2026-05-21_CLA_HARNESS_V1] 신규 하네스 출력 ──────────────────────────
satellite_failure_gate_json: sfgResult,
sapg_json: sapgResult,
// ── [2026-05-21_AEW_V1] ─────────────────────────────────────────────────────
alpha_evaluation_window_json: aewRows,
sfg_v1_lock: true
};
}
// ═══════════════════════════════════════════════════════════════════════════════
// [2026-05-23_PROPOSAL46] PA1~PA5 신규 하네스 calc 함수
// spec/13b_harness_formulas.yaml: PA1 PREDICTIVE_ALPHA_ENGINE_V1
// PA2 ANTI_LATE_ENTRY_GATE_V2
// PA3 CASH_PRESERVATION_SELL_ENGINE_V2
// PA4 MACRO_EVENT_SYNCHRONIZER_V1
// PA5 CONSISTENCY_VALIDATOR_V2
// ═══════════════════════════════════════════════════════════════════════════════
/**
* [PROPOSAL47_B6 / PROPOSAL48_B6_FALLBACK] prediction_accuracy_rate 읽기.
* 우선순위: ① monthly_history.prediction_accuracy_rate
* ② settings.prediction_accuracy_rate
* ③ 상수 기본값 48.48 (운영 중 실측값으로 교체 예정)
* 값이 0~1 범위면 *100 변환, 0~100 범위면 그대로 사용.
*/
var PREDICTION_ACCURACY_RATE_DEFAULT_ = 48.48; // 2026-05-23 실측, 매월 갱신
function getPredictionAccuracyRate_() {
function parseAccuracy_(val) {
if (val === '' || val === null || val === undefined) return null;
var num = typeof val === 'number' ? val : parseFloat(String(val));
if (isNaN(num)) return null;
return num <= 1 ? Math.round(num * 1000) / 10 : num;
}
try {
var ss = getSpreadsheet_();
// ① monthly_history 시트
var sh = ss.getSheetByName('monthly_history');
if (sh) {
var mhData = sh.getDataRange().getValues();
if (mhData && mhData.length >= 2) {
var header = mhData[0] || [];
var colIdx = -1;
for (var i = 0; i < header.length; i++) {
if (String(header[i]).trim().toLowerCase() === 'prediction_accuracy_rate') {
colIdx = i; break;
}
}
if (colIdx >= 0) {
for (var r = mhData.length - 1; r >= 1; r--) {
var parsed = parseAccuracy_(mhData[r][colIdx]);
if (parsed !== null) return parsed;
}
}
}
}
// ② settings 시트 (Key-Value 구조)
var settingsSh = ss.getSheetByName('settings');
if (settingsSh) {
var sData = settingsSh.getDataRange().getValues();
for (var si = 0; si < sData.length; si++) {
var key = String(sData[si][0] || '').trim().toLowerCase();
if (key === 'prediction_accuracy_rate') {
var parsed2 = parseAccuracy_(sData[si][1]);
if (parsed2 !== null) return parsed2;
}
}
}
} catch(e) { /* fallback to default */ }
// ③ 상수 기본값
return PREDICTION_ACCURACY_RATE_DEFAULT_;
}
/**
* [PA1 V1.2] 팩터 가중치 오버라이드 읽
* settings 시트의 pa1_w_<factor> 키-값을 읽어 기본값과 병합.
* 오버라이드가 존재하면 _source='DYNAMIC', 없으면 'STATIC'.
*/
function getPa1WeightOverrides_() {
var defaults = {
pullback_entry: 20, flow_strong: 20, rs_leader: 15,
volume_confirm: 15, rsi_healthy: 15, brt_leader: 15,
chase_risk: 25, distribution: 20, rsi_overbought: 20,
foreign_sell: 15, usd_krw_weak: 10, stale_position: 10,
_source: 'STATIC'
};
try {
var ss = getSpreadsheet_();
var sh = ss.getSheetByName('settings');
if (!sh) return defaults;
var data = sh.getDataRange().getValues();
var overrides = {};
for (var i = 0; i < data.length; i++) {
var key = String(data[i][0] || '').trim();
if (key.indexOf('pa1_w_') !== 0) continue;
var factorName = key.slice(6); // 'pa1_w_' = 6자
var val = parseFloat(String(data[i][1] || ''));
if (!isNaN(val) && val >= 0 && val <= 50) overrides[factorName] = val;
}
if (Object.keys(overrides).length === 0) return defaults;
var merged = {};
for (var k in defaults) merged[k] = defaults[k];
for (var k in overrides) merged[k] = overrides[k];
merged._source = 'DYNAMIC';
return merged;
} catch(e) {
return defaults;
}
}
/**
* [PA1 V1.3] T+5 피드백 기록
* STRONG_BUY_SIGNAL / EXIT_SIGNAL / TRIM_SIGNAL 예측 → pa1_feedback 시트 기록.
* V1.3: TRIM_SIGNAL 추가, signal_type 컬럼 추가 (BUY/SELL 분리 정확도 추적)
* evaluatePa1FeedbackBatch_() 주간 배치에서 결과를 평가.
*/
function recordPa1FeedbackEntry_(paeRows, dfMap) {
if (!paeRows || !paeRows.length) return;
// [V1.3] TRIM_SIGNAL 추가
var RECORD_VERDICTS = { STRONG_BUY_SIGNAL: 1, EXIT_SIGNAL: 1, TRIM_SIGNAL: 1 };
var toRecord = paeRows.filter(function(pa) { return !!RECORD_VERDICTS[pa.synthesis_verdict]; });
if (!toRecord.length) return;
try {
var ss = getSpreadsheet_();
var sh = ss.getSheetByName('pa1_feedback');
if (!sh) {
sh = ss.insertSheet('pa1_feedback');
sh.appendRow(['date','ticker','synthesis_verdict','direction_confidence',
'close_at_record','signal_type','t5_evaluated','t5_return_pct','t5_correct']);
} else {
// [V1.3] signal_type 컬럼 없으면 헤더 확인 — 없어도 appendRow는 동작함
}
var today = Utilities.formatDate(new Date(), 'Asia/Seoul', 'yyyy-MM-dd');
toRecord.forEach(function(pa) {
var df = dfMap[pa.ticker] || {};
var closeNow = df.close || 0;
var signalType = (pa.synthesis_verdict === 'STRONG_BUY_SIGNAL') ? 'BUY' : 'SELL';
sh.appendRow([today, pa.ticker, pa.synthesis_verdict,
pa.direction_confidence, closeNow, signalType, false, '', '']);
});
} catch(e) {
Logger.log('[PA1_FEEDBACK] recordPa1FeedbackEntry_ error: ' + e.message);
}
}
/**
* [PA1 V1.3] 매도 PASS 정확도 조회
* pa1_feedback 시트에서 signal_type=SELL + t5_evaluated=true 행의 정확도 산출.
* @return {number|null} sell_pass_accuracy_rate (0~100) or null if insufficient data
*/
function getSellPassAccuracyRate_() {
try {
var ss = getSpreadsheet_();
var fbSh = ss.getSheetByName('pa1_feedback');
if (!fbSh) return null;
var data = fbSh.getDataRange().getValues();
if (data.length < 2) return null;
var header = data[0];
var COL = {};
header.forEach(function(h, i) { COL[String(h)] = i; });
if (COL['signal_type'] == null || COL['t5_evaluated'] == null || COL['t5_correct'] == null) return null;
var sellRows = data.slice(1).filter(function(row) {
return String(row[COL['signal_type']] || '').toUpperCase() === 'SELL'
&& (row[COL['t5_evaluated']] === true || String(row[COL['t5_evaluated']]).toUpperCase() === 'TRUE');
});
if (sellRows.length < 5) return null;
var correct = sellRows.filter(function(row) {
return row[COL['t5_correct']] === true || String(row[COL['t5_correct']]).toUpperCase() === 'TRUE';
}).length;
return Math.round(correct / sellRows.length * 1000) / 10;
} catch(e) {
Logger.log('[PA1_V1.3] getSellPassAccuracyRate_ error: ' + e.message);
return null;
}
}
/**
* [PA1 V1.2] 주간 배치 — T+5(7캘린더일) 결과 평가 + prediction_accuracy_rate 갱신
* GAS 트리거에 주 1회 등록해 사용 (매주 월요일 권장).
*/
function evaluatePa1FeedbackBatch_() {
try {
var ss = getSpreadsheet_();
var fbSh = ss.getSheetByName('pa1_feedback');
if (!fbSh) { Logger.log('[PA1_V1.2] pa1_feedback 시트 없음'); return; }
var data = fbSh.getDataRange().getValues();
if (data.length < 2) return;
var header = data[0];
var COL = {};
header.forEach(function(h, i) { COL[String(h)] = i; });
var reqCols = ['date','ticker','synthesis_verdict','close_at_record','t5_evaluated','t5_return_pct','t5_correct'];
for (var ci = 0; ci < reqCols.length; ci++) {
if (COL[reqCols[ci]] == null) { Logger.log('[PA1_V1.2] 컬럼 누락: ' + reqCols[ci]); return; }
}
// 현재 종가 맵 (data_feed 시트)
var priceMap = {};
var dfSheet = ss.getSheetByName('data_feed');
if (dfSheet) {
var dfData = dfSheet.getDataRange().getValues();
if (dfData.length > 1) {
var dfHeader = dfData[0];
var tCol = dfHeader.indexOf('Ticker');
var cCol = dfHeader.indexOf('Close');
if (tCol >= 0 && cCol >= 0) {
for (var ri = 1; ri < dfData.length; ri++) {
var t = String(dfData[ri][tCol] || '').trim();
var c = parseFloat(String(dfData[ri][cCol] || ''));
if (t && !isNaN(c) && c > 0) priceMap[t] = c;
}
}
}
}
var todayMs = new Date().getTime();
var evalThisRun = 0;
for (var i = 1; i < data.length; i++) {
var row = data[i];
var evaled = row[COL['t5_evaluated']];
if (evaled === true || String(evaled).toUpperCase() === 'TRUE') continue;
var daysDiff = (todayMs - new Date(row[COL['date']]).getTime()) / 86400000;
if (daysDiff < 7) continue;
var ticker = String(row[COL['ticker']] || '');
var verdict = String(row[COL['synthesis_verdict']] || '');
var closeAt = parseFloat(String(row[COL['close_at_record']] || ''));
var closeNow = priceMap[ticker] || 0;
if (closeAt <= 0 || closeNow <= 0) continue;
var t5Ret = Math.round((closeNow - closeAt) / closeAt * 10000) / 100;
var isCorrect = (verdict === 'STRONG_BUY_SIGNAL') ? (t5Ret > 0) : (t5Ret < 0);
fbSh.getRange(i + 1, COL['t5_evaluated'] + 1).setValue(true);
fbSh.getRange(i + 1, COL['t5_return_pct'] + 1).setValue(t5Ret);
fbSh.getRange(i + 1, COL['t5_correct'] + 1).setValue(isCorrect ? 'CORRECT' : 'WRONG');
evalThisRun++;
}
// prediction_accuracy_rate 갱신 (최소 10건 평가 완료 후)
var freshData = fbSh.getDataRange().getValues();
var allEval = 0, allCorrect = 0;
for (var j = 1; j < freshData.length; j++) {
var ev = freshData[j][COL['t5_evaluated']];
if (ev !== true && String(ev).toUpperCase() !== 'TRUE') continue;
allEval++;
if (String(freshData[j][COL['t5_correct']] || '') === 'CORRECT') allCorrect++;
}
if (allEval >= 10) {
var newRate = Math.round(allCorrect / allEval * 1000) / 10;
var settingSh = ss.getSheetByName('settings');
if (settingSh) {
var sData = settingSh.getDataRange().getValues();
var updated = false;
for (var si = 0; si < sData.length; si++) {
if (String(sData[si][0] || '').trim().toLowerCase() === 'prediction_accuracy_rate') {
settingSh.getRange(si + 1, 2).setValue(newRate);
updated = true;
break;
}
}
if (!updated) settingSh.appendRow(['prediction_accuracy_rate', newRate]);
Logger.log('[PA1_V1.2] prediction_accuracy_rate=' + newRate + '% (' + allCorrect + '/' + allEval + ')');
}
}
Logger.log('[PA1_V1.2] evaluatePa1FeedbackBatch_ 완료: 이번 평가=' + evalThisRun + '건');
// [PA1 V1.2] 정확도 기반 가중치 자동 조정 (평가 완료 후)
if (allEval >= 10) {
var accuracy7d = allCorrect / allEval;
adjustPaeWeights_();
}
} catch(e) {
Logger.log('[PA1_V1.2] evaluatePa1FeedbackBatch_ 오류: ' + e.message);
}
}
/**
* [PA1 V1.2] adjustPaeWeights_
* T+5 예측 정확도(7일) 기반으로 thesis/antithesis 가중치 자동 조정.
* 조정값을 settings 시트에 pa1_w_<factor> 형태로 기록 → 다음 실행 시 반영.
*/
function adjustPaeWeights_() {
try {
// 현재 precision 읽기
var accRate = getPredictionAccuracyRate_();
if (accRate === null) return; // 데이터 부족 시 조정 안 함
var accuracy = accRate / 100; // 0~1 범위로 변환
var ss = getSpreadsheet_();
var settingSh = ss.getSheetByName('settings');
if (!settingSh) return;
var sData = settingSh.getDataRange().getValues();
var currentWeights = {};
var rowIndex = {};
sData.forEach(function(row, i) {
var key = String(row[0] || '').trim().toLowerCase();
if (key.indexOf('pa1_w_') === 0) {
currentWeights[key] = parseFloat(String(row[1] || '')) || null;
rowIndex[key] = i + 1; // 1-based
}
});
// 기본 thesis/antithesis 총합 (12개 팩터 기본 가중치 합)
var DEFAULT_THESIS_TOTAL = 100; // 20+20+15+15+15+15
var DEFAULT_ANTI_TOTAL = 100; // 25+20+20+15+10+10
// 조정 방향 결정
var adjustThesis = 0;
var adjustAnti = 0;
if (accuracy < 0.55) {
// 정확도 낮음 → antithesis 강화 (+5% of base)
adjustThesis = -5;
adjustAnti = +5;
} else if (accuracy > 0.75) {
// 정확도 높음 → thesis 강화 (+3% of base)
adjustThesis = +3;
adjustAnti = 0;
} else {
Logger.log('[PA1_V1.2] adjustPaeWeights_: 정확도 정상범위(' + Math.round(accuracy*100) + '%) — 조정 불필요');
return;
}
// thesis 팩터 가중치 조정 (각 비례 분배)
var thesisFactors = ['pullback_entry','flow_strong','rs_leader','volume_confirm','rsi_healthy','brt_leader'];
var thesisDefaults = { pullback_entry: 20, flow_strong: 20, rs_leader: 15, volume_confirm: 15, rsi_healthy: 15, brt_leader: 15 };
thesisFactors.forEach(function(f) {
var key = 'pa1_w_' + f;
var baseW = thesisDefaults[f] || 0;
var currentW = currentWeights[key] != null ? currentWeights[key] : baseW;
var delta = Math.round(baseW / DEFAULT_THESIS_TOTAL * adjustThesis);
var newW = Math.max(5, Math.min(35, currentW + delta));
if (rowIndex[key]) {
settingSh.getRange(rowIndex[key], 2).setValue(newW);
} else {
settingSh.appendRow([key, newW]);
}
});
// antithesis 팩터 가중치 조정
var antiFactors = ['chase_risk','distribution','rsi_overbought','foreign_sell','usd_krw_weak','stale_position'];
var antiDefaults = { chase_risk: 25, distribution: 20, rsi_overbought: 20, foreign_sell: 15, usd_krw_weak: 10, stale_position: 10 };
antiFactors.forEach(function(f) {
var key = 'pa1_w_' + f;
var baseW = antiDefaults[f] || 0;
var currentW = currentWeights[key] != null ? currentWeights[key] : baseW;
var delta = Math.round(baseW / DEFAULT_ANTI_TOTAL * adjustAnti);
var newW = Math.max(5, Math.min(40, currentW + delta));
if (rowIndex[key]) {
settingSh.getRange(rowIndex[key], 2).setValue(newW);
} else {
settingSh.appendRow([key, newW]);
}
});
Logger.log('[PA1_V1.2] adjustPaeWeights_ 완료: accuracy=' + Math.round(accuracy*100) + '% adjustThesis=' + adjustThesis + ' adjustAnti=' + adjustAnti);
} catch(e) {
Logger.log('[PA1_V1.2] adjustPaeWeights_ 오류: ' + e.message);
}
}
/**
* updatePa1WeightsManual_
* PA1 팩터 가중치를 Work-1 승인값으로 settings 시트에 직접 기록.
* 근거: 기존 8.0x 획일 비율(thesis=30, anti=240) → 2.6x 차별화(thesis=70, anti=185)
* 효과: 모든 종목이 EXIT(-83~-95)로 획일화됐던 synthesis가 종목별 차별화됨
* (예: 000270 기아 +20 BULLISH / 005930 삼성전자 -18 BEARISH 등)
* 사용법: GAS 에디터 → updatePa1WeightsManual_ 선택 → 실행
*/
function updatePa1WeightsManual_() {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var settingSh = ss.getSheetByName(SETTINGS_SHEET_NAME);
if (!settingSh) {
Logger.log('[updatePa1WeightsManual_] settings 시트를 찾을 수 없음');
return;
}
// Work-1 승인 PA1 가중치 (thesis 70pt, antithesis 185pt, ratio=2.6x)
var APPROVED_WEIGHTS = {
// Thesis 팩터 (개별종목 차별화 강화): 5→10~15
pa1_w_pullback_entry: 15, // 눌림목 진입 — 핵심 타이밍
pa1_w_flow_strong: 15, // 수급 강세
pa1_w_rs_leader: 10, // 상대강도 선도
pa1_w_volume_confirm: 10, // 거래량 확인
pa1_w_rsi_healthy: 10, // RSI 여력
pa1_w_brt_leader: 10, // BRT 선도
// Antithesis 팩터 (핵심만 유지, 획일화 해소): 일부 완화
pa1_w_chase_risk: 40, // 뒷박 위험 — 유지
pa1_w_distribution: 40, // 분배 신호 — 유지
pa1_w_rsi_overbought: 40, // RSI 과열 — 유지
pa1_w_foreign_sell: 30, // 외인 매도 — 완화 (단기 노이즈)
pa1_w_usd_krw_weak: 15, // 환율 약세 — 대폭 완화 (전 종목 동일 페널티 방지)
pa1_w_stale_position: 20 // 장기보유 페널티 — 완화
};
// settings 시트에서 기존 pa1_w_* 행 인덱스 수집
var data = settingSh.getDataRange().getValues();
var rowIndex = {};
data.forEach(function(row, i) {
var key = String(row[0] || '').trim().toLowerCase();
if (key.indexOf('pa1_w_') === 0) {
rowIndex[key] = i + 1; // 1-based
}
});
// 값 쓰기 (존재하면 업데이트, 없으면 추가)
var updated = []; var added = [];
Object.keys(APPROVED_WEIGHTS).forEach(function(key) {
var val = APPROVED_WEIGHTS[key];
if (rowIndex[key]) {
settingSh.getRange(rowIndex[key], 2).setValue(val);
updated.push(key + '=' + val);
} else {
settingSh.appendRow([key, val]);
added.push(key + '=' + val);
}
});
var thesisTotal = 15+15+10+10+10+10;
var antiTotal = 40+40+40+30+15+20;
Logger.log('[updatePa1WeightsManual_] 완료');
Logger.log(' 업데이트: ' + updated.join(', '));
Logger.log(' 신규 추가: ' + (added.length ? added.join(', ') : '없음'));
Logger.log(' thesis합=' + thesisTotal + 'pt antithesis합=' + antiTotal + 'pt ratio=' + (antiTotal/thesisTotal).toFixed(1) + 'x');
SpreadsheetApp.getUi().alert(
'PA1 가중치 업데이트 완료\n' +
'thesis합=' + thesisTotal + 'pt / antithesis합=' + antiTotal + 'pt (ratio=' + (antiTotal/thesisTotal).toFixed(1) + 'x)\n' +
'업데이트: ' + updated.length + '개 / 추가: ' + added.length + '개\n\n' +
'다음 runDataFeed 실행 시 새 가중치가 PA1 계산에 반영됩니다.'
);
} catch(e) {
Logger.log('[updatePa1WeightsManual_] 오류: ' + e.message);
SpreadsheetApp.getUi().alert('오류: ' + e.message);
}
}
/**
* PA4 — MACRO_EVENT_SYNCHRONIZER_V1
* 외국인 순매도 연속일·USD/KRW·FOMC·VIX 등 거시 변수를 macro_risk_score로 환산.
* heat_gate_adj(-3/-1/0/+1) 및 mega_sell_alert 산출.
* @param {Object} macroJson getMacroJson() 반환값
* @param {Array} eventRows getEventRiskJson().events (DaysLeft, Type 컬럼)
*/
function calcMacroEventSynchronizerV1_(macroJson, eventRows) {
return calcMacroEventSynchronizerV1Impl_(macroJson, eventRows);
}
/**
* PA1 — PREDICTIVE_ALPHA_ENGINE_V1
* 正(thesis) + 反(antithesis) = 合(direction_confidence) 3계층 점수.
* synthesis_verdict=BEARISH(EXIT/TRIM) → BUY 차단 근거.
* @param {Array} holdings
* @param {Object} dfMap
* @param {Object} macroJson getMacroJson() 반환값
* @param {Object} mesResult calcMacroEventSynchronizerV1_ 반환값
*/
function calcPredictiveAlphaEngineV1_(holdings, dfMap, macroJson, mesResult, weightOverrides) {
return calcPredictiveAlphaEngineV1Impl_(holdings, dfMap, macroJson, mesResult, weightOverrides);
}
/**
* PA2 — ANTI_LATE_ENTRY_GATE_V2
* 3중 AND 게이트: velocity_1d / velocity_5d / distribution_weighted_sum.
* ANTI_CHASING_VELOCITY_V1을 완전 대체.
* @param {Array} holdings
* @param {Object} dfMap
*/
function calcAntiLateEntryGateV2_(holdings, dfMap) {
return calcAntiLateEntryGateV2Impl_(holdings, dfMap);
}
/**
* PA3 — CASH_PRESERVATION_SELL_ENGINE_V2
* K2(분할) + C1(폭포수) + C2(타이밍)를 통합. 매도 스타일 결정 + value_preservation_score.
* h3.sellQty에 수량이 있는 종목만 처리.
* @param {Array} holdings
* @param {Object} dfMap
* @param {Object} cashShortfallInfo calcCashShortfallHarness_ 반환값
* @param {Object} h3 calcQuantities_ 반환값 (.sellQty 배열)
*/
function calcCashPreservationSellEngineV2_(holdings, dfMap, cashShortfallInfo, h3) {
var shortfallKrw = (cashShortfallInfo && cashShortfallInfo.cash_shortfall_min_krw) || 0;
var sellQtyMap = {};
((h3 && h3.sellQty) || []).forEach(function(sq) {
if (typeof sq.sell_qty === 'number' && sq.sell_qty > 0) {
sellQtyMap[sq.ticker] = Math.floor(sq.sell_qty);
}
});
var rows = [];
holdings.forEach(function(h) {
var df = dfMap[h.ticker] || {};
var baseQty = sellQtyMap[h.ticker] || 0;
if (baseQty <= 0 && shortfallKrw <= 0) return;
var close = h.close || df.close || 0;
var prevClose = df.prevClose || close;
var rsi14 = typeof df.rsi14 === 'number' ? df.rsi14 : 50;
var atr20 = typeof df.atr20 === 'number' ? df.atr20 : (close * 0.02);
var stopPrice = h.stopPrice || 0;
var frg5d = typeof df.frg5d === 'number' ? df.frg5d : 0;
var inst5d = typeof df.inst5d === 'number' ? df.inst5d : 0;
var volume = typeof df.volume === 'number' ? df.volume : 0;
var avgVol5d = typeof df.avgVolume5d === 'number' ? df.avgVolume5d : 0;
// 현금 부족 시 baseQty 추정 (h3 미포함 종목)
if (baseQty <= 0 && shortfallKrw > 0 && close > 0) {
baseQty = Math.min(Math.floor(shortfallKrw / close), h.holdingQty || 0);
}
if (baseQty <= 0) return;
// distribution weighted_sum (inline)
var distWS = 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 (df.acGate === 'BLOCK') distWS += 1.0;
var emergencyFullSell = h.stopBreach === true;
// ── execution_style 결정 ─────────────────────────────────────────────────
var execStyle;
if (emergencyFullSell) execStyle = 'EMERGENCY_FULL_EXIT';
else if (rsi14 < 30) execStyle = 'OVERSOLD_REBOUND_SELL';
else execStyle = 'STAGED_WATERFALL';
// ── 수량 산출 ────────────────────────────────────────────────────────────
var immediateQty = 0, reboundWaitQty = 0, reboundTriggerPrice = 0, reboundDeadlineDays = 0;
if (execStyle === 'OVERSOLD_REBOUND_SELL') {
immediateQty = Math.floor(baseQty * 0.50);
reboundWaitQty = baseQty - immediateQty;
// TICK_NORMALIZER_V1 간소화: 10원 단위 반올림
reboundTriggerPrice = Math.round((prevClose + 0.5 * atr20) / 10) * 10;
reboundDeadlineDays = 3;
} else if (execStyle === 'EMERGENCY_FULL_EXIT') {
immediateQty = baseQty;
reboundWaitQty = 0;
reboundTriggerPrice = 0;
reboundDeadlineDays = 0;
} else {
immediateQty = Math.floor(baseQty * 0.50);
reboundWaitQty = baseQty - immediateQty;
reboundTriggerPrice = prevClose > 0 ? prevClose : close;
reboundDeadlineDays = 5;
}
// ── rebound_scenario ─────────────────────────────────────────────────────
var limitPrice = prevClose > 0 ? prevClose : close;
var immediateKrw = immediateQty * limitPrice;
var reboundUpsideKrw = reboundWaitQty * (reboundTriggerPrice > 0 ? reboundTriggerPrice : limitPrice);
var downsideRiskKrw = reboundWaitQty * (stopPrice > 0 ? stopPrice : close * 0.92);
var rrNum = reboundUpsideKrw - immediateKrw;
var rrDen = Math.max(1, immediateKrw - downsideRiskKrw);
var riskRewardRatio = round2_(rrNum / rrDen);
// ── value_preservation_score ─────────────────────────────────────────────
var vpScore = 100;
if (immediateQty >= baseQty && rsi14 < 30) vpScore -= 30; // full_sell_oversold
if (distWS >= 3.0) vpScore -= 15; // distribution_high
if (reboundWaitQty > 0) vpScore += 15; // rebound_wait_exists
if (reboundTriggerPrice > 0 && limitPrice > 0
&& reboundTriggerPrice <= limitPrice * 1.03) vpScore += 10; // tight_trigger
vpScore = Math.max(0, Math.min(100, Math.round(vpScore)));
rows.push({
ticker: h.ticker,
name: h.name || df.name || '',
execution_style: execStyle,
base_qty: baseQty,
immediate_qty: immediateQty,
rebound_wait_qty: reboundWaitQty,
rebound_trigger_price: reboundTriggerPrice,
rebound_deadline_days: reboundDeadlineDays,
risk_reward_ratio: riskRewardRatio,
value_preservation_score: vpScore,
immediate_sell_krw: Math.round(immediateKrw),
rebound_upside_krw: Math.round(reboundUpsideKrw),
emergency_full_sell_flag: emergencyFullSell,
sell_value_damage_warning: vpScore < 50,
dist_weighted_sum: Math.round(distWS * 10) / 10,
formula_id: 'CASH_PRESERVATION_SELL_ENGINE_V2'
});
});
return rows;
}
/**
* PA5 — CONSISTENCY_VALIDATOR_V2
* 12개 논리 검증 항목으로 hApex 일관성 점검. score < 90 → cv_verdict=BLOCK.
* Sprint C 마지막에 실행 — 이전 PA1~PA4 결과까지 모두 포함한 hApex 검증.
* @param {Object} hApex
* @param {Object} asResult
* @param {Object} cashFloorInfo
* @param {string} capturedAtIso
* @param {Date} now
*/
function calcConsistencyValidatorV2_(hApex, asResult, cashFloorInfo, capturedAtIso, now) {
return calcConsistencyValidatorV2Impl_(hApex, asResult, cashFloorInfo, capturedAtIso, now);
}
/**
* [PROPOSAL47_A1] WATCH_BREAKOUT_REALTIME_GATE_V1
* REVIEW / EXIT 라이프사이클 단계의 보유 종목 중 velocity_1d >= 2.0% 급등 탐지.
* 감시 중 급등 누락(49건 근본 원인) 해결 — 당일 급등 감지 시 후보 승격 검토 신호 생성.
* anti_late_entry_grade가 F(BLOCK)인 경우 승격 제외 (추격 매수 방지).
*
* @param {Array} holdings asResult.holdings
* @param {Object} dfMap 종목별 데이터 피드
* @param {Array} slgRows satellite_lifecycle_gate_json (lifecycle_stage 포함)
* @param {Array} aleRows anti_late_entry_json (entry_grade 포함, F면 제외)
* @returns {Array} watch_breakout_candidates_json
*/
function calcWatchBreakoutRealtimeGateV1_(holdings, dfMap, slgRows, aleRows) {
var VELOCITY_THRESHOLD = 2.0;
var REVIEW_STAGES = ['REVIEW', 'EXIT'];
var slgMap = {};
(slgRows || []).forEach(function(r) {
slgMap[String(r.ticker || '')] = String(r.lifecycle_stage || '');
});
var aleMap = {};
(aleRows || []).forEach(function(r) {
aleMap[String(r.ticker || '')] = r;
});
var results = [];
(holdings || []).forEach(function(h) {
var ticker = String(h.ticker || '');
var stage = slgMap[ticker] || '';
if (REVIEW_STAGES.indexOf(stage) < 0) return;
var df = dfMap[ticker] || {};
var close = Number(df.close || h.close || 0);
var prevClose = Number(df.prevClose || 0);
if (close <= 0 || prevClose <= 0) return;
var velocity1d = Math.round((close - prevClose) / prevClose * 10000) / 100;
if (velocity1d < VELOCITY_THRESHOLD) return;
var aleEntry = aleMap[ticker] || {};
var aleGrade = aleEntry.entry_grade || 'B';
if (aleGrade === 'F') return; // 추격매수 방지: anti_late_entry_grade F 제외
results.push({
ticker: ticker,
name: h.name || df.name || '',
lifecycle_stage: stage,
velocity_1d: velocity1d,
promotion_signal: 'WATCH_BREAKOUT',
anti_late_entry_grade: aleGrade,
formula_id: 'WATCH_BREAKOUT_REALTIME_GATE_V1'
});
});
return results;
}
/**
* [PROPOSAL48_A3] ANTI_WHIPSAW_REENTRY_GATE_V1
* 매도 압박(tier=1/2) 종목이 당일 +3% 이상 급반등 시 REENTRY_CANDIDATE 마킹.
* 9건 "매도 신호 후 반등" 패턴 처리. 매도 실행 전 재검토 신호 제공.
*
* @param {Array} sellCandidates hApex.sell_candidates_json (tier, ticker, action 포함)
* @param {Object} dfMap 종목별 데이터 피드
* @param {Array} holdings asResult.holdings
* @returns {Array} anti_whipsaw_reentry_json
*/
function calcAntiWhipsawReentryGateV1_(sellCandidates, dfMap, holdings) {
var REENTRY_VELOCITY_THRESHOLD = 3.0; // 재진입 급반등 기준: +3%
var WHIPSAW_TIERS = [1, 2]; // 즉시·단계 매도 압박 대상
var results = [];
(sellCandidates || []).forEach(function(cand) {
var tier = typeof cand.tier === 'number' ? cand.tier : parseInt(cand.tier) || 99;
if (WHIPSAW_TIERS.indexOf(tier) < 0) return;
var ticker = cand.ticker;
var df = dfMap[ticker] || {};
var h = (holdings || []).find(function(x) { return x.ticker === ticker; }) || {};
var close = h.close || df.close || 0;
var prevClose = df.prevClose || 0;
if (close <= 0 || prevClose <= 0) return;
var velocity1d = Math.round((close - prevClose) / prevClose * 10000) / 100;
if (velocity1d < REENTRY_VELOCITY_THRESHOLD) return;
var profitPct = h.avgCost > 0
? Math.round((close - h.avgCost) / h.avgCost * 1000) / 10
: null;
var rsi14 = typeof df.rsi14 === 'number' ? df.rsi14 : null;
// 재진입 등급: A(rsi<50 + rs_leader), B(rsi<60), C(기본)
var reentryGrade = 'C';
if (rsi14 !== null && rsi14 < 50 && df.rs_verdict === 'LEADER') reentryGrade = 'A';
else if (rsi14 !== null && rsi14 < 60) reentryGrade = 'B';
results.push({
ticker: ticker,
name: h.name || df.name || '',
sell_tier: tier,
sell_action: cand.action || '',
velocity_1d: velocity1d,
close: close,
prev_close: prevClose,
rsi14: rsi14,
rs_verdict: df.rs_verdict || '',
profit_pct: profitPct,
reentry_grade: reentryGrade,
reentry_signal: 'REENTRY_CANDIDATE',
whipsaw_warning: '매도 압박 중 반등 — 실행 전 재검토 권고',
formula_id: 'ANTI_WHIPSAW_REENTRY_GATE_V1'
});
});
return results;
}
/**
* [PROPOSAL48_C7] getAlphaHistorySummary_
* alpha_history 시트의 T20/T60 alpha gate 결과를 집계.
* 위성 종목의 장기 알파 생성 능력 추적 — T+5 피드백 루프 대용 지표.
* DATA_INSUFFICIENT 상태에서도 구조를 갖춰 LLM 참조 가능하게 유지.
*/
function getAlphaHistorySummary_() {
try {
var ss = getSpreadsheet_();
var sh = ss.getSheetByName('alpha_history');
if (!sh) return { status: 'NO_SHEET', formula_id: 'ALPHA_HISTORY_SUMMARY_V1' };
var rows = sh.getDataRange().getValues();
if (!rows || rows.length < 2) return { status: 'EMPTY', formula_id: 'ALPHA_HISTORY_SUMMARY_V1' };
var header = rows[0].map(function(h) { return String(h).trim(); });
var idx = {};
['Ticker','T20_Alpha_Gate','T60_Alpha_Gate','T20_Vs_Core_Pctp','T60_Vs_Core_Pctp','SAQG_Grade_At_Entry'].forEach(function(k) {
idx[k] = header.indexOf(k);
});
var t20 = { total: 0, pass: 0, fail: 0, missing: 0 };
var t60 = { total: 0, pass: 0, fail: 0, missing: 0 };
var gradeCount = {};
for (var r = 1; r < rows.length; r++) {
var row = rows[r];
var g20 = idx['T20_Alpha_Gate'] >= 0 ? String(row[idx['T20_Alpha_Gate']] || '') : '';
var g60 = idx['T60_Alpha_Gate'] >= 0 ? String(row[idx['T60_Alpha_Gate']] || '') : '';
var grade = idx['SAQG_Grade_At_Entry'] >= 0 ? String(row[idx['SAQG_Grade_At_Entry']] || '') : '';
if (g20 && g20 !== 'PENDING') {
t20.total++;
if (g20 === 'PASS') t20.pass++;
else if (g20 === 'FAIL') t20.fail++;
else t20.missing++;
}
if (g60 && g60 !== 'PENDING') {
t60.total++;
if (g60 === 'PASS') t60.pass++;
else if (g60 === 'FAIL') t60.fail++;
else t60.missing++;
}
if (grade) gradeCount[grade] = (gradeCount[grade] || 0) + 1;
}
var t20Rate = t20.total > 0 ? Math.round(t20.pass / t20.total * 1000) / 10 : null;
var t60Rate = t60.total > 0 ? Math.round(t60.pass / t60.total * 1000) / 10 : null;
return {
status: (t20.total > 0 || t60.total > 0) ? 'OK' : 'DATA_INSUFFICIENT',
t20_total: t20.total,
t20_pass_rate: t20Rate,
t20_pass: t20.pass,
t20_fail: t20.fail,
t60_total: t60.total,
t60_pass_rate: t60Rate,
t60_pass: t60.pass,
t60_fail: t60.fail,
grade_count: gradeCount,
total_rows: rows.length - 1,
formula_id: 'ALPHA_HISTORY_SUMMARY_V1'
};
} catch(e) {
return { status: 'ERROR', error: e.message, formula_id: 'ALPHA_HISTORY_SUMMARY_V1' };
}
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P0-1: EXPORT_GATE_V1 — PENDING_EXPORT 원인 자동 진단
// Direction G5: PENDING_EXPORT 원인 진단 의무
// ═══════════════════════════════════════════════════════════════════════
/**
* calcExportGate_
* 5개 체크리스트 자동 평가 → EXPORT_READY / PENDING_EXPORT
* PASS 전 HTS 입력 금지 조건을 결정론적으로 산출.
*/
function calcExportGate_(hApex, asResult, cashFloorInfo) {
var checks = [];
// CHECK_1: account_snapshot 캡처 완료 여부
var captureRequired = !(asResult && asResult.holdings && asResult.holdings.length > 0
&& asResult.settlementCashD2Krw > 0);
checks.push({
check_id: 'CHECK_1_SNAPSHOT_CAPTURED',
status: captureRequired ? 'FAIL' : 'PASS',
message: captureRequired
? 'account_snapshot 미캡처 — HTS 화면 캡처 후 재실행 필요'
: 'account_snapshot OK'
});
// CHECK_2: 데이터 완성도 (buy_permission_json 기준 전 종목 존재)
var bpJson = (hApex && hApex.buy_permission_json) || [];
var holdingCount = (asResult && asResult.holdings) ? asResult.holdings.length : 0;
var dataOk = holdingCount > 0 && bpJson.length >= holdingCount;
checks.push({
check_id: 'CHECK_2_DATA_COMPLETENESS',
status: dataOk ? 'PASS' : 'FAIL',
message: dataOk
? 'data_feed 완성도 OK (' + bpJson.length + '/' + holdingCount + ')'
: 'data_feed 누락 — npm run convert-data-json 후 재실행'
});
// CHECK_3: 하네스 무결성 체크섬 (consistency_score 기준)
var cvScore = (hApex && typeof hApex.consistency_score === 'number') ? hApex.consistency_score : null;
var cvOk = cvScore !== null && cvScore >= 70;
checks.push({
check_id: 'CHECK_3_HARNESS_INTEGRITY',
status: cvOk ? 'PASS' : 'FAIL',
message: cvOk
? 'consistency_score=' + cvScore + ' 무결성 OK'
: 'consistency_score=' + (cvScore !== null ? cvScore : 'null') + ' — 70 미만 또는 미산출'
});
// CHECK_4: SELL_PRICE_SANITY — INVALID 주문 없음
var blueprint = (hApex && hApex.order_blueprint_json) || [];
var invalidPrices = blueprint.filter(function(b) {
return String(b.validation_status || '').indexOf('INVALID') >= 0;
});
checks.push({
check_id: 'CHECK_4_NO_INVALID_PRICES',
status: invalidPrices.length === 0 ? 'PASS' : 'FAIL',
message: invalidPrices.length === 0
? 'SELL_PRICE_SANITY 이상 없음'
: 'INVALID 가격 ' + invalidPrices.length + '건: ' +
invalidPrices.map(function(b) { return b.ticker; }).join(',')
});
// CHECK_5: cashFloor 블록 상태 확인 (HARD_BLOCK 시 현금 부족 경보)
var cashStatus = (cashFloorInfo && cashFloorInfo.status) || 'UNKNOWN';
var cashOk = cashStatus !== 'UNKNOWN';
checks.push({
check_id: 'CHECK_5_CASH_LEDGER',
status: cashOk ? 'PASS' : 'WARN',
message: cashOk
? 'cash_floor_status=' + cashStatus + ' (기록됨)'
: 'cash_floor_status=UNKNOWN — settlement_cash_d2_krw 확인 필요'
});
// [PROPOSAL51] P1-A: CHECK_6 — SCRS_RENDER 검증 (immediate_sell_qty 유효값 필수)
var scrsV2 = (hApex && hApex.scrs_v2_json) || {};
// [PROPOSAL51-FIX] GAS는 immediate_qty 반환 (calcSmartCashRecoverySell_ 확인)
var scrsRows = scrsV2.selected_combo || scrsV2.candidates || scrsV2.rows || [];
var scrsRenderOk = scrsRows.length === 0 || scrsRows.every(function(r) {
var qty = r.immediate_qty !== undefined ? r.immediate_qty : r.immediate_sell_qty;
return qty !== null && qty !== undefined && qty !== '-' && qty !== '';
});
checks.push({
check_id: 'CHECK_6_SCRS_RENDER',
status: scrsRenderOk ? 'PASS' : 'WARN',
message: scrsRenderOk
? 'SCRS-V2 immediate_sell_qty 렌더링 OK'
: 'SCRS-V2 immediate_sell_qty 누락 — render_operational_report 키 불일치 확인 필요'
});
// [PROPOSAL51] P1-A: CHECK_7 — PORTFOLIO_HEALTH_SCORE 타입 (Boolean 금지)
var healthScore = hApex && hApex.portfolio_health_score;
var healthTypeOk = (typeof healthScore === 'number' && !isNaN(healthScore));
checks.push({
check_id: 'CHECK_7_HEALTH_SCORE_TYPE',
status: healthTypeOk ? 'PASS' : 'WARN',
message: healthTypeOk
? 'portfolio_health_score=' + healthScore + ' (숫자 OK)'
: 'portfolio_health_score=' + JSON.stringify(healthScore) + ' — 숫자여야 함 (Boolean/null 금지)'
});
// [PROPOSAL51] P1-A: CHECK_8 — CLUSTER_SYNC 교정 없음 확인
var clusterSync = (hApex && hApex.cluster_sync_result_json) || {};
var clusterSyncOk = clusterSync.status === 'SYNCED' || !clusterSync.status;
checks.push({
check_id: 'CHECK_8_CLUSTER_SYNC',
status: clusterSyncOk ? 'PASS' : 'WARN',
message: clusterSyncOk
? 'SEMICONDUCTOR_CLUSTER_SYNC: 정합성 OK'
: 'CLUSTER_SYNC 교정 발생 (cluster_pct=' + (clusterSync.cluster_pct || '?')
+ '%, threshold=' + (clusterSync.threshold_pct || '?') + '%)'
});
var failChecks = checks.filter(function(c) { return c.status === 'FAIL'; });
var warnChecks = checks.filter(function(c) { return c.status === 'WARN'; });
var exportStatus;
if (failChecks.length > 0) exportStatus = 'PENDING_EXPORT';
else if (warnChecks.length > 0) exportStatus = 'REVIEW_ONLY';
else exportStatus = 'EXPORT_READY';
var htsAllowed = exportStatus === 'EXPORT_READY';
var nonPassChecks = checks.filter(function(c) { return c.status !== 'PASS'; });
var resolutionGuide = nonPassChecks.map(function(c) {
return '[' + c.check_id + '] ' + c.message;
});
return {
json_validation_status: exportStatus,
export_gate_status: exportStatus,
all_checks_passed: failChecks.length === 0 && warnChecks.length === 0,
checks: checks,
failed_checks: failChecks.map(function(c) { return c.check_id; }),
warn_checks: warnChecks.map(function(c) { return c.check_id; }),
resolution_guide: resolutionGuide,
hts_entry_allowed: htsAllowed,
formula_id: 'EXPORT_GATE_V2'
};
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P0-2: ROUTING_TRACE_V1 — 라우팅 Trace 필수 출력 (Direction G4)
// ═══════════════════════════════════════════════════════════════════════
/**
* buildRoutingTrace_
* 모든 보고서 선행 출력 의무 — request_route, bundle, prompt, 검증 상태 etc.
* 누락 시 보고서 전체 INCOMPLETE_REPORT.
*/
function buildRoutingTrace_(intradayLock, cashFloorInfo, hApex, capturedAtIso) {
var scope = intradayLock ? 'TRIM_ONLY' : 'FULL_ANALYSIS';
var bundleSelected = (function() {
var cv = (hApex && hApex.consistency_score);
if (cv === null || cv === undefined) return 'retirement_portfolio_ultra_compact';
if (cv < 70) return 'retirement_portfolio_ultra_compact';
return 'retirement_portfolio_compact';
})();
var exportGate = (hApex && hApex.export_gate_json) || {};
var jsonValStatus = exportGate.json_validation_status || 'PENDING_EXPORT';
var captureRequired = exportGate.checks
? !exportGate.checks.some(function(c) {
return c.check_id === 'CHECK_1_SNAPSHOT_CAPTURED' && c.status === 'PASS';
})
: true;
var cashLedgerBasis = 'D2_ONLY';
var snapshotExecGate = (cashFloorInfo && cashFloorInfo.status === 'PASS')
? 'FULL_EXECUTION' : 'REVIEW_ONLY';
return {
request_route: 'PIPELINE_EOD_BATCH',
bundle_selected: bundleSelected,
prompt_entrypoint: 'prompts/analysis_prompt.md',
json_validation_status: jsonValStatus,
capture_required: captureRequired,
intraday_scope: scope,
snapshot_execution_gate: snapshotExecGate,
price_basis: capturedAtIso || 'UNKNOWN',
cash_ledger_basis: cashLedgerBasis,
routing_trace_complete: true,
formula_id: 'ROUTING_TRACE_V1'
};
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P0-3: WATCH_LEDGER_V1 — WATCH 감시 원장 (Direction I4)
// HTS 입력 금지 컬럼명만 허용 — 주문표와 물리적 분리
// ═══════════════════════════════════════════════════════════════════════
/**
* buildWatchLedger_
* order_blueprint_json에서 validation_status != PASS 행을 분리.
* 허용 컬럼: ticker/name, reference_stop_price, reference_tp_state, hts_allowed, reason_code
* 금지 컬럼: 지정가, 손절가, 익절가, 주문가, 주문수량 등 (INVALID_COLUMN)
*/
function buildWatchLedger_(orderBlueprint, h4) {
var priceMap = {};
((h4 && h4.prices) || []).forEach(function(p) { priceMap[p.ticker] = p; });
var blueprintRows = Array.isArray(orderBlueprint) ? orderBlueprint : [];
var watchRows = blueprintRows.filter(function(b) {
return b.validation_status !== 'PASS';
});
return watchRows.map(function(b) {
var p = priceMap[b.ticker] || {};
var tpState = (function() {
if (!p.tp1_price) return 'INVALID_TP_STALE';
if (p.tp_state === 'TP1_ALREADY_TRIGGERED') return 'TP1_ALREADY_TRIGGERED';
return 'PENDING';
})();
return {
ticker: b.ticker,
name: b.name || '',
reference_stop_price: p.stop_price || null,
reference_tp_state: tpState,
hts_allowed: false,
reason_code: b.validation_status || 'NO_EXECUTION:WATCH',
note: '주문 아님. HTS 입력 금지.'
};
});
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P1-1: EXPERT_JUDGMENT_CONSENSUS_ENGINE_V1 (EJCE-V1)
// 30년 전문가 수준 3관점(애널리스트·트레이더·퀀트) 합의 게이트
// Direction EJ1: consensus_result=NO_BUY 시 BUY 절대 금지
// ═══════════════════════════════════════════════════════════════════════
/**
* calcExpertJudgmentConsensus_
* 3관점 독립 채점 → majority_rule → final_allowed_action 고착화
* LLM "분위기 좋으니까" 판단을 결정론적 합의로 대체.
*/
function calcExpertJudgmentConsensus_(ticker, df, paeRow, h1, hApex, dfMap) {
df = df || {};
paeRow = paeRow || {};
// ── ANALYST_VIEW: 펀더멘털·밸류에이션 ─────────────────────────────────────
var compositeScore = toNumber_(df['SS001_Score'] || df['composite_score']) || 0;
var pegScore = toNumber_(df['PEG_Score'] || df['peg_score']) || 0;
var upsidePct = toNumber_(df['Upside_Pct'] || df['upside_pct']) || 0;
var epsMiss = toNumber_(df['EPS_Revision_Status'] === 'MISS' ? 1 : 0);
var dartRisk = String(df['DART_Risk'] || '').toUpperCase() === 'Y';
var analystScore = 0;
if (compositeScore >= 70) analystScore += 30;
else if (compositeScore >= 50) analystScore += 15;
if (pegScore >= 8 || upsidePct > 15) analystScore += 20;
if (upsidePct > 15) analystScore += 5;
if (epsMiss >= 2) analystScore -= 30;
if (dartRisk) analystScore -= 20;
var analystVerdict = analystScore >= 30 ? 'BULLISH'
: analystScore >= -10 ? 'NEUTRAL'
: 'BEARISH';
// ── TRADER_VIEW: 타이밍·수급·추세 ─────────────────────────────────────────
var flowCredit = toNumber_(df['Flow_Credit'] || df['flow_credit']) || 0;
var rsVerdict = String(df['RS_Verdict'] || df['rs_verdict'] || '').toUpperCase();
var velocity1d = toNumber_(df['Ret5D'] != null ? df['Close'] / (df['Close'] / (1 + toNumber_(df['Ret5D']) / 100)) - 1 : 0) * 100;
// 더 단순하게: Ret5D/5 근사
var ret5d = toNumber_(df['Ret5D'] || df['ret5d']) || 0;
var vel1d_approx = ret5d / 5;
var paeAnti = toNumber_(paeRow.antithesis_score) || 0;
var distCount = toNumber_(df['Dist_Signals'] || df['distribution_signals_count']) || 0;
var ma20 = toNumber_(df['MA20']) || 0;
var close = toNumber_(df['Close'] || df['close']) || 0;
var atr20 = toNumber_(df['ATR20']) || 0;
var inPullback = (ma20 > 0 && close > 0) ? close <= ma20 * 1.03 : false;
var traderScore = 0;
if (flowCredit >= 0.55 && rsVerdict === 'LEADER') traderScore += 25;
if (inPullback) traderScore += 20;
if (vel1d_approx < 1.5 && ret5d > 0) traderScore += 20;
if (vel1d_approx >= 3.0) traderScore -= 30; // 뒷박 강한 패널티
if (paeAnti >= 50) traderScore -= 25; // 설거지 경보
if (distCount >= 2) traderScore -= 25;
var traderVerdict = traderScore >= 20 ? 'ENTRY_OK'
: traderScore >= -10 ? 'WAIT'
: 'BLOCK_ENTRY';
// ── QUANT_VIEW: 통계·팩터·리스크예산 ─────────────────────────────────────
var pacVal = toNumber_((hApex && hApex.portfolio_alpha_confidence)) || 0;
var heatGate = String((hApex && hApex.heat_gate_status) || '').toUpperCase();
var ddGuard = String((hApex && hApex.drawdown_guard_state) || '').toUpperCase();
var expectedEdge = toNumber_(df['Expected_Edge'] || df['expected_edge']) || 0;
var atrAvail = atr20 > 0;
var quantScore = 0;
if (expectedEdge > 0 && atrAvail) quantScore += 25;
if (atrAvail) quantScore += 10;
if (pacVal > 20) quantScore += 20;
if (pacVal < -20) quantScore -= 30; // 전체 알파 신뢰도 BLOCK
if (heatGate === 'BLOCK_NEW_BUY') quantScore -= 20;
if (ddGuard === 'NO_BUY') quantScore -= 15;
var quantVerdict = quantScore >= 20 ? 'APPROVED'
: quantScore >= -10 ? 'REDUCED'
: 'REJECTED';
// ── CONSENSUS_MATRIX: 2/3 이상 BLOCK → NO_BUY ───────────────────────────
var blockCount = 0;
if (analystVerdict === 'BEARISH') blockCount++;
if (traderVerdict === 'BLOCK_ENTRY') blockCount++;
if (quantVerdict === 'REJECTED') blockCount++;
var consensusResult, finalAllowedAction;
if (blockCount >= 2) {
consensusResult = 'NO_BUY';
finalAllowedAction = 'HOLD';
} else if (analystVerdict === 'BULLISH' && traderVerdict === 'ENTRY_OK' && quantVerdict === 'APPROVED') {
consensusResult = 'STRONG_BUY';
finalAllowedAction = 'BUY';
} else if (analystVerdict === 'BULLISH' && traderVerdict === 'ENTRY_OK') {
consensusResult = 'BUY_HALF';
finalAllowedAction = 'BUY_HALF';
} else if (analystVerdict === 'BULLISH' && traderVerdict === 'WAIT') {
consensusResult = 'BUY_PULLBACK';
finalAllowedAction = 'WAIT_PULLBACK';
} else if (analystVerdict === 'NEUTRAL' && traderVerdict === 'ENTRY_OK') {
consensusResult = 'BUY_PILOT';
finalAllowedAction = 'PILOT';
} else {
consensusResult = 'HOLD_WATCH';
finalAllowedAction = 'WATCH';
}
var blockReasons = [];
if (analystVerdict === 'BEARISH') blockReasons.push('ANALYST_BEARISH');
if (traderVerdict === 'BLOCK_ENTRY') blockReasons.push('TRADER_BLOCK_ENTRY_vel=' + vel1d_approx.toFixed(1) + '%');
if (quantVerdict === 'REJECTED') blockReasons.push('QUANT_REJECTED_pac=' + pacVal.toFixed(1));
return {
ticker: ticker,
analyst_score: analystScore,
analyst_verdict: analystVerdict,
trader_score: traderScore,
trader_verdict: traderVerdict,
quant_score: quantScore,
quant_verdict: quantVerdict,
block_count: blockCount,
consensus_result: consensusResult,
final_allowed_action: finalAllowedAction,
block_reasons: blockReasons,
override_required: blockCount >= 2,
formula_id: 'EXPERT_JUDGMENT_CONSENSUS_ENGINE_V1'
};
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P1-2: SMART_CASH_RECOVERY_SELL_ENGINE_V2 (SCRS-V2)
// 세련된 현금확보 매도 — 주식가치 보호 + 반등 포착 통합 엔진
// Direction C3: SCRS-V2 selected_combo만 HTS 주문표 기재 허용
// ═══════════════════════════════════════════════════════════════════════
/**
* calcSmartCashRecoverySell_
* 현금 부족액을 최소 주식가치 훼손으로 회수.
* 반등 기대 수익(expected_rebound_gain_krw) 사전 산출.
* "현금 급함" 이유로 Stage_2 우회 원천 차단.
*/
function calcSmartCashRecoverySell_(holdings, dfMap, cashShortfallInfo, h2, hApex) {
var shortfall = toNumber_((cashShortfallInfo && cashShortfallInfo.cash_shortfall_min_krw)) || 0;
var totalAsset = toNumber_((hApex && hApex.total_asset_krw) || (cashShortfallInfo && cashShortfallInfo.total_asset_krw)) || 1;
var emergencyScore = shortfall / totalAsset * 100;
var level = emergencyScore >= 15 ? 'EMERGENCY'
: emergencyScore >= 8 ? 'URGENT'
: emergencyScore >= 3 ? 'NORMAL'
: 'TRIM_ONLY';
var holdMap = {};
(holdings || []).forEach(function(h) { holdMap[h.ticker] = h; });
var sellQtyMap = {};
((hApex && hApex.sell_quantities_json) || []).forEach(function(sq) {
sellQtyMap[sq.ticker] = sq;
});
var candidates = ((h2 && h2.candidates) || []).slice();
// [Phase 3] SMART_CASH_RECOVERY_V6: value_damage_score(가치 훼손 점수) 기준 오름차순 정렬
candidates.forEach(function(c) {
var h = holdMap[c.ticker] || {};
var df = dfMap[c.ticker] || {};
var close = toNumber_(h.close || df['Close'] || df.close) || 0;
var atr20 = toNumber_(df['ATR20'] || df.atr20) || (close * 0.02);
// 가치 훼손 점수: 슬리피지 및 낙폭 리스크를 수치화 (낮을수록 매도 유리)
c.value_damage_score = close > 0 ? ((atr20 * 0.3) / close) * 100 : 100;
});
candidates.sort(function(a, b) {
return (a.value_damage_score || 0) - (b.value_damage_score || 0);
});
var cumulative = 0;
var combo = [];
for (var i = 0; i < candidates.length; i++) {
if (shortfall > 0 && cumulative >= shortfall) break;
var c = candidates[i];
var h = holdMap[c.ticker] || {};
var df = dfMap[c.ticker] || {};
var close = toNumber_(h.close || df['Close'] || df.close) || 0;
var atr20 = toNumber_(df['ATR20'] || df.atr20) || (close * 0.02);
var holding = toNumber_(h.holdingQty || h.holding_qty) || 0;
var sqRow = sellQtyMap[c.ticker] || {};
var baseQty = toNumber_(sqRow.sell_qty) || Math.floor(holding * 0.33);
if (close <= 0 || baseQty <= 0) continue;
var currentValue = holding * close;
var immediateQty = Math.floor(baseQty * 0.50);
var reboundWaitQty = baseQty - immediateQty;
var slippage = atr20 * 0.3;
var immediateKrw = immediateQty * Math.max(0, close - slippage);
var damagePct = currentValue > 0 ? immediateKrw / currentValue * 100 : 100;
if (damagePct > 30 && level !== 'EMERGENCY') continue;
var reboundTrigger = tickNormalize_(close + atr20 * 0.5, close);
var expectedReboundKrw = reboundWaitQty * Math.max(0, reboundTrigger - close);
// [Phase 3] 유동성 기준 exec_mode 강제 지정
var avgTradeValue = toNumber_(df['AvgTradeValue_20D_M'] || df.avgTradeVal20d) || 10000000000;
var execMode = 'LIMIT_NEAR_BID';
if (avgTradeValue < 5000000000) {
execMode = 'TWAP_5_SPLIT';
} else if (avgTradeValue > 50000000000) {
execMode = 'MARKET';
}
cumulative += immediateKrw;
combo.push({
rank: c.rank,
ticker: c.ticker,
name: c.name || (h.name || ''),
exec_mode: execMode,
value_damage_score: Math.round(c.value_damage_score * 10) / 10,
immediate_qty: immediateQty,
rebound_wait_qty: reboundWaitQty,
immediate_krw: Math.round(immediateKrw),
rebound_trigger_price: reboundTrigger,
expected_rebound_krw: Math.round(expectedReboundKrw),
value_damage_pct: Math.round(damagePct * 10) / 10,
rebound_deadline_date: addBusinessDays_(new Date(), 3)
});
}
var totalReboundGain = combo.reduce(function(s, c) { return s + c.expected_rebound_krw; }, 0);
var avgDamage = combo.length > 0
? combo.reduce(function(s, c) { return s + c.value_damage_pct; }, 0) / combo.length : 0;
var emergencyFullSell = combo.length > 0
&& combo[0].immediate_krw * 2 < shortfall
&& level === 'EMERGENCY';
return {
emergency_level: level,
shortfall_krw: Math.round(shortfall),
selected_combo: combo,
total_immediate_sell_krw: Math.round(cumulative),
expected_rebound_gain_krw: Math.round(totalReboundGain),
value_damage_pct_avg: Math.round(avgDamage * 10) / 10,
emergency_full_sell: emergencyFullSell,
shortfall_covered: shortfall <= 0 || cumulative >= shortfall,
formula_id: 'SMART_CASH_RECOVERY_SELL_ENGINE_V6'
};
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL51] P1-C: CASH_RECOVERY_DISPLAY_LOCK_V1 (CRDL-V1)
// 현금회복 금액 3분리 표시 잠금 — 207억 과대표시 차단
// min_required / optimal_combo / reference_total (주문 아님)
// ═══════════════════════════════════════════════════════════════════════
/**
* calcCashRecoveryDisplayLock_
* 현금회복 금액을 3분리(최소필요/최적조합/전체후보) 표시 잠금.
* reference_total_krw는 "주문 아님" 레이블 필수.
*/
function calcCashRecoveryDisplayLock_(scrsJson, trimPlanJson, cashInfo) {
function normalizeRows_(v) {
if (Array.isArray(v)) return v;
if (!v) return [];
if (typeof v === 'string') {
try { return normalizeRows_(JSON.parse(v)); } catch (e) { return []; }
}
if (typeof v === 'object') {
var vals = [];
for (var k in v) if (Object.prototype.hasOwnProperty.call(v, k)) vals.push(v[k]);
return vals;
}
return [];
}
var scrs = scrsJson || {};
if (typeof scrs === 'string') {
try { scrs = JSON.parse(scrs); } catch (e0) { scrs = {}; }
}
var trim = normalizeRows_(trimPlanJson);
var cash = cashInfo || {};
var minRequired = toNumber_(cash.cash_shortfall_min_krw) || 0;
var combo = normalizeRows_(scrs.selected_combo);
var optimalCombo = combo.reduce(function(s, r) { return s + (toNumber_(r.immediate_krw) || 0); }, 0);
var refTotal = trim.reduce(function(s, r) {
return s + (toNumber_(r.sell_amount_krw || r.trim_amount_krw || r.trimming_krw) || 0);
}, 0);
var coverageStatus;
if (minRequired <= 0) coverageStatus = 'NO_SHORTFALL';
else if (optimalCombo < minRequired) coverageStatus = 'UNCOVERED';
else if (optimalCombo > minRequired * 2) coverageStatus = 'OVER_SELL';
else coverageStatus = 'COVERED';
return {
formula_id: 'CASH_RECOVERY_DISPLAY_LOCK_V1',
min_required_krw: Math.round(minRequired),
optimal_combo_krw: Math.round(optimalCombo),
reference_total_krw: Math.round(refTotal),
coverage_status: coverageStatus,
display_mode: 'SHOW_MIN_OPTIMAL',
reference_label: '참고용 전체 후보 누적 — 주문 아님',
over_sell_warning: coverageStatus === 'OVER_SELL'
? 'OVER_SELL_WARNING: 최적조합(' + Math.round(optimalCombo/10000) + '만원)이 최소필요(' + Math.round(minRequired/10000) + '만원)의 2배 초과' : null,
shortfall_uncovered: coverageStatus === 'UNCOVERED'
? 'CASH_SHORTFALL_UNCOVERED: SCRS-V2 재실행 필요' : null
};
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL51] P1-B: DATA_QUALITY_GATE_V2 (DQG-V2)
// 데이터 완성도 필드충족률 기반 게이트 — 행수 카운트 폐기
// COMPLETE(≥90%) / PARTIAL(≥60%) / INSUFFICIENT(<60%)
// ═══════════════════════════════════════════════════════════════════════
/**
* calcDataQualityGateV2_
* 핵심 필드 충족률로 데이터 완성도 등급 산출.
* T+20=0건, trade_quality=0건 시 특수 경고 발동.
*/
function calcDataQualityGateV2_(hApex) {
var h = hApex || {};
var pa1 = ((h.alpha_lead_json || [])[0]) || {};
var tradeQualRecords = ((h.trade_quality_report_json || {}).records || []);
var tqFirst = tradeQualRecords[0] || {};
var alphaHist = (h.alpha_history_summary_json) || {};
var scrsV2 = (h.scrs_v2_json) || {};
var combo = scrsV2.selected_combo || [];
var cluster = (h.semiconductor_cluster_json) || {};
var alphaEval = (h.alpha_evaluation_window_json || []);
var firstAlpha = alphaEval[0] || {};
var pp0 = ((h.profit_preservation_json) || [])[0] || {};
var isValid = function(v) {
return v !== null && v !== undefined && v !== '-' && v !== 'PENDING' && v !== '';
};
// [R2-1c] 필드경로 버그 수정: 실재 데이터를 0으로 깔던 false-negative 제거.
// prediction: alpha_lead_json[0] → pa1_report_json(PA1 진짜 필드).
// cash: cash_shortfall_json.cash_shortfall_min_krw(None) → 직접키 h.cash_shortfall_min_krw.
// cluster: h.semiconductor_cluster_json → h.semiconductor_cluster_gate_json 또는 직접 필드.
// stop_loss: final_stop_price/stop_price(없는 키) → protected_stop_price/auto_trailing_stop.
// trade_quality/alpha_eval/pattern: 표본 필요 → PENDING 값으로 명시(분모 제외).
var pa1Report = h.pa1_report_json || {};
if (typeof pa1Report === 'string') { try { pa1Report = JSON.parse(pa1Report); } catch(e) { pa1Report = {}; } }
var pa1Rows = Array.isArray(pa1Report) ? pa1Report : (pa1Report.rows || []);
var pa1Row0 = pa1Rows[0] || {};
var clusterDirect = h.semiconductor_cluster_json || {};
if (typeof clusterDirect === 'string') { try { clusterDirect = JSON.parse(clusterDirect); } catch(e) { clusterDirect = {}; } }
var CATEGORIES = {
prediction: [pa1Row0.direction_confidence, pa1Row0.synthesis_verdict, pa1Row0.thesis_score, pa1Row0.antithesis_score],
trade_quality: [tqFirst.grade || 'PENDING', tqFirst.feedback_tag || 'PENDING', tqFirst.t5_return_pct, tqFirst.t20_vs_core_pct],
pattern: [(h.pattern_blacklist_auto_json || {}).status || 'PENDING', (h.pattern_blacklist_auto_json || {}).accumulated_poor_count],
["stop_loss"]: [pp0.auto_trailing_stop, pp0.protected_stop_price, pp0.profit_preservation_state],
cash: [h.settlement_cash_d2_krw, h.cash_floor_status, h.cash_shortfall_min_krw],
sell_engine: [scrsV2.emergency_level, (combo[0] || {}).immediate_qty, (combo[0] || {}).rebound_wait_qty],
cluster: [clusterDirect.cluster_state, clusterDirect.combined_pct],
alpha_eval: [firstAlpha.alpha_gate_verdict || 'PENDING', alphaHist.prediction_accuracy_rate]
};
var categoryScores = {};
Object.keys(CATEGORIES).forEach(function(cat) {
var fields = CATEGORIES[cat];
var filled = fields.filter(isValid).length;
categoryScores[cat] = Math.round(filled / fields.length * 100);
});
var catVals = Object.keys(categoryScores).map(function(k) { return categoryScores[k]; });
var overallPct = catVals.length > 0
? Math.round(catVals.reduce(function(s, v) { return s + v; }, 0) / catVals.length) : 0;
var grade = overallPct >= 90 ? 'COMPLETE' : overallPct >= 60 ? 'PARTIAL' : 'INSUFFICIENT';
var warnings = [];
var t20Count = toNumber_((alphaHist).t20_evaluation_count) || 0;
var tqCount = tradeQualRecords.length;
var accRate = alphaHist.prediction_accuracy_rate;
var t5Count = toNumber_(alphaHist.t5_match_count) || 0;
if (t20Count === 0) warnings.push('warn_t20_zero: T+20 평가 0건 — 장기 예측 신뢰도 미검증');
if (tqCount === 0) warnings.push('warn_quality_unverified: 거래 품질 기록 0건');
if (!isValid(accRate)) warnings.push('warn_accuracy_unknown: 예측 정확도 미산출(PENDING)');
if (t5Count < 5) warnings.push('warn_insufficient_samples: T+5 표본 ' + t5Count + '건(최소 5건 미달)');
return {
formula_id: 'DATA_QUALITY_GATE_V2',
overall_completeness_pct: overallPct,
completeness_grade: grade,
category_scores: categoryScores,
special_warnings: warnings,
t20_evaluation_count: t20Count,
trade_quality_record_count: tqCount,
prediction_accuracy_rate: accRate || null,
confidence_ceiling: grade === 'INSUFFICIENT'
? 'BUY_SELL_CONFIDENCE_LIMITED: 핵심 데이터 부족 — 신호 신뢰도 상한 경고' : null
};
}
/**
* addBusinessDays_: 영업일 기준 날짜 계산 (토·일 제외)
*/
function addBusinessDays_(startDate, days) {
var d = new Date(startDate.getTime());
var added = 0;
while (added < days) {
d.setDate(d.getDate() + 1);
var dow = d.getDay();
if (dow !== 0 && dow !== 6) added++;
}
return Utilities.formatDate(d, 'Asia/Seoul', 'yyyy-MM-dd');
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P2-1: DETERMINISTIC_SERVING_LOCK_ENGINE_V1 (DSLE-V1)
// 11단계 stage_token 잠금 + LLM 수치 생성 = 0 강제
// Direction D3: LLM 서빙 수치 생성 절대 금지
// ═══════════════════════════════════════════════════════════════════════
/**
* calcDeterministicServingLock_
* 11단계 파이프라인 각 단계의 status·checksum을 토큰으로 기록.
* integrity_checksum 불일치 시 INVALID_SERVING_OVERRIDE 자동 표시.
*/
function calcDeterministicServingLock_(hApex, capturedAtIso, now) {
var stages = [
{ id: 'Stage_01_freshness', key: 'data_freshness_status' },
{ id: 'Stage_02_intraday', key: 'intraday_scope' },
{ id: 'Stage_03_portfolio', key: 'cash_floor_status' },
{ id: 'Stage_04_macro', key: 'macro_risk_score' },
{ id: 'Stage_05_sell_radar', key: 'distribution_sell_detector_json' },
{ id: 'Stage_06_buy_gate', key: 'anti_late_entry_json' },
{ id: 'Stage_07_sell_priority', key: 'sell_candidates_json' },
{ id: 'Stage_08_cash_recovery', key: 'scrs_v2_json' },
{ id: 'Stage_09_rs_quality', key: 'rs_verdict' },
{ id: 'Stage_10_tick_norm', key: 'tick_normalized_prices_json' },
{ id: 'Stage_11_serving', key: 'order_blueprint_json' },
];
var tokens = [];
var blockDetected = false;
var blockReason = null;
for (var i = 0; i < stages.length; i++) {
var s = stages[i];
var value = hApex ? hApex[s.key] : null;
var status = (value !== null && value !== undefined) ? 'OK' : 'MISSING';
if (status === 'MISSING' && i < 4) {
blockDetected = true;
blockReason = blockReason || (s.id + '_MISSING');
}
tokens.push({
stage_id: s.id,
key: s.key,
status: status,
checksum: computeStringChecksum_(safeStringifyForChecksum_(value))
});
}
var tokenChecksum = computeStringChecksum_(safeStringifyForChecksum_(tokens));
return {
route_lock_status: blockDetected ? 'PARTIALLY_LOCKED' : 'FULLY_LOCKED',
stage_tokens: tokens,
integrity_checksum: tokenChecksum,
llm_serving_budget: {
max_tokens: 1000,
numeric_generation_allowed: 0,
constraint: 'LLM_SERVING_CONSTRAINT_V1'
},
block_reason: blockReason,
captured_at: capturedAtIso || null,
generated_at: now ? Utilities.formatDate(now, 'Asia/Seoul', 'yyyy-MM-dd HH:mm') : null,
formula_id: 'DETERMINISTIC_SERVING_LOCK_ENGINE_V1'
};
}