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)
# 将字典格式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模型返回空响应请稍后重试"}

@ -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线图 -->
<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 class="tr-kline-tabs" style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
<button class="tr-kline-tab active" data-period="daily" onclick="trSwitchKlinePeriod('daily')">日线</button>
<button class="tr-kline-tab" data-period="60min" onclick="trSwitchKlinePeriod('60min')">60分钟</button>
<button class="tr-kline-tab" data-period="15min" onclick="trSwitchKlinePeriod('15min')">15分钟</button>
<button class="tr-kline-tab" data-period="5min" onclick="trSwitchKlinePeriod('5min')">5分钟</button>
<button id="tr-detail-reload-kline" class="tr-btn tr-btn-secondary" style="margin-left:auto;">刷新K线</button>
</div>
<div id="tr-detail-kline" style="width:100%;height:400px;"></div>
</div>

@ -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,37 +1382,128 @@ 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',
animation: false,
@ -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)',

Binary file not shown.
Loading…
Cancel
Save