diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e7c7415..0ad2379 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -22,6 +22,11 @@ import { // Hooks import { useFeedProcessor } from './hooks/useFeedProcessor'; +import { useRuntimeStore } from './store/runtimeStore'; +import { useMarketStore } from './store/marketStore'; +import { usePortfolioStore } from './store/portfolioStore'; +import { useAgentStore } from './store/agentStore'; +import { useUIStore } from './store/uiStore'; // Styles import GlobalStyles from './styles/GlobalStyles'; @@ -70,100 +75,73 @@ function ViewLoadingFallback({ label = '加载中...' }) { */ export default function LiveTradingApp() { - const [isConnected, setIsConnected] = useState(false); - const [connectionStatus, setConnectionStatus] = useState('connecting'); // 'connecting' | 'connected' | 'disconnected' - const [systemStatus, setSystemStatus] = useState('initializing'); // 'initializing' | 'running' | 'completed' - const [currentDate, setCurrentDate] = useState(null); - const [progress, setProgress] = useState({ current: 0, total: 0 }); - const [now, setNow] = useState(() => new Date()); + // Connection & system state - from runtimeStore + const { isConnected, setIsConnected, connectionStatus, setConnectionStatus, systemStatus, setSystemStatus, currentDate, setCurrentDate, progress, setProgress, now, setNow } = useRuntimeStore(); // View toggle: 'traders' | 'room' | 'explain' | 'chart' | 'statistics' | 'runtime' - const [currentView, setCurrentView] = useState('traders'); - const [isInitialAnimating, setIsInitialAnimating] = useState(true); - const [lastUpdate, setLastUpdate] = useState(new Date()); - const [isUpdating, setIsUpdating] = useState(false); + const { currentView, setCurrentView, chartTab, setChartTab, isInitialAnimating, setIsInitialAnimating, lastUpdate, setLastUpdate, isUpdating, setIsUpdating } = useUIStore(); - // Chart data - const [chartTab, setChartTab] = useState('all'); - const [portfolioData, setPortfolioData] = useState({ - netValue: 10000, - pnl: 0, - equity: [], - baseline: [], // Baseline strategy (Buy & Hold - Equal Weight) - baseline_vw: [], // Baseline strategy (Buy & Hold - Value Weighted) - momentum: [], // Momentum strategy - strategies: [] // Other strategies - }); + // Chart data - from portfolioStore + const { portfolioData, setPortfolioData, holdings, setHoldings, trades, setTrades, stats, setStats, leaderboard, setLeaderboard } = usePortfolioStore(); // Feed data (using hook for simplified processing) const { feed, processHistoricalFeed, processFeedEvent, addSystemMessage } = useFeedProcessor(); - // Statistics data - const [holdings, setHoldings] = useState([]); - const [trades, setTrades] = useState([]); - const [stats, setStats] = useState(null); - const [leaderboard, setLeaderboard] = useState([]); - - // Ticker prices (now from real-time data) + // Ticker prices - keep local state with INITIAL_TICKERS const [tickers, setTickers] = useState(INITIAL_TICKERS); - const [rollingTickers, setRollingTickers] = useState({}); - const [priceHistoryByTicker, setPriceHistoryByTicker] = useState({}); - const [ohlcHistoryByTicker, setOhlcHistoryByTicker] = useState({}); - const [explainEventsByTicker, setExplainEventsByTicker] = useState({}); - const [newsByTicker, setNewsByTicker] = useState({}); - const [insiderTradesByTicker, setInsiderTradesByTicker] = useState({}); - const [technicalIndicatorsByTicker, setTechnicalIndicatorsByTicker] = useState({}); - const [selectedExplainSymbol, setSelectedExplainSymbol] = useState(''); - const [historySourceByTicker, setHistorySourceByTicker] = useState({}); + const { rollingTickers, setRollingTickers, priceHistoryByTicker, setPriceHistoryByTicker, ohlcHistoryByTicker, setOhlcHistoryByTicker, explainEventsByTicker, setExplainEventsByTicker, newsByTicker, setNewsByTicker, insiderTradesByTicker, setInsiderTradesByTicker, technicalIndicatorsByTicker, setTechnicalIndicatorsByTicker, selectedExplainSymbol, setSelectedExplainSymbol, historySourceByTicker, setHistorySourceByTicker } = useMarketStore(); - // Room bubbles - const [bubbles, setBubbles] = useState({}); + // Room bubbles - from uiStore + const { bubbles, setBubbles, leftWidth, setLeftWidth, isResizing, setIsResizing } = useUIStore(); - // Resizable panels - const [leftWidth, setLeftWidth] = useState(70); // percentage - const [isResizing, setIsResizing] = useState(false); + // Market status & runtime config - from runtimeStore + const { + serverMode, setServerMode, + marketStatus, setMarketStatus, + virtualTime, setVirtualTime, + dataSources, setDataSources, + runtimeConfig, setRuntimeConfig, + isWatchlistPanelOpen, setIsWatchlistPanelOpen, + isRuntimeSettingsOpen, setIsRuntimeSettingsOpen, + watchlistDraftSymbols, setWatchlistDraftSymbols, + watchlistInputValue, setWatchlistInputValue, + watchlistFeedback, setWatchlistFeedback, + isWatchlistSaving, setIsWatchlistSaving, + scheduleModeDraft, setScheduleModeDraft, + intervalMinutesDraft, setIntervalMinutesDraft, + triggerTimeDraft, setTriggerTimeDraft, + maxCommCyclesDraft, setMaxCommCyclesDraft, + initialCashDraft, setInitialCashDraft, + marginRequirementDraft, setMarginRequirementDraft, + enableMemoryDraft, setEnableMemoryDraft, + modeDraft, setModeDraft, + pollIntervalDraft, setPollIntervalDraft, + startDateDraft, setStartDateDraft, + endDateDraft, setEndDateDraft, + enableMockDraft, setEnableMockDraft, + runtimeConfigFeedback, setRuntimeConfigFeedback, + isRuntimeConfigSaving, setIsRuntimeConfigSaving, + lastDayHistory, setLastDayHistory + } = useRuntimeStore(); - // Market status - const [serverMode, setServerMode] = useState(null); // 'live' | 'backtest' | null - const [marketStatus, setMarketStatus] = useState(null); // { status, status_text, ... } - const [virtualTime, setVirtualTime] = useState(null); // Virtual time from server (for mock mode) - const [dataSources, setDataSources] = useState(null); - const [runtimeConfig, setRuntimeConfig] = useState(null); - const [isWatchlistPanelOpen, setIsWatchlistPanelOpen] = useState(false); - const [isRuntimeSettingsOpen, setIsRuntimeSettingsOpen] = useState(false); - const [watchlistDraftSymbols, setWatchlistDraftSymbols] = useState([]); - const [watchlistInputValue, setWatchlistInputValue] = useState(''); - const [watchlistFeedback, setWatchlistFeedback] = useState(null); - const [isWatchlistSaving, setIsWatchlistSaving] = useState(false); - const [scheduleModeDraft, setScheduleModeDraft] = useState('daily'); - const [intervalMinutesDraft, setIntervalMinutesDraft] = useState('60'); - const [triggerTimeDraft, setTriggerTimeDraft] = useState('09:30'); - const [maxCommCyclesDraft, setMaxCommCyclesDraft] = useState('2'); - const [initialCashDraft, setInitialCashDraft] = useState('100000'); - const [marginRequirementDraft, setMarginRequirementDraft] = useState('0'); - const [enableMemoryDraft, setEnableMemoryDraft] = useState(false); - const [modeDraft, setModeDraft] = useState('live'); - const [pollIntervalDraft, setPollIntervalDraft] = useState('10'); - const [startDateDraft, setStartDateDraft] = useState(''); - const [endDateDraft, setEndDateDraft] = useState(''); - const [enableMockDraft, setEnableMockDraft] = useState(false); - const [runtimeConfigFeedback, setRuntimeConfigFeedback] = useState(null); - const [isRuntimeConfigSaving, setIsRuntimeConfigSaving] = useState(false); - const [selectedSkillAgentId, setSelectedSkillAgentId] = useState(AGENTS[0]?.id || 'portfolio_manager'); - const [agentProfilesByAgent, setAgentProfilesByAgent] = useState({}); - const [agentSkillsByAgent, setAgentSkillsByAgent] = useState({}); - const [skillDetailsByName, setSkillDetailsByName] = useState({}); - const [localSkillDraftsByKey, setLocalSkillDraftsByKey] = useState({}); - const [isAgentSkillsLoading, setIsAgentSkillsLoading] = useState(false); - const [skillDetailLoadingKey, setSkillDetailLoadingKey] = useState(null); - const [agentSkillsSavingKey, setAgentSkillsSavingKey] = useState(null); - const [agentSkillsFeedback, setAgentSkillsFeedback] = useState(null); - const [selectedWorkspaceFile, setSelectedWorkspaceFile] = useState(EDITABLE_AGENT_WORKSPACE_FILES[0]); - const [workspaceFilesByAgent, setWorkspaceFilesByAgent] = useState({}); - const [workspaceDraftContent, setWorkspaceDraftContent] = useState(''); - const [isWorkspaceFileLoading, setIsWorkspaceFileLoading] = useState(false); - const [workspaceFileSavingKey, setWorkspaceFileSavingKey] = useState(null); - const [workspaceFileFeedback, setWorkspaceFileFeedback] = useState(null); + // Agent state - from agentStore + const { + selectedSkillAgentId, setSelectedSkillAgentId, + agentProfilesByAgent, setAgentProfilesByAgent, + agentSkillsByAgent, setAgentSkillsByAgent, + skillDetailsByName, setSkillDetailsByName, + localSkillDraftsByKey, setLocalSkillDraftsByKey, + isAgentSkillsLoading, setIsAgentSkillsLoading, + skillDetailLoadingKey, setSkillDetailLoadingKey, + agentSkillsSavingKey, setAgentSkillsSavingKey, + agentSkillsFeedback, setAgentSkillsFeedback, + selectedWorkspaceFile, setSelectedWorkspaceFile, + workspaceFilesByAgent, setWorkspaceFilesByAgent, + workspaceDraftContent, setWorkspaceDraftContent, + isWorkspaceFileLoading, setIsWorkspaceFileLoading, + workspaceFileSavingKey, setWorkspaceFileSavingKey, + workspaceFileFeedback, setWorkspaceFileFeedback + } = useAgentStore(); const clientRef = useRef(null); const containerRef = useRef(null); @@ -176,9 +154,6 @@ export default function LiveTradingApp() { const lastVirtualTimeRef = useRef(null); const virtualTimeOffsetRef = useRef(0); - // Last day history for replay - const [lastDayHistory, setLastDayHistory] = useState([]); - const buildTickersFromSymbols = useCallback((symbols, previousTickers = []) => { if (!Array.isArray(symbols) || symbols.length === 0) { return previousTickers; diff --git a/frontend/src/services/newsApi.js b/frontend/src/services/newsApi.js new file mode 100644 index 0000000..6221ddd --- /dev/null +++ b/frontend/src/services/newsApi.js @@ -0,0 +1,110 @@ +const trimTrailingSlash = (value) => value.replace(/\/+$/, ''); +const isLocalDevHost = () => { + if (typeof window === 'undefined') { + return false; + } + const host = String(window.location.hostname || '').trim().toLowerCase(); + return host === 'localhost' || host === '127.0.0.1'; +}; +const NEWS_SERVICE_BASE = trimTrailingSlash(import.meta.env.VITE_NEWS_SERVICE_URL || '') || ( + isLocalDevHost() ? 'http://localhost:8002' : '' +); + +export function hasDirectNewsService() { + return Boolean(NEWS_SERVICE_BASE); +} + +export async function fetchStockStoryDirect(ticker, asOfDate) { + if (!NEWS_SERVICE_BASE) { + throw new Error('Direct news service is not configured'); + } + + const params = new URLSearchParams(); + if (asOfDate) { + params.set('as_of_date', asOfDate); + } + + const query = params.toString(); + const url = `${NEWS_SERVICE_BASE}/api/stories/${encodeURIComponent(ticker)}${query ? `?${query}` : ''}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(await response.text()); + } + return response.json(); +} + +export async function fetchSimilarDaysDirect(ticker, date, topK = 8) { + if (!NEWS_SERVICE_BASE) { + throw new Error('Direct news service is not configured'); + } + + const params = new URLSearchParams(); + params.set('ticker', ticker); + params.set('date', date); + params.set('n_similar', String(topK)); + + const response = await fetch(`${NEWS_SERVICE_BASE}/api/similar-days?${params.toString()}`); + if (!response.ok) { + throw new Error(await response.text()); + } + return response.json(); +} + +export async function fetchRangeExplainDirect(ticker, startDate, endDate, articleIds = []) { + if (!NEWS_SERVICE_BASE) { + throw new Error('Direct news service is not configured'); + } + + const params = new URLSearchParams(); + params.set('ticker', ticker); + params.set('start_date', startDate); + params.set('end_date', endDate); + for (const articleId of Array.isArray(articleIds) ? articleIds : []) { + params.append('article_ids', articleId); + } + + const response = await fetch(`${NEWS_SERVICE_BASE}/api/range-explain?${params.toString()}`); + if (!response.ok) { + throw new Error(await response.text()); + } + return response.json(); +} + +export async function fetchNewsForDateDirect(ticker, date, limit = 20) { + if (!NEWS_SERVICE_BASE) { + throw new Error('Direct news service is not configured'); + } + + const params = new URLSearchParams(); + params.set('ticker', ticker); + params.set('date', date); + params.set('limit', String(limit)); + + const response = await fetch(`${NEWS_SERVICE_BASE}/api/news-for-date?${params.toString()}`); + if (!response.ok) { + throw new Error(await response.text()); + } + return response.json(); +} + +export async function fetchNewsCategoriesDirect(ticker, startDate, endDate, limit = 200) { + if (!NEWS_SERVICE_BASE) { + throw new Error('Direct news service is not configured'); + } + + const params = new URLSearchParams(); + params.set('ticker', ticker); + params.set('limit', String(limit)); + if (startDate) { + params.set('start_date', startDate); + } + if (endDate) { + params.set('end_date', endDate); + } + + const response = await fetch(`${NEWS_SERVICE_BASE}/api/categories?${params.toString()}`); + if (!response.ok) { + throw new Error(await response.text()); + } + return response.json(); +} diff --git a/frontend/src/services/tradingApi.js b/frontend/src/services/tradingApi.js new file mode 100644 index 0000000..b5cca7b --- /dev/null +++ b/frontend/src/services/tradingApi.js @@ -0,0 +1,55 @@ +const trimTrailingSlash = (value) => value.replace(/\/+$/, ''); +const isLocalDevHost = () => { + if (typeof window === 'undefined') { + return false; + } + const host = String(window.location.hostname || '').trim().toLowerCase(); + return host === 'localhost' || host === '127.0.0.1'; +}; + +const TRADING_SERVICE_BASE = trimTrailingSlash(import.meta.env.VITE_TRADING_SERVICE_URL || '') || ( + isLocalDevHost() ? 'http://localhost:8001' : '' +); + +export function hasDirectTradingService() { + return Boolean(TRADING_SERVICE_BASE); +} + +export async function fetchInsiderTradesDirect(ticker, startDate = null, endDate = null, limit = 50) { + if (!TRADING_SERVICE_BASE) { + throw new Error('Direct trading service is not configured'); + } + + const params = new URLSearchParams(); + params.set('ticker', ticker); + params.set('limit', String(limit)); + if (startDate) { + params.set('start_date', startDate); + } + if (endDate) { + params.set('end_date', endDate); + } + + const response = await fetch(`${TRADING_SERVICE_BASE}/api/insider-trades?${params.toString()}`); + if (!response.ok) { + throw new Error(await response.text()); + } + return response.json(); +} + +export async function fetchStockHistoryDirect(ticker, startDate, endDate) { + if (!TRADING_SERVICE_BASE) { + throw new Error('Direct trading service is not configured'); + } + + const params = new URLSearchParams(); + params.set('ticker', ticker); + params.set('start_date', startDate); + params.set('end_date', endDate); + + const response = await fetch(`${TRADING_SERVICE_BASE}/api/prices?${params.toString()}`); + if (!response.ok) { + throw new Error(await response.text()); + } + return response.json(); +}