|
|
|
|
|
const API_BASE = '/api/v1/futures';
|
|
|
|
|
|
|
|
|
|
|
|
let klineChart = null;
|
|
|
|
|
|
let currentSymbol = null;
|
|
|
|
|
|
let currentPeriod = '15';
|
|
|
|
|
|
let allFuturesData = [];
|
|
|
|
|
|
let watchedSymbols = [];
|
|
|
|
|
|
let currentDetailData = null;
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
addScoreGradient();
|
|
|
|
|
|
updateTime();
|
|
|
|
|
|
setInterval(updateTime, 1000);
|
|
|
|
|
|
|
|
|
|
|
|
initEventListeners();
|
|
|
|
|
|
loadWatchedSymbols();
|
|
|
|
|
|
loadFuturesList();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function addScoreGradient() {
|
|
|
|
|
|
const svg = document.querySelector('.score-ring svg');
|
|
|
|
|
|
if (svg && !svg.querySelector('defs')) {
|
|
|
|
|
|
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
|
|
|
|
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
|
|
|
|
|
|
gradient.setAttribute('id', 'scoreGradient');
|
|
|
|
|
|
gradient.setAttribute('x1', '0%');
|
|
|
|
|
|
gradient.setAttribute('y1', '0%');
|
|
|
|
|
|
gradient.setAttribute('x2', '100%');
|
|
|
|
|
|
gradient.setAttribute('y2', '0%');
|
|
|
|
|
|
|
|
|
|
|
|
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
|
|
|
|
|
stop1.setAttribute('offset', '0%');
|
|
|
|
|
|
stop1.setAttribute('stop-color', '#ef4444');
|
|
|
|
|
|
|
|
|
|
|
|
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
|
|
|
|
|
stop2.setAttribute('offset', '50%');
|
|
|
|
|
|
stop2.setAttribute('stop-color', '#f59e0b');
|
|
|
|
|
|
|
|
|
|
|
|
const stop3 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
|
|
|
|
|
stop3.setAttribute('offset', '100%');
|
|
|
|
|
|
stop3.setAttribute('stop-color', '#10b981');
|
|
|
|
|
|
|
|
|
|
|
|
gradient.appendChild(stop1);
|
|
|
|
|
|
gradient.appendChild(stop2);
|
|
|
|
|
|
gradient.appendChild(stop3);
|
|
|
|
|
|
defs.appendChild(gradient);
|
|
|
|
|
|
svg.insertBefore(defs, svg.firstChild);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateTime() {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const timeStr = now.getFullYear() + '-' +
|
|
|
|
|
|
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
|
|
|
|
|
String(now.getDate()).padStart(2, '0') + ' ' +
|
|
|
|
|
|
String(now.getHours()).padStart(2, '0') + ':' +
|
|
|
|
|
|
String(now.getMinutes()).padStart(2, '0') + ':' +
|
|
|
|
|
|
String(now.getSeconds()).padStart(2, '0');
|
|
|
|
|
|
document.getElementById('current-time').textContent = timeStr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function initEventListeners() {
|
|
|
|
|
|
const backBtn = document.getElementById('back-btn');
|
|
|
|
|
|
if (backBtn) backBtn.addEventListener('click', showListView);
|
|
|
|
|
|
|
|
|
|
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
|
|
|
|
if (themeToggle) themeToggle.addEventListener('click', toggleTheme);
|
|
|
|
|
|
|
|
|
|
|
|
const refreshAllBtn = document.getElementById('refresh-all-btn');
|
|
|
|
|
|
if (refreshAllBtn) refreshAllBtn.addEventListener('click', refreshAllSymbols);
|
|
|
|
|
|
|
|
|
|
|
|
const aiAnalyzeAllBtn = document.getElementById('ai-analyze-all-btn');
|
|
|
|
|
|
if (aiAnalyzeAllBtn) aiAnalyzeAllBtn.addEventListener('click', analyzeAllSymbols);
|
|
|
|
|
|
|
|
|
|
|
|
const refreshSymbolBtn = document.getElementById('refresh-symbol-btn');
|
|
|
|
|
|
if (refreshSymbolBtn) refreshSymbolBtn.addEventListener('click', function() {
|
|
|
|
|
|
if (currentSymbol) {
|
|
|
|
|
|
refreshSingleSymbol(currentSymbol);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const savedTheme = localStorage.getItem('futures-theme');
|
|
|
|
|
|
if (savedTheme === 'dark') {
|
|
|
|
|
|
document.body.classList.remove('theme-minimal');
|
|
|
|
|
|
updateThemeIcon(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.body.classList.add('theme-minimal');
|
|
|
|
|
|
updateThemeIcon(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.period-btn').forEach(btn => {
|
|
|
|
|
|
btn.addEventListener('click', function() {
|
|
|
|
|
|
document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active'));
|
|
|
|
|
|
this.classList.add('active');
|
|
|
|
|
|
currentPeriod = this.dataset.period;
|
|
|
|
|
|
loadKlineData(currentSymbol, currentPeriod);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const searchInput = document.getElementById('search-input');
|
|
|
|
|
|
if (searchInput) searchInput.addEventListener('input', function() {
|
|
|
|
|
|
filterFuturesList(this.value);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.pill').forEach(tab => {
|
|
|
|
|
|
if (tab.dataset.category) {
|
|
|
|
|
|
tab.addEventListener('click', function() {
|
|
|
|
|
|
document.querySelectorAll('.pill').forEach(t => t.classList.remove('active'));
|
|
|
|
|
|
this.classList.add('active');
|
|
|
|
|
|
filterByCategory(this.dataset.category);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const sortSelect = document.getElementById('sort-select');
|
|
|
|
|
|
if (sortSelect) sortSelect.addEventListener('change', function() {
|
|
|
|
|
|
sortFuturesList(this.value);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.modal-overlay').forEach(modal => {
|
|
|
|
|
|
modal.addEventListener('click', function(e) {
|
|
|
|
|
|
if (e.target === this) {
|
|
|
|
|
|
this.classList.remove('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeModal(modalId) {
|
|
|
|
|
|
document.getElementById(modalId).classList.remove('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showListView() {
|
|
|
|
|
|
document.getElementById('list-view').classList.add('active');
|
|
|
|
|
|
document.getElementById('detail-view').classList.remove('active');
|
|
|
|
|
|
if (klineChart) {
|
|
|
|
|
|
klineChart.dispose();
|
|
|
|
|
|
klineChart = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function showDetailView(symbol) {
|
|
|
|
|
|
currentSymbol = symbol;
|
|
|
|
|
|
document.getElementById('list-view').classList.remove('active');
|
|
|
|
|
|
document.getElementById('detail-view').classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 加载行情数据
|
|
|
|
|
|
loadFuturesDetail(symbol);
|
|
|
|
|
|
loadKlineData(symbol, currentPeriod);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 加载历史记录
|
|
|
|
|
|
await loadHistoryListForAnalysis(symbol);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadHistoryListForAnalysis(symbol) {
|
|
|
|
|
|
console.log(`[AI分析] 开始加载 ${symbol} 的历史分析记录...`);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}/history?limit=20`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[AI分析] API响应:`, data);
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
renderHistoryList(data.data);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 查找今天的最新分析记录
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
const todayStr = today.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[AI分析] 查找日期: ${todayStr}`);
|
|
|
|
|
|
console.log(`[AI分析] 历史记录数量: ${data.data?.length || 0}`);
|
|
|
|
|
|
|
|
|
|
|
|
let todayRecord = null;
|
|
|
|
|
|
if (data.data && data.data.length > 0) {
|
|
|
|
|
|
for (const record of data.data) {
|
|
|
|
|
|
const recordDate = new Date(record.analysis_time);
|
|
|
|
|
|
const recordDateStr = recordDate.toISOString().split('T')[0];
|
|
|
|
|
|
console.log(`[AI分析] 检查记录: ${record.analysis_time} -> ${recordDateStr}`);
|
|
|
|
|
|
if (recordDateStr === todayStr) {
|
|
|
|
|
|
todayRecord = record;
|
|
|
|
|
|
console.log(`[AI分析] ✓ 找到今天的记录!`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 根据是否有今天的记录进行不同处理
|
|
|
|
|
|
if (todayRecord) {
|
|
|
|
|
|
console.log(`[AI分析] 显示AI分析结果...`);
|
|
|
|
|
|
currentAIAnalysis = {
|
|
|
|
|
|
id: todayRecord.id,
|
|
|
|
|
|
symbol: todayRecord.symbol,
|
|
|
|
|
|
analysis_time: todayRecord.analysis_time,
|
|
|
|
|
|
result: todayRecord.analysis_data
|
|
|
|
|
|
};
|
|
|
|
|
|
displayAIAnalysisResult(currentAIAnalysis);
|
|
|
|
|
|
syncAIToPanels(todayRecord.analysis_data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`[AI分析] 没有找到今天的分析记录,显示占位符`);
|
|
|
|
|
|
showAIAnalysisPlaceholder();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`[AI分析] API返回失败,显示占位符`);
|
|
|
|
|
|
showAIAnalysisPlaceholder();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[AI分析] 加载历史记录失败:', error);
|
|
|
|
|
|
showAIAnalysisPlaceholder();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showAIAnalysisPlaceholder() {
|
|
|
|
|
|
console.log('[AI分析] 显示占位符...');
|
|
|
|
|
|
const content = document.getElementById('ai-analysis-content');
|
|
|
|
|
|
console.log('[AI分析] ai-analysis-content元素:', content);
|
|
|
|
|
|
if (!content) {
|
|
|
|
|
|
console.error('[AI分析] 错误: 找不到ai-analysis-content元素!');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
|
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
|
|
|
|
|
|
<p>点击"智能分析"按钮获取AI分析结果</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
console.log('[AI分析] 占位符已显示');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadWatchedSymbols() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/watched`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
watchedSymbols = data.data.map(s => s.symbol);
|
|
|
|
|
|
document.getElementById('count-watched').textContent = watchedSymbols.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载自选列表失败:', error);
|
|
|
|
|
|
watchedSymbols = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function toggleWatch(symbol, name, event) {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
const isWatched = watchedSymbols.includes(symbol);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isWatched) {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/watched/${symbol}`, { method: 'DELETE' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
watchedSymbols = watchedSymbols.filter(s => s !== symbol);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/watched`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ symbol, name })
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
watchedSymbols.push(symbol);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
document.getElementById('count-watched').textContent = watchedSymbols.length;
|
|
|
|
|
|
|
|
|
|
|
|
const activeTab = document.querySelector('.filter-tab.active');
|
|
|
|
|
|
if (activeTab && activeTab.dataset.category === 'watched') {
|
|
|
|
|
|
filterByCategory('watched');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
renderFuturesGrid(getCurrentFilteredData());
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('切换自选失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getCurrentFilteredData() {
|
|
|
|
|
|
const activeTab = document.querySelector('.filter-tab.active');
|
|
|
|
|
|
const category = activeTab ? activeTab.dataset.category : 'all';
|
|
|
|
|
|
return filterDataByCategory(allFuturesData, category);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function filterDataByCategory(data, category) {
|
|
|
|
|
|
if (category === 'all') return data;
|
|
|
|
|
|
if (category === 'watched') {
|
|
|
|
|
|
return data.filter(item => watchedSymbols.includes(item.symbol));
|
|
|
|
|
|
}
|
|
|
|
|
|
const categoryMap = {
|
|
|
|
|
|
'energy': ['SC', 'FU', 'LU', 'BU', 'RU', 'NR'],
|
|
|
|
|
|
'metal': ['AU', 'AG', 'CU', 'AL', 'ZN', 'NI', 'SN', 'PB', 'SS', 'RB', 'HC', 'I', 'J', 'JM', 'AO', 'SI', 'LC', 'PS'],
|
|
|
|
|
|
'agriculture': ['M', 'RM', 'C', 'CS', 'A', 'B', 'Y', 'P', 'OI', 'CF', 'SR', 'AP', 'LH'],
|
|
|
|
|
|
'finance': ['IF', 'IC', 'IH', 'IM', 'T', 'TF', 'TS', 'TL']
|
|
|
|
|
|
};
|
|
|
|
|
|
const symbols = categoryMap[category] || [];
|
|
|
|
|
|
return data.filter(item => {
|
|
|
|
|
|
const symbolBase = item.symbol.replace(/[0-9]/g, '').toUpperCase();
|
|
|
|
|
|
return symbols.includes(symbolBase);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadFuturesList() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('正在加载品种列表...');
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/list`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
console.log('品种列表响应:', data);
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
allFuturesData = data.data.map(item => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
hasAIAnalysis: false // 默认没有AI分析数据
|
|
|
|
|
|
}));
|
|
|
|
|
|
console.log('加载的品种数据:', allFuturesData.length, '个');
|
|
|
|
|
|
renderFuturesGrid(allFuturesData);
|
|
|
|
|
|
updateStats(allFuturesData);
|
|
|
|
|
|
|
|
|
|
|
|
// 异步加载每个合约的最新AI分析结果
|
|
|
|
|
|
loadAllAIAnalysis();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('加载品种列表失败:', data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载品种列表失败:', error);
|
|
|
|
|
|
loadFuturesFromConfig();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAllAIAnalysis() {
|
|
|
|
|
|
console.log('开始加载所有合约的AI分析结果...');
|
|
|
|
|
|
|
|
|
|
|
|
// 获取今天的日期字符串用于比较
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
const todayStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
|
|
|
|
|
|
|
|
|
|
// 分批加载,避免并发请求过多
|
|
|
|
|
|
const batchSize = 5;
|
|
|
|
|
|
for (let i = 0; i < allFuturesData.length; i += batchSize) {
|
|
|
|
|
|
const batch = allFuturesData.slice(i, i + batchSize);
|
|
|
|
|
|
const promises = batch.map(async (item) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取历史记录
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/${item.symbol}/history?limit=1`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success && data.data && data.data.length > 0) {
|
|
|
|
|
|
const latestRecord = data.data[0]; // 最新的一条记录
|
|
|
|
|
|
const analysisTime = latestRecord.analysis_time;
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否是今天的记录
|
|
|
|
|
|
const recordDate = new Date(analysisTime);
|
|
|
|
|
|
const recordDateStr = recordDate.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
|
|
|
|
if (recordDateStr === todayStr) {
|
|
|
|
|
|
// 是今天的记录,加载数据
|
|
|
|
|
|
const result = latestRecord.analysis_data;
|
|
|
|
|
|
const analysisItem = allFuturesData.find(d => d.symbol === item.symbol);
|
|
|
|
|
|
if (analysisItem) {
|
|
|
|
|
|
analysisItem.hasAIAnalysis = true;
|
|
|
|
|
|
analysisItem.aiResult = result;
|
|
|
|
|
|
analysisItem.analysisTime = analysisTime;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新操作建议
|
|
|
|
|
|
if (result.trading_suggestion?.direction) {
|
|
|
|
|
|
analysisItem.suggestion = result.trading_suggestion.direction;
|
|
|
|
|
|
analysisItem.suggestionType = result.trading_suggestion.direction === '做多' ? 'up' : result.trading_suggestion.direction === '做空' ? 'down' : 'neutral';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新压力支撑位
|
|
|
|
|
|
if (result.pivot_points) {
|
|
|
|
|
|
if (result.pivot_points.r1) analysisItem.resistance = result.pivot_points.r1;
|
|
|
|
|
|
if (result.pivot_points.s1) analysisItem.support = result.pivot_points.s1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新多周期趋势
|
|
|
|
|
|
if (result.four_dimensional) {
|
|
|
|
|
|
const periodMap = { '60min': '60', '30min': '30', '15min': '15', '5min': '5' };
|
|
|
|
|
|
analysisItem.periods = {};
|
|
|
|
|
|
Object.entries(result.four_dimensional).forEach(([period, pdata]) => {
|
|
|
|
|
|
const periodNum = periodMap[period];
|
|
|
|
|
|
if (periodNum) {
|
|
|
|
|
|
const trend = pdata.conclusion || pdata.macd?.trend || 'neutral';
|
|
|
|
|
|
analysisItem.periods[periodNum] = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新趋势评分(使用AI置信度)
|
|
|
|
|
|
if (result.trading_suggestion?.confidence) {
|
|
|
|
|
|
analysisItem.trendScore = result.trading_suggestion.confidence;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新成功率(根据判断方向设置一致概率)
|
|
|
|
|
|
const direction = analysisItem.suggestionType;
|
|
|
|
|
|
if (direction === 'up') {
|
|
|
|
|
|
analysisItem.successRate = 85; // 做多成功率
|
|
|
|
|
|
} else if (direction === 'down') {
|
|
|
|
|
|
analysisItem.successRate = 82; // 做空成功率
|
|
|
|
|
|
} else {
|
|
|
|
|
|
analysisItem.successRate = 60; // 观望成功率
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`${item.symbol} 的分析记录不是今天的 (${recordDateStr}),不加载`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`${item.symbol} 没有AI分析记录`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`加载 ${item.symbol} AI分析失败:`, error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 所有批次加载完成后只渲染一次
|
|
|
|
|
|
renderFuturesGrid(allFuturesData);
|
|
|
|
|
|
console.log('所有合约AI分析结果加载完成');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadFuturesFromConfig() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/v1/config');
|
|
|
|
|
|
const config = await response.json();
|
|
|
|
|
|
const futuresConfig = config.futures || {};
|
|
|
|
|
|
|
|
|
|
|
|
allFuturesData = Object.entries(futuresConfig).map(([name, symbol]) => ({
|
|
|
|
|
|
symbol: symbol,
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
price: 0,
|
|
|
|
|
|
change: 0,
|
|
|
|
|
|
changePct: 0,
|
|
|
|
|
|
suggestion: '等待数据',
|
|
|
|
|
|
suggestionType: 'neutral',
|
|
|
|
|
|
periods: { '5': 'neutral', '15': 'neutral', '30': 'neutral', '60': 'neutral' },
|
|
|
|
|
|
successRate: 0,
|
|
|
|
|
|
trendScore: 0,
|
|
|
|
|
|
resistance: 0,
|
|
|
|
|
|
support: 0,
|
|
|
|
|
|
open: 0,
|
|
|
|
|
|
high: 0,
|
|
|
|
|
|
low: 0,
|
|
|
|
|
|
volume: 0
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
renderFuturesGrid(allFuturesData);
|
|
|
|
|
|
updateStats(allFuturesData);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载配置失败:', error);
|
|
|
|
|
|
const mockData = generateMockFuturesData();
|
|
|
|
|
|
allFuturesData = mockData;
|
|
|
|
|
|
renderFuturesGrid(mockData);
|
|
|
|
|
|
updateStats(mockData);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function generateMockFuturesData() {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ symbol: 'SC2606', name: '原油', price: 528.6, change: 12.1, changePct: 2.35, suggestion: '逢低做多', suggestionType: 'up', periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'neutral' }, successRate: 72, trendScore: 85, resistance: 535, support: 518, open: 516.5, high: 530, low: 515, volume: 78000 },
|
|
|
|
|
|
{ symbol: 'AU2606', name: '黄金', price: 685.2, change: 12.45, changePct: 1.85, suggestion: '逢低做多', suggestionType: 'up', periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, successRate: 78, trendScore: 92, resistance: 692, support: 678, open: 672.75, high: 688, low: 670, volume: 128000 },
|
|
|
|
|
|
{ symbol: 'AG2606', name: '白银', price: 8250, change: 165, changePct: 2.04, suggestion: '逢低做多', suggestionType: 'up', periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, successRate: 75, trendScore: 88, resistance: 8350, support: 8100, open: 8085, high: 8280, low: 8050, volume: 95000 },
|
|
|
|
|
|
{ symbol: 'CU2606', name: '沪铜', price: 80610, change: 112, changePct: 0.14, suggestion: '观望等待', suggestionType: 'neutral', periods: { '5': 'neutral', '15': 'up', '30': 'neutral', '60': 'up' }, successRate: 58, trendScore: 65, resistance: 81200, support: 79800, open: 80498, high: 80850, low: 80200, volume: 42000 },
|
|
|
|
|
|
{ symbol: 'M2609', name: '豆粕', price: 2985, change: -51, changePct: -1.68, suggestion: '逢高做空', suggestionType: 'down', periods: { '5': 'down', '15': 'down', '30': 'down', '60': 'neutral' }, successRate: 65, trendScore: 35, resistance: 3050, support: 2920, open: 3036, high: 3040, low: 2980, volume: 185000 }
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderFuturesGrid(data) {
|
|
|
|
|
|
const grid = document.getElementById('futures-grid');
|
|
|
|
|
|
console.log('渲染品种网格,数据量:', data.length);
|
|
|
|
|
|
if (data.length === 0) {
|
|
|
|
|
|
grid.innerHTML = '<div class="empty-state">暂无数据</div>';
|
|
|
|
|
|
console.log('显示暂无数据');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
grid.innerHTML = data.map(item => {
|
|
|
|
|
|
const code = item.symbol.replace(/[0-9]/g, '').substring(0, 2);
|
|
|
|
|
|
const changeIcon = item.change >= 0 ? '↑' : '↓';
|
|
|
|
|
|
const changeSign = item.change >= 0 ? '+' : '';
|
|
|
|
|
|
const pctSign = item.changePct >= 0 ? '+' : '';
|
|
|
|
|
|
|
|
|
|
|
|
let actionPillClass = 'watch';
|
|
|
|
|
|
let actionPillText = '观望';
|
|
|
|
|
|
if (item.suggestion?.includes('做多') || item.suggestion?.includes('试多')) {
|
|
|
|
|
|
actionPillClass = 'do-more';
|
|
|
|
|
|
actionPillText = '做多';
|
|
|
|
|
|
} else if (item.suggestion?.includes('做空') || item.suggestion?.includes('试空')) {
|
|
|
|
|
|
actionPillClass = 'do-more';
|
|
|
|
|
|
actionPillText = '做空';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const priceClass = item.change >= 0 ? 'up' : 'down';
|
|
|
|
|
|
const changeClass = item.change >= 0 ? 'up' : 'down';
|
|
|
|
|
|
|
|
|
|
|
|
const successRate = item.successRate || 0;
|
|
|
|
|
|
const trendScore = item.trendScore || 0;
|
|
|
|
|
|
const successColor = successRate >= 70 ? 'var(--color-down)' : successRate >= 60 ? 'var(--color-neutral)' : 'var(--color-up)';
|
|
|
|
|
|
const trendColor = trendScore >= 70 ? 'var(--color-down)' : trendScore >= 50 ? 'var(--color-neutral)' : 'var(--color-up)';
|
|
|
|
|
|
|
|
|
|
|
|
const periods = item.periods || {};
|
|
|
|
|
|
const resistance = item.resistance ? formatNumber(item.resistance) : '--';
|
|
|
|
|
|
const support = item.support ? formatNumber(item.support) : '--';
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="card" onclick="showDetailView('${item.symbol}')">
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="code-box">${code}</div>
|
|
|
|
|
|
<div style="font-weight:600;">${item.name}</div>
|
|
|
|
|
|
<div style="font-size:12px; color:var(--text-tertiary);">${item.symbol}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="price-area">
|
|
|
|
|
|
<div class="price ${priceClass}">¥${formatNumber(item.price)}</div>
|
|
|
|
|
|
<div class="change ${changeClass}">${changeIcon} ${changeSign}${formatNumber(item.change)} (${pctSign}${item.changePct.toFixed(2)}%)</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="action-pill ${actionPillClass}">${actionPillText}</div>
|
|
|
|
|
|
<div class="metrics">
|
|
|
|
|
|
<div class="metric">
|
|
|
|
|
|
<div class="metric-label">成功率 ${successRate}%</div>
|
|
|
|
|
|
<div class="bar-bg"><div class="bar-fill" style="width:${successRate}%; background:${successColor};"></div></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric">
|
|
|
|
|
|
<div class="metric-label">趋势评分 ${trendScore}</div>
|
|
|
|
|
|
<div class="bar-bg"><div class="bar-fill" style="width:${trendScore}%; background:${trendColor};"></div></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="timeframes">
|
|
|
|
|
|
<div class="tf ${periods['5'] === 'up' ? 'active' : ''}">5M</div>
|
|
|
|
|
|
<div class="tf ${periods['15'] === 'up' ? 'active' : ''}">15M</div>
|
|
|
|
|
|
<div class="tf ${periods['30'] === 'up' ? 'active' : ''}">30M</div>
|
|
|
|
|
|
<div class="tf ${periods['60'] === 'up' ? 'active' : ''}">1H</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-footer">
|
|
|
|
|
|
<div class="support-resist">
|
|
|
|
|
|
<span>压力 <b class="red">${resistance}</b></span>
|
|
|
|
|
|
<span>支撑 <b class="green">${support}</b></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a href="#" class="link" onclick="event.stopPropagation(); showDetailView('${item.symbol}'); return false;">详情 →</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatNumber(num) {
|
|
|
|
|
|
if (num === 0 || num === undefined || num === null) return '--';
|
|
|
|
|
|
return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calcPriceChangePercent(current, target) {
|
|
|
|
|
|
if (!current || !target || current === 0) return '--';
|
|
|
|
|
|
const pct = ((target - current) / current * 100).toFixed(2);
|
|
|
|
|
|
return (pct >= 0 ? '+' : '') + pct + '%';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateStats(data) {
|
|
|
|
|
|
const total = data.length;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据AI分析结果统计趋势(字符串包含判断)
|
|
|
|
|
|
const upCount = data.filter(d =>
|
|
|
|
|
|
d.suggestion?.includes('做多') || d.suggestion?.includes('试多')
|
|
|
|
|
|
).length;
|
|
|
|
|
|
|
|
|
|
|
|
const downCount = data.filter(d =>
|
|
|
|
|
|
d.suggestion?.includes('做空') || d.suggestion?.includes('试空')
|
|
|
|
|
|
).length;
|
|
|
|
|
|
|
|
|
|
|
|
const neutralCount = data.filter(d =>
|
|
|
|
|
|
d.suggestion?.includes('观望')
|
|
|
|
|
|
).length;
|
|
|
|
|
|
|
|
|
|
|
|
// 安全检查:确保元素存在再设置textContent
|
|
|
|
|
|
const totalCountEl = document.getElementById('total-count');
|
|
|
|
|
|
if (totalCountEl) totalCountEl.textContent = total;
|
|
|
|
|
|
|
|
|
|
|
|
const upCountEl = document.getElementById('up-count');
|
|
|
|
|
|
if (upCountEl) upCountEl.textContent = upCount;
|
|
|
|
|
|
|
|
|
|
|
|
const downCountEl = document.getElementById('down-count');
|
|
|
|
|
|
if (downCountEl) downCountEl.textContent = downCount;
|
|
|
|
|
|
|
|
|
|
|
|
const neutralCountEl = document.getElementById('neutral-count');
|
|
|
|
|
|
if (neutralCountEl) neutralCountEl.textContent = neutralCount;
|
|
|
|
|
|
|
|
|
|
|
|
const countAllEl = document.getElementById('count-all');
|
|
|
|
|
|
if (countAllEl) countAllEl.textContent = total;
|
|
|
|
|
|
|
|
|
|
|
|
const countWatchedEl = document.getElementById('count-watched');
|
|
|
|
|
|
if (countWatchedEl) countWatchedEl.textContent = watchedSymbols.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function filterByTrend(trend) {
|
|
|
|
|
|
console.log('按趋势筛选:', trend);
|
|
|
|
|
|
let filtered = allFuturesData;
|
|
|
|
|
|
|
|
|
|
|
|
if (trend === 'up') {
|
|
|
|
|
|
filtered = allFuturesData.filter(d =>
|
|
|
|
|
|
d.suggestion?.includes('做多') || d.suggestion?.includes('试多')
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (trend === 'down') {
|
|
|
|
|
|
filtered = allFuturesData.filter(d =>
|
|
|
|
|
|
d.suggestion?.includes('做空') || d.suggestion?.includes('试空')
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (trend === 'neutral') {
|
|
|
|
|
|
filtered = allFuturesData.filter(d =>
|
|
|
|
|
|
d.suggestion?.includes('观望')
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderFuturesGrid(filtered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function filterFuturesList(keyword) {
|
|
|
|
|
|
keyword = keyword.toLowerCase();
|
|
|
|
|
|
const activeTab = document.querySelector('.filter-tab.active');
|
|
|
|
|
|
const category = activeTab ? activeTab.dataset.category : 'all';
|
|
|
|
|
|
|
|
|
|
|
|
let filtered = filterDataByCategory(allFuturesData, category);
|
|
|
|
|
|
|
|
|
|
|
|
if (keyword) {
|
|
|
|
|
|
filtered = filtered.filter(item =>
|
|
|
|
|
|
item.name.toLowerCase().includes(keyword) ||
|
|
|
|
|
|
item.symbol.toLowerCase().includes(keyword)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderFuturesGrid(filtered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function filterByCategory(category) {
|
|
|
|
|
|
let filtered = filterDataByCategory(allFuturesData, category);
|
|
|
|
|
|
renderFuturesGrid(filtered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sortFuturesList(sortBy) {
|
|
|
|
|
|
let sorted = [...getCurrentFilteredData()];
|
|
|
|
|
|
switch(sortBy) {
|
|
|
|
|
|
case 'success_rate':
|
|
|
|
|
|
sorted.sort((a, b) => b.successRate - a.successRate);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'trend_score':
|
|
|
|
|
|
sorted.sort((a, b) => b.trendScore - a.trendScore);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'change_pct':
|
|
|
|
|
|
sorted.sort((a, b) => b.changePct - a.changePct);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'name':
|
|
|
|
|
|
sorted.sort((a, b) => a.name.localeCompare(b.name, 'zh'));
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
renderFuturesGrid(sorted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadFuturesDetail(symbol) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/detail/${symbol}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
currentDetailData = data.data;
|
|
|
|
|
|
updateDetailView(data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载详情失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateDetailView(data) {
|
|
|
|
|
|
const nameEl = document.getElementById('detail-name');
|
|
|
|
|
|
if (nameEl) nameEl.textContent = data.name || '--';
|
|
|
|
|
|
|
|
|
|
|
|
const symbolEl = document.getElementById('detail-symbol');
|
|
|
|
|
|
if (symbolEl) symbolEl.textContent = data.symbol || '--';
|
|
|
|
|
|
|
|
|
|
|
|
const priceEl = document.getElementById('detail-price');
|
|
|
|
|
|
if (priceEl) {
|
|
|
|
|
|
priceEl.textContent = '¥' + formatNumber(data.price);
|
|
|
|
|
|
priceEl.className = 'price-value ' + (data.change >= 0 ? 'up' : 'down');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const changeEl = document.getElementById('detail-change');
|
|
|
|
|
|
if (changeEl) {
|
|
|
|
|
|
changeEl.className = 'price-change ' + (data.change >= 0 ? 'up' : 'down');
|
|
|
|
|
|
changeEl.innerHTML = `${data.change >= 0 ? '↑' : '↓'} ${data.change >= 0 ? '+' : ''}${formatNumber(data.change)} (${data.changePct >= 0 ? '+' : ''}${data.changePct.toFixed(2)}%)`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openEl = document.getElementById('detail-open');
|
|
|
|
|
|
if (openEl) openEl.textContent = formatNumber(data.open);
|
|
|
|
|
|
|
|
|
|
|
|
const highEl = document.getElementById('detail-high');
|
|
|
|
|
|
if (highEl) highEl.textContent = formatNumber(data.high);
|
|
|
|
|
|
|
|
|
|
|
|
const lowEl = document.getElementById('detail-low');
|
|
|
|
|
|
if (lowEl) lowEl.textContent = formatNumber(data.low);
|
|
|
|
|
|
|
|
|
|
|
|
const volumeEl = document.getElementById('detail-volume');
|
|
|
|
|
|
if (volumeEl) volumeEl.textContent = formatNumber(data.volume);
|
|
|
|
|
|
|
|
|
|
|
|
// 注意: detail-r1 和 detail-s1 在新HTML中不存在,已移除
|
|
|
|
|
|
|
|
|
|
|
|
// 注意: suggestion-badge, suggestion-reason, entry-price, target-price, stop-loss, risk-level 在新HTML中不存在,已移除
|
|
|
|
|
|
|
|
|
|
|
|
if (data.macd) {
|
|
|
|
|
|
const macdSignalEl = document.getElementById('macd-signal');
|
|
|
|
|
|
if (macdSignalEl) macdSignalEl.textContent = data.macd.signal;
|
|
|
|
|
|
const macdDetailEl = document.getElementById('macd-detail');
|
|
|
|
|
|
if (macdDetailEl) macdDetailEl.textContent = data.macd.detail;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.rsi) {
|
|
|
|
|
|
const rsiValueEl = document.getElementById('rsi-value');
|
|
|
|
|
|
if (rsiValueEl) rsiValueEl.textContent = data.rsi.value;
|
|
|
|
|
|
const rsiStatusEl = document.getElementById('rsi-status');
|
|
|
|
|
|
if (rsiStatusEl) rsiStatusEl.textContent = data.rsi.status;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.boll) {
|
|
|
|
|
|
const bollSignalEl = document.getElementById('boll-signal');
|
|
|
|
|
|
if (bollSignalEl) bollSignalEl.textContent = data.boll.signal;
|
|
|
|
|
|
const bollDetailEl = document.getElementById('boll-detail');
|
|
|
|
|
|
if (bollDetailEl) bollDetailEl.textContent = data.boll.detail;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.kdj) {
|
|
|
|
|
|
const kdjSignalEl = document.getElementById('kdj-signal');
|
|
|
|
|
|
if (kdjSignalEl) kdjSignalEl.textContent = data.kdj.signal;
|
|
|
|
|
|
const kdjDetailEl = document.getElementById('kdj-detail');
|
|
|
|
|
|
if (kdjDetailEl) kdjDetailEl.textContent = data.kdj.detail;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.resistances) {
|
|
|
|
|
|
for (let i = 0; i < 2; i++) {
|
|
|
|
|
|
const el = document.getElementById(`resistance-${i + 1}`);
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
el.textContent = formatNumber(data.resistances[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.supports) {
|
|
|
|
|
|
for (let i = 0; i < 2; i++) {
|
|
|
|
|
|
const el = document.getElementById(`support-${i + 1}`);
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
el.textContent = formatNumber(data.supports[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.pivotPoint) {
|
|
|
|
|
|
const ppEl = document.getElementById('pivot-point');
|
|
|
|
|
|
if (ppEl) ppEl.textContent = formatNumber(data.pivotPoint);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.periodConsistency) {
|
|
|
|
|
|
const container = document.getElementById('period-trends');
|
|
|
|
|
|
if (container) {
|
|
|
|
|
|
const periodNames = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' };
|
|
|
|
|
|
container.innerHTML = Object.entries(data.periodConsistency).map(([period, trend]) => `
|
|
|
|
|
|
<div class="trend-row">
|
|
|
|
|
|
<span class="trend-period">${periodNames[period]}</span>
|
|
|
|
|
|
<span class="trend-badge ${trend}">
|
|
|
|
|
|
${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 注意: trend-score 和 score-fill 在新HTML中不存在,已移除
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadHistoryList(symbol) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}/history?limit=20`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
renderHistoryList(data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载历史记录失败:', error);
|
|
|
|
|
|
document.getElementById('history-list').innerHTML = '<div class="empty-state">暂无历史记录</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAIAnalysis() {
|
|
|
|
|
|
if (!currentSymbol) return;
|
|
|
|
|
|
|
|
|
|
|
|
const content = document.getElementById('ai-analysis-content');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log(`加载合约 ${currentSymbol} 的AI分析...`);
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`合约 ${currentSymbol} AI分析响应:`, data);
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success && data.data) {
|
|
|
|
|
|
console.log(`合约 ${currentSymbol} 分析数据 - symbol:`, data.data.symbol);
|
|
|
|
|
|
currentAIAnalysis = data.data;
|
|
|
|
|
|
displayAIAnalysisResult(data.data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`合约 ${currentSymbol} 无分析结果`);
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
|
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
|
|
|
|
|
|
<p>点击"智能分析"按钮获取AI分析结果</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`加载合约 ${currentSymbol} AI分析失败:`, error);
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
|
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
|
|
|
|
|
|
<p>点击"智能分析"按钮获取AI分析结果</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderHistoryList(records) {
|
|
|
|
|
|
const container = document.getElementById('history-list');
|
|
|
|
|
|
if (!records || records.length === 0) {
|
|
|
|
|
|
container.innerHTML = '<div class="empty-state">暂无历史记录</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('渲染历史记录,记录数量:', records.length);
|
|
|
|
|
|
console.log('历史记录合约分布:', records.map(r => r.symbol));
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = records.map(record => {
|
|
|
|
|
|
const analysisData = record.analysis_data || {};
|
|
|
|
|
|
const suggestion = analysisData.trading_suggestion || {};
|
|
|
|
|
|
const timeStr = record.analysis_time ? record.analysis_time.replace('T', ' ').substring(0, 16) : '--';
|
|
|
|
|
|
const summary = analysisData.summary || '--';
|
|
|
|
|
|
const direction = suggestion.direction || '--';
|
|
|
|
|
|
const confidence = suggestion.confidence || 0;
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`历史记录 ID:${record.id} 合约:${record.symbol}`);
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="history-item" onclick="showAIHistoryDetail(${record.id})">
|
|
|
|
|
|
<div class="history-item-left">
|
|
|
|
|
|
<span class="history-time">${timeStr}</span>
|
|
|
|
|
|
<span class="history-suggestion">${summary.substring(0, 30)}${summary.length > 30 ? '...' : ''}</span>
|
|
|
|
|
|
<span class="history-score">方向: ${direction} | 置信度: ${confidence}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="history-item-right">
|
|
|
|
|
|
<button class="history-detail-btn" onclick="event.stopPropagation(); showAIHistoryDetail(${record.id})">
|
|
|
|
|
|
<i class="fas fa-chevron-right"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showSuggestionModal(data) {
|
|
|
|
|
|
const body = document.getElementById('suggestion-modal-body');
|
|
|
|
|
|
body.innerHTML = `
|
|
|
|
|
|
<div class="modal-suggestion-main">
|
|
|
|
|
|
<div class="suggestion-badge ${data.suggestionType || 'neutral'}" style="font-size:24px;font-weight:700;">${data.suggestion || '--'}</div>
|
|
|
|
|
|
<div class="suggestion-reason" style="margin-top:8px;">${data.suggestionReason || '--'}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-params-grid">
|
|
|
|
|
|
<div class="modal-param-card">
|
|
|
|
|
|
<span class="param-label">建议入场</span>
|
|
|
|
|
|
<span class="param-value">${formatNumber(data.entryPrice)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-param-card">
|
|
|
|
|
|
<span class="param-label">目标价位</span>
|
|
|
|
|
|
<span class="param-value up">${formatNumber(data.targetPrice)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-param-card">
|
|
|
|
|
|
<span class="param-label">止损价位</span>
|
|
|
|
|
|
<span class="param-value down">${formatNumber(data.stopLoss)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-param-card">
|
|
|
|
|
|
<span class="param-label">风险等级</span>
|
|
|
|
|
|
<span class="param-value">${data.riskLevel || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.getElementById('suggestion-modal').classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showHistoryModal(record) {
|
|
|
|
|
|
const body = document.getElementById('history-modal-body');
|
|
|
|
|
|
body.innerHTML = `
|
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
|
<div class="modal-section-title"><i class="fas fa-robot"></i> AI交易建议</div>
|
|
|
|
|
|
<div class="modal-suggestion-main" style="margin-bottom:0;">
|
|
|
|
|
|
<div class="suggestion-badge ${record.suggestion_type || 'neutral'}" style="font-size:20px;font-weight:700;">${record.suggestion || '--'}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-params-grid" style="margin-top:12px;">
|
|
|
|
|
|
<div class="modal-param-card">
|
|
|
|
|
|
<span class="param-label">入场</span>
|
|
|
|
|
|
<span class="param-value">${formatNumber(record.entry_price)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-param-card">
|
|
|
|
|
|
<span class="param-label">目标</span>
|
|
|
|
|
|
<span class="param-value up">${formatNumber(record.target_price)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-param-card">
|
|
|
|
|
|
<span class="param-label">止损</span>
|
|
|
|
|
|
<span class="param-value down">${formatNumber(record.stop_loss)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-param-card">
|
|
|
|
|
|
<span class="param-label">风险</span>
|
|
|
|
|
|
<span class="param-value">${record.risk_level || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
|
<div class="modal-section-title"><i class="fas fa-wave-pulse"></i> 技术指标</div>
|
|
|
|
|
|
<div class="modal-indicators-grid">
|
|
|
|
|
|
<div class="modal-indicator-item">
|
|
|
|
|
|
<span class="indicator-label">MACD</span>
|
|
|
|
|
|
<span class="indicator-value">${record.macd_signal || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-indicator-item">
|
|
|
|
|
|
<span class="indicator-label">RSI</span>
|
|
|
|
|
|
<span class="indicator-value">${record.rsi_value || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-indicator-item">
|
|
|
|
|
|
<span class="indicator-label">BOLL</span>
|
|
|
|
|
|
<span class="indicator-value">${record.boll_signal || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-indicator-item">
|
|
|
|
|
|
<span class="indicator-label">KDJ</span>
|
|
|
|
|
|
<span class="indicator-value">${record.kdj_signal || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
|
<div class="modal-section-title"><i class="fas fa-crosshairs"></i> 关键点位</div>
|
|
|
|
|
|
<div class="modal-levels-list">
|
|
|
|
|
|
${(record.resistance_levels || []).map((v, i) => `
|
|
|
|
|
|
<div class="modal-level-row">
|
|
|
|
|
|
<span class="level-label">压力${i + 1}</span>
|
|
|
|
|
|
<span class="level-value down">${formatNumber(v)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
${record.pivot_point ? `
|
|
|
|
|
|
<div class="modal-level-row" style="background:rgba(139,92,246,0.1);border-radius:8px;">
|
|
|
|
|
|
<span class="level-label" style="color:#8b5cf6;font-weight:600;">中枢 (PP)</span>
|
|
|
|
|
|
<span class="level-value" style="color:#8b5cf6;">${formatNumber(record.pivot_point)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
${(record.support_levels || []).map((v, i) => `
|
|
|
|
|
|
<div class="modal-level-row">
|
|
|
|
|
|
<span class="level-label">支撑${i + 1}</span>
|
|
|
|
|
|
<span class="level-value up">${formatNumber(v)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
|
<div class="modal-section-title"><i class="fas fa-timeline"></i> 多周期趋势</div>
|
|
|
|
|
|
<div class="modal-trends-list">
|
|
|
|
|
|
${Object.entries(record.period_trends || {}).map(([period, trend]) => {
|
|
|
|
|
|
const names = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' };
|
|
|
|
|
|
return `<div class="modal-trend-row">
|
|
|
|
|
|
<span class="modal-trend-period">${names[period] || period}</span>
|
|
|
|
|
|
<span class="modal-trend-badge ${trend}">${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}</span>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
|
<div class="modal-section-title"><i class="fas fa-gauge-high"></i> 趋势评分</div>
|
|
|
|
|
|
<div class="modal-param-card" style="text-align:center;">
|
|
|
|
|
|
<span class="param-value" style="font-size:32px;">${record.trend_score || '--'}</span>
|
|
|
|
|
|
<span class="param-label" style="display:block;margin-top:4px;">综合评分</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.getElementById('history-modal').classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadKlineData(symbol, period) {
|
|
|
|
|
|
if (!symbol) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/kline/${symbol}?period=${period}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success && data.data) {
|
|
|
|
|
|
renderKlineChart(data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载K线数据失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function showAIHistoryDetail(recordId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/history/${recordId}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success && data.data) {
|
|
|
|
|
|
const record = data.data;
|
|
|
|
|
|
const result = record.analysis_data;
|
|
|
|
|
|
const timestamp = new Date(record.analysis_time).toLocaleString('zh-CN');
|
|
|
|
|
|
|
|
|
|
|
|
// 构建弹窗内容
|
|
|
|
|
|
const modalBody = document.getElementById('ai-analysis-modal-body');
|
|
|
|
|
|
|
|
|
|
|
|
const direction = result.trading_suggestion?.direction || '观望';
|
|
|
|
|
|
const directionClass = direction === '做多' ? 'long' : direction === '做空' ? 'short' : 'neutral';
|
|
|
|
|
|
const directionIcon = direction === '做多' ? 'fa-arrow-up' : direction === '做空' ? 'fa-arrow-down' : 'fa-arrows-left-right';
|
|
|
|
|
|
const confidence = result.trading_suggestion?.confidence || 0;
|
|
|
|
|
|
|
|
|
|
|
|
modalBody.innerHTML = `
|
|
|
|
|
|
<div class="ai-history-detail">
|
|
|
|
|
|
<div class="detail-header">
|
|
|
|
|
|
<h4><i class="fas fa-file-alt"></i> ${record.symbol} AI分析报告</h4>
|
|
|
|
|
|
<span class="detail-time"><i class="fas fa-clock"></i> ${timestamp}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="detail-summary">
|
|
|
|
|
|
<i class="fas fa-quote-left"></i>
|
|
|
|
|
|
<p>${result.summary || '暂无总结'}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- AI交易建议卡片已隐藏 -->
|
|
|
|
|
|
<!-- <div class="detail-suggestion">
|
|
|
|
|
|
<div class="suggestion-card ${directionClass}">
|
|
|
|
|
|
<i class="fas ${directionIcon}"></i>
|
|
|
|
|
|
<span class="suggestion-text">${direction}</span>
|
|
|
|
|
|
<span class="confidence-text">置信度: ${confidence}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div> -->
|
|
|
|
|
|
|
|
|
|
|
|
${result.four_dimensional ? `
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<h5><i class="fas fa-brain"></i> AI思维分析</h5>
|
|
|
|
|
|
<table class="four-d-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>周期</th>
|
|
|
|
|
|
<th>MACD趋势</th>
|
|
|
|
|
|
<th>成交量</th>
|
|
|
|
|
|
<th>KDJ状态</th>
|
|
|
|
|
|
<th>结论</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
${(() => {
|
|
|
|
|
|
const periodNames = { '60min': '60分钟', '30min': '30分钟', '15min': '15分钟', '5min': '5分钟' };
|
|
|
|
|
|
const periodOrder = ['60min', '30min', '15min', '5min'];
|
|
|
|
|
|
const sortedEntries = Object.entries(result.four_dimensional).sort((a, b) => {
|
|
|
|
|
|
return periodOrder.indexOf(a[0]) - periodOrder.indexOf(b[0]);
|
|
|
|
|
|
});
|
|
|
|
|
|
return sortedEntries.map(([period, d]) => `
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td><strong>${periodNames[period] || period}</strong></td>
|
|
|
|
|
|
<td>${d.macd?.trend || '--'}</td>
|
|
|
|
|
|
<td>${d.volume?.status || '--'}</td>
|
|
|
|
|
|
<td>${d.kdj?.status || '--'}</td>
|
|
|
|
|
|
<td>${d.conclusion || '--'}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
})()}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
|
|
<div class="detail-metrics">
|
|
|
|
|
|
<div class="metric-card">
|
|
|
|
|
|
<span class="metric-label">入场区间</span>
|
|
|
|
|
|
<span class="metric-value">${result.trading_suggestion?.entry_range?.min || '--'}-${result.trading_suggestion?.entry_range?.max || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-card">
|
|
|
|
|
|
<span class="metric-label">止损位</span>
|
|
|
|
|
|
<span class="metric-value down">${result.trading_suggestion?.stop_loss || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-card">
|
|
|
|
|
|
<span class="metric-label">建议仓位</span>
|
|
|
|
|
|
<span class="metric-value">${result.trading_suggestion?.position_size || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-card">
|
|
|
|
|
|
<span class="metric-label">纪律评分</span>
|
|
|
|
|
|
<span class="metric-value">${result.discipline_score?.total || '--'}/${result.discipline_score?.max || '11'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
${result.kdj_diagnosis ? `
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<h5><i class="fas fa-stethoscope"></i> KDJ诊断</h5>
|
|
|
|
|
|
<div class="kdj-diagnosis-grid">
|
|
|
|
|
|
<div class="kdj-item">
|
|
|
|
|
|
<span class="kdj-label">当前状态</span>
|
|
|
|
|
|
<span class="kdj-value">${result.kdj_diagnosis.current_status || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kdj-item">
|
|
|
|
|
|
<span class="kdj-label">背离</span>
|
|
|
|
|
|
<span class="kdj-value">${result.kdj_diagnosis.divergence || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kdj-item">
|
|
|
|
|
|
<span class="kdj-label">钝化</span>
|
|
|
|
|
|
<span class="kdj-value">${result.kdj_diagnosis.paralysis || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kdj-item" style="grid-column: 1 / -1;">
|
|
|
|
|
|
<span class="kdj-label">建议</span>
|
|
|
|
|
|
<span class="kdj-value">${result.kdj_diagnosis.recommendation || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
|
|
${result.pivot_points ? `
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<h5><i class="fas fa-crosshairs"></i> 关键点位</h5>
|
|
|
|
|
|
<div class="pivot-points-grid">
|
|
|
|
|
|
<div class="pivot-item resistance">
|
|
|
|
|
|
<span>R2</span><strong>${result.pivot_points.r2 || '--'}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pivot-item resistance">
|
|
|
|
|
|
<span>R1</span><strong>${result.pivot_points.r1 || '--'}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pivot-item center">
|
|
|
|
|
|
<span>PP</span><strong>${result.pivot_points.pp || '--'}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pivot-item support">
|
|
|
|
|
|
<span>S1</span><strong>${result.pivot_points.s1 || '--'}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pivot-item support">
|
|
|
|
|
|
<span>S2</span><strong>${result.pivot_points.s2 || '--'}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
|
|
${result.risk_warnings && result.risk_warnings.length > 0 ? `
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<h5><i class="fas fa-exclamation-triangle"></i> 风险提示</h5>
|
|
|
|
|
|
<ul class="warning-list">
|
|
|
|
|
|
${result.risk_warnings.map(w => `<li>${w}</li>`).join('')}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 显示弹窗
|
|
|
|
|
|
document.getElementById('ai-analysis-modal').classList.add('active');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast('error', '加载失败', data.error || '记录不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载历史记录详情失败:', error);
|
|
|
|
|
|
showToast('error', '加载失败', '网络错误,请稍后重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderKlineChart(data) {
|
|
|
|
|
|
if (klineChart) {
|
|
|
|
|
|
klineChart.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const chartDom = document.getElementById('kline-chart');
|
|
|
|
|
|
klineChart = echarts.init(chartDom, 'dark');
|
|
|
|
|
|
|
|
|
|
|
|
const dates = data.map(d => d[0]);
|
|
|
|
|
|
const values = data.map(d => [parseFloat(d[1]), parseFloat(d[2]), parseFloat(d[3]), parseFloat(d[4])]);
|
|
|
|
|
|
const volumes = data.map(d => [parseInt(d[5]), d[2] >= d[1] ? 1 : -1]);
|
|
|
|
|
|
|
|
|
|
|
|
const ma5 = calculateMA(data, 5);
|
|
|
|
|
|
const ma10 = calculateMA(data, 10);
|
|
|
|
|
|
const ma20 = calculateMA(data, 20);
|
|
|
|
|
|
const macdData = calculateMACD(data);
|
|
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
backgroundColor: 'transparent',
|
|
|
|
|
|
animation: false,
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
|
axisPointer: { type: 'cross' },
|
|
|
|
|
|
backgroundColor: 'rgba(10, 15, 25, 0.95)',
|
|
|
|
|
|
borderColor: 'rgba(56, 189, 248, 0.2)',
|
|
|
|
|
|
textStyle: { color: '#e2e8f0', fontSize: 12 }
|
|
|
|
|
|
},
|
|
|
|
|
|
axisPointer: {
|
|
|
|
|
|
link: [{ xAxisIndex: 'all' }],
|
|
|
|
|
|
label: { backgroundColor: '#06b6d4' }
|
|
|
|
|
|
},
|
|
|
|
|
|
grid: [
|
|
|
|
|
|
{ left: 70, right: 20, top: 10, height: '50%' },
|
|
|
|
|
|
{ left: 70, right: 20, top: '56%', height: '16%' },
|
|
|
|
|
|
{ left: 70, right: 20, top: '76%', height: '16%' }
|
|
|
|
|
|
],
|
|
|
|
|
|
xAxis: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'category',
|
|
|
|
|
|
data: dates,
|
|
|
|
|
|
boundaryGap: true,
|
|
|
|
|
|
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
|
|
|
|
|
axisLabel: { color: '#64748b', fontSize: 10 },
|
|
|
|
|
|
splitLine: { show: false }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'category',
|
|
|
|
|
|
gridIndex: 1,
|
|
|
|
|
|
data: dates,
|
|
|
|
|
|
axisLine: { show: false },
|
|
|
|
|
|
axisLabel: { show: false },
|
|
|
|
|
|
splitLine: { show: false }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'category',
|
|
|
|
|
|
gridIndex: 2,
|
|
|
|
|
|
data: dates,
|
|
|
|
|
|
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
|
|
|
|
|
axisLabel: { color: '#64748b', fontSize: 10 },
|
|
|
|
|
|
splitLine: { show: false }
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
yAxis: [
|
|
|
|
|
|
{
|
|
|
|
|
|
scale: true,
|
|
|
|
|
|
axisLine: { show: false },
|
|
|
|
|
|
axisLabel: { color: '#64748b' },
|
|
|
|
|
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)', type: 'dashed' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
scale: true,
|
|
|
|
|
|
gridIndex: 1,
|
|
|
|
|
|
axisLine: { show: false },
|
|
|
|
|
|
axisLabel: { show: false },
|
|
|
|
|
|
splitLine: { show: false }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
scale: true,
|
|
|
|
|
|
gridIndex: 2,
|
|
|
|
|
|
axisLine: { show: false },
|
|
|
|
|
|
axisLabel: { color: '#64748b', fontSize: 10 },
|
|
|
|
|
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)', type: 'dashed' } }
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
dataZoom: [
|
|
|
|
|
|
{ type: 'inside', xAxisIndex: [0, 1, 2], start: 50, end: 100 },
|
|
|
|
|
|
{
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
xAxisIndex: [0, 1, 2],
|
|
|
|
|
|
type: 'slider',
|
|
|
|
|
|
bottom: 5,
|
|
|
|
|
|
height: 16,
|
|
|
|
|
|
borderColor: 'transparent',
|
|
|
|
|
|
backgroundColor: 'rgba(15, 20, 30, 0.5)',
|
|
|
|
|
|
fillerColor: 'rgba(6, 182, 212, 0.15)',
|
|
|
|
|
|
handleStyle: { color: '#06b6d4' },
|
|
|
|
|
|
textStyle: { color: '#64748b' }
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'K线',
|
|
|
|
|
|
type: 'candlestick',
|
|
|
|
|
|
data: values,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: '#10b981',
|
|
|
|
|
|
color0: '#ef4444',
|
|
|
|
|
|
borderColor: '#10b981',
|
|
|
|
|
|
borderColor0: '#ef4444'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'MA5',
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: ma5,
|
|
|
|
|
|
lineStyle: { width: 1, color: '#f59e0b' },
|
|
|
|
|
|
symbol: 'none'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'MA10',
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: ma10,
|
|
|
|
|
|
lineStyle: { width: 1, color: '#3b82f6' },
|
|
|
|
|
|
symbol: 'none'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'MA20',
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: ma20,
|
|
|
|
|
|
lineStyle: { width: 1, color: '#8b5cf6' },
|
|
|
|
|
|
symbol: 'none'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '成交量',
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
xAxisIndex: 1,
|
|
|
|
|
|
yAxisIndex: 1,
|
|
|
|
|
|
data: volumes.map(v => ({
|
|
|
|
|
|
value: v[0],
|
|
|
|
|
|
itemStyle: { color: v[1] >= 0 ? 'rgba(16,185,129,0.5)' : 'rgba(239,68,68,0.5)' }
|
|
|
|
|
|
}))
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'DIF',
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
xAxisIndex: 2,
|
|
|
|
|
|
yAxisIndex: 2,
|
|
|
|
|
|
data: macdData.dif,
|
|
|
|
|
|
lineStyle: { width: 1.5, color: '#3b82f6' },
|
|
|
|
|
|
symbol: 'none'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'DEA',
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
xAxisIndex: 2,
|
|
|
|
|
|
yAxisIndex: 2,
|
|
|
|
|
|
data: macdData.dea,
|
|
|
|
|
|
lineStyle: { width: 1.5, color: '#f59e0b' },
|
|
|
|
|
|
symbol: 'none'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'MACD',
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
xAxisIndex: 2,
|
|
|
|
|
|
yAxisIndex: 2,
|
|
|
|
|
|
data: macdData.macd.map(val => ({
|
|
|
|
|
|
value: val,
|
|
|
|
|
|
itemStyle: { color: val >= 0 ? 'rgba(16,185,129,0.6)' : 'rgba(239,68,68,0.6)' }
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
klineChart.setOption(option);
|
|
|
|
|
|
window.addEventListener('resize', () => klineChart && klineChart.resize());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calculateMA(data, dayCount) {
|
|
|
|
|
|
const result = [];
|
|
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
|
|
|
if (i < dayCount - 1) {
|
|
|
|
|
|
result.push('-');
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let sum = 0;
|
|
|
|
|
|
for (let j = 0; j < dayCount; j++) {
|
|
|
|
|
|
sum += parseFloat(data[i - j][2]);
|
|
|
|
|
|
}
|
|
|
|
|
|
result.push(parseFloat((sum / dayCount).toFixed(2)));
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleTheme() {
|
|
|
|
|
|
const isMinimal = document.body.classList.toggle('theme-minimal');
|
|
|
|
|
|
localStorage.setItem('futures-theme', isMinimal ? 'minimal' : 'dark');
|
|
|
|
|
|
updateThemeIcon(isMinimal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateThemeIcon(isMinimal) {
|
|
|
|
|
|
const icon = document.querySelector('#theme-toggle i');
|
|
|
|
|
|
if (icon) {
|
|
|
|
|
|
icon.className = isMinimal ? 'fas fa-sun' : 'fas fa-moon';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calculateMACD(data) {
|
|
|
|
|
|
const closes = data.map(d => parseFloat(d[2]));
|
|
|
|
|
|
const ema12 = calcEMA(closes, 12);
|
|
|
|
|
|
const ema26 = calcEMA(closes, 26);
|
|
|
|
|
|
|
|
|
|
|
|
const dif = [];
|
|
|
|
|
|
for (let i = 0; i < closes.length; i++) {
|
|
|
|
|
|
if (ema12[i] !== null && ema26[i] !== null) {
|
|
|
|
|
|
dif.push(ema12[i] - ema26[i]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dif.push(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dea = calcEMA(dif, 9);
|
|
|
|
|
|
const macd = dif.map((d, i) => 2 * (d - (dea[i] || 0)));
|
|
|
|
|
|
|
|
|
|
|
|
return { dif, dea, macd };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calcEMA(data, period) {
|
|
|
|
|
|
const result = new Array(data.length).fill(null);
|
|
|
|
|
|
const multiplier = 2 / (period + 1);
|
|
|
|
|
|
|
|
|
|
|
|
if (data.length < period) return result;
|
|
|
|
|
|
|
|
|
|
|
|
let sum = 0;
|
|
|
|
|
|
for (let i = 0; i < period; i++) sum += data[i];
|
|
|
|
|
|
result[period - 1] = sum / period;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = period; i < data.length; i++) {
|
|
|
|
|
|
result[i] = (data[i] - result[i - 1]) * multiplier + result[i - 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== Toast 提示 ====================
|
|
|
|
|
|
|
|
|
|
|
|
function showToast(type, title, message, duration = 3000) {
|
|
|
|
|
|
const container = document.getElementById('toast-container');
|
|
|
|
|
|
|
|
|
|
|
|
const iconMap = {
|
|
|
|
|
|
success: 'fas fa-check',
|
|
|
|
|
|
info: 'fas fa-info',
|
|
|
|
|
|
warning: 'fas fa-exclamation',
|
|
|
|
|
|
error: 'fas fa-times'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toast = document.createElement('div');
|
|
|
|
|
|
toast.className = `toast ${type}`;
|
|
|
|
|
|
toast.innerHTML = `
|
|
|
|
|
|
<div class="toast-icon"><i class="${iconMap[type]}"></i></div>
|
|
|
|
|
|
<div class="toast-content">
|
|
|
|
|
|
<div class="toast-title">${title}</div>
|
|
|
|
|
|
<div class="toast-message">${message}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
container.appendChild(toast);
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
toast.classList.add('removing');
|
|
|
|
|
|
setTimeout(() => toast.remove(), 300);
|
|
|
|
|
|
}, duration);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 数据刷新功能 ====================
|
|
|
|
|
|
|
|
|
|
|
|
let isRefreshing = false;
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshAllSymbols() {
|
|
|
|
|
|
if (isRefreshing) return;
|
|
|
|
|
|
|
|
|
|
|
|
const btn = document.getElementById('refresh-all-btn');
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
btn.classList.add('spinning');
|
|
|
|
|
|
isRefreshing = true;
|
|
|
|
|
|
|
|
|
|
|
|
showToast('info', '开始刷新', '正在同步所有品种数据...');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/refresh-all`, { method: 'POST' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
pollRefreshStatus();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast('error', '刷新失败', data.message || '请稍后重试');
|
|
|
|
|
|
resetRefreshButton(btn);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('刷新全部失败:', error);
|
|
|
|
|
|
showToast('error', '刷新失败', '网络错误,请稍后重试');
|
|
|
|
|
|
resetRefreshButton(btn);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function pollRefreshStatus() {
|
|
|
|
|
|
const btn = document.getElementById('refresh-all-btn');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/refresh-status`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success && data.data) {
|
|
|
|
|
|
const status = data.data;
|
|
|
|
|
|
|
|
|
|
|
|
if (!status.running) {
|
|
|
|
|
|
resetRefreshButton(btn);
|
|
|
|
|
|
|
|
|
|
|
|
await loadFuturesList();
|
|
|
|
|
|
|
|
|
|
|
|
if (currentSymbol) {
|
|
|
|
|
|
await loadFuturesDetail(currentSymbol);
|
|
|
|
|
|
await loadKlineData(currentSymbol, currentPeriod);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showToast('success', '刷新完成', `已同步 ${status.total} 个品种数据`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
btn.innerHTML = `<i class="fas fa-sync-alt"></i><span>刷新中 ${status.progress}/${status.total}</span>`;
|
|
|
|
|
|
setTimeout(pollRefreshStatus, 2000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取刷新状态失败:', error);
|
|
|
|
|
|
resetRefreshButton(btn);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetRefreshButton(btn) {
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
btn.classList.remove('spinning');
|
|
|
|
|
|
btn.innerHTML = '<i class="fas fa-sync-alt"></i><span>刷新全部</span>';
|
|
|
|
|
|
isRefreshing = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshSingleSymbol(symbol, btnElement = null) {
|
|
|
|
|
|
// 优先使用传入的按钮元素,其次尝试从事件获取,最后使用详情页按钮
|
|
|
|
|
|
let btn = btnElement;
|
|
|
|
|
|
if (!btn) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const evt = event;
|
|
|
|
|
|
if (evt && evt.target) {
|
|
|
|
|
|
const cardBtn = evt.target.closest('.card-refresh-btn');
|
|
|
|
|
|
if (cardBtn) {
|
|
|
|
|
|
btn = cardBtn;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// event 不存在时忽略
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!btn) {
|
|
|
|
|
|
btn = document.getElementById('refresh-symbol-btn');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!btn) {
|
|
|
|
|
|
showToast('error', '刷新失败', '无法找到刷新按钮');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const originalContent = btn.innerHTML;
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
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}/ai-analysis/${symbol}?force_refresh=false`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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.error || '未知错误');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
showToast('error', '刷新失败', '网络错误,请稍后重试');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
btn.innerHTML = originalContent;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== AI智能分析功能 ====================
|
|
|
|
|
|
|
|
|
|
|
|
let currentAIAnalysis = null;
|
|
|
|
|
|
|
|
|
|
|
|
async function runAIAnalysis(forceRefresh = false) {
|
|
|
|
|
|
if (!currentSymbol) {
|
|
|
|
|
|
showToast('warning', '提示', '请先选择一个品种');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const btn = document.getElementById('ai-analyze-btn');
|
|
|
|
|
|
const content = document.getElementById('ai-analysis-content');
|
|
|
|
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
btn.textContent = '分析中...';
|
|
|
|
|
|
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
|
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 28px; gap: 14px;">
|
|
|
|
|
|
<div style="font-size: 32px; color: var(--color-ai); animation: spin 1.5s linear infinite;">⟳</div>
|
|
|
|
|
|
<span>AI正在分析中...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}?force_refresh=${forceRefresh}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
currentAIAnalysis = data.data;
|
|
|
|
|
|
displayAIAnalysisResult(data.data);
|
|
|
|
|
|
showToast('success', '分析完成', `${currentSymbol} AI分析已完成`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
|
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
|
|
|
|
|
|
<p>${data.error || 'AI分析失败,请稍后重试'}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
showToast('error', '分析失败', data.error || 'AI分析失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('AI分析请求失败:', error);
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
|
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
|
|
|
|
|
|
<p>网络错误,请检查网络连接</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
showToast('error', '请求失败', '网络错误,请稍后重试');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
btn.textContent = '智能分析';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function displayAIAnalysisResult(data) {
|
|
|
|
|
|
console.log('[AI分析] 开始显示AI分析结果...', data);
|
|
|
|
|
|
const content = document.getElementById('ai-analysis-content');
|
|
|
|
|
|
console.log('[AI分析] ai-analysis-content元素:', content);
|
|
|
|
|
|
if (!content) {
|
|
|
|
|
|
console.error('[AI分析] 错误: 找不到ai-analysis-content元素!');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const result = data.result;
|
|
|
|
|
|
const timestamp = new Date(data.analysis_time).toLocaleString('zh-CN');
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[AI分析] 分析结果数据:', result);
|
|
|
|
|
|
|
|
|
|
|
|
const direction = result.trading_suggestion?.direction || '观望';
|
|
|
|
|
|
const directionClass = direction === '做多' ? 'long' : direction === '做空' ? 'short' : 'neutral';
|
|
|
|
|
|
const directionIcon = direction === '做多' ? '↑' : direction === '做空' ? '↓' : '↔';
|
|
|
|
|
|
const confidence = result.trading_suggestion?.confidence || 0;
|
|
|
|
|
|
|
|
|
|
|
|
const entryMin = result.trading_suggestion?.entry_range?.min || '--';
|
|
|
|
|
|
const entryMax = result.trading_suggestion?.entry_range?.max || '--';
|
|
|
|
|
|
const stopLoss = result.trading_suggestion?.stop_loss || '--';
|
|
|
|
|
|
const positionSize = result.trading_suggestion?.position_size || '--';
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[AI分析] 生成HTML模板...');
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
|
<div class="ai-analysis-result">
|
|
|
|
|
|
<div class="ai-summary">${result.summary || '暂无总结'}</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-suggestion-row">
|
|
|
|
|
|
<div class="ai-suggestion-direction ${directionClass}">
|
|
|
|
|
|
<span style="font-size:18px;font-weight:700;">${directionIcon}</span>
|
|
|
|
|
|
<span style="font-weight:600;">${direction}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-confidence">
|
|
|
|
|
|
<span>置信度</span>
|
|
|
|
|
|
<div class="ai-confidence-bar">
|
|
|
|
|
|
<div class="ai-confidence-fill" style="width: ${confidence}%"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span>${confidence}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-key-metrics">
|
|
|
|
|
|
<div class="ai-metric-item">
|
|
|
|
|
|
<span class="label">入场区间</span>
|
|
|
|
|
|
<span class="value">${entryMin}-${entryMax}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-metric-item">
|
|
|
|
|
|
<span class="label">止损位</span>
|
|
|
|
|
|
<span class="value down">${stopLoss}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-metric-item">
|
|
|
|
|
|
<span class="label">建议仓位</span>
|
|
|
|
|
|
<span class="value">${positionSize}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-metric-item">
|
|
|
|
|
|
<span class="label">纪律评分</span>
|
|
|
|
|
|
<span class="value">${result.discipline_score?.total || '--'}/${result.discipline_score?.max || '11'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-timestamp">
|
|
|
|
|
|
<span>🕐</span> 分析时间: ${timestamp}
|
|
|
|
|
|
<button class="ai-detail-btn" onclick="showAIDetailModal()" style="margin-left: 8px; background: none; border: 1px solid var(--border-color); padding: 4px 12px; border-radius: 4px; color: var(--cyan); cursor: pointer; font-size: 11px;">
|
|
|
|
|
|
查看详情 →
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[AI分析] HTML已设置到DOM');
|
|
|
|
|
|
|
|
|
|
|
|
// 同步AI分析数据到主面板各个卡片
|
|
|
|
|
|
syncAIToPanels(result);
|
|
|
|
|
|
console.log('[AI分析] AI分析结果显示完成');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncAIToPanels(result) {
|
|
|
|
|
|
const suggestion = result.trading_suggestion || {};
|
|
|
|
|
|
const fourDim = result.four_dimensional || {};
|
|
|
|
|
|
const pivotPoints = result.pivot_points || {};
|
|
|
|
|
|
const kdjDiag = result.kdj_diagnosis || {};
|
|
|
|
|
|
const scenarios = result.scenario_plans || {};
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 同步到技术指标卡片
|
|
|
|
|
|
// 从60min周期提取MACD和KDJ信息
|
|
|
|
|
|
const macd60 = fourDim['60min']?.macd || {};
|
|
|
|
|
|
const kdj60 = fourDim['60min']?.kdj || {};
|
|
|
|
|
|
|
|
|
|
|
|
const macdSignalEl = document.getElementById('macd-signal');
|
|
|
|
|
|
if (macdSignalEl) macdSignalEl.textContent = macd60.trend || '--';
|
|
|
|
|
|
|
|
|
|
|
|
const macdDetailEl = document.getElementById('macd-detail');
|
|
|
|
|
|
if (macdDetailEl) macdDetailEl.textContent = macd60.position ? `${macd60.position} | ${macd60.histogram || ''}` : '--';
|
|
|
|
|
|
|
|
|
|
|
|
const kdjSignalEl = document.getElementById('kdj-signal');
|
|
|
|
|
|
if (kdjSignalEl) kdjSignalEl.textContent = kdj60.status || '--';
|
|
|
|
|
|
|
|
|
|
|
|
const kdjDetailEl = document.getElementById('kdj-detail');
|
|
|
|
|
|
if (kdjDetailEl) kdjDetailEl.textContent = kdj60.signal || '--';
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 同步到关键点位卡片
|
|
|
|
|
|
if (pivotPoints.r1) {
|
|
|
|
|
|
const r1El = document.getElementById('resistance-1');
|
|
|
|
|
|
if (r1El) r1El.textContent = pivotPoints.r1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pivotPoints.r2) {
|
|
|
|
|
|
const r2El = document.getElementById('resistance-2');
|
|
|
|
|
|
if (r2El) r2El.textContent = pivotPoints.r2;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pivotPoints.pp) {
|
|
|
|
|
|
const ppEl = document.getElementById('pivot-point');
|
|
|
|
|
|
if (ppEl) ppEl.textContent = pivotPoints.pp;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pivotPoints.s1) {
|
|
|
|
|
|
const s1El = document.getElementById('support-1');
|
|
|
|
|
|
if (s1El) s1El.textContent = pivotPoints.s1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pivotPoints.s2) {
|
|
|
|
|
|
const s2El = document.getElementById('support-2');
|
|
|
|
|
|
if (s2El) s2El.textContent = pivotPoints.s2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 同步到多周期趋势卡片
|
|
|
|
|
|
const periodTrendsEl = document.getElementById('period-trends');
|
|
|
|
|
|
if (periodTrendsEl && Object.keys(fourDim).length > 0) {
|
|
|
|
|
|
const periodNames = { '60min': '60分钟', '30min': '30分钟', '15min': '15分钟', '5min': '5分钟' };
|
|
|
|
|
|
const periodOrder = ['60min', '30min', '15min', '5min'];
|
|
|
|
|
|
|
|
|
|
|
|
// 按固定顺序排列周期
|
|
|
|
|
|
const sortedEntries = Object.entries(fourDim).sort((a, b) => {
|
|
|
|
|
|
return periodOrder.indexOf(a[0]) - periodOrder.indexOf(b[0]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
periodTrendsEl.innerHTML = sortedEntries.map(([period, data]) => {
|
|
|
|
|
|
const trend = data.conclusion || data.macd?.trend || 'neutral';
|
|
|
|
|
|
const trendClass = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral';
|
|
|
|
|
|
const trendText = trend.includes('多') ? '偏多' : trend.includes('空') ? '偏空' : '震荡';
|
|
|
|
|
|
return `<div class="trend-item"><span class="trend-period">${periodNames[period] || period}</span><span class="trend-badge ${trendClass}">${trendText}</span></div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 同步到情景预案卡片
|
|
|
|
|
|
const scenarioPanel = document.getElementById('scenario-panel');
|
|
|
|
|
|
const scenarioPlansEl = document.getElementById('scenario-plans');
|
|
|
|
|
|
if (scenarioPanel && scenarioPlansEl && Object.keys(scenarios).length > 0) {
|
|
|
|
|
|
scenarioPanel.style.display = 'block';
|
|
|
|
|
|
const scenarioNames = {
|
|
|
|
|
|
'breakthrough': '突破',
|
|
|
|
|
|
'consolidation': '震荡',
|
|
|
|
|
|
'reversal': '反转',
|
|
|
|
|
|
'news_impact': '消息影响'
|
|
|
|
|
|
};
|
|
|
|
|
|
scenarioPlansEl.innerHTML = Object.entries(scenarios).map(([key, data]) => `
|
|
|
|
|
|
<div class="scenario-item">
|
|
|
|
|
|
<span class="scenario-name">${scenarioNames[key] || key}</span>
|
|
|
|
|
|
<span class="scenario-probability">${data.probability || 0}%</span>
|
|
|
|
|
|
<span class="scenario-action">${data.action || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
} else if (scenarioPanel) {
|
|
|
|
|
|
scenarioPanel.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showAIDetailModal() {
|
|
|
|
|
|
if (!currentAIAnalysis) {
|
|
|
|
|
|
showToast('warning', '提示', '暂无AI分析数据');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = currentAIAnalysis.result;
|
|
|
|
|
|
const modalBody = document.getElementById('ai-analysis-modal-body');
|
|
|
|
|
|
|
|
|
|
|
|
let fourDimensionalHTML = '';
|
|
|
|
|
|
const periods = ['60min', '30min', '15min', '5min'];
|
|
|
|
|
|
const periodNames = { '60min': '60分钟', '30min': '30分钟', '15min': '15分钟', '5min': '5分钟' };
|
|
|
|
|
|
periods.forEach(period => {
|
|
|
|
|
|
const data = result.four_dimensional?.[period];
|
|
|
|
|
|
if (data) {
|
|
|
|
|
|
fourDimensionalHTML += `
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td class="period-cell">${periodNames[period] || period}</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<div>趋势: ${data.macd?.trend || '--'}</div>
|
|
|
|
|
|
<div>位置: ${data.macd?.position || '--'}</div>
|
|
|
|
|
|
<div>柱状图: ${data.macd?.histogram || '--'}</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<div>状态: ${data.volume?.status || '--'}</div>
|
|
|
|
|
|
<div>量比: ${data.volume?.ratio || '--'}</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<div>K: ${data.kdj?.k || '--'} D: ${data.kdj?.d || '--'}</div>
|
|
|
|
|
|
<div>信号: ${data.kdj?.signal || '--'}</div>
|
|
|
|
|
|
<div>状态: ${data.kdj?.status || '--'}</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td>${data.conclusion || '--'}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let scenariosHTML = '';
|
|
|
|
|
|
const scenarios = result.scenario_plans || {};
|
|
|
|
|
|
const scenarioIcons = {
|
|
|
|
|
|
'breakthrough': 'fa-rocket',
|
|
|
|
|
|
'consolidation': 'fa-exchange-alt',
|
|
|
|
|
|
'reversal': 'fa-undo',
|
|
|
|
|
|
'news_impact': 'fa-newspaper'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(scenarios).forEach(([key, scenario]) => {
|
|
|
|
|
|
scenariosHTML += `
|
|
|
|
|
|
<div class="scenario-card ${key}">
|
|
|
|
|
|
<div class="scenario-probability">${scenario.probability || 0}%</div>
|
|
|
|
|
|
<div class="scenario-action">${scenario.action || '--'}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let redLinesHTML = '';
|
|
|
|
|
|
if (result.red_lines_check?.violated?.length > 0) {
|
|
|
|
|
|
result.red_lines_check.violated.forEach(line => {
|
|
|
|
|
|
redLinesHTML += `<div class="red-line-item"><i class="fas fa-exclamation-circle"></i> ${line}</div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
redLinesHTML = '<div class="red-line-item pass"><i class="fas fa-check-circle"></i> 未触碰交易红线</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let disciplineHTML = '';
|
|
|
|
|
|
const discipline = result.discipline_score?.details || {};
|
|
|
|
|
|
const disciplineLabels = {
|
|
|
|
|
|
'trend': '趋势',
|
|
|
|
|
|
'position': '位置',
|
|
|
|
|
|
'signal': '信号',
|
|
|
|
|
|
'risk': '风险',
|
|
|
|
|
|
'mindset': '心态'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(disciplineLabels).forEach(([key, label]) => {
|
|
|
|
|
|
const isPass = discipline[key];
|
|
|
|
|
|
disciplineHTML += `
|
|
|
|
|
|
<div class="discipline-item ${isPass ? 'pass' : 'fail'}">
|
|
|
|
|
|
<i class="fas ${isPass ? 'fa-check-circle' : 'fa-times-circle'}"></i>
|
|
|
|
|
|
<span class="discipline-label">${label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let experiencesHTML = '';
|
|
|
|
|
|
const experiences = result.experience_lessons || [];
|
|
|
|
|
|
if (experiences.length > 0) {
|
|
|
|
|
|
experiences.forEach(exp => {
|
|
|
|
|
|
experiencesHTML += `<div class="experience-item"><i class="fas fa-lightbulb"></i> ${exp}</div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
experiencesHTML = '<div class="experience-item"><i class="fas fa-info-circle"></i> 暂无经验提醒</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modalBody.innerHTML = `
|
|
|
|
|
|
<div class="ai-modal-section">
|
|
|
|
|
|
<div class="ai-modal-section-title">
|
|
|
|
|
|
<i class="fas fa-chart-bar"></i>
|
|
|
|
|
|
四维联合信号表 (4D-XV)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<table class="four-dimensional-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>周期</th>
|
|
|
|
|
|
<th>MACD(趋势)</th>
|
|
|
|
|
|
<th>成交量(资金)</th>
|
|
|
|
|
|
<th>KDJ(时机)</th>
|
|
|
|
|
|
<th>结论</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
${fourDimensionalHTML || '<tr><td colspan="5">暂无数据</td></tr>'}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-modal-section">
|
|
|
|
|
|
<div class="ai-modal-section-title">
|
|
|
|
|
|
<i class="fas fa-crosshairs"></i>
|
|
|
|
|
|
关键点位 (Pivot Point)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-key-metrics">
|
|
|
|
|
|
<div class="ai-metric-item">
|
|
|
|
|
|
<span class="label">R2</span>
|
|
|
|
|
|
<span class="value down">${result.pivot_points?.r2 || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-metric-item">
|
|
|
|
|
|
<span class="label">R1</span>
|
|
|
|
|
|
<span class="value down">${result.pivot_points?.r1 || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-metric-item">
|
|
|
|
|
|
<span class="label">PP</span>
|
|
|
|
|
|
<span class="value" style="color: var(--purple);">${result.pivot_points?.pp || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-metric-item">
|
|
|
|
|
|
<span class="label">S1</span>
|
|
|
|
|
|
<span class="value up">${result.pivot_points?.s1 || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-metric-item">
|
|
|
|
|
|
<span class="label">S2</span>
|
|
|
|
|
|
<span class="value up">${result.pivot_points?.s2 || '--'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-modal-section">
|
|
|
|
|
|
<div class="ai-modal-section-title">
|
|
|
|
|
|
<i class="fas fa-balance-scale"></i>
|
|
|
|
|
|
交易红线审查
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="red-lines-list">
|
|
|
|
|
|
${redLinesHTML}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-modal-section">
|
|
|
|
|
|
<div class="ai-modal-section-title">
|
|
|
|
|
|
<i class="fas fa-check-double"></i>
|
|
|
|
|
|
11项纪律检查
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="discipline-grid">
|
|
|
|
|
|
${disciplineHTML}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-modal-section">
|
|
|
|
|
|
<div class="ai-modal-section-title">
|
|
|
|
|
|
<i class="fas fa-project-diagram"></i>
|
|
|
|
|
|
情景预案
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="scenario-cards">
|
|
|
|
|
|
${scenariosHTML || '<div class="scenario-card"><div class="scenario-action">暂无情景预案</div></div>'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-modal-section">
|
|
|
|
|
|
<div class="ai-modal-section-title">
|
|
|
|
|
|
<i class="fas fa-lightbulb"></i>
|
|
|
|
|
|
经验教训提醒
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-experience-list">
|
|
|
|
|
|
${experiencesHTML}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-modal-section">
|
|
|
|
|
|
<div class="ai-modal-section-title">
|
|
|
|
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
|
|
|
|
风险提示
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-experience-list">
|
|
|
|
|
|
${(result.risk_warnings || []).map(w => `<div class="experience-item"><i class="fas fa-exclamation-circle"></i> ${w}</div>`).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('ai-analysis-modal').classList.add('active');
|
|
|
|
|
|
}
|