feat: initial commit - EvoTraders project

量化交易多智能体系统,包含:
- 分析师、投资组合经理、风险经理等智能体
- 股票分析、投资组合管理、风险控制工具
- React 前端界面
- FastAPI 后端服务

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-03-13 04:34:06 +08:00
commit 12de93aa30
115 changed files with 29304 additions and 0 deletions

View File

@@ -0,0 +1,517 @@
import React from 'react';
import { ASSETS } from '../config/constants';
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
/**
* Get rank medal/trophy
*/
function getRankMedal(rank) {
if (rank === 1) return { emoji: '🏆', color: '#FFD700', label: '金牌' };
if (rank === 2) return { emoji: '🥈', color: '#C0C0C0', label: '银牌' };
if (rank === 3) return { emoji: '🥉', color: '#CD7F32', label: '铜牌' };
return { emoji: `#${rank}`, color: '#333333', label: `#${rank}` };
}
/**
* Agent Performance Card Component
* Horizontal dropdown panel displayed below the agent indicator bar
*/
export default function AgentCard({ agent, onClose, isClosing }) {
if (!agent) return null;
const bullTotal = agent.bull?.n || 0;
const bullWins = agent.bull?.win || 0;
const bullUnknown = agent.bull?.unknown || 0;
const bearTotal = agent.bear?.n || 0;
const bearWins = agent.bear?.win || 0;
const bearUnknown = agent.bear?.unknown || 0;
const totalSignals = bullTotal + bearTotal;
const evaluatedBull = Math.max(bullTotal - bullUnknown, 0);
const evaluatedBear = Math.max(bearTotal - bearUnknown, 0);
const evaluatedTotal = evaluatedBull + evaluatedBear;
const bullWinRate = evaluatedBull > 0 ? (bullWins / evaluatedBull) : null;
const bearWinRate = evaluatedBear > 0 ? (bearWins / evaluatedBear) : null;
const overallWinRate = agent.winRate != null
? agent.winRate
: (evaluatedTotal > 0 ? ((bullWins + bearWins) / evaluatedTotal) : null);
const overallColor = overallWinRate != null
? (overallWinRate >= 0.5 ? '#00C853' : '#FF1744')
: '#555555';
const rankMedal = agent.rank ? getRankMedal(agent.rank) : null;
const isPortfolioManager = agent.id === 'portfolio_manager';
const isRiskManager = agent.id === 'risk_manager';
const displayName = isPortfolioManager ? '团队' : agent.name;
// Get model icon configuration
const modelInfo = getModelIcon(agent.modelName, agent.modelProvider);
const shortModelName = getShortModelName(agent.modelName);
return (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
background: '#ffffff',
borderBottom: '2px solid #000000',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
zIndex: 1000,
animation: isClosing ? 'slideUp 0.2s ease-out forwards' : 'slideDown 0.25s ease-out'
}}>
{/* Horizontal scrollable content */}
<div style={{
overflowX: 'auto',
overflowY: 'hidden',
padding: '12px',
/* Hide scrollbar for all browsers */
scrollbarWidth: 'none', /* Firefox */
msOverflowStyle: 'none', /* IE and Edge */
}}>
<style>
{`
div::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
`}
</style>
<div style={{
display: 'flex',
gap: '12px',
minWidth: 'max-content'
}}>
{/* Agent Info with Rank */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: 10,
padding: '8px 12px',
background: '#fafafa',
border: '2px solid #000000',
minWidth: 200
}}>
{isPortfolioManager ? (
<img
src={ASSETS.teamLogo}
alt="Team"
style={{
height: 50,
width: 50,
objectFit: 'contain'
}}
/>
) : agent.avatar ? (
<img
src={agent.avatar}
alt={agent.name}
style={{
height: 50,
width: 50,
objectFit: 'contain'
}}
/>
) : null}
<div>
<div style={{
fontSize: 16,
fontWeight: 700,
color: '#000000',
marginBottom: 2
}}>
{displayName}
</div>
{rankMedal && !isPortfolioManager && (
<div style={{ fontSize: 18 }}>
{rankMedal.emoji} Rank #{agent.rank}
</div>
)}
</div>
</div>
{/* Risk Manager Note */}
{isRiskManager && (
<div style={{
padding: '8px 12px',
background: '#FFF9E6',
border: '2px solid #FFA726',
minWidth: 220,
maxWidth: 280,
display: 'flex',
alignItems: 'center'
}}>
<div style={{
fontSize: 12,
color: '#E65100',
fontStyle: 'italic',
lineHeight: 1.5,
whiteSpace: 'normal',
wordWrap: 'break-word'
}}>
风控经理专注于风险管理不参与预测准确率排名
</div>
</div>
)}
{/* Portfolio Manager Note */}
{isPortfolioManager && (
<div style={{
padding: '8px 12px',
background: '#E8F5E9',
border: '2px solid #66BB6A',
minWidth: 220,
maxWidth: 280,
display: 'flex',
alignItems: 'center'
}}>
<div style={{
fontSize: 12,
color: '#2E7D32',
fontStyle: 'italic',
lineHeight: 1.5,
whiteSpace: 'normal',
wordWrap: 'break-word'
}}>
投资经理综合所有分析师建议提供团队最终交易信号不参与排名
</div>
</div>
)}
{/* Model Info Card */}
{agent.modelName && (
<div style={{
padding: '8px 12px',
background: '#ffffff',
border: `2px solid ${modelInfo.color}`,
minWidth: 140,
position: 'relative',
cursor: 'help'
}}
title={`Model: ${agent.modelName}\nProvider: ${modelInfo.provider}`}>
<div style={{
fontSize: 10,
fontWeight: 700,
color: modelInfo.color,
letterSpacing: 1,
marginBottom: 4,
textTransform: 'uppercase'
}}>
模型
</div>
<div style={{
height: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 4
}}>
{modelInfo.logoPath ? (
<img
src={modelInfo.logoPath}
alt={modelInfo.provider}
style={{
maxHeight: '100%',
maxWidth: '100%',
objectFit: 'contain'
}}
/>
) : (
<div style={{
fontSize: 28,
lineHeight: 1
}}>
🤖
</div>
)}
</div>
<div style={{
fontSize: 11,
fontWeight: 600,
color: modelInfo.color,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
{shortModelName}
</div>
<div style={{
fontSize: 8,
color: '#666666',
marginTop: 2
}}>
{modelInfo.provider}
</div>
</div>
)}
{/* Overall Win Rate */}
{!isRiskManager && !isPortfolioManager && (
<div style={{
padding: '8px 14px',
background: '#fafafa',
border: '2px solid #e0e0e0',
textAlign: 'center',
minWidth: 160
}}>
<div style={{
fontSize: 10,
color: '#333333',
fontWeight: 700,
letterSpacing: 1,
marginBottom: 4,
textTransform: 'uppercase'
}}>
胜率
</div>
<div style={{
fontSize: 36,
fontWeight: 700,
color: overallColor,
fontFamily: '"Courier New", monospace',
lineHeight: 1,
marginBottom: 2
}}>
{overallWinRate != null ? `${(overallWinRate * 100).toFixed(1)}%` : 'N/A'}
</div>
<div style={{
fontSize: 9,
color: '#555555'
}}>
{bullWins + bearWins} / {evaluatedTotal}
</div>
<div style={{
fontSize: 8,
color: '#888888',
marginTop: 4,
fontStyle: 'italic',
lineHeight: 1.2,
whiteSpace: 'pre-line'
}}>
评估: 总评估多空信号数{'\n'}胜率 = 正确信号 / 总评估信号
</div>
</div>
)}
{/* Bull Stats */}
{!isRiskManager && !isPortfolioManager && (
<div style={{
padding: '8px 12px',
background: '#F0FFF4',
border: '2px solid #00C853',
minWidth: 140
}}>
<div style={{
fontSize: 10,
fontWeight: 700,
color: '#00C853',
letterSpacing: 1,
marginBottom: 4,
textTransform: 'uppercase'
}}>
牛市胜率
</div>
<div style={{
fontSize: 28,
fontWeight: 700,
color: bullWinRate != null ? (bullWinRate >= 0.5 ? '#00C853' : '#333333') : '#555555',
marginBottom: 2,
lineHeight: 1
}}>
{bullWinRate != null ? `${(bullWinRate * 100).toFixed(1)}%` : 'N/A'}
</div>
<div style={{
fontSize: 9,
color: '#333333'
}}>
{bullWins} / {evaluatedBull}
{bullUnknown > 0 && ` / ${bullUnknown}P`}
</div>
</div>
)}
{/* Bear Stats */}
{!isRiskManager && !isPortfolioManager && (
<div style={{
padding: '8px 12px',
background: '#FFF5F5',
border: '2px solid #FF1744',
minWidth: 140
}}>
<div style={{
fontSize: 10,
fontWeight: 700,
color: '#FF1744',
letterSpacing: 1,
marginBottom: 4,
textTransform: 'uppercase'
}}>
熊市胜率
</div>
<div style={{
fontSize: 28,
fontWeight: 700,
color: bearWinRate != null ? (bearWinRate >= 0.5 ? '#00C853' : '#333333') : '#555555',
marginBottom: 2,
lineHeight: 1
}}>
{bearWinRate != null ? `${(bearWinRate * 100).toFixed(1)}%` : 'N/A'}
</div>
<div style={{
fontSize: 9,
color: '#333333'
}}>
{bearWins} / {evaluatedBear}
{bearUnknown > 0 && ` / ${bearUnknown}P`}
</div>
</div>
)}
{/* Recent Signals - Horizontal scroll */}
{agent.signals && agent.signals.length > 0 && (
<div style={{
display: 'flex',
gap: 6,
padding: '8px 12px',
background: '#fafafa',
border: '2px solid #e0e0e0'
}}>
{[...agent.signals]
.filter(signal => signal && signal.signal)
.sort((a, b) => {
// Sort by date descending (newest first)
const dateA = a.date || '';
const dateB = b.date || '';
return dateB.localeCompare(dateA);
})
.slice(0, 35)
.map((signal, idx) => {
const signalType = signal.signal.toLowerCase();
const isBull = signalType.includes('bull') || signalType === 'long';
const isBear = signalType.includes('bear') || signalType === 'short';
const isNeutral = (!isBull && !isBear) || signalType.includes('neutral') || signalType === 'hold';
const isCorrect = signal.is_correct === true;
const isUnknown = signal.is_correct === 'unknown' || signal.is_correct === null;
// Determine result symbol/text and color: unknown has priority over neutral
let resultDisplay;
let resultColor = '#555555';
let resultFontSize = 18;
if (isNeutral) {
resultDisplay = '-';
resultColor = '#555555'; // Gray for neutral
} else if (isUnknown) {
resultDisplay = '?';
resultColor = '#FFA726'; // Orange for unknown
resultFontSize = 14; // Smaller font for text
} else {
resultDisplay = isCorrect ? '✓' : '✗';
resultColor = isCorrect ? '#00C853' : '#FF1744'; // Green for correct, Red for wrong
}
return (
<div key={idx} style={{
fontSize: 9,
fontFamily: '"Courier New", monospace',
padding: '6px 8px',
background: '#ffffff',
border: '1px solid #e0e0e0',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 3,
minWidth: 70
}}>
<div style={{
fontWeight: 700,
color: isBull ? '#00C853' : isBear ? '#FF1744' : '#555555'
}}>
{signal.ticker}
</div>
<div style={{
fontSize: 16,
color: isBull ? '#00C853' : isBear ? '#FF1744' : '#555555'
}}>
{isBull ? '看涨' : isBear ? '看跌' : '中性'}
</div>
<div style={{
fontSize: 8,
color: '#555555'
}}>
{signal.date?.substring(5, 10) || 'N/A'}
</div>
<div style={{
fontSize: resultFontSize,
fontWeight: 700,
color: resultColor
}}>
{resultDisplay}
</div>
</div>
);
})}
{/* Info card explaining signal display */}
<div style={{
fontSize: 9,
fontFamily: '"Courier New", monospace',
padding: '6px 8px',
background: '#E3F2FD',
border: '1px solid #90CAF9',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
minWidth: 70,
textAlign: 'center'
}}>
<div style={{
fontSize: 10,
fontWeight: 700,
color: '#1976D2'
}}>
说明
</div>
<div style={{
fontSize: 8,
color: '#1976D2',
lineHeight: 1.2
}}>
仅显示最近5个交易日(1)的信号
</div>
</div>
</div>
)}
</div>
</div>
<style>
{`
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
`}
</style>
</div>
);
}