stock-ai-agent/frontend/signals.html
2026-02-19 21:45:27 +08:00

460 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>交易信号 - Stock Agent</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: #333;
font-size: 28px;
margin-bottom: 8px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-top: 16px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 16px;
color: white;
}
.stat-card.stock {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card h3 {
font-size: 14px;
opacity: 0.9;
margin-bottom: 8px;
}
.stat-card .value {
font-size: 28px;
font-weight: bold;
}
.stat-card .sub-value {
font-size: 12px;
opacity: 0.8;
margin-top: 4px;
}
.tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.tab {
padding: 12px 24px;
background: rgba(255, 255, 255, 0.9);
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
color: #333;
transition: all 0.3s;
}
.tab.active {
background: white;
color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.tab:hover {
transform: translateY(-2px);
}
.signals-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 24px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.signal-card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
border-left: 4px solid #667eea;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
}
.signal-card:hover {
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.signal-card.buy {
border-left-color: #00c853;
}
.signal-card.sell {
border-left-color: #ff1744;
}
.signal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.signal-symbol {
font-size: 20px;
font-weight: bold;
color: #333;
}
.signal-action {
padding: 6px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
.signal-action.buy {
background: #e8f5e9;
color: #2e7d32;
}
.signal-action.sell {
background: #ffebee;
color: #c62828;
}
.signal-grade {
display: inline-block;
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
margin-left: 8px;
}
.signal-grade.A {
background: #fff3e0;
color: #e65100;
}
.signal-grade.B {
background: #e3f2fd;
color: #1565c0;
}
.signal-grade.C {
background: #f3e5f5;
color: #7b1fa2;
}
.signal-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #eee;
}
.signal-detail {
font-size: 14px;
color: #666;
}
.signal-detail strong {
color: #333;
}
.signal-reason {
margin-top: 12px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
font-size: 14px;
color: #666;
}
.signal-time {
font-size: 12px;
color: #999;
margin-top: 8px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 16px;
opacity: 0.5;
}
.loading {
text-align: center;
padding: 40px;
color: #999;
}
.confidence-bar {
height: 4px;
background: #eee;
border-radius: 2px;
margin-top: 8px;
overflow: hidden;
}
.confidence-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
}
@media (max-width: 768px) {
.header h1 {
font-size: 24px;
}
.stats {
grid-template-columns: 1fr;
}
.tabs {
flex-wrap: wrap;
}
.signal-details {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<div class="header">
<h1>🎯 交易信号中心</h1>
<div class="stats" id="stats">
<div class="stat-card">
<h3>加密货币信号</h3>
<div class="value" id="crypto-total">-</div>
<div class="sub-value" id="crypto-recent">最近24小时: -</div>
</div>
<div class="stat-card stock">
<h3>美股信号</h3>
<div class="value" id="stock-total">-</div>
<div class="sub-value" id="stock-recent">最近24小时: -</div>
</div>
<div class="stat-card">
<h3>总信号数</h3>
<div class="value" id="total-signals">-</div>
</div>
</div>
</div>
<!-- 标签页 -->
<div class="tabs">
<button class="tab active" onclick="switchTab('crypto')">加密货币</button>
<button class="tab" onclick="switchTab('stock')">美股</button>
<button class="tab" onclick="switchTab('all')">全部</button>
</div>
<!-- 信号列表 -->
<div class="signals-container">
<div id="signals-list" class="loading">
加载中...
</div>
</div>
</div>
<script>
let currentTab = 'crypto';
// 切换标签页
function switchTab(tab) {
currentTab = tab;
// 更新标签样式
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
// 加载数据
loadSignals();
}
// 加载信号数据
async function loadSignals() {
const container = document.getElementById('signals-list');
container.innerHTML = '<div class="loading">加载中...</div>';
try {
let data;
if (currentTab === 'crypto') {
const response = await fetch('/api/signals/crypto?limit=50');
data = await response.json();
} else if (currentTab === 'stock') {
const response = await fetch('/api/signals/stock?limit=50');
data = await response.json();
} else {
const response = await fetch('/api/signals/latest?limit=50');
data = await response.json();
}
if (data.success && data.signals && data.signals.length > 0) {
renderSignals(data.signals);
} else {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v6l4 2"/>
</svg>
<p>暂无信号</p>
</div>
`;
}
} catch (error) {
container.innerHTML = `<div class="empty-state"><p>加载失败: ${error.message}</p></div>`;
}
}
// 渲染信号列表
function renderSignals(signals) {
const container = document.getElementById('signals-list');
container.innerHTML = signals.map(signal => createSignalCard(signal)).join('');
}
// 创建信号卡片
function createSignalCard(signal) {
const action = signal.action || 'hold';
const grade = signal.grade || 'N';
const confidence = signal.confidence || 0;
const isBuy = action === 'buy';
const time = signal.timestamp ? new Date(signal.timestamp).toLocaleString('zh-CN') : '-';
let prices = '';
if (signal.entry_price) {
prices = `
<div class="signal-detail">
<strong>入场:</strong> $${signal.entry_price.toFixed(2)}
</div>
`;
}
if (signal.stop_loss) {
prices += `
<div class="signal-detail">
<strong>止损:</strong> $${signal.stop_loss.toFixed(2)}
</div>
`;
}
if (signal.take_profit) {
prices += `
<div class="signal-detail">
<strong>止盈:</strong> $${signal.take_profit.toFixed(2)}
</div>
`;
}
return `
<div class="signal-card ${action}">
<div class="signal-header">
<div>
<span class="signal-symbol">${signal.symbol || 'N/A'}</span>
<span class="signal-grade ${grade}">${grade}级</span>
${signal.signal_type === 'stock' ? '<span style="font-size:12px;color:#999;margin-left:8px;">美股</span>' : '<span style="font-size:12px;color:#999;margin-left:8px;">加密货币</span>'}
</div>
<span class="signal-action ${action}">${isBuy ? '🟢 做多' : '🔴 做空'}</span>
</div>
<div class="confidence-bar">
<div class="confidence-fill" style="width: ${confidence}%"></div>
</div>
<div style="margin-top:8px;font-size:14px;color:#666;">
置信度: ${confidence}%
</div>
${prices ? `<div class="signal-details">${prices}</div>` : ''}
${signal.reason ? `
<div class="signal-reason">
<strong>分析理由:</strong> ${signal.reason}
</div>
` : ''}
<div class="signal-time">${time}</div>
</div>
`;
}
// 加载统计数据
async function loadStats() {
try {
const response = await fetch('/api/signals/stats');
const data = await response.json();
if (data.success) {
document.getElementById('crypto-total').textContent = data.crypto.total;
document.getElementById('crypto-recent').textContent = `最近24小时: ${data.crypto.recent_24h}`;
document.getElementById('stock-total').textContent = data.stock.total;
document.getElementById('stock-recent').textContent = `最近24小时: ${data.stock.recent_24h}`;
document.getElementById('total-signals').textContent = data.total;
}
} catch (error) {
console.error('加载统计失败:', error);
}
}
// 初始化
loadStats();
loadSignals();
// 每30秒刷新一次
setInterval(() => {
loadStats();
loadSignals();
}, 30000);
</script>
</body>
</html>