Add explain analysis workflow and UI
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
||||
import React, { Suspense, lazy, useEffect, useMemo, useRef, useState, useCallback } from "react";
|
||||
|
||||
// Configuration and constants
|
||||
import { AGENTS, INITIAL_TICKERS } from './config/constants';
|
||||
@@ -13,19 +13,40 @@ import { useFeedProcessor } from './hooks/useFeedProcessor';
|
||||
import GlobalStyles from './styles/GlobalStyles';
|
||||
|
||||
// Components
|
||||
import RoomView from './components/RoomView';
|
||||
import NetValueChart from './components/NetValueChart';
|
||||
import AgentFeed from './components/AgentFeed';
|
||||
import StockLogo from './components/StockLogo';
|
||||
import StatisticsView from './components/StatisticsView';
|
||||
import PerformanceView from './components/PerformanceView';
|
||||
import StockExplainView from './components/StockExplainView.jsx';
|
||||
import Header from './components/Header.jsx';
|
||||
import WatchlistPanel from './components/WatchlistPanel.jsx';
|
||||
|
||||
// Utils
|
||||
import { formatNumber, formatTickerPrice } from './utils/formatters';
|
||||
|
||||
const RoomView = lazy(() => import('./components/RoomView'));
|
||||
const AgentFeed = lazy(() => import('./components/AgentFeed'));
|
||||
const StatisticsView = lazy(() => import('./components/StatisticsView'));
|
||||
const StockExplainView = lazy(() => import('./components/StockExplainView.jsx'));
|
||||
|
||||
function ViewLoadingFallback({ label = '加载中...' }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: 240,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '1px solid #000000',
|
||||
background: '#ffffff',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.4
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Live Trading Intelligence Platform - Read-Only Dashboard
|
||||
* Geek Style - Terminal-inspired, minimal, monochrome
|
||||
@@ -73,6 +94,7 @@ export default function LiveTradingApp() {
|
||||
const [priceHistoryByTicker, setPriceHistoryByTicker] = useState({});
|
||||
const [ohlcHistoryByTicker, setOhlcHistoryByTicker] = useState({});
|
||||
const [explainEventsByTicker, setExplainEventsByTicker] = useState({});
|
||||
const [newsByTicker, setNewsByTicker] = useState({});
|
||||
const [selectedExplainSymbol, setSelectedExplainSymbol] = useState('');
|
||||
const [historySourceByTicker, setHistorySourceByTicker] = useState({});
|
||||
|
||||
@@ -237,35 +259,59 @@ export default function LiveTradingApp() {
|
||||
|
||||
const raw = typeof marketStatus.status_text === 'string' ? marketStatus.status_text.trim() : '';
|
||||
const normalized = raw.toLowerCase();
|
||||
if (normalized === 'market closed (non-trading day)') {
|
||||
return '休市';
|
||||
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 (normalized === 'market open') {
|
||||
return '开盘';
|
||||
if (marketStatus.status && byStatus[marketStatus.status]) {
|
||||
return byStatus[marketStatus.status];
|
||||
}
|
||||
if (normalized === 'market closed') {
|
||||
return '收盘';
|
||||
}
|
||||
|
||||
return raw || (marketStatus.status === 'open' ? '开盘' : '收盘');
|
||||
return raw || '状态未知';
|
||||
}, [marketStatus]);
|
||||
|
||||
const priceSourceLabel = useMemo(() => {
|
||||
const providerLabelMap = useMemo(() => ({
|
||||
yfinance: 'YFinance',
|
||||
finnhub: 'Finnhub',
|
||||
financial_datasets: 'Financial Datasets',
|
||||
local_csv: 'CSV',
|
||||
polygon: 'Polygon',
|
||||
mock: 'Mock',
|
||||
backtest: 'Backtest'
|
||||
}), []);
|
||||
|
||||
const livePriceSourceLabel = useMemo(() => {
|
||||
const source = marketStatus?.live_quote_provider;
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = String(source).trim().toLowerCase();
|
||||
return `实时 ${providerLabelMap[normalized] || String(source).trim()}`;
|
||||
}, [marketStatus, providerLabelMap]);
|
||||
|
||||
const historicalPriceSourceLabel = useMemo(() => {
|
||||
const source = dataSources?.last_success?.prices;
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = String(source).trim().toLowerCase();
|
||||
const labels = {
|
||||
yfinance: '数据源 Yahoo',
|
||||
finnhub: '数据源 Finnhub',
|
||||
financial_datasets: '数据源 Financial Datasets',
|
||||
local_csv: '数据源 CSV'
|
||||
};
|
||||
|
||||
return labels[normalized] || `数据源 ${String(source).trim()}`;
|
||||
}, [dataSources]);
|
||||
return `历史 ${providerLabelMap[normalized] || String(source).trim()}`;
|
||||
}, [dataSources, providerLabelMap]);
|
||||
|
||||
const parseWatchlistInput = useCallback((value) => {
|
||||
if (typeof value !== 'string') {
|
||||
@@ -413,6 +459,131 @@ export default function LiveTradingApp() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestStockNews = useCallback((symbol) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_news',
|
||||
ticker: normalized,
|
||||
lookback_days: 45,
|
||||
limit: 12
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestStockNewsForDate = useCallback((symbol, date) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !date || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_news_for_date',
|
||||
ticker: normalized,
|
||||
date,
|
||||
limit: 20
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestStockNewsTimeline = useCallback((symbol) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_news_timeline',
|
||||
ticker: normalized,
|
||||
lookback_days: 90
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestStockNewsCategories = useCallback((symbol) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_news_categories',
|
||||
ticker: normalized,
|
||||
lookback_days: 90
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestStockRangeExplain = useCallback((symbol, startDate, endDate, articleIds = []) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !startDate || !endDate || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_range_explain',
|
||||
ticker: normalized,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
article_ids: Array.isArray(articleIds) ? articleIds : []
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestStockStory = useCallback((symbol, asOfDate = null) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_story',
|
||||
ticker: normalized,
|
||||
as_of_date: asOfDate
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestStockSimilarDays = useCallback((symbol, date, topK = 8) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !date || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_similar_days',
|
||||
ticker: normalized,
|
||||
date,
|
||||
top_k: topK
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestStockEnrich = useCallback((symbol, options = {}) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
const startDate = typeof options.startDate === 'string' ? options.startDate.trim() : '';
|
||||
const endDate = typeof options.endDate === 'string' ? options.endDate.trim() : '';
|
||||
if (!startDate || !endDate) {
|
||||
return false;
|
||||
}
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: {
|
||||
...(prev[normalized] || {}),
|
||||
maintenanceStatus: {
|
||||
running: true,
|
||||
error: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
stats: null
|
||||
}
|
||||
}
|
||||
}));
|
||||
return clientRef.current.send({
|
||||
type: 'run_stock_enrich',
|
||||
ticker: normalized,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
force: Boolean(options.force),
|
||||
only_local_to_llm: Boolean(options.onlyLocalToLlm),
|
||||
rebuild_story: Boolean(options.rebuildStory),
|
||||
rebuild_similar_days: Boolean(options.rebuildSimilarDays),
|
||||
story_date: options.storyDate || null,
|
||||
target_date: options.targetDate || null
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Switch away from LIVE tab when market closes
|
||||
useEffect(() => {
|
||||
if (!isLiveEnabled && chartTab === 'live') {
|
||||
@@ -439,7 +610,21 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
requestStockHistory(selectedExplainSymbol);
|
||||
requestStockExplainEvents(selectedExplainSymbol);
|
||||
}, [currentView, requestStockExplainEvents, requestStockHistory, selectedExplainSymbol]);
|
||||
requestStockNews(selectedExplainSymbol);
|
||||
requestStockNewsTimeline(selectedExplainSymbol);
|
||||
requestStockNewsCategories(selectedExplainSymbol);
|
||||
requestStockStory(selectedExplainSymbol, currentDate);
|
||||
}, [
|
||||
currentDate,
|
||||
currentView,
|
||||
requestStockExplainEvents,
|
||||
requestStockHistory,
|
||||
requestStockNews,
|
||||
requestStockNewsCategories,
|
||||
requestStockNewsTimeline,
|
||||
requestStockStory,
|
||||
selectedExplainSymbol
|
||||
]);
|
||||
|
||||
// Clock - use virtual time if available (for mock mode)
|
||||
useEffect(() => {
|
||||
@@ -802,6 +987,193 @@ export default function LiveTradingApp() {
|
||||
}));
|
||||
},
|
||||
|
||||
stock_news_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
...(prev[symbol] || {}),
|
||||
items: Array.isArray(e.news) ? e.news : [],
|
||||
source: e.source || null,
|
||||
startDate: e.start_date || null,
|
||||
endDate: e.end_date || null
|
||||
}
|
||||
}));
|
||||
requestStockNewsTimeline(symbol);
|
||||
},
|
||||
|
||||
stock_news_for_date_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
const date = typeof e.date === 'string' ? e.date.trim() : '';
|
||||
if (!symbol || !date) {
|
||||
return;
|
||||
}
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
...(prev[symbol] || {}),
|
||||
byDate: {
|
||||
...((prev[symbol] && prev[symbol].byDate) || {}),
|
||||
[date]: Array.isArray(e.news) ? e.news : []
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
stock_news_timeline_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
...(prev[symbol] || {}),
|
||||
timeline: Array.isArray(e.timeline) ? e.timeline : [],
|
||||
timelineStartDate: e.start_date || null,
|
||||
timelineEndDate: e.end_date || null
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
stock_news_categories_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
...(prev[symbol] || {}),
|
||||
categories: e.categories || {},
|
||||
categoriesStartDate: e.start_date || null,
|
||||
categoriesEndDate: e.end_date || null
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
stock_range_explain_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
const result = e.result && typeof e.result === 'object' ? e.result : null;
|
||||
if (!result?.start_date || !result?.end_date) {
|
||||
return;
|
||||
}
|
||||
const cacheKey = `${result.start_date}:${result.end_date}`;
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
...(prev[symbol] || {}),
|
||||
rangeExplainCache: {
|
||||
...((prev[symbol] && prev[symbol].rangeExplainCache) || {}),
|
||||
[cacheKey]: result
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
stock_story_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
const asOfDate = typeof e.as_of_date === 'string' ? e.as_of_date.trim() : '';
|
||||
if (!symbol || !asOfDate) {
|
||||
return;
|
||||
}
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
...(prev[symbol] || {}),
|
||||
storyCache: {
|
||||
...((prev[symbol] && prev[symbol].storyCache) || {}),
|
||||
[asOfDate]: {
|
||||
story: e.story || '',
|
||||
source: e.source || null,
|
||||
asOfDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
stock_similar_days_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
const date = typeof e.target_date === 'string'
|
||||
? e.target_date.trim()
|
||||
: typeof e.date === 'string'
|
||||
? e.date.trim()
|
||||
: '';
|
||||
if (!symbol || !date) {
|
||||
return;
|
||||
}
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
...(prev[symbol] || {}),
|
||||
similarDaysCache: {
|
||||
...((prev[symbol] && prev[symbol].similarDaysCache) || {}),
|
||||
[date]: {
|
||||
target_features: e.target_features || {},
|
||||
items: Array.isArray(e.items) ? e.items : [],
|
||||
error: e.error || null
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
stock_enrich_completed: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
const completedAt = new Date().toISOString();
|
||||
const historyEntry = {
|
||||
timestamp: completedAt,
|
||||
startDate: e.start_date || '',
|
||||
endDate: e.end_date || '',
|
||||
force: Boolean(e.force),
|
||||
onlyLocalToLlm: Boolean(e.only_local_to_llm),
|
||||
error: e.error || null,
|
||||
stats: e.stats || null,
|
||||
storyStatus: e.story_status || null,
|
||||
similarStatus: e.similar_status || null
|
||||
};
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
...(prev[symbol] || {}),
|
||||
items: [],
|
||||
byDate: {},
|
||||
timeline: [],
|
||||
categories: {},
|
||||
rangeExplainCache: {},
|
||||
storyCache: {},
|
||||
similarDaysCache: {},
|
||||
maintenanceStatus: {
|
||||
running: false,
|
||||
error: e.error || null,
|
||||
updatedAt: completedAt,
|
||||
stats: e.stats || null,
|
||||
storyStatus: e.story_status || null,
|
||||
similarStatus: e.similar_status || null
|
||||
},
|
||||
maintenanceHistory: [
|
||||
historyEntry,
|
||||
...(((prev[symbol] && prev[symbol].maintenanceHistory) || []).slice(0, 7))
|
||||
]
|
||||
}
|
||||
}));
|
||||
if (!e.error) {
|
||||
requestStockNews(symbol);
|
||||
requestStockNewsTimeline(symbol);
|
||||
requestStockNewsCategories(symbol);
|
||||
}
|
||||
},
|
||||
|
||||
// Real-time price updates
|
||||
price_update: (e) => {
|
||||
try {
|
||||
@@ -1146,7 +1518,14 @@ export default function LiveTradingApp() {
|
||||
clientRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
}, [addSystemMessage, buildTickersFromSymbols, processFeedEvent, processHistoricalFeed]); // Only reconnect if handlers change
|
||||
}, [
|
||||
addSystemMessage,
|
||||
buildTickersFromSymbols,
|
||||
processFeedEvent,
|
||||
processHistoricalFeed,
|
||||
requestStockNewsCategories,
|
||||
requestStockNewsTimeline
|
||||
]); // Only reconnect if handlers change
|
||||
|
||||
// Resizing handlers
|
||||
const handleMouseDown = (e) => {
|
||||
@@ -1318,16 +1697,24 @@ export default function LiveTradingApp() {
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{priceSourceLabel && (
|
||||
{livePriceSourceLabel && (
|
||||
<>
|
||||
<span className="status-sep">·</span>
|
||||
<span className="market-text backtest">
|
||||
{priceSourceLabel}
|
||||
{livePriceSourceLabel}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{historicalPriceSourceLabel && (
|
||||
<>
|
||||
<span className="status-sep">·</span>
|
||||
<span className="market-text backtest">
|
||||
{historicalPriceSourceLabel}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="status-sep">·</span>
|
||||
<span className="time-text">{lastUpdate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}</span>
|
||||
<span className="time-text">{now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}</span>
|
||||
</div>
|
||||
|
||||
<WatchlistPanel
|
||||
@@ -1407,7 +1794,7 @@ export default function LiveTradingApp() {
|
||||
className={`view-nav-btn ${currentView === 'explain' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('explain')}
|
||||
>
|
||||
个股解释
|
||||
个股分析
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -1437,30 +1824,41 @@ export default function LiveTradingApp() {
|
||||
} ${!isInitialAnimating ? 'normal-speed' : ''}`}>
|
||||
{/* Room View Panel */}
|
||||
<div className="view-panel">
|
||||
<RoomView
|
||||
bubbles={bubbles}
|
||||
bubbleFor={bubbleFor}
|
||||
leaderboard={leaderboard}
|
||||
feed={feed}
|
||||
onJumpToMessage={handleJumpToMessage}
|
||||
/>
|
||||
<Suspense fallback={<ViewLoadingFallback label="加载交易室..." />}>
|
||||
<RoomView
|
||||
bubbles={bubbles}
|
||||
bubbleFor={bubbleFor}
|
||||
leaderboard={leaderboard}
|
||||
feed={feed}
|
||||
onJumpToMessage={handleJumpToMessage}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* Stock Explain View Panel */}
|
||||
<div className="view-panel">
|
||||
<StockExplainView
|
||||
tickers={displayTickers}
|
||||
holdings={holdings}
|
||||
trades={trades}
|
||||
leaderboard={leaderboard}
|
||||
feed={feed}
|
||||
priceHistoryByTicker={priceHistoryByTicker}
|
||||
ohlcHistoryByTicker={ohlcHistoryByTicker}
|
||||
selectedSymbol={selectedExplainSymbol}
|
||||
onSelectedSymbolChange={setSelectedExplainSymbol}
|
||||
selectedHistorySource={historySourceByTicker[selectedExplainSymbol] || null}
|
||||
explainEventsSnapshot={explainEventsByTicker[selectedExplainSymbol] || null}
|
||||
/>
|
||||
<Suspense fallback={<ViewLoadingFallback label="加载个股分析..." />}>
|
||||
<StockExplainView
|
||||
tickers={displayTickers}
|
||||
holdings={holdings}
|
||||
trades={trades}
|
||||
leaderboard={leaderboard}
|
||||
feed={feed}
|
||||
priceHistoryByTicker={priceHistoryByTicker}
|
||||
ohlcHistoryByTicker={ohlcHistoryByTicker}
|
||||
selectedSymbol={selectedExplainSymbol}
|
||||
onSelectedSymbolChange={setSelectedExplainSymbol}
|
||||
selectedHistorySource={historySourceByTicker[selectedExplainSymbol] || null}
|
||||
explainEventsSnapshot={explainEventsByTicker[selectedExplainSymbol] || null}
|
||||
newsSnapshot={newsByTicker[selectedExplainSymbol] || null}
|
||||
onRequestRangeExplain={requestStockRangeExplain}
|
||||
onRequestNewsForDate={requestStockNewsForDate}
|
||||
onRequestStory={requestStockStory}
|
||||
currentDate={currentDate}
|
||||
onRequestSimilarDays={requestStockSimilarDays}
|
||||
onRequestStockEnrich={requestStockEnrich}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* Chart View Panel */}
|
||||
@@ -1502,14 +1900,16 @@ export default function LiveTradingApp() {
|
||||
|
||||
{/* Statistics View Panel */}
|
||||
<div className="view-panel">
|
||||
<StatisticsView
|
||||
trades={trades}
|
||||
holdings={holdings}
|
||||
stats={stats}
|
||||
baseline_vw={portfolioData.baseline_vw}
|
||||
equity={portfolioData.equity}
|
||||
leaderboard={leaderboard}
|
||||
/>
|
||||
<Suspense fallback={<ViewLoadingFallback label="加载统计视图..." />}>
|
||||
<StatisticsView
|
||||
trades={trades}
|
||||
holdings={holdings}
|
||||
stats={stats}
|
||||
baseline_vw={portfolioData.baseline_vw}
|
||||
equity={portfolioData.equity}
|
||||
leaderboard={leaderboard}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1524,7 +1924,9 @@ export default function LiveTradingApp() {
|
||||
|
||||
{/* Right Panel: Agent Feed */}
|
||||
<div className="right-panel" style={{ width: `${100 - leftWidth}%` }}>
|
||||
<AgentFeed ref={agentFeedRef} feed={feed} leaderboard={leaderboard} />
|
||||
<Suspense fallback={<ViewLoadingFallback label="加载消息流..." />}>
|
||||
<AgentFeed ref={agentFeedRef} feed={feed} leaderboard={leaderboard} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user