Ready for first real test

This commit is contained in:
Aryan Singh committed 2026-07-02 00:46:34 -05:00
1 parent acddf9135f
commit b91c81ebdf
46 files changed
+34903 -26

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
"""Analyst report generation.
Turns an agent's picks + research trace into a professional Markdown research note (with any
charts the agent generated via run_analysis embedded), a combined daily report, and PDF
exports. Stored under state/reports/<trade_date>/.
"""
from __future__ import annotations
import re
import shutil
from pathlib import Path
from loguru import logger
from langchain_core.messages import HumanMessage, SystemMessage
from agents.schemas import AgentOutput
from config.settings import get_settings
from llm.client import build_llm
_CHART_RE = re.compile(r"([^\s,]+\.png)")
REPORT_SYSTEM = (
"You are a senior sell-side equity research analyst. Write a clear, professional research "
"note in GitHub-flavored Markdown. Be specific and data-driven; cite the tickers, prices, and "
"numbers from the research. Do not invent data you weren't given. Output ONLY Markdown."
)
def _reports_day_dir(trade_date: str) -> Path:
d = get_settings().reports_dir / trade_date
d.mkdir(parents=True, exist_ok=True)
return d
def _collect_charts(out: AgentOutput, dest_dir: Path) -> list[str]:
"""Find chart PNGs the agent made via run_analysis (logged in reasoning_trace), copy them
next to the report, and return the local filenames to embed."""
names: list[str] = []
seen: set[str] = set()
for line in out.reasoning_trace:
if "Artifacts written" not in line:
continue
for m in _CHART_RE.findall(line):
src = Path(m)
if src.suffix.lower() == ".png" and src.exists() and str(src) not in seen:
seen.add(str(src))
fname = f"{out.persona_id}_{len(names) + 1}.png"
try:
shutil.copyfile(src, dest_dir / fname)
names.append(fname)
except Exception:
pass
return names
def write_agent_report(out: AgentOutput, trade_date: str) -> Path:
"""Generate + save one agent's Markdown research note. Returns the .md path."""
day_dir = _reports_day_dir(trade_date)
charts = _collect_charts(out, day_dir)
picks_json = "\n".join(
f"- {p.ticker} [{p.instrument.value}/{p.direction.value}] conv={p.conviction} "
f"weight={p.requested_weight_pct} stop={p.stop_loss_pct} take={p.take_profit_pct} :: {p.thesis}"
for p in out.picks
) or "(no picks)"
findings = "\n".join(out.reasoning_trace[-60:])
chart_note = (
"Embed these charts where relevant using Markdown image syntax "
f"(e.g. ![]({charts[0]})): {', '.join(charts)}" if charts else "No charts were generated."
)
human = (
f"Trade date: {trade_date}. Analyst persona: {out.agent_name}.\n"
f"Status: {out.status.value}. Market summary: {out.market_summary or '(none)'}\n\n"
f"FINAL PICKS:\n{picks_json}\n\n"
f"RESEARCH TRACE (tool calls + findings):\n{findings}\n\n"
f"{chart_note}\n\n"
"Write the note with these sections:\n"
"# <Analyst name> — Research Note (<date>)\n"
"## Market Overview (what regime/backdrop framed today's work)\n"
"## Picks — one `###` per pick: direction+conviction, thesis, supporting data/valuation, "
"catalysts, key risks, and the entry/stop/take plan.\n"
"## Portfolio Notes (how these fit existing holdings)\n"
"## Summary Table (Markdown table: Ticker | Dir | Conv | Weight | Stop | Take | One-liner)"
)
try:
resp = build_llm(max_tokens=4096).invoke(
[SystemMessage(content=REPORT_SYSTEM), HumanMessage(content=human)]
)
md = resp.content if isinstance(resp.content, str) else str(resp.content)
if not md.strip():
raise ValueError("empty report content")
except Exception as e:
logger.warning("report gen failed for {}: {}", out.persona_id, e)
md = f"# {out.agent_name} — Research Note ({trade_date})\n\n(Report generation failed: {e})\n\n{picks_json}"
md_path = day_dir / f"{out.persona_id}.md"
md_path.write_text(md)
logger.info("wrote report {}", md_path)
return md_path
def write_daily_report(trade_date: str, outputs: list[AgentOutput], exec_summary: str = "") -> Path:
"""Combine the agents into one daily report (links their notes + a portfolio-wide summary)."""
day_dir = _reports_day_dir(trade_date)
lines = [f"# Daily Research — {trade_date}\n"]
if exec_summary:
lines += ["## Execution Results\n", exec_summary, ""]
lines.append("## Agents\n")
for out in outputs:
tickers = ", ".join(
f"{p.ticker}({p.direction.value},{p.conviction:.2f})" for p in out.picks
) or "no picks"
lines.append(f"### {out.agent_name} — *{out.status.value}*")
lines.append(f"- Picks: {tickers}")
lines.append(f"- Full note: [`{out.persona_id}.md`]({out.persona_id}.md)\n")
path = day_dir / "daily.md"
path.write_text("\n".join(lines))
logger.info("wrote daily report {}", path)
return path
def to_pdf(md_path: Path) -> Path | None:
"""Render a Markdown report to PDF (embeds relative-path charts). None on failure."""
try:
import markdown as md_lib
from xhtml2pdf import pisa
except Exception as e:
logger.warning("PDF deps missing: {}", e)
return None
md_path = Path(md_path)
base = md_path.parent
html_body = md_lib.markdown(md_path.read_text(), extensions=["tables", "fenced_code"])
html = (
"<html><head><style>body{font-family:Helvetica,Arial,sans-serif;font-size:11px;} "
"h1{font-size:20px;} h2{font-size:15px;border-bottom:1px solid #ccc;} h3{font-size:13px;} "
"table{border-collapse:collapse;} td,th{border:1px solid #999;padding:3px;} "
f"img{{max-width:520px;}}</style></head><body>{html_body}</body></html>"
)
def _link_cb(uri: str, rel: str) -> str:
p = base / uri
return str(p) if p.exists() else uri
pdf_path = md_path.with_suffix(".pdf")
try:
with open(pdf_path, "wb") as f:
pisa.CreatePDF(html, dest=f, link_callback=_link_cb)
return pdf_path
except Exception as e:
logger.warning("PDF render failed for {}: {}", md_path, e)
return None