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:
@@ -7,17 +7,25 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.quant_engine.etf_representative_monitor import build_etf_representative_monitor
|
||||
from src.quant_engine.sector_trend_analysis import build_sector_trend_analysis
|
||||
|
||||
SECTION_ORDER = [
|
||||
"exec_safety_declaration", "final_judgment_table", "final_execution_decision",
|
||||
"concise_hts_input_sheet", "watch_breakout_gate",
|
||||
"single_conclusion", "immediate_execution_playbook", "market_context_learning_note",
|
||||
"investment_quality_headline", "operational_truth_score",
|
||||
"portfolio_performance_summary",
|
||||
"portfolio_sector_exposure_summary",
|
||||
"sector_trend_analysis_v1", "etf_representative_monitor_v1", "investment_quality_headline", "operational_truth_score",
|
||||
"execution_readiness_matrix", "pass_100_criteria",
|
||||
"today_decision_summary_card", "routing_serving_trace",
|
||||
"export_gate_diagnosis", "QEH_AUDIT_BLOCK",
|
||||
@@ -48,6 +56,10 @@ SECTION_TITLES = {
|
||||
"single_conclusion": "단일 결론",
|
||||
"immediate_execution_playbook": "즉시 실행 플레이북",
|
||||
"market_context_learning_note": "시장 컨텍스트 학습 노트",
|
||||
"portfolio_performance_summary": "포트폴리오 성과 요약",
|
||||
"portfolio_sector_exposure_summary": "포트폴리오 섹터 노출",
|
||||
"sector_trend_analysis_v1": "섹터 동향 분석",
|
||||
"etf_representative_monitor_v1": "ETF 대표 종목 모니터",
|
||||
"investment_quality_headline": "투자 품질 헤드라인",
|
||||
"operational_truth_score": "운영 진실성 점수",
|
||||
"execution_readiness_matrix": "실행 준비도 매트릭스",
|
||||
@@ -142,6 +154,34 @@ def _first_keys(items: list, n: int = 6) -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
def _num(value: Any, default: float = 0.0) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _sparkline(values: list[Any]) -> str:
|
||||
points: list[float] = []
|
||||
for value in values:
|
||||
try:
|
||||
points.append(float(value))
|
||||
except Exception:
|
||||
continue
|
||||
if not points:
|
||||
return "n/a"
|
||||
lo = min(points)
|
||||
hi = max(points)
|
||||
bars = "▁▂▃▄▅▆▇█"
|
||||
if hi == lo:
|
||||
return bars[len(bars) // 2] * len(points)
|
||||
out = []
|
||||
for value in points:
|
||||
idx = int(round((value - lo) / (hi - lo) * (len(bars) - 1)))
|
||||
out.append(bars[max(0, min(len(bars) - 1, idx))])
|
||||
return "".join(out)
|
||||
|
||||
|
||||
# ── PHASE-0 렌더러 ────────────────────────────────────────────────────────────
|
||||
|
||||
def _exec_safety_declaration(hctx: dict, se: list) -> str:
|
||||
@@ -263,6 +303,283 @@ def _market_context_learning_note(hctx: dict, se: list) -> str:
|
||||
return _kv(rows)
|
||||
|
||||
|
||||
def _portfolio_performance_summary(data_root: dict, hctx: dict, se: list) -> str:
|
||||
data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
|
||||
daily = _sj(data.get("daily_history", []))
|
||||
monthly = _sj(data.get("monthly_history", []))
|
||||
account = _sj(data.get("account_snapshot", []))
|
||||
if not isinstance(daily, list):
|
||||
daily = []
|
||||
if not isinstance(monthly, list):
|
||||
monthly = []
|
||||
if not isinstance(account, list):
|
||||
account = []
|
||||
|
||||
latest_daily = daily[-1] if daily else {}
|
||||
latest_month = monthly[-1] if monthly else {}
|
||||
latest_capture = ""
|
||||
latest_holdings: list[dict[str, Any]] = []
|
||||
for row in account:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
cap = str(row.get("captured_at", "") or "")
|
||||
if cap and cap >= latest_capture:
|
||||
latest_capture = cap
|
||||
if latest_capture:
|
||||
latest_holdings = [r for r in account if isinstance(r, dict) and str(r.get("captured_at", "") or "") == latest_capture]
|
||||
|
||||
asset_series = []
|
||||
mdd_series = []
|
||||
monthly_return_series = []
|
||||
for row in daily[-10:]:
|
||||
if isinstance(row, dict):
|
||||
asset_series.append(row.get("Total_Asset_KRW", row.get("total_asset_krw", "")))
|
||||
mdd_series.append(row.get("MDD_Pct", row.get("mdd_pct", "")))
|
||||
for row in monthly[-10:]:
|
||||
if isinstance(row, dict):
|
||||
monthly_return_series.append(row.get("Actual_Return_Pct", row.get("actual_return_pct", "")))
|
||||
|
||||
rows = [
|
||||
("최신 일간 자산", latest_daily.get("Total_Asset_KRW", latest_daily.get("total_asset_krw", ""))),
|
||||
("최신 일간 MDD(%)", latest_daily.get("MDD_Pct", latest_daily.get("mdd_pct", ""))),
|
||||
("최신 월간 자산", latest_month.get("Total_Asset", latest_month.get("total_asset", ""))),
|
||||
("최신 월간 실현 수익률(%)", latest_month.get("Actual_Return_Pct", latest_month.get("actual_return_pct", ""))),
|
||||
("최신 월간 MoM 수익률(%)", latest_month.get("MoM_Return_Pct", latest_month.get("mom_return_pct", ""))),
|
||||
("최신 월간 YTD 수익률(%)", latest_month.get("YTD_Return_Pct", latest_month.get("ytd_return_pct", ""))),
|
||||
("최신 스냅샷 시각", latest_capture or hctx.get("captured_at", "")),
|
||||
("최신 보유 수", len(latest_holdings)),
|
||||
]
|
||||
md = "## 포트폴리오 성과 요약\n\n" + _kv(rows)
|
||||
md += "\n\n**일간 자산 추이** \n" + _sparkline(asset_series)
|
||||
md += "\n\n**일간 MDD 추이** \n" + _sparkline(mdd_series)
|
||||
md += "\n\n**월간 수익률 추이** \n" + _sparkline(monthly_return_series)
|
||||
if latest_holdings:
|
||||
md += "\n\n**최신 보유 상위 스냅샷**\n\n"
|
||||
md += _tbl(latest_holdings[:10], ["name", "ticker", "holding_quantity", "market_value", "return_pct"], max_rows=10)
|
||||
else:
|
||||
md += "\n\n_최신 보유 스냅샷 없음_"
|
||||
return md
|
||||
|
||||
|
||||
def _sector_trend_analysis_v1(data_root: dict, hctx: dict, se: list) -> str:
|
||||
inner_data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
|
||||
payload = {"data": inner_data, "data_root": data_root, "_harness_context": hctx}
|
||||
result = build_sector_trend_analysis(payload)
|
||||
if not isinstance(result, dict) or not result:
|
||||
return _err(se, "sector_trend_analysis_v1", "sector trend analysis unavailable")
|
||||
summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
|
||||
concentration = result.get("concentration") if isinstance(result.get("concentration"), dict) else {}
|
||||
rows = [
|
||||
("최신 스냅샷", result.get("latest_snapshot_date", "")),
|
||||
("이전 스냅샷", result.get("previous_snapshot_date", "")),
|
||||
("섹터 수", result.get("sector_count", "")),
|
||||
("ETF 프록시 섹터 수", summary.get("etf_proxy_count", "")),
|
||||
("상승 섹터 수", summary.get("rising_count", "")),
|
||||
("하락 섹터 수", summary.get("fading_count", "")),
|
||||
("정체 섹터 수", summary.get("stable_count", "")),
|
||||
("탑아웃 섹터 수", summary.get("topping_out_count", "")),
|
||||
("양(+) breadth", summary.get("positive_breadth_count", "")),
|
||||
("스마트자금 유입", summary.get("smart_money_inflow_count", "")),
|
||||
("스마트자금 유출", summary.get("smart_money_outflow_count", "")),
|
||||
("수급 정렬", summary.get("flow_aligned_count", "")),
|
||||
("수급 이탈", summary.get("flow_diverging_count", "")),
|
||||
("프록시 저신뢰", summary.get("low_proxy_confidence_count", "")),
|
||||
("트렌드 포지션", summary.get("trend_posture", "")),
|
||||
("집중 섹터", concentration.get("top_sector", "")),
|
||||
("집중도 Top1%", concentration.get("top_sector_weight_pct", "")),
|
||||
("집중도 Top2%", concentration.get("top2_weight_pct", "")),
|
||||
]
|
||||
md = _kv(rows)
|
||||
md += "\n\n**ETF/수급 교차 진단**\n\n"
|
||||
md += _kv([
|
||||
("ETF 프록시 커버리지(%)", result.get("source", {}).get("proxy_coverage_pct", "")),
|
||||
("유동성 경고 섹터", ", ".join(summary.get("outflow_warning_sectors", [])[:3]) if isinstance(summary.get("outflow_warning_sectors"), list) else ""),
|
||||
("스마트머니 강세", ", ".join(summary.get("strong_smart_money_sectors", [])[:3]) if isinstance(summary.get("strong_smart_money_sectors"), list) else ""),
|
||||
])
|
||||
md += "\n\n**최근 시계열 추세**\n\n"
|
||||
timeline = result.get("timeline") if isinstance(result.get("timeline"), list) else []
|
||||
if timeline:
|
||||
recent_timeline = timeline[-6:]
|
||||
md += _tbl(recent_timeline, [
|
||||
"snapshot_date", "sector_count", "avg_sector_score", "top_sector",
|
||||
"top_sector_score", "positive_breadth_count", "liquidity_warn_count",
|
||||
"net_smart_money_5d_krw",
|
||||
], max_rows=6)
|
||||
score_line = _sparkline([r.get("avg_sector_score") for r in recent_timeline])
|
||||
money_line = _sparkline([r.get("net_smart_money_5d_krw") for r in recent_timeline])
|
||||
md += "\n\n| 추세 | 그래프 |\n| --- | --- |\n"
|
||||
md += f"| 섹터 평균 점수 | {score_line} |\n"
|
||||
md += f"| 5D 스마트머니 합계 | {money_line} |\n"
|
||||
else:
|
||||
md += "_시계열 데이터 없음_"
|
||||
md += "\n\n**섹터 상위 유입/경고**\n\n"
|
||||
md += _kv([
|
||||
("상위 유입", ", ".join(summary.get("top_inflow_sectors", [])[:3]) or "없음"),
|
||||
("경고 섹터", ", ".join(summary.get("outflow_warning_sectors", [])[:3]) or "없음"),
|
||||
("강한 수급", ", ".join(summary.get("strong_smart_money_sectors", [])[:3]) or "없음"),
|
||||
])
|
||||
rows_data = result.get("rows") if isinstance(result.get("rows"), list) else []
|
||||
if rows_data:
|
||||
md += "\n\n**섹터 상세 트렌드**\n\n" + _tbl(rows_data, [
|
||||
"sector", "proxy_ticker", "proxy_name", "proxy_type", "etf_execution_use",
|
||||
"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",
|
||||
"flow_breadth_5d_raw", "liquidity_direction", "flow_alignment_state",
|
||||
"alert_level", "decision_use", "momentum_state", "concentration_weight_pct",
|
||||
], max_rows=20)
|
||||
history_rows = data_root.get("data", {}).get("sector_flow_history", [])
|
||||
if isinstance(history_rows, list) and history_rows:
|
||||
sector_histories: dict[str, list[dict[str, Any]]] = {}
|
||||
for item in history_rows:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
sector = str(item.get("Sector") or "").strip()
|
||||
if not sector:
|
||||
continue
|
||||
sector_histories.setdefault(sector, []).append(item)
|
||||
tracked = [r.get("sector") for r in rows_data[:6] if r.get("sector")]
|
||||
spark_rows = []
|
||||
for sector in tracked:
|
||||
series = sorted(sector_histories.get(sector, []), key=lambda r: str(r.get("Snapshot_Date") or ""))
|
||||
latest_row = next((r for r in rows_data if r.get("sector") == sector), {})
|
||||
spark_rows.append({
|
||||
"sector": sector,
|
||||
"score_trend": _sparkline([r.get("Sector_Score") for r in series[-6:]]),
|
||||
"smart_money_trend": _sparkline([r.get("SmartMoney_5D_KRW") for r in series[-6:]]),
|
||||
"latest_score": series[-1].get("Sector_Score", "") if series else "",
|
||||
"latest_smart_money_5d": series[-1].get("SmartMoney_5D_KRW", "") if series else "",
|
||||
"sector_ret20d": latest_row.get("sector_ret20d", ""),
|
||||
"smart_money_direction": latest_row.get("smart_money_direction", ""),
|
||||
"flow_alignment_state": latest_row.get("flow_alignment_state", ""),
|
||||
})
|
||||
if spark_rows:
|
||||
md += "\n\n**섹터별 시계열 그래프**\n\n"
|
||||
md += _tbl(spark_rows, [
|
||||
"sector", "score_trend", "smart_money_trend", "latest_score", "latest_smart_money_5d",
|
||||
"sector_ret20d", "smart_money_direction", "flow_alignment_state",
|
||||
], max_rows=6)
|
||||
md += "\n\n**포트폴리오 / 자금 맥락**\n\n"
|
||||
beta_gate = _sj(hctx.get("portfolio_beta_gate_json", {}))
|
||||
corr_gate = _sj(hctx.get("portfolio_correlation_gate_json", {}))
|
||||
md += _kv([
|
||||
("목표 자산", hctx.get("goal_asset_krw", "")),
|
||||
("현재 자산", hctx.get("goal_current_asset_krw", hctx.get("total_asset_krw", ""))),
|
||||
("목표 달성율(%)", hctx.get("goal_achievement_pct", "")),
|
||||
("목표 상태", hctx.get("goal_status", "")),
|
||||
("남은 목표액", hctx.get("goal_remaining_krw", "")),
|
||||
("ETA", hctx.get("goal_eta_label", "")),
|
||||
("ETA(개월)", hctx.get("goal_eta_months", "")),
|
||||
("수익 보전 단계", hctx.get("profit_lock_stage", hctx.get("profit_preservation_lock", ""))),
|
||||
("포트폴리오 헬스", (hctx.get("portfolio_health_json", {}) or {}).get("label", hctx.get("portfolio_health_label", "")) if isinstance(hctx.get("portfolio_health_json", {}), dict) else hctx.get("portfolio_health_label", "")),
|
||||
("포트폴리오 점수", (hctx.get("portfolio_health_json", {}) or {}).get("score", hctx.get("portfolio_health_score", "")) if isinstance(hctx.get("portfolio_health_json", {}), dict) else hctx.get("portfolio_health_score", "")),
|
||||
("알파 신뢰도", hctx.get("portfolio_alpha_confidence", "")),
|
||||
("드로우다운 상태", hctx.get("drawdown_guard_state", hctx.get("portfolio_drawdown_gate", ""))),
|
||||
("베타 게이트", beta_gate.get("gate_status", beta_gate.get("gate", "")) if isinstance(beta_gate, dict) else ""),
|
||||
("포트폴리오 베타", beta_gate.get("portfolio_beta", "") if isinstance(beta_gate, dict) else ""),
|
||||
("상관 게이트", corr_gate.get("correlation_gate_status", "") if isinstance(corr_gate, dict) else ""),
|
||||
("상관 유효베타", corr_gate.get("effective_portfolio_beta", "") if isinstance(corr_gate, dict) else ""),
|
||||
])
|
||||
md += "\n\n**개선 제안**\n\n"
|
||||
md += (
|
||||
"- 섹터 수급은 ETF 프록시와 직접 스마트머니를 분리해서 보여주고, 둘이 어긋날 때 경고를 강화해야 합니다.\n"
|
||||
"- 현재 시계열은 스코어와 스마트머니 중심이므로, 다음 단계에서는 5D/20D 수익률 변화를 동일한 스파크라인 패널에 추가하는 것이 좋습니다.\n"
|
||||
"- 포트폴리오 자금 패널은 목표 달성율, 드로우다운, 베타, 알파 신뢰도를 함께 묶어 보여줘야 실제 투자 판단과 연결됩니다.\n"
|
||||
)
|
||||
return md
|
||||
|
||||
|
||||
def _etf_representative_monitor_v1(data_root: dict, hctx: dict, se: list) -> str:
|
||||
inner_data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
|
||||
payload = {"data": inner_data, "data_root": data_root, "_harness_context": hctx}
|
||||
result = build_etf_representative_monitor(payload)
|
||||
if not isinstance(result, dict) or not result:
|
||||
return _err(se, "etf_representative_monitor_v1", "etf representative monitor unavailable")
|
||||
summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
|
||||
rows_data = result.get("rows") if isinstance(result.get("rows"), list) else []
|
||||
md = _kv([
|
||||
("ETF 섹터 수", result.get("etf_sector_count", "")),
|
||||
("추적 대표 종목 수", result.get("tracked_count", "")),
|
||||
("BUY_REVIEW", summary.get("buy_review_count", "")),
|
||||
("TRACK", summary.get("track_count", "")),
|
||||
("WATCH", summary.get("watch_count", "")),
|
||||
("CAUTION", summary.get("caution_count", "")),
|
||||
("정렬(ETF vs 대표종목)", summary.get("aligned_count", "")),
|
||||
("구성비중 기반", summary.get("weighted_basis_count", "")),
|
||||
("리퀴디티 대체", summary.get("fallback_basis_count", "")),
|
||||
("완전 바스켓", summary.get("complete_basket_count", "")),
|
||||
("부분 바스켓", summary.get("partial_basket_count", "")),
|
||||
("바스켓 미싱", summary.get("basket_missing_total", "")),
|
||||
])
|
||||
md += "\n\n**ETF 대표 종목 추출 원칙**\n\n"
|
||||
md += (
|
||||
"- 대표 종목은 우선 ETF 구성비중이 가장 큰 종목을 선택하고, 그 종목이 현재 유동성/호가/추세 조건을 충족하는지로 계속 모니터링합니다.\n"
|
||||
"- 구성비중 데이터가 비어 있거나 비정상일 때만 같은 섹터의 유동성 우선 후보로 대체합니다.\n"
|
||||
"- BUY_REVIEW는 ETF 수급이 대표 종목의 추세와 같이 붙을 때만 후보로 승격합니다.\n"
|
||||
)
|
||||
if rows_data:
|
||||
display_rows = []
|
||||
for row in rows_data:
|
||||
reps = row.get("representatives", [])
|
||||
rep_names = []
|
||||
rep_states = []
|
||||
rep_weights = []
|
||||
if isinstance(reps, list):
|
||||
for rep in reps[:3]:
|
||||
if isinstance(rep, dict):
|
||||
rep_names.append(f"{rep.get('name', '')}({rep.get('ticker', '')})")
|
||||
rep_states.append(str(rep.get("monitor_state", "")))
|
||||
rep_weights.append(str(rep.get("weight", "")))
|
||||
display_rows.append({
|
||||
"sector": row.get("sector", ""),
|
||||
"etf_proxy_ticker": row.get("etf_proxy_ticker", ""),
|
||||
"etf_proxy_name": row.get("etf_proxy_name", ""),
|
||||
"representative_basket": " / ".join(rep_names),
|
||||
"representative_count": row.get("representative_count", ""),
|
||||
"basket_weights": ", ".join(rep_weights),
|
||||
"basket_states": ", ".join(rep_states),
|
||||
"representative_basis": row.get("representative_basis", ""),
|
||||
"representative_basis_detail": row.get("representative_basis_detail", ""),
|
||||
"basket_quality_state": row.get("basket_quality_state", ""),
|
||||
"basket_coverage_pct": row.get("basket_coverage_pct", ""),
|
||||
"selection_source": ", ".join(str(rep.get("selection_source", "")) for rep in reps[:3] if isinstance(rep, dict)),
|
||||
"selection_score": ", ".join(str(rep.get("selection_score", "")) for rep in reps[:3] if isinstance(rep, dict)),
|
||||
"basket_state": row.get("monitor_state", ""),
|
||||
"basket_buy_review_count": row.get("basket_buy_review_count", ""),
|
||||
"basket_caution_count": row.get("basket_caution_count", ""),
|
||||
"basket_aligned_count": row.get("basket_aligned_count", ""),
|
||||
"monitor_reason": row.get("monitor_reason", ""),
|
||||
})
|
||||
md += "\n\n**대표 종목 모니터 테이블**\n\n"
|
||||
md += _tbl(display_rows, [
|
||||
"sector", "etf_proxy_ticker", "etf_proxy_name", "representative_basket",
|
||||
"representative_count", "basket_weights", "basket_states", "representative_basis",
|
||||
"representative_basis_detail", "basket_quality_state", "basket_coverage_pct",
|
||||
"selection_source", "selection_score", "basket_state", "basket_buy_review_count",
|
||||
"basket_aligned_count", "monitor_reason",
|
||||
], max_rows=20)
|
||||
spark_rows = []
|
||||
for row in rows_data[:5]:
|
||||
reps = row.get("representatives", [])
|
||||
rep_states = ", ".join(str(rep.get("monitor_state", "")) for rep in reps if isinstance(rep, dict))
|
||||
spark_rows.append({
|
||||
"sector": row.get("sector", ""),
|
||||
"basket_states": rep_states,
|
||||
"basket_bars": _sparkline([
|
||||
_num(row.get("basket_buy_review_count"), 0.0),
|
||||
_num(row.get("basket_aligned_count"), 0.0),
|
||||
_num(row.get("basket_aligned_count"), 0.0) - _num(row.get("basket_caution_count"), 0.0),
|
||||
]),
|
||||
"primary_ret20d": row.get("representative_ret20d", ""),
|
||||
"basket_state": row.get("monitor_state", ""),
|
||||
})
|
||||
md += "\n\n**대표 종목 추세 미니차트**\n\n"
|
||||
md += _tbl(spark_rows, ["sector", "basket_states", "basket_bars", "primary_ret20d", "basket_state"], max_rows=5)
|
||||
return md
|
||||
|
||||
|
||||
# ── PHASE-2 렌더러 ────────────────────────────────────────────────────────────
|
||||
|
||||
def _investment_quality_headline(hctx: dict, se: list) -> str:
|
||||
@@ -834,6 +1151,8 @@ def main() -> int:
|
||||
"single_conclusion": lambda: _single_conclusion(hctx, se),
|
||||
"immediate_execution_playbook": lambda: _immediate_execution_playbook(hctx, se),
|
||||
"market_context_learning_note": lambda: _market_context_learning_note(hctx, se),
|
||||
"portfolio_performance_summary": lambda: _portfolio_performance_summary(data_root, hctx, se),
|
||||
"sector_trend_analysis_v1": lambda: _sector_trend_analysis_v1(data_root, hctx, se),
|
||||
"investment_quality_headline": lambda: _investment_quality_headline(hctx, se),
|
||||
"operational_truth_score": lambda: _operational_truth_score(hctx, se),
|
||||
"execution_readiness_matrix": lambda: _execution_readiness_matrix(hctx, packet, se),
|
||||
@@ -842,6 +1161,7 @@ def main() -> int:
|
||||
"routing_serving_trace": lambda: _routing_serving_trace(hctx, se),
|
||||
"export_gate_diagnosis": lambda: _export_gate_diagnosis(hctx, se),
|
||||
"QEH_AUDIT_BLOCK": lambda: _qeh_audit_block(hctx, se),
|
||||
"etf_representative_monitor_v1": lambda: _etf_representative_monitor_v1(data_root, hctx, se),
|
||||
"fundamental_quality_gate_v1": lambda: _fundamental_quality_gate_v1(hctx, se),
|
||||
"horizon_allocation_lock_v1": lambda: _horizon_allocation_lock_v1(hctx, se),
|
||||
"smart_money_liquidity_gate_v1": lambda: _smart_money_liquidity_gate_v1(hctx, se),
|
||||
|
||||
Reference in New Issue
Block a user