Align branding, prompts, and deployment tooling

This commit is contained in:
2026-03-28 22:16:56 +08:00
parent 4aa69650e8
commit 4295293a21
90 changed files with 1320 additions and 2044 deletions

View File

@@ -2,13 +2,13 @@
```bash
cd frontend
npm install
npm ci
npm run dev
```
Default dev URL: `http://localhost:5173`
The frontend expects the EvoTraders gateway WebSocket on `ws://localhost:8765` unless overridden.
The frontend expects the 大时代 gateway WebSocket on `ws://localhost:8765` unless overridden.
## Recommended Local Backend Stack
@@ -40,6 +40,16 @@ VITE_WS_URL=ws://localhost:8765
There is also a starter template at [frontend/env.template](./env.template).
For production deployments, prefer:
```bash
cd frontend
npm ci
npm run build
```
This ensures the deployed frontend matches the checked-in `package-lock.json`.
## Direct-Service Coverage
Current direct-call coverage includes:

View File

@@ -1,10 +1,24 @@
# Frontend Environment Variables Template
# 复制此文件为 .env 并修改配置
# WebSocket服务器地址
# 本地开发
# 控制面 APIagent/workspaces/guard
VITE_CONTROL_API_BASE_URL=http://localhost:8000/api
# 运行时 APIstart/stop/runtime info
VITE_RUNTIME_API_BASE_URL=http://localhost:8003/api/runtime
# 新闻服务(可选,未配置时走默认回退)
VITE_NEWS_SERVICE_URL=http://localhost:8002
# 交易数据服务(可选,未配置时走默认回退)
VITE_TRADING_SERVICE_URL=http://localhost:8001
# WebSocket Gateway
VITE_WS_URL=ws://localhost:8765
# 生产环境(替换为你的实际服务器地址)
# VITE_WS_URL=wss://your-server.com:8765
# 生产环境示例
# VITE_CONTROL_API_BASE_URL=https://your-domain.com/api
# VITE_RUNTIME_API_BASE_URL=https://your-domain.com/api/runtime
# VITE_NEWS_SERVICE_URL=https://your-domain.com/news
# VITE_TRADING_SERVICE_URL=https://your-domain.com/trading
# VITE_WS_URL=wss://your-domain.com/ws

View File

@@ -4,7 +4,7 @@
<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" />
<title>EvoTraders</title>
<title>大时代</title>
</head>
<body>
<div id="root"></div>

View File

