510 lines
17 KiB
JavaScript
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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|