确认PokieTicker新闻库数据源

This commit is contained in:
2026-03-16 02:19:25 +08:00
parent 78f133617f
commit 564c92c0c8
182 changed files with 6436 additions and 1050 deletions

View File

@@ -1,7 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<script src="https://cdn.tailwindcss.com"></script>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/trading_logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

View File

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

View File

@@ -1,294 +0,0 @@
import React, { useState } from 'react';
import Header from './Header.jsx';
export default function AboutModal({ onClose }) {
const [isClosing, setIsClosing] = useState(false);
const [language] = useState('zh');
const handleClose = () => {
setIsClosing(true);
// Wait for animation to complete before actually closing
setTimeout(() => {
onClose();
}, 600); // Match animation duration
};
const overlayStyle = {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: '#ffffff',
zIndex: 9999,
animation: isClosing
? 'collapseUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards'
: 'expandDown 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
transformOrigin: 'top center',
overflowY: 'auto'
};
const contentStyle = {
maxWidth: '900px',
width: '90%',
margin: '0 auto',
textAlign: 'left',
fontFamily: "'IBM Plex Mono', monospace",
color: '#000000',
lineHeight: 1.8,
fontSize: '14px',
letterSpacing: '0.01em',
padding: '60px 20px 80px',
animation: isClosing
? 'fadeOutContent 0.4s ease forwards'
: 'fadeInContent 0.8s ease 0.3s backwards'
};
const highlight = {
color: '#615CED',
fontWeight: 600
};
const linkStyle = {
color: '#615CED',
textDecoration: 'none',
borderBottom: '1px solid #615CED',
transition: 'all 0.2s'
};
const closeHintStyle = {
marginTop: '50px',
fontSize: '11px',
color: '#999',
cursor: 'pointer',
textAlign: 'center'
};
const languageSwitchStyle = {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginBottom: '25px',
marginTop: '10px',
gap: '0px',
fontSize: '11px',
fontFamily: "'IBM Plex Mono', monospace"
};
const getLangStyle = (isActive) => ({
padding: '3px 8px',
cursor: 'pointer',
transition: 'all 0.2s',
background: isActive ? '#000' : '#fff',
color: isActive ? '#fff' : '#000',
border: 'none'
});
const content = {
en: {
question: "What happens if AI models don't compete with each other, but instead trade like a ",
questionHighlight: "well-coordinated, high-performance team",
questionEnd: "?",
intro: "Not arena, but TEAM. We Hope that AI is no longer entering the financial markets as isolated models—it is stepping in as ",
introHighlight1: "teams",
introContinue: ", collaborating in one of the most challenging and noise-filled ",
introHighlight2: "real-time environments",
introContinue2: ".",
point1Highlight: "✦ Complementary skills",
point1: " - across multiple agents—data analysis, strategy generation, risk management—working together like a real trading desk, exchanging information through notifications and meetings.",
point2Highlight: "✦ An agent system that continually evolves",
point2: " — with memory modules that retain experience, learn from market feedback, reflect, and develop their own methodology over time.",
point3Highlight: "✦ AI teams interacting with live markets",
point3: " — learning from real-time data and making immediate decisions, not just theoretical simulations."
},
zh: {
intro: "如果不是让模型彼此竞争,而是像一支高效协作的团队一样进行实时交易,会发生什么?",
question: "这里不是竞技场而是团队。我们希望Agents不再单打独斗而是「组团」进入实时金融市场——这一十分困难且充满噪声的环境。",
trying: "我们正在探索多智能体协作在实时金融交易中的可能性。",
title1: "✦ 多智能体的技能互补",
point1: "不同模型、不同角色的智能体像真实的金融团队一样协作,各自承担数据分析、策略生成、风险控制等职责。",
point1Sub: "通过通知和会议机制进行信息交换,实现高效协作。",
title2: "✦ 能够持续进化的智能体系统",
point2: "依托「记忆」模块每个智能体都能跨回合保留经验不断学习、反思与调整。我们希望能看到在长期实时交易中Agent形成自己的独特方法论而不是一次性偶然的推理。",
point2Sub: "ReMe 记忆框架帮助 Agents 持续改进。",
title3: "✦ 实时参与市场的 AI Agents",
point3: "Agents从实时行情中学习并给予即时决策不是纸上谈兵而是面对市场的真实波动。"
}
};
return (
<>
<style>{`
@keyframes expandDown {
from {
transform: scaleY(0);
opacity: 0;
}
to {
transform: scaleY(1);
opacity: 1;
}
}
@keyframes collapseUp {
from {
transform: scaleY(1);
opacity: 1;
}
to {
transform: scaleY(0);
opacity: 0;
}
}
@keyframes fadeInContent {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeOutContent {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
`}</style>
<div style={overlayStyle} onClick={handleClose}>
{/* Header */}
<div className="header" style={{
animation: isClosing
? 'fadeOutContent 0.4s ease forwards'
: 'fadeInContent 0.8s ease 0.3s backwards'
}} onClick={(e) => e.stopPropagation()}>
<Header
onEvoTradersClick={handleClose}
evoTradersLinkStyle="close"
/>
</div>
{/* Content */}
<div style={contentStyle} onClick={(e) => e.stopPropagation()}>
<div style={languageSwitchStyle}>
<span
style={getLangStyle(true)}
>
中文
</span>
</div>
// Chinese Content
<>
<div style={{ marginBottom: '30px' }}>
{content.zh.intro}
</div>
<div style={{ marginBottom: '40px', fontSize: '15px', fontWeight: 600 }}>
{content.zh.question}
</div>
<div style={{ marginBottom: '30px', fontSize: '14px', opacity: 0.8 }}>
{content.zh.trying}
</div>
<div style={{ marginBottom: '30px' }}>
<div style={{ ...highlight, marginBottom: '10px' }}>
{content.zh.title1}
</div>
<div style={{ marginBottom: '10px' }}>
{content.zh.point1}
</div>
<div style={{ fontSize: '13px', opacity: 0.7 }}>
{content.zh.point1Sub}
</div>
</div>
<div style={{ marginBottom: '30px' }}>
<div style={{ ...highlight, marginBottom: '10px' }}>
{content.zh.title2}
</div>
<div style={{ marginBottom: '10px' }}>
{content.zh.point2}
</div>
<div style={{ fontSize: '13px', opacity: 0.7 }}>
{content.zh.point2Sub}
</div>
</div>
<div style={{ marginBottom: '30px' }}>
<div style={{ ...highlight, marginBottom: '10px' }}>
{content.zh.title3}
</div>
<div>
{content.zh.point3}
</div>
</div>
<div style={{ marginBottom: '10px', opacity: 0.7 }}>
我们已经在 GitHub 上开源
</div>
<div style={{ marginBottom: '25px', opacity: 0.7 }}>
EvoTraders 基于{' '}
<a
href="https://github.com/agentscope-ai"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
AgentScope
</a>
{' '}搭建并使用其中的{' '}
<a
href="https://github.com/agentscope-ai/ReMe"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
ReMe
</a>
{' '}作为记忆管理核心
</div>
<div style={{ marginBottom: '10px', fontSize: '14px' }}>
你可以在此找到完整项目与示例
</div>
</>
<div style={{ marginTop: '40px' }}>
<a
href="https://github.com/agentscope-ai/agentscope-samples"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
github.com/agentscope-ai/agentscope-samples
</a>
</div>
<div style={closeHintStyle} onClick={handleClose}>
点击此处关闭
</div>
</div>
</div>
</>
);
}

