确认PokieTicker新闻库数据源
This commit is contained in:
@@ -19,9 +19,9 @@ import AgentFeed from './components/AgentFeed';
|
||||
import StockLogo from './components/StockLogo';
|
||||
import StatisticsView from './components/StatisticsView';
|
||||
import PerformanceView from './components/PerformanceView';
|
||||
import AboutModal from './components/AboutModal';
|
||||
import RulesView from './components/RulesView';
|
||||
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';
|
||||
@@ -39,9 +39,8 @@ export default function LiveTradingApp() {
|
||||
const [currentDate, setCurrentDate] = useState(null);
|
||||
const [progress, setProgress] = useState({ current: 0, total: 0 });
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
const [showAboutModal, setShowAboutModal] = useState(false);
|
||||
|
||||
// View toggle: 'rules' | 'room' | 'chart' | 'statistics'
|
||||
// View toggle: 'room' | 'explain' | 'chart' | 'statistics'
|
||||
const [currentView, setCurrentView] = useState('chart'); // Start with chart, then animate to room
|
||||
const [isInitialAnimating, setIsInitialAnimating] = useState(true);
|
||||
const [lastUpdate, setLastUpdate] = useState(new Date());
|
||||
@@ -71,6 +70,11 @@ export default function LiveTradingApp() {
|
||||
// Ticker prices (now from real-time data)
|
||||
const [tickers, setTickers] = useState(INITIAL_TICKERS);
|
||||
const [rollingTickers, setRollingTickers] = useState({});
|
||||
const [priceHistoryByTicker, setPriceHistoryByTicker] = useState({});
|
||||
const [ohlcHistoryByTicker, setOhlcHistoryByTicker] = useState({});
|
||||
const [explainEventsByTicker, setExplainEventsByTicker] = useState({});
|
||||
const [selectedExplainSymbol, setSelectedExplainSymbol] = useState('');
|
||||
const [historySourceByTicker, setHistorySourceByTicker] = useState({});
|
||||
|
||||
// Room bubbles
|
||||
const [bubbles, setBubbles] = useState({});
|
||||
@@ -84,10 +88,18 @@ export default function LiveTradingApp() {
|
||||
const [marketStatus, setMarketStatus] = useState(null); // { status, status_text, ... }
|
||||
const [virtualTime, setVirtualTime] = useState(null); // Virtual time from server (for mock mode)
|
||||
const [dataSources, setDataSources] = useState(null);
|
||||
const [runtimeConfig, setRuntimeConfig] = useState(null);
|
||||
const [isWatchlistPanelOpen, setIsWatchlistPanelOpen] = useState(false);
|
||||
const [watchlistDraftSymbols, setWatchlistDraftSymbols] = useState([]);
|
||||
const [watchlistInputValue, setWatchlistInputValue] = useState('');
|
||||
const [watchlistFeedback, setWatchlistFeedback] = useState(null);
|
||||
const [isWatchlistSaving, setIsWatchlistSaving] = useState(false);
|
||||
|
||||
const clientRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const agentFeedRef = useRef(null);
|
||||
const isWatchlistSavingRef = useRef(false);
|
||||
const requestedStockHistoryRef = useRef(new Set());
|
||||
|
||||
// Track last virtual time update to calculate increment
|
||||
const lastVirtualTimeRef = useRef(null);
|
||||
@@ -96,12 +108,311 @@ export default function LiveTradingApp() {
|
||||
// Last day history for replay
|
||||
const [lastDayHistory, setLastDayHistory] = useState([]);
|
||||
|
||||
const buildTickersFromSymbols = useCallback((symbols, previousTickers = []) => {
|
||||
if (!Array.isArray(symbols) || symbols.length === 0) {
|
||||
return previousTickers;
|
||||
}
|
||||
|
||||
return symbols
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim())
|
||||
.map((symbol) => {
|
||||
const normalized = symbol.trim().toUpperCase();
|
||||
const existing = previousTickers.find((ticker) => ticker.symbol === normalized);
|
||||
return existing || {
|
||||
symbol: normalized,
|
||||
price: null,
|
||||
change: null
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const normalizePriceHistory = useCallback((payload) => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
Object.entries(payload).forEach(([symbol, points]) => {
|
||||
const ticker = String(symbol || '').trim().toUpperCase();
|
||||
if (!ticker || !Array.isArray(points)) {
|
||||
return;
|
||||
}
|
||||
|
||||
normalized[ticker] = points
|
||||
.map((point) => {
|
||||
if (Array.isArray(point) && point.length >= 2) {
|
||||
const [label, value] = point;
|
||||
const price = Number(value);
|
||||
if (!label || !Number.isFinite(price)) return null;
|
||||
return {
|
||||
timestamp: String(label),
|
||||
label: String(label),
|
||||
price
|
||||
};
|
||||
}
|
||||
|
||||
if (point && typeof point === 'object') {
|
||||
const rawTimestamp = point.timestamp ?? point.t ?? point.date ?? point.label;
|
||||
const price = Number(point.price ?? point.v ?? point.value ?? point.close);
|
||||
if (!rawTimestamp || !Number.isFinite(price)) return null;
|
||||
return {
|
||||
timestamp: String(rawTimestamp),
|
||||
label: String(rawTimestamp),
|
||||
price
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice(-120);
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}, []);
|
||||
|
||||
// Determine if LIVE tab should be enabled
|
||||
const isLiveEnabled = useMemo(() => {
|
||||
if (!marketStatus) return false;
|
||||
return marketStatus.status === 'open';
|
||||
}, [marketStatus]);
|
||||
|
||||
const displayTickers = useMemo(() => {
|
||||
const symbols = runtimeConfig?.tickers;
|
||||
if (Array.isArray(symbols) && symbols.length > 0) {
|
||||
return buildTickersFromSymbols(symbols, tickers);
|
||||
}
|
||||
return tickers;
|
||||
}, [buildTickersFromSymbols, runtimeConfig, tickers]);
|
||||
|
||||
const runtimeWatchlistSymbols = useMemo(() => {
|
||||
const symbols = runtimeConfig?.tickers;
|
||||
if (Array.isArray(symbols) && symbols.length > 0) {
|
||||
return symbols
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim())
|
||||
.map((symbol) => symbol.trim().toUpperCase());
|
||||
}
|
||||
|
||||
return displayTickers
|
||||
.map((ticker) => ticker.symbol)
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim());
|
||||
}, [displayTickers, runtimeConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
const symbols = 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]);
|
||||
}
|
||||
}, [displayTickers, selectedExplainSymbol]);
|
||||
|
||||
const watchlistSuggestions = useMemo(
|
||||
() => INITIAL_TICKERS.map((ticker) => ticker.symbol).filter((symbol, index, list) => list.indexOf(symbol) === index),
|
||||
[]
|
||||
);
|
||||
|
||||
const isWatchlistDraftDirty = useMemo(() => {
|
||||
if (watchlistInputValue.trim()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (watchlistDraftSymbols.length !== runtimeWatchlistSymbols.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return watchlistDraftSymbols.some((symbol, index) => symbol !== runtimeWatchlistSymbols[index]);
|
||||
}, [runtimeWatchlistSymbols, watchlistDraftSymbols, watchlistInputValue]);
|
||||
|
||||
const marketStatusLabel = useMemo(() => {
|
||||
if (!marketStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = typeof marketStatus.status_text === 'string' ? marketStatus.status_text.trim() : '';
|
||||
const normalized = raw.toLowerCase();
|
||||
if (normalized === 'market closed (non-trading day)') {
|
||||
return '休市';
|
||||
}
|
||||
if (normalized === 'market open') {
|
||||
return '开盘';
|
||||
}
|
||||
if (normalized === 'market closed') {
|
||||
return '收盘';
|
||||
}
|
||||
|
||||
return raw || (marketStatus.status === 'open' ? '开盘' : '收盘');
|
||||
}, [marketStatus]);
|
||||
|
||||
const priceSourceLabel = 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]);
|
||||
|
||||
const parseWatchlistInput = useCallback((value) => {
|
||||
if (typeof value !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
value
|
||||
.split(/[\s,]+/)
|
||||
.map((symbol) => symbol.trim().toUpperCase())
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const commitWatchlistInput = useCallback((value) => {
|
||||
const parsed = parseWatchlistInput(value);
|
||||
if (parsed.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
setWatchlistDraftSymbols((prev) => Array.from(new Set([...prev, ...parsed])));
|
||||
setWatchlistInputValue('');
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
return parsed;
|
||||
}, [parseWatchlistInput, watchlistFeedback]);
|
||||
|
||||
const handleWatchlistRemove = useCallback((symbolToRemove) => {
|
||||
setWatchlistDraftSymbols((prev) => prev.filter((symbol) => symbol !== symbolToRemove));
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
}, [watchlistFeedback]);
|
||||
|
||||
const handleWatchlistPanelToggle = useCallback(() => {
|
||||
setIsWatchlistPanelOpen((open) => {
|
||||
const nextOpen = !open;
|
||||
if (nextOpen) {
|
||||
setWatchlistDraftSymbols(runtimeWatchlistSymbols);
|
||||
setWatchlistInputValue('');
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
return nextOpen;
|
||||
});
|
||||
}, [runtimeWatchlistSymbols]);
|
||||
|
||||
const handleWatchlistInputChange = useCallback((value) => {
|
||||
setWatchlistInputValue(value);
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
}, [watchlistFeedback]);
|
||||
|
||||
const handleWatchlistInputKeyDown = useCallback((e) => {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
commitWatchlistInput(watchlistInputValue);
|
||||
}
|
||||
}, [commitWatchlistInput, watchlistInputValue]);
|
||||
|
||||
const handleWatchlistSuggestionClick = useCallback((symbol) => {
|
||||
if (watchlistDraftSymbols.includes(symbol)) {
|
||||
return;
|
||||
}
|
||||
setWatchlistDraftSymbols((prev) => [...prev, symbol]);
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
}, [watchlistDraftSymbols, watchlistFeedback]);
|
||||
|
||||
const handleWatchlistRestoreCurrent = useCallback(() => {
|
||||
setWatchlistDraftSymbols(runtimeWatchlistSymbols);
|
||||
setWatchlistInputValue('');
|
||||
setWatchlistFeedback(null);
|
||||
}, [runtimeWatchlistSymbols]);
|
||||
|
||||
const handleWatchlistRestoreDefault = useCallback(() => {
|
||||
setWatchlistDraftSymbols(watchlistSuggestions);
|
||||
setWatchlistInputValue('');
|
||||
setWatchlistFeedback(null);
|
||||
}, [watchlistSuggestions]);
|
||||
|
||||
const handleWatchlistSave = useCallback(() => {
|
||||
const pendingTickers = parseWatchlistInput(watchlistInputValue);
|
||||
const nextTickers = Array.from(new Set([...watchlistDraftSymbols, ...pendingTickers]));
|
||||
if (nextTickers.length === 0) {
|
||||
setWatchlistFeedback({ type: 'error', text: '至少输入 1 个有效股票代码' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
setWatchlistFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsWatchlistSaving(true);
|
||||
setWatchlistFeedback(null);
|
||||
setWatchlistDraftSymbols(nextTickers);
|
||||
setWatchlistInputValue('');
|
||||
const success = clientRef.current.send({
|
||||
type: 'update_watchlist',
|
||||
tickers: nextTickers
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
setIsWatchlistSaving(false);
|
||||
setWatchlistFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
|
||||
}
|
||||
}, [parseWatchlistInput, watchlistDraftSymbols, watchlistInputValue]);
|
||||
|
||||
const requestStockHistory = useCallback((symbol, { force = false } = {}) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!force && requestedStockHistoryRef.current.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = clientRef.current.send({
|
||||
type: 'get_stock_history',
|
||||
ticker: normalized,
|
||||
lookback_days: 120
|
||||
});
|
||||
|
||||
if (success) {
|
||||
requestedStockHistoryRef.current.add(normalized);
|
||||
}
|
||||
|
||||
return success;
|
||||
}, []);
|
||||
|
||||
const requestStockExplainEvents = useCallback((symbol) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_explain_events',
|
||||
ticker: normalized
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Switch away from LIVE tab when market closes
|
||||
useEffect(() => {
|
||||
if (!isLiveEnabled && chartTab === 'live') {
|
||||
@@ -109,6 +420,27 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
}, [isLiveEnabled, chartTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWatchlistPanelOpen || !isWatchlistDraftDirty) {
|
||||
setWatchlistDraftSymbols(runtimeWatchlistSymbols);
|
||||
if (!isWatchlistPanelOpen) {
|
||||
setWatchlistInputValue('');
|
||||
}
|
||||
}
|
||||
}, [isWatchlistDraftDirty, isWatchlistPanelOpen, runtimeWatchlistSymbols]);
|
||||
|
||||
useEffect(() => {
|
||||
isWatchlistSavingRef.current = isWatchlistSaving;
|
||||
}, [isWatchlistSaving]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentView !== 'explain' || !selectedExplainSymbol) {
|
||||
return;
|
||||
}
|
||||
requestStockHistory(selectedExplainSymbol);
|
||||
requestStockExplainEvents(selectedExplainSymbol);
|
||||
}, [currentView, requestStockExplainEvents, requestStockHistory, selectedExplainSymbol]);
|
||||
|
||||
// Clock - use virtual time if available (for mock mode)
|
||||
useEffect(() => {
|
||||
if (virtualTime) {
|
||||
@@ -253,6 +585,10 @@ export default function LiveTradingApp() {
|
||||
// Error response (for fast forward errors)
|
||||
error: (e) => {
|
||||
console.error('[Error]', e.message);
|
||||
if (isWatchlistSavingRef.current) {
|
||||
setIsWatchlistSaving(false);
|
||||
setWatchlistFeedback({ type: 'error', text: e.message || '更新 watchlist 失败' });
|
||||
}
|
||||
|
||||
// Handle fast forward errors
|
||||
if (e.message && e.message.includes('fast forward')) {
|
||||
@@ -307,6 +643,12 @@ export default function LiveTradingApp() {
|
||||
if (state.data_sources) {
|
||||
setDataSources(state.data_sources);
|
||||
}
|
||||
if (state.runtime_config) {
|
||||
setRuntimeConfig(state.runtime_config);
|
||||
}
|
||||
if (Array.isArray(state.tickers) && state.tickers.length > 0) {
|
||||
setTickers(prevTickers => buildTickersFromSymbols(state.tickers, prevTickers));
|
||||
}
|
||||
// 检查是否是mock模式
|
||||
const isMockMode = state.is_mock_mode === true;
|
||||
if (state.market_status) {
|
||||
@@ -356,6 +698,9 @@ export default function LiveTradingApp() {
|
||||
if (state.dashboard.leaderboard) setLeaderboard(state.dashboard.leaderboard);
|
||||
}
|
||||
if (state.realtime_prices) updateTickersFromPrices(state.realtime_prices);
|
||||
if (state.price_history) {
|
||||
setPriceHistoryByTicker(normalizePriceHistory(state.price_history));
|
||||
}
|
||||
|
||||
// Load and process historical feed data
|
||||
if (state.feed_history && Array.isArray(state.feed_history)) {
|
||||
@@ -388,6 +733,75 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
},
|
||||
|
||||
runtime_assets_reloaded: (e) => {
|
||||
if (e.runtime_config_applied) {
|
||||
setRuntimeConfig(e.runtime_config_applied);
|
||||
}
|
||||
if (Array.isArray(e.runtime_config_applied?.tickers)) {
|
||||
setTickers(prevTickers => buildTickersFromSymbols(e.runtime_config_applied.tickers, prevTickers));
|
||||
setWatchlistDraftSymbols(e.runtime_config_applied.tickers.map((symbol) => String(symbol).trim().toUpperCase()));
|
||||
setWatchlistInputValue('');
|
||||
}
|
||||
if (isWatchlistSavingRef.current) {
|
||||
setIsWatchlistSaving(false);
|
||||
}
|
||||
addSystemMessage('运行时配置已热更新');
|
||||
},
|
||||
|
||||
watchlist_updated: (e) => {
|
||||
if (Array.isArray(e.tickers)) {
|
||||
const normalizedTickers = e.tickers.map((symbol) => String(symbol).trim().toUpperCase());
|
||||
requestedStockHistoryRef.current = new Set(
|
||||
Array.from(requestedStockHistoryRef.current).filter((symbol) => normalizedTickers.includes(symbol))
|
||||
);
|
||||
setRuntimeConfig((prev) => ({
|
||||
...(prev || {}),
|
||||
tickers: normalizedTickers
|
||||
}));
|
||||
setTickers((prevTickers) => buildTickersFromSymbols(normalizedTickers, prevTickers));
|
||||
setWatchlistDraftSymbols(normalizedTickers);
|
||||
setWatchlistInputValue('');
|
||||
}
|
||||
setIsWatchlistSaving(false);
|
||||
setWatchlistFeedback({
|
||||
type: 'success',
|
||||
text: `已更新为 ${Array.isArray(e.tickers) ? e.tickers.join(', ') : '最新列表'}`
|
||||
});
|
||||
},
|
||||
|
||||
stock_history_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(e.prices)) {
|
||||
setOhlcHistoryByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: e.prices
|
||||
}));
|
||||
setHistorySourceByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: e.source || null
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
stock_explain_events_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
setExplainEventsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
events: Array.isArray(e.events) ? e.events : [],
|
||||
signals: Array.isArray(e.signals) ? e.signals : [],
|
||||
trades: Array.isArray(e.trades) ? e.trades : []
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
// Real-time price updates
|
||||
price_update: (e) => {
|
||||
try {
|
||||
@@ -402,6 +816,24 @@ export default function LiveTradingApp() {
|
||||
setIsConnected(true);
|
||||
console.log(`[Price Update] ${symbol}: $${price} (ret: ${ret !== undefined ? ret.toFixed(2) : 'N/A'}%)`);
|
||||
|
||||
setPriceHistoryByTicker((prev) => {
|
||||
const ticker = String(symbol).trim().toUpperCase();
|
||||
const nextPoint = {
|
||||
timestamp: new Date().toISOString(),
|
||||
label: now.toISOString(),
|
||||
price: Number(price)
|
||||
};
|
||||
const existing = Array.isArray(prev[ticker]) ? prev[ticker] : [];
|
||||
const lastPoint = existing[existing.length - 1];
|
||||
if (lastPoint && Number(lastPoint.price) === Number(nextPoint.price)) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[ticker]: [...existing, nextPoint].slice(-120)
|
||||
};
|
||||
});
|
||||
|
||||
// Update ticker price with animation
|
||||
setTickers(prevTickers => {
|
||||
return prevTickers.map(ticker => {
|
||||
@@ -714,7 +1146,7 @@ export default function LiveTradingApp() {
|
||||
clientRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
}, []); // Empty dependency array - only run once on mount
|
||||
}, [addSystemMessage, buildTickersFromSymbols, processFeedEvent, processHistoricalFeed]); // Only reconnect if handlers change
|
||||
|
||||
// Resizing handlers
|
||||
const handleMouseDown = (e) => {
|
||||
@@ -755,10 +1187,7 @@ export default function LiveTradingApp() {
|
||||
|
||||
{/* Header */}
|
||||
<div className="header">
|
||||
<Header
|
||||
onEvoTradersClick={() => setShowAboutModal(true)}
|
||||
evoTradersLinkStyle="default"
|
||||
/>
|
||||
<Header />
|
||||
|
||||
<div className="header-right" style={{ display: 'flex', alignItems: 'center', gap: 24, marginLeft: 'auto', flexWrap: 'wrap', minWidth: 0 }}>
|
||||
{/* Mock Mode Indicator */}
|
||||
@@ -885,21 +1314,41 @@ export default function LiveTradingApp() {
|
||||
<>
|
||||
<span className="status-sep">·</span>
|
||||
<span className={`market-text ${serverMode === 'backtest' ? 'backtest' : (marketStatus.status === 'open' ? 'open' : 'closed')}`}>
|
||||
{marketStatus.status_text || (marketStatus.status === 'open' ? '开盘' : '收盘')}
|
||||
{marketStatusLabel}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{dataSources?.last_success?.prices && (
|
||||
{priceSourceLabel && (
|
||||
<>
|
||||
<span className="status-sep">·</span>
|
||||
<span className="market-text backtest">
|
||||
DATA {String(dataSources.last_success.prices).toUpperCase()}
|
||||
{priceSourceLabel}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<WatchlistPanel
|
||||
isOpen={isWatchlistPanelOpen}
|
||||
isConnected={isConnected}
|
||||
isSaving={isWatchlistSaving}
|
||||
draftSymbols={watchlistDraftSymbols}
|
||||
inputValue={watchlistInputValue}
|
||||
feedback={watchlistFeedback}
|
||||
suggestions={watchlistSuggestions}
|
||||
onToggle={handleWatchlistPanelToggle}
|
||||
onClose={() => setIsWatchlistPanelOpen(false)}
|
||||
onInputChange={handleWatchlistInputChange}
|
||||
onInputKeyDown={handleWatchlistInputKeyDown}
|
||||
onAdd={() => commitWatchlistInput(watchlistInputValue)}
|
||||
onRemove={handleWatchlistRemove}
|
||||
onRestoreCurrent={handleWatchlistRestoreCurrent}
|
||||
onRestoreDefault={handleWatchlistRestoreDefault}
|
||||
onSuggestionClick={handleWatchlistSuggestionClick}
|
||||
onSave={handleWatchlistSave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -910,7 +1359,7 @@ export default function LiveTradingApp() {
|
||||
<div className="ticker-track">
|
||||
{[0, 1].map((groupIdx) => (
|
||||
<div key={groupIdx} className="ticker-group">
|
||||
{tickers.map(ticker => (
|
||||
{displayTickers.map(ticker => (
|
||||
<div key={`${ticker.symbol}-${groupIdx}`} className="ticker-item">
|
||||
<StockLogo ticker={ticker.symbol} size={16} />
|
||||
<span className="ticker-symbol">{ticker.symbol}</span>
|
||||
@@ -947,13 +1396,6 @@ export default function LiveTradingApp() {
|
||||
<div className="chart-section">
|
||||
<div className="view-container">
|
||||
<div className="view-nav-bar">
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'rules' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('rules')}
|
||||
>
|
||||
规则
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'room' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('room')}
|
||||
@@ -961,6 +1403,13 @@ export default function LiveTradingApp() {
|
||||
交易室
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'explain' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('explain')}
|
||||
>
|
||||
个股解释
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'chart' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('chart')}
|
||||
@@ -977,12 +1426,15 @@ export default function LiveTradingApp() {
|
||||
</div>
|
||||
|
||||
{/* Slider container with four views */}
|
||||
<div className={`view-slider-four ${currentView === 'rules' ? 'show-rules' : currentView === 'room' ? 'show-room' : currentView === 'statistics' ? 'show-statistics' : 'show-chart'} ${!isInitialAnimating ? 'normal-speed' : ''}`}>
|
||||
{/* Rules View Panel */}
|
||||
<div className="view-panel">
|
||||
<RulesView />
|
||||
</div>
|
||||
|
||||
<div className={`view-slider-four ${
|
||||
currentView === 'room'
|
||||
? 'show-room'
|
||||
: currentView === 'explain'
|
||||
? 'show-explain'
|
||||
: currentView === 'statistics'
|
||||
? 'show-statistics'
|
||||
: 'show-chart'
|
||||
} ${!isInitialAnimating ? 'normal-speed' : ''}`}>
|
||||
{/* Room View Panel */}
|
||||
<div className="view-panel">
|
||||
<RoomView
|
||||
@@ -994,6 +1446,23 @@ export default function LiveTradingApp() {
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chart View Panel */}
|
||||
<div className="view-panel">
|
||||
<div className="chart-container">
|
||||
@@ -1059,9 +1528,6 @@ export default function LiveTradingApp() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* About Modal */}
|
||||
{showAboutModal && <AboutModal onClose={() => setShowAboutModal(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user