184 lines
7.1 KiB
Python
184 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
from pathlib import Path
|
|
from typing import Any, Mapping
|
|
|
|
|
|
def save_report_bundle(
|
|
final_state: Mapping[str, Any],
|
|
ticker: str,
|
|
save_path: Path,
|
|
*,
|
|
generated_at: dt.datetime | None = None,
|
|
language: str = "English",
|
|
) -> Path:
|
|
"""Persist a complete TradingAgents report bundle to disk."""
|
|
|
|
generated_at = generated_at or dt.datetime.now()
|
|
save_path = Path(save_path)
|
|
save_path.mkdir(parents=True, exist_ok=True)
|
|
labels = _labels_for(language)
|
|
analysis_date = _coerce_text(final_state.get("analysis_date"))
|
|
trade_date = _coerce_text(final_state.get("trade_date"))
|
|
|
|
sections: list[str] = []
|
|
|
|
analysts_dir = save_path / "1_analysts"
|
|
analyst_parts: list[tuple[str, str]] = []
|
|
for file_name, title, key in (
|
|
("market.md", labels["market_analyst"], "market_report"),
|
|
("sentiment.md", labels["social_analyst"], "sentiment_report"),
|
|
("news.md", labels["news_analyst"], "news_report"),
|
|
("fundamentals.md", labels["fundamentals_analyst"], "fundamentals_report"),
|
|
):
|
|
content = _coerce_text(final_state.get(key))
|
|
if not content:
|
|
continue
|
|
analysts_dir.mkdir(exist_ok=True)
|
|
_write_text(analysts_dir / file_name, content)
|
|
analyst_parts.append((title, content))
|
|
|
|
if analyst_parts:
|
|
sections.append(
|
|
f"## {labels['section_analysts']}\n\n"
|
|
+ "\n\n".join(f"### {title}\n{content}" for title, content in analyst_parts)
|
|
)
|
|
|
|
debate = final_state.get("investment_debate_state") or {}
|
|
research_dir = save_path / "2_research"
|
|
research_parts: list[tuple[str, str]] = []
|
|
for file_name, title, key in (
|
|
("bull.md", labels["bull_researcher"], "bull_history"),
|
|
("bear.md", labels["bear_researcher"], "bear_history"),
|
|
("manager.md", labels["research_manager"], "judge_decision"),
|
|
):
|
|
content = _coerce_text(debate.get(key))
|
|
if not content:
|
|
continue
|
|
research_dir.mkdir(exist_ok=True)
|
|
_write_text(research_dir / file_name, content)
|
|
research_parts.append((title, content))
|
|
|
|
if research_parts:
|
|
sections.append(
|
|
f"## {labels['section_research']}\n\n"
|
|
+ "\n\n".join(f"### {title}\n{content}" for title, content in research_parts)
|
|
)
|
|
|
|
trader_plan = _coerce_text(final_state.get("trader_investment_plan"))
|
|
if trader_plan:
|
|
trading_dir = save_path / "3_trading"
|
|
trading_dir.mkdir(exist_ok=True)
|
|
_write_text(trading_dir / "trader.md", trader_plan)
|
|
sections.append(
|
|
f"## {labels['section_trading']}\n\n### {labels['trader']}\n{trader_plan}"
|
|
)
|
|
|
|
risk = final_state.get("risk_debate_state") or {}
|
|
risk_dir = save_path / "4_risk"
|
|
risk_parts: list[tuple[str, str]] = []
|
|
for file_name, title, key in (
|
|
("aggressive.md", labels["aggressive_analyst"], "aggressive_history"),
|
|
("conservative.md", labels["conservative_analyst"], "conservative_history"),
|
|
("neutral.md", labels["neutral_analyst"], "neutral_history"),
|
|
):
|
|
content = _coerce_text(risk.get(key))
|
|
if not content:
|
|
continue
|
|
risk_dir.mkdir(exist_ok=True)
|
|
_write_text(risk_dir / file_name, content)
|
|
risk_parts.append((title, content))
|
|
|
|
if risk_parts:
|
|
sections.append(
|
|
f"## {labels['section_risk']}\n\n"
|
|
+ "\n\n".join(f"### {title}\n{content}" for title, content in risk_parts)
|
|
)
|
|
|
|
portfolio_decision = _coerce_text(risk.get("judge_decision"))
|
|
if portfolio_decision:
|
|
portfolio_dir = save_path / "5_portfolio"
|
|
portfolio_dir.mkdir(exist_ok=True)
|
|
_write_text(portfolio_dir / "decision.md", portfolio_decision)
|
|
sections.append(
|
|
f"## {labels['section_portfolio']}\n\n"
|
|
f"### {labels['portfolio_manager']}\n{portfolio_decision}"
|
|
)
|
|
|
|
metadata_lines = [f"{labels['generated_at']}: {generated_at.strftime('%Y-%m-%d %H:%M:%S')}"]
|
|
if analysis_date:
|
|
metadata_lines.append(f"{labels['analysis_date']}: {analysis_date}")
|
|
if trade_date:
|
|
metadata_lines.append(f"{labels['trade_date']}: {trade_date}")
|
|
|
|
header = f"# {labels['report_title']}: {ticker}\n\n" + "\n".join(metadata_lines) + "\n\n"
|
|
complete_report = save_path / "complete_report.md"
|
|
_write_text(complete_report, header + "\n\n".join(sections))
|
|
return complete_report
|
|
|
|
|
|
def _coerce_text(value: Any) -> str:
|
|
if value is None:
|
|
return ""
|
|
if isinstance(value, str):
|
|
return value
|
|
if isinstance(value, list):
|
|
return "\n".join(str(item) for item in value)
|
|
return str(value)
|
|
|
|
|
|
def _write_text(path: Path, content: str) -> None:
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
def _labels_for(language: str) -> dict[str, str]:
|
|
if str(language).strip().lower() == "korean":
|
|
return {
|
|
"report_title": "트레이딩 분석 리포트",
|
|
"generated_at": "생성 시각",
|
|
"analysis_date": "분석 기준일",
|
|
"trade_date": "시장 데이터 기준일",
|
|
"section_analysts": "I. 애널리스트 팀 리포트",
|
|
"section_research": "II. 리서치 팀 판단",
|
|
"section_trading": "III. 트레이딩 팀 계획",
|
|
"section_risk": "IV. 리스크 관리 팀 판단",
|
|
"section_portfolio": "V. 포트폴리오 매니저 최종 판단",
|
|
"market_analyst": "시장 애널리스트",
|
|
"social_analyst": "소셜 심리 애널리스트",
|
|
"news_analyst": "뉴스 애널리스트",
|
|
"fundamentals_analyst": "펀더멘털 애널리스트",
|
|
"bull_researcher": "강세 리서처",
|
|
"bear_researcher": "약세 리서처",
|
|
"research_manager": "리서치 매니저",
|
|
"trader": "트레이더",
|
|
"aggressive_analyst": "공격적 리스크 애널리스트",
|
|
"conservative_analyst": "보수적 리스크 애널리스트",
|
|
"neutral_analyst": "중립 리스크 애널리스트",
|
|
"portfolio_manager": "포트폴리오 매니저",
|
|
}
|
|
|
|
return {
|
|
"report_title": "Trading Analysis Report",
|
|
"generated_at": "Generated",
|
|
"analysis_date": "Analysis date",
|
|
"trade_date": "Market data date",
|
|
"section_analysts": "I. Analyst Team Reports",
|
|
"section_research": "II. Research Team Decision",
|
|
"section_trading": "III. Trading Team Plan",
|
|
"section_risk": "IV. Risk Management Team Decision",
|
|
"section_portfolio": "V. Portfolio Manager Decision",
|
|
"market_analyst": "Market Analyst",
|
|
"social_analyst": "Social Analyst",
|
|
"news_analyst": "News Analyst",
|
|
"fundamentals_analyst": "Fundamentals Analyst",
|
|
"bull_researcher": "Bull Researcher",
|
|
"bear_researcher": "Bear Researcher",
|
|
"research_manager": "Research Manager",
|
|
"trader": "Trader",
|
|
"aggressive_analyst": "Aggressive Analyst",
|
|
"conservative_analyst": "Conservative Analyst",
|
|
"neutral_analyst": "Neutral Analyst",
|
|
"portfolio_manager": "Portfolio Manager",
|
|
}
|