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