diff --git a/backend/data/market_ingest.py b/backend/data/market_ingest.py index 6c6bb26..5af55c1 100644 --- a/backend/data/market_ingest.py +++ b/backend/data/market_ingest.py @@ -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], *, diff --git a/backend/services/gateway_cycle_support.py b/backend/services/gateway_cycle_support.py index 363e07c..a04cf36 100644 --- a/backend/services/gateway_cycle_support.py +++ b/backend/services/gateway_cycle_support.py @@ -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...") diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 97ad9b7..a4ce83f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -466,6 +466,7 @@ export default function LiveTradingApp() { currentDate={currentDate} stockRequests={stockRequests} agentRequests={agentRequests} + agentProfilesByAgent={agentProfilesByAgent} leftWidth={leftWidth} isResizing={isResizing} onMouseDown={() => useUIStore.getState().setIsResizing(true)} diff --git a/frontend/src/components/AgentFeed.jsx b/frontend/src/components/AgentFeed.jsx index c45bf09..36bef9e 100644 --- a/frontend/src/components/AgentFeed.jsx +++ b/frontend/src/components/AgentFeed.jsx @@ -35,14 +35,22 @@ const stripMarkdown = (text) => { .replace(/^[-=]+$/gm, ''); }; -const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => { +const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref) => { const feedContentRef = useRef(null); const [highlightedId, setHighlightedId] = useState(null); const [selectedAgent, setSelectedAgent] = useState('all'); const [dropdownOpen, setDropdownOpen] = useState(false); const getAgentModelInfo = (agentId) => { - if (!leaderboard || !agentId) return { modelName: null, modelProvider: null }; + if (!agentId) return { modelName: null, modelProvider: null }; + const profile = agentProfilesByAgent?.[agentId]; + if (profile?.model_name) { + return { + modelName: profile.model_name, + modelProvider: profile.model_provider + }; + } + if (!leaderboard) return { modelName: null, modelProvider: null }; const agentData = leaderboard.find(lb => lb.id === agentId || lb.agentId === agentId); return { modelName: agentData?.modelName, @@ -52,7 +60,17 @@ const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => { // Get agent info by name const getAgentInfoByName = (agentName) => { - if (!leaderboard || !agentName) return null; + if (!agentName) return null; + const agentConfig = AGENTS.find((agent) => agent.name === agentName); + const profile = agentConfig ? agentProfilesByAgent?.[agentConfig.id] : null; + if (agentConfig && profile?.model_name) { + return { + agentId: agentConfig.id, + modelName: profile.model_name, + modelProvider: profile.model_provider + }; + } + if (!leaderboard) return null; const agentData = leaderboard.find(lb => lb.name === agentName || lb.agentName === agentName); if (!agentData) return null; return { diff --git a/frontend/src/components/AppShell.jsx b/frontend/src/components/AppShell.jsx index 6d4b03b..86eca78 100644 --- a/frontend/src/components/AppShell.jsx +++ b/frontend/src/components/AppShell.jsx @@ -128,6 +128,7 @@ export default function AppShell({ stockRequests, // Agent request handlers agentRequests, + agentProfilesByAgent, // Layout leftWidth, isResizing, @@ -440,6 +441,7 @@ export default function AppShell({ bubbles={bubbles} bubbleFor={bubbleFor} leaderboard={leaderboard} + agentProfilesByAgent={agentProfilesByAgent} feed={feed} onJumpToMessage={handleJumpToMessage} onOpenLaunchConfig={() => setIsRuntimeSettingsOpen(true)} @@ -518,6 +520,7 @@ export default function AppShell({ trades={trades} holdings={holdings} stats={stats} + portfolioData={portfolioData} baseline_vw={portfolioData.baseline_vw} equity={portfolioData.equity} leaderboard={leaderboard} @@ -535,7 +538,7 @@ export default function AppShell({ {/* Right Panel: Agent Feed */}