|
|
|
@ -1216,6 +1216,19 @@ function trCloseAnalysisModal() {
|
|
|
|
// ==================== 交易详情弹窗 ====================
|
|
|
|
// ==================== 交易详情弹窗 ====================
|
|
|
|
let trDetailChart = null;
|
|
|
|
let trDetailChart = null;
|
|
|
|
let trCurrentPair = null;
|
|
|
|
let trCurrentPair = null;
|
|
|
|
|
|
|
|
let trCurrentPeriod = 'daily'; // 当前K线周期
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function trSwitchKlinePeriod(period) {
|
|
|
|
|
|
|
|
trCurrentPeriod = period;
|
|
|
|
|
|
|
|
// 更新标签页样式
|
|
|
|
|
|
|
|
document.querySelectorAll('.tr-kline-tab').forEach(btn => {
|
|
|
|
|
|
|
|
btn.classList.toggle('active', btn.dataset.period === period);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
// 重新加载K线
|
|
|
|
|
|
|
|
if (trCurrentPair) {
|
|
|
|
|
|
|
|
trLoadDetailKline(trCurrentPair);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function trShowTradeDetail(pair) {
|
|
|
|
async function trShowTradeDetail(pair) {
|
|
|
|
trCurrentPair = pair;
|
|
|
|
trCurrentPair = pair;
|
|
|
|
@ -1289,7 +1302,7 @@ async function trShowTradeDetail(pair) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function trLoadDetailKline(pair) {
|
|
|
|
async function trLoadDetailKline(pair) {
|
|
|
|
const period = document.getElementById('tr-detail-period').value;
|
|
|
|
const period = trCurrentPeriod;
|
|
|
|
const chartDom = document.getElementById('tr-detail-kline');
|
|
|
|
const chartDom = document.getElementById('tr-detail-kline');
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[K线加载] 开始加载K线数据', {
|
|
|
|
console.log('[K线加载] 开始加载K线数据', {
|
|
|
|
@ -1301,11 +1314,13 @@ async function trLoadDetailKline(pair) {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// 构建日期范围:开仓日前后各7天
|
|
|
|
// 构建日期范围:开仓日前7天到平仓日后7天,确保交易前后都有K线数据
|
|
|
|
const openDate = pair.open_date ? new Date(pair.open_date) : new Date();
|
|
|
|
const openDate = pair.open_date ? new Date(pair.open_date) : new Date();
|
|
|
|
|
|
|
|
const closeDate = pair.close_date ? new Date(pair.close_date) : openDate;
|
|
|
|
|
|
|
|
|
|
|
|
const startDate = new Date(openDate);
|
|
|
|
const startDate = new Date(openDate);
|
|
|
|
startDate.setDate(startDate.getDate() - 7);
|
|
|
|
startDate.setDate(startDate.getDate() - 7);
|
|
|
|
const endDate = new Date(openDate);
|
|
|
|
const endDate = new Date(closeDate);
|
|
|
|
endDate.setDate(endDate.getDate() + 7);
|
|
|
|
endDate.setDate(endDate.getDate() + 7);
|
|
|
|
|
|
|
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
@ -1367,37 +1382,128 @@ function trRenderDetailKline(data, pair) {
|
|
|
|
const ma10 = calculateMA(candles, 10);
|
|
|
|
const ma10 = calculateMA(candles, 10);
|
|
|
|
const ma20 = calculateMA(candles, 20);
|
|
|
|
const ma20 = calculateMA(candles, 20);
|
|
|
|
|
|
|
|
|
|
|
|
// 构建买卖标记
|
|
|
|
// 构建买卖标记(每根K线柱只保留一对买/卖标记)
|
|
|
|
const buyMarkers = [];
|
|
|
|
const buyMarkers = [];
|
|
|
|
const sellMarkers = [];
|
|
|
|
const sellMarkers = [];
|
|
|
|
const markers = data.trade_markers || [];
|
|
|
|
const markers = data.trade_markers || [];
|
|
|
|
|
|
|
|
const period = data.period || 'daily';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 按K线索引分组,每根K线只保留第一个买和第一个卖
|
|
|
|
|
|
|
|
const buyByIndex = {}; // { candleIndex: marker }
|
|
|
|
|
|
|
|
const sellByIndex = {};
|
|
|
|
|
|
|
|
|
|
|
|
markers.forEach(m => {
|
|
|
|
markers.forEach(m => {
|
|
|
|
const dateIdx = dates.indexOf(m.date);
|
|
|
|
let dateIdx = -1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (period === 'daily') {
|
|
|
|
|
|
|
|
// 日线:按日期匹配
|
|
|
|
|
|
|
|
dateIdx = dates.indexOf(m.date);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// 分钟线:按时间戳匹配(找到最接近的K线)
|
|
|
|
|
|
|
|
const tradeTime = new Date(`${m.date} ${m.time || '00:00:00'}`).getTime();
|
|
|
|
|
|
|
|
let minDiff = Infinity;
|
|
|
|
|
|
|
|
for (let i = 0; i < candles.length; i++) {
|
|
|
|
|
|
|
|
const candleTime = new Date(candles[i][0]).getTime();
|
|
|
|
|
|
|
|
const diff = Math.abs(candleTime - tradeTime);
|
|
|
|
|
|
|
|
if (diff < minDiff) {
|
|
|
|
|
|
|
|
minDiff = diff;
|
|
|
|
|
|
|
|
dateIdx = i;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (dateIdx === -1) return;
|
|
|
|
if (dateIdx === -1) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 按K线索引去重,只保留第一个
|
|
|
|
|
|
|
|
if (m.direction === '买') {
|
|
|
|
|
|
|
|
if (buyByIndex[dateIdx] === undefined) {
|
|
|
|
|
|
|
|
buyByIndex[dateIdx] = m;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if (sellByIndex[dateIdx] === undefined) {
|
|
|
|
|
|
|
|
sellByIndex[dateIdx] = m;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 生成标记数据
|
|
|
|
|
|
|
|
let minMarkerIdx = Infinity;
|
|
|
|
|
|
|
|
let maxMarkerIdx = -Infinity;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(buyByIndex).forEach(([idx, m]) => {
|
|
|
|
|
|
|
|
const dateIdx = parseInt(idx);
|
|
|
|
const candle = candles[dateIdx];
|
|
|
|
const candle = candles[dateIdx];
|
|
|
|
const price = parseFloat(m.price);
|
|
|
|
const price = parseFloat(m.price);
|
|
|
|
const low = parseFloat(candle[3]);
|
|
|
|
const low = parseFloat(candle[3]);
|
|
|
|
const high = parseFloat(candle[4]);
|
|
|
|
const high = parseFloat(candle[4]);
|
|
|
|
const markerPrice = Math.max(low, Math.min(high, price));
|
|
|
|
const markerPrice = Math.max(low, Math.min(high, price));
|
|
|
|
|
|
|
|
|
|
|
|
if (m.direction === '买') {
|
|
|
|
|
|
|
|
buyMarkers.push({
|
|
|
|
buyMarkers.push({
|
|
|
|
name: `买${m.offset || ''}`,
|
|
|
|
name: `买${m.offset || ''}`,
|
|
|
|
coord: [dateIdx, markerPrice],
|
|
|
|
coord: [dateIdx, markerPrice],
|
|
|
|
value: `买${m.offset || ''} ${price}`,
|
|
|
|
value: `买${m.offset || ''} ${price}`,
|
|
|
|
itemStyle: { color: '#34C759' },
|
|
|
|
itemStyle: { color: '#10b981' },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
|
|
minMarkerIdx = Math.min(minMarkerIdx, dateIdx);
|
|
|
|
|
|
|
|
maxMarkerIdx = Math.max(maxMarkerIdx, dateIdx);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(sellByIndex).forEach(([idx, m]) => {
|
|
|
|
|
|
|
|
const dateIdx = parseInt(idx);
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
sellMarkers.push({
|
|
|
|
sellMarkers.push({
|
|
|
|
name: `卖${m.offset || ''}`,
|
|
|
|
name: `卖${m.offset || ''}`,
|
|
|
|
coord: [dateIdx, markerPrice],
|
|
|
|
coord: [dateIdx, markerPrice],
|
|
|
|
value: `卖${m.offset || ''} ${price}`,
|
|
|
|
value: `卖${m.offset || ''} ${price}`,
|
|
|
|
itemStyle: { color: '#FF3B30' },
|
|
|
|
itemStyle: { color: '#ef4444' },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
minMarkerIdx = Math.min(minMarkerIdx, dateIdx);
|
|
|
|
|
|
|
|
maxMarkerIdx = Math.max(maxMarkerIdx, dateIdx);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 计算dataZoom范围:确保至少显示50根K线,并自动定位到买卖点区域
|
|
|
|
|
|
|
|
let zoomStart = 0;
|
|
|
|
|
|
|
|
let zoomEnd = 100;
|
|
|
|
|
|
|
|
const totalLen = candles.length;
|
|
|
|
|
|
|
|
const MIN_CANDLES = 50; // 最少显示50根K线
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (minMarkerIdx !== Infinity && maxMarkerIdx !== -Infinity) {
|
|
|
|
|
|
|
|
const markerRange = maxMarkerIdx - minMarkerIdx;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 计算需要显示的范围:至少50根,或者买卖点范围+前后各20根
|
|
|
|
|
|
|
|
let displayCount = Math.max(MIN_CANDLES, markerRange + 40);
|
|
|
|
|
|
|
|
displayCount = Math.min(displayCount, totalLen); // 不超过总长度
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 以买卖点中心为基准,前后扩展
|
|
|
|
|
|
|
|
const centerIdx = Math.floor((minMarkerIdx + maxMarkerIdx) / 2);
|
|
|
|
|
|
|
|
let startIdx = centerIdx - Math.floor(displayCount / 2);
|
|
|
|
|
|
|
|
let endIdx = startIdx + displayCount - 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 边界调整
|
|
|
|
|
|
|
|
if (startIdx < 0) {
|
|
|
|
|
|
|
|
startIdx = 0;
|
|
|
|
|
|
|
|
endIdx = Math.min(displayCount - 1, totalLen - 1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (endIdx >= totalLen) {
|
|
|
|
|
|
|
|
endIdx = totalLen - 1;
|
|
|
|
|
|
|
|
startIdx = Math.max(0, endIdx - displayCount + 1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
zoomStart = Math.floor((startIdx / totalLen) * 100);
|
|
|
|
|
|
|
|
zoomEnd = Math.ceil(((endIdx + 1) / totalLen) * 100);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// 没有买卖点,显示全部数据
|
|
|
|
|
|
|
|
zoomStart = 0;
|
|
|
|
|
|
|
|
zoomEnd = 100;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
const option = {
|
|
|
|
backgroundColor: 'transparent',
|
|
|
|
backgroundColor: 'transparent',
|
|
|
|
animation: false,
|
|
|
|
animation: false,
|
|
|
|
@ -1513,9 +1619,10 @@ function trRenderDetailKline(data, pair) {
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
],
|
|
|
|
dataZoom: [
|
|
|
|
dataZoom: [
|
|
|
|
{ type: 'inside', xAxisIndex: [0, 1], start: 0, end: 100 },
|
|
|
|
{ type: 'inside', xAxisIndex: [0, 1], start: zoomStart, end: zoomEnd },
|
|
|
|
{
|
|
|
|
{
|
|
|
|
show: true, type: 'slider', xAxisIndex: [0, 1], bottom: 5, height: 16,
|
|
|
|
show: true, type: 'slider', xAxisIndex: [0, 1], bottom: 5, height: 16,
|
|
|
|
|
|
|
|
start: zoomStart, end: zoomEnd,
|
|
|
|
borderColor: 'transparent',
|
|
|
|
borderColor: 'transparent',
|
|
|
|
backgroundColor: 'rgba(15, 20, 30, 0.5)',
|
|
|
|
backgroundColor: 'rgba(15, 20, 30, 0.5)',
|
|
|
|
fillerColor: 'rgba(6, 182, 212, 0.15)',
|
|
|
|
fillerColor: 'rgba(6, 182, 212, 0.15)',
|
|
|
|
|