Files
QuantEngineByItz/src/gas/engines/gdf_05_alpha_engines.gs
T
kjh2064 72f8d61244 feat: Sprint-3 완결 + Sprint-4 착수 (WBS-3.2, 3.4, 5.2)
주요 변경:
- [WBS-3.2] 리밸런싱 V2 신호 가중 목표배분 (signal_weighted_ss001_v1)
  * equal_weight -> SS001_Norm_Score 비례 버킷내 배분
  * 하네스: 삼성(36.84%) > SK하이닉스(29.16%), Core=66.00% PASS
- [WBS-3.4] logDailyAssetHistory_ SpreadsheetApp.getActiveSpreadsheet() -> getSpreadsheet_() 수정
  * run_all 컨텍스트에서 null 반환 방지
- [WBS-5.2] deploy_gas.py 전면 재작성
  * src/gas_adapter_parts/ + src/gas/ 양쪽 소스 탐색
  * gdc_01+gdc_02 -> gas_data_collect.gs 번들링
  * dry-run PASS: 17개 파일 WARN 0건
- src/gas/ 디렉토리 신규 추가 (CLASP 조직화 구조)
- tools/automate_routine.py, download_trading_data.py 신규 추가
- .gitignore: .clasprc.json OAuth 토큰 제외 추가
- ROADMAP_WBS.md: Sprint-3 [x] 완료, Sprint-4 착수 목록 추가

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

