|
|
|
@ -631,7 +631,6 @@ function renderFuturesGrid(data) {
|
|
|
|
|
|
|
|
|
|
|
|
const TR_API_BASE = '/api/v1/trade-review';
|
|
|
|
const TR_API_BASE = '/api/v1/trade-review';
|
|
|
|
let trCurrentPage = 1;
|
|
|
|
let trCurrentPage = 1;
|
|
|
|
let trKlineChart = null;
|
|
|
|
|
|
|
|
let trInitialized = false;
|
|
|
|
let trInitialized = false;
|
|
|
|
|
|
|
|
|
|
|
|
function initTradeReview() {
|
|
|
|
function initTradeReview() {
|
|
|
|
@ -665,14 +664,9 @@ function initTradeReview() {
|
|
|
|
const sub = this.dataset.sub;
|
|
|
|
const sub = this.dataset.sub;
|
|
|
|
document.querySelectorAll('.tr-sub-page').forEach(p => p.classList.remove('active'));
|
|
|
|
document.querySelectorAll('.tr-sub-page').forEach(p => p.classList.remove('active'));
|
|
|
|
document.getElementById('tr-sub-' + sub).classList.add('active');
|
|
|
|
document.getElementById('tr-sub-' + sub).classList.add('active');
|
|
|
|
// 延迟加载
|
|
|
|
|
|
|
|
if (sub === 'kline') trLoadKlineSymbolOptions();
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// K线加载
|
|
|
|
|
|
|
|
document.getElementById('tr-btn-load-kline').addEventListener('click', trLoadKlineWithTrades);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 初始加载:先获取最后交易日,设为默认日期,再加载数据
|
|
|
|
// 初始加载:先获取最后交易日,设为默认日期,再加载数据
|
|
|
|
trInitDefaultDate();
|
|
|
|
trInitDefaultDate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -947,174 +941,6 @@ async function trDeleteByDate() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== K线标注 ====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function trLoadKlineSymbolOptions() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 从品种汇总获取已交易的品种代码
|
|
|
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`);
|
|
|
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
|
|
|
if (!json.success) return;
|
|
|
|
|
|
|
|
const select = document.getElementById('tr-kline-symbol');
|
|
|
|
|
|
|
|
const current = select.value;
|
|
|
|
|
|
|
|
select.innerHTML = '<option value="">选择品种</option>';
|
|
|
|
|
|
|
|
// 用已交易的品种代码,K线API会按variety匹配交易记录
|
|
|
|
|
|
|
|
json.data.forEach(v => {
|
|
|
|
|
|
|
|
const opt = document.createElement('option');
|
|
|
|
|
|
|
|
opt.value = v.variety;
|
|
|
|
|
|
|
|
opt.textContent = `${v.symbol_name || v.variety} (${v.variety})`;
|
|
|
|
|
|
|
|
select.appendChild(opt);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (current) select.value = current;
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
console.error('加载品种选项失败', e);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function trLoadKlineWithTrades() {
|
|
|
|
|
|
|
|
const symbol = document.getElementById('tr-kline-symbol').value;
|
|
|
|
|
|
|
|
const period = document.getElementById('tr-kline-period').value;
|
|
|
|
|
|
|
|
if (!symbol) { alert('请选择品种'); return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const chartDom = document.getElementById('tr-kline-chart');
|
|
|
|
|
|
|
|
const emptyEl = document.getElementById('tr-kline-empty');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const params = trBuildParams({ period });
|
|
|
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/kline-with-trades/${symbol}?${params}`);
|
|
|
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!json.success || !json.data.candles || json.data.candles.length === 0) {
|
|
|
|
|
|
|
|
chartDom.style.display = 'none';
|
|
|
|
|
|
|
|
emptyEl.style.display = 'flex';
|
|
|
|
|
|
|
|
if (trKlineChart) { trKlineChart.dispose(); trKlineChart = null; }
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chartDom.style.display = '';
|
|
|
|
|
|
|
|
emptyEl.style.display = 'none';
|
|
|
|
|
|
|
|
trRenderKlineWithMarkers(json.data);
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
console.error('加载K线失败', e);
|
|
|
|
|
|
|
|
alert('加载K线数据失败');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function trRenderKlineWithMarkers(data) {
|
|
|
|
|
|
|
|
if (trKlineChart) trKlineChart.dispose();
|
|
|
|
|
|
|
|
const chartDom = document.getElementById('tr-kline-chart');
|
|
|
|
|
|
|
|
trKlineChart = echarts.init(chartDom, 'dark');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const candles = data.candles;
|
|
|
|
|
|
|
|
const dates = candles.map(c => c[0]);
|
|
|
|
|
|
|
|
const values = candles.map(c => [parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3]), parseFloat(c[4])]);
|
|
|
|
|
|
|
|
const volumes = candles.map(c => [parseInt(c[5]), parseFloat(c[2]) >= parseFloat(c[1]) ? 1 : -1]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 构建买卖标记
|
|
|
|
|
|
|
|
const buyMarkers = [];
|
|
|
|
|
|
|
|
const sellMarkers = [];
|
|
|
|
|
|
|
|
const markers = data.trade_markers || [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
markers.forEach(m => {
|
|
|
|
|
|
|
|
const dateIdx = dates.indexOf(m.date);
|
|
|
|
|
|
|
|
if (dateIdx === -1) return;
|
|
|
|
|
|
|
|
const candle = candles[dateIdx];
|
|
|
|
|
|
|
|
const price = parseFloat(m.price);
|
|
|
|
|
|
|
|
const low = parseFloat(candle[3]);
|
|
|
|
|
|
|
|
const high = parseFloat(candle[4]);
|
|
|
|
|
|
|
|
// 确保标记价格在K线范围内
|
|
|
|
|
|
|
|
const markerPrice = Math.max(low, Math.min(high, price));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (m.direction === '买') {
|
|
|
|
|
|
|
|
buyMarkers.push({
|
|
|
|
|
|
|
|
name: `买${m.offset || ''}`,
|
|
|
|
|
|
|
|
coord: [dateIdx, markerPrice],
|
|
|
|
|
|
|
|
value: `买${m.offset || ''} ${price}`,
|
|
|
|
|
|
|
|
itemStyle: { color: '#34C759' },
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
sellMarkers.push({
|
|
|
|
|
|
|
|
name: `卖${m.offset || ''}`,
|
|
|
|
|
|
|
|
coord: [dateIdx, markerPrice],
|
|
|
|
|
|
|
|
value: `卖${m.offset || ''} ${price}`,
|
|
|
|
|
|
|
|
itemStyle: { color: '#FF3B30' },
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 },
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
legend: { data: ['K线'], top: 10, textStyle: { color: '#94a3b8' } },
|
|
|
|
|
|
|
|
grid: [
|
|
|
|
|
|
|
|
{ left: 70, right: 20, top: 40, height: '55%' },
|
|
|
|
|
|
|
|
{ left: 70, right: 20, top: '62%', height: '14%' },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
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 } },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
yAxis: [
|
|
|
|
|
|
|
|
{ scale: true, splitArea: { show: false }, axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, axisLabel: { color: '#64748b' } },
|
|
|
|
|
|
|
|
{ scale: true, gridIndex: 1, splitNumber: 2, axisLabel: { show: false }, axisLine: { show: false }, splitLine: { show: false } },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
dataZoom: [
|
|
|
|
|
|
|
|
{ type: 'inside', xAxisIndex: [0, 1], start: candles.length > 60 ? 60 : 0, end: 100 },
|
|
|
|
|
|
|
|
{ show: true, xAxisIndex: [0, 1], type: 'slider', bottom: 10, height: 16, start: candles.length > 60 ? 60 : 0, end: 100 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
series: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
name: 'K线',
|
|
|
|
|
|
|
|
type: 'candlestick',
|
|
|
|
|
|
|
|
data: values,
|
|
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
|
|
color: '#34C759', color0: '#FF3B30',
|
|
|
|
|
|
|
|
borderColor: '#34C759', borderColor0: '#FF3B30',
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
markPoint: {
|
|
|
|
|
|
|
|
symbol: 'pin',
|
|
|
|
|
|
|
|
symbolSize: 40,
|
|
|
|
|
|
|
|
data: [
|
|
|
|
|
|
|
|
...buyMarkers.map(m => ({
|
|
|
|
|
|
|
|
...m,
|
|
|
|
|
|
|
|
symbol: 'triangle',
|
|
|
|
|
|
|
|
symbolSize: 14,
|
|
|
|
|
|
|
|
symbolRotate: 0,
|
|
|
|
|
|
|
|
label: { show: true, formatter: 'B', fontSize: 9, color: '#fff' },
|
|
|
|
|
|
|
|
})),
|
|
|
|
|
|
|
|
...sellMarkers.map(m => ({
|
|
|
|
|
|
|
|
...m,
|
|
|
|
|
|
|
|
symbol: 'triangle',
|
|
|
|
|
|
|
|
symbolSize: 14,
|
|
|
|
|
|
|
|
symbolRotate: 180,
|
|
|
|
|
|
|
|
label: { show: true, formatter: 'S', fontSize: 9, color: '#fff' },
|
|
|
|
|
|
|
|
})),
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
name: '成交量',
|
|
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
|
|
xAxisIndex: 1,
|
|
|
|
|
|
|
|
yAxisIndex: 1,
|
|
|
|
|
|
|
|
data: volumes.map(v => ({
|
|
|
|
|
|
|
|
value: v[0],
|
|
|
|
|
|
|
|
itemStyle: { color: v[1] > 0 ? 'rgba(52,199,89,0.5)' : 'rgba(255,59,48,0.5)' },
|
|
|
|
|
|
|
|
})),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trKlineChart.setOption(option);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 每日分析 ====================
|
|
|
|
// ==================== 每日分析 ====================
|
|
|
|
|
|
|
|
|
|
|
|
async function trLoadDailySummary() {
|
|
|
|
async function trLoadDailySummary() {
|
|
|
|
@ -1247,10 +1073,11 @@ function trRenderTradePairs(pairs) {
|
|
|
|
const pnlClass = p.net_pnl >= 0 ? 'profit' : 'loss';
|
|
|
|
const pnlClass = p.net_pnl >= 0 ? 'profit' : 'loss';
|
|
|
|
const dirText = p.direction === '多' ? '做多' : '做空';
|
|
|
|
const dirText = p.direction === '多' ? '做多' : '做空';
|
|
|
|
const dirClass = p.direction === '多' ? 'tr-dir-buy' : 'tr-dir-sell';
|
|
|
|
const dirClass = p.direction === '多' ? 'tr-dir-buy' : 'tr-dir-sell';
|
|
|
|
|
|
|
|
const pairData = JSON.stringify(p).replace(/"/g, '"');
|
|
|
|
return `
|
|
|
|
return `
|
|
|
|
<div class="tr-pair-card ${pnlClass}">
|
|
|
|
<div class="tr-pair-card ${pnlClass}">
|
|
|
|
<div class="tr-pair-header">
|
|
|
|
<div class="tr-pair-header">
|
|
|
|
<span class="tr-pair-symbol">${p.symbol_name || p.symbol} <span class="${dirClass}" style="font-size:12px;">${dirText}</span></span>
|
|
|
|
<span class="tr-pair-symbol-link" onclick='trShowTradeDetail(${pairData})'>${p.symbol_name || p.symbol} <span class="${dirClass}" style="font-size:12px;">${dirText}</span></span>
|
|
|
|
<span class="tr-pair-pnl ${pnlClass}">${p.net_pnl >= 0 ? '+' : ''}${p.net_pnl}</span>
|
|
|
|
<span class="tr-pair-pnl ${pnlClass}">${p.net_pnl >= 0 ? '+' : ''}${p.net_pnl}</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="tr-pair-info">
|
|
|
|
<div class="tr-pair-info">
|
|
|
|
@ -1262,7 +1089,7 @@ function trRenderTradePairs(pairs) {
|
|
|
|
<div class="tr-pair-info-item"><span class="tr-pair-info-label">手续费</span><span class="tr-pair-info-value">${p.commission}</span></div>
|
|
|
|
<div class="tr-pair-info-item"><span class="tr-pair-info-label">手续费</span><span class="tr-pair-info-value">${p.commission}</span></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="tr-pair-actions">
|
|
|
|
<div class="tr-pair-actions">
|
|
|
|
<button class="tr-pair-btn-analyze" onclick='trAnalyzeTrade(${JSON.stringify(p)})'>AI 分析</button>
|
|
|
|
<button class="tr-pair-btn-analyze" onclick='trShowTradeDetail(${pairData})'>查看详情</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>`;
|
|
|
|
</div>`;
|
|
|
|
}).join('');
|
|
|
|
}).join('');
|
|
|
|
@ -1307,6 +1134,300 @@ function trCloseAnalysisModal() {
|
|
|
|
document.getElementById('tr-analysis-modal').classList.remove('show');
|
|
|
|
document.getElementById('tr-analysis-modal').classList.remove('show');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 交易详情弹窗 ====================
|
|
|
|
|
|
|
|
let trDetailChart = null;
|
|
|
|
|
|
|
|
let trCurrentPair = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function trShowTradeDetail(pair) {
|
|
|
|
|
|
|
|
trCurrentPair = pair;
|
|
|
|
|
|
|
|
const modal = document.getElementById('tr-detail-modal');
|
|
|
|
|
|
|
|
const title = document.getElementById('tr-detail-title');
|
|
|
|
|
|
|
|
const info = document.getElementById('tr-detail-info');
|
|
|
|
|
|
|
|
const aiContent = document.getElementById('tr-detail-ai-content');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
title.textContent = `${pair.symbol_name || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 交易详情`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染交易信息
|
|
|
|
|
|
|
|
const pnlClass = pair.net_pnl >= 0 ? 'profit' : 'loss';
|
|
|
|
|
|
|
|
info.innerHTML = `
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">品种</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.symbol_name || pair.symbol}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">方向</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.direction === '多' ? '做多' : '做空'}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">开仓日期</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.open_date || '-'}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">开仓时间</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.open_time || '-'}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">开仓价</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.open_price != null ? pair.open_price.toFixed(2) : '-'}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">平仓日期</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.close_date || '-'}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">平仓时间</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.close_time || '-'}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">平仓价</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.close_price != null ? pair.close_price.toFixed(2) : '-'}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">手数</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.volume || '-'}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">手续费</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value">${pair.commission || '-'}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="tr-detail-info-item">
|
|
|
|
|
|
|
|
<span class="tr-detail-info-label">净盈亏</span>
|
|
|
|
|
|
|
|
<span class="tr-detail-info-value ${pnlClass}">${pair.net_pnl >= 0 ? '+' : ''}${pair.net_pnl || 0}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 重置AI分析区域
|
|
|
|
|
|
|
|
aiContent.innerHTML = `<button id="tr-detail-ai-btn" class="tr-btn tr-btn-primary">开始AI分析</button>`;
|
|
|
|
|
|
|
|
document.getElementById('tr-detail-ai-btn').addEventListener('click', () => trAnalyzeInDetail(pair));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modal.classList.add('show');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定刷新K线按钮事件
|
|
|
|
|
|
|
|
document.getElementById('tr-detail-reload-kline').onclick = () => trLoadDetailKline(pair);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 加载K线
|
|
|
|
|
|
|
|
await trLoadDetailKline(pair);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function trLoadDetailKline(pair) {
|
|
|
|
|
|
|
|
const period = document.getElementById('tr-detail-period').value;
|
|
|
|
|
|
|
|
const chartDom = document.getElementById('tr-detail-kline');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[K线加载] 开始加载K线数据', {
|
|
|
|
|
|
|
|
symbol: pair.symbol,
|
|
|
|
|
|
|
|
variety: pair.variety,
|
|
|
|
|
|
|
|
period: period,
|
|
|
|
|
|
|
|
open_date: pair.open_date,
|
|
|
|
|
|
|
|
close_date: pair.close_date,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 构建日期范围:开仓日前后各7天
|
|
|
|
|
|
|
|
const openDate = pair.open_date ? new Date(pair.open_date) : new Date();
|
|
|
|
|
|
|
|
const startDate = new Date(openDate);
|
|
|
|
|
|
|
|
startDate.setDate(startDate.getDate() - 7);
|
|
|
|
|
|
|
|
const endDate = new Date(openDate);
|
|
|
|
|
|
|
|
endDate.setDate(endDate.getDate() + 7);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
|
|
|
start_date: startDate.toISOString().slice(0, 10),
|
|
|
|
|
|
|
|
end_date: endDate.toISOString().slice(0, 10),
|
|
|
|
|
|
|
|
period: period,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const url = `${TR_API_BASE}/kline-with-trades/${pair.symbol}?${params}`;
|
|
|
|
|
|
|
|
console.log('[K线加载] 请求URL:', url);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const res = await fetch(url);
|
|
|
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[K线加载] API响应:', {
|
|
|
|
|
|
|
|
success: json.success,
|
|
|
|
|
|
|
|
message: json.message,
|
|
|
|
|
|
|
|
hasData: !!json.data,
|
|
|
|
|
|
|
|
symbol: json.data?.symbol,
|
|
|
|
|
|
|
|
candlesCount: json.data?.candles?.length || 0,
|
|
|
|
|
|
|
|
markersCount: json.data?.trade_markers?.length || 0,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!json.success) {
|
|
|
|
|
|
|
|
console.warn('[K线加载] API返回失败:', json.message);
|
|
|
|
|
|
|
|
chartDom.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-tertiary);">暂无K线数据: ${json.message || '未知错误'}</div>`;
|
|
|
|
|
|
|
|
if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; }
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!json.data.candles || json.data.candles.length === 0) {
|
|
|
|
|
|
|
|
console.warn('[K线加载] K线数据为空');
|
|
|
|
|
|
|
|
chartDom.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-tertiary);">暂无K线数据</div>';
|
|
|
|
|
|
|
|
if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; }
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[K线加载] 开始渲染K线图');
|
|
|
|
|
|
|
|
trRenderDetailKline(json.data, pair);
|
|
|
|
|
|
|
|
console.log('[K线加载] K线图渲染完成');
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
console.error('[K线加载] 加载K线失败:', e);
|
|
|
|
|
|
|
|
chartDom.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-tertiary);">加载K线失败: ${e.message}</div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function trRenderDetailKline(data, pair) {
|
|
|
|
|
|
|
|
if (trDetailChart) trDetailChart.dispose();
|
|
|
|
|
|
|
|
const chartDom = document.getElementById('tr-detail-kline');
|
|
|
|
|
|
|
|
trDetailChart = echarts.init(chartDom, 'dark');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const candles = data.candles;
|
|
|
|
|
|
|
|
const dates = candles.map(c => c[0]);
|
|
|
|
|
|
|
|
const values = candles.map(c => [parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3]), parseFloat(c[4])]);
|
|
|
|
|
|
|
|
const volumes = candles.map(c => [parseInt(c[5]), parseFloat(c[2]) >= parseFloat(c[1]) ? 1 : -1]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 构建买卖标记
|
|
|
|
|
|
|
|
const buyMarkers = [];
|
|
|
|
|
|
|
|
const sellMarkers = [];
|
|
|
|
|
|
|
|
const markers = data.trade_markers || [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
markers.forEach(m => {
|
|
|
|
|
|
|
|
const dateIdx = dates.indexOf(m.date);
|
|
|
|
|
|
|
|
if (dateIdx === -1) return;
|
|
|
|
|
|
|
|
const candle = candles[dateIdx];
|
|
|
|
|
|
|
|
const price = parseFloat(m.price);
|
|
|
|
|
|
|
|
const low = parseFloat(candle[3]);
|
|
|
|
|
|
|
|
const high = parseFloat(candle[4]);
|
|
|
|
|
|
|
|
const markerPrice = Math.max(low, Math.min(high, price));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (m.direction === '买') {
|
|
|
|
|
|
|
|
buyMarkers.push({
|
|
|
|
|
|
|
|
name: `买${m.offset || ''}`,
|
|
|
|
|
|
|
|
coord: [dateIdx, markerPrice],
|
|
|
|
|
|
|
|
value: `买${m.offset || ''} ${price}`,
|
|
|
|
|
|
|
|
itemStyle: { color: '#34C759' },
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
sellMarkers.push({
|
|
|
|
|
|
|
|
name: `卖${m.offset || ''}`,
|
|
|
|
|
|
|
|
coord: [dateIdx, markerPrice],
|
|
|
|
|
|
|
|
value: `卖${m.offset || ''} ${price}`,
|
|
|
|
|
|
|
|
itemStyle: { color: '#FF3B30' },
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
|
|
backgroundColor: 'transparent',
|
|
|
|
|
|
|
|
animation: false,
|
|
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
|
|
|
axisPointer: { type: 'cross' },
|
|
|
|
|
|
|
|
backgroundColor: 'rgba(10, 15, 25, 0.95)',
|
|
|
|
|
|
|
|
borderColor: 'rgba(255,255,255,0.1)',
|
|
|
|
|
|
|
|
textStyle: { color: '#E5E5E7', fontSize: 12 },
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
grid: [
|
|
|
|
|
|
|
|
{ left: '8%', right: '3%', top: '5%', height: '55%' },
|
|
|
|
|
|
|
|
{ left: '8%', right: '3%', top: '68%', height: '22%' },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
xAxis: [
|
|
|
|
|
|
|
|
{ type: 'category', data: dates, gridIndex: 0, axisLine: { lineStyle: { color: '#333' } }, axisLabel: { show: false } },
|
|
|
|
|
|
|
|
{ type: 'category', data: dates, gridIndex: 1, axisLine: { lineStyle: { color: '#333' } }, axisLabel: { color: '#999', fontSize: 10 } },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
yAxis: [
|
|
|
|
|
|
|
|
{ scale: true, gridIndex: 0, splitLine: { lineStyle: { color: '#1a1a1a' } }, axisLabel: { color: '#999' } },
|
|
|
|
|
|
|
|
{ scale: true, gridIndex: 1, splitLine: { show: false }, axisLabel: { show: false } },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
series: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
name: 'K线',
|
|
|
|
|
|
|
|
type: 'candlestick',
|
|
|
|
|
|
|
|
data: values,
|
|
|
|
|
|
|
|
xAxisIndex: 0, yAxisIndex: 0,
|
|
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
|
|
color: '#FF3B30', color0: '#34C759',
|
|
|
|
|
|
|
|
borderColor: '#FF3B30', borderColor0: '#34C759',
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
markPoint: {
|
|
|
|
|
|
|
|
symbol: 'triangle',
|
|
|
|
|
|
|
|
symbolSize: 12,
|
|
|
|
|
|
|
|
data: buyMarkers,
|
|
|
|
|
|
|
|
label: { show: true, formatter: '{b}', fontSize: 10, color: '#34C759' },
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
name: 'K线',
|
|
|
|
|
|
|
|
type: 'candlestick',
|
|
|
|
|
|
|
|
data: values,
|
|
|
|
|
|
|
|
xAxisIndex: 0, yAxisIndex: 0,
|
|
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
|
|
color: '#FF3B30', color0: '#34C759',
|
|
|
|
|
|
|
|
borderColor: '#FF3B30', borderColor0: '#34C759',
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
markPoint: {
|
|
|
|
|
|
|
|
symbol: 'triangle',
|
|
|
|
|
|
|
|
symbolSize: 12,
|
|
|
|
|
|
|
|
symbolRotate: 180,
|
|
|
|
|
|
|
|
data: sellMarkers,
|
|
|
|
|
|
|
|
label: { show: true, formatter: '{b}', fontSize: 10, color: '#FF3B30' },
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
name: '成交量',
|
|
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
|
|
xAxisIndex: 1, yAxisIndex: 1,
|
|
|
|
|
|
|
|
data: volumes.map(v => ({
|
|
|
|
|
|
|
|
value: v[0],
|
|
|
|
|
|
|
|
itemStyle: { color: v[1] > 0 ? 'rgba(255,59,48,0.5)' : 'rgba(52,199,89,0.5)' }
|
|
|
|
|
|
|
|
})),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
dataZoom: [
|
|
|
|
|
|
|
|
{ type: 'inside', xAxisIndex: [0, 1], start: 0, end: 100 },
|
|
|
|
|
|
|
|
{ type: 'slider', xAxisIndex: [0, 1], bottom: '2%', height: 16, borderColor: '#333', fillerColor: 'rgba(212,167,71,0.15)' },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trDetailChart.setOption(option);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function trCloseDetailModal() {
|
|
|
|
|
|
|
|
document.getElementById('tr-detail-modal').classList.remove('show');
|
|
|
|
|
|
|
|
if (trDetailChart) {
|
|
|
|
|
|
|
|
trDetailChart.dispose();
|
|
|
|
|
|
|
|
trDetailChart = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
trCurrentPair = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function trAnalyzeInDetail(pair) {
|
|
|
|
|
|
|
|
const aiContent = document.getElementById('tr-detail-ai-content');
|
|
|
|
|
|
|
|
aiContent.innerHTML = '<div style="color:var(--text-tertiary);">正在分析中,请稍候...</div>';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/analyze-trade`, {
|
|
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
|
|
symbol: pair.symbol,
|
|
|
|
|
|
|
|
open_date: pair.open_date,
|
|
|
|
|
|
|
|
open_time: pair.open_time,
|
|
|
|
|
|
|
|
close_date: pair.close_date,
|
|
|
|
|
|
|
|
close_time: pair.close_time,
|
|
|
|
|
|
|
|
direction: pair.direction,
|
|
|
|
|
|
|
|
open_price: pair.open_price,
|
|
|
|
|
|
|
|
close_price: pair.close_price,
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
|
|
|
if (json.success) {
|
|
|
|
|
|
|
|
aiContent.innerHTML = `<div style="white-space:pre-wrap;">${json.data.analysis}</div>`;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
aiContent.innerHTML = `<div style="color:var(--color-up);">${json.message || '分析失败'}</div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
aiContent.innerHTML = `<div style="color:var(--color-up);">分析请求失败: ${e.message}</div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 子页面切换时懒加载
|
|
|
|
// 子页面切换时懒加载
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
if (!e.target.classList.contains('tr-sub-nav-item')) return;
|
|
|
|
if (!e.target.classList.contains('tr-sub-nav-item')) return;
|
|
|
|
|