-
-
+
+
+ ⌘K
-
-
-
+
+
+
+
-
- 分类:
-
-
-
-
-
+
+
+
+
+
+
+
-
- 排序:
-
-
-
- 共 8 个品种
- 7
- 1
+
+
+
@@ -95,180 +164,201 @@
+
+
+
+ 0
+ 监控品种
+
+
+
+
+
+ 0
+ 上涨趋势
+
+
+
+
+
+ 0
+ 下跌趋势
+
+
+
+
+ 0
+ 震荡整理
+
+
-
+
+
+
+
-
-
-
- ¥2,150
-
- +196.00 (+10.06%)
-
+
+
-
+ --
+ --
-
+
+
+ --
+ --
+
+
+ R1: --
+ S1: --
+
+
+
开盘
- 1,960
+ --
最高
- 2,200
+ --
最低
- 1,940
+ --
- 持仓量
- 45,600
+ 成交量
+ --
-
- 周期选择
-
-
-
-
+
+ 周期
+
+
+
+
+
+
-
-
+
+
-
+
+
+
+
+ K线图
+
+
+
+ MA5
+ MA10
+ MA20
+
+
+
+
+ 历史分析记录
+
+
+
+
+
-
-
-
-
- 交易建议
+
+
+
+
+
+ AI 交易建议
+
-
-
操作建议
- 逢低做多
- 涨停突破,地缘风险推升运价
+
+
- --
+ --
-
- 建议入场
- 2,137.1
+
+
+ 入场
+ --
-
- 目标价位
- 2,236
+
+ 目标
+ --
-
- 止损价位
- 2,107
+
-
+ 止损
+ --
-
- 风险等级
- 低
+
+ 风险
+ --
-
-
+
+
+
技术指标
-
-
MACD
- 金叉
- DIF: -0.0147
+
+ MACD
+ --
+ --
-
-
RSI
- 47
- 正常
+
+ RSI
+ --
+ --
-
-
- 布林带
- 中轨
- 区间: 2086-2215
+
+ BOLL
+ --
+ --
-
-
KDJ
- 中性
- K: 71 D: 87
+
+ KDJ
+ --
+ --
-
-
+
+
+
关键点位
-
-
压力位
-
- 压力 1
- 2,200
-
-
- 压力 2
- 2,300
+
+
+ 压力
+
- R1--
+ R2--
+ R3--
- 压力 3
- 2,400
+
+
-
+ 支撑
+
S1--
+ S2--
+ S3--
-
+
+
+ 支撑位
-
- 支撑 1
- 2,000
-
-
- 支撑 2
- 1,900
-
-
- 支撑 3
- 1,800
-
+
+
-
-
+
+ 多周期趋势
+
+
+
-
-
- 多周期一致性
+
+
+
+
+
+
+ 趋势评分
-
-
@@ -277,6 +367,32 @@
- 5分钟
- 上涨
-
-
- 15分钟
- 上涨
-
-
- 30分钟
- 上涨
-
-
- 60分钟
- 震荡
+
+
+
+ --
+ 综合评分
+
+
+
+
+
+
+
+ AI 交易建议详情
+ +
+
+
+
+
+
diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js
index acae11e..ce31b65 100644
--- a/app/static/futures_analysis.js
+++ b/app/static/futures_analysis.js
@@ -4,19 +4,54 @@ 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') + '/' +
+ 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') + ':' +
@@ -27,6 +62,23 @@ function updateTime() {
function initEventListeners() {
document.getElementById('back-btn').addEventListener('click', showListView);
+ document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
+
+ document.getElementById('suggestion-card').addEventListener('click', function() {
+ if (currentDetailData) {
+ showSuggestionModal(currentDetailData);
+ }
+ });
+
+ 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'));
@@ -40,9 +92,9 @@ function initEventListeners() {
filterFuturesList(this.value);
});
- document.querySelectorAll('.filter-btn').forEach(btn => {
- btn.addEventListener('click', function() {
- document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
+ document.querySelectorAll('.filter-tab').forEach(tab => {
+ tab.addEventListener('click', function() {
+ document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
filterByCategory(this.dataset.category);
});
@@ -51,6 +103,18 @@ function initEventListeners() {
document.getElementById('sort-select').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() {
@@ -68,6 +132,80 @@ function showDetailView(symbol) {
document.getElementById('detail-view').classList.add('active');
loadFuturesDetail(symbol);
loadKlineData(symbol, currentPeriod);
+ loadHistoryList(symbol);
+}
+
+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() {
@@ -123,265 +261,125 @@ async function loadFuturesFromConfig() {
function generateMockFuturesData() {
return [
- {
- symbol: 'EC',
- name: '集运指数',
- price: 2150,
- change: 196,
- changePct: 10.06,
- suggestion: '逢低做多',
- suggestionType: 'up',
- periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' },
- successRate: 80,
- trendScore: 90,
- resistance: 2200,
- support: 2000,
- open: 1960,
- high: 2200,
- low: 1940,
- volume: 45600
- },
- {
- symbol: 'AU',
- 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: 'AG',
- 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: 'SC',
- 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: 'I',
- name: '铁矿石',
- price: 785.5,
- change: 28,
- changePct: 3.7,
- suggestion: '逢低做多',
- suggestionType: 'up',
- periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' },
- successRate: 68,
- trendScore: 82,
- resistance: 792,
- support: 770,
- open: 757.5,
- high: 788,
- low: 755,
- volume: 156000
- },
- {
- symbol: 'CU',
- 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: 'P',
- name: '棕榈油',
- price: 8750,
- change: 0,
- changePct: 0,
- suggestion: '观望等待',
- suggestionType: 'neutral',
- periods: { '5': 'neutral', '15': 'neutral', '30': 'neutral', '60': 'neutral' },
- successRate: 52,
- trendScore: 50,
- resistance: 8850,
- support: 8650,
- open: 8750,
- high: 8780,
- low: 8720,
- volume: 65000
- },
- {
- symbol: 'M',
- 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
- }
+ { 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');
- grid.innerHTML = data.map(item => `
+ if (data.length === 0) {
+ grid.innerHTML = '
+
+
+
+ 分析记录详情
+ +
+
+
+ 暂无数据
';
+ return;
+ }
+
+ grid.innerHTML = data.map(item => {
+ const isWatched = watchedSymbols.includes(item.symbol);
+ return `
-
-
- ${item.name}
- (${item.symbol})
+
+
${item.suggestion}
-
+
${item.symbol.replace(/[0-9]/g, '').substring(0, 2)}
+
+
${item.name}
+ ${item.symbol}
+ ¥${formatNumber(item.price)}
- +${formatNumber(item.change)} (+${item.changePct.toFixed(2)}%)
+ ${item.change >= 0 ? '+' : ''}${formatNumber(item.change)} (${item.changePct >= 0 ? '+' : ''}${item.changePct.toFixed(2)}%)
-
多周期趋势
-
- 5分
- 15分
- 30分
- 60分
+
+
-
+ 成功率
+
-
+ ${item.successRate}%
-
- 交易成功率
-
-
-
-
-
- ${item.successRate}%
-
-
-
趋势评分
-
-
-
-
-
- ${item.trendScore}/100
+
-
+ 趋势评分
+
+ ${item.trendScore}
-
- `).join('');
-}
-
-function getArrow(type) {
- if (type === 'up') return 'up';
- if (type === 'down') return 'down';
- return 'right';
+ `}).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) {
- document.getElementById('total-count').textContent = data.length;
- const upCount = data.filter(d => d.change >= 0).length;
- const downCount = data.length - upCount;
+ const total = data.length;
+ const upCount = data.filter(d => d.change > 0).length;
+ const downCount = data.filter(d => d.change < 0).length;
+ const neutralCount = total - upCount - downCount;
+
+ document.getElementById('total-count').textContent = total;
document.getElementById('up-count').textContent = upCount;
document.getElementById('down-count').textContent = downCount;
+ document.getElementById('neutral-count').textContent = neutralCount;
+ document.getElementById('count-all').textContent = total;
+ document.getElementById('count-watched').textContent = watchedSymbols.length;
}
function filterFuturesList(keyword) {
keyword = keyword.toLowerCase();
- const filtered = allFuturesData.filter(item =>
- item.name.toLowerCase().includes(keyword) ||
- item.symbol.toLowerCase().includes(keyword)
- );
+ 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);
- updateStats(filtered);
}
function filterByCategory(category) {
- if (category === 'all') {
- renderFuturesGrid(allFuturesData);
- updateStats(allFuturesData);
- } else {
- const categoryMap = {
- 'energy': ['SC', 'EC', 'FU', 'LU', 'BU', 'RU', 'NR', 'ZC'],
- 'metal': ['AU', 'AG', 'CU', 'AL', 'ZN', 'NI', 'SN', 'PB', 'SS', 'RB', 'HC', 'I', 'J', 'JM', 'SF', 'SM'],
- 'agriculture': ['M', 'RM', 'C', 'CS', 'A', 'B', 'Y', 'P', 'OI', 'CF', 'SR', 'AP', 'JD', 'LH', 'MA', 'TA', 'EG', 'PP', 'L', 'V', 'SA', 'FG', 'UR', 'SP'],
- 'finance': ['IF', 'IC', 'IH', 'IM', 'T', 'TF', 'TS', 'TL']
- };
- const symbols = categoryMap[category] || [];
- const filtered = allFuturesData.filter(item => {
- const symbolBase = item.symbol.replace(/[0-9]/g, '').toUpperCase();
- return symbols.includes(symbolBase);
- });
- renderFuturesGrid(filtered);
- updateStats(filtered);
- }
+ let filtered = filterDataByCategory(allFuturesData, category);
+ renderFuturesGrid(filtered);
}
function sortFuturesList(sortBy) {
- let sorted = [...allFuturesData];
+ let sorted = [...getCurrentFilteredData()];
switch(sortBy) {
case 'success_rate':
sorted.sort((a, b) => b.successRate - a.successRate);
@@ -404,59 +402,46 @@ async function loadFuturesDetail(symbol) {
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);
- const item = allFuturesData.find(d => d.symbol === symbol);
- if (item) {
- updateDetailView({
- ...item,
- entryPrice: item.price * 0.99,
- targetPrice: item.resistance,
- stopLoss: item.support,
- riskLevel: item.trendScore >= 80 ? '低' : item.trendScore >= 60 ? '中' : '高',
- macd: { signal: '金叉', detail: 'DIF: -0.0147' },
- rsi: { value: 47, status: '正常' },
- boll: { signal: '中轨', detail: '区间: 2086-2215' },
- kdj: { signal: '中性', detail: 'K: 71 D: 87' },
- resistances: [item.resistance, item.resistance * 1.05, item.resistance * 1.1],
- supports: [item.support, item.support * 0.95, item.support * 0.9],
- periodConsistency: {
- '5': item.periods['5'],
- '15': item.periods['15'],
- '30': item.periods['30'],
- '60': item.periods['60']
- },
- suggestionReason: '技术面突破,趋势明确'
- });
- }
}
}
function updateDetailView(data) {
- document.getElementById('detail-price').textContent = '¥' + formatNumber(data.price);
- document.getElementById('detail-price').className = 'current-price ' + (data.change >= 0 ? 'up' : 'down');
+ document.getElementById('detail-name').textContent = data.name || '--';
+ document.getElementById('detail-symbol').textContent = data.symbol || '--';
+
+ const priceEl = document.getElementById('detail-price');
+ priceEl.textContent = '¥' + formatNumber(data.price);
+ priceEl.className = 'price-value ' + (data.change >= 0 ? 'up' : 'down');
const changeEl = document.getElementById('detail-change');
- const changeIcon = data.change >= 0 ? 'up' : 'down';
changeEl.className = 'price-change ' + (data.change >= 0 ? 'up' : 'down');
- changeEl.innerHTML = ` ${data.change >= 0 ? '+' : ''}${formatNumber(data.change)} (${data.changePct >= 0 ? '+' : ''}${data.changePct.toFixed(2)}%)`;
+ changeEl.innerHTML = ` ${data.change >= 0 ? '+' : ''}${formatNumber(data.change)} (${data.changePct >= 0 ? '+' : ''}${data.changePct.toFixed(2)}%)`;
document.getElementById('detail-open').textContent = formatNumber(data.open);
document.getElementById('detail-high').textContent = formatNumber(data.high);
document.getElementById('detail-low').textContent = formatNumber(data.low);
document.getElementById('detail-volume').textContent = formatNumber(data.volume);
- const suggestionBox = document.getElementById('suggestion-box');
- suggestionBox.className = 'suggestion-box ' + data.suggestionType;
- document.getElementById('suggestion-action').textContent = data.suggestion;
- document.getElementById('suggestion-reason').textContent = data.suggestionReason || '';
+ const r1 = data.resistances ? data.resistances[0] : data.resistance;
+ const s1 = data.supports ? data.supports[0] : data.support;
+
+ document.getElementById('detail-r1').textContent = `R1: ${formatNumber(r1)} (${calcPriceChangePercent(data.price, r1)})`;
+ document.getElementById('detail-s1').textContent = `S1: ${formatNumber(s1)} (${calcPriceChangePercent(data.price, s1)})`;
- document.getElementById('entry-price').textContent = formatNumber(data.entryPrice || data.price * 0.99);
- document.getElementById('target-price').textContent = formatNumber(data.targetPrice || data.resistance);
- document.getElementById('stop-loss').textContent = formatNumber(data.stopLoss || data.support);
- document.getElementById('risk-level').textContent = data.riskLevel || '中';
+ const badge = document.getElementById('suggestion-badge');
+ badge.textContent = data.suggestion || '--';
+ badge.className = 'suggestion-badge ' + (data.suggestionType || 'neutral');
+
+ document.getElementById('suggestion-reason').textContent = data.suggestionReason || '--';
+ document.getElementById('entry-price').textContent = formatNumber(data.entryPrice);
+ document.getElementById('target-price').textContent = formatNumber(data.targetPrice);
+ document.getElementById('stop-loss').textContent = formatNumber(data.stopLoss);
+ document.getElementById('risk-level').textContent = data.riskLevel || '--';
if (data.macd) {
document.getElementById('macd-signal').textContent = data.macd.signal;
@@ -478,70 +463,214 @@ function updateDetailView(data) {
if (data.resistances) {
for (let i = 0; i < 3; i++) {
const el = document.getElementById(`resistance-${i + 1}`);
- if (el && data.resistances[i]) {
- el.querySelector('.level-value').textContent = formatNumber(data.resistances[i]);
+ if (el) {
+ el.querySelector('span:last-child').textContent = formatNumber(data.resistances[i]);
}
}
}
if (data.supports) {
for (let i = 0; i < 3; i++) {
const el = document.getElementById(`support-${i + 1}`);
- if (el && data.supports[i]) {
- el.querySelector('.level-value').textContent = formatNumber(data.supports[i]);
+ if (el) {
+ el.querySelector('span:last-child').textContent = formatNumber(data.supports[i]);
}
}
}
if (data.periodConsistency) {
- const container = document.getElementById('period-consistency');
+ const container = document.getElementById('period-trends');
const periodNames = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' };
container.innerHTML = Object.entries(data.periodConsistency).map(([period, trend]) => `
- 关键点位
-
- 压力: ${formatNumber(item.resistance)}
- 支撑: ${formatNumber(item.support)}
-
+
+ 5M
+ 15M
+ 30M
+ 1H
- 查看详情
+
+ 压力 ${formatNumber(item.resistance)}
+ 支撑 ${formatNumber(item.support)}
+
+
+
+ 详情
+
- ${periodNames[period]}
-
-
+
低: ${l} 高: ${h}`; - } else if (p.seriesName === '成交量') { - result += `
成交量: ${p.data}`; - } else if (p.seriesName === 'DIF' || p.seriesName === 'DEA') { - result += `
${p.seriesName}: ${p.data}`; - } else if (p.seriesName === 'MACD') { - result += `
MACD: ${p.data}`; - } else { - result += `
${p.seriesName}: ${p.data}`; - } - }); - return result; - } + 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: '#22c55e' - } + label: { backgroundColor: '#06b6d4' } }, grid: [ - { left: 70, right: 20, top: 60, height: '48%' }, - { left: 70, right: 20, top: '54%', height: '14%' }, - { left: 70, right: 20, top: '73%', height: '17%' } + { 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: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab', fontSize: 10 }, + axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, + axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false } }, { type: 'category', gridIndex: 1, data: dates, - boundaryGap: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, + axisLine: { show: false }, axisLabel: { show: false }, splitLine: { show: false } }, @@ -632,53 +730,46 @@ function renderKlineChart(data) { type: 'category', gridIndex: 2, data: dates, - boundaryGap: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab', fontSize: 10 }, + axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, + axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false } } ], yAxis: [ { scale: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab' }, - splitLine: { lineStyle: { color: '#2a2d3a', type: 'dashed' } } + axisLine: { show: false }, + axisLabel: { color: '#64748b' }, + splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)', type: 'dashed' } } }, { scale: true, gridIndex: 1, axisLine: { show: false }, - axisTick: { show: false }, axisLabel: { show: false }, splitLine: { show: false } }, { scale: true, gridIndex: 2, - axisLine: { lineStyle: { color: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab', fontSize: 10 }, - splitLine: { lineStyle: { color: '#2a2d3a', type: 'dashed' } } + 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 - }, + { type: 'inside', xAxisIndex: [0, 1, 2], start: 50, end: 100 }, { show: true, xAxisIndex: [0, 1, 2], type: 'slider', bottom: 5, - height: 18, + height: 16, borderColor: 'transparent', - backgroundColor: '#1a1d28', - fillerColor: 'rgba(34, 197, 94, 0.15)', - handleStyle: { color: '#22c55e' }, - textStyle: { color: '#9aa0ab' } + backgroundColor: 'rgba(15, 20, 30, 0.5)', + fillerColor: 'rgba(6, 182, 212, 0.15)', + handleStyle: { color: '#06b6d4' }, + textStyle: { color: '#64748b' } } ], series: [ @@ -687,9 +778,9 @@ function renderKlineChart(data) { type: 'candlestick', data: values, itemStyle: { - color: '#22c55e', + color: '#10b981', color0: '#ef4444', - borderColor: '#22c55e', + borderColor: '#10b981', borderColor0: '#ef4444' } }, @@ -721,10 +812,7 @@ function renderKlineChart(data) { yAxisIndex: 1, data: volumes.map(v => ({ value: v[0], - itemStyle: { - color: v[1] >= 0 ? '#22c55e' : '#ef4444', - opacity: 0.6 - } + itemStyle: { color: v[1] >= 0 ? 'rgba(16,185,129,0.5)' : 'rgba(239,68,68,0.5)' } })) }, { @@ -750,22 +838,16 @@ function renderKlineChart(data) { type: 'bar', xAxisIndex: 2, yAxisIndex: 2, - data: macdData.macd.map((val, idx) => ({ + data: macdData.macd.map(val => ({ value: val, - itemStyle: { - color: val >= 0 ? '#22c55e' : '#ef4444', - opacity: 0.7 - } + 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(); - }); + window.addEventListener('resize', () => klineChart && klineChart.resize()); } function calculateMA(data, dayCount) { @@ -784,6 +866,19 @@ function calculateMA(data, dayCount) { 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); @@ -799,7 +894,6 @@ function calculateMACD(data) { } const dea = calcEMA(dif, 9); - const macd = dif.map((d, i) => 2 * (d - (dea[i] || 0))); return { dif, dea, macd }; @@ -812,9 +906,7 @@ function calcEMA(data, period) { if (data.length < period) return result; let sum = 0; - for (let i = 0; i < period; i++) { - sum += data[i]; - } + for (let i = 0; i < period; i++) sum += data[i]; result[period - 1] = sum / period; for (let i = period; i < data.length; i++) {
+ ${periodNames[period]}
+
${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}
`).join('');
}
+
+ if (data.trendScore !== undefined) {
+ document.getElementById('trend-score').textContent = data.trendScore;
+ const circle = document.getElementById('score-fill');
+ const circumference = 2 * Math.PI * 45;
+ const offset = circumference - (data.trendScore / 100) * circumference;
+ circle.style.strokeDasharray = circumference;
+ circle.style.strokeDashoffset = offset;
+ }
}
-async function loadKlineData(symbol, period) {
+async function loadHistoryList(symbol) {
try {
- const response = await fetch(`${API_BASE}/kline/${symbol}?period=${period}`);
+ const response = await fetch(`${API_BASE}/analysis/history/${symbol}?limit=10`);
const data = await response.json();
if (data.success) {
- renderKlineChart(data.data);
+ renderHistoryList(data.data);
}
} catch (error) {
- console.error('加载K线数据失败:', error);
- const mockKline = generateMockKlineData();
- renderKlineChart(mockKline);
+ console.error('加载历史记录失败:', error);
+ document.getElementById('history-list').innerHTML = '暂无历史记录
';
}
}
-function generateMockKlineData() {
- const data = [];
- let basePrice = 2100;
- const now = new Date();
- now.setHours(13, 0, 0, 0);
-
- for (let i = 0; i < 60; i++) {
- const time = new Date(now.getTime() + i * 15 * 60000);
- const timeStr = String(time.getHours()).padStart(2, '0') + ':' + String(time.getMinutes()).padStart(2, '0');
-
- const open = basePrice + (Math.random() - 0.5) * 20;
- const close = open + (Math.random() - 0.45) * 25;
- const high = Math.max(open, close) + Math.random() * 10;
- const low = Math.min(open, close) - Math.random() * 10;
- const volume = Math.floor(Math.random() * 1000 + 200);
-
- data.push([timeStr, open.toFixed(2), close.toFixed(2), low.toFixed(2), high.toFixed(2), volume]);
- basePrice = close;
+function renderHistoryList(records) {
+ const container = document.getElementById('history-list');
+ if (!records || records.length === 0) {
+ container.innerHTML = '暂无历史记录
';
+ return;
}
- return data;
+ container.innerHTML = records.map(record => `
+
+
+ `).join('');
+}
+
+function showSuggestionModal(data) {
+ const body = document.getElementById('suggestion-modal-body');
+ body.innerHTML = `
+
+ ${record.analysis_time ? record.analysis_time.replace('T', ' ').substring(0, 16) : '--'}
+ ${record.suggestion || '--'}
+ 评分: ${record.trend_score || '--'}
+
+
+
+
+ MACD
+ ${record.macd_signal || '--'}
+
+
+ RSI
+ ${record.rsi_value || '--'}
+
+
+
+
+ ${data.suggestion || '--'}
+ ${data.suggestionReason || '--'}
+
+
+ `;
+ document.getElementById('suggestion-modal').classList.add('active');
+}
+
+function showHistoryModal(record) {
+ const body = document.getElementById('history-modal-body');
+ body.innerHTML = `
+
+ 建议入场
+ ${formatNumber(data.entryPrice)}
+
+
+ 目标价位
+ ${formatNumber(data.targetPrice)}
+
+
+ 止损价位
+ ${formatNumber(data.stopLoss)}
+
+
+ 风险等级
+ ${data.riskLevel || '--'}
+
+
+
+ AI交易建议
+
+
+ ${record.suggestion || '--'}
+
+
+
+ 入场
+ ${formatNumber(record.entry_price)}
+
+
+ 目标
+ ${formatNumber(record.target_price)}
+
+
+ 止损
+ ${formatNumber(record.stop_loss)}
+
+
+ 风险
+ ${record.risk_level || '--'}
+
+
+
+ 技术指标
+
+
+
+ MACD
+ ${record.macd_signal || '--'}
+
+
+ RSI
+ ${record.rsi_value || '--'}
+
+
+ BOLL
+ ${record.boll_signal || '--'}
+
+
+ KDJ
+ ${record.kdj_signal || '--'}
+
+
+
+ 关键点位
+
+ ${(record.resistance_levels || []).map((v, i) => `
+
+
+ 压力${i + 1}
+ ${formatNumber(v)}
+
+ `).join('')}
+ ${(record.support_levels || []).map((v, i) => `
+
+ 支撑${i + 1}
+ ${formatNumber(v)}
+
+ `).join('')}
+
+
+ 多周期趋势
+
+ ${Object.entries(record.period_trends || {}).map(([period, trend]) => {
+ const names = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' };
+ return `
+
+ ${names[period] || period}
+ ${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}
+
`;
+ }).join('')}
+
+
+ `;
+ document.getElementById('history-modal').classList.add('active');
+}
+
+async function loadKlineData(symbol, period) {
+ try {
+ const response = await fetch(`${API_BASE}/kline/${symbol}?period=${period}`);
+ const data = await response.json();
+ if (data.success) {
+ renderKlineChart(data.data);
+ }
+ } catch (error) {
+ console.error('加载K线数据失败:', error);
+ }
}
function renderKlineChart(data) {
@@ -564,67 +693,36 @@ function renderKlineChart(data) {
const option = {
backgroundColor: 'transparent',
animation: false,
- legend: {
- data: ['K线', 'MA5', 'MA10', 'MA20', 'DIF', 'DEA', 'MACD'],
- top: 10,
- left: 10,
- textStyle: { color: '#9aa0ab', fontSize: 11 }
- },
tooltip: {
trigger: 'axis',
- axisPointer: {
- type: 'cross',
- crossStyle: { color: '#999' }
- },
- backgroundColor: 'rgba(26, 29, 40, 0.95)',
- borderColor: '#2a2d3a',
- textStyle: { color: '#e8eaed', fontSize: 12 },
- formatter: function(params) {
- if (!params || params.length === 0) return '';
- let result = ` 趋势评分
+
+ ${record.trend_score || '--'}
+ 综合评分
+
+ ${params[0].axisValue}
`;
- params.forEach(p => {
- if (p.seriesName === 'K线' && p.data) {
- const [o, c, l, h] = p.data;
- result += `开: ${o} 收: ${c}低: ${l} 高: ${h}`; - } else if (p.seriesName === '成交量') { - result += `
成交量: ${p.data}`; - } else if (p.seriesName === 'DIF' || p.seriesName === 'DEA') { - result += `
${p.seriesName}: ${p.data}`; - } else if (p.seriesName === 'MACD') { - result += `
MACD: ${p.data}`; - } else { - result += `
${p.seriesName}: ${p.data}`; - } - }); - return result; - } + 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: '#22c55e' - } + label: { backgroundColor: '#06b6d4' } }, grid: [ - { left: 70, right: 20, top: 60, height: '48%' }, - { left: 70, right: 20, top: '54%', height: '14%' }, - { left: 70, right: 20, top: '73%', height: '17%' } + { 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: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab', fontSize: 10 }, + axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, + axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false } }, { type: 'category', gridIndex: 1, data: dates, - boundaryGap: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, + axisLine: { show: false }, axisLabel: { show: false }, splitLine: { show: false } }, @@ -632,53 +730,46 @@ function renderKlineChart(data) { type: 'category', gridIndex: 2, data: dates, - boundaryGap: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab', fontSize: 10 }, + axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, + axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false } } ], yAxis: [ { scale: true, - axisLine: { lineStyle: { color: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab' }, - splitLine: { lineStyle: { color: '#2a2d3a', type: 'dashed' } } + axisLine: { show: false }, + axisLabel: { color: '#64748b' }, + splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)', type: 'dashed' } } }, { scale: true, gridIndex: 1, axisLine: { show: false }, - axisTick: { show: false }, axisLabel: { show: false }, splitLine: { show: false } }, { scale: true, gridIndex: 2, - axisLine: { lineStyle: { color: '#2a2d3a' } }, - axisLabel: { color: '#9aa0ab', fontSize: 10 }, - splitLine: { lineStyle: { color: '#2a2d3a', type: 'dashed' } } + 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 - }, + { type: 'inside', xAxisIndex: [0, 1, 2], start: 50, end: 100 }, { show: true, xAxisIndex: [0, 1, 2], type: 'slider', bottom: 5, - height: 18, + height: 16, borderColor: 'transparent', - backgroundColor: '#1a1d28', - fillerColor: 'rgba(34, 197, 94, 0.15)', - handleStyle: { color: '#22c55e' }, - textStyle: { color: '#9aa0ab' } + backgroundColor: 'rgba(15, 20, 30, 0.5)', + fillerColor: 'rgba(6, 182, 212, 0.15)', + handleStyle: { color: '#06b6d4' }, + textStyle: { color: '#64748b' } } ], series: [ @@ -687,9 +778,9 @@ function renderKlineChart(data) { type: 'candlestick', data: values, itemStyle: { - color: '#22c55e', + color: '#10b981', color0: '#ef4444', - borderColor: '#22c55e', + borderColor: '#10b981', borderColor0: '#ef4444' } }, @@ -721,10 +812,7 @@ function renderKlineChart(data) { yAxisIndex: 1, data: volumes.map(v => ({ value: v[0], - itemStyle: { - color: v[1] >= 0 ? '#22c55e' : '#ef4444', - opacity: 0.6 - } + itemStyle: { color: v[1] >= 0 ? 'rgba(16,185,129,0.5)' : 'rgba(239,68,68,0.5)' } })) }, { @@ -750,22 +838,16 @@ function renderKlineChart(data) { type: 'bar', xAxisIndex: 2, yAxisIndex: 2, - data: macdData.macd.map((val, idx) => ({ + data: macdData.macd.map(val => ({ value: val, - itemStyle: { - color: val >= 0 ? '#22c55e' : '#ef4444', - opacity: 0.7 - } + 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(); - }); + window.addEventListener('resize', () => klineChart && klineChart.resize()); } function calculateMA(data, dayCount) { @@ -784,6 +866,19 @@ function calculateMA(data, dayCount) { 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); @@ -799,7 +894,6 @@ function calculateMACD(data) { } const dea = calcEMA(dif, 9); - const macd = dif.map((d, i) => 2 * (d - (dea[i] || 0))); return { dif, dea, macd }; @@ -812,9 +906,7 @@ function calcEMA(data, period) { if (data.length < period) return result; let sum = 0; - for (let i = 0; i < period; i++) { - sum += data[i]; - } + for (let i = 0; i < period; i++) sum += data[i]; result[period - 1] = sum / period; for (let i = period; i < data.length; i++) {