@@ -3,6 +3,10 @@
"private": true,
"version": "0.0.0",
"type": "module",
"engines": {
"node": ">=20",
"npm": ">=10"
},
"scripts": {
"dev": "vite",
"build": "vite build",
@@ -15,7 +19,7 @@
"dependencies": {
"@dicebear/collection": "^9.4.2",
"@dicebear/core": "^9.4.2",
"@lobehub/icons": "^5.0.1",
"@lobehub/icons": "^5.2.0",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",

View File

@@ -21,10 +21,7 @@ const EDITABLE_AGENT_WORKSPACE_FILES = [
'PROFILE.md',
'AGENTS.md',
'MEMORY.md',
'POLICY.md',
'HEARTBEAT.md',
'ROLE.md',
'STYLE.md'
'POLICY.md'
];
export default function LiveTradingApp() {

View File

@@ -2,7 +2,6 @@ import React, { Suspense, lazy, useRef, useEffect, useMemo } from 'react';
import GlobalStyles from '../styles/GlobalStyles';
import Header from './Header.jsx';
import RuntimeSettingsPanel from './RuntimeSettingsPanel.jsx';
import StockLogo from './StockLogo.jsx';
import NetValueChart from './NetValueChart.jsx';
import { AGENTS } from '../config/constants';
import { useRuntimeStore } from '../store/runtimeStore';
@@ -322,7 +321,6 @@ export default function AppShell({
<div key={groupIdx} className="ticker-group">
{displayTickers.map(ticker => (
<div key={`${ticker.symbol}-${groupIdx}`} className="ticker-item">
<StockLogo ticker={ticker.symbol} size={16} />
<span className="ticker-symbol">{ticker.symbol}</span>
<span className="ticker-price">
<span className={`ticker-price-value ${rollingTickers[ticker.symbol] ? 'rolling' : ''}`}>

View File

@@ -2,7 +2,7 @@ import React from 'react';
/**
* Header Component
* Reusable header brand for EvoTraders.
* Reusable header brand for 大时代.
*/
export default function Header() {
return (
@@ -19,10 +19,10 @@ export default function Header() {
>
<img
src="/trading_logo.png"
alt="EvoTraders Logo"
alt="大时代 Logo"
style={{ height: '24px', width: 'auto' }}
/>
EvoTraders
大时代
</span>
</div>
);

View File

@@ -123,7 +123,7 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
// Legend descriptions
const legendDescriptions = {
'EvoTraders': 'EvoTraders is our agents investment strategy',
'大时代': '大时代 is our agents investment strategy',
'Buy & Hold (EW)': 'Equal Weight: Can be viewed as an equal-weighted index of all invested stocks',
'Buy & Hold (VW)': 'Value Weighted: Can be viewed as a market-cap weighted index of all invested stocks',
'Momentum': 'Momentum Strategy: Buy stocks that have performed well in the past',
@@ -758,7 +758,7 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
<Line
type="linear"
dataKey="portfolio"
name="EvoTraders"
name="大时代"
stroke="#00C853"
strokeWidth={2.5}
dot={(props) => <CustomDot {...props} dataKey="portfolio" />}

View File

@@ -1,6 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { createAvatar } from "@dicebear/core";
import { lorelei } from "@dicebear/collection";
import ModelIcon from "@lobehub/icons/es/features/ModelIcon";
import { useOpenClawStore } from "../store/openclawStore";
import { useOpenClawPanel } from "../hooks/useOpenClawPanel";
@@ -27,6 +25,7 @@ const AGENT_COLORS = [
];
const OPENCLAW_EXPANDED_PANEL_MAX_HEIGHT = 420;
const OPENCLAW_AVATAR_POOL = Array.from({ length: 101 }, (_, index) => `/images/${index + 1}.png`);
function getAgentColor(agentId) {
let hash = 0;
@@ -37,6 +36,16 @@ function getAgentColor(agentId) {
return AGENT_COLORS[Math.abs(hash) % AGENT_COLORS.length].accent;
}
function getStableAvatarPath(agentId) {
const raw = String(agentId || "unknown");
let hash = 0;
for (let i = 0; i < raw.length; i++) {
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
hash |= 0;
}
return OPENCLAW_AVATAR_POOL[Math.abs(hash) % OPENCLAW_AVATAR_POOL.length];
}
function agentStateFromPresence(presence, agentId) {
const p = presence?.[agentId];
if (!p) return "idle";
@@ -50,15 +59,7 @@ function agentStateFromPresence(presence, agentId) {
function AvatarIcon({ agentId, size = 56, borderRadius = 14 }) {
const color = getAgentColor(agentId);
const avatarUri = useMemo(() => {
const seed = String(agentId || "unknown");
return createAvatar(lorelei, {
seed,
size: Math.max(64, size * 2),
backgroundColor: ["d1d4f9", "ffd5dc", "c0f0d1", "ffe7b8", "cde9ff"],
radius: 18,
}).toDataUri();
}, [agentId, size]);
const avatarPath = useMemo(() => getStableAvatarPath(agentId), [agentId]);
return (
<div style={{
@@ -75,7 +76,7 @@ function AvatarIcon({ agentId, size = 56, borderRadius = 14 }) {
overflow: "hidden",
}}>
<img
src={avatarUri}
src={avatarPath}
alt={agentId || "agent"}
style={{
width: "100%",
@@ -1041,7 +1042,7 @@ export function OpenClawStatus() {
/>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
<div style={{ fontSize: 10, color: store.chatError ? "#EF4444" : "#9CA3AF" }}>
{store.chatError || "消息将通过 EvoTraders Gateway 转发到 OpenClaw Gateway"}
{store.chatError || "消息将通过 大时代 Gateway 转发到 OpenClaw Gateway"}
</div>
<button
disabled={!selectedSession || !(chatDraftBySession[selectedSessionKey || "__none__"] || "").trim()}

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react';
import StockLogo from './StockLogo';
import { formatNumber, formatDateTime } from '../utils/formatters';
/**
@@ -497,7 +496,6 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
<tr key={h.ticker}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{h.ticker !== 'CASH' && <StockLogo ticker={h.ticker} size={18} />}
<span style={{ fontWeight: 700, color: '#000000' }}>{h.ticker}</span>
</div>
</td>
@@ -623,7 +621,6 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<StockLogo ticker={t.ticker} size={16} />
<span style={{ fontWeight: 700, color: '#000000' }}>{t.ticker}</span>
</div>
</td>

View File

@@ -1,21 +1,10 @@
import React, { useEffect, useState } from 'react';
import StockLogo from './StockLogo';
import ExplainEventsSection from './explain/ExplainEventsSection';
import ExplainMentionsSection from './explain/ExplainMentionsSection';
import ExplainMaintenanceSection from './explain/ExplainMaintenanceSection';
import ExplainNewsSection from './explain/ExplainNewsSection';
import ExplainPriceSection from './explain/ExplainPriceSection';
import ExplainRangeSection from './explain/ExplainRangeSection';
import ExplainStorySection from './explain/ExplainStorySection';
import ExplainSimilarDaysSection from './explain/ExplainSimilarDaysSection';
import ExplainSignalsSection from './explain/ExplainSignalsSection';
import ExplainSummarySection from './explain/ExplainSummarySection';
import ExplainTradesSection from './explain/ExplainTradesSection';
import ExplainInsiderSection from './explain/ExplainInsiderSection';
import ExplainTechnicalSection from './explain/ExplainTechnicalSection';
import { EVENT_CATEGORY_META, eventDateKey } from './explain/explainUtils';
import useExplainModel from './explain/useExplainModel';
import { formatDateTime, formatNumber, formatTickerPrice } from '../utils/formatters';
import { formatNumber, formatTickerPrice } from '../utils/formatters';
export default function StockExplainView({
tickers,
@@ -28,74 +17,34 @@ export default function StockExplainView({
selectedSymbol,
onSelectedSymbolChange,
selectedHistorySource,
explainEventsSnapshot,
newsSnapshot,
insiderTradesSnapshot,
technicalIndicatorsSnapshot,
onRequestRangeExplain,
onRequestHistory,
onRequestExplainEvents,
onRequestNews,
onRequestNewsForDate,
onRequestStory,
onRequestInsiderTrades,
onRequestTechnicalIndicators,
currentDate,
onRequestSimilarDays,
onRequestStockEnrich
}) {
const [selectedEventDate, setSelectedEventDate] = useState('');
const [activeEventCategory, setActiveEventCategory] = useState('all');
const [activeNewsCategory, setActiveNewsCategory] = useState('all');
const [activeNewsSentiment, setActiveNewsSentiment] = useState('all');
const [isPriceOpen, setIsPriceOpen] = useState(true);
const [isSummaryOpen, setIsSummaryOpen] = useState(true);
const [isSignalsOpen, setIsSignalsOpen] = useState(true);
const [isNewsOpen, setIsNewsOpen] = useState(true);
const [isRangeOpen, setIsRangeOpen] = useState(true);
const [isMentionsPanelOpen, setIsMentionsPanelOpen] = useState(false);
const [isEventPanelOpen, setIsEventPanelOpen] = useState(false);
const [isMaintenanceOpen, setIsMaintenanceOpen] = useState(false);
const [isStoryOpen, setIsStoryOpen] = useState(false);
const [isTradesOpen, setIsTradesOpen] = useState(false);
const [isInsiderOpen, setIsInsiderOpen] = useState(false);
const [isTechnicalOpen, setIsTechnicalOpen] = useState(true);
const [isSimilarDaysOpen, setIsSimilarDaysOpen] = useState(false);
const [enrichStartDate, setEnrichStartDate] = useState('');
const [enrichEndDate, setEnrichEndDate] = useState('');
const [forceEnrich, setForceEnrich] = useState(false);
const [onlyLocalToLlm, setOnlyLocalToLlm] = useState(false);
const [rebuildStory, setRebuildStory] = useState(true);
const [rebuildSimilarDays, setRebuildSimilarDays] = useState(true);
const {
availableSymbols,
selectedTicker,
holding,
tickerSignals,
signalSummary,
tickerTrades,
recentMentions,
tickerNews,
visibleNews,
newsCategories,
visibleNewsByCategory,
selectedNewsFreshness,
selectedRangeWindow,
selectedRangeExplain,
latestSignal,
priceColor,
exposureWeight,
recentTrade,
ohlcSeries,
priceSeries,
explainSummary,
selectedStory,
selectedSimilarDays,
explainTimeline,
availableEventDates,
eventCategoryCounts,
visibleExplainEvents,
chartModel
} = useExplainModel({
tickers,
@@ -106,10 +55,9 @@ export default function StockExplainView({
priceHistoryByTicker,
ohlcHistoryByTicker,
selectedSymbol,
explainEventsSnapshot,
newsSnapshot,
selectedEventDate,
activeEventCategory,
selectedEventDate: '',
activeEventCategory: 'all',
activeNewsCategory,
activeNewsSentiment
});
@@ -125,25 +73,10 @@ export default function StockExplainView({
}
}, [availableSymbols, onSelectedSymbolChange, selectedSymbol]);
useEffect(() => {
if (!availableEventDates.length) {
setSelectedEventDate('');
return;
}
if (!selectedEventDate || !availableEventDates.includes(selectedEventDate)) {
setSelectedEventDate(availableEventDates[0]);
}
}, [availableEventDates, selectedEventDate]);
useEffect(() => {
setActiveEventCategory('all');
}, [selectedSymbol]);
useEffect(() => {
setActiveNewsCategory('all');
setActiveNewsSentiment('all');
}, [selectedSymbol, selectedEventDate]);
}, [selectedSymbol]);
useEffect(() => {
if (!selectedSymbol) {
@@ -154,53 +87,17 @@ export default function StockExplainView({
onRequestHistory(selectedSymbol);
}
if (onRequestExplainEvents && !explainEventsSnapshot) {
onRequestExplainEvents(selectedSymbol);
}
if (onRequestNews && (!Array.isArray(newsSnapshot?.items) || newsSnapshot.items.length === 0)) {
onRequestNews(selectedSymbol);
}
}, [
explainEventsSnapshot,
newsSnapshot,
ohlcHistoryByTicker,
onRequestExplainEvents,
onRequestHistory,
onRequestNews,
selectedSymbol,
]);
useEffect(() => {
if (!selectedSymbol || !selectedEventDate || !onRequestNewsForDate) {
return;
}
if (Object.prototype.hasOwnProperty.call(newsSnapshot?.byDate || {}, selectedEventDate)) {
return;
}
onRequestNewsForDate(selectedSymbol, selectedEventDate);
}, [newsSnapshot, onRequestNewsForDate, selectedEventDate, selectedSymbol]);
useEffect(() => {
if (!selectedSymbol || !onRequestStory || !currentDate) {
return;
}
if (Object.prototype.hasOwnProperty.call(newsSnapshot?.storyCache || {}, currentDate)) {
return;
}
onRequestStory(selectedSymbol, currentDate);
}, [currentDate, newsSnapshot, onRequestStory, selectedStory, selectedSymbol]);
useEffect(() => {
if (!selectedSymbol || !selectedEventDate || !onRequestSimilarDays) {
return;
}
if (Object.prototype.hasOwnProperty.call(newsSnapshot?.similarDaysCache || {}, selectedEventDate)) {
return;
}
onRequestSimilarDays(selectedSymbol, selectedEventDate);
}, [newsSnapshot, onRequestSimilarDays, selectedEventDate, selectedSimilarDays, selectedSymbol]);
useEffect(() => {
if (!selectedSymbol || !onRequestTechnicalIndicators) {
return;
@@ -211,67 +108,6 @@ export default function StockExplainView({
onRequestTechnicalIndicators(selectedSymbol);
}, [selectedSymbol, onRequestTechnicalIndicators, technicalIndicatorsSnapshot]);
useEffect(() => {
if (!selectedRangeWindow || !selectedSymbol || !onRequestRangeExplain) {
return;
}
if (selectedRangeExplain) {
return;
}
onRequestRangeExplain(selectedSymbol, selectedRangeWindow.startDate, selectedRangeWindow.endDate, visibleNews.map((item) => item.id));
}, [onRequestRangeExplain, selectedRangeExplain, selectedRangeWindow, selectedSymbol, visibleNews]);
useEffect(() => {
const nextStartDate = selectedRangeWindow?.startDate || selectedEventDate || currentDate || '';
const nextEndDate = selectedRangeWindow?.endDate || selectedEventDate || currentDate || '';
setEnrichStartDate(nextStartDate);
setEnrichEndDate(nextEndDate);
}, [currentDate, selectedEventDate, selectedRangeWindow, selectedSymbol]);
const handleRunStockEnrich = () => {
if (!selectedSymbol || !enrichStartDate || !enrichEndDate || !onRequestStockEnrich) {
return;
}
onRequestStockEnrich(selectedSymbol, {
startDate: enrichStartDate,
endDate: enrichEndDate,
force: forceEnrich,
onlyLocalToLlm,
rebuildStory,
rebuildSimilarDays,
storyDate: currentDate || enrichEndDate,
targetDate: selectedEventDate || enrichEndDate,
});
};
const handleSelectHistory = (item) => {
if (!item || typeof item !== 'object') {
return;
}
setEnrichStartDate(item.startDate || '');
setEnrichEndDate(item.endDate || '');
setForceEnrich(Boolean(item.force));
setOnlyLocalToLlm(Boolean(item.onlyLocalToLlm));
setRebuildStory(Boolean(item.storyStatus));
setRebuildSimilarDays(Boolean(item.similarStatus));
};
const handleReplayHistory = (item) => {
if (!item || typeof item !== 'object' || !selectedSymbol || !onRequestStockEnrich) {
return;
}
onRequestStockEnrich(selectedSymbol, {
startDate: item.startDate || '',
endDate: item.endDate || '',
force: Boolean(item.force),
onlyLocalToLlm: Boolean(item.onlyLocalToLlm),
rebuildStory: Boolean(item.storyStatus),
rebuildSimilarDays: Boolean(item.similarStatus),
storyDate: currentDate || item.endDate || '',
targetDate: selectedEventDate || item.endDate || '',
});
};
return (
<div className="performance-page">
<div className="section">
@@ -285,7 +121,6 @@ export default function StockExplainView({
onClick={() => onSelectedSymbolChange?.(symbol)}
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
>
<StockLogo ticker={symbol} size={14} />
<span>{symbol}</span>
</button>
))}
@@ -326,15 +161,6 @@ export default function StockExplainView({
</div>
</div>
<div className="stat-card">
<div className="stat-card-label">最近动作</div>
<div className="stat-card-value" style={{ fontSize: 22 }}>
{recentTrade ? recentTrade.side === 'LONG' ? '做多' : recentTrade.side === 'SHORT' ? '做空' : recentTrade.side : '暂无'}
</div>
<div style={{ marginTop: 8, fontSize: 11, color: '#666666' }}>
{recentTrade ? `${formatDateTime(recentTrade.timestamp)} · ${recentTrade.qty} 股,成交价 $${Number(recentTrade.price).toFixed(2)}` : '尚无成交'}
</div>
</div>
</div>
)}
</div>
@@ -347,22 +173,10 @@ export default function StockExplainView({
selectedHistorySource={selectedHistorySource}
chartModel={chartModel}
selectedTicker={selectedTicker}
onSelectEventDate={setSelectedEventDate}
isOpen={isPriceOpen}
onToggle={() => setIsPriceOpen((prev) => !prev)}
/>
<ExplainSummarySection
explainSummary={explainSummary}
tickerSignals={tickerSignals}
recentMentions={recentMentions}
tickerTrades={tickerTrades}
tickerNews={tickerNews}
selectedSymbol={selectedSymbol}
isOpen={isSummaryOpen}
onToggle={() => setIsSummaryOpen((prev) => !prev)}
/>
<ExplainNewsSection
newsSnapshot={newsSnapshot}
visibleNewsByCategory={visibleNewsByCategory}
@@ -378,45 +192,6 @@ export default function StockExplainView({
onToggle={() => setIsNewsOpen((prev) => !prev)}
/>
<ExplainRangeSection
selectedRangeWindow={selectedRangeWindow}
selectedRangeExplain={selectedRangeExplain}
isOpen={isRangeOpen}
onToggle={() => setIsRangeOpen((prev) => !prev)}
/>
<ExplainSignalsSection
tickerSignals={tickerSignals}
signalSummary={signalSummary}
latestSignal={latestSignal}
eventDateKey={eventDateKey}
isOpen={isSignalsOpen}
onToggle={() => setIsSignalsOpen((prev) => !prev)}
/>
<ExplainStorySection
selectedStory={selectedStory}
selectedSymbol={selectedSymbol}
currentDate={currentDate}
isOpen={isStoryOpen}
onToggle={() => setIsStoryOpen((prev) => !prev)}
/>
<ExplainSimilarDaysSection
selectedSimilarDays={selectedSimilarDays}
selectedEventDate={selectedEventDate}
onSelectSimilarDate={setSelectedEventDate}
isOpen={isSimilarDaysOpen}
onToggle={() => setIsSimilarDaysOpen((prev) => !prev)}
/>
<ExplainTradesSection
tickerTrades={tickerTrades}
selectedSymbol={selectedSymbol}
isOpen={isTradesOpen}
onToggle={() => setIsTradesOpen((prev) => !prev)}
/>
<ExplainInsiderSection
insiderTrades={insiderTradesSnapshot?.trades || []}
selectedSymbol={selectedSymbol}
@@ -431,50 +206,6 @@ export default function StockExplainView({
isOpen={isTechnicalOpen}
onToggle={() => setIsTechnicalOpen((prev) => !prev)}
/>
<ExplainMentionsSection
recentMentions={recentMentions}
isOpen={isMentionsPanelOpen}
onToggle={() => setIsMentionsPanelOpen((prev) => !prev)}
/>
<ExplainEventsSection
explainTimeline={explainTimeline}
isOpen={isEventPanelOpen}
onToggle={() => setIsEventPanelOpen((prev) => !prev)}
availableEventDates={availableEventDates}
selectedEventDate={selectedEventDate}
onSelectEventDate={setSelectedEventDate}
eventCategoryCounts={eventCategoryCounts}
activeEventCategory={activeEventCategory}
onSelectEventCategory={setActiveEventCategory}
eventCategoryMeta={EVENT_CATEGORY_META}
visibleExplainEvents={visibleExplainEvents}
/>
<ExplainMaintenanceSection
selectedSymbol={selectedSymbol}
enrichStartDate={enrichStartDate}
enrichEndDate={enrichEndDate}
onChangeStartDate={setEnrichStartDate}
onChangeEndDate={setEnrichEndDate}
forceEnrich={forceEnrich}
onToggleForce={() => setForceEnrich((prev) => !prev)}
onlyLocalToLlm={onlyLocalToLlm}
onToggleOnlyLocalToLlm={() => setOnlyLocalToLlm((prev) => !prev)}
rebuildStory={rebuildStory}
onToggleRebuildStory={() => setRebuildStory((prev) => !prev)}
rebuildSimilarDays={rebuildSimilarDays}
onToggleRebuildSimilarDays={() => setRebuildSimilarDays((prev) => !prev)}
isRunning={Boolean(newsSnapshot?.maintenanceStatus?.running)}
onRunEnrich={handleRunStockEnrich}
maintenanceStatus={newsSnapshot?.maintenanceStatus || null}
maintenanceHistory={newsSnapshot?.maintenanceHistory || []}
onSelectHistory={handleSelectHistory}
onReplayHistory={handleReplayHistory}
isOpen={isMaintenanceOpen}
onToggle={() => setIsMaintenanceOpen((prev) => !prev)}
/>
</>
)}
</div>

View File

@@ -1,28 +0,0 @@
import React from 'react';
import { STOCK_LOGOS } from '../config/constants';
/**
* Stock Logo Component
* Displays company logo for a given ticker symbol
*/
export default function StockLogo({ ticker, size = 20 }) {
const logoUrl = STOCK_LOGOS[ticker];
if (!logoUrl) return null;
return (
<img
src={logoUrl}
alt={ticker}
style={{
width: size,
height: size,
borderRadius: '4px',
objectFit: 'contain',
marginRight: '8px',
verticalAlign: 'middle'
}}
onError={(e) => { e.target.style.display = 'none'; }}
/>
);
}

View File

@@ -198,9 +198,6 @@ export default function ExplainPriceSection({
图表说明{ohlcSeries.length > 1 ? '历史日线K线' : '基于盘中价格点聚合的简化K线'}
</div>
<div style={{ fontSize: 11, color: '#2563eb' }}>蓝点新闻日期</div>
<div style={{ fontSize: 11, color: '#666666' }}>黑点讨论提及</div>
<div style={{ fontSize: 11, color: '#00C853' }}>绿点偏多信号或做多成交</div>
<div style={{ fontSize: 11, color: '#FF1744' }}>红点偏空信号或做空成交</div>
</div>
</div>
)}

View File

@@ -2,8 +2,6 @@ import React from 'react';
export default function ExplainSummarySection({
explainSummary,
tickerSignals,
recentMentions,
tickerTrades,
tickerNews,
selectedSymbol,
@@ -16,7 +14,7 @@ export default function ExplainSummarySection({
<h2 className="section-title">分析摘要</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<div style={{ fontSize: 11, color: '#666666' }}>
基于当前持仓信号和讨论自动汇总
基于当前持仓成交和新闻自动汇总
</div>
<button
onClick={onToggle}
@@ -55,17 +53,9 @@ export default function ExplainSummarySection({
<div style={{ border: '1px solid #000000', background: '#ffffff', padding: 16 }}>
<div style={{ fontSize: 11, color: '#666666', marginBottom: 12, textTransform: 'uppercase', letterSpacing: 1 }}>
信号密度
分析概览
</div>
<div style={{ display: 'grid', gap: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
<span>分析师信号</span>
<strong>{tickerSignals.length}</strong>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
<span>讨论提及</span>
<strong>{recentMentions.length}</strong>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
<span>成交记录</span>
<strong>{tickerTrades.length}</strong>
@@ -76,7 +66,7 @@ export default function ExplainSummarySection({
</div>
<div style={{ height: 1, background: '#e0e0e0', margin: '4px 0' }} />
<div style={{ fontSize: 12, lineHeight: 1.7, color: '#666666' }}>
当前分析优先读取已落库的历史记录缺失时再回退到本次运行中的实时事件
当前分析综合读取信号成交新闻与已生成的解释结果
</div>
</div>
</div>

View File

@@ -1,28 +1,16 @@
import { useMemo } from 'react';
import { formatDateTime } from '../../utils/formatters';
import {
aggregatePriceSeriesToCandles,
buildLinePath,
eventDateKey,
flattenFeedMessages,
includesTicker,
normalizeMentionRow,
normalizeNewsRow,
normalizeNewsTimelineRow,
normalizeSignalDirection,
normalizeSignalRow,
normalizeTradeRow,
parsePointTime,
resolveEventCategory,
snippetText
resolveEventCategory
} from './explainUtils';
function tradeSideLabel(value) {
if (value === 'LONG') return '做多';
if (value === 'SHORT') return '做空';
return value || '交易';
}
export default function useExplainModel({
tickers,
holdings,
@@ -55,13 +43,6 @@ export default function useExplainModel({
[holdings, selectedSymbol]
);
const fallbackTrades = useMemo(
() => trades
.filter((trade) => trade.ticker === selectedSymbol)
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()),
[selectedSymbol, trades]
);
const tickerSignals = useMemo(() => {
const snapshotSignals = Array.isArray(explainEventsSnapshot?.signals)
? explainEventsSnapshot.signals.map((signal, index) => normalizeSignalRow(signal, index)).filter(Boolean)
@@ -84,45 +65,6 @@ export default function useExplainModel({
}).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}, [explainEventsSnapshot, leaderboard, selectedSymbol]);
const signalSummary = useMemo(() => {
const summary = { bullish: 0, bearish: 0, neutral: 0 };
tickerSignals.forEach((signal) => {
summary[signal.normalizedDirection] += 1;
});
return summary;
}, [tickerSignals]);
const fallbackRecentMentions = useMemo(() => {
const flattened = flattenFeedMessages(feed);
return flattened
.filter((message) => message.agent !== 'System' && includesTicker(message.content, selectedSymbol))
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 8);
}, [feed, selectedSymbol]);
const tickerTrades = useMemo(() => {
const snapshotTrades = Array.isArray(explainEventsSnapshot?.trades)
? explainEventsSnapshot.trades.map((trade, index) => normalizeTradeRow(trade, index)).filter(Boolean)
: [];
if (snapshotTrades.length > 0) {
return snapshotTrades.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
}
return fallbackTrades;
}, [explainEventsSnapshot, fallbackTrades]);
const recentMentions = useMemo(() => {
const snapshotMentions = Array.isArray(explainEventsSnapshot?.events)
? explainEventsSnapshot.events
.map((event, index) => normalizeMentionRow(event, index))
.filter(Boolean)
.slice(0, 8)
: [];
if (snapshotMentions.length > 0) {
return snapshotMentions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
}
return fallbackRecentMentions;
}, [explainEventsSnapshot, fallbackRecentMentions]);
const tickerNews = useMemo(() => {
const items = Array.isArray(newsSnapshot?.items)
? newsSnapshot.items.map((item, index) => normalizeNewsRow(item, index)).filter(Boolean)
@@ -140,16 +82,7 @@ export default function useExplainModel({
return rows.map((item, index) => normalizeNewsRow(item, index)).filter(Boolean);
}, [newsSnapshot, selectedEventDate]);
const visibleNews = useMemo(() => {
if (!selectedEventDate) {
return tickerNews;
}
if (dateScopedNews.length > 0) {
return dateScopedNews.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}
const scoped = tickerNews.filter((item) => item.dateKey === selectedEventDate);
return scoped.length > 0 ? scoped : tickerNews;
}, [dateScopedNews, selectedEventDate, tickerNews]);
const visibleNews = useMemo(() => tickerNews, [tickerNews]);
const tickerNewsTimeline = useMemo(() => {
const items = Array.isArray(newsSnapshot?.timeline)
@@ -215,28 +148,13 @@ export default function useExplainModel({
return storyCache[keys[keys.length - 1]] || null;
}, [newsSnapshot]);
const selectedSimilarDays = useMemo(() => {
if (!selectedEventDate) {
return null;
}
const similarCache = newsSnapshot?.similarDaysCache;
if (!similarCache || typeof similarCache !== 'object') {
return null;
}
return similarCache[selectedEventDate] || null;
}, [newsSnapshot, selectedEventDate]);
const selectedNewsFreshness = useMemo(
() => newsSnapshot?.freshness || newsSnapshot?.categoriesFreshness || newsSnapshot?.timelineFreshness || null,
[newsSnapshot]
);
const selectedNewsFreshness = useMemo(() => {
if (selectedEventDate && newsSnapshot?.byDateFreshness?.[selectedEventDate]) {
return newsSnapshot.byDateFreshness[selectedEventDate];
}
return newsSnapshot?.categoriesFreshness || newsSnapshot?.timelineFreshness || newsSnapshot?.freshness || null;
}, [newsSnapshot, selectedEventDate]);
const latestSignal = tickerSignals[0] || null;
const priceColor = selectedTicker?.change > 0 ? '#00C853' : selectedTicker?.change < 0 ? '#FF1744' : '#000000';
const exposureWeight = holding && Number.isFinite(Number(holding.weight)) ? Number(holding.weight) * 100 : null;
const recentTrade = tickerTrades[0] || null;
const ohlcSeries = useMemo(() => {
const raw = ohlcHistoryByTicker?.[selectedSymbol];
@@ -248,38 +166,6 @@ export default function useExplainModel({
return Array.isArray(raw) ? raw.filter((point) => Number.isFinite(Number(point.price))).slice(-60) : [];
}, [priceHistoryByTicker, selectedSymbol]);
const explainSummary = useMemo(() => {
if (!selectedSymbol) return [];
const lines = [];
if (latestSignal) {
const directionText = latestSignal.normalizedDirection === 'bullish'
? '偏多'
: latestSignal.normalizedDirection === 'bearish'
? '偏空'
: '观望';
lines.push(`最新分析师结论为${directionText},来自${latestSignal.agentName}`);
} else {
lines.push('当前还没有形成结构化分析师信号,更多依赖讨论内容和持仓状态。');
}
if (holding) {
lines.push(`组合当前持有 ${selectedSymbol},权重约 ${exposureWeight != null ? `${exposureWeight.toFixed(2)}%` : '0.00%'}`);
} else {
lines.push(`组合当前未持有 ${selectedSymbol},仍处于观察阶段。`);
}
if (recentTrade) {
lines.push(`最近一次相关交易为${tradeSideLabel(recentTrade.side)},时间是 ${formatDateTime(recentTrade.timestamp)}`);
}
if (recentMentions.length > 0) {
lines.push(`最近讨论中共有 ${recentMentions.length} 条直接提及 ${selectedSymbol} 的观点。`);
}
return lines;
}, [exposureWeight, holding, latestSignal, recentMentions.length, recentTrade, selectedSymbol]);
const explainTimeline = useMemo(() => {
const signalEvents = tickerSignals.slice(0, 12).map((signal, index) => ({
id: `signal-${signal.agentId}-${signal.date}-${index}`,
@@ -293,27 +179,7 @@ export default function useExplainModel({
tone: signal.normalizedDirection === 'bullish' ? 'positive' : signal.normalizedDirection === 'bearish' ? 'negative' : 'neutral'
}));
const mentionEvents = recentMentions.slice(0, 12).map((message, index) => ({
id: `mention-${message.feedId || message.id}-${index}`,
type: 'mention',
timestamp: message.timestamp,
title: `${message.agent || '未知角色'}${message.conferenceTitle || '讨论流'}中提及 ${selectedSymbol}`,
meta: message.conferenceTitle || (message.feedType === 'conference' ? '投资讨论' : '即时消息'),
body: snippetText(message.content, selectedSymbol),
tone: 'neutral'
}));
const tradeEvents = tickerTrades.slice(0, 12).map((trade, index) => ({
id: `trade-${trade.id || `${trade.ticker}-${trade.timestamp}-${index}`}`,
type: 'trade',
timestamp: trade.timestamp,
title: `${tradeSideLabel(trade.side)} ${trade.qty}`,
meta: '交易执行',
body: `成交价 $${Number(trade.price).toFixed(2)}`,
tone: trade.side === 'LONG' ? 'positive' : trade.side === 'SHORT' ? 'negative' : 'neutral'
}));
const fallbackTimeline = [...signalEvents, ...mentionEvents, ...tradeEvents]
const fallbackTimeline = [...signalEvents]
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 24)
.map((event) => ({
@@ -356,49 +222,7 @@ export default function useExplainModel({
})
.filter(Boolean);
const dbMentionEvents = (Array.isArray(explainEventsSnapshot.events) ? explainEventsSnapshot.events : [])
.map((event, index) => {
if (event?.type === 'mention' && event?.timestamp) {
return event;
}
const normalized = normalizeMentionRow(event, index);
if (!normalized) return null;
return {
id: normalized.id,
type: 'mention',
timestamp: normalized.timestamp,
title: `${normalized.agent || '未知角色'}${normalized.conferenceTitle || '讨论流'}中提及 ${selectedSymbol}`,
meta: normalized.conferenceTitle || (normalized.feedType === 'conference' ? '投资讨论' : '即时消息'),
body: snippetText(normalized.content, selectedSymbol),
tone: 'neutral'
};
})
.filter(Boolean);
const dbTradeEvents = (Array.isArray(explainEventsSnapshot.trades) ? explainEventsSnapshot.trades : [])
.map((trade, index) => {
if (trade?.type === 'trade' && trade?.timestamp) {
return trade;
}
const normalized = normalizeTradeRow(trade, index);
if (!normalized) return null;
return {
id: normalized.id,
type: 'trade',
timestamp: normalized.timestamp,
title: `${tradeSideLabel(normalized.side)} ${normalized.qty}`,
meta: '交易执行',
body: `成交价 $${Number(normalized.price).toFixed(2)}`,
tone: normalized.side === 'LONG' ? 'positive' : normalized.side === 'SHORT' ? 'negative' : 'neutral'
};
})
.filter(Boolean);
const dbEvents = [
...dbSignalEvents,
...dbMentionEvents,
...dbTradeEvents
]
const dbEvents = [...dbSignalEvents]
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 24)
.map((event) => ({
@@ -408,7 +232,7 @@ export default function useExplainModel({
}));
return dbEvents.length > 0 ? dbEvents : fallbackTimeline;
}, [explainEventsSnapshot, recentMentions, selectedSymbol, tickerSignals, tickerTrades]);
}, [explainEventsSnapshot, selectedSymbol, tickerSignals]);
const availableEventDates = useMemo(
() => Array.from(new Set(explainTimeline.map((event) => event.dateKey).filter(Boolean))),
@@ -644,9 +468,6 @@ export default function useExplainModel({
selectedTicker,
holding,
tickerSignals,
signalSummary,
tickerTrades,
recentMentions,
tickerNews,
visibleNews,
newsCategories,
@@ -655,14 +476,10 @@ export default function useExplainModel({
selectedRangeWindow,
selectedRangeExplain,
selectedStory,
selectedSimilarDays,
latestSignal,
priceColor,
exposureWeight,
recentTrade,
ohlcSeries,
priceSeries,
explainSummary,
explainTimeline,
availableEventDates,
eventCategoryCounts,

View File

@@ -117,7 +117,7 @@ describe('useExplainModel', () => {
const { result } = renderHook(() => useExplainModel(buildBaseProps()));
expect(result.current.availableSymbols).toEqual(['AAPL']);
expect(result.current.visibleNews).toHaveLength(1);
expect(result.current.visibleNews).toHaveLength(2);
expect(result.current.visibleNewsByCategory).toHaveLength(1);
expect(result.current.visibleNewsByCategory[0].id).toBe('news-1');
expect(result.current.selectedRangeWindow).toEqual({
@@ -127,18 +127,12 @@ describe('useExplainModel', () => {
expect(result.current.selectedRangeExplain).toEqual({
summary: '区间内主要由财报催化推动。'
});
expect(result.current.selectedSimilarDays?.items).toHaveLength(1);
});
it('builds timeline, counts, and chart markers from explain data', () => {
const { result } = renderHook(() => useExplainModel(buildBaseProps()));
expect(result.current.availableEventDates).toContain('2026-03-10');
expect(result.current.eventCategoryCounts.all).toBe(3);
expect(result.current.eventCategoryCounts.technical).toBe(1);
expect(result.current.eventCategoryCounts.discussion).toBe(1);
expect(result.current.eventCategoryCounts.trade).toBe(1);
expect(result.current.visibleExplainEvents).toHaveLength(3);
expect(result.current.chartModel.markers.length).toBeGreaterThan(0);
expect(result.current.chartModel.path).toMatch(/^M/);
});

View File

@@ -36,15 +36,6 @@ export const CDN_ASSETS = {
"Groq": "https://img.alicdn.com/imgextra/i1/O1CN01WxASMc1QjXzhVl3eQ_!!6000000002012-2-tps-170-148.png",
"Ollama": "https://img.alicdn.com/imgextra/i1/O1CN01pN615e1i4vxLkQjVd_!!6000000004360-2-tps-204-192.png",
},
stockLogos: {
"TSLA": "https://img.alicdn.com/imgextra/i4/O1CN01Pch4DD1DDrad8BQAQ_!!6000000000183-2-tps-128-128.png",
"AMZN": "https://img.alicdn.com/imgextra/i3/O1CN01KMsfnU25Wd4MGSgue_!!6000000007534-2-tps-128-128.png",
"NVDA": "https://img.alicdn.com/imgextra/i4/O1CN01Lq1eJr1mLeslgx6a0_!!6000000004938-2-tps-128-128.png",
"GOOGL": "https://img.alicdn.com/imgextra/i2/O1CN01kjJJbb25B6SESkOCn_!!6000000007487-2-tps-128-128.png",
"MSFT": "https://img.alicdn.com/imgextra/i4/O1CN01tdlNtQ1aFS7vHYfMG_!!6000000003300-2-tps-128-128.png",
"AAPL": "https://img.alicdn.com/imgextra/i4/O1CN01r0GH0q1diiHHOwxiO_!!6000000003770-2-tps-128-128.png",
"META": "https://img.alicdn.com/imgextra/i3/O1CN01pWAvHt1IkRqZoUG96_!!6000000000931-2-tps-130-96.png",
}
};
// Derived asset shortcuts
@@ -54,9 +45,6 @@ export const ASSETS = {
remeLogo: CDN_ASSETS.companyRoom.reme_logo,
};
// Stock logos mapping
export const STOCK_LOGOS = { ...CDN_ASSETS.stockLogos };
// Scene dimensions (actual image size)
export const SCENE_NATIVE = { width: 1248, height: 832 };

View File

@@ -1,7 +1,7 @@
import React from 'react';
/**
* Global CSS Styles for the EvoTraders Platform
* Global CSS Styles for the 大时代 Platform
* Terminal-inspired, minimal, monochrome design
*/
export default function GlobalStyles() {