feat: Sprint-3 (펀더멘털 피드 완성, MDD 모니터링 구축, Gitea CI/CD 파이프라인 추가) (2026-06-13)

주요 변경 사항:
- tools/ingest_fundamental_raw.py 수정:
  * yfinance 패키지를 활용한 Yahoo Finance 펀더멘털 연동 파이프라인 전면 개편
  * FCF, OCF 및 순부채(totalDebt - totalCash) 자동 폴백 계산을 구현하여 40개 NULL 컬럼 수집 완성
- src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs 수정:
  * 일별 자산 및 MDD를 기록하는 logDailyAssetHistory_ 함수 구현 및 runDataFeed() 연동
- tools/build_realized_performance_v1.py 수정:
  * daily_history 탭으로부터 MDD_realized를 실시간 파싱하여 insufficient_data 제거
- .gitea/workflows/ci.yml 추가:
  * Gitea Actions 용 Spec 검증, 릴리즈 게이트 및 번들 빌드 자동화 파이프라인 구축
- docs/ROADMAP_WBS.md 수정:
  * WBS-2.1, WBS-3.4, WBS-5.1 과업의 체크박스를 완료[x] 상태로 갱신
- 검증 결과: npm run full-gate (55단계 릴리즈 게이트) PASS 검증 완료

Co-Authored-By: Antigravity AI <noreply@google.com>
This commit is contained in:
2026-06-13 14:31:40 +09:00
parent 64e6d54b67
commit eabacde438
5 changed files with 231 additions and 78 deletions
@@ -2556,9 +2556,82 @@ function runDataFeed() {
// F4: account_snapshot trailing stop 일괄 갱신
applyTrailingStopUpdates_();
// [WBS-3.4] 일별 자산 총액 및 고점, MDD를 기록
logDailyAssetHistory_(totalAssetKrw_, today);
// 개별 실행에서는 기존 연쇄를 유지하고, run_all() 모드에서는 상위 오케스트레이터가 다음 단계를 수행한다.
if (!isRunAllOrchestrated_()) {
runSectorFlow();
}
}
/**
* [WBS-3.4] 일별 자산 총액 및 고점, MDD를 기록한다.
*/
function logDailyAssetHistory_(totalAsset, todayStr) {
try {
if (!Number.isFinite(totalAsset) || totalAsset <= 0) {
Logger.log("[MDD_GUARD] totalAsset이 유효하지 않아 일별 기록을 건너뜁니다.");
return;
}
// daily_history 시트 획득 또는 생성
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("daily_history");
if (!sheet) {
sheet = ss.insertSheet("daily_history");
// 헤더 작성
sheet.appendRow(["Date", "Total_Asset_KRW", "Peak_Asset_KRW", "MDD_Pct"]);
Logger.log("[MDD_GUARD] daily_history 시트를 신규 생성하고 헤더를 작성했습니다.");
}
// 기존 데이터 읽기
var data = sheet.getDataRange().getValues();
// 오늘 날짜가 이미 존재하는지 체크 (중복 기록 방지)
var todayIndex = -1;
for (var i = 1; i < data.length; i++) {
var dateVal = data[i][0];
var dateStr = "";
if (dateVal instanceof Date) {
dateStr = Utilities.formatDate(dateVal, "Asia/Seoul", "yyyy-MM-dd");
} else {
dateStr = String(dateVal).trim();
}
if (dateStr === todayStr) {
todayIndex = i + 1; // 1-based index
break;
}
}
// 역사적 고점(Peak) 계산
var peakAsset = totalAsset;
for (var i = 1; i < data.length; i++) {
if (i + 1 === todayIndex) continue;
var assetVal = parseFloat(data[i][1]);
if (Number.isFinite(assetVal) && assetVal > peakAsset) {
peakAsset = assetVal;
}
}
// MDD 계산
var mddPct = 0.0;
if (peakAsset > 0) {
mddPct = parseFloat(((peakAsset - totalAsset) / peakAsset * 100).toFixed(2));
}
if (todayIndex > 0) {
// 이미 오늘 날짜가 있으면 해당 행 업데이트
sheet.getRange(todayIndex, 1, 1, 4).setValues([[todayStr, totalAsset, peakAsset, mddPct]]);
Logger.log("[MDD_GUARD] 오늘(" + todayStr + ") 자산 기록을 업데이트했습니다: Asset=" + totalAsset + ", Peak=" + peakAsset + ", MDD=" + mddPct + "%");
} else {
// 없으면 새 행 추가
sheet.appendRow([todayStr, totalAsset, peakAsset, mddPct]);
Logger.log("[MDD_GUARD] 오늘(" + todayStr + ") 자산 기록을 추가했습니다: Asset=" + totalAsset + ", Peak=" + peakAsset + ", MDD=" + mddPct + "%");
}
} catch(e) {
Logger.log("[MDD_GUARD] daily_history 기록 실패: " + e.message);
}
}