547 lines
16 KiB
JavaScript
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
|
|
};
|
|
}
|