Prefer SQLite signals in stock explain view

This commit is contained in:
2026-03-16 02:22:59 +08:00
parent 564c92c0c8
commit a41cd705b4

View File

@@ -153,6 +153,71 @@ function resolveEventCategory(event) {
return 'signal'; return 'signal';
} }
function normalizeTradeRow(row, fallbackIndex = 0) {
if (!row || typeof row !== 'object') return null;
const timestamp = row.timestamp || row.ts || row.created_at || null;
const ticker = row.ticker || '';
const side = row.side || '';
const qtyValue = Number(row.qty ?? row.quantity ?? 0);
const priceValue = Number(row.price ?? 0);
return {
id: row.id || `trade-${ticker}-${timestamp || fallbackIndex}-${fallbackIndex}`,
timestamp,
trading_date: row.trading_date || row.trade_date || null,
ticker,
side,
qty: Number.isFinite(qtyValue) ? qtyValue : 0,
price: Number.isFinite(priceValue) ? priceValue : 0
};
}
function normalizeSignalRow(row, fallbackIndex = 0) {
if (!row || typeof row !== 'object') return null;
const timestamp = row.timestamp || row.created_at || null;
const date = row.date || row.trade_date || eventDateKey(timestamp) || '';
const rawSignal = row.signal || row.title || '';
const normalizedDirection = normalizeSignalDirection(rawSignal);
const confidenceValue = Number(row.confidence);
const realReturnValue = Number(row.real_return);
const parsedCorrect = typeof row.is_correct === 'string'
? row.is_correct.toLowerCase() === 'true'
? true
: row.is_correct.toLowerCase() === 'false'
? false
: null
: typeof row.is_correct === 'boolean'
? row.is_correct
: null;
return {
id: row.id || `signal-${row.agent_id || row.agentId || 'agent'}-${date || fallbackIndex}-${fallbackIndex}`,
timestamp,
date,
ticker: row.ticker || '',
signal: rawSignal,
confidence: Number.isFinite(confidenceValue) ? confidenceValue : null,
real_return: Number.isFinite(realReturnValue) ? realReturnValue : null,
is_correct: parsedCorrect,
agentId: row.agent_id || row.agentId || '',
agentName: row.agent_name || row.agentName || row.meta || '未知分析师',
role: row.role || row.meta || '',
normalizedDirection
};
}
function normalizeMentionRow(row, fallbackIndex = 0) {
if (!row || typeof row !== 'object') return null;
return {
id: row.id || `mention-${fallbackIndex}`,
feedId: row.id || `mention-${fallbackIndex}`,
timestamp: row.timestamp || null,
agent: row.agent || row.agentName || '未知角色',
content: row.body || row.content || '',
conferenceTitle: row.meta || '',
feedType: 'sqlite'
};
}
const EVENT_CATEGORY_META = { const EVENT_CATEGORY_META = {
all: { label: '全部事件', color: '#111111' }, all: { label: '全部事件', color: '#111111' },
discussion: { label: '讨论', color: '#555555' }, discussion: { label: '讨论', color: '#555555' },
@@ -210,7 +275,7 @@ export default function StockExplainView({
[holdings, selectedSymbol] [holdings, selectedSymbol]
); );
const tickerTrades = useMemo( const fallbackTrades = useMemo(
() => trades () => trades
.filter((trade) => trade.ticker === selectedSymbol) .filter((trade) => trade.ticker === selectedSymbol)
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()), .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()),
@@ -218,6 +283,12 @@ export default function StockExplainView({
); );
const tickerSignals = useMemo(() => { const tickerSignals = useMemo(() => {
const snapshotSignals = Array.isArray(explainEventsSnapshot?.signals)
? explainEventsSnapshot.signals.map((signal, index) => normalizeSignalRow(signal, index)).filter(Boolean)
: [];
if (snapshotSignals.length > 0) {
return snapshotSignals.sort((a, b) => new Date(b.timestamp || b.date).getTime() - new Date(a.timestamp || a.date).getTime());
}
if (!selectedSymbol) return []; if (!selectedSymbol) return [];
return (Array.isArray(leaderboard) ? leaderboard : []).flatMap((agent) => { return (Array.isArray(leaderboard) ? leaderboard : []).flatMap((agent) => {
const signals = Array.isArray(agent.signals) ? agent.signals : []; const signals = Array.isArray(agent.signals) ? agent.signals : [];
@@ -231,7 +302,7 @@ export default function StockExplainView({
normalizedDirection: normalizeSignalDirection(signal.signal) normalizedDirection: normalizeSignalDirection(signal.signal)
})); }));
}).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}, [leaderboard, selectedSymbol]); }, [explainEventsSnapshot, leaderboard, selectedSymbol]);
const signalSummary = useMemo(() => { const signalSummary = useMemo(() => {
const summary = { bullish: 0, bearish: 0, neutral: 0 }; const summary = { bullish: 0, bearish: 0, neutral: 0 };
@@ -241,7 +312,7 @@ export default function StockExplainView({
return summary; return summary;
}, [tickerSignals]); }, [tickerSignals]);
const recentMentions = useMemo(() => { const fallbackRecentMentions = useMemo(() => {
const flattened = flattenFeedMessages(feed); const flattened = flattenFeedMessages(feed);
return flattened return flattened
.filter((message) => message.agent !== 'System' && includesTicker(message.content, selectedSymbol)) .filter((message) => message.agent !== 'System' && includesTicker(message.content, selectedSymbol))
@@ -249,6 +320,29 @@ export default function StockExplainView({
.slice(0, 8); .slice(0, 8);
}, [feed, selectedSymbol]); }, [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 latestSignal = tickerSignals[0] || null; const latestSignal = tickerSignals[0] || null;
const priceColor = selectedTicker?.change > 0 ? '#00C853' : selectedTicker?.change < 0 ? '#FF1744' : '#000000'; 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 exposureWeight = holding && Number.isFinite(Number(holding.weight)) ? Number(holding.weight) * 100 : null;
@@ -769,7 +863,7 @@ export default function StockExplainView({
</div> </div>
<div style={{ height: 1, background: '#e0e0e0', margin: '4px 0' }} /> <div style={{ height: 1, background: '#e0e0e0', margin: '4px 0' }} />
<div style={{ fontSize: 12, lineHeight: 1.7, color: '#666666' }}> <div style={{ fontSize: 12, lineHeight: 1.7, color: '#666666' }}>
这版解释以运行中的 agent 输出为主不依赖外部新闻库或单股历史事件数据库 这版解释优先读取当前 run SQLite 历史快照拿不到时再回退到运行中的内存态事件
</div> </div>
</div> </div>
</div> </div>
@@ -809,7 +903,7 @@ export default function StockExplainView({
: '暂无'} : '暂无'}
</div> </div>
<div style={{ marginTop: 8, fontSize: 11, color: '#666666' }}> <div style={{ marginTop: 8, fontSize: 11, color: '#666666' }}>
{latestSignal ? `${latestSignal.agentName} · ${latestSignal.date}` : '还没有历史信号'} {latestSignal ? `${latestSignal.agentName} · ${latestSignal.date || eventDateKey(latestSignal.timestamp)}` : '还没有历史信号'}
</div> </div>
</div> </div>
</div> </div>
@@ -846,8 +940,8 @@ export default function StockExplainView({
: '#666666'; : '#666666';
return ( return (
<tr key={`${signal.agentId}-${signal.date}-${index}`}> <tr key={signal.id || `${signal.agentId}-${signal.date}-${index}`}>
<td>{signal.date}</td> <td>{signal.date || eventDateKey(signal.timestamp) || '-'}</td>
<td> <td>
<div style={{ fontWeight: 700 }}>{signal.agentName}</div> <div style={{ fontWeight: 700 }}>{signal.agentName}</div>
<div style={{ fontSize: 10, color: '#666666' }}>{signal.role}</div> <div style={{ fontSize: 10, color: '#666666' }}>{signal.role}</div>