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:
@@ -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],
|
||||
*,
|
||||
|
||||
@@ -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...")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user