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:
@@ -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;
|
||||||
|
|||||||
110
frontend/src/services/newsApi.js
Normal file
110
frontend/src/services/newsApi.js
Normal 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();
|
||||||
|
}
|
||||||
55
frontend/src/services/tradingApi.js
Normal file
55
frontend/src/services/tradingApi.js
Normal 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user