feat: 添加新闻增量刷新和前端组件修复

- 新增 refresh_news_incremental/refresh_news_for_symbols 函数支持增量新闻获取
- 在 live cycle 中集成新闻刷新逻辑
- AgentFeed 支持 agentProfilesByAgent 显示模型信息
- StatisticsView 修复 stats 计算逻辑,使用 portfolioData 作为 fallback
- StockExplainView 修复 useEffect 依赖项问题
- AppShell/RoomView 传递 agentProfilesByAgent 属性
- start-dev.sh 调整日志级别为 warning 减少噪音

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 10:50:45 +08:00
parent 16bb3c4211
commit 7e7a58769a
9 changed files with 196 additions and 21 deletions

View File

@@ -8,6 +8,7 @@ from typing import Iterable
from backend.data.market_store import MarketStore
from backend.data.news_alignment import align_news_for_symbol
from backend.data.provider_router import ProviderRouter
from backend.data.polygon_client import (
fetch_news,
fetch_ohlc,
@@ -24,6 +25,35 @@ def _default_start(years: int = 2) -> str:
return (datetime.now(timezone.utc).date() - timedelta(days=years * 366)).isoformat()
def _normalize_provider_news_rows(ticker: str, news_items: Iterable[Any]) -> list[dict]:
rows: list[dict] = []
for item in news_items:
payload = item.model_dump() if hasattr(item, "model_dump") else dict(item or {})
related = payload.get("related")
if isinstance(related, str):
related_list = [value.strip().upper() for value in related.split(",") if value.strip()]
elif isinstance(related, list):
related_list = [str(value).strip().upper() for value in related if str(value).strip()]
else:
related_list = []
if ticker not in related_list:
related_list.append(ticker)
rows.append(
{
"title": payload.get("title"),
"description": payload.get("summary"),
"summary": payload.get("summary"),
"article_url": payload.get("url"),
"published_utc": payload.get("date"),
"publisher": payload.get("source"),
"tickers": related_list,
"category": payload.get("category"),
"raw_json": payload,
}
)
return rows
def ingest_ticker_history(
symbol: str,
*,
@@ -114,6 +144,80 @@ def update_ticker_incremental(
}
def refresh_news_incremental(
symbol: str,
*,
end_date: str | None = None,
store: MarketStore | None = None,
) -> dict:
"""Incrementally fetch company news using the configured provider router."""
ticker = normalize_symbol(symbol)
market_store = store or MarketStore()
watermarks = market_store.get_ticker_watermarks(ticker)
end = end_date or _today_utc()
start_news = (
(datetime.fromisoformat(watermarks["last_news_fetch"]) + timedelta(days=1)).date().isoformat()
if watermarks.get("last_news_fetch")
else _default_start()
)
if start_news > end:
return {
"symbol": ticker,
"start_news_date": start_news,
"end_date": end,
"news": 0,
"aligned": 0,
}
router = ProviderRouter()
news_items, source = router.get_company_news(
ticker=ticker,
start_date=start_news,
end_date=end,
limit=1000,
)
news_rows = _normalize_provider_news_rows(ticker, news_items)
news_count = market_store.upsert_news(ticker, news_rows, source=source) if news_rows else 0
aligned_count = align_news_for_symbol(market_store, ticker)
market_store.update_fetch_watermark(
symbol=ticker,
news_date=end if news_rows or watermarks.get("last_news_fetch") else None,
)
return {
"symbol": ticker,
"start_news_date": start_news,
"end_date": end,
"news": news_count,
"aligned": aligned_count,
"source": source,
}
def refresh_news_for_symbols(
symbols: Iterable[str],
*,
end_date: str | None = None,
store: MarketStore | None = None,
) -> list[dict]:
"""Incrementally refresh company news for a list of tickers."""
market_store = store or MarketStore()
results = []
for symbol in symbols:
ticker = normalize_symbol(symbol)
if not ticker:
continue
results.append(
refresh_news_incremental(
ticker,
end_date=end_date,
store=market_store,
)
)
return results
def ingest_symbols(
symbols: Iterable[str],
*,

View File

@@ -7,7 +7,7 @@ import asyncio
import logging
from typing import Any
from backend.data.market_ingest import ingest_symbols
from backend.data.market_ingest import ingest_symbols, refresh_news_for_symbols
from backend.domains import trading as trading_domain
from backend.utils.msg_adapter import FrontendAdapter
@@ -200,6 +200,23 @@ async def run_live_cycle(gateway: Any, date: str, tickers: list[str]) -> None:
trading_date = gateway.market_service.get_live_trading_date()
logger.info("Live cycle: triggered=%s, trading_date=%s", date, trading_date)
try:
news_refresh = await asyncio.to_thread(
refresh_news_for_symbols,
tickers,
end_date=trading_date,
store=gateway.storage.market_store,
)
logger.info(
"News refresh complete: %s",
", ".join(
f"{item['symbol']} news={item['news']}"
for item in news_refresh
) or "no symbols",
)
except Exception as exc:
logger.warning("Live cycle news refresh failed: %s", exc)
await gateway.state_sync.on_cycle_start(trading_date)
gateway._dashboard.update(date=trading_date, status="Analyzing...")