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
+111
View File
@@ -15,6 +15,7 @@ DEFAULT_HARNESS_JSON = ROOT / "Temp" / "prediction_improvement_harness.json"
DEFAULT_GATE_RESULT_JSON = ROOT / "Temp" / "engine_harness_gate_result.json"
DEFAULT_RULE_LIFECYCLE_JSON = ROOT / "Temp" / "rule_lifecycle_policy.json"
DEFAULT_STRATEGY_HARNESS_JSON = ROOT / "Temp" / "strategy_harness_v2.json"
DEFAULT_SECTOR_TREND_JSON = ROOT / "Temp" / "sector_trend_analysis_v1.json"
def _ensure_utf8_stdio() -> None:
@@ -64,6 +65,7 @@ def main() -> int:
result_json_path = Path(args.result_json_path)
rule_lifecycle_json_path = Path(args.rule_lifecycle_json_path)
strategy_harness_json_path = Path(args.strategy_harness_json_path)
sector_trend_json_path = DEFAULT_SECTOR_TREND_JSON
if not json_path.is_absolute():
json_path = ROOT / json_path
if not report_path.is_absolute():
@@ -138,6 +140,16 @@ def main() -> int:
],
["REPORT RENDERED OK", "PREDICTION_IMPROVEMENT_HARNESS_EXPORTED"],
),
(
"build_sector_trend_analysis_v1",
["python", "tools/build_sector_trend_analysis_v1.py"],
["SECTOR_TREND_ANALYSIS_V1"],
),
(
"build_etf_representative_monitor_v1",
["python", "tools/build_etf_representative_monitor_v1.py"],
["ETF_REPRESENTATIVE_MONITOR_V1"],
),
("validate_report_quality", ["python", "tools/validate_report_quality.py", str(report_path)], ["PASS: report quality validation"]),
("validate_specs", ["python", "tools/validate_specs.py"], ["VALIDATION OK"]),
("validate_harness_sync_markdown", ["python", "tools/validate_harness_sync.py", "--from-markdown", str(json_path), str(report_path)], ["MARKDOWN_SYNC_OK"]),
@@ -1710,6 +1722,105 @@ def main() -> int:
if not check87_ok:
failed = True
# CHECK_87B: SECTOR_TREND_ANALYSIS_V1 — ETF proxy + smart money lens exported
sector_path = ROOT / "Temp" / "sector_trend_analysis_v1.json"
sector_data = _load_json(sector_path)
sector_rows = sector_data.get("rows") if isinstance(sector_data, dict) else []
sector_summary = sector_data.get("summary") if isinstance(sector_data, dict) else {}
sector_source = sector_data.get("source") if isinstance(sector_data, dict) else {}
sector_gate = str(sector_data.get("gate") or "") if isinstance(sector_data, dict) else ""
first_sector = sector_rows[0] if isinstance(sector_rows, list) and sector_rows and isinstance(sector_rows[0], dict) else {}
sector_section_present = "sector_trend_analysis_v1" in section_names
sector_md_has_etf = False
if isinstance(op_report, dict):
for sec in report_sections or []:
if isinstance(sec, dict) and sec.get("name") == "sector_trend_analysis_v1":
md_text = str(sec.get("markdown") or "")
sector_md_has_etf = (
("Proxy_Ticker" in md_text or "ETF 프록시" in md_text)
and "최근 시계열" in md_text
and "포트폴리오 / 자금 맥락" in md_text
)
break
check87b_ok = (
isinstance(sector_data, dict)
and str(sector_data.get("formula_id") or "") == "SECTOR_TREND_ANALYSIS_V1"
and sector_gate == "PASS"
and isinstance(sector_rows, list)
and len(sector_rows) > 0
and isinstance(first_sector.get("proxy_ticker"), str)
and isinstance(first_sector.get("proxy_name"), str)
and "smart_money_direction" in first_sector
and "flow_alignment_state" in first_sector
and isinstance(sector_summary, dict)
and "trend_posture" in sector_summary
and isinstance(sector_data.get("timeline"), list)
and len(sector_data.get("timeline") or []) > 0
and isinstance(sector_source, dict)
and sector_section_present
and sector_md_has_etf
)
results.append({
"name": "CHECK_87B_SECTOR_TREND_ANALYSIS_V1",
"exit_code": 0 if check87b_ok else 1,
"output": (
f"sector_trend gate={sector_gate or 'MISSING'} rows={len(sector_rows) if isinstance(sector_rows, list) else 0} "
f"etf_proxy={first_sector.get('proxy_ticker', 'MISSING') if first_sector else 'MISSING'} "
f"section_present={sector_section_present}"
+ (" OK" if check87b_ok else " => FAIL — sector trend harness 재생성 필요")
),
})
if not check87b_ok:
failed = True
# CHECK_87C: ETF_REPRESENTATIVE_MONITOR_V1 — ETF proxy와 대표 종목의 지속 모니터링
etf_rep_path = ROOT / "Temp" / "etf_representative_monitor_v1.json"
etf_rep_data = _load_json(etf_rep_path)
etf_rep_rows = etf_rep_data.get("rows") if isinstance(etf_rep_data, dict) else []
etf_rep_summary = etf_rep_data.get("summary") if isinstance(etf_rep_data, dict) else {}
etf_rep_gate = str(etf_rep_data.get("gate") or "") if isinstance(etf_rep_data, dict) else ""
etf_rep_section_present = "etf_representative_monitor_v1" in section_names
etf_rep_md_has_monitor = False
if isinstance(op_report, dict):
for sec in report_sections or []:
if isinstance(sec, dict) and sec.get("name") == "etf_representative_monitor_v1":
md_text = str(sec.get("markdown") or "")
etf_rep_md_has_monitor = (
"대표 종목 추출 원칙" in md_text
and "구성비중" in md_text
and "대표 종목 모니터 테이블" in md_text
and "대표 종목 추세 미니차트" in md_text
)
break
check87c_ok = (
isinstance(etf_rep_data, dict)
and str(etf_rep_data.get("formula_id") or "") == "ETF_REPRESENTATIVE_MONITOR_V1"
and etf_rep_gate == "PASS"
and isinstance(etf_rep_rows, list)
and len(etf_rep_rows) > 0
and isinstance(etf_rep_rows[0], dict)
and "representative_basis" in etf_rep_rows[0]
and "constituent_weight" in etf_rep_rows[0]
and int(etf_rep_rows[0].get("representative_count") or 0) >= 3
and isinstance(etf_rep_rows[0].get("representatives"), list)
and len(etf_rep_rows[0].get("representatives") or []) >= 3
and isinstance(etf_rep_summary, dict)
and "buy_review_count" in etf_rep_summary
and etf_rep_section_present
and etf_rep_md_has_monitor
)
results.append({
"name": "CHECK_87C_ETF_REPRESENTATIVE_MONITOR_V1",
"exit_code": 0 if check87c_ok else 1,
"output": (
f"etf_rep_monitor gate={etf_rep_gate or 'MISSING'} rows={len(etf_rep_rows) if isinstance(etf_rep_rows, list) else 0} "
f"section_present={etf_rep_section_present}"
+ (" OK" if check87c_ok else " => FAIL — ETF 대표 종목 모니터 재생성 필요")
),
})
if not check87c_ok:
failed = True
# CHECK_88: effective_coverage_pct=100.0 (GAS+Python)
cov_path = ROOT / "Temp" / "harness_coverage_audit.json"
cov_data = _load_json(cov_path)