feat(frontend): 完成 Zustand 状态管理迁移

- 将 App.jsx 中的 useState 迁移到 5 个 Zustand stores
- useRuntimeStore: 连接状态、运行时配置
- useMarketStore: 市场数据、股票价格
- usePortfolioStore: 组合、持仓、交易
- useAgentStore: Agent 技能,工作区
- useUIStore: UI 状态、视图切换
- 保留 tickers useState(需与 INITIAL_TICKERS 同步)
- 恢复 newsApi.js 和 tradingApi.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 18:03:23 +08:00
parent 38102d0805
commit 609b509446
3 changed files with 226 additions and 86 deletions

View File

@@ -22,6 +22,11 @@ import {
// Hooks // Hooks
import { useFeedProcessor } from './hooks/useFeedProcessor'; 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 // Styles
import GlobalStyles from './styles/GlobalStyles'; import GlobalStyles from './styles/GlobalStyles';
@@ -70,100 +75,73 @@ function ViewLoadingFallback({ label = '加载中...' }) {
*/ */
export default function LiveTradingApp() { export default function LiveTradingApp() {
const [isConnected, setIsConnected] = useState(false); // Connection & system state - from runtimeStore
const [connectionStatus, setConnectionStatus] = useState('connecting'); // 'connecting' | 'connected' | 'disconnected' const { isConnected, setIsConnected, connectionStatus, setConnectionStatus, systemStatus, setSystemStatus, currentDate, setCurrentDate, progress, setProgress, now, setNow } = useRuntimeStore();
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());
// View toggle: 'traders' | 'room' | 'explain' | 'chart' | 'statistics' | 'runtime' // View toggle: 'traders' | 'room' | 'explain' | 'chart' | 'statistics' | 'runtime'
const [currentView, setCurrentView] = useState('traders'); const { currentView, setCurrentView, chartTab, setChartTab, isInitialAnimating, setIsInitialAnimating, lastUpdate, setLastUpdate, isUpdating, setIsUpdating } = useUIStore();
const [isInitialAnimating, setIsInitialAnimating] = useState(true);
const [lastUpdate, setLastUpdate] = useState(new Date());
const [isUpdating, setIsUpdating] = useState(false);
// Chart data // Chart data - from portfolioStore
const [chartTab, setChartTab] = useState('all'); const { portfolioData, setPortfolioData, holdings, setHoldings, trades, setTrades, stats, setStats, leaderboard, setLeaderboard } = usePortfolioStore();
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
});
// Feed data (using hook for simplified processing) // Feed data (using hook for simplified processing)
const { feed, processHistoricalFeed, processFeedEvent, addSystemMessage } = useFeedProcessor(); const { feed, processHistoricalFeed, processFeedEvent, addSystemMessage } = useFeedProcessor();
// Statistics data // Ticker prices - keep local state with INITIAL_TICKERS
const [holdings, setHoldings] = useState([]);
const [trades, setTrades] = useState([]);
const [stats, setStats] = useState(null);
const [leaderboard, setLeaderboard] = useState([]);
// Ticker prices (now from real-time data)
const [tickers, setTickers] = useState(INITIAL_TICKERS); const [tickers, setTickers] = useState(INITIAL_TICKERS);
const [rollingTickers, setRollingTickers] = useState({}); const { rollingTickers, setRollingTickers, priceHistoryByTicker, setPriceHistoryByTicker, ohlcHistoryByTicker, setOhlcHistoryByTicker, explainEventsByTicker, setExplainEventsByTicker, newsByTicker, setNewsByTicker, insiderTradesByTicker, setInsiderTradesByTicker, technicalIndicatorsByTicker, setTechnicalIndicatorsByTicker, selectedExplainSymbol, setSelectedExplainSymbol, historySourceByTicker, setHistorySourceByTicker } = useMarketStore();
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({});
// Room bubbles // Room bubbles - from uiStore
const [bubbles, setBubbles] = useState({}); const { bubbles, setBubbles, leftWidth, setLeftWidth, isResizing, setIsResizing } = useUIStore();
// Resizable panels // Market status & runtime config - from runtimeStore
const [leftWidth, setLeftWidth] = useState(70); // percentage const {
const [isResizing, setIsResizing] = useState(false); 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 // Agent state - from agentStore
const [serverMode, setServerMode] = useState(null); // 'live' | 'backtest' | null const {
const [marketStatus, setMarketStatus] = useState(null); // { status, status_text, ... } selectedSkillAgentId, setSelectedSkillAgentId,
const [virtualTime, setVirtualTime] = useState(null); // Virtual time from server (for mock mode) agentProfilesByAgent, setAgentProfilesByAgent,
const [dataSources, setDataSources] = useState(null); agentSkillsByAgent, setAgentSkillsByAgent,
const [runtimeConfig, setRuntimeConfig] = useState(null); skillDetailsByName, setSkillDetailsByName,
const [isWatchlistPanelOpen, setIsWatchlistPanelOpen] = useState(false); localSkillDraftsByKey, setLocalSkillDraftsByKey,
const [isRuntimeSettingsOpen, setIsRuntimeSettingsOpen] = useState(false); isAgentSkillsLoading, setIsAgentSkillsLoading,
const [watchlistDraftSymbols, setWatchlistDraftSymbols] = useState([]); skillDetailLoadingKey, setSkillDetailLoadingKey,
const [watchlistInputValue, setWatchlistInputValue] = useState(''); agentSkillsSavingKey, setAgentSkillsSavingKey,
const [watchlistFeedback, setWatchlistFeedback] = useState(null); agentSkillsFeedback, setAgentSkillsFeedback,
const [isWatchlistSaving, setIsWatchlistSaving] = useState(false); selectedWorkspaceFile, setSelectedWorkspaceFile,
const [scheduleModeDraft, setScheduleModeDraft] = useState('daily'); workspaceFilesByAgent, setWorkspaceFilesByAgent,
const [intervalMinutesDraft, setIntervalMinutesDraft] = useState('60'); workspaceDraftContent, setWorkspaceDraftContent,
const [triggerTimeDraft, setTriggerTimeDraft] = useState('09:30'); isWorkspaceFileLoading, setIsWorkspaceFileLoading,
const [maxCommCyclesDraft, setMaxCommCyclesDraft] = useState('2'); workspaceFileSavingKey, setWorkspaceFileSavingKey,
const [initialCashDraft, setInitialCashDraft] = useState('100000'); workspaceFileFeedback, setWorkspaceFileFeedback
const [marginRequirementDraft, setMarginRequirementDraft] = useState('0'); } = useAgentStore();
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);
const clientRef = useRef(null); const clientRef = useRef(null);
const containerRef = useRef(null); const containerRef = useRef(null);
@@ -176,9 +154,6 @@ export default function LiveTradingApp() {
const lastVirtualTimeRef = useRef(null); const lastVirtualTimeRef = useRef(null);
const virtualTimeOffsetRef = useRef(0); const virtualTimeOffsetRef = useRef(0);
// Last day history for replay
const [lastDayHistory, setLastDayHistory] = useState([]);
const buildTickersFromSymbols = useCallback((symbols, previousTickers = []) => { const buildTickersFromSymbols = useCallback((symbols, previousTickers = []) => {
if (!Array.isArray(symbols) || symbols.length === 0) { if (!Array.isArray(symbols) || symbols.length === 0) {
return previousTickers; return previousTickers;

View File

@@ -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();
}

View File

@@ -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();
}