460 lines
13 KiB
HTML
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>
|