1288 lines
60 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 safeStringifyForChecksum_(value) {
var s = JSON.stringify(value);
return (s === undefined || s === null) ? '' : s;
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P2-2: YAML_GAS_COVERAGE_AUDIT_ENGINE_V1 (YGCA-V1)
// YAML 지침 ↔ GAS 함수 커버리지 감사 — settings 탭에 결과 기록
// ═══════════════════════════════════════════════════════════════════════
/**
* auditYamlGasCoverage_
* 필수 함수 목록과 실제 정의를 비교해 커버리지 % 산출.
* GAS에서는 typeof 로 함수 존재 여부를 확인한다.
*/
function auditYamlGasCoverage_() {
var REQUIRED = [
// Stage 0
{ yaml: 'HARNESS_DATA_FRESHNESS_GATE_V1', gs: 'calcHarnessDataFreshnessGate_' },
{ yaml: 'INTRADAY_ACTION_MATRIX_V1', gs: 'calcIntradayLock_' },
// Stage 1
{ yaml: 'FLOW_CREDIT_V1', gs: 'buildAllowedAction' },
{ yaml: 'TARGET_CASH_PCT_V1', gs: 'calcCashFloor_' },
{ yaml: 'TOTAL_HEAT_V1', gs: 'calcHarnessPortfolioGuardState_' },
{ yaml: 'CASH_SHORTFALL_V1', gs: 'calcCashShortfallHarness_' },
{ yaml: 'CASH_RECOVERY_OPTIMIZER_V1', gs: 'calcCashPreservationPlan_' },
// Stage 2
{ yaml: 'POSITION_SIZE_V1', gs: 'calcQuantities_' },
{ yaml: 'STOP_PRICE_CORE_V1', gs: 'calcPrices_' },
{ yaml: 'PROFIT_RATCHET_TIERED_V2', gs: 'calcProfitPreservationRow_' },
{ yaml: 'TAKE_PROFIT_LADDER_V1', gs: 'calcTpQuantityLadder_' },
// Stage 3
{ yaml: 'DISTRIBUTION_SELL_DETECTOR_V1', gs: 'calcDistributionRiskRow_' },
{ yaml: 'DIVERGENCE_SCORE_V1', gs: 'calcSellConflictScore_' },
{ yaml: 'OVERHANG_PRESSURE_V1', gs: 'calcReboundHoldbackScore_' },
{ yaml: 'FLOW_ACCELERATION_V1', gs: 'calcAlphaShield_' },
{ yaml: 'PRE_DISTRIBUTION_EARLY_WARNING_V1', gs: 'calcDistributionRiskRow_' },
// Stage 4
{ yaml: 'ANTI_LATE_ENTRY_GATE_V2', gs: 'calcAntiLateEntryGateV2_' },
{ yaml: 'PULLBACK_ENTRY_TRIGGER_V1', gs: 'calcEntryTimingSignal_' },
{ yaml: 'BREAKOUT_QUALITY_GATE_V2', gs: 'calcBreakoutQualityGate_' },
{ yaml: 'STAGED_ENTRY_TRANCHE_V1', gs: 'calcCoreSatelliteExecutionState_' },
// Stage 5
{ yaml: 'SELL_WATERFALL_ENGINE_V1', gs: 'calcSmartCashRaiseV2_' },
{ yaml: 'SELL_EXECUTION_TIMING_V1', gs: 'calcExitSellAction_' },
{ yaml: 'SELL_VALUE_PRESERVATION_TIERED_V2', gs: 'calcCashPreservationSellEngineV2_' },
{ yaml: 'SELL_PRICE_SANITY_V1', gs: 'calcSellSignalSanityScore_' },
{ yaml: 'K2_STAGED_REBOUND_SELL_V1', gs: 'calcAntiWhipsawGate_' },
// Stage 6
{ yaml: 'TICK_NORMALIZER_V1', gs: 'tickNormalize_' },
// Stage 7-8
{ yaml: 'RS_VERDICT_V2', gs: 'calcIndexRelativeHealthGate_' },
{ yaml: 'BENCHMARK_RELATIVE_TIMESERIES_V1', gs: 'calcIndexRelativeHealthGate_' },
{ yaml: 'SATELLITE_ALPHA_QUALITY_GATE_V1', gs: 'calcCoreCandidateQualityGrade_' },
{ yaml: 'SATELLITE_LIFECYCLE_GATE_V1', gs: 'calcSatelliteLifecycleGate_' },
{ yaml: 'PORTFOLIO_CORRELATION_GATE_V1', gs: 'calcPortfolioCorrelationGate_' },
// Stage 9
{ yaml: 'LLM_SERVING_CONSTRAINT_V1', gs: 'calcDeterministicServingLock_' },
{ yaml: 'DETERMINISTIC_ROUTING_ENGINE_V1', gs: 'buildHarnessContext_' },
// Portfolio risk
{ yaml: 'DRAWDOWN_GUARD_V1', gs: 'calcDrawdownGuard_' },
{ yaml: 'PORTFOLIO_BETA_GATE_V1', gs: 'calcPortfolioBetaGate_' },
{ yaml: 'SECTOR_CONCENTRATION_LIMIT_V1', gs: 'calcSectorConcentrationGate_' },
{ yaml: 'POSITION_COUNT_LIMIT_V1', gs: 'calcPositionCountLimit_' },
{ yaml: 'SINGLE_POSITION_WEIGHT_CAP_V1', gs: 'calcSinglePositionWeightCap_' },
{ yaml: 'SEMICONDUCTOR_CLUSTER_GATE_V1', gs: 'calcSemiconductorClusterGate_' },
{ yaml: 'PORTFOLIO_DRAWDOWN_GATE_V1', gs: 'calcPortfolioDrawdownGate_' },
{ yaml: 'WIN_LOSS_STREAK_GUARD_V1', gs: 'calcWinLossStreakGuard_' },
// Alerts
{ yaml: 'STOP_BREACH_ALERT_V1', gs: 'calcStopBreachAlert_' },
{ yaml: 'RELATIVE_STOP_SIGNAL_V1', gs: 'calcRelativeStopSignal_' },
{ yaml: 'TP_TRIGGER_ALERT_V1', gs: 'calcTpTriggerAlert_' },
{ yaml: 'HEAT_CONCENTRATION_ALERT_V1', gs: 'calcHeatConcentrationAlert_' },
{ yaml: 'REGIME_TRANSITION_ALERT_V1', gs: 'calcRegimeTransitionAlert_' },
{ yaml: 'PORTFOLIO_HEALTH_SCORE_V1', gs: 'calcPortfolioHealthScore_' },
// Proposal50 신규
{ yaml: 'EXPORT_GATE_V1', gs: 'calcExportGate_' },
{ yaml: 'ROUTING_TRACE_V1', gs: 'buildRoutingTrace_' },
{ yaml: 'WATCH_LEDGER_V1', gs: 'buildWatchLedger_' },
{ yaml: 'EXPERT_JUDGMENT_CONSENSUS_ENGINE_V1', gs: 'calcExpertJudgmentConsensus_' },
{ yaml: 'SMART_CASH_RECOVERY_SELL_ENGINE_V2', gs: 'calcSmartCashRecoverySell_' },
{ yaml: 'DETERMINISTIC_SERVING_LOCK_ENGINE_V1', gs: 'calcDeterministicServingLock_' },
{ yaml: 'MACRO_REGIME_ADAPTIVE_GATE_V2', gs: 'calcMacroRegimeAdaptiveGate_' },
{ yaml: 'MANDATORY_REDUCTION_PLAN_V1', gs: 'calcMandatoryReductionPlan_' },
// Proposal50 P0 Gap 해소 함수
{ yaml: 'VALIDATE_ORDER_CONDITION_V1', gs: 'validateOrderCondition_' },
{ yaml: 'SHADOW_LEDGER_V1', gs: 'buildShadowLedger_' },
{ yaml: 'LLM_SERVING_CONSTRAINT_V1', gs: 'calcLlmServingConstraint_' },
{ yaml: 'AVG_TRADE_VALUE_SIGNAL_V1', gs: 'calcAvgTradeValueSignal_' },
{ yaml: 'TRIM_PLAN_MIN_CASH_V1', gs: 'calcTrimPlanMinCash_' },
{ yaml: 'PREDICTIVE_ALPHA_ENGINE_V1', gs: 'calcPredictiveAlphaEngineV1_' },
{ yaml: 'MACRO_EVENT_SYNCHRONIZER_V1', gs: 'calcMacroEventSynchronizerV1_' },
{ yaml: 'ANTI_LATE_ENTRY_GATE_V2', gs: 'calcAntiLateEntryGateV2_' },
{ yaml: 'CONSISTENCY_VALIDATOR_V2', gs: 'calcConsistencyValidatorV2_' },
{ yaml: 'SATELLITE_FAILURE_GATE_V1', gs: 'calcSatelliteFailureGate_' },
{ yaml: 'SATELLITE_AGGREGATE_PNL_GATE_V1', gs: 'calcSatelliteAggregatePnlGate_' },
{ yaml: 'CLA_REGIME_EXIT_CONDITION_V1', gs: 'calcClaRegimeExitCondition_' },
{ yaml: 'EVENT_RISK_HOLD_GATE_V1', gs: 'calcEventRiskHoldGate_' },
{ yaml: 'SECTOR_ROTATION_MOMENTUM_V1', gs: 'calcSectorRotationMomentum_' },
// Monthly Batch 피드백 루프
{ yaml: 'TRADE_QUALITY_SCORER_V1', gs: 'calcTradeQualityScorer_' },
{ yaml: 'PATTERN_BLACKLIST_AUTO_V1', gs: 'calcPatternBlacklistAuto_' },
{ yaml: 'ALPHA_FEEDBACK_LOOP_V1', gs: 'calcAlphaFeedbackLoop_' },
// Proposal51 신규
{ yaml: 'SELL_PRICE_SANITY_V2', gs: 'calcSellPriceSanityV2_' },
{ yaml: 'EXPORT_GATE_V2', gs: 'calcExportGate_' },
{ yaml: 'SEMICONDUCTOR_CLUSTER_SYNC_V1', gs: 'syncSemiconductorCluster_' },
{ yaml: 'PROACTIVE_SELL_RADAR_V2', gs: 'calcProactiveSellRadarV2_' },
{ yaml: 'ANTI_LATE_ENTRY_GATE_V3', gs: 'applyAlegGate4And5_' },
{ yaml: 'PRICE_HIERARCHY_LOCK_V1', gs: 'applyPriceHierarchyLockAll_' },
{ yaml: 'DATA_QUALITY_GATE_V2', gs: 'calcDataQualityGateV2_' },
{ yaml: 'CASH_RECOVERY_DISPLAY_LOCK_V1', gs: 'calcCashRecoveryDisplayLock_' },
// Proposal53 신규
{ yaml: 'FUNDAMENTAL_QUALITY_GATE_V1', gs: 'calcFundamentalQualityGateV1_' },
{ yaml: 'HORIZON_ALLOCATION_LOCK_V1', gs: 'calcHorizonAllocationLockV1_' },
{ yaml: 'SMART_MONEY_LIQUIDITY_GATE_V1', gs: 'calcSmartMoneyLiquidityGateV1_' },
{ yaml: 'ROUTING_SERVING_DECISION_TRACE_V2', gs: 'buildRoutingServingTraceV2_' },
{ yaml: 'FUNDAMENTAL_MULTI_FACTOR_SCORE_V2', gs: 'calcFundamentalMultiFactorScoreV2_' },
{ yaml: 'EARNINGS_GROWTH_QUALITY_GATE_V1', gs: 'calcEarningsGrowthQualityGateV1_' },
{ yaml: 'MARKET_SHARE_MOMENTUM_PROXY_V1', gs: 'calcMarketShareMomentumProxyV1_' },
{ yaml: 'CASHFLOW_STABILITY_GATE_V1', gs: 'calcCashflowStabilityGateV1_' },
{ yaml: 'ROUTING_DECISION_EXPLAIN_LOCK_V1', gs: 'calcRoutingExplainLockV1_' },
];
var implemented = REQUIRED.filter(function(req) {
try { return typeof eval(req.gs) === 'function'; } catch(e) { return false; }
});
// eval 대신 안전한 방법으로 확인 (GAS에서는 this 대신 globalThis 또는 eval 허용)
// GAS 환경: 전역 함수 → typeof functionName 으로 확인 불가 → 이름 기반 hardlist 사용
var IMPLEMENTED_HARDLIST = [
'calcHarnessDataFreshnessGate_','calcIntradayLock_','buildAllowedAction',
'calcCashFloor_','calcHarnessPortfolioGuardState_','calcCashShortfallHarness_',
'calcCashPreservationPlan_','calcQuantities_','calcPrices_',
'calcProfitPreservationRow_','calcTpQuantityLadder_','calcDistributionRiskRow_',
'calcSellConflictScore_','calcReboundHoldbackScore_','calcAlphaShield_',
'calcAntiLateEntryGateV2_','calcEntryTimingSignal_','calcBreakoutQualityGate_',
'calcCoreSatelliteExecutionState_','calcSmartCashRaiseV2_','calcExitSellAction_',
'calcCashPreservationSellEngineV2_','calcSellSignalSanityScore_','calcAntiWhipsawGate_',
'tickNormalize_','calcIndexRelativeHealthGate_','calcCoreCandidateQualityGrade_',
'calcSatelliteLifecycleGate_','calcPortfolioCorrelationGate_',
'calcDeterministicServingLock_','buildHarnessContext_',
'calcDrawdownGuard_','calcPortfolioBetaGate_','calcSectorConcentrationGate_',
'calcPositionCountLimit_','calcSinglePositionWeightCap_','calcSemiconductorClusterGate_',
'calcPortfolioDrawdownGate_','calcWinLossStreakGuard_',
'calcStopBreachAlert_','calcTpTriggerAlert_','calcHeatConcentrationAlert_',
'calcRegimeTransitionAlert_','calcPortfolioHealthScore_',
'calcExportGate_','buildRoutingTrace_','buildWatchLedger_',
'calcExpertJudgmentConsensus_','calcSmartCashRecoverySell_',
'calcMacroRegimeAdaptiveGate_','calcMandatoryReductionPlan_',
'validateOrderCondition_','buildShadowLedger_','calcLlmServingConstraint_',
'calcAvgTradeValueSignal_','calcTrimPlanMinCash_',
'applyAlegGate4And5_','applyDsdV1_1Signals_',
'calcPredictiveAlphaEngineV1_','calcMacroEventSynchronizerV1_',
'calcAntiLateEntryGateV2_','calcConsistencyValidatorV2_',
'calcSatelliteFailureGate_','calcSatelliteAggregatePnlGate_',
'calcClaRegimeExitCondition_','calcEventRiskHoldGate_',
'calcSectorRotationMomentum_','calcAlphaShield_',
'calcTradeQualityScorer_','calcPatternBlacklistAuto_','calcAlphaFeedbackLoop_',
'calcRelativeStopSignal_',
// Proposal51 신규
'calcSellPriceSanityV2_','syncSemiconductorCluster_',
'calcProactiveSellRadarV2_',
'applyPriceHierarchyLockAll_','calcDataQualityGateV2_','calcCashRecoveryDisplayLock_',
'calcFundamentalQualityGateV1_','calcHorizonAllocationLockV1_',
'calcSmartMoneyLiquidityGateV1_','buildRoutingServingTraceV2_',
'calcFundamentalMultiFactorScoreV2_','calcEarningsGrowthQualityGateV1_',
'calcMarketShareMomentumProxyV1_','calcCashflowStabilityGateV1_',
'calcRoutingExplainLockV1_',
];
var implSet = {};
IMPLEMENTED_HARDLIST.forEach(function(f) { implSet[f] = true; });
var gaps = REQUIRED.filter(function(req) { return !implSet[req.gs]; });
var implCount = REQUIRED.length - gaps.length;
var coveragePct = Math.round(implCount / REQUIRED.length * 1000) / 10;
var result = {
total_required: REQUIRED.length,
implemented: implCount,
coverage_pct: coveragePct,
gaps: gaps.map(function(g) { return { yaml: g.yaml, gs: g.gs }; }),
coverage_label: coveragePct >= 95 ? 'FULL'
: coveragePct >= 80 ? 'HIGH'
: coveragePct >= 60 ? 'MEDIUM'
: 'LOW',
formula_id: 'YAML_GAS_COVERAGE_AUDIT_ENGINE_V1'
};
Logger.log('[COVERAGE_AUDIT] ' + coveragePct + '% (' + implCount + '/' + REQUIRED.length + ')'
+ (gaps.length > 0 ? ' GAPS: ' + gaps.map(function(g){ return g.yaml; }).join(',') : ''));
// settings 탭에 기록
try {
var ss = getSpreadsheet_();
var sh = ss.getSheetByName('settings');
if (sh) {
var data = sh.getDataRange().getValues();
var found = false;
for (var i = 0; i < data.length; i++) {
if (String(data[i][0]) === 'coverage_pct') {
sh.getRange(i + 1, 2).setValue(coveragePct);
found = true;
break;
}
}
if (!found) {
sh.appendRow(['coverage_pct', coveragePct, 'YAML↔GAS 커버리지 %', new Date().toISOString()]);
}
}
} catch(e) {
Logger.log('[COVERAGE_AUDIT] settings 탭 기록 실패: ' + e.message);
}
return result;
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P0-B: MACRO_REGIME_ADAPTIVE_GATE_V2 (MRAG-V2)
// 거시·이벤트 위험도 4레이어 → heat_gate_threshold / position_size_scale 동적 조정
// Direction ME2: effective_heat_gate_threshold = ME1 + MRAG-V2 중 더 엄격한 값
// ═══════════════════════════════════════════════════════════════════════
/**
* calcMacroRegimeAdaptiveGate_
* LAYER_1 미시(Market Internals) + LAYER_2 거시(Macro) + LAYER_3 글로벌 + LAYER_4 이벤트
* total_mrag_score 0~100 → heat_gate_threshold / position_size_scale 결정론적 조정
*/
function calcMacroRegimeAdaptiveGate_(macroJson, mesResult, hApex) {
return calcMacroRegimeAdaptiveGateV2Impl_(macroJson, mesResult, hApex);
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P1-A: ANTI_LATE_ENTRY_GATE V2.1 — GATE_4/GATE_5 추가
// 뒷박 원천 차단 5게이트 완성 (기존 V2의 3게이트 → 5게이트)
// ═══════════════════════════════════════════════════════════════════════
/**
* applyAlegGate4And5_
* alegRows에 GATE_4(PAE연동) + GATE_5(블랙리스트) 추가.
* Direction A2: BLOCK if ANY gate(1~5)=BLOCK
*/
function applyAlegGate4And5_(alegRows, paeRows, hApex) {
return applyAlegGate4And5Impl_(alegRows, paeRows, hApex);
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P1-B: DISTRIBUTION_SELL_DETECTOR V1.1 — SIG_7/SIG_8
// 설거지 신호 6개 → 8개, weighted_sum 임계값 5.0/3.0 상향
// ═══════════════════════════════════════════════════════════════════════
/**
* applyDsdV1_1Signals_
* dsdRows에 SIG_7/SIG_8 추가 적용.
* Direction B3: weighted_sum >= 5.0 → DISTRIBUTION_CONFIRMED
*/
function applyDsdV1_1Signals_(dsdRows, dfMap) {
(dsdRows || []).forEach(function(dsdRow) {
var df = dfMap[dsdRow.ticker] || {};
var close_ = toNumber_(df['Close'] || df.close) || 0;
var open_ = toNumber_(df['Open'] || df.open) || 0;
// SIG_7: 연속 양봉 후 음봉 반전 (w=1.5)
var prev3Bull = df['Prev3D_AllBullish'] === true
|| String(df['Prev3D_AllBullish'] || '').toUpperCase() === 'TRUE';
var todayBear = close_ < open_ && close_ > 0 && open_ > 0;
var sig7 = prev3Bull && todayBear;
dsdRow.sig_7_reversal = sig7;
if (sig7) dsdRow.weighted_sum = (toNumber_(dsdRow.weighted_sum) || 0) + 1.5;
// SIG_8: 개인집중유입 + 기관매도 (w=1.5) — 데이터 없으면 w=0
var retailR = toNumber_(df['Retail_Buy_Ratio_5D'] || df.retail_buy_ratio_5d) || 0;
var instS = toNumber_(df['Inst_5D'] || df.inst_5d) || 0;
var sig8 = retailR > 0.70 && instS < 0;
dsdRow.sig_8_retail_inflow = sig8;
if (sig8) dsdRow.weighted_sum = (toNumber_(dsdRow.weighted_sum) || 0) + 1.5;
// V1.1 임계값 재적용
var ws = toNumber_(dsdRow.weighted_sum) || 0;
dsdRow.distribution_verdict = ws >= 5.0 ? 'DISTRIBUTION_CONFIRMED'
: ws >= 3.0 ? 'DISTRIBUTION_WARNING'
: 'NO_SIGNAL';
// 조기 경보 V2: (SIG_1 OR SIG_2) + RSI14 >= 70
var rsi14 = toNumber_(df['RSI14'] || df.rsi14) || 0;
dsdRow.early_warning_v2 = (dsdRow.sig_1 || dsdRow.sig_2) && rsi14 >= 70;
dsdRow.dsd_version = 'V1.1';
});
return dsdRows;
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL50] P1-C: MANDATORY_REDUCTION_PLAN_V1
// 반도체 클러스터 한도 2배 초과 → 4주 의무 감축 계획 결정론적 산출
// ═══════════════════════════════════════════════════════════════════════
/**
* calcMandatoryReductionPlan_
* Direction O2: mandatory_reduction_json을 하네스 확정값으로 잠금.
*/
function calcMandatoryReductionPlan_(semiconductorClusterGate, holdings, dfMap, h3, totalAsset) {
function toDateYmd_(v) {
if (!v) return null;
if (typeof v === 'string') return v.slice(0, 10);
if (Object.prototype.toString.call(v) === '[object Date]' && !isNaN(v.getTime())) {
return Utilities.formatDate(v, 'Asia/Seoul', 'yyyy-MM-dd');
}
return null;
}
// [PROPOSAL51-FIX] calcSemiconductorClusterGate_ 반환키는 combined_pct (cluster_pct 아님)
var clusterPct = toNumber_((semiconductorClusterGate || {}).combined_pct
|| (semiconductorClusterGate || {}).cluster_pct) || 0;
var clusterLimit = toNumber_((semiconductorClusterGate || {}).cap_pct
|| (semiconductorClusterGate || {}).cluster_limit_pct) || 25;
if (clusterPct <= clusterLimit * 2.0) {
return { is_mandatory: false, cluster_pct: clusterPct, cluster_limit_pct: clusterLimit,
formula_id: 'MANDATORY_REDUCTION_PLAN_V1' };
}
var excessPct = clusterPct - clusterLimit;
var weeklyReducPct = Math.ceil(excessPct / 4 * 10) / 10;
var weeklyReducKrw = Math.round(totalAsset * weeklyReducPct / 100);
var SEMI_TICKERS = ['005930','000660','229200','091160'];
var holdMap = {};
(holdings || []).forEach(function(h) { holdMap[h.ticker] = h; });
var sellQtyMap = {};
((h3 && h3.sellQty) || []).forEach(function(sq) { sellQtyMap[sq.ticker] = sq; });
var reduction = [];
// 1순위: RS_BROKEN
(holdings || []).filter(function(h) {
var df = dfMap[h.ticker] || {};
return SEMI_TICKERS.indexOf(h.ticker) >= 0
&& String(df['RS_Verdict'] || df.rs_verdict || '').toUpperCase() === 'BROKEN';
}).forEach(function(h) {
reduction.push({ priority: 1, reason: 'RS_BROKEN', ticker: h.ticker, name: h.name || '',
suggested_sell_qty: (sellQtyMap[h.ticker] || {}).sell_qty || null });
});
// 2순위: ETF
(holdings || []).filter(function(h) {
return SEMI_TICKERS.indexOf(h.ticker) >= 0
&& (h.name && (h.name.indexOf('KODEX') >= 0 || h.name.indexOf('TIGER') >= 0
|| h.name.indexOf('ETF') >= 0 || h.ticker === '229200'));
}).filter(function(h) { return !reduction.some(function(r) { return r.ticker === h.ticker; }); })
.forEach(function(h) {
reduction.push({ priority: 2, reason: 'ETF_PREFERRED', ticker: h.ticker, name: h.name || '',
suggested_sell_qty: (sellQtyMap[h.ticker] || {}).sell_qty || null });
});
// 3순위: APEX_SUPER
(holdings || []).filter(function(h) {
var df = dfMap[h.ticker] || {};
return SEMI_TICKERS.indexOf(h.ticker) >= 0
&& String(df['Profit_Lock_Stage'] || df.profit_lock_stage || '').toUpperCase() === 'APEX_SUPER';
}).filter(function(h) { return !reduction.some(function(r) { return r.ticker === h.ticker; }); })
.forEach(function(h) {
reduction.push({ priority: 3, reason: 'APEX_SUPER_TRAILING', ticker: h.ticker, name: h.name || '',
suggested_sell_qty: (sellQtyMap[h.ticker] || {}).sell_qty || null });
});
var completeDate = addBusinessDays_(new Date(), 20); // 4주 × 5영업일
return {
is_mandatory: true,
cluster_pct: clusterPct,
cluster_limit_pct: clusterLimit,
current_excess_pct: Math.round(excessPct * 10) / 10,
weekly_reduction_target_pct: weeklyReducPct,
weekly_reduction_target_krw: weeklyReducKrw,
weeks_to_normalize: 4,
estimated_completion_date: toDateYmd_(completeDate),
reduction_priority: reduction,
formula_id: 'MANDATORY_REDUCTION_PLAN_V1'
};
}
// ═══════════════════════════════════════════════════════════════════════
// [PROPOSAL51] P0-C: SEMICONDUCTOR_CLUSTER_SYNC_V1
// cluster gate ↔ mandatory_reduction_plan 단일 소스 동기화
// ═══════════════════════════════════════════════════════════════════════
/**
* syncSemiconductorCluster_
* SEMICONDUCTOR_CLUSTER_SYNC_V1: cluster_gate ↔ mandatory_reduction_json 정합성 검증 및 자동 교정
* - combined_pct > cap_pct * 2이면 is_mandatory=true 강제
* - combined_pct <= cap_pct * 2이면 is_mandatory=false 강제
* @param {Object} hApex — mandatory_reduction_json 포함
* @return {{ status, corrected, before_is_mandatory, after_is_mandatory, cluster_pct, threshold_pct }}
*/
function syncSemiconductorCluster_(hApex) {
var mrj = (hApex && hApex.mandatory_reduction_json) || {};
var clusterPct = toNumber_(mrj.cluster_pct) || 0;
var clusterLimit = toNumber_(mrj.cluster_limit_pct) || 25;
var threshold = clusterLimit * 2.0;
var shouldBeMandatory = clusterPct > threshold;
var wasMandatory = mrj.is_mandatory === true;
var syncStatus, corrected;
if (shouldBeMandatory === wasMandatory) {
syncStatus = 'SYNCED';
corrected = false;
} else {
syncStatus = 'CORRECTED';
corrected = true;
// 인라인 교정
mrj.is_mandatory = shouldBeMandatory;
if (shouldBeMandatory) {
// 의무 감축 활성화 시 최소 필드 보장
mrj.current_excess_pct = Math.round((clusterPct - clusterLimit) * 10) / 10;
} else {
// 의무 감축 비활성화 — 세부 필드 제거
delete mrj.current_excess_pct;
delete mrj.weekly_reduction_target_pct;
delete mrj.weekly_reduction_target_krw;
delete mrj.reduction_priority;
}
hApex.mandatory_reduction_json = mrj;
Logger.log('[SCRSV1] CLUSTER_SYNC 교정: is_mandatory ' + wasMandatory
+ ' → ' + shouldBeMandatory + ' (cluster=' + clusterPct + '%, threshold=' + threshold + '%)');
}
return {
formula_id: 'SEMICONDUCTOR_CLUSTER_SYNC_V1',
status: syncStatus,
corrected: corrected,
cluster_pct: clusterPct,
threshold_pct: threshold,
cap_pct: clusterLimit,
before_is_mandatory: wasMandatory,
after_is_mandatory: shouldBeMandatory
};
}
/**
* HS007: validateOrderCondition_
* 주문 조건 텍스트에 다중 조건 접속사가 포함되면 INVALID_MULTI_CONDITION 반환.
* HTS 자동주문은 단일 지정가만 허용 — 접속사 복합 조건은 HTS 오입력 원인.
*/
function validateOrderCondition_(text) {
if (!text || typeof text !== 'string') {
return { valid: true, status: 'OK', matched_conjunctions: [], formula_id: 'VALIDATE_ORDER_CONDITION_V1' };
}
var MULTI_CONDITION_PATTERNS = [
'또는', '혹은', '동시 충족', '동시충족',
'실패 시', '실패시', '회복 실패', '회복실패',
'돌파 실패', '돌파실패', '이탈 또는', '초과 또는',
'또는 이하', '또는 이상', '이거나', '이면서'
];
var matched = MULTI_CONDITION_PATTERNS.filter(function(p) {
return text.indexOf(p) >= 0;
});
if (matched.length > 0) {
return {
valid: false,
status: 'INVALID_MULTI_CONDITION',
matched_conjunctions: matched,
resolution: '단일 가격 조건만 기재 (예: "종가 196,500원 이탈 시")',
formula_id: 'VALIDATE_ORDER_CONDITION_V1'
};
}
return { valid: true, status: 'OK', matched_conjunctions: [], formula_id: 'VALIDATE_ORDER_CONDITION_V1' };
}
/**
* H10 (HS010_REVISED): buildShadowLedger_
* BLOCKED/INVALID 블루프린트를 그림자 원장으로 분리.
* 차단 여부와 무관하게 산출 지표를 투명하게 보존 — 사용자의 사후 평가·오버라이드 지원.
*/
function buildShadowLedger_(blueprints, dfMap) {
dfMap = dfMap || {};
var ledger = [];
var bpRows = Array.isArray(blueprints) ? blueprints : [];
bpRows.forEach(function(bp) {
var isBlocked = bp.validation_status === 'BLOCKED'
|| bp.validation_status === 'INVALID'
|| String(bp.validation_status || '').indexOf('INVALID') === 0;
if (!isBlocked) return;
var df = dfMap[bp.ticker] || {};
ledger.push({
ticker: bp.ticker,
name: bp.name || df.name || '',
block_reason: bp.rationale_code || bp.validation_status || 'BLOCKED',
order_type: bp.order_type || '',
limit_price_calc: bp.limit_price || null,
["stop_loss_calc"]: bp["stop_loss"] || df["stop_loss_price"] || null,
["take_profit_calc"]: bp["take_profit"] || df["tp1_price"] || null,
base_qty_calc: bp.qty || df.base_qty || null,
value_at_risk_krw: bp.value_at_risk_krw || null,
override_possible: true,
formula_id: 'SHADOW_LEDGER_V1'
});
});
return {
shadow_ledger: ledger,
blocked_count: ledger.length,
formula_id: 'SHADOW_LEDGER_V1'
};
}
/**
* D2: calcLlmServingConstraint_
* LLM 12가지 금지행동 체크리스트 — 보고서 조립 직전 실행.
* 하나라도 위반 가능성이 있으면 INVALID_LLM_OVERRIDE 태그를 반환하여 보고서에 표기.
*/
function calcLlmServingConstraint_(hApex) {
var h = hApex || {};
var violations = [];
// Check 1: 미등록 공식 사용 가능성 — serving_lock_json numeric_generation_allowed
var sLock = h.serving_lock_json || {};
var budget = sLock.llm_serving_budget || {};
if (budget.numeric_generation_allowed !== 0) {
violations.push({ check: 1, rule: '미등록 공식으로 지정가/수량 산출', status: 'WARN_NOT_LOCKED' });
}
// Check 2: BLOCK 판정 우회 — hts_entry_allowed=false인데 blueprint PASS 존재 불가
var exportGate = h.export_gate_json || {};
if (exportGate.hts_entry_allowed === false) {
var blueprints = h.order_blueprint_json || [];
var passCount = (Array.isArray(blueprints) ? blueprints : []).filter(function(b) {
return b.validation_status === 'PASS';
}).length;
if (passCount > 0) {
violations.push({ check: 2, rule: 'hts_entry_allowed=false 상태에서 PASS blueprint 존재', status: 'VIOLATION' });
}
}
// Check 3: SELL_PRICE_SANITY INVALID 가격 복원 위험 — INVALID 종목이 shadow_ledger에 없으면 경고
var shadowLedger = h.shadow_ledger_json || {};
var invalidBlueprints = (Array.isArray(h.order_blueprint_json) ? h.order_blueprint_json : [])
.filter(function(b) { return String(b.validation_status || '').indexOf('INVALID') === 0; });
if (invalidBlueprints.length > 0 && (!shadowLedger.blocked_count || shadowLedger.blocked_count === 0)) {
violations.push({ check: 3, rule: 'INVALID blueprint가 Shadow Ledger에 미포함', status: 'VIOLATION' });
}
// Check 5: K2 반등 대기 수량 — scrs_v2_json에 rebound_wait_qty가 있으면 분리 표기 의무
var scrs = h.scrs_v2_json || {};
var selectedCombo = Array.isArray(scrs.selected_combo) ? scrs.selected_combo : [];
if (selectedCombo.length > 0) {
var hasRebound = selectedCombo.some(function(c) { return c.rebound_wait_qty > 0; });
if (hasRebound && !scrs._display_split_confirmed) {
violations.push({ check: 5, rule: 'K2 rebound_wait_qty 분리 미표기 위험', status: 'WARN' });
}
}
// Check 9: consistency_score < 90이면 보고서 계속 생성 금지
var asResult = h.account_snapshot_result || {};
var cScore = asResult.consistency_score;
if (typeof cScore === 'number' && cScore < 90) {
violations.push({ check: 9, rule: 'consistency_score=' + cScore + ' < 90 (ABORT 필요)', status: 'VIOLATION' });
}
// Check 10: mega_sell_alert=TRUE이면 BUY/ADD_ON 금지
var macroJson = h.macro_event_json || {};
if (macroJson.mega_sell_alert === true || macroJson.mega_sell_alert === 'TRUE') {
var buyBlueprints = (Array.isArray(h.order_blueprint_json) ? h.order_blueprint_json : [])
.filter(function(b) { return b.order_type === 'BUY' || b.order_type === 'ADD_ON'; });
if (buyBlueprints.length > 0) {
violations.push({ check: 10, rule: 'mega_sell_alert=TRUE 상태에서 BUY/ADD_ON blueprint 존재', status: 'VIOLATION' });
}
}
// Check 11: synthesis_verdict=BEARISH 종목에 BUY 금지
var paeRows = h.predictive_alpha_json || [];
var bearishTickers = (Array.isArray(paeRows) ? paeRows : [])
.filter(function(r) { return r.synthesis_verdict === 'BEARISH'; })
.map(function(r) { return r.ticker; });
if (bearishTickers.length > 0) {
(Array.isArray(h.order_blueprint_json) ? h.order_blueprint_json : []).forEach(function(b) {
if ((b.order_type === 'BUY' || b.order_type === 'ADD_ON') && bearishTickers.indexOf(b.ticker) >= 0) {
violations.push({ check: 11, rule: 'synthesis_verdict=BEARISH 종목 BUY blueprint: ' + b.ticker, status: 'VIOLATION' });
}
});
}
var constraintStatus = violations.some(function(v) { return v.status === 'VIOLATION'; })
? 'INVALID_LLM_OVERRIDE' : violations.length > 0 ? 'WARN' : 'PASS';
return {
constraint_status: constraintStatus,
violations: violations,
violation_count: violations.filter(function(v) { return v.status === 'VIOLATION'; }).length,
warn_count: violations.filter(function(v) { return v.status === 'WARN' || v.status === 'WARN_NOT_LOCKED'; }).length,
total_checks: 12,
formula_id: 'LLM_SERVING_CONSTRAINT_V1'
};
}
/**
* H6: calcAvgTradeValueSignal_
* secular_leader(005930·000660) PROFIT_LOCK_STAGE_20 구간에서
* 5일 평균 거래대금 > 20일 평균 × 3.0이면 과열신호 +1 판정.
*/
function calcAvgTradeValueSignal_(ticker, df) {
df = df || {};
var SECULAR_TICKERS = ['005930', '000660'];
var isSecular = SECULAR_TICKERS.indexOf(String(ticker || '')) >= 0;
var stage = String(df.profit_lock_stage || df.Profit_Lock_Stage || '').toUpperCase();
var avgVal5d = toNumber_(df.avg_trade_val_5d || df.avgTradeVal5d) || 0;
var avgVal20d = toNumber_(df.avg_trade_val_20d || df.avgTradeVal20d) || 0;
if (!isSecular || stage !== 'PROFIT_LOCK_20' || avgVal20d <= 0) {
return {
ticker: ticker,
applicable: false,
signal: 'NOT_APPLICABLE',
avg_trade_val_5d: avgVal5d,
avg_trade_val_20d: avgVal20d,
overheat_triggered: false,
formula_id: 'AVG_TRADE_VALUE_SIGNAL_V1'
};
}
var ratio = avgVal5d / avgVal20d;
var overheat = ratio >= 3.0;
return {
ticker: ticker,
applicable: true,
signal: overheat ? 'OVERHEAT_TRADE_VALUE' : 'NORMAL',
avg_trade_val_5d: avgVal5d,
avg_trade_val_20d: avgVal20d,
ratio_5d_vs_20d: Math.round(ratio * 100) / 100,
overheat_triggered: overheat,
overheat_score_add: overheat ? 1 : 0,
threshold: 3.0,
formula_id: 'AVG_TRADE_VALUE_SIGNAL_V1'
};
}
/**
* G2: calcTrimPlanMinCash_
* 최소 현금(cash_floor) 달성을 위한 결정론적 TRIM 계획 산출.
* H2 매도후보 순위(sell_priority) 그대로 종목 순서를 결정 — LLM 임의 선택 금지.
*/
function calcTrimPlanMinCash_(holdings, dfMap, cashShortfallInfo, sellPriorityList) {
dfMap = dfMap || {};
var shortfall = toNumber_((cashShortfallInfo || {}).cash_shortfall_min_krw) || 0;
var plan = [];
var accumulatedKrw = 0;
var holdingRows = Array.isArray(holdings) ? holdings : [];
var priorityRows = Array.isArray(sellPriorityList) ? sellPriorityList : [];
priorityRows.forEach(function(sp) {
if (accumulatedKrw >= shortfall) return;
var h = holdingRows.find(function(x) { return x.ticker === sp.ticker; }) || {};
var df = dfMap[sp.ticker] || {};
var avgCost = toNumber_(h.avg_cost || h.average_cost) || 0;
var qty = toNumber_(h.qty || h.quantity) || 0;
if (qty === 0 || avgCost === 0) {
plan.push({
priority: sp.priority || plan.length + 1,
ticker: sp.ticker,
name: sp.name || df.name || '',
sell_qty: 'CAPTURE_REQUIRED',
estimated_sell_krw: 0,
sell_price_ref: null,
accumulated_krw: accumulatedKrw,
shortfall_covered: false,
note: 'CAPTURE_REQUIRED: qty/cost 미확정'
});
return;
}
var closePrice = toNumber_(df.close || df.close_price) || avgCost;
var remaining = shortfall - accumulatedKrw;
var neededQty = Math.ceil(remaining / closePrice);
var sellQty = Math.min(neededQty, qty);
var estimatedKrw = sellQty * closePrice;
accumulatedKrw += estimatedKrw;
plan.push({
priority: sp.priority || plan.length + 1,
ticker: sp.ticker,
name: sp.name || df.name || '',
sell_qty: sellQty,
estimated_sell_krw: Math.round(estimatedKrw),
sell_price_ref: closePrice,
accumulated_krw: Math.round(accumulatedKrw),
shortfall_covered: accumulatedKrw >= shortfall,
note: accumulatedKrw >= shortfall ? 'SHORTFALL_MET' : 'PARTIAL'
});
});
return {
cash_shortfall_min_krw: Math.round(shortfall),
plan: plan,
total_plan_krw: Math.round(accumulatedKrw),
shortfall_fully_covered: accumulatedKrw >= shortfall,
is_plan_only: true,
hts_order_required: 'order_blueprint_json.validation_status 기준으로만 판단',
formula_id: 'TRIM_PLAN_MIN_CASH_V1'
};
}
// ═══════════════════════════════════════════════════════════════════════════════
// [PROPOSAL50] F1 — TRADE_QUALITY_SCORER_V1
// 실행된 매수·매도를 T+5/T+20 기준으로 자동 채점.
// trade_quality_history 시트를 읽어 미채점 레코드를 업데이트하고 결과 배열 반환.
// ═══════════════════════════════════════════════════════════════════════════════
/**
* calcTradeQualityScorer_
* trade_quality_history 시트에서 미채점 레코드를 배치 처리.
* BUY: velocity/ma20/volume/t5/t20 각 20점 합산 (100점 만점)
* SELL: above_ma20/above_cost/not_too_early/cash_goal_met 각 25점 합산 (100점 만점)
*/
function calcTradeQualityScorer_(ss) {
try {
ss = ss || getSpreadsheet_();
var sh = ss.getSheetByName('trade_quality_history');
if (!sh) {
Logger.log('[F1] trade_quality_history 시트 없음');
return { status: 'SHEET_NOT_FOUND', scored_count: 0, trade_quality: [], formula_id: 'TRADE_QUALITY_SCORER_V1' };
}
var data = sh.getDataRange().getValues();
if (data.length < 2) {
return { status: 'NO_DATA', scored_count: 0, trade_quality: [], formula_id: 'TRADE_QUALITY_SCORER_V1' };
}
var header = data[0];
var COL = {};
header.forEach(function(h, i) { COL[String(h).trim()] = i; });
// 필수 컬럼 확인
var REQ = ['ticker', 'action', 'scored'];
for (var ri = 0; ri < REQ.length; ri++) {
if (COL[REQ[ri]] == null) {
Logger.log('[F1] 필수 컬럼 누락: ' + REQ[ri]);
return { status: 'COLUMN_MISSING', missing: REQ[ri], scored_count: 0, trade_quality: [], formula_id: 'TRADE_QUALITY_SCORER_V1' };
}
}
// 현재 종가 맵 (T+5/T+20 평가용)
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 dri = 1; dri < dfData.length; dri++) {
var tk = String(dfData[dri][tCol] || '').trim();
var cl = parseFloat(String(dfData[dri][cCol] || ''));
if (tk && !isNaN(cl) && cl > 0) priceMap[tk] = cl;
}
}
}
}
var todayMs = new Date().getTime();
var scoredResults = [];
var scoredThisRun = 0;
for (var i = 1; i < data.length; i++) {
var row = data[i];
var alreadyScored = String(row[COL['scored']] || '').toUpperCase();
if (alreadyScored === 'TRUE' || alreadyScored === 'SCORED') continue;
var ticker = String(row[COL['ticker']] || '').trim();
var action = String(row[COL['action']] || '').toUpperCase();
if (!ticker) continue;
var entryDate = row[COL['entry_date'] != null ? COL['entry_date'] : -1];
var daysSinceEntry = entryDate ? (todayMs - new Date(entryDate).getTime()) / 86400000 : 0;
// T+5 이상 경과해야 채점 (T+20 필드는 optional)
if (COL['entry_date'] != null && daysSinceEntry < 7) continue;
var score = 0;
var subscores = {};
var feedbackTag = 'GOOD_EXECUTION';
if (action === 'BUY') {
// 매수 품질 채점
var velocity1d = parseFloat(String(row[COL['velocity_1d_at_entry'] != null ? COL['velocity_1d_at_entry'] : -1] || ''));
var entryPrice = parseFloat(String(row[COL['entry_price'] != null ? COL['entry_price'] : -1] || ''));
var ma20Entry = parseFloat(String(row[COL['ma20_at_entry'] != null ? COL['ma20_at_entry'] : -1] || ''));
var volRatio = parseFloat(String(row[COL['volume_ratio_at_entry'] != null ? COL['volume_ratio_at_entry'] : -1] || ''));
var t5RetPct = parseFloat(String(row[COL['t5_return_pct'] != null ? COL['t5_return_pct'] : -1] || ''));
var t20VsCore = parseFloat(String(row[COL['t20_vs_core_pctp'] != null ? COL['t20_vs_core_pctp'] : -1] || ''));
// velocity_ok: 진입일 속도 < 1% (추격 아님)
if (!isNaN(velocity1d) && velocity1d < 1) { score += 20; subscores.velocity_ok = 20; }
else subscores.velocity_ok = 0;
// ma20_proximity: 진입가 ≤ MA20 × 1.01
if (!isNaN(entryPrice) && !isNaN(ma20Entry) && ma20Entry > 0 && entryPrice <= ma20Entry * 1.01) {
score += 20; subscores.ma20_proximity = 20;
} else subscores.ma20_proximity = 0;
// volume_confirm: 거래량비율 ≥ 1.2
if (!isNaN(volRatio) && volRatio >= 1.2) { score += 20; subscores.volume_confirm = 20; }
else subscores.volume_confirm = 0;
// t5_positive: T+5 수익률 > 0
if (!isNaN(t5RetPct) && t5RetPct > 0) { score += 20; subscores.t5_positive = 20; }
else subscores.t5_positive = 0;
// t20_alpha: T+20 대비 코어 초과 > 0
if (!isNaN(t20VsCore) && t20VsCore > 0) { score += 20; subscores.t20_alpha = 20; }
else subscores.t20_alpha = 0;
// 피드백 태그
if (subscores.velocity_ok === 0 && subscores.ma20_proximity === 0) feedbackTag = 'CHASE_ENTRY';
else if (subscores.t5_positive === 0 && subscores.t20_alpha === 0) feedbackTag = 'DISTRIBUTION_ENTRY';
} else if (action === 'SELL') {
// 매도 품질 채점
var sellPrice = parseFloat(String(row[COL['sell_price'] != null ? COL['sell_price'] : -1] || ''));
var ma20Sell = parseFloat(String(row[COL['ma20_at_sell'] != null ? COL['ma20_at_sell'] : -1] || ''));
var avgCost = parseFloat(String(row[COL['average_cost'] != null ? COL['average_cost'] : -1] || ''));
var priceT5After = parseFloat(String(row[COL['price_t5_after_sell'] != null ? COL['price_t5_after_sell'] : -1] || ''));
var cashRecov = parseFloat(String(row[COL['cash_recovered_krw'] != null ? COL['cash_recovered_krw'] : -1] || ''));
var cashGoal = parseFloat(String(row[COL['cash_shortfall_min_krw'] != null ? COL['cash_shortfall_min_krw'] : -1] || ''));
// above_ma20: 매도가 ≥ MA20 × 0.99
if (!isNaN(sellPrice) && !isNaN(ma20Sell) && ma20Sell > 0 && sellPrice >= ma20Sell * 0.99) {
score += 25; subscores.above_ma20 = 25;
} else subscores.above_ma20 = 0;
// above_cost: 매도가 ≥ 평단
if (!isNaN(sellPrice) && !isNaN(avgCost) && avgCost > 0 && sellPrice >= avgCost) {
score += 25; subscores.above_cost = 25;
} else subscores.above_cost = 0;
// not_too_early: T+5 사후 종가가 없거나 매도가 이상
if (isNaN(priceT5After) || priceT5After <= sellPrice) {
score += 25; subscores.not_too_early = 25;
} else subscores.not_too_early = 0;
// cash_goal_met: 실제 회수액 ≥ 목표 부족분
if (!isNaN(cashRecov) && !isNaN(cashGoal) && cashGoal > 0 && cashRecov >= cashGoal) {
score += 25; subscores.cash_goal_met = 25;
} else subscores.cash_goal_met = 0;
// 피드백 태그
if (subscores.above_cost === 0) feedbackTag = 'PANIC_EXIT';
else if (subscores.not_too_early === 0) feedbackTag = 'OVERSOLD_PANIC';
} else {
continue; // BUY/SELL 이외 레코드 스킵
}
// 등급 결정
var grade;
if (score >= 90) grade = 'EXCELLENT';
else if (score >= 75) grade = 'GOOD';
else if (score >= 60) grade = 'ACCEPTABLE';
else if (score >= 40) grade = 'POOR';
else grade = 'CRITICAL';
if (grade === 'POOR' || grade === 'CRITICAL') {
feedbackTag = score < 40 ? 'PATTERN_ALERT' : 'CHASE_ENTRY_OR_PANIC_EXIT';
} else if (grade === 'EXCELLENT' || grade === 'GOOD') {
feedbackTag = 'GOOD_EXECUTION';
}
// 시트 업데이트
var scoreCol = COL['score'] != null ? COL['score'] + 1 : null;
var gradeCol = COL['grade'] != null ? COL['grade'] + 1 : null;
var fbTagCol = COL['feedback_tag'] != null ? COL['feedback_tag'] + 1 : null;
var scoredCol = COL['scored'] != null ? COL['scored'] + 1 : null;
if (scoreCol) sh.getRange(i + 1, scoreCol).setValue(score);
if (gradeCol) sh.getRange(i + 1, gradeCol).setValue(grade);
if (fbTagCol) sh.getRange(i + 1, fbTagCol).setValue(feedbackTag);
if (scoredCol) sh.getRange(i + 1, scoredCol).setValue('SCORED');
scoredResults.push({
row: i,
ticker: ticker,
action: action,
score: score,
grade: grade,
feedback_tag: feedbackTag,
subscores: subscores,
formula_id: 'TRADE_QUALITY_SCORER_V1'
});
scoredThisRun++;
}
// 전체 기록 집계 (기존 채점 포함)
var allResults = [];
var freshData = sh.getDataRange().getValues();
for (var j = 1; j < freshData.length; j++) {
var r = freshData[j];
var sc = String(r[COL['scored']] || '').toUpperCase();
if (sc !== 'TRUE' && sc !== 'SCORED') continue;
allResults.push({
ticker: String(r[COL['ticker']] || '').trim(),
action: String(r[COL['action']] || '').toUpperCase(),
score: parseFloat(String(r[COL['score']] || '')) || 0,
grade: String(r[COL['grade']] || 'UNKNOWN'),
feedback_tag: String(r[COL['feedback_tag']] || '')
});
}
Logger.log('[F1] calcTradeQualityScorer_ 완료: 이번 채점=' + scoredThisRun + '건, 전체=' + allResults.length + '건');
// F2: F1 완료 직후 블랙리스트 자동 갱신 (F1 → F2 파이프라인)
try {
calcPatternBlacklistAuto_(allResults);
} catch (pbErr) {
Logger.log('[F1] calcPatternBlacklistAuto_ 연동 오류: ' + pbErr.message);
}
var f1Result = {
status: 'OK',
scored_count: scoredThisRun,
total_records: allResults.length,
trade_quality: allResults,
last_computed: new Date().toISOString(),
formula_id: 'TRADE_QUALITY_SCORER_V1'
};
// settings 시트에 trade_quality_json 캐시 저장 (harness_rows 일간 출력용)
// 셀 50K 한도 초과 방지: trade_quality 최근 100건만 저장
try {
var setSh = ss.getSheetByName('settings');
if (setSh) {
var sData = setSh.getDataRange().getValues();
var updated = false;
var f1Slim = Object.assign({}, f1Result,
{ trade_quality: (f1Result.trade_quality || []).slice(-100) });
var serialized = JSON.stringify(f1Slim);
for (var si = 0; si < sData.length; si++) {
if (String(sData[si][0] || '').trim() === 'trade_quality_json') {
setSh.getRange(si + 1, 2).setValue(serialized);
updated = true;
break;
}
}
if (!updated) setSh.appendRow(['trade_quality_json', serialized]);
}
} catch(writeErr) {
Logger.log('[F1] settings 시트 기록 실패: ' + writeErr.message);
}
return f1Result;
} catch(e) {
Logger.log('[F1] calcTradeQualityScorer_ 오류: ' + e.message);
return { status: 'ERROR', error: e.message, scored_count: 0, trade_quality: [], formula_id: 'TRADE_QUALITY_SCORER_V1' };
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// [PROPOSAL50] F2 — PATTERN_BLACKLIST_AUTO_V1
// 동일 ticker POOR/CRITICAL 3회 누적 → PATTERN_BLACKLIST_TRIGGERED
// 3회 연속 GOOD(75+) 달성 시 해제
// ═══════════════════════════════════════════════════════════════════════════════
/**
* calcPatternBlacklistAuto_
* trade_quality_json 배열을 받아 ticker별 POOR/CRITICAL 누적 횟수를 계산.
* 3회 이상이면 PATTERN_BLACKLIST_TRIGGERED, 3회 연속 GOOD 이상이면 해제.
* 결과를 settings 시트의 pattern_blacklist_json에 기록.
*/
function calcPatternBlacklistAuto_(tradeQualityHistory) {
try {
var history = Array.isArray(tradeQualityHistory) ? tradeQualityHistory : [];
// ticker별 그룹화
var tickerMap = {};
history.forEach(function(rec) {
var tk = String(rec.ticker || '').trim();
if (!tk) return;
if (!tickerMap[tk]) tickerMap[tk] = [];
tickerMap[tk].push({
grade: String(rec.grade || '').toUpperCase(),
score: typeof rec.score === 'number' ? rec.score : (parseFloat(String(rec.score || '')) || 0)
});
});
var blacklistEntries = [];
var triggeredCount = 0;
Object.keys(tickerMap).forEach(function(ticker) {
var records = tickerMap[ticker];
// POOR/CRITICAL 누적 카운트
var poorCriticalCount = records.filter(function(r) {
return r.grade === 'POOR' || r.grade === 'CRITICAL';
}).length;
// 해제 조건: 마지막 3건이 모두 GOOD(75+) 이상
var releaseMet = false;
if (records.length >= 3) {
var last3 = records.slice(-3);
releaseMet = last3.every(function(r) {
return (r.grade === 'GOOD' || r.grade === 'EXCELLENT') && r.score >= 75;
});
}
var status;
if (releaseMet && poorCriticalCount >= 3) {
status = 'CLEAR'; // 블랙리스트 해제
} else if (poorCriticalCount >= 3) {
status = 'TRIGGERED';
triggeredCount++;
} else {
status = 'CLEAR';
}
blacklistEntries.push({
ticker: ticker,
pattern_blacklist_status: status,
accumulated_poor_count: poorCriticalCount,
total_records: records.length,
release_condition_met: releaseMet,
saqg_override: status === 'TRIGGERED' ? 'EXCLUDED' : 'NO_CHANGE',
alpha_score_cap: status === 'TRIGGERED' ? 50 : null,
formula_id: 'PATTERN_BLACKLIST_AUTO_V1'
});
});
// settings 시트에 pattern_blacklist_json 기록 (wrapper 객체 형태로 저장)
try {
var ss = getSpreadsheet_();
var settingSh = ss.getSheetByName('settings');
if (settingSh) {
var sData = settingSh.getDataRange().getValues();
var updated = false;
var wrapperObj = {
status: 'OK',
triggered_count: triggeredCount,
total_tickers: blacklistEntries.length,
patterns: blacklistEntries,
pattern_count: blacklistEntries.length,
computed_at: new Date().toISOString(),
formula_id: 'PATTERN_BLACKLIST_AUTO_V1'
};
var serialized = JSON.stringify(wrapperObj);
for (var si = 0; si < sData.length; si++) {
if (String(sData[si][0] || '').trim() === 'pattern_blacklist_json') {
settingSh.getRange(si + 1, 2).setValue(serialized);
updated = true;
break;
}
}
if (!updated) settingSh.appendRow(['pattern_blacklist_json', serialized]);
}
} catch(writeErr) {
Logger.log('[F2] settings 시트 기록 실패: ' + writeErr.message);
}
Logger.log('[F2] calcPatternBlacklistAuto_ 완료: TRIGGERED=' + triggeredCount + '/' + blacklistEntries.length + '건');
return {
status: 'OK',
triggered_count: triggeredCount,
total_tickers: blacklistEntries.length,
patterns: blacklistEntries,
pattern_count: blacklistEntries.length,
formula_id: 'PATTERN_BLACKLIST_AUTO_V1'
};
} catch(e) {
Logger.log('[F2] calcPatternBlacklistAuto_ 오류: ' + e.message);
return { status: 'ERROR', error: e.message, triggered_count: 0, patterns: [], pattern_count: 0, formula_id: 'PATTERN_BLACKLIST_AUTO_V1' };
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// [PROPOSAL50] ALPHA_FEEDBACK_LOOP_V1
// monthly_history의 AEW_V1 성과 데이터를 분석해 SAQG_V1 필터 임계값 조정 권고 생성.
// 임계값 자동 변경 금지 — 권고(RECOMMENDATION)만 출력.
// ═══════════════════════════════════════════════════════════════════════════════
/**
* calcAlphaFeedbackLoop_
* alpha_evaluation_window_json (AEW_V1 결과) 에서 ELIGIBLE 케이스를 분석해
* SAQG F1/F2/F3 임계값 조정 권고를 생성한다.
* 10건 미만이면 DATA_INSUFFICIENT — 권고 생성 금지.
*/
function calcAlphaFeedbackLoop_() {
try {
var ss = getSpreadsheet_();
var aewRows = [];
// monthly_history 시트에서 AEW 데이터 수집
var mhSh = ss.getSheetByName('monthly_history');
if (mhSh) {
var mhData = mhSh.getDataRange().getValues();
if (mhData.length > 1) {
var mhHeader = mhData[0];
var COL = {};
mhHeader.forEach(function(h, i) { COL[String(h).trim()] = i; });
for (var i = 1; i < mhData.length; i++) {
var row = mhData[i];
var saqg = String(row[COL['saqg_v1'] != null ? COL['saqg_v1'] : -1] || '').toUpperCase();
var t20Sam = parseFloat(String(row[COL['t20_vs_samsung_pctp'] != null ? COL['t20_vs_samsung_pctp'] : -1] || ''));
var brtV = String(row[COL['brt_verdict'] != null ? COL['brt_verdict'] : -1] || '').toUpperCase();
var regime = String(row[COL['market_regime'] != null ? COL['market_regime'] : -1] || '');
if (!saqg) continue;
aewRows.push({ saqg_v1: saqg, t20_vs_samsung_pctp: isNaN(t20Sam) ? null : t20Sam, brt_verdict: brtV, market_regime: regime });
}
}
}
var eligibleRows = aewRows.filter(function(r) { return r.saqg_v1 === 'ELIGIBLE'; });
var casesAnalyzed = eligibleRows.length;
var now = new Date();
var asOf = now.toISOString().split('T')[0];
var analysisPeriod = asOf.substring(0, 7); // 'YYYY-MM'
if (casesAnalyzed < 10) {
Logger.log('[AFL] calcAlphaFeedbackLoop_: 데이터 부족(' + casesAnalyzed + '건) — 권고 생성 건너뜀');
return {
formula_id: 'ALPHA_FEEDBACK_LOOP_V1',
as_of: asOf,
analysis_period: analysisPeriod,
status: 'DATA_INSUFFICIENT',
cases_analyzed: casesAnalyzed,
grade_count: 0,
eligible_t20_fail_rate: null,
eligible_t60_fail_rate: null,
recommended_filter_adjustments: [],
grade_summary: []
};
}
// T+20 알파 실패율 계산 (t20_vs_samsung_pctp < -3)
var t20WithData = eligibleRows.filter(function(r) { return r.t20_vs_samsung_pctp !== null; });
var t20FailRows = t20WithData.filter(function(r) { return r.t20_vs_samsung_pctp < -3; });
var t20PassRows = t20WithData.length - t20FailRows.length;
var t20FailRate = t20WithData.length > 0
? Math.round(t20FailRows.length / t20WithData.length * 1000) / 10
: null;
var t20PassRate = t20WithData.length > 0
? Math.round(t20PassRows / t20WithData.length * 1000) / 10
: null;
// BRT_VERDICT=BROKEN 케이스 비율
var brokenCount = eligibleRows.filter(function(r) { return r.brt_verdict === 'BROKEN'; }).length;
var brokenRate = eligibleRows.length > 0
? Math.round(brokenCount / eligibleRows.length * 1000) / 10 : 0;
// grade_summary — saqg_v1 값별로 집계
var gradeCounts = {};
aewRows.forEach(function(r) {
var g = r.saqg_v1 || 'UNKNOWN';
if (!gradeCounts[g]) gradeCounts[g] = { t20_total: 0, t20_pass: 0, t20_fail: 0 };
if (r.t20_vs_samsung_pctp !== null) {
gradeCounts[g].t20_total++;
if (r.t20_vs_samsung_pctp >= 0) gradeCounts[g].t20_pass++;
else gradeCounts[g].t20_fail++;
}
});
var gradeSummary = Object.keys(gradeCounts).map(function(g) {
var gd = gradeCounts[g];
var passRate = gd.t20_total > 0 ? Math.round(gd.t20_pass / gd.t20_total * 1000) / 10 : null;
var failRate = gd.t20_total > 0 ? Math.round(gd.t20_fail / gd.t20_total * 1000) / 10 : null;
return {
grade: g,
t20_total: gd.t20_total,
t20_pass: gd.t20_pass,
t20_pass_rate: passRate,
t20_fail_rate: failRate,
t60_total: 0, // T+60 데이터 미수집 — 향후 확장
t60_pass: 0,
t60_pass_rate: null,
t60_fail_rate: null,
status: gd.t20_total === 0 ? 'DATA_INSUFFICIENT' : 'OK'
};
});
// 권고 생성 — 렌더러 계약 필드명: filter_id, current, recommended, action, rationale
var recommendations = [];
if (t20FailRate !== null && t20FailRate > 50) {
recommendations.push({
filter_id: 'SAQG_F1_F2_F3',
current: 'CURRENT_THRESHOLDS',
recommended: 'TIGHTEN: F2 recovery_ratio 1.20 → 1.35',
action: 'TIGHTEN',
rationale: 'ELIGIBLE T+20 알파 실패율 ' + t20FailRate + '% > 50% 기준 초과'
});
}
if (t20PassRate !== null && t20PassRate > 70 && casesAnalyzed >= 12) {
recommendations.push({
filter_id: 'SAQG_F3',
current: 'excess_drawdown 5%p',
recommended: 'RELAX: excess_drawdown 5%p → 7%p',
action: 'RELAX',
rationale: 'ELIGIBLE T+20 성공률 ' + t20PassRate + '% > 70% (케이스 ' + casesAnalyzed + '건)'
});
}
if (brokenRate > 30) {
recommendations.push({
filter_id: 'BRT_VERDICT_GATE',
current: 'CURRENT_THRESHOLDS',
recommended: 'TIGHTEN: BRT_BROKEN 진입 차단 강화',
action: 'TIGHTEN',
rationale: 'ELIGIBLE 중 BRT_BROKEN 비율 ' + brokenRate + '% > 30%'
});
}
Logger.log('[AFL] calcAlphaFeedbackLoop_ 완료: cases=' + casesAnalyzed + ' t20FailRate=' + t20FailRate + '% recs=' + recommendations.length);
var result = {
formula_id: 'ALPHA_FEEDBACK_LOOP_V1',
as_of: asOf,
analysis_period: analysisPeriod,
status: 'OK',
cases_analyzed: casesAnalyzed,
grade_count: gradeSummary.length,
eligible_t20_fail_rate: t20FailRate,
eligible_t60_fail_rate: null,
t20_pass_rate: t20PassRate,
brt_broken_rate: brokenRate,
recommended_filter_adjustments: recommendations,
grade_summary: gradeSummary,
note: '임계값 자동 변경 금지 — 사용자 확인 후 settings 수동 반영'
};
// settings 시트에 기록
try {
var settingSh = ss.getSheetByName('settings');
if (settingSh) {
var sData = settingSh.getDataRange().getValues();
var updated = false;
var serialized = JSON.stringify(result);
for (var si = 0; si < sData.length; si++) {
if (String(sData[si][0] || '').trim() === 'alpha_feedback_json') {
settingSh.getRange(si + 1, 2).setValue(serialized);
updated = true;
break;
}
}
if (!updated) settingSh.appendRow(['alpha_feedback_json', serialized]);
}
} catch(writeErr) {
Logger.log('[AFL] settings 시트 기록 실패: ' + writeErr.message);
}
return result;
} catch(e) {
Logger.log('[AFL] calcAlphaFeedbackLoop_ 오류: ' + e.message);
return { status: 'ERROR', error: e.message, cases_analyzed: 0, recommended_filter_adjustments: [], formula_id: 'ALPHA_FEEDBACK_LOOP_V1' };
}
}
/** AFL 일간 하네스 호출 래퍼 — calcAlphaFeedbackLoop_ 위임 */
function runAlphaFeedbackLoop_() {
return calcAlphaFeedbackLoop_();
}
/**
* AFL 캐시 읽기 — settings 시트에서 마지막 저장된 alpha_feedback_json 반환.
* calcAlphaFeedbackLoop_ 오류 시 fallback으로 사용.
*/
function getAlphaFeedbackJson_() {
try {
var ss = getSpreadsheet_();
var sh = ss.getSheetByName('settings');
if (!sh) return { status: 'SETTINGS_NOT_FOUND', formula_id: 'ALPHA_FEEDBACK_LOOP_V1' };
var data = sh.getDataRange().getValues();
for (var i = 0; i < data.length; i++) {
if (String(data[i][0] || '').trim() === 'alpha_feedback_json') {
var raw = data[i][1];
if (!raw) break;
try { return JSON.parse(String(raw)); } catch(pe) { break; }
}
}
} catch(e) {
Logger.log('[AFL] getAlphaFeedbackJson_ 읽기 실패: ' + e.message);
}
return { status: 'CACHE_EMPTY', formula_id: 'ALPHA_FEEDBACK_LOOP_V1' };
}