View File

@@ -149,7 +149,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => {
// Get current selection display info
const getCurrentSelectionInfo = () => {
if (selectedAgent === 'all') {
return { label: 'All Agents', modelInfo: null };
return { label: '全部角色', modelInfo: null };
}
const agentInfo = getAgentInfoByName(selectedAgent);
const modelInfo = agentInfo ? getModelIcon(agentInfo.modelName, agentInfo.modelProvider) : null;
@@ -191,7 +191,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => {
setDropdownOpen(false);
}}
>
<span>全部 Agents</span>
<span>全部角色</span>
</div>
{uniqueAgents.map(agent => {
const agentInfo = getAgentInfoByName(agent);
@@ -419,17 +419,14 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<a
href="https://github.com/agentscope-ai/ReMe"
target="_blank"
rel="noopener noreferrer"
<span
style={{ display: 'flex', alignItems: 'center', textDecoration: 'none' }}
>
<img
src={ASSETS.remeLogo}
alt="ReMe"
alt="Memory"
style={{
cursor: 'pointer',
cursor: 'default',
height: '12px',
width: 'auto',
objectFit: 'contain',
@@ -449,9 +446,9 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
lineHeight: 1,
pointerEvents: 'none'
}}>
MEMORY
</span>
</a>
</span>
</div>
<span style={{
background: 'linear-gradient(90deg, #00C2FF 0%, #5C4CE0 100%)',
@@ -497,10 +494,10 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
color: 'transparent',
display: 'inline-block'
}}>
Memory powered by AgentScope-ReMe
Runtime Memory Layer
</div>
<div style={{ color: '#475569', opacity: 0.9 }}>
Not only retrieves historical memories but also generates suggestions and hints for the current task based on latest context.
Retrieves relevant historical context and produces guidance for the current task based on the latest conversation state.
</div>
</div>

View File

@@ -1,253 +1,29 @@
import React, { useState } from 'react';
import React from 'react';
/**
* Header Component
* Reusable header brand with EvoTraders logo, GitHub link, and Contact Us section
*
* @param {Function} onEvoTradersClick - Optional callback when EvoTraders is clicked
* @param {string} evoTradersLinkStyle - Optional style variant: 'default' | 'close'
* Reusable header brand for EvoTraders.
*/
export default function Header({
onEvoTradersClick = null,
evoTradersLinkStyle = 'default' // 'default' shows ↗, 'close' shows ↙
}) {
const [activeContactCard, setActiveContactCard] = useState({ yue: false, jiaji: false });
const [clickedContactCard, setClickedContactCard] = useState(null);
const handleEvoTradersClick = () => {
if (onEvoTradersClick) {
onEvoTradersClick();
}
};
export default function Header() {
return (
<div className="header-title" style={{ flex: '0 1 auto', minWidth: 0 }}>
<span
className="header-link"
onClick={handleEvoTradersClick}
style={{ cursor: 'pointer', padding: '4px 8px', borderRadius: '3px', display: 'inline-flex', alignItems: 'center', gap: '8px' }}
style={{
padding: '4px 8px',
borderRadius: '3px',
display: 'inline-flex',
alignItems: 'center',
gap: '8px'
}}
>
<img
src="/trading_logo.png"
alt="EvoTraders Logo"
style={{ height: '24px', width: 'auto' }}
/>
EvoTraders {evoTradersLinkStyle === 'close' ? (
<span className="link-arrow"></span>
) : (
<span className="link-arrow"></span>
)}
EvoTraders
</span>
<span style={{
width: '2px',
height: '16px',
background: '#666',
margin: '0 16px',
display: 'inline-block',
verticalAlign: 'middle'
}} />
<span style={{
padding: '1px 5px',
fontSize: '9px',
fontWeight: 700,
color: '#00C853',
background: 'rgba(0, 200, 83, 0.1)',
border: '1px solid #00C853',
borderRadius: '3px',
letterSpacing: '0.5px',
marginRight: '0px'
}}>
开源
</span>
<a
href="https://github.com/agentscope-ai/agentscope-samples"
target="_blank"
rel="noopener noreferrer"
className="header-link"
style={{ display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: '6px' }}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="currentColor"
style={{ display: 'inline-block' }}
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
<span>agentscope-samples</span>
<span className="link-arrow"></span>
</a>
<a
href="https://github.com/agentscope-ai/ReMe"
target="_blank"
rel="noopener noreferrer"
className="header-link"
style={{ display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: '6px', marginLeft: '0px' }}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="currentColor"
style={{ display: 'inline-block' }}
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
<span>agentscope-ReMe</span>
<span className="link-arrow"></span>
</a>
<span style={{
width: '2px',
height: '16px',
background: '#666',
margin: '0 16px',
display: 'inline-block',
verticalAlign: 'middle'
}} />
<div
style={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
cursor: 'pointer'
}}
onClick={() => {
const bothActive = activeContactCard.yue && activeContactCard.jiaji;
if (!bothActive) {
setActiveContactCard({ yue: true, jiaji: true });
setClickedContactCard('both');
} else {
setActiveContactCard({ yue: false, jiaji: false });
setClickedContactCard(null);
}
}}
>
<span className="header-link">
联系我们
</span>
{/* Two contact buttons */}
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
<div
onClick={(e) => {
e.stopPropagation();
if (activeContactCard.yue) {
setActiveContactCard(prev => ({ ...prev, yue: false }));
if (clickedContactCard === 'yue' || clickedContactCard === 'both') {
setClickedContactCard(null);
}
} else {
setActiveContactCard(prev => ({ ...prev, yue: true }));
setClickedContactCard('yue');
}
}}
onMouseEnter={() => {
if (!clickedContactCard || clickedContactCard === 'yue' || clickedContactCard === 'both') {
setActiveContactCard(prev => ({ ...prev, yue: true }));
}
}}
onMouseLeave={() => {
if (clickedContactCard !== 'yue' && clickedContactCard !== 'both') {
setActiveContactCard(prev => ({ ...prev, yue: false }));
}
}}
style={{
padding: '4px 8px',
background: activeContactCard.yue ? '#615CED' : '#f5f5f5',
color: activeContactCard.yue ? '#fff' : '#333',
border: '1px solid',
borderColor: activeContactCard.yue ? '#615CED' : '#e0e0e0',
borderRadius: '3px',
fontSize: '10px',
fontWeight: 700,
fontFamily: "'IBM Plex Mono', monospace",
cursor: 'pointer',
transition: 'all 0.2s',
letterSpacing: '0.5px',
whiteSpace: 'nowrap',
overflow: 'hidden',
maxWidth: activeContactCard.yue ? '80px' : '32px',
minWidth: activeContactCard.yue ? '80px' : '32px'
}}
>
{activeContactCard.yue ? (
<a
href="https://1mycell.github.io/"
target="_blank"
rel="noopener noreferrer"
style={{ color: 'inherit', textDecoration: 'none' }}
onClick={(e) => e.stopPropagation()}
>
Yue Wu
</a>
) : 'YW'}
</div>
<div
onClick={(e) => {
e.stopPropagation();
if (activeContactCard.jiaji) {
setActiveContactCard(prev => ({ ...prev, jiaji: false }));
if (clickedContactCard === 'jiaji' || clickedContactCard === 'both') {
setClickedContactCard(null);
}
} else {
setActiveContactCard(prev => ({ ...prev, jiaji: true }));
setClickedContactCard('jiaji');
}
}}
onMouseEnter={() => {
if (!clickedContactCard || clickedContactCard === 'jiaji' || clickedContactCard === 'both') {
setActiveContactCard(prev => ({ ...prev, jiaji: true }));
}
}}
onMouseLeave={() => {
if (clickedContactCard !== 'jiaji' && clickedContactCard !== 'both') {
setActiveContactCard(prev => ({ ...prev, jiaji: false }));
}
}}
style={{
padding: '4px 8px',
background: activeContactCard.jiaji ? '#615CED' : '#f5f5f5',
color: activeContactCard.jiaji ? '#fff' : '#333',
border: '1px solid',
borderColor: activeContactCard.jiaji ? '#615CED' : '#e0e0e0',
borderRadius: '3px',
fontSize: '10px',
fontWeight: 700,
fontFamily: "'IBM Plex Mono', monospace",
cursor: 'pointer',
transition: 'all 0.2s',
letterSpacing: '0.5px',
whiteSpace: 'nowrap',
overflow: 'hidden',
maxWidth: activeContactCard.jiaji ? '100px' : '32px',
minWidth: activeContactCard.jiaji ? '100px' : '32px'
}}
>
{activeContactCard.jiaji ? (
<a
href="https://dengjiaji.github.io/self/"
target="_blank"
rel="noopener noreferrer"
style={{ color: 'inherit', textDecoration: 'none' }}
onClick={(e) => e.stopPropagation()}
>
Jiaji Deng
</a>
) : 'JD'}
</div>
</div>
</div>
</div>
);
}

View File

@@ -554,7 +554,7 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
fontFamily: '"Courier New", monospace',
fontSize: '12px'
}}>
NO DATA AVAILABLE
暂无图表数据
</div>
);
}
@@ -828,4 +828,3 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
</ResponsiveContainer>
);
}

