You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/app/services/reflection_ai_analysis.py

336 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
交易反思 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}}