import { useCallback, useEffect } from "react"; import { fetchNewsCategoriesDirect, fetchNewsForDateDirect, fetchRangeExplainDirect, fetchSimilarDaysDirect, fetchStockStoryDirect, hasDirectNewsService } from "../services/newsApi"; import { fetchInsiderTradesDirect, fetchStockHistoryDirect, hasDirectTradingService } from "../services/tradingApi"; export function useStockExplainData({ clientRef, currentDate, currentView, selectedExplainSymbol, requestedStockHistoryRef, setOhlcHistoryByTicker, setPriceHistoryByTicker, setHistorySourceByTicker, setNewsByTicker, setInsiderTradesByTicker }) { const requestStockHistory = useCallback((symbol, { force = false } = {}) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized) { return false; } if (!force && requestedStockHistoryRef.current.has(normalized)) { return false; } const endDate = currentDate ? String(currentDate).slice(0, 10) : new Date().toISOString().slice(0, 10); const end = new Date(`${endDate}T00:00:00`); const start = new Date(end); start.setDate(start.getDate() - 120); const startDate = start.toISOString().slice(0, 10); if (hasDirectTradingService()) { void fetchStockHistoryDirect(normalized, startDate, endDate) .then((payload) => { const prices = Array.isArray(payload?.prices) ? payload.prices : []; setOhlcHistoryByTicker((prev) => ({ ...prev, [normalized]: prices })); setPriceHistoryByTicker((prev) => ({ ...prev, [normalized]: prices .map((point) => { const price = Number(point?.close); const timestamp = point?.time; if (!timestamp || !Number.isFinite(price)) { return null; } return { timestamp: String(timestamp), label: String(timestamp), price }; }) .filter(Boolean) })); setHistorySourceByTicker((prev) => ({ ...prev, [normalized]: "trading_service" })); }) .catch((error) => { console.error("Direct stock-history fetch failed, falling back to websocket:", error); if (clientRef.current) { const success = clientRef.current.send({ type: "get_stock_history", ticker: normalized, lookback_days: 120 }); if (success) { requestedStockHistoryRef.current.add(normalized); } } }); requestedStockHistoryRef.current.add(normalized); return true; } if (!clientRef.current) { return false; } const success = clientRef.current.send({ type: "get_stock_history", ticker: normalized, lookback_days: 120 }); if (success) { requestedStockHistoryRef.current.add(normalized); } return success; }, [ clientRef, currentDate, requestedStockHistoryRef, setHistorySourceByTicker, setOhlcHistoryByTicker, setPriceHistoryByTicker ]); const requestStockExplainEvents = useCallback((symbol) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized || !clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_explain_events", ticker: normalized }); }, [clientRef]); const requestStockNews = useCallback((symbol) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized || !clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_news", ticker: normalized, lookback_days: 45, limit: 12 }); }, [clientRef]); const requestStockNewsForDate = useCallback((symbol, date) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized || !date) { return false; } if (hasDirectNewsService()) { void fetchNewsForDateDirect(normalized, date, 20) .then((payload) => { const targetDate = typeof payload?.date === "string" ? payload.date.trim() : date; const news = Array.isArray(payload?.news) ? payload.news : []; const freshness = payload?.freshness || null; setNewsByTicker((prev) => ({ ...prev, [normalized]: { ...(prev[normalized] || {}), byDate: { ...((prev[normalized] && prev[normalized].byDate) || {}), [targetDate]: news }, byDateFreshness: { ...((prev[normalized] && prev[normalized].byDateFreshness) || {}), [targetDate]: freshness } } })); }) .catch((error) => { console.error("Direct news-for-date fetch failed, falling back to websocket:", error); if (clientRef.current) { clientRef.current.send({ type: "get_stock_news_for_date", ticker: normalized, date, limit: 20 }); } }); return true; } if (!clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_news_for_date", ticker: normalized, date, limit: 20 }); }, [clientRef, setNewsByTicker]); const requestStockNewsTimeline = useCallback((symbol) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized || !clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_news_timeline", ticker: normalized, lookback_days: 90 }); }, [clientRef]); const requestStockNewsCategories = useCallback((symbol) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized) { return false; } const endDate = currentDate ? String(currentDate).slice(0, 10) : new Date().toISOString().slice(0, 10); const end = new Date(`${endDate}T00:00:00`); const start = new Date(end); start.setDate(start.getDate() - 90); const startDate = start.toISOString().slice(0, 10); if (hasDirectNewsService()) { void fetchNewsCategoriesDirect(normalized, startDate, endDate, 200) .then((payload) => { const freshness = payload?.freshness || null; setNewsByTicker((prev) => ({ ...prev, [normalized]: { ...(prev[normalized] || {}), categories: payload?.categories || {}, categoriesStartDate: startDate, categoriesEndDate: endDate, categoriesFreshness: freshness } })); }) .catch((error) => { console.error("Direct news-categories fetch failed, falling back to websocket:", error); if (clientRef.current) { clientRef.current.send({ type: "get_stock_news_categories", ticker: normalized, lookback_days: 90 }); } }); return true; } if (!clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_news_categories", ticker: normalized, lookback_days: 90 }); }, [clientRef, currentDate, setNewsByTicker]); const requestStockInsiderTrades = useCallback((symbol, startDate = null, endDate = null) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized) { return false; } if (hasDirectTradingService()) { void fetchInsiderTradesDirect(normalized, startDate, endDate, 50) .then((payload) => { const rows = Array.isArray(payload?.insider_trades) ? payload.insider_trades : []; setInsiderTradesByTicker((prev) => ({ ...prev, [normalized]: { ticker: normalized, startDate: startDate || null, endDate: endDate || null, trades: rows } })); }) .catch((error) => { console.error("Direct insider-trades fetch failed, falling back to websocket:", error); if (clientRef.current) { clientRef.current.send({ type: "get_stock_insider_trades", ticker: normalized, start_date: startDate, end_date: endDate, limit: 50 }); } }); return true; } if (!clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_insider_trades", ticker: normalized, start_date: startDate, end_date: endDate, limit: 50 }); }, [clientRef, setInsiderTradesByTicker]); const requestStockTechnicalIndicators = useCallback((symbol) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized || !clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_technical_indicators", ticker: normalized }); }, [clientRef]); const requestStockRangeExplain = useCallback((symbol, startDate, endDate, articleIds = []) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized || !startDate || !endDate) { return false; } if (hasDirectNewsService()) { void fetchRangeExplainDirect(normalized, startDate, endDate, articleIds) .then((payload) => { const result = payload?.result && typeof payload.result === "object" ? payload.result : null; const freshness = payload?.freshness || null; if (!result?.start_date || !result?.end_date) { return; } const cacheKey = `${result.start_date}:${result.end_date}`; setNewsByTicker((prev) => ({ ...prev, [normalized]: { ...(prev[normalized] || {}), rangeExplainCache: { ...((prev[normalized] && prev[normalized].rangeExplainCache) || {}), [cacheKey]: { ...result, freshness } } } })); }) .catch((error) => { console.error("Direct range explain fetch failed, falling back to websocket:", error); if (clientRef.current) { clientRef.current.send({ type: "get_stock_range_explain", ticker: normalized, start_date: startDate, end_date: endDate, article_ids: Array.isArray(articleIds) ? articleIds : [] }); } }); return true; } if (!clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_range_explain", ticker: normalized, start_date: startDate, end_date: endDate, article_ids: Array.isArray(articleIds) ? articleIds : [] }); }, [clientRef, setNewsByTicker]); const requestStockStory = useCallback((symbol, asOfDate = null) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized) { return false; } if (hasDirectNewsService()) { void fetchStockStoryDirect(normalized, asOfDate) .then((payload) => { const storyDate = typeof payload?.as_of_date === "string" ? payload.as_of_date.trim() : ""; const freshness = payload?.freshness || null; if (!storyDate) { return; } setNewsByTicker((prev) => ({ ...prev, [normalized]: { ...(prev[normalized] || {}), storyCache: { ...((prev[normalized] && prev[normalized].storyCache) || {}), [storyDate]: { story: payload.story || "", source: payload.source || "news_service", asOfDate: storyDate, freshness } } } })); }) .catch((error) => { console.error("Direct story fetch failed, falling back to websocket:", error); if (clientRef.current) { clientRef.current.send({ type: "get_stock_story", ticker: normalized, as_of_date: asOfDate }); } }); return true; } if (!clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_story", ticker: normalized, as_of_date: asOfDate }); }, [clientRef, setNewsByTicker]); const requestStockSimilarDays = useCallback((symbol, date, topK = 8) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized || !date) { return false; } if (hasDirectNewsService()) { void fetchSimilarDaysDirect(normalized, date, topK) .then((payload) => { const targetDate = typeof payload?.target_date === "string" ? payload.target_date.trim() : date; if (!targetDate) { return; } setNewsByTicker((prev) => ({ ...prev, [normalized]: { ...(prev[normalized] || {}), similarDaysCache: { ...((prev[normalized] && prev[normalized].similarDaysCache) || {}), [targetDate]: payload } } })); }) .catch((error) => { console.error("Direct similar-days fetch failed, falling back to websocket:", error); if (clientRef.current) { clientRef.current.send({ type: "get_stock_similar_days", ticker: normalized, date, top_k: topK }); } }); return true; } if (!clientRef.current) { return false; } return clientRef.current.send({ type: "get_stock_similar_days", ticker: normalized, date, top_k: topK }); }, [clientRef, setNewsByTicker]); const requestStockEnrich = useCallback((symbol, options = {}) => { const normalized = typeof symbol === "string" ? symbol.trim().toUpperCase() : ""; if (!normalized || !clientRef.current) { return false; } const startDate = typeof options.startDate === "string" ? options.startDate.trim() : ""; const endDate = typeof options.endDate === "string" ? options.endDate.trim() : ""; if (!startDate || !endDate) { return false; } setNewsByTicker((prev) => ({ ...prev, [normalized]: { ...(prev[normalized] || {}), maintenanceStatus: { running: true, error: null, updatedAt: new Date().toISOString(), stats: null } } })); return clientRef.current.send({ type: "run_stock_enrich", ticker: normalized, start_date: startDate, end_date: endDate, force: Boolean(options.force), only_local_to_llm: Boolean(options.onlyLocalToLlm), rebuild_story: Boolean(options.rebuildStory), rebuild_similar_days: Boolean(options.rebuildSimilarDays), story_date: options.storyDate || null, target_date: options.targetDate || null }); }, [clientRef, setNewsByTicker]); useEffect(() => { if (currentView !== "explain" || !selectedExplainSymbol) { return; } requestStockHistory(selectedExplainSymbol); requestStockExplainEvents(selectedExplainSymbol); requestStockNews(selectedExplainSymbol); requestStockNewsTimeline(selectedExplainSymbol); requestStockNewsCategories(selectedExplainSymbol); requestStockStory(selectedExplainSymbol, currentDate); }, [ currentDate, currentView, requestStockExplainEvents, requestStockHistory, requestStockNews, requestStockNewsCategories, requestStockNewsTimeline, requestStockStory, selectedExplainSymbol ]); return { requestStockHistory, requestStockExplainEvents, requestStockNews, requestStockNewsForDate, requestStockNewsTimeline, requestStockNewsCategories, requestStockInsiderTrades, requestStockTechnicalIndicators, requestStockRangeExplain, requestStockStory, requestStockSimilarDays, requestStockEnrich }; }