View File

@@ -1,360 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import { LLM_MODEL_LOGOS } from '../config/constants';
export default function RulesView() {
const [language] = useState('zh');
const [scale, setScale] = useState(1);
const containerRef = useRef(null);
const contentRef = useRef(null);
// Auto-scale content to fit container without scrolling
useEffect(() => {
const handleResize = () => {
if (containerRef.current && contentRef.current) {
const containerHeight = containerRef.current.clientHeight;
const contentHeight = contentRef.current.scrollHeight;
if (contentHeight > containerHeight) {
const newScale = containerHeight / contentHeight;
setScale(Math.max(newScale * 0.95, 0.5)); // Min scale 0.5, with 95% of available space
} else {
setScale(1);
}
}
};
// Initial resize
handleResize();
// Listen to window resize
window.addEventListener('resize', handleResize);
// Observe content changes
const observer = new ResizeObserver(handleResize);
if (contentRef.current) {
observer.observe(contentRef.current);
}
return () => {
window.removeEventListener('resize', handleResize);
observer.disconnect();
};
}, [language]);
const containerStyle = {
width: '100%',
height: '100%',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#FFFFFF',
padding: '10px'
};
const contentWrapperStyle = {
transform: `scale(${scale})`,
transformOrigin: 'center center',
transition: 'transform 0.3s ease',
width: '100%',
maxWidth: '900px'
};
const innerContentStyle = {
color: '#000000',
fontFamily: "'IBM Plex Mono', monospace",
fontSize: '13px',
lineHeight: '1.6',
letterSpacing: '0.01em',
padding: '0 10px'
};
const highlight = {
color: '#000000',
fontWeight: 700
};
const sectionTitleStyle = {
color: '#615CED',
fontSize: '16px',
fontWeight: 700,
marginBottom: '8px',
marginTop: '12px',
marginLeft: '-10px',
marginRight: '-10px',
width: 'calc(100% + 20px)',
padding: '8px 10px',
backgroundColor: '#FFFFFF',
letterSpacing: '0.5px',
boxSizing: 'border-box'
};
const subsectionStyle = {
marginBottom: '8px',
paddingLeft: '10px',
borderLeft: '2px solid #CCCCCC'
};
const linkStyle = {
color: '#615CED',
textDecoration: 'none',
borderBottom: '1px solid #615CED',
transition: 'all 0.2s'
};
const languageSwitchStyle = {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginBottom: '12px',
gap: '0px',
fontSize: '11px',
fontFamily: "'IBM Plex Mono', monospace"
};
const getLangStyle = (isActive) => ({
padding: '4px 10px',
cursor: 'pointer',
transition: 'all 0.2s',
background: isActive ? '#000000' : 'transparent',
color: isActive ? '#FFFFFF' : '#666666',
border: 'none',
borderRadius: '2px'
});
const llmLogos = [
{ name: 'Alibaba', file: 'Alibaba.jpeg', label: 'Qwen', url: LLM_MODEL_LOGOS['Alibaba'] },
{ name: 'DeepSeek', file: 'DeepSeek.png', label: 'DeepSeek', url: LLM_MODEL_LOGOS['DeepSeek'] },
{ name: 'Moonshot', file: 'Moonshot.jpeg', label: 'Moonshot', url: LLM_MODEL_LOGOS['Moonshot'] },
{ name: 'Zhipu AI', file: 'Zhipu AI.png', label: 'Zhipu AI', url: LLM_MODEL_LOGOS['Zhipu AI'] }
];
const content = {
en: {
section1Title: "Agent Setup",
pmRole: "Portfolio Manager",
pmDesc: "Makes final trading decisions and orchestrates team collaboration",
rmRole: "Risk Manager",
rmDesc: "Monitors portfolio risk and enforces risk limits",
analystsRole: "Analysts",
analystsDesc: "Conduct specialized research with different tools and AI models:",
analysts: [
{ name: "Valuation Analyst", model: "Moonshot", modelKey: "Moonshot" },
{ name: "Sentiment Analyst", model: "Qwen", modelKey: "Alibaba" },
{ name: "Fundamentals Analyst", model: "DeepSeek", modelKey: "DeepSeek" },
{ name: "Technical Analyst", model: "Zhipu AI", modelKey: "Zhipu AI" }
],
section2Title: "Agent Decision Mechanism",
tradingProcess: "Daily Trading Process",
tradingDesc: "Agents trade on a daily frequency while continuously tracking portfolio performance. Before each day's final trading decision, agents go through three key phases:",
analysisPhase: "• Analysis Phase",
analysisDesc: "All agents independently analyze information and form judgments based on their specialized tools.",
communicationPhase: "• Communication Phase",
commIntro: "Multiple communication channels enable effective collaboration: 1v1 Private Chat / 1vN Notification / NvN Conference",
decisionPhase: "• Decision Phase",
decisionDesc: "Portfolio Manager aggregates all information and makes the final team trading decision. The original trading signals from analysts are only used for individual-level ranking.",
reflectionTitle: "Learning & Evolution",
reflectionDesc: "Agents reflect on daily investment performance, summarize insights, and store them in ",
remeLink: "ReMe",
reflectionDesc2: " memory framework for continuous improvement.",
section3Title: "Performance Evaluation",
chartTitle: "• Performance Chart",
chartDesc: "Track portfolio equity curve vs. benchmarks (equal-weight, value-weighted, momentum). Use this to assess overall strategy effectiveness.",
rankingTitle: "• Analyst Rankings",
rankingDesc: "Click avatars in Trading Room to view analyst performance (Win Rate, Bull/Bear Win Rate). Use this to understand which analysts provide the most valuable insights.",
statsTitle: "• Statistics",
statsDesc: "Detailed holdings and trade history. Use this for in-depth analysis of position management and execution quality.",
callToAction: "Fork on ",
repoLink: "GitHub",
callToAction2: " to customize!"
},
zh: {
section1Title: "Agent 设定",
pmRole: "投资经理",
pmDesc: "负责最终交易决策和团队协作",
rmRole: "风控经理",
rmDesc: "监控组合风险并执行风险限制",
analystsRole: "分析师",
analystsDesc: "使用不同工具和 AI 模型进行专业研究:",
analysts: [
{ name: "估值分析师", model: "Moonshot", modelKey: "Moonshot" },
{ name: "情绪分析师", model: "Qwen", modelKey: "Alibaba" },
{ name: "基本面分析师", model: "DeepSeek", modelKey: "DeepSeek" },
{ name: "技术分析师", model: "Zhipu AI", modelKey: "Zhipu AI" }
],
section2Title: "Agent 决策机制",
tradingProcess: "交易流程",
tradingDesc: "智能体以日频进行交易并持续跟踪组合净值。每天最终交易决策前,会经历三个关键阶段:",
analysisPhase: "• 分析阶段",
analysisDesc: "所有智能体根据各自的工具和信息独立分析并形成判断。",
communicationPhase: "• 沟通阶段",
commIntro: "提供了多种沟通渠道1v1 私聊 / 1vN 通知 / NvN 会议",
decisionPhase: "• 决策阶段",
decisionDesc: "由投资经理汇总所有信息,并给出最终的团队交易决策。分析师给出的原始交易信号仅用于个人维度排名。",
reflectionTitle: "学习与进化",
reflectionDesc: "智能体根据当日实际收益反思决策、总结经验,并存入 ",
remeLink: "ReMe",
reflectionDesc2: " 记忆框架以持续改进。",
section3Title: "收益评估",
chartTitle: "• 业绩图表",
chartDesc: "追踪组合收益曲线 vs. 基准策略(等权、市值加权、动量)。用于评估整体策略有效性。",
rankingTitle: "• 分析师排名",
rankingDesc: "在交易室点击头像查看分析师表现(胜率、牛/熊市胜率),用来了解哪些分析师提供了最有价值的洞察。",
statsTitle: "• 统计数据",
statsDesc: "详细的持仓和交易历史。用于深入分析仓位管理和执行质量。",
callToAction: "可在 ",
repoLink: "GitHub",
callToAction2: " 上 Fork 并自行定制。"
}
};
return (
<div ref={containerRef} style={containerStyle}>
<div ref={contentRef} style={contentWrapperStyle}>
<div style={innerContentStyle}>
<div style={languageSwitchStyle}>
<span
style={getLangStyle(true)}
>
中文
</span>
</div>
// Chinese Content
<>
{/* 第一部分Agent 设定 */}
<div style={sectionTitleStyle}>{content.zh.section1Title}</div>
{/* 角色 */}
<div style={{ marginBottom: '8px', fontSize: '12px' }}>
<div style={{ marginBottom: '3px' }}>
<span style={{ fontWeight: 600 }}>{content.zh.pmRole}:</span> {content.zh.pmDesc}
</div>
<div style={{ marginBottom: '3px' }}>
<span style={{ fontWeight: 600 }}>{content.zh.rmRole}:</span> {content.zh.rmDesc}
</div>
<div style={{ marginBottom: '3px' }}>
<span style={{ fontWeight: 600 }}>{content.zh.analystsRole}:</span> {content.zh.analystsDesc}
</div>
</div>
{/* Analysts 与 AI 模型 */}
<div style={{ marginLeft: '10px', marginBottom: '8px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px 14px', fontSize: '11px' }}>
{content.zh.analysts.map(analyst => {
const logo = llmLogos.find(l => l.name === analyst.modelKey);
return (
<div key={analyst.name} style={{
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
{logo && (
<img
src={logo.url}
alt={logo.label}
style={{
height: '16px',
width: 'auto',
objectFit: 'contain'
}}
/>
)}
<span style={{ fontWeight: 600 }}>{analyst.name}</span>
<span style={{ color: '#666' }}>- {analyst.model}</span>
</div>
);
})}
</div>
<div style={{ marginBottom: '10px', fontSize: '11px', fontStyle: 'italic', opacity: 0.8 }}>
{content.zh.callToAction}
<a
href="https://github.com/agentscope-ai/agentscope-samples"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
{content.zh.repoLink}
</a>
{content.zh.callToAction2}
</div>
{/* 第二部分Agent 决策机制 */}
<div style={sectionTitleStyle}>{content.zh.section2Title}</div>
<div style={{ marginBottom: '6px' }}>
<div style={{ fontWeight: 600, marginBottom: '3px' }}>{content.zh.tradingProcess}</div>
<div style={{ marginBottom: '6px', fontSize: '12px' }}>{content.zh.tradingDesc}</div>
<div style={subsectionStyle}>
<div style={{ marginBottom: '4px', fontSize: '12px' }}>
<span style={highlight}>{content.zh.analysisPhase.replace('• ', '')}:</span> {content.zh.analysisDesc}
</div>
<div style={{ marginBottom: '4px', fontSize: '12px' }}>
<span style={highlight}>{content.zh.communicationPhase.replace('• ', '')}:</span> {content.zh.commIntro}
</div>
<div style={{ fontSize: '12px' }}>
<span style={highlight}>{content.zh.decisionPhase.replace('• ', '')}:</span> {content.zh.decisionDesc}
</div>
</div>
</div>
<div style={{ marginBottom: '10px' }}>
<div style={{ fontWeight: 600, marginBottom: '3px' }}>{content.zh.reflectionTitle}</div>
<div style={{ fontSize: '12px' }}>
{content.zh.reflectionDesc}
<a
href="https://github.com/agentscope-ai/ReMe"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
{content.zh.remeLink}
</a>
{content.zh.reflectionDesc2}
</div>
</div>
{/* 第三部分:收益评估 */}
<div style={sectionTitleStyle}>{content.zh.section3Title}</div>
<div style={subsectionStyle}>
<div style={{ marginBottom: '3px', fontSize: '12px' }}>
<span style={{ fontWeight: 600 }}>{content.zh.chartTitle.replace('• ', '')}:</span> {content.zh.chartDesc}
</div>
<div style={{ marginBottom: '3px', fontSize: '12px' }}>
<span style={{ fontWeight: 600 }}>{content.zh.rankingTitle.replace('• ', '')}:</span> {content.zh.rankingDesc}
</div>
<div style={{ fontSize: '12px' }}>
<span style={{ fontWeight: 600 }}>{content.zh.statsTitle.replace('• ', '')}:</span> {content.zh.statsDesc}
</div>
</div>
</>
</div>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,244 @@
import React from 'react';
export default function WatchlistPanel({
isOpen,
isConnected,
isSaving,
draftSymbols,
inputValue,
feedback,
suggestions,
onToggle,
onClose,
onInputChange,
onInputKeyDown,
onAdd,
onRemove,
onRestoreCurrent,
onRestoreDefault,
onSuggestionClick,
onSave
}) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, position: 'relative' }}>
<button
onClick={onToggle}
style={{
padding: '6px 10px',
borderRadius: 4,
border: '1px solid #333333',
background: isOpen ? '#1E1E1E' : '#111111',
color: '#FFFFFF',
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.6px',
cursor: 'pointer',
whiteSpace: 'nowrap'
}}
>
WATCHLIST
</button>
{isOpen && (
<div style={{
position: 'absolute',
top: 'calc(100% + 10px)',
right: 0,
width: 360,
maxWidth: 'min(360px, 92vw)',
padding: '14px',
borderRadius: 8,
border: '1px solid #D9D9D9',
background: '#FFFFFF',
boxShadow: '0 12px 36px rgba(0, 0, 0, 0.14)',
zIndex: 40,
display: 'flex',
flexDirection: 'column',
gap: 12
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
<div>
<div style={{ fontSize: '12px', fontWeight: 700, color: '#111111', letterSpacing: '0.3px' }}>
自选股管理
</div>
<div style={{ fontSize: '11px', color: '#666666', marginTop: 2 }}>
保存后会立即更新当前 run watchlist
</div>
</div>
<button
onClick={onClose}
style={{
border: 'none',
background: 'transparent',
color: '#666666',
cursor: 'pointer',
fontSize: '14px',
lineHeight: 1
}}
>
×
</button>
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 8,
minHeight: 36,
padding: '2px 0'
}}>
{draftSymbols.map((symbol) => (
<button
key={symbol}
onClick={() => onRemove(symbol)}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
padding: '6px 10px',
borderRadius: 999,
border: '1px solid #D0D7DE',
background: '#F7F9FB',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
<span>{symbol}</span>
<span style={{ color: '#777777' }}>×</span>
</button>
))}
{draftSymbols.length === 0 && (
<div style={{ fontSize: '11px', color: '#888888', padding: '8px 2px' }}>
还没有股票输入代码后回车添加
</div>
)}
</div>
<div style={{ display: 'flex', gap: 8 }}>
<input
value={inputValue}
onChange={(e) => onInputChange(e.target.value)}
onKeyDown={onInputKeyDown}
placeholder="输入股票代码,回车添加"
style={{
flex: 1,
padding: '9px 10px',
borderRadius: 6,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
<button
onClick={onAdd}
style={{
padding: '9px 12px',
borderRadius: 6,
border: '1px solid #D0D7DE',
background: '#F7F9FB',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
添加
</button>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{suggestions.map((symbol) => {
const active = draftSymbols.includes(symbol);
return (
<button
key={symbol}
onClick={() => onSuggestionClick(symbol)}
disabled={active}
style={{
padding: '5px 8px',
borderRadius: 999,
border: '1px solid',
borderColor: active ? '#B6E3C5' : '#D0D7DE',
background: active ? '#ECFDF3' : '#FFFFFF',
color: active ? '#157347' : '#4A5568',
fontSize: '10px',
fontWeight: 700,
cursor: active ? 'default' : 'pointer'
}}
>
{symbol}
</button>
);
})}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button
onClick={onRestoreCurrent}
style={{
padding: '8px 10px',
borderRadius: 6,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
恢复当前
</button>
<button
onClick={onRestoreDefault}
style={{
padding: '8px 10px',
borderRadius: 6,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
恢复默认
</button>
</div>
<button
onClick={onSave}
disabled={!isConnected || isSaving}
style={{
padding: '9px 14px',
borderRadius: 6,
border: '1px solid #1565C0',
background: isConnected && !isSaving ? '#0D47A1' : '#94A3B8',
color: '#FFFFFF',
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.4px',
cursor: isConnected && !isSaving ? 'pointer' : 'not-allowed'
}}
>
{isSaving ? '保存中' : '保存'}
</button>
</div>
{feedback && (
<span style={{
color: feedback.type === 'success' ? '#00C853' : '#FF5252',
fontSize: '11px',
fontFamily: '"Courier New", monospace'
}}>
{feedback.text}
</span>
)}
</div>
)}
</div>
);
}

View File

@@ -3,6 +3,88 @@ import { AGENTS } from "../config/constants";
const MAX_FEED_ITEMS = 200;
const normalizeSystemContent = (content) => {
if (typeof content !== "string") {
return content;
}
const trimmed = content.trim();
if (!trimmed) {
return trimmed;
}
if (trimmed === "Runtime assets reloaded." || trimmed === "运行时配置已热更新") {
return "配置已刷新";
}
if (trimmed.startsWith("Watchlist updated:")) {
const symbols = trimmed.replace("Watchlist updated:", "").trim();
return symbols ? `自选已更新: ${symbols}` : "自选已更新";
}
if (trimmed === "已连接实时数据服务") {
return "已连接";
}
if (trimmed === "正在尝试连接数据服务...") {
return "连接中...";
}
if (trimmed.startsWith("day_start:")) {
const value = trimmed.replace("day_start:", "").trim();
return value ? `交易日开始:${value}` : "交易日开始";
}
if (trimmed.startsWith("day_complete:")) {
const value = trimmed.replace("day_complete:", "").trim();
return value ? `交易日结束:${value}` : "交易日结束";
}
if (trimmed.startsWith("day_error:")) {
const value = trimmed.replace("day_error:", "").trim();
return value ? `交易日异常:${value}` : "交易日异常";
}
return trimmed;
};
const normalizeConferenceTitle = (title) => {
if (typeof title !== "string") {
return "投资讨论";
}
const trimmed = title.trim();
if (!trimmed) {
return "投资讨论";
}
if (trimmed.startsWith("Investment Discussion -")) {
const date = trimmed.replace("Investment Discussion -", "").trim();
return date ? `投资讨论 · ${date}` : "投资讨论";
}
if (trimmed === "Team Conference") {
return "投资讨论";
}
return trimmed;
};
const normalizeAgentLabel = (agentName, agentId) => {
if (typeof agentName === "string") {
const trimmed = agentName.trim();
if (trimmed.toLowerCase() === "conference summary") {
return "会议总结";
}
}
if (typeof agentId === "string" && agentId.trim().toLowerCase() === "conference summary") {
return "会议总结";
}
return agentName;
};
/**
* Generate a unique ID for feed items
*/
@@ -26,7 +108,7 @@ const eventToMessage = (evt) => {
id: generateId("msg"),
timestamp,
agentId: evt.agentId,
agent: agent?.name || evt.agentName || evt.agentId || "Agent",
agent: normalizeAgentLabel(agent?.name || evt.agentName || evt.agentId || "Agent", evt.agentId),
role: agent?.role || evt.role || "Agent",
content: evt.content
};
@@ -50,7 +132,7 @@ const eventToMessage = (evt) => {
timestamp,
agent: "System",
role: "System",
content: evt.content || `${evt.type}: ${evt.date || ""}`
content: normalizeSystemContent(evt.content || `${evt.type}: ${evt.date || ""}`)
};
default:
@@ -129,7 +211,7 @@ export function useFeedProcessor() {
// Start a new conference
currentConference = {
id: evt.conferenceId || generateId("conf"),
title: evt.title || "Team Conference",
title: normalizeConferenceTitle(evt.title || "Team Conference"),
startTime: evt.timestamp || evt.ts || Date.now(),
endTime: null,
isLive: false,
@@ -209,7 +291,7 @@ export function useFeedProcessor() {
if (evt.type === "conference_start") {
const conference = {
id: evt.conferenceId || generateId("conf"),
title: evt.title || "Team Conference",
title: normalizeConferenceTitle(evt.title || "Team Conference"),
startTime: evt.timestamp || evt.ts || Date.now(),
endTime: null,
isLive: true,
@@ -312,7 +394,7 @@ export function useFeedProcessor() {
timestamp: Date.now(),
agent: "System",
role: "System",
content
content: normalizeSystemContent(content)
};
const activeConf = activeConferenceRef.current;

View File

@@ -1030,8 +1030,9 @@ export default function GlobalStyles() {
/* Three-view slider (Room / Chart / Statistics) */
.view-slider-three {
position: absolute;
top: 40px;
width: 300%;
height: 100%;
height: calc(100% - 40px);
display: flex;
transition: transform 1.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@@ -1052,7 +1053,7 @@ export default function GlobalStyles() {
transform: translateX(-66.666%);
}
/* Four-view slider (Rules / Room / Chart / Statistics) */
/* Four-view slider (Room / Explain / Chart / Statistics) */
.view-slider-four {
position: absolute;
top: 40px;
@@ -1066,11 +1067,11 @@ export default function GlobalStyles() {
transition: transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.view-slider-four.show-rules {
.view-slider-four.show-room {
transform: translateX(0);
}
.view-slider-four.show-room {
.view-slider-four.show-explain {
transform: translateX(-25%);
}
@@ -1873,4 +1874,3 @@ export default function GlobalStyles() {
`}</style>
);
}