From e2c8d5976de9336d1e0c9471f968d20e75409772 Mon Sep 17 00:00:00 2001 From: Lxy Date: Mon, 6 Jul 2026 00:12:31 +0800 Subject: [PATCH] feat: add AI reanalysis backend for trade reflections - Added TradeAIAnalysis model for multi-version AI analysis results - Created reflection_ai_analysis.py service with reflection-enhanced prompt - Implemented analyze_with_reflection(): builds prompt, calls AI, parses JSON, saves version - Added get_analysis_history(), get_latest_analysis(), save_suggestion_as_experience() - Added 4 new API endpoints: POST /reanalyze, GET /reanalyze/{id}/history, GET /reanalyze/{id}/latest, POST /ai-analysis/{id}/save-experience - Updated analysis_db.py to import TradeAIAnalysis - Verified app imports successfully --- app/analysis_db.py | 2 + app/analysis_models.py | 28 +++ app/api/trade_review.py | 68 +++++ app/services/reflection_ai_analysis.py | 335 +++++++++++++++++++++++++ data/futures_analysis.db | Bin 12312576 -> 12406784 bytes 5 files changed, 433 insertions(+) create mode 100644 app/services/reflection_ai_analysis.py diff --git a/app/analysis_db.py b/app/analysis_db.py index a4ad733..eb8579a 100644 --- a/app/analysis_db.py +++ b/app/analysis_db.py @@ -76,6 +76,7 @@ def init_analysis_db(): SymbolScoreV2, TradingPlanV2, SectorHeat, ReviewPlanV2, TradeRecord, TradeImportBatch, TradePair, TradeReflection, DailyReflection, TradeTag, TradeRecordTag, TradeExperience, + TradeAIAnalysis, ) AnalysisBase.metadata.create_all(bind=analysis_engine) @@ -95,6 +96,7 @@ def init_analysis_mysql(mysql_engine): SymbolScoreV2, TradingPlanV2, SectorHeat, ReviewPlanV2, TradeRecord, TradeImportBatch, TradePair, TradeReflection, DailyReflection, TradeTag, TradeRecordTag, TradeExperience, + TradeAIAnalysis, ) AnalysisBase.metadata.create_all(bind=mysql_engine) logger.info("MySQL analysis 表结构初始化完成") diff --git a/app/analysis_models.py b/app/analysis_models.py index 78fe9ce..b1f931e 100644 --- a/app/analysis_models.py +++ b/app/analysis_models.py @@ -447,3 +447,31 @@ class TradeExperience(AnalysisBase): def __repr__(self): return f"" + + +class TradeAIAnalysis(AnalysisBase): + """交易 AI 分析结果表(多版本)""" + __tablename__ = "trade_ai_analysis" + + id = Column(Integer, primary_key=True, autoincrement=True) + trade_pair_id = Column(Integer, nullable=False, index=True, comment="交易配对ID") + version = Column(Integer, nullable=False, comment="分析版本号") + # AI 输出结构化字段 + overall_evaluation = Column(Text, nullable=True, comment="综合评价") + strengths = Column(JSON, nullable=True, comment="优点列表") + weaknesses = Column(JSON, nullable=True, comment="不足列表") + experience_suggestion = Column(Text, nullable=True, comment="经验提炼建议") + suggestion_type = Column(String(16), nullable=True, comment="建议类型: lesson/tip/warning") + raw_response = Column(Text, nullable=True, comment="AI 原始响应") + prompt_snapshot = Column(Text, nullable=True, comment="提示词快照") + # 是否已保存到经验库 + saved_to_experience = Column(Boolean, nullable=False, default=False, comment="是否已保存到经验库") + experience_id = Column(Integer, nullable=True, comment="关联经验ID") + created_at = Column(DateTime, nullable=False, default=datetime.now) + + __table_args__ = ( + Index('ix_trade_ai_analysis_pair_version', 'trade_pair_id', 'version', unique=True), + ) + + def __repr__(self): + return f"" diff --git a/app/api/trade_review.py b/app/api/trade_review.py index 8d46812..d9cfa4e 100644 --- a/app/api/trade_review.py +++ b/app/api/trade_review.py @@ -24,6 +24,12 @@ from app.services.trade_pairing_engine import ( create_manual_pair, delete_pair, ) +from app.services.reflection_ai_analysis import ( + analyze_with_reflection, + get_analysis_history, + get_latest_analysis, + save_suggestion_as_experience, +) logger = logging.getLogger(__name__) router = APIRouter(prefix="/trade-review", tags=["交易复盘"]) @@ -1036,3 +1042,65 @@ def api_delete_experience( db.delete(exp) db.commit() return {"success": True, "message": "经验已删除"} + + +# ==================== AI 重新分析 ==================== + +class ReanalyzeRequest(BaseModel): + trade_pair_id: int + + +@router.post("/reanalyze") +async def api_reanalyze_trade( + req: ReanalyzeRequest, + db: Session = Depends(get_analysis_db), +): + """基于反思内容重新 AI 分析交易""" + result = analyze_with_reflection(db, req.trade_pair_id) + if not result.get("success"): + raise HTTPException(status_code=400, detail=result.get("message", "分析失败")) + return result + + +@router.get("/reanalyze/{trade_pair_id}/history") +def api_get_analysis_history( + trade_pair_id: int, + db: Session = Depends(get_analysis_db), +): + """获取交易的 AI 分析历史版本""" + history = get_analysis_history(db, trade_pair_id) + return {"success": True, "data": history} + + +@router.get("/reanalyze/{trade_pair_id}/latest") +def api_get_latest_analysis( + trade_pair_id: int, + db: Session = Depends(get_analysis_db), +): + """获取交易最新 AI 分析结果""" + analysis = get_latest_analysis(db, trade_pair_id) + return {"success": True, "data": analysis} + + +class SaveSuggestionRequest(BaseModel): + title: Optional[str] = None + content: Optional[str] = None + tags: Optional[list[str]] = None + + +@router.post("/ai-analysis/{analysis_id}/save-experience") +def api_save_suggestion_as_experience( + analysis_id: int, + req: SaveSuggestionRequest, + db: Session = Depends(get_analysis_db), +): + """将 AI 分析建议保存到经验库""" + result = save_suggestion_as_experience( + db, analysis_id, + title=req.title, + content=req.content, + tags=req.tags, + ) + if not result.get("success"): + raise HTTPException(status_code=400, detail=result.get("message", "保存失败")) + return result diff --git a/app/services/reflection_ai_analysis.py b/app/services/reflection_ai_analysis.py new file mode 100644 index 0000000..c1363d0 --- /dev/null +++ b/app/services/reflection_ai_analysis.py @@ -0,0 +1,335 @@ +""" +交易反思 AI 分析服务 - 基于反思内容重新分析交易并提炼经验 +""" +import json +import logging +import re +from typing import Dict, List, Optional +from sqlalchemy.orm import Session + +from app.analysis_models import TradePair, TradeReflection, TradeAIAnalysis, TradeExperience +from app.services.ai_analysis import AIFuturesAnalyzer + +logger = logging.getLogger(__name__) + + +REFLECTION_ANALYSIS_PROMPT = """你是一位资深的交易心理与策略复盘教练。请基于以下交易数据和交易者的反思内容,进行深度分析。 + +=== 交易基础信息 === +品种:{variety} +方向:{direction_text} +开仓日期时间:{open_date} {open_time} +平仓日期时间:{close_date} {close_time} +开仓均价:{open_price} +平仓均价:{close_price} +交易手数:{volume} +平仓盈亏:{close_pnl} +手续费:{commission} +净盈亏:{net_pnl} + +=== 交易者反思内容 === +入场理由:{entry_reason} +入场时机评价:{entry_timing} +仓位管理反思:{position_management} +出场理由:{exit_reason} +出场时机评价:{exit_timing} +纪律评分(1-5):{discipline_score} +自由反思:{free_reflection} + +=== 当日整体反思 === +情绪状态:{emotion_state} +市场判断:{market_judgment} +当日总结:{daily_summary} +改进方向:{daily_improvements} + +=== 任务要求 === +请严格按以下 JSON 格式输出分析结果(不要输出任何其他内容): + +{{ + "overall_evaluation": "综合评价:这笔交易的执行质量、决策逻辑、与市场的匹配度等", + "strengths": ["优点1", "优点2", "优点3"], + "weaknesses": ["不足1", "不足2", "不足3"], + "experience_suggestion": "提炼出的核心经验教训或改进建议,语言简洁 actionable", + "suggestion_type": "lesson", + "actionable_plan": ["下次可以做的具体改进1", "下次可以做的具体改进2"] +}} + +注意: +1. suggestion_type 只能是 lesson(经验)、tip(技巧)、warning(警告)三者之一 +2. 必须结合反思内容进行分析,不要脱离交易者自己的总结 +3. 如果交易亏损,重点分析亏损原因和避免方案 +4. 如果交易盈利,重点分析盈利是否来自计划还是运气 +""" + + +def _timing_text(timing: Optional[str]) -> str: + """时机评价转中文""" + mapping = {"good": "良好", "fair": "一般", "poor": "较差"} + return mapping.get(timing, timing or "未评价") + + +def _direction_text(direction: Optional[str]) -> str: + """方向转中文""" + mapping = {"long": "多头", "short": "空头"} + return mapping.get(direction, direction or "未知") + + +def _get_records_text(db: Session, pair: TradePair) -> str: + """获取配对关联的原始记录信息""" + from app.analysis_models import TradeRecord + record_ids = (pair.open_record_ids or []) + (pair.close_record_ids or []) + if not record_ids: + return "" + records = db.query(TradeRecord).filter(TradeRecord.id.in_(record_ids)).all() + lines = [] + for r in sorted(records, key=lambda x: f"{x.trade_date} {x.trade_time or ''}"): + lines.append(f"{r.trade_date} {r.trade_time or ''} {r.symbol} {r.direction}{r.offset} {r.price}×{r.volume}") + return "\n".join(lines) + + +def build_reflection_prompt(db: Session, pair: TradePair, reflection: TradeReflection, + daily_reflection: Optional[Dict] = None) -> str: + """构建反思增强分析提示词""" + daily = daily_reflection or {} + + # 获取第一条开仓和第一条平仓记录的时间 + from app.analysis_models import TradeRecord + open_time = "" + close_time = "" + open_record_ids = pair.open_record_ids or [] + close_record_ids = pair.close_record_ids or [] + if open_record_ids: + open_rec = db.query(TradeRecord).filter(TradeRecord.id == open_record_ids[0]).first() + if open_rec: + open_time = open_rec.trade_time or "" + if close_record_ids: + close_rec = db.query(TradeRecord).filter(TradeRecord.id == close_record_ids[0]).first() + if close_rec: + close_time = close_rec.trade_time or "" + + return REFLECTION_ANALYSIS_PROMPT.format( + variety=pair.variety, + direction_text=_direction_text(pair.direction), + open_date=pair.open_date or "", + open_time=open_time, + close_date=pair.close_date or "", + close_time=close_time, + open_price=pair.open_price, + close_price=pair.close_price, + volume=pair.total_volume, + close_pnl=pair.close_pnl, + commission=pair.total_commission, + net_pnl=pair.net_pnl, + entry_reason=reflection.entry_reason or "未填写", + entry_timing=_timing_text(reflection.entry_timing), + position_management=reflection.position_management or "未填写", + exit_reason=reflection.exit_reason or "未填写", + exit_timing=_timing_text(reflection.exit_timing), + discipline_score=reflection.discipline_score or "未评分", + free_reflection=reflection.free_reflection or "未填写", + emotion_state=daily.get("emotion_state", "未填写"), + market_judgment=daily.get("market_judgment", "未填写"), + daily_summary=daily.get("summary", "未填写"), + daily_improvements=daily.get("improvements", "未填写"), + ) + + +def _parse_ai_response(response: str) -> Dict: + """解析 AI 返回的 JSON""" + try: + # 尝试直接解析 + data = json.loads(response) + return data + except json.JSONDecodeError: + pass + + # 尝试从 Markdown 代码块中提取 + code_block_pattern = re.compile(r'```(?:json)?\s*([\s\S]*?)```', re.MULTILINE) + matches = code_block_pattern.findall(response) + for match in matches: + try: + return json.loads(match.strip()) + except json.JSONDecodeError: + continue + + # 尝试从文本中提取第一个 JSON 对象 + json_pattern = re.compile(r'\{[\s\S]*\}') + matches = json_pattern.findall(response) + for match in matches: + try: + return json.loads(match) + except json.JSONDecodeError: + continue + + # 解析失败,返回原始内容作为 overall_evaluation + return { + "overall_evaluation": response, + "strengths": [], + "weaknesses": [], + "experience_suggestion": "", + "suggestion_type": "lesson", + "actionable_plan": [], + } + + +def _get_next_version(db: Session, trade_pair_id: int) -> int: + """获取下一个版本号""" + latest = db.query(TradeAIAnalysis).filter( + TradeAIAnalysis.trade_pair_id == trade_pair_id + ).order_by(TradeAIAnalysis.version.desc()).first() + return (latest.version + 1) if latest else 1 + + +def analyze_with_reflection(db: Session, trade_pair_id: int) -> Dict: + """ + 基于反思内容重新分析交易 + 返回:{success, data/version, message} + """ + pair = db.query(TradePair).filter(TradePair.id == trade_pair_id).first() + if not pair: + return {"success": False, "message": "交易配对不存在"} + + reflection = db.query(TradeReflection).filter( + TradeReflection.trade_pair_id == trade_pair_id + ).first() + if not reflection: + return {"success": False, "message": "该交易尚未填写反思,无法进行分析"} + + # 获取当日反思 + daily_reflection = None + from app.analysis_models import DailyReflection + dr = db.query(DailyReflection).filter( + DailyReflection.reflection_date == pair.close_date + ).first() + if dr: + daily_reflection = { + "emotion_state": dr.emotion_state, + "market_judgment": dr.market_judgment, + "summary": dr.summary, + "improvements": dr.improvements, + } + + # 构建提示词 + prompt = build_reflection_prompt(db, pair, reflection, daily_reflection) + + # 调用 AI + analyzer = AIFuturesAnalyzer(db) + model = analyzer.get_active_model() + if not model: + return {"success": False, "message": "未配置AI模型或模型未激活"} + + response = analyzer.call_ai_model(prompt, model) + if not response: + return {"success": False, "message": "AI模型返回空响应,请稍后重试"} + + # 解析结果 + parsed = _parse_ai_response(response) + + # 计算版本号 + version = _get_next_version(db, trade_pair_id) + + # 保存分析结果 + analysis = TradeAIAnalysis( + trade_pair_id=trade_pair_id, + version=version, + overall_evaluation=parsed.get("overall_evaluation", ""), + strengths=parsed.get("strengths", []), + weaknesses=parsed.get("weaknesses", []), + experience_suggestion=parsed.get("experience_suggestion", ""), + suggestion_type=parsed.get("suggestion_type", "lesson"), + raw_response=response, + prompt_snapshot=prompt, + ) + db.add(analysis) + + # 更新反思的 AI 分析状态 + reflection.ai_analyzed = True + reflection.ai_version = version + + db.commit() + + return { + "success": True, + "data": { + "version": version, + "overall_evaluation": analysis.overall_evaluation, + "strengths": analysis.strengths, + "weaknesses": analysis.weaknesses, + "experience_suggestion": analysis.experience_suggestion, + "suggestion_type": analysis.suggestion_type, + "actionable_plan": parsed.get("actionable_plan", []), + }, + } + + +def get_analysis_history(db: Session, trade_pair_id: int) -> List[Dict]: + """获取交易的所有 AI 分析版本""" + analyses = db.query(TradeAIAnalysis).filter( + TradeAIAnalysis.trade_pair_id == trade_pair_id + ).order_by(TradeAIAnalysis.version.desc()).all() + + return [{ + "id": a.id, + "version": a.version, + "overall_evaluation": a.overall_evaluation, + "strengths": a.strengths, + "weaknesses": a.weaknesses, + "experience_suggestion": a.experience_suggestion, + "suggestion_type": a.suggestion_type, + "saved_to_experience": a.saved_to_experience, + "experience_id": a.experience_id, + "created_at": a.created_at.strftime('%Y-%m-%d %H:%M:%S') if a.created_at else None, + } for a in analyses] + + +def get_latest_analysis(db: Session, trade_pair_id: int) -> Optional[Dict]: + """获取最新 AI 分析结果""" + analysis = db.query(TradeAIAnalysis).filter( + TradeAIAnalysis.trade_pair_id == trade_pair_id + ).order_by(TradeAIAnalysis.version.desc()).first() + + if not analysis: + return None + + return { + "id": analysis.id, + "version": analysis.version, + "overall_evaluation": analysis.overall_evaluation, + "strengths": analysis.strengths, + "weaknesses": analysis.weaknesses, + "experience_suggestion": analysis.experience_suggestion, + "suggestion_type": analysis.suggestion_type, + "saved_to_experience": analysis.saved_to_experience, + "experience_id": analysis.experience_id, + "created_at": analysis.created_at.strftime('%Y-%m-%d %H:%M:%S') if analysis.created_at else None, + } + + +def save_suggestion_as_experience(db: Session, analysis_id: int, title: Optional[str] = None, + content: Optional[str] = None, tags: Optional[List[str]] = None) -> Dict: + """将 AI 分析建议保存为经验""" + analysis = db.query(TradeAIAnalysis).filter(TradeAIAnalysis.id == analysis_id).first() + if not analysis: + return {"success": False, "message": "分析记录不存在"} + + pair = db.query(TradePair).filter(TradePair.id == analysis.trade_pair_id).first() + + exp_title = title or analysis.experience_suggestion[:50] if analysis.experience_suggestion else "AI提炼经验" + exp_content = content or analysis.experience_suggestion or "" + + exp = TradeExperience( + title=exp_title, + content=exp_content, + exp_type=analysis.suggestion_type or "lesson", + source_pair_id=analysis.trade_pair_id, + source_date=pair.close_date if pair else None, + tags=tags or [], + ) + db.add(exp) + db.flush() + + analysis.saved_to_experience = True + analysis.experience_id = exp.id + db.commit() + + return {"success": True, "data": {"experience_id": exp.id}} diff --git a/data/futures_analysis.db b/data/futures_analysis.db index 1d659408eea0af8118599259ff939346f6d27703..5671cf0793bd375d422b25e343871044ace1aaf6 100644 GIT binary patch delta 5065 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