fix: 修复前端显示问题

This commit is contained in:
Cassianvale
2025-03-06 20:30:54 +08:00
parent 35cf295b29
commit ff5b820a57
4 changed files with 608 additions and 117 deletions

View File

@@ -4,10 +4,12 @@ import os
import json
import asyncio
import httpx
import re
from typing import Dict, List, Optional, Any, Generator, AsyncGenerator
from dotenv import load_dotenv
from logger import get_logger
from utils.api_utils import APIUtils
from datetime import datetime
# 获取日志器
logger = get_logger()
@@ -55,6 +57,26 @@ class AIAnalyzer:
try:
logger.info(f"开始AI分析 {stock_code}, 流式模式: {stream}")
# 提取关键技术指标
latest_data = df.iloc[-1]
# 计算技术指标
rsi = latest_data.get('RSI')
price = latest_data.get('Close')
price_change = latest_data.get('Change')
# 确定MA趋势
ma_trend = 'UP' if latest_data.get('MA5', 0) > latest_data.get('MA20', 0) else 'DOWN'
# 确定MACD信号
macd = latest_data.get('MACD', 0)
macd_signal = latest_data.get('MACD_Signal', 0)
macd_signal_type = 'BUY' if macd > macd_signal else 'SELL'
# 确定成交量状态
volume_ratio = latest_data.get('Volume_Ratio', 1)
volume_status = 'HIGH' if volume_ratio > 1.5 else ('LOW' if volume_ratio < 0.5 else 'NORMAL')
# AI 分析内容
# 最近14天的股票数据记录
recent_data = df.tail(14).to_dict('records')
@@ -78,25 +100,19 @@ class AIAnalyzer:
近14日交易数据
{recent_data}
分析该基金的技术面状况,包括
1. 趋势分析:判断基金当前的趋势方向
2. 量分析基于RSI和交易量评估基金动量
3. 支撑与阻力位:确定关键价格位
4. 技术面总结
5. 投资建议
将分析结果格式化为JSON像这样
{{
"trend_analysis": "趋势分析结果...",
"momentum_analysis": "动量分析结果...",
"support_resistance": "支撑阻力位分析...",
"technical_summary": "技术面总结...",
"investment_advice": "投资建议..."
}}
提供
1. 净值走势分析(包含支撑位和压力位)
2. 成交量分析及其对净值的影响
3. 风险评估(包含波动率和折溢价分析)
4. 短期和中期净值预测
5. 关键价格位分析
6. 申购赎回建议(包含止损位)
请基于技术指标和市场表现进行分析,给出具体数据支持。
"""
else:
elif market_type == 'US':
prompt = f"""
分析股 {stock_code}
分析{stock_code}
技术指标概要:
{technical_summary}
@@ -104,25 +120,55 @@ class AIAnalyzer:
近14日交易数据
{recent_data}
分析该股票的技术面状况,包括
1. 趋势分析:当前趋势方向及强度
2. 量分析基于MACD、RSI等指标
3. 支撑与阻力位:关键价格位分析
4. 成交量分析:交易量的变化及意义
5. 波动性评估ATR和波动率分析
6. 技术面总结
7. 投资建议:根据技术分析给出操作建议
提供
1. 趋势分析(包含支撑位和压力位,美元计价)
2. 成交量分析及其含义
3. 风险评估(包含波动率和美股市场特有风险)
4. 短期和中期目标价位(美元)
5. 关键技术位分析
6. 具体交易建议(包含止损位)
请基于技术指标和美股市场特点进行分析,给出具体数据支持。
"""
elif market_type == 'HK':
prompt = f"""
分析港股 {stock_code}
将分析结果格式化为JSON像这样
{{
"trend_analysis": "趋势分析结果...",
"momentum_analysis": "动量分析结果...",
"support_resistance": "支撑阻力位分析...",
"volume_analysis": "成交量分析...",
"volatility_assessment": "波动性评估...",
"technical_summary": "技术面总结...",
"investment_advice": "投资建议..."
}}
技术指标概要
{technical_summary}
近14日交易数据
{recent_data}
请提供:
1. 趋势分析(包含支撑位和压力位,港币计价)
2. 成交量分析及其含义
3. 风险评估(包含波动率和港股市场特有风险)
4. 短期和中期目标价位(港币)
5. 关键技术位分析
6. 具体交易建议(包含止损位)
请基于技术指标和港股市场特点进行分析,给出具体数据支持。
"""
else: # A股
prompt = f"""
分析A股 {stock_code}
技术指标概要:
{technical_summary}
近14日交易数据
{recent_data}
请提供:
1. 趋势分析(包含支撑位和压力位)
2. 成交量分析及其含义
3. 风险评估(包含波动率分析)
4. 短期和中期目标价位
5. 关键技术位分析
6. 具体交易建议(包含止损位)
请基于技术指标和A股市场特点进行分析给出具体数据支持。
"""
# 格式化API URL
@@ -142,11 +188,27 @@ class AIAnalyzer:
"Authorization": f"Bearer {self.API_KEY}"
}
# 获取当前日期作为分析日期
analysis_date = datetime.now().strftime("%Y-%m-%d")
# 异步请求API
async with httpx.AsyncClient(timeout=self.API_TIMEOUT) as client:
# 记录请求
logger.debug(f"发送AI请求: URL={api_url}, MODEL={self.API_MODEL}, STREAM={stream}")
# 先发送技术指标数据
yield json.dumps({
"stock_code": stock_code,
"status": "analyzing",
"rsi": rsi,
"price": price,
"price_change": price_change,
"ma_trend": ma_trend,
"macd_signal": macd_signal_type,
"volume_status": volume_status,
"analysis_date": analysis_date
})
if stream:
# 流式响应处理
async with client.stream("POST", api_url, json=request_data, headers=headers) as response:
@@ -155,12 +217,17 @@ class AIAnalyzer:
error_data = json.loads(error_text)
error_message = error_data.get('error', {}).get('message', '未知错误')
logger.error(f"AI API请求失败: {response.status_code} - {error_message}")
yield json.dumps({"error": f"API请求失败: {error_message}"})
yield json.dumps({
"stock_code": stock_code,
"error": f"API请求失败: {error_message}",
"status": "error"
})
return
# 处理流式响应
buffer = ""
collected_messages = []
chunk_count = 0
async for chunk in response.aiter_text():
if chunk:
@@ -169,6 +236,7 @@ class AIAnalyzer:
chunk_str = chunk_str[6:] # 去除"data: "前缀
if chunk_str == "[DONE]":
logger.debug("收到流结束标记 [DONE]")
continue
try:
@@ -178,49 +246,48 @@ class AIAnalyzer:
content = delta.get("content", "")
if content:
chunk_count += 1
buffer += content
# 尝试提取完整的JSON
if buffer.strip().startswith("{") and buffer.strip().endswith("}"):
try:
result_json = json.loads(buffer)
yield json.dumps({
"stock_code": stock_code,
"analysis": result_json
})
buffer = "" # 重置缓冲区
except json.JSONDecodeError:
# JSON不完整继续收集
pass
collected_messages.append(content)
# 达到一定长度就输出
if len(buffer) > 100:
yield json.dumps({
"stock_code": stock_code,
"ai_analysis_chunk": buffer
})
collected_messages.append(buffer)
buffer = ""
# 直接发送每个内容片段,不累积
yield json.dumps({
"stock_code": stock_code,
"ai_analysis_chunk": content,
"status": "analyzing"
})
except json.JSONDecodeError:
# 忽略无法解析的块
logger.error(f"JSON解析错误块内容: {chunk_str[:100]}...")
continue
# 处理最后的缓冲区
if buffer:
logger.info(f"AI流式处理完成共收到 {chunk_count} 个内容片段,总长度: {len(buffer)}")
# 如果buffer不为空且不以换行符结束发送一个换行符
if buffer and not buffer.endswith('\n'):
logger.debug("发送换行符")
yield json.dumps({
"stock_code": stock_code,
"ai_analysis_chunk": buffer
"ai_analysis_chunk": "\n",
"status": "analyzing"
})
collected_messages.append(buffer)
# 尝试从整个内容中提取JSON
full_content = "".join(collected_messages)
# 完整的分析内容
full_content = buffer
# 如果没有成功解析JSON返回原始内容
if not full_content.strip().startswith("{"):
yield json.dumps({
"stock_code": stock_code,
"raw_analysis": full_content
})
# 尝试从分析内容中提取投资建议
recommendation = self._extract_recommendation(full_content)
# 计算分析评分
score = self._calculate_analysis_score(full_content, technical_summary)
# 发送完成状态和评分、建议
yield json.dumps({
"stock_code": stock_code,
"status": "completed",
"score": score,
"recommendation": recommendation
})
else:
# 非流式响应处理
response = await client.post(api_url, json=request_data, headers=headers)
@@ -229,32 +296,100 @@ class AIAnalyzer:
error_data = response.json()
error_message = error_data.get('error', {}).get('message', '未知错误')
logger.error(f"AI API请求失败: {response.status_code} - {error_message}")
yield json.dumps({"error": f"API请求失败: {error_message}"})
yield json.dumps({
"stock_code": stock_code,
"error": f"API请求失败: {error_message}",
"status": "error"
})
return
response_data = response.json()
analysis_text = response_data.get("choices", [{}])[0].get("message", {}).get("content", "")
try:
# 尝试解析JSON
analysis_json = json.loads(analysis_text)
yield json.dumps({
"stock_code": stock_code,
"analysis": analysis_json
})
except json.JSONDecodeError:
# 返回原始文本
yield json.dumps({
"stock_code": stock_code,
"raw_analysis": analysis_text
})
logger.info(f"完成对 {stock_code} 的AI分析")
# 尝试从分析内容中提取投资建议
recommendation = self._extract_recommendation(analysis_text)
# 计算分析评分
score = self._calculate_analysis_score(analysis_text, technical_summary)
# 发送完整的分析结果
yield json.dumps({
"stock_code": stock_code,
"status": "completed",
"analysis": analysis_text,
"score": score,
"recommendation": recommendation,
"rsi": rsi,
"price": price,
"price_change": price_change,
"ma_trend": ma_trend,
"macd_signal": macd_signal_type,
"volume_status": volume_status,
"analysis_date": analysis_date
})
except Exception as e:
logger.error(f"AI分析 {stock_code}出错: {str(e)}")
logger.exception(e)
yield json.dumps({"error": f"分析出错: {str(e)}"})
logger.error(f"AI分析出错: {str(e)}", exc_info=True)
yield json.dumps({
"stock_code": stock_code,
"error": f"分析出错: {str(e)}",
"status": "error"
})
def _extract_recommendation(self, analysis_text: str) -> str:
"""从分析文本中提取投资建议"""
# 查找投资建议部分
investment_advice_pattern = r"##\s*投资建议\s*\n(.*?)(?:\n##|\Z)"
match = re.search(investment_advice_pattern, analysis_text, re.DOTALL)
if match:
advice_text = match.group(1).strip()
# 提取关键建议
if "买入" in advice_text or "增持" in advice_text:
return "买入"
elif "卖出" in advice_text or "减持" in advice_text:
return "卖出"
elif "持有" in advice_text:
return "持有"
else:
return "观望"
return "观望" # 默认建议
def _calculate_analysis_score(self, analysis_text: str, technical_summary: dict) -> int:
"""计算分析评分"""
score = 50 # 基础分数
# 根据技术指标调整分数
if technical_summary['trend'] == 'upward':
score += 10
else:
score -= 10
if technical_summary['volume_trend'] == 'increasing':
score += 5
else:
score -= 5
rsi = technical_summary['rsi_level']
if rsi < 30: # 超卖
score += 15
elif rsi > 70: # 超买
score -= 15
# 根据分析文本中的关键词调整分数
if "强烈买入" in analysis_text or "显著上涨" in analysis_text:
score += 20
elif "买入" in analysis_text or "看涨" in analysis_text:
score += 10
elif "强烈卖出" in analysis_text or "显著下跌" in analysis_text:
score -= 20
elif "卖出" in analysis_text or "看跌" in analysis_text:
score -= 10
# 确保分数在0-100范围内
return max(0, min(100, score))
def _truncate_json_for_logging(self, json_obj, max_length=500):
"""

