feat(deployment): Add deployment script and signal tracking system
배포 및 실전 운영 준비: 1. 배포 스크립트 (deploy.sh) - SSH 기반 자동 배포 - 원격 백업 생성 - nginx 자동 재시작 - 헬스 체크 2. Live Outcome Ledger (live_outcome_ledger.gs) - addSignal_(): 신호 기록 - updatePriceT5_(): T+5 가격 입력 - updatePriceT20_(): T+20 가격 + outcome 자동 계산 - calculateStats_(): 통계 계산 (win_rate, avg_margin) - checkCalibrationReady_(): CALIBRATED 전환 조건 확인 - calibrateIfReady_(): 자동 전환 (30개 신호 + 60% 승률) 3. 일일 추적 가이드 (DAILY_SIGNAL_TRACKING.md) - 신호 발생 시 → T+5 → T+20 프로세스 - 주간 리뷰 체크리스트 - 마일스톤 일정 (6주) - CALIBRATED 전환 조건 - honest_proof_score 개선 경로 배포 준비: - publish 폴더: 24MB (172개 파일) - appsettings.json: PostgreSQL 연결 설정됨 - MudBlazor UI: 반응형 대시보드 - GAS 함수: 7개 (P3~P6) 실전 운영: - 신호 수집 기간: 2026-06-25 ~ 2026-08-10 (6주) - 목표: 30개 신호 + win_rate >= 60% - 최종 목표: honest_proof_score 95.0 달성 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* 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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user