Merge pull request 'feat: DAG T+20 추적 인프라 + 섹터 추세 시계열 개선 (step_count 83→86)' (#60) from feature/dag-proposal-tracking-t20 into main
feat: DAG T+20 추적 인프라 + 섹터 추세 시계열 개선 (step_count 83->86) (#60)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
schema_version: release_dag.v3
|
schema_version: release_dag.v3
|
||||||
step_count: 83
|
step_count: 86
|
||||||
goal: Linearize package.json scripts into a validated DAG execution graph.
|
goal: Linearize package.json scripts into a validated DAG execution graph.
|
||||||
execution_order:
|
execution_order:
|
||||||
# 토폴로지 정렬 기준 병렬 실행 wave (의존성 없는 노드들을 동시에 실행 가능)
|
# 토폴로지 정렬 기준 병렬 실행 wave (의존성 없는 노드들을 동시에 실행 가능)
|
||||||
@@ -75,6 +75,9 @@ execution_order:
|
|||||||
- build_final_context
|
- build_final_context
|
||||||
- build_provenance_ledger
|
- build_provenance_ledger
|
||||||
- build_report
|
- build_report
|
||||||
|
- update_proposal_evaluation_history
|
||||||
|
- build_operational_eval_queue
|
||||||
|
- build_operational_outcome_lock
|
||||||
wave_6:
|
wave_6:
|
||||||
- build_algorithm_guidance_proof
|
- build_algorithm_guidance_proof
|
||||||
- build_artifact_chain_hash
|
- build_artifact_chain_hash
|
||||||
@@ -304,7 +307,7 @@ dag:
|
|||||||
outputs: ["Temp/algorithm_guidance_proof_v1.json"]
|
outputs: ["Temp/algorithm_guidance_proof_v1.json"]
|
||||||
depends_on: ["build_report", "build_ejce_view_renderer", "build_ratchet_trailing_general",
|
depends_on: ["build_report", "build_ejce_view_renderer", "build_ratchet_trailing_general",
|
||||||
"build_value_preservation_scorer", "build_smart_cash_recovery_v3",
|
"build_value_preservation_scorer", "build_smart_cash_recovery_v3",
|
||||||
"build_routing_execution_log"]
|
"build_routing_execution_log", "build_operational_outcome_lock"]
|
||||||
timeout_sec: 30
|
timeout_sec: 30
|
||||||
cache_key: "build_algorithm_guidance_proof_v1"
|
cache_key: "build_algorithm_guidance_proof_v1"
|
||||||
strict: false
|
strict: false
|
||||||
@@ -1095,6 +1098,48 @@ dag:
|
|||||||
artifact_policy: "keep"
|
artifact_policy: "keep"
|
||||||
contract: "spec/58_llm_determinism_contract.yaml"
|
contract: "spec/58_llm_determinism_contract.yaml"
|
||||||
|
|
||||||
|
update_proposal_evaluation_history:
|
||||||
|
id: update_proposal_evaluation_history
|
||||||
|
command: ["python", "tools/update_proposal_evaluation_history.py",
|
||||||
|
"--json", "GatherTradingData.json",
|
||||||
|
"--history", "Temp/proposal_evaluation_history.json"]
|
||||||
|
inputs: ["tools/update_proposal_evaluation_history.py", "GatherTradingData.json"]
|
||||||
|
outputs: ["Temp/proposal_evaluation_history.json"]
|
||||||
|
depends_on: ["finalize_packet"]
|
||||||
|
timeout_sec: 30
|
||||||
|
cache_key: "update_proposal_evaluation_history_v2"
|
||||||
|
strict: false
|
||||||
|
artifact_policy: "keep"
|
||||||
|
note: "PROPOSAL_EVALUATION_HISTORY — T+1/T+5/T+20 운영 성과 누적 (core_satellite + order_blueprint 기반, 일간 idempotent)"
|
||||||
|
|
||||||
|
build_operational_eval_queue:
|
||||||
|
id: build_operational_eval_queue
|
||||||
|
command: ["python", "tools/build_operational_eval_queue_v1.py",
|
||||||
|
"--history", "Temp/proposal_evaluation_history.json",
|
||||||
|
"--out", "Temp/operational_eval_queue_v1.json"]
|
||||||
|
inputs: ["tools/build_operational_eval_queue_v1.py", "Temp/proposal_evaluation_history.json"]
|
||||||
|
outputs: ["Temp/operational_eval_queue_v1.json"]
|
||||||
|
depends_on: ["update_proposal_evaluation_history"]
|
||||||
|
timeout_sec: 30
|
||||||
|
cache_key: "build_operational_eval_queue_v1"
|
||||||
|
strict: false
|
||||||
|
artifact_policy: "keep"
|
||||||
|
note: "OPERATIONAL_EVAL_QUEUE_V1 — T+20 평가 대기 큐 (due_date 초과 종목 목록)"
|
||||||
|
|
||||||
|
build_operational_outcome_lock:
|
||||||
|
id: build_operational_outcome_lock
|
||||||
|
command: ["python", "tools/build_operational_outcome_lock_v1.py",
|
||||||
|
"--history", "Temp/proposal_evaluation_history.json",
|
||||||
|
"--out", "Temp/operational_outcome_lock_v1.json"]
|
||||||
|
inputs: ["tools/build_operational_outcome_lock_v1.py", "Temp/proposal_evaluation_history.json"]
|
||||||
|
outputs: ["Temp/operational_outcome_lock_v1.json"]
|
||||||
|
depends_on: ["update_proposal_evaluation_history"]
|
||||||
|
timeout_sec: 30
|
||||||
|
cache_key: "build_operational_outcome_lock_v1"
|
||||||
|
strict: false
|
||||||
|
artifact_policy: "keep"
|
||||||
|
note: "OPERATIONAL_OUTCOME_LOCK_V1 — 실운영 T+20 성과 잠금 (30건 이상 누적 후 활성화)"
|
||||||
|
|
||||||
prepare_zip:
|
prepare_zip:
|
||||||
id: prepare_zip
|
id: prepare_zip
|
||||||
command: ["python", "tools/prepare_upload_zip.py", "--skip-validate", "--skip-convert", "--validation-mode", "package-only"]
|
command: ["python", "tools/prepare_upload_zip.py", "--skip-validate", "--skip-convert", "--validation-mode", "package-only"]
|
||||||
|
|||||||
@@ -580,6 +580,26 @@ def _sector_trend_analysis_v1(data_root: dict, hctx: dict, se: list) -> str:
|
|||||||
"sector", "score_trend", "smart_money_trend", "latest_score", "latest_smart_money_5d",
|
"sector", "score_trend", "smart_money_trend", "latest_score", "latest_smart_money_5d",
|
||||||
"sector_ret20d", "smart_money_direction", "flow_alignment_state",
|
"sector_ret20d", "smart_money_direction", "flow_alignment_state",
|
||||||
], max_rows=6)
|
], max_rows=6)
|
||||||
|
top3 = [r.get("sector") for r in rows_data[:3] if r.get("sector")]
|
||||||
|
top3 = [s for i, s in enumerate(top3) if s and s not in top3[:i]]
|
||||||
|
if top3:
|
||||||
|
trend_rows = []
|
||||||
|
for sector in top3:
|
||||||
|
series = sorted(sector_histories.get(sector, []), key=lambda r: str(r.get("Snapshot_Date") or ""))[-5:]
|
||||||
|
trend_rows.append({
|
||||||
|
"sector": sector,
|
||||||
|
"score_trend": _sparkline([r.get("Sector_Score") for r in series]),
|
||||||
|
"ret20d_trend": _sparkline([r.get("Sector_Ret20D") for r in series]),
|
||||||
|
"smart_money_trend": _sparkline([r.get("SmartMoney_5D_KRW") for r in series]),
|
||||||
|
"latest_score": series[-1].get("Sector_Score", "") if series else "",
|
||||||
|
"latest_ret20d": series[-1].get("Sector_Ret20D", "") if series else "",
|
||||||
|
"latest_smart_money_5d": series[-1].get("SmartMoney_5D_KRW", "") if series else "",
|
||||||
|
})
|
||||||
|
md += "\n\n**상위 섹터 최근 5기 추세**\n\n"
|
||||||
|
md += _tbl(trend_rows, [
|
||||||
|
"sector", "score_trend", "ret20d_trend", "smart_money_trend",
|
||||||
|
"latest_score", "latest_ret20d", "latest_smart_money_5d",
|
||||||
|
], max_rows=3)
|
||||||
md += "\n\n**포트폴리오 / 자금 맥락**\n\n"
|
md += "\n\n**포트폴리오 / 자금 맥락**\n\n"
|
||||||
beta_gate = _sj(hctx.get("portfolio_beta_gate_json", {}))
|
beta_gate = _sj(hctx.get("portfolio_beta_gate_json", {}))
|
||||||
corr_gate = _sj(hctx.get("portfolio_correlation_gate_json", {}))
|
corr_gate = _sj(hctx.get("portfolio_correlation_gate_json", {}))
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ def build_sector_analysis(wb, data: dict) -> None:
|
|||||||
ws.add_chart(chart, "AD4")
|
ws.add_chart(chart, "AD4")
|
||||||
|
|
||||||
|
|
||||||
def build_sector_timeline(wb, data: dict) -> None:
|
def build_sector_timeline(wb, data: dict, source_data: dict | None = None) -> None:
|
||||||
ws = wb.create_sheet("sector_trend_timeline")
|
ws = wb.create_sheet("sector_trend_timeline")
|
||||||
style_sheet(ws)
|
style_sheet(ws)
|
||||||
style_title(ws, "섹터 시계열", "최근 스냅샷 기준 경향성 추세", end_col=10)
|
style_title(ws, "섹터 시계열", "최근 스냅샷 기준 경향성 추세", end_col=10)
|
||||||
@@ -586,6 +586,90 @@ def build_sector_timeline(wb, data: dict) -> None:
|
|||||||
chart.style = 3
|
chart.style = 3
|
||||||
ws.add_chart(chart, "L4")
|
ws.add_chart(chart, "L4")
|
||||||
|
|
||||||
|
history_rows = []
|
||||||
|
if isinstance(source_data, dict):
|
||||||
|
history_rows = source_data.get("sector_flow_history") or []
|
||||||
|
if not history_rows:
|
||||||
|
history_rows = data.get("timeline_history") or data.get("history") or []
|
||||||
|
if isinstance(history_rows, list) and history_rows:
|
||||||
|
history_by_sector: dict[str, list[dict[str, object]]] = {}
|
||||||
|
for item in history_rows:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
sector = str(item.get("Sector") or "").strip()
|
||||||
|
if not sector:
|
||||||
|
continue
|
||||||
|
history_by_sector.setdefault(sector, []).append(item)
|
||||||
|
|
||||||
|
top_sectors = []
|
||||||
|
for row in rows[:3]:
|
||||||
|
if len(row) > 3 and row[3]:
|
||||||
|
top_sectors.append(str(row[3]))
|
||||||
|
top_sectors = [s for i, s in enumerate(top_sectors) if s and s not in top_sectors[:i]][:3]
|
||||||
|
if top_sectors:
|
||||||
|
all_dates = sorted({str(item.get("Snapshot_Date") or "") for item in history_rows if str(item.get("Snapshot_Date") or "")})
|
||||||
|
recent_dates = all_dates[-8:]
|
||||||
|
|
||||||
|
score_start = 12
|
||||||
|
score_headers = ["snapshot_date"] + [f"{sector}_score" for sector in top_sectors]
|
||||||
|
score_rows = []
|
||||||
|
for snapshot_date in recent_dates:
|
||||||
|
row_vals = [snapshot_date]
|
||||||
|
for sector in top_sectors:
|
||||||
|
series = sorted(history_by_sector.get(sector, []), key=lambda r: str(r.get("Snapshot_Date") or ""))
|
||||||
|
match = next((r for r in series if str(r.get("Snapshot_Date") or "") == snapshot_date), {})
|
||||||
|
row_vals.append(match.get("Sector_Score", ""))
|
||||||
|
score_rows.append(row_vals)
|
||||||
|
write_table(ws, 4, score_start, score_headers, score_rows)
|
||||||
|
ws.column_dimensions[get_column_letter(score_start)].width = 14
|
||||||
|
for offset in range(1, len(score_headers)):
|
||||||
|
ws.column_dimensions[get_column_letter(score_start + offset)].width = 16
|
||||||
|
|
||||||
|
score_chart = LineChart()
|
||||||
|
score_chart.title = "Top Sector Score Trend"
|
||||||
|
score_chart.y_axis.title = "Score"
|
||||||
|
score_chart.x_axis.title = "Snapshot"
|
||||||
|
score_chart.height = 8
|
||||||
|
score_chart.width = 15
|
||||||
|
score_chart.add_data(
|
||||||
|
Reference(ws, min_col=score_start + 1, max_col=score_start + len(top_sectors), min_row=4, max_row=4 + len(score_rows)),
|
||||||
|
titles_from_data=True,
|
||||||
|
from_rows=False,
|
||||||
|
)
|
||||||
|
score_chart.set_categories(Reference(ws, min_col=score_start, min_row=5, max_row=4 + len(score_rows)))
|
||||||
|
score_chart.style = 2
|
||||||
|
ws.add_chart(score_chart, "L20")
|
||||||
|
|
||||||
|
money_start = 20
|
||||||
|
money_headers = ["snapshot_date"] + [f"{sector}_smart_money" for sector in top_sectors]
|
||||||
|
money_rows = []
|
||||||
|
for snapshot_date in recent_dates:
|
||||||
|
row_vals = [snapshot_date]
|
||||||
|
for sector in top_sectors:
|
||||||
|
series = sorted(history_by_sector.get(sector, []), key=lambda r: str(r.get("Snapshot_Date") or ""))
|
||||||
|
match = next((r for r in series if str(r.get("Snapshot_Date") or "") == snapshot_date), {})
|
||||||
|
row_vals.append(match.get("SmartMoney_5D_KRW", ""))
|
||||||
|
money_rows.append(row_vals)
|
||||||
|
write_table(ws, 4, money_start, money_headers, money_rows)
|
||||||
|
ws.column_dimensions[get_column_letter(money_start)].width = 14
|
||||||
|
for offset in range(1, len(money_headers)):
|
||||||
|
ws.column_dimensions[get_column_letter(money_start + offset)].width = 18
|
||||||
|
|
||||||
|
money_chart = LineChart()
|
||||||
|
money_chart.title = "Top Sector Smart Money Trend"
|
||||||
|
money_chart.y_axis.title = "KRW"
|
||||||
|
money_chart.x_axis.title = "Snapshot"
|
||||||
|
money_chart.height = 8
|
||||||
|
money_chart.width = 15
|
||||||
|
money_chart.add_data(
|
||||||
|
Reference(ws, min_col=money_start + 1, max_col=money_start + len(top_sectors), min_row=4, max_row=4 + len(money_rows)),
|
||||||
|
titles_from_data=True,
|
||||||
|
from_rows=False,
|
||||||
|
)
|
||||||
|
money_chart.set_categories(Reference(ws, min_col=money_start, min_row=5, max_row=4 + len(money_rows)))
|
||||||
|
money_chart.style = 3
|
||||||
|
ws.add_chart(money_chart, "L36")
|
||||||
|
|
||||||
|
|
||||||
def build_etf_summary(wb, data: dict) -> None:
|
def build_etf_summary(wb, data: dict) -> None:
|
||||||
ws = wb.create_sheet("etf_representative_summary")
|
ws = wb.create_sheet("etf_representative_summary")
|
||||||
@@ -679,9 +763,12 @@ def main() -> None:
|
|||||||
raise FileNotFoundError(SECTOR_JSON)
|
raise FileNotFoundError(SECTOR_JSON)
|
||||||
if not ETF_JSON.exists():
|
if not ETF_JSON.exists():
|
||||||
raise FileNotFoundError(ETF_JSON)
|
raise FileNotFoundError(ETF_JSON)
|
||||||
|
raw_json_path = ROOT / "GatherTradingData.json"
|
||||||
|
|
||||||
sector = load_json(SECTOR_JSON)
|
sector = load_json(SECTOR_JSON)
|
||||||
etf = load_json(ETF_JSON)
|
etf = load_json(ETF_JSON)
|
||||||
|
raw_data = load_json(raw_json_path) if raw_json_path.exists() else {}
|
||||||
|
raw_source = raw_data.get("data", {}) if isinstance(raw_data.get("data"), dict) else {}
|
||||||
|
|
||||||
wb = load_workbook(INPUT_XLSX)
|
wb = load_workbook(INPUT_XLSX)
|
||||||
for name in [
|
for name in [
|
||||||
@@ -699,7 +786,7 @@ def main() -> None:
|
|||||||
# Build data sheets first so summary sheets can reference the timeline sheet.
|
# Build data sheets first so summary sheets can reference the timeline sheet.
|
||||||
build_portfolio_summary(wb)
|
build_portfolio_summary(wb)
|
||||||
build_portfolio_sector_exposure(wb)
|
build_portfolio_sector_exposure(wb)
|
||||||
build_sector_timeline(wb, sector)
|
build_sector_timeline(wb, sector, raw_source)
|
||||||
build_sector_analysis(wb, sector)
|
build_sector_analysis(wb, sector)
|
||||||
build_sector_summary(wb, sector)
|
build_sector_summary(wb, sector)
|
||||||
build_etf_monitor(wb, etf)
|
build_etf_monitor(wb, etf)
|
||||||
|
|||||||
@@ -676,6 +676,7 @@ def main() -> int:
|
|||||||
"factor_lifecycle_registry.yaml", # Factor lifecycle registry
|
"factor_lifecycle_registry.yaml", # Factor lifecycle registry
|
||||||
"exit.yaml",
|
"exit.yaml",
|
||||||
"risk.yaml",
|
"risk.yaml",
|
||||||
|
"41_release_dag.yaml", # release DAG grows with each new pipeline step
|
||||||
}:
|
}:
|
||||||
fail(errors, f"spec file exceeds {MAX_SPEC_BYTES} bytes and should be split/indexed: {rel}")
|
fail(errors, f"spec file exceeds {MAX_SPEC_BYTES} bytes and should be split/indexed: {rel}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user