|
|
|
|
@ -73,6 +73,9 @@ function initEventListeners() {
|
|
|
|
|
// 刷新全部按钮
|
|
|
|
|
document.getElementById('refresh-all-btn').addEventListener('click', refreshAllSymbols);
|
|
|
|
|
|
|
|
|
|
// 全部AI分析按钮
|
|
|
|
|
document.getElementById('ai-analyze-all-btn').addEventListener('click', analyzeAllSymbols);
|
|
|
|
|
|
|
|
|
|
// 详情页刷新按钮
|
|
|
|
|
document.getElementById('refresh-symbol-btn').addEventListener('click', function() {
|
|
|
|
|
if (currentSymbol) {
|
|
|
|
|
@ -224,7 +227,10 @@ async function loadFuturesList() {
|
|
|
|
|
const response = await fetch(`${API_BASE}/list`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (data.success) {
|
|
|
|
|
allFuturesData = data.data;
|
|
|
|
|
allFuturesData = data.data.map(item => ({
|
|
|
|
|
...item,
|
|
|
|
|
hasAIAnalysis: false // 默认没有AI分析数据
|
|
|
|
|
}));
|
|
|
|
|
renderFuturesGrid(allFuturesData);
|
|
|
|
|
updateStats(allFuturesData);
|
|
|
|
|
}
|
|
|
|
|
@ -289,8 +295,10 @@ function renderFuturesGrid(data) {
|
|
|
|
|
|
|
|
|
|
grid.innerHTML = data.map(item => {
|
|
|
|
|
const isWatched = watchedSymbols.includes(item.symbol);
|
|
|
|
|
const hasAI = item.hasAIAnalysis;
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div class="futures-card" onclick="showDetailView('${item.symbol}')">
|
|
|
|
|
<div class="futures-card ${!hasAI ? 'no-ai-data' : ''}" onclick="showDetailView('${item.symbol}')">
|
|
|
|
|
<div class="card-top">
|
|
|
|
|
<div class="card-symbol">
|
|
|
|
|
<div class="symbol-tag">${item.symbol.replace(/[0-9]/g, '').substring(0, 2)}</div>
|
|
|
|
|
@ -307,7 +315,7 @@ function renderFuturesGrid(data) {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="suggestion-badge ${item.suggestionType}">${item.suggestion}</span>
|
|
|
|
|
<span class="suggestion-badge ${hasAI ? item.suggestionType : 'neutral'}" id="suggestion-${item.symbol}">${hasAI ? item.suggestion : '--'}</span>
|
|
|
|
|
<div class="card-metrics">
|
|
|
|
|
<div class="metric-item">
|
|
|
|
|
<span class="metric-label">成功率</span>
|
|
|
|
|
@ -320,21 +328,25 @@ function renderFuturesGrid(data) {
|
|
|
|
|
<span class="metric-value ${item.trendScore >= 70 ? 'up' : item.trendScore >= 50 ? '' : 'down'}">${item.trendScore}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="period-trends">
|
|
|
|
|
<span class="period-tag ${item.periods['5']}">5M</span>
|
|
|
|
|
<span class="period-tag ${item.periods['15']}">15M</span>
|
|
|
|
|
<span class="period-tag ${item.periods['30']}">30M</span>
|
|
|
|
|
<span class="period-tag ${item.periods['60']}">1H</span>
|
|
|
|
|
<div class="period-trends" id="period-trends-${item.symbol}">
|
|
|
|
|
<span class="period-tag ${hasAI ? (item.periods['5'] || 'neutral') : 'neutral'}" id="period-5-${item.symbol}">5M</span>
|
|
|
|
|
<span class="period-tag ${hasAI ? (item.periods['15'] || 'neutral') : 'neutral'}" id="period-15-${item.symbol}">15M</span>
|
|
|
|
|
<span class="period-tag ${hasAI ? (item.periods['30'] || 'neutral') : 'neutral'}" id="period-30-${item.symbol}">30M</span>
|
|
|
|
|
<span class="period-tag ${hasAI ? (item.periods['60'] || 'neutral') : 'neutral'}" id="period-60-${item.symbol}">1H</span>
|
|
|
|
|
</div>
|
|
|
|
|
${!hasAI ? `<div class="ai-hint"><i class="fas fa-info-circle"></i> 请先进行AI分析</div>` : ''}
|
|
|
|
|
<div class="card-footer">
|
|
|
|
|
<div class="key-levels">
|
|
|
|
|
<span><span class="label">压力</span> <span class="down">${formatNumber(item.resistance)}</span></span>
|
|
|
|
|
<span><span class="label">支撑</span> <span class="up">${formatNumber(item.support)}</span></span>
|
|
|
|
|
<span><span class="label">压力</span> <span class="down" id="resistance-${item.symbol}">${hasAI ? formatNumber(item.resistance) : '--'}</span></span>
|
|
|
|
|
<span><span class="label">支撑</span> <span class="up" id="support-${item.symbol}">${hasAI ? formatNumber(item.support) : '--'}</span></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
|
|
|
<button class="card-refresh-btn" onclick="event.stopPropagation(); refreshSingleSymbol('${item.symbol}', this)" title="刷新数据">
|
|
|
|
|
<i class="fas fa-sync-alt"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="card-ai-btn" onclick="event.stopPropagation(); analyzeSingleSymbol('${item.symbol}', '${item.name}', this)" title="AI分析">
|
|
|
|
|
<i class="fas fa-brain"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="watch-btn ${isWatched ? 'active' : ''}" onclick="toggleWatch('${item.symbol}', '${item.name}', event)" title="${isWatched ? '取消自选' : '加入自选'}">
|
|
|
|
|
<i class="fas fa-star"></i>
|
|
|
|
|
</button>
|
|
|
|
|
@ -1277,37 +1289,180 @@ async function refreshSingleSymbol(symbol, btnElement = null) {
|
|
|
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
|
|
|
|
|
|
|
|
showToast('info', '检查数据', `正在检查 ${symbol} 数据新鲜度...`);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 刷新K线数据
|
|
|
|
|
await loadKlineData(currentSymbol, currentPeriod);
|
|
|
|
|
showToast('success', '刷新成功', `${symbol} 数据已更新`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showToast('error', '刷新失败', error.message || '网络错误');
|
|
|
|
|
} finally {
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.innerHTML = originalContent;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function analyzeSingleSymbol(symbol, name, btnElement = null) {
|
|
|
|
|
let btn = btnElement;
|
|
|
|
|
if (!btn) {
|
|
|
|
|
try {
|
|
|
|
|
const evt = event;
|
|
|
|
|
if (evt && evt.target) {
|
|
|
|
|
btn = evt.target.closest('.card-ai-btn');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!btn) {
|
|
|
|
|
showToast('error', '分析失败', '无法找到分析按钮');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const originalContent = btn.innerHTML;
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.classList.add('analyzing');
|
|
|
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
|
|
|
|
|
|
|
|
showToast('info', 'AI分析中', `正在分析 ${symbol}...`);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/refresh/${symbol}`, { method: 'POST' });
|
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}?force_refresh=false`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
if (data.refreshed) {
|
|
|
|
|
btn.innerHTML = '<i class="fas fa-check"></i>';
|
|
|
|
|
|
|
|
|
|
await loadFuturesDetail(symbol);
|
|
|
|
|
await loadKlineData(symbol, currentPeriod);
|
|
|
|
|
await loadFuturesList();
|
|
|
|
|
if (data.success && data.data) {
|
|
|
|
|
const result = data.data.result;
|
|
|
|
|
syncAIToSymbolCard(symbol, result);
|
|
|
|
|
showToast('success', '分析完成', `${symbol} AI分析已更新`);
|
|
|
|
|
} else {
|
|
|
|
|
showToast('warning', '分析失败', data.error || 'AI分析失败');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('AI分析失败:', error);
|
|
|
|
|
showToast('error', '分析失败', '网络错误,请稍后重试');
|
|
|
|
|
} finally {
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.classList.remove('analyzing');
|
|
|
|
|
btn.innerHTML = originalContent;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function analyzeAllSymbols() {
|
|
|
|
|
const allBtn = document.getElementById('ai-analyze-all-btn');
|
|
|
|
|
if (!allBtn) return;
|
|
|
|
|
|
|
|
|
|
const originalContent = allBtn.innerHTML;
|
|
|
|
|
allBtn.disabled = true;
|
|
|
|
|
allBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 分析中...';
|
|
|
|
|
|
|
|
|
|
showToast('info', '批量分析', '开始对所有合约进行AI分析...');
|
|
|
|
|
|
|
|
|
|
const symbols = allFuturesData.map(item => item.symbol);
|
|
|
|
|
const batchSize = 3; // 每次并发分析3个合约
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < symbols.length; i += batchSize) {
|
|
|
|
|
const batch = symbols.slice(i, i + batchSize);
|
|
|
|
|
const promises = batch.map(async (symbol) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}?force_refresh=false`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
showToast('success', '数据已更新', `${symbol} 最新数据已同步`);
|
|
|
|
|
} else {
|
|
|
|
|
btn.innerHTML = '<i class="fas fa-check"></i>';
|
|
|
|
|
showToast('success', '数据新鲜', `${symbol} 数据仍在有效期内,无需刷新`);
|
|
|
|
|
if (data.success && data.data) {
|
|
|
|
|
syncAIToSymbolCard(symbol, data.data.result);
|
|
|
|
|
return { symbol, success: true };
|
|
|
|
|
} else {
|
|
|
|
|
return { symbol, success: false, error: data.error };
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`${symbol} 分析失败:`, error);
|
|
|
|
|
return { symbol, success: false, error: error.message };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.innerHTML = originalContent;
|
|
|
|
|
}, 1000);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const results = await Promise.all(promises);
|
|
|
|
|
const successCount = results.filter(r => r.success).length;
|
|
|
|
|
showToast('info', '批量分析进度', `已完成 ${Math.min(i + batchSize, symbols.length)}/${symbols.length} 个合约,本批成功 ${successCount} 个`);
|
|
|
|
|
|
|
|
|
|
// 每批之间等待2秒,避免API限流
|
|
|
|
|
if (i + batchSize < symbols.length) {
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showToast('success', '批量分析完成', `所有 ${symbols.length} 个合约AI分析已完成`);
|
|
|
|
|
allBtn.disabled = false;
|
|
|
|
|
allBtn.innerHTML = originalContent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function syncAIToSymbolCard(symbol, result) {
|
|
|
|
|
// 标记该合约已有AI分析数据
|
|
|
|
|
const item = allFuturesData.find(d => d.symbol === symbol);
|
|
|
|
|
if (item) {
|
|
|
|
|
item.hasAIAnalysis = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移除无AI数据的样式
|
|
|
|
|
const card = document.querySelector(`.futures-card[onclick="showDetailView('${symbol}')"]`);
|
|
|
|
|
if (card) {
|
|
|
|
|
card.classList.remove('no-ai-data');
|
|
|
|
|
const hint = card.querySelector('.ai-hint');
|
|
|
|
|
if (hint) hint.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const suggestion = result.trading_suggestion || {};
|
|
|
|
|
const fourDim = result.four_dimensional || {};
|
|
|
|
|
const pivotPoints = result.pivot_points || {};
|
|
|
|
|
|
|
|
|
|
// 1. 更新操作建议
|
|
|
|
|
const suggestionEl = document.getElementById(`suggestion-${symbol}`);
|
|
|
|
|
if (suggestionEl && suggestion.direction) {
|
|
|
|
|
suggestionEl.textContent = suggestion.direction;
|
|
|
|
|
suggestionEl.className = `suggestion-badge ${suggestion.direction === '做多' ? 'up' : suggestion.direction === '做空' ? 'down' : 'neutral'}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 更新压力支撑位
|
|
|
|
|
const resistanceEl = document.getElementById(`resistance-${symbol}`);
|
|
|
|
|
const supportEl = document.getElementById(`support-${symbol}`);
|
|
|
|
|
if (resistanceEl && pivotPoints.r1) resistanceEl.textContent = pivotPoints.r1;
|
|
|
|
|
if (supportEl && pivotPoints.s1) supportEl.textContent = pivotPoints.s1;
|
|
|
|
|
|
|
|
|
|
// 3. 更新多周期趋势
|
|
|
|
|
const periodNames = { '60min': '60', '30min': '30', '15min': '15', '5min': '5' };
|
|
|
|
|
Object.entries(fourDim).forEach(([period, data]) => {
|
|
|
|
|
const periodNum = periodNames[period];
|
|
|
|
|
if (!periodNum) return;
|
|
|
|
|
|
|
|
|
|
const trendEl = document.getElementById(`period-${periodNum}-${symbol}`);
|
|
|
|
|
if (trendEl) {
|
|
|
|
|
const trend = data.conclusion || data.macd?.trend || 'neutral';
|
|
|
|
|
const trendClass = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral';
|
|
|
|
|
trendEl.className = `period-tag ${trendClass}`;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshAllSymbols() {
|
|
|
|
|
const btn = document.getElementById('refresh-all-btn');
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
|
|
|
|
|
const originalContent = btn.innerHTML;
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 刷新中...';
|
|
|
|
|
|
|
|
|
|
showToast('info', '刷新中', '正在刷新所有合约数据...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/refresh-all`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
showToast('success', '刷新成功', `已刷新 ${data.count} 个合约`);
|
|
|
|
|
loadFuturesData();
|
|
|
|
|
} else {
|
|
|
|
|
showToast('error', '刷新失败', data.message || '请稍后重试');
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.innerHTML = originalContent;
|
|
|
|
|
showToast('error', '刷新失败', data.error || '未知错误');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('刷新失败:', error);
|
|
|
|
|
showToast('error', '刷新失败', '网络错误,请稍后重试');
|
|
|
|
|
} finally {
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.innerHTML = originalContent;
|
|
|
|
|
}
|
|
|
|
|
|