Files
evotraders/frontend/src/hooks/useStockExplainData.js
2026-03-30 17:46:44 +08:00

547 lines
16 KiB
JavaScript

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
};
}