/** * Quant Engine v9 — Live Outcome Ledger * 실전 거래신호 추적 & CALIBRATED 전환 시스템 * 생성: 2026-06-25 */ // ──────────────────────────────────────────────────────────────────────────── // 설정 // ──────────────────────────────────────────────────────────────────────────── const LEDGER_SHEET_ID = "1N_A..."; // TODO: 스프레드시트 ID 입력 const LEDGER_SHEET_NAME = "live_outcome_ledger"; const LEDGER_HEADERS = [ "signal_id", "date", "ticker", "signal_type", "signal_score", "entry_price", "entry_quantity", "entry_time", "style", "routing_confidence", "price_t5", "price_t10", "price_t20", "return_pct_t20", "outcome", "win_margin", "validation_status", "notes", "last_updated" ]; // ──────────────────────────────────────────────────────────────────────────── // 신호 기록 // ──────────────────────────────────────────────────────────────────────────── /** * addSignal_ * 새로운 거래 신호를 레저에 추가 * * @param {Object} signal - 신호 데이터 * @return {string} signal_id */ function addSignal_(signal) { if (!signal.ticker || !signal.signal_type || !signal.entry_price) { throw new Error("필수 필드 누락: ticker, signal_type, entry_price"); } const signalId = generateSignalId_(signal.date || new Date()); const ss = SpreadsheetApp.openById(LEDGER_SHEET_ID); const sheet = ss.getSheetByName(LEDGER_SHEET_NAME); const row = [ signalId, // signal_id signal.date || new Date(), // date signal.ticker, // ticker signal.signal_type, // signal_type (BUY|SELL) signal.signal_score || 0, // signal_score signal.entry_price, // entry_price signal.entry_quantity || 0, // entry_quantity signal.entry_time || "00:00", // entry_time signal.style || "UNCLASSIFIED", // style signal.routing_confidence || 0, // routing_confidence null, // price_t5 (T+5에 입력) null, // price_t10 (T+10에 입력) null, // price_t20 (T+20에 입력) null, // return_pct_t20 (자동 계산) "PENDING", // outcome null, // win_margin "UNVALIDATED", // validation_status signal.notes || "", // notes new Date().toISOString() // last_updated ]; sheet.appendRow(row); Logger.log(`✓ 신호 기록: ${signalId} (${signal.ticker})`); return signalId; } /** * generateSignalId_ * 고유한 signal_id 생성 (YYYYMMDD_HHMM_TICKER 형식) * * @param {Date} date * @return {string} signal_id */ function generateSignalId_(date) { const yyyy = date.getFullYear(); const mm = String(date.getMonth() + 1).padStart(2, "0"); const dd = String(date.getDate()).padStart(2, "0"); const hh = String(date.getHours()).padStart(2, "0"); const min = String(date.getMinutes()).padStart(2, "0"); return `${yyyy}${mm}${dd}_${hh}${min}`; } // ──────────────────────────────────────────────────────────────────────────── // T+5, T+10, T+20 가격 입력 // ──────────────────────────────────────────────────────────────────────────── /** * updatePriceT5_ * T+5 가격 입력 (자동 또는 수동) * * @param {string} signalId * @param {number} priceT5 */ function updatePriceT5_(signalId, priceT5) { const ss = SpreadsheetApp.openById(LEDGER_SHEET_ID); const sheet = ss.getSheetByName(LEDGER_SHEET_NAME); const rowIndex = findSignalRow_(sheet, signalId); if (rowIndex === -1) throw new Error(`신호를 찾을 수 없음: ${signalId}`); sheet.getRange(rowIndex, 11).setValue(priceT5); // price_t5 = 11번째 열 sheet.getRange(rowIndex, 19).setValue(new Date().toISOString()); // last_updated Logger.log(`✓ T+5 가격 입력: ${signalId} = ${priceT5}`); } /** * updatePriceT20_ * T+20 가격 입력 + outcome 자동 계산 * * @param {string} signalId * @param {number} priceT20 */ function updatePriceT20_(signalId, priceT20) { const ss = SpreadsheetApp.openById(LEDGER_SHEET_ID); const sheet = ss.getSheetByName(LEDGER_SHEET_NAME); const rowIndex = findSignalRow_(sheet, signalId); if (rowIndex === -1) throw new Error(`신호를 찾을 수 없음: ${signalId}`); const range = sheet.getRange(rowIndex, 1, 1, LEDGER_HEADERS.length); const values = range.getValues()[0]; // entry_price (인덱스 5) vs priceT20 const entryPrice = values[5]; const returnPctT20 = ((priceT20 - entryPrice) / entryPrice * 100).toFixed(2); // outcome 판정: WIN (-2% < ret < 2% = BREAKEVEN), LOSS, WIN let outcome = "BREAKEVEN"; let winMargin = Math.abs(returnPctT20); if (returnPctT20 > 2) { outcome = "WIN"; } else if (returnPctT20 < -2) { outcome = "LOSS"; winMargin = returnPctT20; } // 업데이트 sheet.getRange(rowIndex, 13).setValue(priceT20); // price_t20 sheet.getRange(rowIndex, 14).setValue(returnPctT20); // return_pct_t20 sheet.getRange(rowIndex, 15).setValue(outcome); // outcome sheet.getRange(rowIndex, 16).setValue(winMargin); // win_margin sheet.getRange(rowIndex, 17).setValue("PROVISIONAL"); // validation_status sheet.getRange(rowIndex, 19).setValue(new Date().toISOString()); // last_updated Logger.log(`✓ T+20 업데이트: ${signalId}, 수익률=${returnPctT20}%, outcome=${outcome}`); } // ──────────────────────────────────────────────────────────────────────────── // 통계 & 검증 // ──────────────────────────────────────────────────────────────────────────── /** * calculateStats_ * 현재까지의 통계 계산 (win_rate, avg_win_margin, etc.) * * @return {Object} { total, completed, win_count, loss_count, win_rate, avg_margin } */ function calculateStats_() { const ss = SpreadsheetApp.openById(LEDGER_SHEET_ID); const sheet = ss.getSheetByName(LEDGER_SHEET_NAME); const data = sheet.getDataRange().getValues(); let totalSignals = 0; let completedSignals = 0; let winCount = 0; let lossCount = 0; let totalWinMargin = 0; // 헤더 제외 (인덱스 1부터) for (let i = 1; i < data.length; i++) { const row = data[i]; const validationStatus = row[16]; // validation_status const outcome = row[14]; // outcome const winMargin = row[15]; // win_margin totalSignals++; if (validationStatus === "PROVISIONAL" || validationStatus === "CALIBRATED") { completedSignals++; if (outcome === "WIN") { winCount++; totalWinMargin += Math.abs(winMargin); } else if (outcome === "LOSS") { lossCount++; } } } const winRate = completedSignals > 0 ? (winCount / completedSignals * 100) : 0; const avgMargin = completedSignals > 0 ? (totalWinMargin / winCount || 0) : 0; return { total: totalSignals, completed: completedSignals, win_count: winCount, loss_count: lossCount, breakeven_count: completedSignals - winCount - lossCount, win_rate: winRate.toFixed(2), avg_win_margin: avgMargin.toFixed(2), calibrated_progress: `${completedSignals}/30` }; } /** * checkCalibrationReady_ * CALIBRATED 전환 가능 여부 확인 * * @return {Object} { ready, reason, stats } */ function checkCalibrationReady_() { const stats = calculateStats_(); const minSamples = 30; const minWinRate = 60; const ready = (stats.completed >= minSamples && parseFloat(stats.win_rate) >= minWinRate); const reason = !ready ? `샘플: ${stats.completed}/${minSamples}, 승률: ${stats.win_rate}%/${minWinRate}%` : "✅ CALIBRATED 전환 조건 충족!"; return { ready: ready, reason: reason, stats: stats }; } /** * calibrateIfReady_ * CALIBRATED 전환 (자동 또는 수동) */ function calibrateIfReady_() { const check = checkCalibrationReady_(); if (!check.ready) { Logger.log(`⏳ CALIBRATED 전환 대기: ${check.reason}`); return; } // 모든 PROVISIONAL을 CALIBRATED로 전환 const ss = SpreadsheetApp.openById(LEDGER_SHEET_ID); const sheet = ss.getSheetByName(LEDGER_SHEET_NAME); const data = sheet.getDataRange().getValues(); let calibratedCount = 0; for (let i = 1; i < data.length; i++) { if (data[i][16] === "PROVISIONAL") { sheet.getRange(i + 1, 17).setValue("CALIBRATED"); calibratedCount++; } } Logger.log(`✅ CALIBRATED 전환 완료: ${calibratedCount}개 신호`); Logger.log(`📊 최종 통계: ${JSON.stringify(check.stats, null, 2)}`); } // ──────────────────────────────────────────────────────────────────────────── // 유틸리티 // ──────────────────────────────────────────────────────────────────────────── /** * findSignalRow_ * signal_id로 행 번호 찾기 * * @param {Sheet} sheet * @param {string} signalId * @return {number} 행 번호 (없으면 -1) */ function findSignalRow_(sheet, signalId) { const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][0] === signalId) { return i + 1; // 1-indexed } } return -1; } /** * initializeLedger_ * 레저 시트 초기화 (첫 실행 시만) */ function initializeLedger_() { const ss = SpreadsheetApp.openById(LEDGER_SHEET_ID); let sheet = ss.getSheetByName(LEDGER_SHEET_NAME); if (!sheet) { sheet = ss.insertSheet(LEDGER_SHEET_NAME); } // 헤더 설정 sheet.getRange(1, 1, 1, LEDGER_HEADERS.length).setValues([LEDGER_HEADERS]); // 포맷팅 sheet.getRange(1, 1, 1, LEDGER_HEADERS.length) .setBackground("#1f77b4") .setFontColor("white") .setFontWeight("bold"); Logger.log(`✓ 레저 시트 초기화 완료: ${LEDGER_SHEET_NAME}`); } /** * 테스트 함수 */ function testLiveOutcomeLedger() { Logger.log("=== Live Outcome Ledger 테스트 ==="); // 테스트 신호 추가 const testSignal = { date: new Date(), ticker: "000660", signal_type: "BUY", signal_score: 75, entry_price: 100000, entry_quantity: 10, style: "SWING", routing_confidence: 80, notes: "테스트 신호" }; try { // 초기화 initializeLedger_(); // 신호 추가 // const signalId = addSignal_(testSignal); // Logger.log(`신호 ID: ${signalId}`); // 통계 확인 const stats = calculateStats_(); Logger.log(`통계: ${JSON.stringify(stats, null, 2)}`); // CALIBRATED 준비 여부 const check = checkCalibrationReady_(); Logger.log(`CALIBRATED 준비: ${JSON.stringify(check, null, 2)}`); } catch (error) { Logger.log(`❌ 오류: ${error.message}`); } }