fix: 修复ai分析失败问题

alphaFuthures
Lxy 4 weeks ago
parent 8ec4d212c5
commit 79502d00de

@ -427,9 +427,11 @@ def get_kline_with_trades(
candles = json.loads(market_data.candles_json) candles = json.loads(market_data.candles_json)
# 将字典格式K线转换为前端期望的数组格式 [date, open, close, low, high, volume] # 将字典格式K线转换为前端期望的数组格式 [date, open, close, low, high, volume]
if candles and isinstance(candles[0], dict): if candles and isinstance(candles[0], dict):
# 根据周期决定是否保留时间部分
is_daily = period == 'daily'
candles = [ 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("open", 0),
c.get("close", 0), c.get("close", 0),
c.get("low", 0), c.get("low", 0),
@ -517,8 +519,6 @@ async def analyze_trade(
db: Session = Depends(get_analysis_db), db: Session = Depends(get_analysis_db),
): ):
"""AI 分析单笔交易结合多周期K线数据""" """AI 分析单笔交易结合多周期K线数据"""
from app.services.ai_analysis import AIAnalysisService
# 收集多周期K线数据 # 收集多周期K线数据
from app.database import SessionLocal as MainSessionLocal from app.database import SessionLocal as MainSessionLocal
from app.models import MarketData from app.models import MarketData
@ -578,13 +578,14 @@ async def analyze_trade(
""" """
try: try:
# 使用 AIAnalysisService 的模型配置和调用能力 # 使用 AIFuturesAnalyzer 的模型配置和调用能力
service = AIAnalysisService.__new__(AIAnalysisService) from app.services.ai_analysis import AIFuturesAnalyzer
model = service.get_active_model() analyzer = AIFuturesAnalyzer(db)
model = analyzer.get_active_model()
if not model: if not model:
return {"success": False, "message": "未配置AI模型或模型未激活请先在AI配置页面设置"} return {"success": False, "message": "未配置AI模型或模型未激活请先在AI配置页面设置"}
response = service.call_ai_model(prompt, model) response = analyzer.call_ai_model(prompt, model)
if not response: if not response:
return {"success": False, "message": "AI模型返回空响应请稍后重试"} return {"success": False, "message": "AI模型返回空响应请稍后重试"}

@ -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 { 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-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-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 { 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; } .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线图 --> <!-- K线图 -->
<div class="tr-detail-section"> <div class="tr-detail-section">
<h4 class="tr-detail-section-title">K线走势与买卖点</h4> <h4 class="tr-detail-section-title">K线走势与买卖点</h4>
<div class="tr-kline-controls" style="margin-bottom:12px;"> <div class="tr-kline-tabs" style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
<select id="tr-detail-period" class="tr-select"> <button class="tr-kline-tab active" data-period="daily" onclick="trSwitchKlinePeriod('daily')">日线</button>
<option value="daily">日线</option> <button class="tr-kline-tab" data-period="60min" onclick="trSwitchKlinePeriod('60min')">60分钟</button>
<option value="60min">60分钟</option> <button class="tr-kline-tab" data-period="15min" onclick="trSwitchKlinePeriod('15min')">15分钟</button>
<option value="15min">15分钟</option> <button class="tr-kline-tab" data-period="5min" onclick="trSwitchKlinePeriod('5min')">5分钟</button>
<option value="5min">5分钟</option> <button id="tr-detail-reload-kline" class="tr-btn tr-btn-secondary" style="margin-left:auto;">刷新K线</button>
</select>
<button id="tr-detail-reload-kline" class="tr-btn tr-btn-secondary">刷新K线</button>
</div> </div>
<div id="tr-detail-kline" style="width:100%;height:400px;"></div> <div id="tr-detail-kline" style="width:100%;height:400px;"></div>
</div> </div>

@ -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: '#10b981' },
itemStyle: { color: '#34C759' }, });
});
} else { minMarkerIdx = Math.min(minMarkerIdx, dateIdx);
sellMarkers.push({ maxMarkerIdx = Math.max(maxMarkerIdx, dateIdx);
name: `${m.offset || ''}`, });
coord: [dateIdx, markerPrice],
value: `${m.offset || ''} ${price}`, Object.entries(sellByIndex).forEach(([idx, m]) => {
itemStyle: { color: '#FF3B30' }, 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 = { 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)',

Binary file not shown.
Loading…
Cancel
Save