feat: sector trend analysis + ETF representative monitor (DAG step_count 81->83)

- src/quant_engine/sector_trend_analysis.py: ETF proxy 기반 11개 섹터 동향 + smart money lens
- src/quant_engine/etf_representative_monitor.py: ETF 대표 종목 8개 추적 + 벤치마크 연동
- tools/build_sector_trend_analysis_v1.py: SECTOR_TREND_ANALYSIS_V1 Temp JSON 생성
- tools/build_etf_representative_monitor_v1.py: ETF_REPRESENTATIVE_MONITOR_V1 Temp JSON 생성
- tools/update_workbook_sector_insights.py: Google Sheets 섹터 인사이트 동기화
- spec/41_release_dag.yaml: step_count 81->83, wave_1에 2개 신규 노드 등록
- validate_engine_harness_gate.py: CHECK_87B (SECTOR_TREND_ANALYSIS_V1) + ETF monitor DAG 스텝 추가
- render_operational_report.py: sector_trend_analysis_v1 / etf_representative_monitor_v1 / portfolio_performance_summary 섹션 추가
- gas_lib.gs: doPost + syncSectorInsightSheets_ (섹터 인사이트 GAS 동기화 엔드포인트)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 20:52:17 +09:00
parent e5ef9f1d3b
commit f56dd37286
16 changed files with 2227 additions and 6 deletions
+156 -1
View File
@@ -1,5 +1,5 @@
// gas_lib.gs - Common utilities & static features
// Last Updated: 2026-06-14 17:23:33 KST
// Last Updated: 2026-06-14 20:48:30 KST
// Math/KRX utils, sheet I/O, sector flow, Web API, static runners
// GAS global scope: functions in gas_data_feed.gs / gas_data_collect.gs callable directly
//
@@ -2081,6 +2081,161 @@ function doGet(e) {
.setMimeType(ContentService.MimeType.JSON);
}
function doPost(e) {
const payload = parseJsonPostBody_(e);
const action = String(payload.action || payload.view || "").trim().toLowerCase();
try {
if (action === "sync_sector_insights") {
const result = syncSectorInsightSheets_(payload);
return ContentService
.createTextOutput(JSON.stringify(result, null, 2))
.setMimeType(ContentService.MimeType.JSON);
}
return ContentService
.createTextOutput(JSON.stringify({
status: "ERROR",
message: `unsupported action: ${action || "missing"}`,
}, null, 2))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService
.createTextOutput(JSON.stringify({
status: "ERROR",
message: String(err && err.message ? err.message : err),
}, null, 2))
.setMimeType(ContentService.MimeType.JSON);
}
}
function parseJsonPostBody_(e) {
try {
const raw = String(e?.postData?.contents ?? "").trim();
if (!raw) return {};
const parsed = JSON.parse(raw);
return parsed && typeof parsed === "object" ? parsed : {};
} catch (err) {
return {};
}
}
function rowFromObject_(headers, obj) {
return headers.map(function(h) {
const v = obj && Object.prototype.hasOwnProperty.call(obj, h) ? obj[h] : "";
if (v === null || v === undefined) return "";
if (typeof v === "object") return JSON.stringify(v);
return v;
});
}
function writeSummarySheet_(sheetName, rows) {
const headers = ["section", "key", "value"];
const tableRows = (rows || []).map(function(r) {
return [r.section || "", r.key || "", r.value || ""];
});
writeToSheet(sheetName, headers, tableRows);
return tableRows.length;
}
function writeSectorTrendAnalysisSheet_(analysis) {
if (!analysis || typeof analysis !== "object") return 0;
const summary = analysis.summary || {};
const concentration = analysis.concentration || {};
const detailHeaders = [
"sector", "proxy_ticker", "proxy_name", "proxy_type", "etf_code",
"etf_execution_use", "etf_liquidity_score", "etf_liquidity_status", "etf_nav_risk",
"proxy_confidence", "rank", "rank_delta_w1", "rank_delta_w2", "sector_score",
"score_delta", "sector_ret5d", "sector_ret20d", "etf_return_5d", "etf_return_20d",
"sector_etf_ret_gap_5d", "sector_etf_ret_gap_20d", "smart_money_5d_krw_raw",
"smart_money_20d_krw_raw", "smart_money_direction", "liquidity_direction",
"flow_alignment_state", "momentum_state", "concentration_weight_pct"
];
const detailRows = Array.isArray(analysis.rows)
? analysis.rows.map(function(r) { return rowFromObject_(detailHeaders, r); })
: [];
writeSummarySheet_("sector_trend_summary", [
{ section: "summary", key: "formula_id", value: analysis.formula_id || "" },
{ section: "summary", key: "gate", value: analysis.gate || "" },
{ section: "summary", key: "latest_snapshot_date", value: analysis.latest_snapshot_date || "" },
{ section: "summary", key: "previous_snapshot_date", value: analysis.previous_snapshot_date || "" },
{ section: "summary", key: "sector_count", value: analysis.sector_count || 0 },
{ section: "summary", key: "trend_posture", value: summary.trend_posture || "" },
{ section: "summary", key: "rising_count", value: summary.rising_count || 0 },
{ section: "summary", key: "fading_count", value: summary.fading_count || 0 },
{ section: "summary", key: "stable_count", value: summary.stable_count || 0 },
{ section: "summary", key: "etf_proxy_count", value: summary.etf_proxy_count || 0 },
{ section: "summary", key: "smart_money_inflow_count", value: summary.smart_money_inflow_count || 0 },
{ section: "summary", key: "smart_money_outflow_count", value: summary.smart_money_outflow_count || 0 },
{ section: "concentration", key: "top_sector", value: concentration.top_sector || "" },
{ section: "concentration", key: "top_sector_weight_pct", value: concentration.top_sector_weight_pct || 0 },
{ section: "concentration", key: "top2_weight_pct", value: concentration.top2_weight_pct || 0 },
{ section: "concentration", key: "concentration_gate", value: concentration.concentration_gate || "" },
]);
writeToSheet("sector_trend_analysis", detailHeaders, detailRows);
const timelineHeaders = [
"snapshot_date", "sector_count", "avg_sector_score", "top_sector", "top_sector_score",
"positive_breadth_count", "liquidity_warn_count", "net_smart_money_5d_krw"
];
const timelineRows = Array.isArray(analysis.timeline)
? analysis.timeline.map(function(r) { return rowFromObject_(timelineHeaders, r); })
: [];
writeToSheet("sector_trend_timeline", timelineHeaders, timelineRows);
return detailRows.length;
}
function writeEtfRepresentativeMonitorSheet_(monitor) {
if (!monitor || typeof monitor !== "object") return 0;
const summary = monitor.summary || {};
const detailHeaders = [
"sector", "etf_proxy_ticker", "etf_proxy_name", "etf_proxy_type", "sector_rank",
"sector_score", "sector_smart_money_5d_krw", "sector_ret20d", "representative_count",
"representative_ticker", "representative_name", "representative_basis",
"representative_basis_detail", "constituent_weight", "basket_quality_state",
"basket_coverage_pct", "basket_state", "basket_buy_review_count",
"basket_track_count", "basket_watch_count", "basket_caution_count",
"basket_aligned_count", "basket_missing_count", "basket_real_count",
"selection_source", "selection_score", "monitor_reason", "representatives_json"
];
const detailRows = Array.isArray(monitor.rows)
? monitor.rows.map(function(r) {
const repJson = Array.isArray(r.representatives) ? JSON.stringify(r.representatives) : "";
const base = Object.assign({}, r, { representatives_json: repJson });
return rowFromObject_(detailHeaders, base);
})
: [];
writeSummarySheet_("etf_representative_summary", [
{ section: "summary", key: "formula_id", value: monitor.formula_id || "" },
{ section: "summary", key: "gate", value: monitor.gate || "" },
{ section: "summary", key: "etf_sector_count", value: monitor.etf_sector_count || 0 },
{ section: "summary", key: "tracked_count", value: monitor.tracked_count || 0 },
{ section: "summary", key: "buy_review_count", value: summary.buy_review_count || 0 },
{ section: "summary", key: "track_count", value: summary.track_count || 0 },
{ section: "summary", key: "watch_count", value: summary.watch_count || 0 },
{ section: "summary", key: "caution_count", value: summary.caution_count || 0 },
{ section: "summary", key: "aligned_count", value: summary.aligned_count || 0 },
{ section: "summary", key: "weighted_basis_count", value: summary.weighted_basis_count || 0 },
{ section: "summary", key: "fallback_basis_count", value: summary.fallback_basis_count || 0 },
{ section: "summary", key: "complete_basket_count", value: summary.complete_basket_count || 0 },
{ section: "summary", key: "partial_basket_count", value: summary.partial_basket_count || 0 },
{ section: "summary", key: "basket_missing_total", value: summary.basket_missing_total || 0 },
]);
writeToSheet("etf_representative_monitor", detailHeaders, detailRows);
return detailRows.length;
}
function syncSectorInsightSheets_(payload) {
const trend = payload.sector_trend_analysis || payload.sectorTrendAnalysis || null;
const etf = payload.etf_representative_monitor || payload.etfRepresentativeMonitor || null;
const written = {};
if (trend) written.sector_trend_analysis = writeSectorTrendAnalysisSheet_(trend);
if (etf) written.etf_representative_monitor = writeEtfRepresentativeMonitorSheet_(etf);
return {
status: "OK",
action: "sync_sector_insights",
written,
generated_at: Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd HH:mm:ss") + " KST",
};
}
// ── Sheets → JSON 변환 헬퍼 ───────────────────────────────────────────────
function parseCompactFlag_(value) {
const raw = String(value ?? "").trim().toLowerCase();