View File

@@ -159,10 +159,10 @@ class StockAnalyzerService:
try:
logger.info(f"开始批量扫描 {len(stock_codes)} 只股票, 市场: {market_type}")
# 输出初始状态
# 输出初始状态 - 发送批量分析初始化消息
yield json.dumps({
"status": "scanning",
"total_stocks": len(stock_codes),
"stream_type": "batch",
"stock_codes": stock_codes,
"market_type": market_type,
"min_score": min_score
})
@@ -177,6 +177,12 @@ class StockAnalyzerService:
stock_with_indicators[code] = self.indicator.calculate_indicators(df)
except Exception as e:
logger.error(f"计算 {code} 技术指标时出错: {str(e)}")
# 发送错误状态
yield json.dumps({
"stock_code": code,
"error": f"计算技术指标时出错: {str(e)}",
"status": "error"
})
# 评分股票
results = self.scorer.batch_score_stocks(stock_with_indicators)
@@ -184,30 +190,39 @@ class StockAnalyzerService:
# 过滤低于最低评分的股票
filtered_results = [r for r in results if r[1] >= min_score]
# 输出评分结果
yield json.dumps({
"scan_results": [
{
# 为每只股票发送基本评分和推荐信息
for code, score, rec in results:
df = stock_with_indicators.get(code)
if df is not None and len(df) > 0:
# 获取最新数据
latest_data = df.iloc[-1]
# 发送股票基本信息和评分
yield json.dumps({
"stock_code": code,
"score": score,
"recommendation": rec
} for code, score, rec in filtered_results
],
"total_matched": len(filtered_results),
"total_scanned": len(results)
})
"recommendation": rec,
"price": float(latest_data.get('Close', 0)),
"price_change": float(latest_data.get('Change', 0)),
"rsi": float(latest_data.get('RSI', 0)) if 'RSI' in latest_data else None,
"ma_trend": "UP" if latest_data.get('MA5', 0) > latest_data.get('MA20', 0) else "DOWN",
"macd_signal": "BUY" if latest_data.get('MACD', 0) > latest_data.get('MACD_Signal', 0) else "SELL",
"volume_status": "HIGH" if latest_data.get('Volume_Ratio', 1) > 1.5 else ("LOW" if latest_data.get('Volume_Ratio', 1) < 0.5 else "NORMAL"),
"status": "completed" if score < min_score else "waiting"
})
# 如果需要进一步分析对评分较高的股票进行AI分析
if stream and filtered_results:
top_stocks = filtered_results[:3] # 只分析前3只评分最高的股票
# 只分析前5只评分最高的股票,避免分析过多导致前端卡顿
top_stocks = filtered_results[:5]
for stock_code, score, _ in top_stocks:
df = stock_with_indicators.get(stock_code)
if df is not None:
# 输出正在分析的股票信息
yield json.dumps({
"analyzing": stock_code,
"score": score
"stock_code": stock_code,
"status": "analyzing"
})
# AI分析
@@ -216,7 +231,7 @@ class StockAnalyzerService:
# 输出扫描完成信息
yield json.dumps({
"status": "completed",
"scan_completed": True,
"total_scanned": len(results),
"total_matched": len(filtered_results)
})