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