diff --git a/app/api/trade_review.py b/app/api/trade_review.py index 2c91d25..aef9c13 100644 --- a/app/api/trade_review.py +++ b/app/api/trade_review.py @@ -427,9 +427,11 @@ def get_kline_with_trades( candles = json.loads(market_data.candles_json) # 将字典格式K线转换为前端期望的数组格式 [date, open, close, low, high, volume] if candles and isinstance(candles[0], dict): + # 根据周期决定是否保留时间部分 + is_daily = period == 'daily' candles = [ [ - c.get("time", "")[:10], # 截取日期部分 YYYY-MM-DD + c.get("time", "")[:10] if is_daily else c.get("time", ""), # 日线只保留日期,分钟线保留完整时间 c.get("open", 0), c.get("close", 0), c.get("low", 0), @@ -517,8 +519,6 @@ async def analyze_trade( db: Session = Depends(get_analysis_db), ): """AI 分析单笔交易(结合多周期K线数据)""" - from app.services.ai_analysis import AIAnalysisService - # 收集多周期K线数据 from app.database import SessionLocal as MainSessionLocal from app.models import MarketData @@ -578,13 +578,14 @@ async def analyze_trade( """ try: - # 使用 AIAnalysisService 的模型配置和调用能力 - service = AIAnalysisService.__new__(AIAnalysisService) - model = service.get_active_model() + # 使用 AIFuturesAnalyzer 的模型配置和调用能力 + from app.services.ai_analysis import AIFuturesAnalyzer + analyzer = AIFuturesAnalyzer(db) + model = analyzer.get_active_model() if not model: return {"success": False, "message": "未配置AI模型或模型未激活,请先在AI配置页面设置"} - response = service.call_ai_model(prompt, model) + response = analyzer.call_ai_model(prompt, model) if not response: return {"success": False, "message": "AI模型返回空响应,请稍后重试"} diff --git a/app/static/futures_analysis.html b/app/static/futures_analysis.html index 590a689..fc920f4 100644 --- a/app/static/futures_analysis.html +++ b/app/static/futures_analysis.html @@ -504,6 +504,9 @@ .tr-date-input { background: #fff; border: 1px solid rgba(0,0,0,0.05); border-radius: 12px; padding: 0 14px; height: 40px; font-size: 13px; color: var(--text-primary); box-shadow: var(--shadow-sm); outline: none; font-family: inherit; } .tr-date-input:focus { border-color: var(--color-brand); box-shadow: 0 0 0 3px rgba(0,122,255,0.15); } .tr-select { background: #fff; border: 1px solid rgba(0,0,0,0.05); border-radius: 12px; padding: 0 14px; height: 40px; font-size: 13px; color: var(--text-primary); box-shadow: var(--shadow-sm); outline: none; font-family: inherit; cursor: pointer; } + .tr-kline-tab { padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; background: #fff; color: var(--text-secondary); border: 1px solid rgba(0,0,0,0.05); cursor: pointer; transition: all 0.2s; box-shadow: var(--shadow-sm); } + .tr-kline-tab:hover { background: var(--color-brand); color: #fff; border-color: var(--color-brand); } + .tr-kline-tab.active { background: var(--color-brand); color: #fff; border-color: var(--color-brand); box-shadow: 0 4px 10px rgba(0,122,255,0.2); } .tr-sub-nav { display: flex; gap: 8px; margin-bottom: 20px; overflow-x: auto; padding-bottom: 4px; } .tr-sub-nav-item { padding: 8px 18px; border-radius: 9999px; font-size: 13px; background: #fff; color: var(--text-secondary); cursor: pointer; white-space: nowrap; transition: all 0.2s; text-decoration: none; box-shadow: var(--shadow-sm); font-weight: 500; } @@ -1097,14 +1100,12 @@

K线走势与买卖点

-
- - +
+ + + + +
diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js index e2f3156..1f3ebf0 100644 --- a/app/static/futures_analysis.js +++ b/app/static/futures_analysis.js @@ -1216,6 +1216,19 @@ function trCloseAnalysisModal() { // ==================== 交易详情弹窗 ==================== let trDetailChart = 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) { trCurrentPair = pair; @@ -1289,7 +1302,7 @@ async function trShowTradeDetail(pair) { } async function trLoadDetailKline(pair) { - const period = document.getElementById('tr-detail-period').value; + const period = trCurrentPeriod; const chartDom = document.getElementById('tr-detail-kline'); console.log('[K线加载] 开始加载K线数据', { @@ -1301,11 +1314,13 @@ async function trLoadDetailKline(pair) { }); try { - // 构建日期范围:开仓日前后各7天 + // 构建日期范围:开仓日前7天到平仓日后7天,确保交易前后都有K线数据 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); startDate.setDate(startDate.getDate() - 7); - const endDate = new Date(openDate); + const endDate = new Date(closeDate); endDate.setDate(endDate.getDate() + 7); const params = new URLSearchParams({ @@ -1367,36 +1382,127 @@ function trRenderDetailKline(data, pair) { const ma10 = calculateMA(candles, 10); const ma20 = calculateMA(candles, 20); - // 构建买卖标记 + // 构建买卖标记(每根K线柱只保留一对买/卖标记) const buyMarkers = []; const sellMarkers = []; const markers = data.trade_markers || []; + const period = data.period || 'daily'; + + // 按K线索引分组,每根K线只保留第一个买和第一个卖 + const buyByIndex = {}; // { candleIndex: marker } + const sellByIndex = {}; 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; + + // 按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 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' }, - }); - } + + buyMarkers.push({ + name: `买${m.offset || ''}`, + coord: [dateIdx, markerPrice], + value: `买${m.offset || ''} ${price}`, + itemStyle: { color: '#10b981' }, + }); + + 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({ + name: `卖${m.offset || ''}`, + coord: [dateIdx, markerPrice], + value: `卖${m.offset || ''} ${price}`, + 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 = { backgroundColor: 'transparent', @@ -1513,9 +1619,10 @@ function trRenderDetailKline(data, pair) { }, ], 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, + start: zoomStart, end: zoomEnd, borderColor: 'transparent', backgroundColor: 'rgba(15, 20, 30, 0.5)', fillerColor: 'rgba(6, 182, 212, 0.15)', diff --git a/data/futures_analysis.db b/data/futures_analysis.db index 9626960..d1edf2f 100644 Binary files a/data/futures_analysis.db and b/data/futures_analysis.db differ