Files
evotraders/frontend/src/App.jsx

510 lines
17 KiB
JavaScript

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 (
<>
<AppShell
isConnected={isConnected}
virtualTime={virtualTime}
now={now}
marketStatus={marketStatus}
serverMode={serverMode}
marketStatusLabel={marketStatusLabel}
dataSourceLabel={dataSourceLabel}
runtimeSummaryLabel={runtimeControls.runtimeSummaryLabel}
isUpdating={isUpdating}
onManualTrigger={handleManualTrigger}
onOpenRuntimeLogs={() => {
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}
/>
<RuntimeLogsModal
isOpen={isRuntimeLogsOpen}
isLoading={isRuntimeLogsLoading}
logPayload={runtimeLogsPayload}
error={runtimeLogsError}
onClose={() => setIsRuntimeLogsOpen(false)}
onRefresh={loadRuntimeLogs}
/>
</>
);
}