Fix scheduled news coverage and run date reporting (#10)
This commit is contained in:
parent
665eb55d36
commit
e7584b8d83
|
|
@ -10,6 +10,8 @@ from tradingagents.reporting import save_report_bundle
|
|||
class ReportLocalizationTests(unittest.TestCase):
|
||||
def test_save_report_bundle_uses_korean_labels(self):
|
||||
final_state = {
|
||||
"analysis_date": "2026-04-06",
|
||||
"trade_date": "2026-04-02",
|
||||
"market_report": "시장 보고서 본문",
|
||||
"sentiment_report": "소셜 보고서 본문",
|
||||
"news_report": "뉴스 보고서 본문",
|
||||
|
|
@ -39,6 +41,8 @@ class ReportLocalizationTests(unittest.TestCase):
|
|||
|
||||
self.assertIn("트레이딩 분석 리포트", report_text)
|
||||
self.assertIn("생성 시각", report_text)
|
||||
self.assertIn("분석 기준일: 2026-04-06", report_text)
|
||||
self.assertIn("시장 데이터 기준일: 2026-04-02", report_text)
|
||||
self.assertIn("애널리스트 팀 리포트", report_text)
|
||||
self.assertIn("포트폴리오 매니저 최종 판단", report_text)
|
||||
self.assertIn("시장 애널리스트", report_text)
|
||||
|
|
|
|||
|
|
@ -24,13 +24,14 @@ class _FakeTradingAgentsGraph:
|
|||
self.config = config or {}
|
||||
self.callbacks = callbacks or []
|
||||
|
||||
def propagate(self, ticker, trade_date):
|
||||
def propagate(self, ticker, trade_date, analysis_date=None):
|
||||
if ticker == "FAIL":
|
||||
raise RuntimeError("synthetic failure")
|
||||
|
||||
final_state = {
|
||||
"company_of_interest": ticker,
|
||||
"trade_date": trade_date,
|
||||
"analysis_date": analysis_date or trade_date,
|
||||
"market_report": f"## Market\n{ticker} market analysis",
|
||||
"sentiment_report": f"## Sentiment\n{ticker} sentiment analysis",
|
||||
"news_report": f"## News\n{ticker} news analysis",
|
||||
|
|
@ -104,6 +105,7 @@ subtitle = "Automated"
|
|||
self.assertEqual(manifest["settings"]["provider"], "codex")
|
||||
self.assertEqual(manifest["settings"]["deep_model"], "gpt-5.4")
|
||||
self.assertEqual(manifest["settings"]["quick_model"], "gpt-5.4")
|
||||
self.assertEqual(manifest["tickers"][0]["analysis_date"], manifest["started_at"][:10])
|
||||
|
||||
run_dir = archive_dir / "runs" / manifest["started_at"][:4] / manifest["run_id"]
|
||||
self.assertTrue((run_dir / "run.json").exists())
|
||||
|
|
@ -118,6 +120,7 @@ subtitle = "Automated"
|
|||
self.assertIn("partial failure", index_html)
|
||||
self.assertIn("NVDA", run_html)
|
||||
self.assertIn("Rendered report", ticker_html)
|
||||
self.assertIn("Analysis date", ticker_html)
|
||||
self.assertTrue((site_dir / "downloads" / manifest["run_id"] / "NVDA" / "complete_report.md").exists())
|
||||
|
||||
def test_main_site_only_rebuilds_from_existing_archive(self):
|
||||
|
|
@ -162,6 +165,7 @@ subtitle = "Automated"
|
|||
{
|
||||
"ticker": "NVDA",
|
||||
"status": "success",
|
||||
"analysis_date": "2026-04-05",
|
||||
"trade_date": "2026-04-04",
|
||||
"decision": "BUY",
|
||||
"started_at": "2026-04-05T09:13:00+09:00",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from tradingagents.dataflows.yfinance_news import get_news_yfinance
|
||||
|
||||
|
||||
def _article(date_value: str, title: str, link: str) -> dict:
|
||||
return {
|
||||
"content": {
|
||||
"title": title,
|
||||
"summary": f"Summary for {title}",
|
||||
"provider": {"displayName": "Unit Test"},
|
||||
"canonicalUrl": {"url": link},
|
||||
"pubDate": f"{date_value}T12:00:00Z",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _FakeTicker:
|
||||
def __init__(self, full_news: list[dict]):
|
||||
self.full_news = list(full_news)
|
||||
|
||||
def get_news(self, count=20):
|
||||
return self.full_news[:count]
|
||||
|
||||
|
||||
class YFinanceNewsTests(unittest.TestCase):
|
||||
def test_get_news_yfinance_expands_feed_depth_to_cover_requested_window(self):
|
||||
recent_articles = [
|
||||
_article(f"2026-04-{day:02d}", f"Recent article {day}", f"https://example.com/recent-{day}")
|
||||
for day in range(6, 2, -1)
|
||||
for _ in range(15)
|
||||
]
|
||||
older_articles = [
|
||||
_article("2026-04-02", "Alphabet April 2 article", "https://example.com/apr2"),
|
||||
_article("2026-04-01", "Alphabet April 1 article", "https://example.com/apr1"),
|
||||
]
|
||||
fake_ticker = _FakeTicker(recent_articles + older_articles)
|
||||
|
||||
with (
|
||||
patch("tradingagents.dataflows.yfinance_news.yf.Ticker", return_value=fake_ticker),
|
||||
patch("tradingagents.dataflows.yfinance_news.yf_retry", side_effect=lambda fn: fn()),
|
||||
):
|
||||
result = get_news_yfinance("GOOGL", "2026-03-26", "2026-04-02")
|
||||
|
||||
self.assertIn("Alphabet April 2 article", result)
|
||||
self.assertIn("[2026-04-02]", result)
|
||||
|
||||
def test_get_news_yfinance_reports_feed_coverage_when_window_is_unavailable(self):
|
||||
fake_ticker = _FakeTicker(
|
||||
[
|
||||
_article("2026-04-06", "Fresh article", "https://example.com/fresh"),
|
||||
_article("2026-04-05", "Fresh article 2", "https://example.com/fresh-2"),
|
||||
]
|
||||
)
|
||||
|
||||
with (
|
||||
patch("tradingagents.dataflows.yfinance_news.yf.Ticker", return_value=fake_ticker),
|
||||
patch("tradingagents.dataflows.yfinance_news.yf_retry", side_effect=lambda fn: fn()),
|
||||
):
|
||||
result = get_news_yfinance("GOOGL", "2026-03-26", "2026-04-02")
|
||||
|
||||
self.assertIn("No news found for GOOGL between 2026-03-26 and 2026-04-02", result)
|
||||
self.assertIn("2026-04-05 to 2026-04-06", result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -10,7 +10,7 @@ from tradingagents.dataflows.config import get_config
|
|||
|
||||
def create_news_analyst(llm):
|
||||
def news_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
current_date = state.get("analysis_date") or state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
|
||||
tools = [
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from tradingagents.dataflows.config import get_config
|
|||
|
||||
def create_social_media_analyst(llm):
|
||||
def social_media_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
current_date = state.get("analysis_date") or state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
|
||||
tools = [
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class RiskDebateState(TypedDict):
|
|||
class AgentState(MessagesState):
|
||||
company_of_interest: Annotated[str, "Company that we are interested in trading"]
|
||||
trade_date: Annotated[str, "What date we are trading at"]
|
||||
analysis_date: Annotated[str, "What date the full analysis is being generated on"]
|
||||
|
||||
sender: Annotated[str, "Agent that sent this message"]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,46 @@
|
|||
"""yfinance-based news data fetching functions."""
|
||||
|
||||
import yfinance as yf
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import yfinance as yf
|
||||
|
||||
from .stockstats_utils import yf_retry
|
||||
|
||||
|
||||
_TICKER_NEWS_FETCH_COUNTS = (20, 50, 100)
|
||||
_MAX_FILTERED_TICKER_ARTICLES = 25
|
||||
|
||||
|
||||
def _parse_pub_date(raw_value) -> datetime | None:
|
||||
"""Normalize yfinance pub date values into a timezone-aware datetime."""
|
||||
if raw_value in (None, ""):
|
||||
return None
|
||||
|
||||
if isinstance(raw_value, datetime):
|
||||
return raw_value
|
||||
|
||||
if isinstance(raw_value, (int, float)):
|
||||
try:
|
||||
return datetime.fromtimestamp(raw_value, tz=timezone.utc)
|
||||
except (OverflowError, OSError, ValueError):
|
||||
return None
|
||||
|
||||
if isinstance(raw_value, str):
|
||||
normalized = raw_value.strip()
|
||||
if not normalized:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromisoformat(normalized.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
try:
|
||||
return datetime.fromtimestamp(float(normalized), tz=timezone.utc)
|
||||
except (OverflowError, OSError, ValueError):
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _extract_article_data(article: dict) -> dict:
|
||||
"""Extract article data from yfinance news format (handles nested 'content' structure)."""
|
||||
# Handle nested content structure
|
||||
|
|
@ -22,13 +56,7 @@ def _extract_article_data(article: dict) -> dict:
|
|||
link = url_obj.get("url", "")
|
||||
|
||||
# Get publish date
|
||||
pub_date_str = content.get("pubDate", "")
|
||||
pub_date = None
|
||||
if pub_date_str:
|
||||
try:
|
||||
pub_date = datetime.fromisoformat(pub_date_str.replace("Z", "+00:00"))
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
pub_date = _parse_pub_date(content.get("pubDate", ""))
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
|
|
@ -44,10 +72,79 @@ def _extract_article_data(article: dict) -> dict:
|
|||
"summary": article.get("summary", ""),
|
||||
"publisher": article.get("publisher", "Unknown"),
|
||||
"link": article.get("link", ""),
|
||||
"pub_date": None,
|
||||
"pub_date": _parse_pub_date(article.get("providerPublishTime")),
|
||||
}
|
||||
|
||||
|
||||
def _article_identity(article: dict) -> str:
|
||||
"""Return a stable identity key for deduplicating news articles."""
|
||||
link = article.get("link", "").strip()
|
||||
if link:
|
||||
return link
|
||||
|
||||
title = article.get("title", "").strip()
|
||||
publisher = article.get("publisher", "").strip()
|
||||
pub_date = article.get("pub_date")
|
||||
stamp = pub_date.isoformat() if isinstance(pub_date, datetime) else ""
|
||||
return f"{publisher}::{title}::{stamp}"
|
||||
|
||||
|
||||
def _collect_ticker_news(
|
||||
ticker: str,
|
||||
start_dt: datetime,
|
||||
) -> tuple[list[dict], datetime | None, datetime | None]:
|
||||
"""Fetch increasingly larger ticker feeds until the requested window is covered."""
|
||||
collected: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
oldest_pub_date = None
|
||||
newest_pub_date = None
|
||||
|
||||
for count in _TICKER_NEWS_FETCH_COUNTS:
|
||||
news = yf_retry(lambda batch_size=count: yf.Ticker(ticker).get_news(count=batch_size))
|
||||
if not news:
|
||||
continue
|
||||
|
||||
for article in news:
|
||||
data = _extract_article_data(article)
|
||||
identity = _article_identity(data)
|
||||
if identity in seen:
|
||||
continue
|
||||
seen.add(identity)
|
||||
collected.append(data)
|
||||
|
||||
pub_date = data.get("pub_date")
|
||||
if pub_date:
|
||||
if newest_pub_date is None or pub_date > newest_pub_date:
|
||||
newest_pub_date = pub_date
|
||||
if oldest_pub_date is None or pub_date < oldest_pub_date:
|
||||
oldest_pub_date = pub_date
|
||||
|
||||
if oldest_pub_date and oldest_pub_date.replace(tzinfo=None) <= start_dt:
|
||||
break
|
||||
if len(news) < count:
|
||||
break
|
||||
|
||||
collected.sort(
|
||||
key=lambda article: article["pub_date"].timestamp() if article.get("pub_date") else float("-inf"),
|
||||
reverse=True,
|
||||
)
|
||||
return collected, oldest_pub_date, newest_pub_date
|
||||
|
||||
|
||||
def _format_coverage_note(oldest_pub_date: datetime | None, newest_pub_date: datetime | None) -> str:
|
||||
"""Describe the yfinance coverage window when no article matches the requested range."""
|
||||
if oldest_pub_date and newest_pub_date:
|
||||
return (
|
||||
"; the current yfinance ticker feed only covered "
|
||||
f"{oldest_pub_date.strftime('%Y-%m-%d')} to {newest_pub_date.strftime('%Y-%m-%d')} at query time"
|
||||
)
|
||||
if oldest_pub_date:
|
||||
return f"; the current yfinance ticker feed only reached back to {oldest_pub_date.strftime('%Y-%m-%d')}"
|
||||
if newest_pub_date:
|
||||
return f"; the current yfinance ticker feed only returned articles up to {newest_pub_date.strftime('%Y-%m-%d')}"
|
||||
return ""
|
||||
|
||||
|
||||
def get_news_yfinance(
|
||||
ticker: str,
|
||||
start_date: str,
|
||||
|
|
@ -65,38 +162,40 @@ def get_news_yfinance(
|
|||
Formatted string containing news articles
|
||||
"""
|
||||
try:
|
||||
stock = yf.Ticker(ticker)
|
||||
news = yf_retry(lambda: stock.get_news(count=20))
|
||||
|
||||
if not news:
|
||||
return f"No news found for {ticker}"
|
||||
|
||||
# Parse date range for filtering
|
||||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
articles, oldest_pub_date, newest_pub_date = _collect_ticker_news(ticker, start_dt)
|
||||
|
||||
if not articles:
|
||||
return f"No news found for {ticker}"
|
||||
|
||||
news_str = ""
|
||||
filtered_count = 0
|
||||
|
||||
for article in news:
|
||||
data = _extract_article_data(article)
|
||||
|
||||
for data in articles:
|
||||
# Filter by date if publish time is available
|
||||
if data["pub_date"]:
|
||||
pub_date_naive = data["pub_date"].replace(tzinfo=None)
|
||||
if not (start_dt <= pub_date_naive <= end_dt + relativedelta(days=1)):
|
||||
continue
|
||||
|
||||
news_str += f"### {data['title']} (source: {data['publisher']})\n"
|
||||
date_prefix = ""
|
||||
if data["pub_date"]:
|
||||
date_prefix = f"[{data['pub_date'].strftime('%Y-%m-%d')}] "
|
||||
|
||||
news_str += f"### {date_prefix}{data['title']} (source: {data['publisher']})\n"
|
||||
if data["summary"]:
|
||||
news_str += f"{data['summary']}\n"
|
||||
if data["link"]:
|
||||
news_str += f"Link: {data['link']}\n"
|
||||
news_str += "\n"
|
||||
filtered_count += 1
|
||||
if filtered_count >= _MAX_FILTERED_TICKER_ARTICLES:
|
||||
break
|
||||
|
||||
if filtered_count == 0:
|
||||
return f"No news found for {ticker} between {start_date} and {end_date}"
|
||||
coverage_note = _format_coverage_note(oldest_pub_date, newest_pub_date)
|
||||
return f"No news found for {ticker} between {start_date} and {end_date}{coverage_note}"
|
||||
|
||||
return f"## {ticker} News, from {start_date} to {end_date}:\n\n{news_str}"
|
||||
|
||||
|
|
|
|||
|
|
@ -16,13 +16,17 @@ class Propagator:
|
|||
self.max_recur_limit = max_recur_limit
|
||||
|
||||
def create_initial_state(
|
||||
self, company_name: str, trade_date: str
|
||||
self,
|
||||
company_name: str,
|
||||
trade_date: str,
|
||||
analysis_date: str | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create the initial state for the agent graph."""
|
||||
return {
|
||||
"messages": [("human", company_name)],
|
||||
"company_of_interest": company_name,
|
||||
"trade_date": str(trade_date),
|
||||
"analysis_date": str(analysis_date or trade_date),
|
||||
"investment_debate_state": InvestDebateState(
|
||||
{
|
||||
"bull_history": "",
|
||||
|
|
|
|||
|
|
@ -202,14 +202,14 @@ class TradingAgentsGraph:
|
|||
),
|
||||
}
|
||||
|
||||
def propagate(self, company_name, trade_date):
|
||||
def propagate(self, company_name, trade_date, analysis_date=None):
|
||||
"""Run the trading agents graph for a company on a specific date."""
|
||||
|
||||
self.ticker = company_name
|
||||
|
||||
# Initialize state
|
||||
init_agent_state = self.propagator.create_initial_state(
|
||||
company_name, trade_date
|
||||
company_name, trade_date, analysis_date=analysis_date
|
||||
)
|
||||
args = self.propagator.get_graph_args()
|
||||
|
||||
|
|
@ -245,6 +245,7 @@ class TradingAgentsGraph:
|
|||
self.log_states_dict[str(trade_date)] = {
|
||||
"company_of_interest": final_state["company_of_interest"],
|
||||
"trade_date": final_state["trade_date"],
|
||||
"analysis_date": final_state.get("analysis_date", final_state["trade_date"]),
|
||||
"market_report": final_state["market_report"],
|
||||
"sentiment_report": final_state["sentiment_report"],
|
||||
"news_report": final_state["news_report"],
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ def save_report_bundle(
|
|||
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] = []
|
||||
|
||||
|
|
@ -104,10 +106,13 @@ def save_report_bundle(
|
|||
f"### {labels['portfolio_manager']}\n{portfolio_decision}"
|
||||
)
|
||||
|
||||
header = (
|
||||
f"# {labels['report_title']}: {ticker}\n\n"
|
||||
f"{labels['generated_at']}: {generated_at.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
)
|
||||
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
|
||||
|
|
@ -132,6 +137,8 @@ def _labels_for(language: str) -> dict[str, str]:
|
|||
return {
|
||||
"report_title": "트레이딩 분석 리포트",
|
||||
"generated_at": "생성 시각",
|
||||
"analysis_date": "분석 기준일",
|
||||
"trade_date": "시장 데이터 기준일",
|
||||
"section_analysts": "I. 애널리스트 팀 리포트",
|
||||
"section_research": "II. 리서치 팀 판단",
|
||||
"section_trading": "III. 트레이딩 팀 계획",
|
||||
|
|
@ -154,6 +161,8 @@ def _labels_for(language: str) -> dict[str, str]:
|
|||
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",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ def _run_single_ticker(
|
|||
|
||||
ticker_started = datetime.now(ZoneInfo(config.run.timezone))
|
||||
timer_start = perf_counter()
|
||||
analysis_date = ticker_started.date().isoformat()
|
||||
|
||||
try:
|
||||
trade_date = resolve_trade_date(ticker, config)
|
||||
|
|
@ -169,7 +170,11 @@ def _run_single_ticker(
|
|||
config=_graph_config(config, engine_results_dir),
|
||||
callbacks=[stats_handler],
|
||||
)
|
||||
final_state, decision = graph.propagate(ticker, trade_date)
|
||||
final_state, decision = graph.propagate(
|
||||
ticker,
|
||||
trade_date,
|
||||
analysis_date=analysis_date,
|
||||
)
|
||||
|
||||
report_dir = ticker_dir / "report"
|
||||
report_file = save_report_bundle(
|
||||
|
|
@ -198,6 +203,7 @@ def _run_single_ticker(
|
|||
"ticker": ticker,
|
||||
"status": "success",
|
||||
"trade_date": trade_date,
|
||||
"analysis_date": analysis_date,
|
||||
"decision": str(decision),
|
||||
"started_at": ticker_started.isoformat(),
|
||||
"finished_at": datetime.now(ZoneInfo(config.run.timezone)).isoformat(),
|
||||
|
|
@ -216,6 +222,7 @@ def _run_single_ticker(
|
|||
"ticker": ticker,
|
||||
"status": "success",
|
||||
"trade_date": trade_date,
|
||||
"analysis_date": analysis_date,
|
||||
"decision": str(decision),
|
||||
"started_at": ticker_started.isoformat(),
|
||||
"finished_at": analysis_payload["finished_at"],
|
||||
|
|
@ -232,6 +239,7 @@ def _run_single_ticker(
|
|||
error_payload = {
|
||||
"ticker": ticker,
|
||||
"status": "failed",
|
||||
"analysis_date": analysis_date,
|
||||
"error": str(exc),
|
||||
"traceback": traceback.format_exc(),
|
||||
"started_at": ticker_started.isoformat(),
|
||||
|
|
@ -244,6 +252,7 @@ def _run_single_ticker(
|
|||
return {
|
||||
"ticker": ticker,
|
||||
"status": "failed",
|
||||
"analysis_date": analysis_date,
|
||||
"trade_date": None,
|
||||
"decision": None,
|
||||
"error": str(exc),
|
||||
|
|
@ -285,6 +294,7 @@ def _serialize_final_state(final_state: dict[str, Any]) -> dict[str, Any]:
|
|||
return {
|
||||
"company_of_interest": final_state.get("company_of_interest"),
|
||||
"trade_date": final_state.get("trade_date"),
|
||||
"analysis_date": final_state.get("analysis_date"),
|
||||
"market_report": final_state.get("market_report"),
|
||||
"sentiment_report": final_state.get("sentiment_report"),
|
||||
"news_report": final_state.get("news_report"),
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ def _render_run_page(manifest: dict[str, Any], settings: SiteSettings) -> str:
|
|||
<a href="{_escape(ticker_summary['ticker'])}.html">{_escape(ticker_summary['ticker'])}</a>
|
||||
<span class="status {ticker_summary['status']}">{_escape(ticker_summary['status'])}</span>
|
||||
</div>
|
||||
<p><strong>Analysis date</strong><span>{_escape(ticker_summary.get('analysis_date') or '-')}</span></p>
|
||||
<p><strong>Trade date</strong><span>{_escape(ticker_summary.get('trade_date') or '-')}</span></p>
|
||||
<p><strong>Duration</strong><span>{ticker_summary.get('duration_seconds', 0):.1f}s</span></p>
|
||||
<p><strong>Decision</strong><span>{_escape(ticker_summary.get('decision') or ticker_summary.get('error') or '-')}</span></p>
|
||||
|
|
@ -239,10 +240,12 @@ def _render_ticker_page(
|
|||
<div>
|
||||
<p class="eyebrow">Ticker report</p>
|
||||
<h1>{_escape(ticker_summary['ticker'])}</h1>
|
||||
<p class="subtitle">{_escape(ticker_summary.get('trade_date') or '-')} / {_escape(ticker_summary['status'])}</p>
|
||||
<p class="subtitle">Analysis {_escape(ticker_summary.get('analysis_date') or '-')} / Market {_escape(ticker_summary.get('trade_date') or '-')} / {_escape(ticker_summary['status'])}</p>
|
||||
</div>
|
||||
<div class="hero-card">
|
||||
<div class="status {ticker_summary['status']}">{_escape(ticker_summary['status'])}</div>
|
||||
<p><strong>Analysis date</strong><span>{_escape(ticker_summary.get('analysis_date') or '-')}</span></p>
|
||||
<p><strong>Trade date</strong><span>{_escape(ticker_summary.get('trade_date') or '-')}</span></p>
|
||||
<p><strong>Decision</strong><span>{_escape(ticker_summary.get('decision') or '-')}</span></p>
|
||||
<p><strong>Duration</strong><span>{ticker_summary.get('duration_seconds', 0):.1f}s</span></p>
|
||||
<p><strong>LLM calls</strong><span>{ticker_summary.get('metrics', {}).get('llm_calls', 0)}</span></p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue