Align branding, prompts, and deployment tooling
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
# Frontend Environment Variables Template
|
||||
# 复制此文件为 .env 并修改配置
|
||||
|
||||
# WebSocket服务器地址
|
||||
# 本地开发
|
||||
# 控制面 API(agent/workspaces/guard)
|
||||
VITE_CONTROL_API_BASE_URL=http://localhost:8000/api
|
||||
|
||||
# 运行时 API(start/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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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' : ''}`}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user