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
refactor3.0
Lxy 2 weeks ago
parent 647937e0b4
commit e2c8d5976d

@ -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 表结构初始化完成")

@ -447,3 +447,31 @@ class TradeExperience(AnalysisBase):
def __repr__(self):
return f"<TradeExperience {self.title} [{self.exp_type}]>"
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"<TradeAIAnalysis pair_id={self.trade_pair_id} version={self.version}>"

@ -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

@ -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}}

Binary file not shown.
Loading…
Cancel
Save