fix: 修复交易复盘bug,目前依然存在k线图bug

master^2
Lxy 4 weeks ago
parent 1498532e05
commit e2f620c783

@ -255,6 +255,8 @@ def get_kline_with_trades(
from app.database import SessionLocal as MainSessionLocal
from app.models import MarketData
logger.info(f"[K线查询] 请求参数: symbol={symbol}, period={period}, start_date={start_date}, end_date={end_date}")
# 判断是品种代码还是完整合约代码
variety_code = symbol.upper()
if re.match(r'^[A-Za-z]+\d', symbol):
@ -265,29 +267,58 @@ def get_kline_with_trades(
# 品种代码如 AG从配置中查找当前合约
kline_symbol = _resolve_symbol_from_config(variety_code)
logger.info(f"[K线查询] 解析结果: variety_code={variety_code}, kline_symbol={kline_symbol}")
main_db = MainSessionLocal()
try:
# 尝试精确匹配先尝试原始symbol再尝试小写
market_data = main_db.query(MarketData).filter(
MarketData.symbol == kline_symbol,
MarketData.period == period,
).first()
logger.info(f"[K线查询] 精确匹配查询: symbol={kline_symbol}, period={period}, found={market_data is not None}")
if not market_data or not market_data.candles_json:
# 尝试小写匹配
market_data = main_db.query(MarketData).filter(
MarketData.symbol == kline_symbol.lower(),
MarketData.period == period,
).first()
logger.info(f"[K线查询] 小写匹配查询: symbol={kline_symbol.lower()}, period={period}, found={market_data is not None}")
if not market_data or not market_data.candles_json:
# 尝试模糊匹配:查找以该品种代码开头的合约
# 尝试模糊匹配:查找以该品种代码开头的合约(不区分大小写)
market_data = main_db.query(MarketData).filter(
MarketData.symbol.like(f'{variety_code}%'),
MarketData.period == period,
).first()
logger.info(f"[K线查询] 模糊匹配查询: like '{variety_code}%', period={period}, found={market_data is not None}")
if market_data:
logger.info(f"[K线查询] 模糊匹配找到: symbol={market_data.symbol}")
if not market_data or not market_data.candles_json:
# 尝试小写模糊匹配
market_data = main_db.query(MarketData).filter(
MarketData.symbol.like(f'{variety_code.lower()}%'),
MarketData.period == period,
).first()
logger.info(f"[K线查询] 小写模糊匹配查询: like '{variety_code.lower()}%', period={period}, found={market_data is not None}")
if market_data:
logger.info(f"[K线查询] 小写模糊匹配找到: symbol={market_data.symbol}")
if not market_data or not market_data.candles_json:
logger.warning(f"[K线查询] 未找到K线数据: {kline_symbol} {period}")
return {"success": False, "message": f"未找到 {kline_symbol}{period} K线数据"}
candles = json.loads(market_data.candles_json)
kline_symbol = market_data.symbol
logger.info(f"[K线查询] 成功获取K线数据: symbol={kline_symbol}, candles_count={len(candles)}")
finally:
main_db.close()
# 获取该品种的交易记录
logger.info(f"[K线查询] 开始查询交易记录: variety={variety_code}, start_date={start_date}, end_date={end_date}")
query = db.query(TradeRecord).filter(TradeRecord.variety == variety_code)
if start_date:
query = query.filter(TradeRecord.trade_date >= start_date)
@ -295,6 +326,10 @@ def get_kline_with_trades(
query = query.filter(TradeRecord.trade_date <= end_date)
trades = query.order_by(TradeRecord.trade_date, TradeRecord.trade_time).all()
logger.info(f"[K线查询] 交易记录查询完成: 找到 {len(trades)} 条记录")
if len(trades) > 0:
logger.info(f"[K线查询] 交易记录示例: {[{'symbol': t.symbol, 'variety': t.variety, 'date': t.trade_date, 'direction': t.direction} for t in trades[:3]]}")
trade_markers = []
for t in trades:
trade_markers.append({
@ -309,6 +344,8 @@ def get_kline_with_trades(
"commission": t.commission,
})
logger.info(f"[K线查询] 返回数据: symbol={kline_symbol}, period={period}, candles={len(candles)}, markers={len(trade_markers)}")
return {
"success": True,
"data": {

@ -546,6 +546,19 @@
.tr-kline-chart { width: 100%; height: 500px; background: var(--bg-card); border-radius: 16px; box-shadow: var(--shadow-sm); }
.tr-chart-container { width: 100%; height: 300px; margin-bottom: 16px; background: var(--bg-card); border-radius: 16px; box-shadow: var(--shadow-sm); }
/* 交易详情弹窗样式 */
.tr-detail-info { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; padding: 16px; background: #F5F5F7; border-radius: 12px; }
.tr-detail-info-item { display: flex; flex-direction: column; gap: 4px; }
.tr-detail-info-label { font-size: 12px; color: var(--text-tertiary); font-weight: 500; }
.tr-detail-info-value { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.tr-detail-info-value.profit { color: var(--color-down); }
.tr-detail-info-value.loss { color: var(--color-up); }
.tr-detail-section { margin-top: 20px; }
.tr-detail-section-title { font-size: 15px; font-weight: 700; color: var(--text-primary); margin-bottom: 12px; padding-left: 8px; border-left: 3px solid var(--accent-gold); }
.tr-detail-ai-content { padding: 16px; background: #F5F5F7; border-radius: 12px; min-height: 100px; font-size: 14px; line-height: 1.8; color: var(--text-primary); white-space: pre-wrap; }
.tr-pair-symbol-link { cursor: pointer; color: var(--accent-gold); text-decoration: none; font-weight: 700; }
.tr-pair-symbol-link:hover { text-decoration: underline; }
.tr-pairs-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; }
.tr-pair-card { background: var(--bg-card); border-radius: 16px; padding: 18px; box-shadow: var(--shadow-sm); transition: all 0.2s; position: relative; overflow: hidden; }
.tr-pair-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
@ -987,7 +1000,6 @@
<!-- 子导航 -->
<nav class="tr-sub-nav">
<a href="#" class="tr-sub-nav-item active" data-sub="records">交易记录</a>
<a href="#" class="tr-sub-nav-item" data-sub="kline">K线标注</a>
<a href="#" class="tr-sub-nav-item" data-sub="daily">每日分析</a>
<a href="#" class="tr-sub-nav-item" data-sub="variety">品种汇总</a>
<a href="#" class="tr-sub-nav-item" data-sub="pairs">逐笔分析</a>
@ -1017,27 +1029,6 @@
</div>
</div>
<!-- 子页面: K线标注 -->
<div id="tr-sub-kline" class="tr-sub-page">
<div class="tr-kline-controls">
<select id="tr-kline-symbol" class="tr-select">
<option value="">选择品种</option>
</select>
<select id="tr-kline-period" class="tr-select">
<option value="daily">日线</option>
<option value="60min">60分钟</option>
<option value="15min">15分钟</option>
<option value="5min">5分钟</option>
</select>
<button id="tr-btn-load-kline" class="tr-btn tr-btn-primary">加载K线</button>
</div>
<div id="tr-kline-chart" class="tr-kline-chart"></div>
<div id="tr-kline-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📈</div>
<div class="tr-empty-text">请选择品种并加载K线数据</div>
</div>
</div>
<!-- 子页面: 每日分析 -->
<div id="tr-sub-daily" class="tr-sub-page">
<div id="tr-daily-chart" class="tr-chart-container"></div>
@ -1089,6 +1080,41 @@
</div>
</div>
<!-- 交易详情弹窗 -->
<div id="tr-detail-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseDetailModal()">
<div class="rv-modal" style="max-width:1000px;">
<div class="rv-modal-header">
<span class="rv-modal-title" id="tr-detail-title">交易详情</span>
<button class="rv-modal-close" onclick="trCloseDetailModal()"></button>
</div>
<div class="rv-modal-body">
<!-- 交易信息 -->
<div id="tr-detail-info" class="tr-detail-info"></div>
<!-- K线图 -->
<div class="tr-detail-section">
<h4 class="tr-detail-section-title">K线走势与买卖点</h4>
<div class="tr-kline-controls" style="margin-bottom:12px;">
<select id="tr-detail-period" class="tr-select">
<option value="daily">日线</option>
<option value="60min">60分钟</option>
<option value="15min">15分钟</option>
<option value="5min">5分钟</option>
</select>
<button id="tr-detail-reload-kline" class="tr-btn tr-btn-secondary">刷新K线</button>
</div>
<div id="tr-detail-kline" style="width:100%;height:400px;"></div>
</div>
<!-- AI分析 -->
<div class="tr-detail-section">
<h4 class="tr-detail-section-title">AI 交易分析</h4>
<div id="tr-detail-ai-content" class="tr-detail-ai-content">
<button id="tr-detail-ai-btn" class="tr-btn tr-btn-primary">开始AI分析</button>
</div>
</div>
</div>
</div>
</div>
<!-- 批次管理弹窗 -->
<div id="tr-batch-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseBatchModal()">
<div class="rv-modal" style="max-width:600px;">

@ -631,7 +631,6 @@ function renderFuturesGrid(data) {
const TR_API_BASE = '/api/v1/trade-review';
let trCurrentPage = 1;
let trKlineChart = null;
let trInitialized = false;
function initTradeReview() {
@ -665,14 +664,9 @@ function initTradeReview() {
const sub = this.dataset.sub;
document.querySelectorAll('.tr-sub-page').forEach(p => p.classList.remove('active'));
document.getElementById('tr-sub-' + sub).classList.add('active');
// 延迟加载
if (sub === 'kline') trLoadKlineSymbolOptions();
});
});
// K线加载
document.getElementById('tr-btn-load-kline').addEventListener('click', trLoadKlineWithTrades);
// 初始加载:先获取最后交易日,设为默认日期,再加载数据
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() {
@ -1247,10 +1073,11 @@ function trRenderTradePairs(pairs) {
const pnlClass = p.net_pnl >= 0 ? 'profit' : 'loss';
const dirText = p.direction === '多' ? '做多' : '做空';
const dirClass = p.direction === '多' ? 'tr-dir-buy' : 'tr-dir-sell';
const pairData = JSON.stringify(p).replace(/"/g, '&quot;');
return `
<div class="tr-pair-card ${pnlClass}">
<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>
</div>
<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>
<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>`;
}).join('');
@ -1307,6 +1134,300 @@ function trCloseAnalysisModal() {
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) {
if (!e.target.classList.contains('tr-sub-nav-item')) return;

@ -1,32 +0,0 @@
import sqlite3
import os
db_path = 'data/futures_analysis.db'
print(f'数据库文件路径: {os.path.abspath(db_path)}')
print(f'数据库存在: {os.path.exists(db_path)}')
if os.path.exists(db_path):
print(f'数据库大小: {os.path.getsize(db_path)} 字节')
conn = sqlite3.connect(db_path)
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = [row[0] for row in cursor.fetchall()]
print(f'数据库表列表: {tables}')
if 'ai_analysis_cache' in tables:
cursor = conn.execute("SELECT COUNT(*) FROM ai_analysis_cache")
count = cursor.fetchone()[0]
print(f'AI分析记录数: {count}')
cursor = conn.execute("SELECT id, symbol, created_at FROM ai_analysis_cache ORDER BY created_at DESC LIMIT 5")
records = cursor.fetchall()
print(f'最近5条记录:')
for r in records:
print(f' ID: {r[0]}, 合约: {r[1]}, 时间: {r[2]}')
else:
print('❌ ai_analysis_cache 表不存在!')
conn.close()
else:
print('❌ 数据库文件不存在!')
Loading…
Cancel
Save