import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import AppShell from './components/AppShell.jsx'; import RuntimeLogsModal from './components/RuntimeLogsModal.jsx'; import { AGENTS } from './config/constants'; import { useAgentDataRequests } from './hooks/useAgentDataRequests'; import { useFeedProcessor } from './hooks/useFeedProcessor'; import { useRuntimeControls } from './hooks/useRuntimeControls'; import { useStockDataRequests } from './hooks/useStockDataRequests'; import { useWebSocketConnection } from './hooks/useWebSocketConnection'; import { fetchRuntimeLogs } from './services/runtimeApi'; import { useAgentStore } from './store/agentStore'; import { useMarketStore } from './store/marketStore'; import { usePortfolioStore } from './store/portfolioStore'; import { useRuntimeStore } from './store/runtimeStore'; import { useOpenClawStore } from './store/openclawStore'; import { useUIStore } from './store/uiStore'; const EDITABLE_AGENT_WORKSPACE_FILES = [ 'SOUL.md', 'PROFILE.md', 'AGENTS.md', 'MEMORY.md', 'POLICY.md', 'HEARTBEAT.md', 'ROLE.md', 'STYLE.md' ]; export default function LiveTradingApp() { const { isConnected, connectionStatus, serverMode, marketStatus, virtualTime, dataSources, currentDate, runtimeConfig, } = useRuntimeStore(); const { currentView, chartTab, isInitialAnimating, lastUpdate, isUpdating, now, setNow, setLastUpdate, setIsUpdating, leftWidth, isResizing, bubbles, } = useUIStore(); const { tickers, rollingTickers, priceHistoryByTicker, ohlcHistoryByTicker, explainEventsByTicker, newsByTicker, insiderTradesByTicker, technicalIndicatorsByTicker, selectedExplainSymbol, historySourceByTicker, setSelectedExplainSymbol, } = useMarketStore(); const { portfolioData, holdings, trades, stats, leaderboard, } = usePortfolioStore(); const { selectedSkillAgentId, agentProfilesByAgent, agentSkillsByAgent, skillDetailsByName, localSkillDraftsByKey, isAgentSkillsLoading, skillDetailLoadingKey, agentSkillsSavingKey, agentSkillsFeedback, selectedWorkspaceFile, workspaceFilesByAgent, workspaceDraftContent, isWorkspaceFileLoading, workspaceFileSavingKey, workspaceFileFeedback, setSelectedWorkspaceFile, setSelectedSkillAgentId, setWorkspaceDraftContent, } = useAgentStore(); const { feed, processHistoricalFeed, processFeedEvent, addSystemMessage, clearFeed } = useFeedProcessor(); const resetRuntimeViewState = useCallback(() => { clearFeed(); useMarketStore.getState().setPriceHistoryByTicker({}); useMarketStore.getState().setOhlcHistoryByTicker({}); useMarketStore.getState().setHistorySourceByTicker({}); useMarketStore.getState().setExplainEventsByTicker({}); useMarketStore.getState().setNewsByTicker({}); useMarketStore.getState().setInsiderTradesByTicker({}); useMarketStore.getState().setTechnicalIndicatorsByTicker({}); usePortfolioStore.getState().setHoldings([]); usePortfolioStore.getState().setTrades([]); usePortfolioStore.getState().setStats(null); usePortfolioStore.getState().setLeaderboard([]); usePortfolioStore.getState().setPortfolioData({ netValue: 10000, pnl: 0, equity: [], baseline: [], baseline_vw: [], momentum: [], strategies: [], equity_return: 0, baseline_return: 0, baseline_vw_return: 0, momentum_return: 0, }); useRuntimeStore.getState().setLastDayHistory([]); useUIStore.getState().setBubbles({}); }, [clearFeed]); const { clientRef, setRequestStockHistory, setRequestStockNewsTimeline, setRequestStockNewsCategories, } = useWebSocketConnection({ processHistoricalFeed, processFeedEvent, addSystemMessage, }); // Make clientRef available to OpenClaw panel via store useEffect(() => { useOpenClawStore.getState().setClientRef(clientRef); }, [clientRef]); const runtimeControls = useRuntimeControls({ clientRef, currentTickers: tickers, addSystemMessage, onRuntimeStarted: resetRuntimeViewState, }); const stockRequests = useStockDataRequests(clientRef, { setRequestStockHistory, setRequestStockNewsTimeline, setRequestStockNewsCategories, }); const { requestAgentSkills, requestAgentProfile, requestSkillDetail, handleCreateLocalSkill, handleLocalSkillDraftChange, handleLocalSkillSave, handleLocalSkillDelete, handleRemoveSharedSkill, handleAgentSkillToggle, handleSkillAgentChange, requestWorkspaceFile, handleWorkspaceFileChange, handleWorkspaceFileSave, handleUploadExternalSkill, } = useAgentDataRequests(clientRef); const [isRuntimeLogsOpen, setIsRuntimeLogsOpen] = useState(false); const [isRuntimeLogsLoading, setIsRuntimeLogsLoading] = useState(false); const [runtimeLogsPayload, setRuntimeLogsPayload] = useState(null); const [runtimeLogsError, setRuntimeLogsError] = useState(null); const agentFeedRef = useRef(null); const isSocketReady = isConnected && connectionStatus === 'connected'; const selectedAgentId = selectedSkillAgentId || AGENTS[0]?.id || null; const selectedAgentProfile = selectedAgentId ? (agentProfilesByAgent[selectedAgentId] || null) : null; const selectedAgentSkills = selectedAgentId ? (agentSkillsByAgent[selectedAgentId] || []) : []; const selectedWorkspaceContent = selectedAgentId && selectedWorkspaceFile ? (workspaceFilesByAgent[selectedAgentId]?.[selectedWorkspaceFile] || '') : ''; useEffect(() => { if (!selectedSkillAgentId && AGENTS.length > 0) { setSelectedSkillAgentId(AGENTS[0].id); } }, [selectedSkillAgentId, setSelectedSkillAgentId]); useEffect(() => { if (!selectedWorkspaceFile) { setSelectedWorkspaceFile('MEMORY.md'); } }, [selectedWorkspaceFile, setSelectedWorkspaceFile]); useEffect(() => { if (!isSocketReady || !selectedAgentId || !clientRef.current) { return; } if (!agentProfilesByAgent[selectedAgentId]) { requestAgentProfile(selectedAgentId); } if (!Array.isArray(agentSkillsByAgent[selectedAgentId])) { requestAgentSkills(selectedAgentId); } if ( selectedWorkspaceFile && workspaceFilesByAgent[selectedAgentId]?.[selectedWorkspaceFile] === undefined ) { requestWorkspaceFile(selectedAgentId, selectedWorkspaceFile); } }, [ agentProfilesByAgent, agentSkillsByAgent, clientRef, isSocketReady, requestAgentProfile, requestAgentSkills, requestWorkspaceFile, selectedAgentId, selectedWorkspaceFile, workspaceFilesByAgent, ]); useEffect(() => { if (!isSocketReady || !clientRef.current) { return; } AGENTS.forEach((agent) => { if (!agent?.id) { return; } if (!agentProfilesByAgent[agent.id]) { requestAgentProfile(agent.id); } }); }, [ agentProfilesByAgent, clientRef, isSocketReady, requestAgentProfile, ]); useEffect(() => { const symbols = runtimeControls.displayTickers .map((ticker) => ticker.symbol) .filter((symbol) => typeof symbol === 'string' && symbol.trim()); if (!symbols.length) { setSelectedExplainSymbol(''); return; } if (!selectedExplainSymbol || !symbols.includes(selectedExplainSymbol)) { setSelectedExplainSymbol(symbols[0]); } }, [runtimeControls.displayTickers, selectedExplainSymbol, setSelectedExplainSymbol]); useEffect(() => { if (virtualTime) { setNow(new Date(virtualTime)); const id = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(id); } const id = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(id); }, [setNow, virtualTime]); useEffect(() => { setLastUpdate(new Date()); setIsUpdating(true); const timer = setTimeout(() => setIsUpdating(false), 500); return () => clearTimeout(timer); }, [holdings, stats, trades, portfolioData.netValue, setIsUpdating, setLastUpdate]); const marketStatusLabel = useMemo(() => { if (!marketStatus) return null; const raw = typeof marketStatus.status_text === 'string' ? marketStatus.status_text.trim() : ''; const normalized = raw.toLowerCase(); const byStatus = { open: '开盘', closed: '休市', premarket: '盘前', afterhours: '盘后', }; const byText = { 'market closed (non-trading day)': '休市', 'market open': '开盘', 'market closed': '收盘', 'pre-market': '盘前', 'after-hours': '盘后', 'after hours': '盘后', 'backtest mode': '回测模式', }; if (normalized && byText[normalized]) return byText[normalized]; if (marketStatus.status && byStatus[marketStatus.status]) return byStatus[marketStatus.status]; return raw || '状态未知'; }, [marketStatus]); const providerLabelMap = useMemo(() => ({ yfinance: 'YFINANCE', finnhub: 'FINNHUB', financial_datasets: 'FINANCIAL DATASETS', local_csv: 'CSV', polygon: 'POLYGON', backtest: 'BACKTEST', }), []); const dataSourceLabel = useMemo(() => { const source = dataSources?.last_success?.prices || marketStatus?.live_quote_provider || (Array.isArray(dataSources?.preferred) ? dataSources.preferred[0] : null); if (!source) return null; const normalized = String(source).trim().toLowerCase(); return `数据源 ${providerLabelMap[normalized] || String(source).trim()}`; }, [dataSources, marketStatus, providerLabelMap]); const bubbleFor = useCallback((idOrName) => { let bubble = bubbles[idOrName]; if (bubble) return bubble; const agent = AGENTS.find((item) => item.name === idOrName || item.id === idOrName); if (agent) { bubble = bubbles[agent.id]; if (bubble) return bubble; } return null; }, [bubbles]); const handleManualTrigger = useCallback(() => { if (!isSocketReady || !clientRef.current) { addSystemMessage('连接未就绪,无法手动触发'); return; } const success = clientRef.current.send({ type: 'trigger_strategy' }); if (!success) { addSystemMessage('手动触发发送失败,请检查连接状态'); return; } addSystemMessage('已发送手动触发请求'); }, [addSystemMessage, clientRef, isSocketReady]); const loadRuntimeLogs = useCallback(async () => { setIsRuntimeLogsLoading(true); setRuntimeLogsError(null); try { const payload = await fetchRuntimeLogs(); setRuntimeLogsPayload(payload); } catch (error) { setRuntimeLogsError(error.message || '无法读取运行日志'); } finally { setIsRuntimeLogsLoading(false); } }, []); const agentRequests = { agents: AGENTS, agentProfilesByAgent, agentSkillsByAgent, workspaceFilesByAgent, selectedAgentId, selectedAgentProfile, selectedAgentSkills, skillDetailsByName, localSkillDraftsByKey, skillDetailLoadingKey, editableFiles: EDITABLE_AGENT_WORKSPACE_FILES, selectedWorkspaceFile, workspaceFileContent: selectedWorkspaceContent, workspaceDraftContent, isConnected, isAgentSkillsLoading, agentSkillsSavingKey, agentSkillsFeedback, isWorkspaceFileLoading, workspaceFileSavingKey, workspaceFileFeedback, onAgentChange: handleSkillAgentChange, onCreateLocalSkill: handleCreateLocalSkill, onSkillDetailRequest: requestSkillDetail, onLocalSkillDraftChange: handleLocalSkillDraftChange, onLocalSkillDelete: handleLocalSkillDelete, onLocalSkillSave: handleLocalSkillSave, onRemoveSharedSkill: handleRemoveSharedSkill, onSkillToggle: handleAgentSkillToggle, onWorkspaceFileChange: handleWorkspaceFileChange, onWorkspaceDraftChange: setWorkspaceDraftContent, onWorkspaceFileSave: handleWorkspaceFileSave, onUploadExternalSkill: handleUploadExternalSkill, clientRef, }; return ( <> { setIsRuntimeLogsOpen(true); void loadRuntimeLogs(); }} onRuntimeSettingsToggle={runtimeControls.handleRuntimeSettingsToggle} isRuntimeSettingsOpen={runtimeControls.isRuntimeSettingsOpen} isRuntimeConfigSaving={runtimeControls.isRuntimeConfigSaving} isWatchlistSaving={runtimeControls.isWatchlistSaving} runtimeConfigFeedback={runtimeControls.runtimeConfigFeedback} watchlistFeedback={runtimeControls.watchlistFeedback} launchModeDraft={runtimeControls.launchModeDraft} restoreRunIdDraft={runtimeControls.restoreRunIdDraft} runtimeHistoryRuns={runtimeControls.runtimeHistoryRuns} scheduleModeDraft={runtimeControls.scheduleModeDraft} intervalMinutesDraft={runtimeControls.intervalMinutesDraft} triggerTimeDraft={runtimeControls.triggerTimeDraft} maxCommCyclesDraft={runtimeControls.maxCommCyclesDraft} initialCashDraft={runtimeControls.initialCashDraft} marginRequirementDraft={runtimeControls.marginRequirementDraft} enableMemoryDraft={runtimeControls.enableMemoryDraft} modeDraft={runtimeControls.modeDraft} pollIntervalDraft={runtimeControls.pollIntervalDraft} startDateDraft={runtimeControls.startDateDraft} endDateDraft={runtimeControls.endDateDraft} watchlistDraftSymbols={runtimeControls.watchlistDraftSymbols} watchlistInputValue={runtimeControls.watchlistInputValue} watchlistSuggestions={runtimeControls.watchlistSuggestions} onLaunchModeChange={runtimeControls.setLaunchModeDraft} onRestoreRunIdChange={runtimeControls.setRestoreRunIdDraft} onScheduleModeChange={runtimeControls.setScheduleModeDraft} onIntervalMinutesChange={runtimeControls.setIntervalMinutesDraft} onTriggerTimeChange={runtimeControls.setTriggerTimeDraft} onMaxCommCyclesChange={runtimeControls.setMaxCommCyclesDraft} onInitialCashChange={runtimeControls.setInitialCashDraft} onMarginRequirementChange={runtimeControls.setMarginRequirementDraft} onEnableMemoryChange={runtimeControls.setEnableMemoryDraft} onModeChange={runtimeControls.setModeDraft} onPollIntervalChange={runtimeControls.setPollIntervalDraft} onStartDateChange={runtimeControls.setStartDateDraft} onEndDateChange={runtimeControls.setEndDateDraft} onWatchlistInputChange={runtimeControls.handleWatchlistInputChange} onWatchlistInputKeyDown={runtimeControls.handleWatchlistInputKeyDown} onWatchlistAdd={runtimeControls.handleWatchlistAdd} onWatchlistRemove={runtimeControls.handleWatchlistRemove} onWatchlistRestoreCurrent={runtimeControls.handleWatchlistRestoreCurrent} onWatchlistRestoreDefault={runtimeControls.handleWatchlistRestoreDefault} onWatchlistSuggestionClick={runtimeControls.handleWatchlistSuggestionClick} onLaunchConfigSave={runtimeControls.handleLaunchConfigSave} onRestoreDefaults={runtimeControls.handleRuntimeDefaultsRestore} displayTickers={runtimeControls.displayTickers} portfolioData={portfolioData} rollingTickers={rollingTickers} feed={feed} bubbles={bubbles} bubbleFor={bubbleFor} leaderboard={leaderboard} currentView={currentView} chartTab={chartTab} holdings={holdings} trades={trades} stats={stats} priceHistoryByTicker={priceHistoryByTicker} ohlcHistoryByTicker={ohlcHistoryByTicker} selectedExplainSymbol={selectedExplainSymbol} onSelectedExplainSymbolChange={setSelectedExplainSymbol} historySourceByTicker={historySourceByTicker} explainEventsByTicker={explainEventsByTicker} newsByTicker={newsByTicker} insiderTradesByTicker={insiderTradesByTicker} technicalIndicatorsByTicker={technicalIndicatorsByTicker} currentDate={currentDate} stockRequests={stockRequests} agentRequests={agentRequests} agentProfilesByAgent={agentProfilesByAgent} leftWidth={leftWidth} isResizing={isResizing} onMouseDown={() => useUIStore.getState().setIsResizing(true)} agentFeedRef={agentFeedRef} /> setIsRuntimeLogsOpen(false)} onRefresh={loadRuntimeLogs} /> ); }