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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user