|
|
|
@ -124,6 +124,25 @@ function initEventListeners() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 导航项点击事件
|
|
|
|
|
|
|
|
document.querySelectorAll('.nav-item[data-page]').forEach(navItem => {
|
|
|
|
|
|
|
|
navItem.addEventListener('click', function(e) {
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
const page = this.dataset.page;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
|
|
|
|
|
|
|
|
this.classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (page === 'review') {
|
|
|
|
|
|
|
|
showReviewView();
|
|
|
|
|
|
|
|
} else if (page === 'watched') {
|
|
|
|
|
|
|
|
showWatchedView();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
showListView();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeModal(modalId) {
|
|
|
|
function closeModal(modalId) {
|
|
|
|
@ -133,10 +152,38 @@ function closeModal(modalId) {
|
|
|
|
function showListView() {
|
|
|
|
function showListView() {
|
|
|
|
document.getElementById('list-view').classList.add('active');
|
|
|
|
document.getElementById('list-view').classList.add('active');
|
|
|
|
document.getElementById('detail-view').classList.remove('active');
|
|
|
|
document.getElementById('detail-view').classList.remove('active');
|
|
|
|
|
|
|
|
document.getElementById('review-view').classList.remove('active');
|
|
|
|
|
|
|
|
document.getElementById('watched-view').classList.remove('active');
|
|
|
|
|
|
|
|
if (klineChart) {
|
|
|
|
|
|
|
|
klineChart.dispose();
|
|
|
|
|
|
|
|
klineChart = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function showWatchedView() {
|
|
|
|
|
|
|
|
document.getElementById('list-view').classList.remove('active');
|
|
|
|
|
|
|
|
document.getElementById('detail-view').classList.remove('active');
|
|
|
|
|
|
|
|
document.getElementById('review-view').classList.remove('active');
|
|
|
|
|
|
|
|
document.getElementById('watched-view').classList.add('active');
|
|
|
|
if (klineChart) {
|
|
|
|
if (klineChart) {
|
|
|
|
klineChart.dispose();
|
|
|
|
klineChart.dispose();
|
|
|
|
klineChart = null;
|
|
|
|
klineChart = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderWatchedList();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function showReviewView() {
|
|
|
|
|
|
|
|
document.getElementById('list-view').classList.remove('active');
|
|
|
|
|
|
|
|
document.getElementById('detail-view').classList.remove('active');
|
|
|
|
|
|
|
|
document.getElementById('review-view').classList.add('active');
|
|
|
|
|
|
|
|
if (klineChart) {
|
|
|
|
|
|
|
|
klineChart.dispose();
|
|
|
|
|
|
|
|
klineChart = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化复盘计划功能
|
|
|
|
|
|
|
|
initReviewPlan();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function showDetailView(symbol) {
|
|
|
|
async function showDetailView(symbol) {
|
|
|
|
@ -231,7 +278,6 @@ async function loadWatchedSymbols() {
|
|
|
|
const data = await response.json();
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
|
|
if (data.success) {
|
|
|
|
watchedSymbols = data.data.map(s => s.symbol);
|
|
|
|
watchedSymbols = data.data.map(s => s.symbol);
|
|
|
|
document.getElementById('count-watched').textContent = watchedSymbols.length;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error('加载自选列表失败:', error);
|
|
|
|
console.error('加载自选列表失败:', error);
|
|
|
|
@ -263,14 +309,13 @@ async function toggleWatch(symbol, name, event) {
|
|
|
|
showToast('success', '已添加自选', `${name}(${symbol}) 已添加到自选列表`);
|
|
|
|
showToast('success', '已添加自选', `${name}(${symbol}) 已添加到自选列表`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
document.getElementById('count-watched').textContent = watchedSymbols.length;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 重新渲染当前视图
|
|
|
|
// 重新渲染当前视图
|
|
|
|
const activePill = document.querySelector('.pill.active');
|
|
|
|
const activePill = document.querySelector('.pill.active');
|
|
|
|
const category = activePill ? activePill.dataset.category : 'all';
|
|
|
|
const category = activePill ? activePill.dataset.category : 'all';
|
|
|
|
if (category === 'watched') {
|
|
|
|
|
|
|
|
filterByCategory('watched');
|
|
|
|
// 如果当前视图不是自选页面,重新渲染主网格
|
|
|
|
} else {
|
|
|
|
if (category !== 'watched') {
|
|
|
|
renderFuturesGrid(getCurrentFilteredData());
|
|
|
|
renderFuturesGrid(getCurrentFilteredData());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
@ -283,8 +328,13 @@ function toggleFav(symbol, name, event) {
|
|
|
|
event.stopPropagation();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
|
|
|
|
// 调用原有的toggleWatch函数处理API请求和数据更新
|
|
|
|
// 调用原有的toggleWatch函数处理API请求和数据更新
|
|
|
|
// toggleWatch 会重新渲染网格,星标状态会根据 watchedSymbols 自动更新
|
|
|
|
|
|
|
|
toggleWatch(symbol, name, event);
|
|
|
|
toggleWatch(symbol, name, event);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 如果当前在自选页面,刷新自选列表
|
|
|
|
|
|
|
|
const watchedView = document.getElementById('watched-view');
|
|
|
|
|
|
|
|
if (watchedView && watchedView.classList.contains('active')) {
|
|
|
|
|
|
|
|
setTimeout(() => renderWatchedList(), 200);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getCurrentFilteredData() {
|
|
|
|
function getCurrentFilteredData() {
|
|
|
|
@ -295,9 +345,6 @@ function getCurrentFilteredData() {
|
|
|
|
|
|
|
|
|
|
|
|
function filterDataByCategory(data, category) {
|
|
|
|
function filterDataByCategory(data, category) {
|
|
|
|
if (category === 'all') return data;
|
|
|
|
if (category === 'all') return data;
|
|
|
|
if (category === 'watched') {
|
|
|
|
|
|
|
|
return data.filter(item => watchedSymbols.includes(item.symbol));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const categoryMap = {
|
|
|
|
const categoryMap = {
|
|
|
|
'energy': ['SC', 'FU', 'LU', 'BU', 'RU', 'NR'],
|
|
|
|
'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'],
|
|
|
|
'metal': ['AU', 'AG', 'CU', 'AL', 'ZN', 'NI', 'SN', 'PB', 'SS', 'RB', 'HC', 'I', 'J', 'JM', 'AO', 'SI', 'LC', 'PS'],
|
|
|
|
@ -1728,8 +1775,16 @@ async function runAIAnalysis(forceRefresh = false) {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 设置5分钟超时
|
|
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
|
|
|
const timeoutId = setTimeout(() => controller.abort(), 300000); // 5分钟 = 300000毫秒
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}?force_refresh=${forceRefresh}`);
|
|
|
|
const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}?force_refresh=${forceRefresh}`, {
|
|
|
|
|
|
|
|
signal: controller.signal
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
if (data.success) {
|
|
|
|
@ -1745,13 +1800,24 @@ async function runAIAnalysis(forceRefresh = false) {
|
|
|
|
showToast('error', '分析失败', data.error || 'AI分析失败');
|
|
|
|
showToast('error', '分析失败', data.error || 'AI分析失败');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
console.error('AI分析请求失败:', error);
|
|
|
|
console.error('AI分析请求失败:', error);
|
|
|
|
content.innerHTML = `
|
|
|
|
|
|
|
|
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
|
|
|
|
if (error.name === 'AbortError') {
|
|
|
|
<p>网络错误,请检查网络连接</p>
|
|
|
|
content.innerHTML = `
|
|
|
|
</div>
|
|
|
|
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
|
|
|
|
`;
|
|
|
|
<p>分析超时,请稍后重试或联系管理员</p>
|
|
|
|
showToast('error', '请求失败', '网络错误,请稍后重试');
|
|
|
|
</div>
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
showToast('error', '请求超时', 'AI分析耗时过长,请稍后重试');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
|
|
|
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
|
|
|
|
|
|
|
|
<p>网络错误,请检查网络连接</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
showToast('error', '请求失败', '网络错误,请稍后重试');
|
|
|
|
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
btn.disabled = false;
|
|
|
|
btn.disabled = false;
|
|
|
|
btn.textContent = '智能分析';
|
|
|
|
btn.textContent = '智能分析';
|
|
|
|
@ -2122,3 +2188,470 @@ function showAIDetailModal() {
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('ai-analysis-modal').classList.add('active');
|
|
|
|
document.getElementById('ai-analysis-modal').classList.add('active');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 复盘计划功能 ====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const REVIEW_API_BASE = '/api/v1/review';
|
|
|
|
|
|
|
|
const DATA_API_BASE = '/api/v1/data';
|
|
|
|
|
|
|
|
let currentReviewDateId = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initReviewPlan() {
|
|
|
|
|
|
|
|
const reviewDateSelector = document.getElementById('review-date-selector');
|
|
|
|
|
|
|
|
const btnReviewPlan = document.getElementById('btn-review-plan');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (reviewDateSelector) {
|
|
|
|
|
|
|
|
reviewDateSelector.addEventListener('change', function(e) {
|
|
|
|
|
|
|
|
const selectedDate = e.target.value;
|
|
|
|
|
|
|
|
if (selectedDate) {
|
|
|
|
|
|
|
|
loadReviewByDate(selectedDate);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hideReviewSummary();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (btnReviewPlan) {
|
|
|
|
|
|
|
|
btnReviewPlan.addEventListener('click', function() {
|
|
|
|
|
|
|
|
handleReviewAndPlan();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loadReviewDates();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleReviewAndPlan() {
|
|
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
|
|
const hour = now.getHours();
|
|
|
|
|
|
|
|
const dayOfWeek = now.getDay();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isTradingTime = checkIsTradingTime(dayOfWeek, hour);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isTradingTime) {
|
|
|
|
|
|
|
|
alert('当前时间在交易时间内,请在每日收盘后进行复盘与计划。\n可复盘与计划的时间为:\n1. 非交易日(周六、周日)\n2. 交易日的 0:00 - 9:00\n3. 交易日的 15:00 - 21:00');
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkDataConsistency();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function checkIsTradingTime(dayOfWeek, hour) {
|
|
|
|
|
|
|
|
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isWeekend) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 交易日 9:00 之前可以复盘
|
|
|
|
|
|
|
|
if (hour < 9) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 交易日 15:00 - 21:00 可以复盘
|
|
|
|
|
|
|
|
if (hour >= 15 && hour < 21) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function checkDataConsistency() {
|
|
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
|
|
const todayStr = today.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fetch(`${DATA_API_BASE}/latest-timestamps`)
|
|
|
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
|
|
if (!data.success || !data.data || data.data.length === 0) {
|
|
|
|
|
|
|
|
alert('交易数据库中没有数据,请先进行数据更新');
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let latestDate = null;
|
|
|
|
|
|
|
|
data.data.forEach(item => {
|
|
|
|
|
|
|
|
if (item.last_refresh_at) {
|
|
|
|
|
|
|
|
const dateStr = item.last_refresh_at.split('T')[0];
|
|
|
|
|
|
|
|
if (!latestDate || dateStr > latestDate) {
|
|
|
|
|
|
|
|
latestDate = dateStr;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!latestDate) {
|
|
|
|
|
|
|
|
alert('交易数据库中没有数据,请先进行数据更新');
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (latestDate < todayStr) {
|
|
|
|
|
|
|
|
alert(`交易数据库中的最新数据日期为 ${latestDate},与当前日期 ${todayStr} 不一致。\n请先进行数据更新,确保数据是最新的。`);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loadOrCreateReviewPlan(todayStr);
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
|
|
console.error('检查数据一致性失败:', err);
|
|
|
|
|
|
|
|
alert('无法获取交易数据状态,请稍后重试');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadOrCreateReviewPlan(todayStr) {
|
|
|
|
|
|
|
|
fetch(`${REVIEW_API_BASE}/dates?limit=1`)
|
|
|
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
|
|
if (data.success && data.data.length > 0) {
|
|
|
|
|
|
|
|
const latestReview = data.data[0];
|
|
|
|
|
|
|
|
if (latestReview.review_date === todayStr) {
|
|
|
|
|
|
|
|
currentReviewDateId = latestReview.id;
|
|
|
|
|
|
|
|
loadReviewSummary(currentReviewDateId);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
createReviewPlan(todayStr);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
createReviewPlan(todayStr);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
|
|
console.error('加载复盘日期失败:', err);
|
|
|
|
|
|
|
|
createReviewPlan(todayStr);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createReviewPlan(todayStr) {
|
|
|
|
|
|
|
|
const weekDays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
|
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
|
|
const weekDay = weekDays[now.getDay() - 1] || "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alert('开始执行复盘与交易计划生成...');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fetch(`${REVIEW_API_BASE}/plans/batch`, {
|
|
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
|
|
review_date: todayStr,
|
|
|
|
|
|
|
|
week_day: weekDay,
|
|
|
|
|
|
|
|
rankings: [],
|
|
|
|
|
|
|
|
plans: []
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
|
|
currentReviewDateId = data.data.review_date_id;
|
|
|
|
|
|
|
|
loadReviewDates();
|
|
|
|
|
|
|
|
loadReviewSummary(currentReviewDateId);
|
|
|
|
|
|
|
|
alert('复盘与计划创建成功');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
alert('创建复盘计划失败');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
|
|
console.error('创建复盘计划失败:', err);
|
|
|
|
|
|
|
|
alert('创建失败,请稍后重试');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadReviewDates() {
|
|
|
|
|
|
|
|
fetch(`${REVIEW_API_BASE}/dates?limit=20`)
|
|
|
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
|
|
if (data.success && data.data.length > 0) {
|
|
|
|
|
|
|
|
const lastDate = data.data[0];
|
|
|
|
|
|
|
|
document.getElementById('review-date-selector').value = lastDate.review_date;
|
|
|
|
|
|
|
|
currentReviewDateId = lastDate.id;
|
|
|
|
|
|
|
|
loadReviewSummary(lastDate.id);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
|
|
console.error('加载复盘日期失败:', err);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadReviewByDate(selectedDate) {
|
|
|
|
|
|
|
|
fetch(`${REVIEW_API_BASE}/dates?limit=100`)
|
|
|
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
|
|
if (data.success && data.data.length > 0) {
|
|
|
|
|
|
|
|
const matchedReview = data.data.find(d => d.review_date === selectedDate);
|
|
|
|
|
|
|
|
if (matchedReview) {
|
|
|
|
|
|
|
|
currentReviewDateId = matchedReview.id;
|
|
|
|
|
|
|
|
loadReviewSummary(currentReviewDateId);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hideReviewSummary();
|
|
|
|
|
|
|
|
document.getElementById('review-summary-content').style.display = 'block';
|
|
|
|
|
|
|
|
document.getElementById('review-empty-state').style.display = 'none';
|
|
|
|
|
|
|
|
document.getElementById('review-rank-volume').innerHTML = '';
|
|
|
|
|
|
|
|
document.getElementById('review-rank-amplitude').innerHTML = '';
|
|
|
|
|
|
|
|
document.getElementById('review-rank-change').innerHTML = '';
|
|
|
|
|
|
|
|
document.getElementById('review-rank-oi').innerHTML = '';
|
|
|
|
|
|
|
|
document.getElementById('review-plan-list').innerHTML = '<div class="plan-note">该日期暂无复盘数据</div>';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hideReviewSummary();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
|
|
console.error('加载复盘日期失败:', err);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadReviewSummary(reviewDateId) {
|
|
|
|
|
|
|
|
fetch(`${REVIEW_API_BASE}/summary/${reviewDateId}`)
|
|
|
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
|
|
renderReviewSummary(data.data);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
alert('加载复盘数据失败');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
|
|
console.error('加载复盘数据失败:', err);
|
|
|
|
|
|
|
|
alert('加载失败');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderReviewSummary(data) {
|
|
|
|
|
|
|
|
document.getElementById('review-current-date').textContent = `${data.review_date} (${data.week_day})`;
|
|
|
|
|
|
|
|
document.getElementById('review-summary-content').style.display = 'block';
|
|
|
|
|
|
|
|
document.getElementById('review-empty-state').style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderReviewRankings(data.rankings);
|
|
|
|
|
|
|
|
renderReviewPlans(data.plans, data.review_date);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderReviewRankings(rankings) {
|
|
|
|
|
|
|
|
const rankTypes = {
|
|
|
|
|
|
|
|
'volume': 'review-rank-volume',
|
|
|
|
|
|
|
|
'amplitude': 'review-rank-amplitude',
|
|
|
|
|
|
|
|
'change': 'review-rank-change',
|
|
|
|
|
|
|
|
'open_interest': 'review-rank-oi'
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const [type, elementId] of Object.entries(rankTypes)) {
|
|
|
|
|
|
|
|
const container = document.getElementById(elementId);
|
|
|
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (rankings[type] && rankings[type].items) {
|
|
|
|
|
|
|
|
rankings[type].items.forEach(item => {
|
|
|
|
|
|
|
|
const row = document.createElement('div');
|
|
|
|
|
|
|
|
row.className = 'rank-row';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isTop = item.rank <= 3;
|
|
|
|
|
|
|
|
const changeClass = item.change_pct && item.change_pct.startsWith('+') ? 'up' :
|
|
|
|
|
|
|
|
item.change_pct && item.change_pct.startsWith('-') ? 'down' : '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
row.innerHTML = `
|
|
|
|
|
|
|
|
<div class="rank-rank ${isTop ? 'top' : ''}">${item.rank}</div>
|
|
|
|
|
|
|
|
<div class="rank-name">${item.symbol} ${item.name}</div>
|
|
|
|
|
|
|
|
<div class="rank-val ${changeClass}">${item.value || item.price || ''}</div>
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
container.appendChild(row);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderReviewPlans(plans, reviewDate) {
|
|
|
|
|
|
|
|
const container = document.getElementById('review-plan-list');
|
|
|
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!plans || plans.length === 0) {
|
|
|
|
|
|
|
|
container.innerHTML = '<div class="plan-note">暂无交易计划</div>';
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
plans.forEach((plan, index) => {
|
|
|
|
|
|
|
|
const item = document.createElement('div');
|
|
|
|
|
|
|
|
item.className = 'plan-list-item';
|
|
|
|
|
|
|
|
item.onclick = function() { toggleReviewPlan(this); };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const planTypeText = plan.plan_type === 'long' ? '做多' : '做空';
|
|
|
|
|
|
|
|
const planTypeClass = plan.plan_type === 'long' ? 'long' : 'short';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
item.innerHTML = `
|
|
|
|
|
|
|
|
<div class="plan-list-left">
|
|
|
|
|
|
|
|
<span class="plan-list-code">${plan.symbol} ${plan.name}</span>
|
|
|
|
|
|
|
|
<span class="plan-badge ${planTypeClass}">${planTypeText}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div style="display:flex;align-items:center;gap:8px">
|
|
|
|
|
|
|
|
<span class="plan-list-score">${plan.score}</span>
|
|
|
|
|
|
|
|
<span class="plan-list-arrow">▼</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const detail = document.createElement('div');
|
|
|
|
|
|
|
|
detail.className = 'plan-detail';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const generatedDate = plan.created_at ? new Date(plan.created_at).toLocaleString('zh-CN') : reviewDate;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
detail.innerHTML = `
|
|
|
|
|
|
|
|
<div class="plan-card" style="margin-bottom:0">
|
|
|
|
|
|
|
|
<div class="plan-ai">
|
|
|
|
|
|
|
|
<strong>评分: ${plan.score}/100</strong><br>
|
|
|
|
|
|
|
|
多空逻辑: ${plan.logic || '暂无'}<br>
|
|
|
|
|
|
|
|
入选理由: ${plan.reason || '暂无'}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="plan-targets">
|
|
|
|
|
|
|
|
<div class="plan-target">
|
|
|
|
|
|
|
|
<div class="plan-target-label">入场</div>
|
|
|
|
|
|
|
|
<div class="plan-target-val neutral">${plan.entry_price || '-'}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="plan-target">
|
|
|
|
|
|
|
|
<div class="plan-target-label">止损</div>
|
|
|
|
|
|
|
|
<div class="plan-target-val red">${plan.stop_loss || '-'}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="plan-target">
|
|
|
|
|
|
|
|
<div class="plan-target-label">止盈</div>
|
|
|
|
|
|
|
|
<div class="plan-target-val green">${plan.take_profit || '-'}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="plan-note">置信度: ${plan.confidence || '-'} | 仓位建议: ${plan.position_suggestion || '-'}</div>
|
|
|
|
|
|
|
|
<div class="plan-note" style="margin-top:12px;color:var(--color-ai);font-weight:500;">计划生成时间: ${generatedDate}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
container.appendChild(item);
|
|
|
|
|
|
|
|
container.appendChild(detail);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function toggleReviewPlan(element) {
|
|
|
|
|
|
|
|
const detail = element.nextElementSibling;
|
|
|
|
|
|
|
|
const isOpen = detail.classList.contains('open');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('#review-plan-list .plan-detail.open').forEach(d => {
|
|
|
|
|
|
|
|
d.classList.remove('open');
|
|
|
|
|
|
|
|
d.previousElementSibling.classList.remove('expanded');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
|
|
|
detail.classList.add('open');
|
|
|
|
|
|
|
|
element.classList.add('expanded');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function hideReviewSummary() {
|
|
|
|
|
|
|
|
document.getElementById('review-summary-content').style.display = 'none';
|
|
|
|
|
|
|
|
document.getElementById('review-empty-state').style.display = 'flex';
|
|
|
|
|
|
|
|
currentReviewDateId = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function clearReviewData() {
|
|
|
|
|
|
|
|
if (confirm('确定要清除所有复盘与交易计划数据吗?此操作不可恢复。')) {
|
|
|
|
|
|
|
|
fetch(`${REVIEW_API_BASE}/clear`, {
|
|
|
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
|
|
alert('复盘数据已清除');
|
|
|
|
|
|
|
|
document.getElementById('review-date-selector').value = '';
|
|
|
|
|
|
|
|
hideReviewSummary();
|
|
|
|
|
|
|
|
loadReviewDates();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
alert(data.message || '清除失败');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
|
|
console.error('清除数据失败:', err);
|
|
|
|
|
|
|
|
alert('清除失败,请稍后重试');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 自选品种功能 ====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderWatchedList() {
|
|
|
|
|
|
|
|
const grid = document.getElementById('watched-grid');
|
|
|
|
|
|
|
|
const emptyState = document.getElementById('watched-empty');
|
|
|
|
|
|
|
|
const totalCountEl = document.getElementById('watched-total-count');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (watchedSymbols.length === 0) {
|
|
|
|
|
|
|
|
grid.style.display = 'none';
|
|
|
|
|
|
|
|
emptyState.style.display = 'flex';
|
|
|
|
|
|
|
|
totalCountEl.textContent = '0 个品种';
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
grid.style.display = 'grid';
|
|
|
|
|
|
|
|
emptyState.style.display = 'none';
|
|
|
|
|
|
|
|
totalCountEl.textContent = `${watchedSymbols.length} 个品种`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const watchedData = allFuturesData.filter(item => watchedSymbols.includes(item.symbol));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
grid.innerHTML = watchedData.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) : '--';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isWatched = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
|
|
<div class="card" onclick="showDetailView('${item.symbol}')">
|
|
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
|
|
<div class="card-left">
|
|
|
|
|
|
|
|
<div class="code-box">${code}</div>
|
|
|
|
|
|
|
|
<div class="info-group">
|
|
|
|
|
|
|
|
<div class="name-row">${item.name} <span class="fav-icon active" onclick="toggleFav('${item.symbol}', '${item.name}', event)">★</span></div>
|
|
|
|
|
|
|
|
<div class="contract-code">${item.symbol}</div>
|
|
|
|
|
|
|
|
</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('');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|